Refund CR Complete with Demop (faiz_dev) #335

Open
FaizHashmiCS22 wants to merge 16 commits from faiz_dev into master

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

After

Width:  |  Height:  |  Size: 1.3 KiB

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

@ -1917,8 +1917,11 @@
"selectRefundMethod": "Select Refund Method", "selectRefundMethod": "Select Refund Method",
"hmgWallet": "HMG Wallet", "hmgWallet": "HMG Wallet",
"bankTransfer": "Bank Transfer", "bankTransfer": "Bank Transfer",
"cash": "Cash",
"selectRefundReason": "Select Refund Reason", "selectRefundReason": "Select Refund Reason",
"refundReason": "Refund Reason", "refundReason": "Refund Reason",
"requestRejectionReason": "Request Rejection Reason",
"procedureRejectionReason": "Procedure Rejection Reason",
"done": "Done", "done": "Done",
"refundReason1": "Company patient treated as self-paying and re-invoiced", "refundReason1": "Company patient treated as self-paying and re-invoiced",
"refundReason2": "Patient does not want a clinical procedure", "refundReason2": "Patient does not want a clinical procedure",
@ -1947,6 +1950,17 @@
"advanceNumber": "Advance Number", "advanceNumber": "Advance Number",
"amountOnly": "Amount", "amountOnly": "Amount",
"invoiceHistory": "Invoice History", "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": "Refunds over 5000 SAR cannot be processed as cash. Please select another refund destination.",
"cashRefundInstructions": "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 once the cashier has created the refund transaction. Provide the OTP to the cashier for validation. Cash will be released only after successful validation.",
"cashRefundApprovalInstructions": "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 once the cashier has created the refund transaction. 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", "parkingQR": "Parking QR Code",
"parkingQRDesc": "Scan the below QR Code at the exit parking gate:", "parkingQRDesc": "Scan the below QR Code at the exit parking gate:",
"livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.", "livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.",
@ -1959,3 +1973,4 @@
} }

@ -211,6 +211,11 @@ class ApiClientImp implements ApiClient {
} }
// body['TokenID'] = "@dm!n"; // body['TokenID'] = "@dm!n";
// body['PatientID'] = 6424682;
// body['PatientID'] = 4772785;
// body['PatientID'] = 4776359;
// body['PatientID'] = 4776466;
// body['PatientID'] = 4769748; // body['PatientID'] = 4769748;
// body['PatientID'] = 89754444; // body['PatientID'] = 89754444;
// body['PatientTypeID'] = 1; // body['PatientTypeID'] = 1;
@ -249,7 +254,7 @@ class ApiClientImp implements ApiClient {
if (kDebugMode) { if (kDebugMode) {
debugPrint("uri: ${Uri.parse(url.trim())}"); debugPrint("uri: ${Uri.parse(url.trim())}");
var requestBodyJSON = json.encode(body); var requestBodyJSON = json.encode(body);
debugPrint("body: $requestBodyJSON", wrapWidth: 2048); debugPrint("body: $requestBodyJSON", wrapWidth: 1024);
// log("body: $requestBodyJSON"); // log("body: $requestBodyJSON");
} }
@ -427,7 +432,7 @@ class ApiClientImp implements ApiClient {
onSuccess(parsed, statusCode, onSuccess(parsed, statusCode,
messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']);
} else { } else {
if (parsed['message'] == null && parsed['ErrorEndUserMessage'] == null) { if (parsed['message'] == null && parsed['ErrorEndUserMessage'] == null && parsed['ErrorMessage'] == null) {
if (parsed['ErrorSearchMsg'] == null) { if (parsed['ErrorSearchMsg'] == null) {
onFailure( onFailure(
LocaleKeys.networkServerErrorNoMessage.tr(), LocaleKeys.networkServerErrorNoMessage.tr(),

@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts { class ApiConsts {
static const maxSmallScreen = 660; static const maxSmallScreen = 660;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat;
// static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT
@ -288,6 +288,8 @@ class ApiConsts {
static String getRefundInvoices = "Services/COCWS.svc/REST/GetRefundInvoices"; static String getRefundInvoices = "Services/COCWS.svc/REST/GetRefundInvoices";
static String submitRefund = "Services/COCWS.svc/REST/SubmitRefund"; static String submitRefund = "Services/COCWS.svc/REST/SubmitRefund";
static String isRefundableInvoice = "Services/Outps.svc/REST/isRefundableInvoice"; 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 getAiAnalysisURL = "https://api.labpredict.com";
static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology"; static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology";
@ -301,9 +303,8 @@ class ApiConsts {
static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations";
// ************ static values for Api **************** // ************ static values for Api ****************
static final double appVersionID = 21.4; static final double appVersionID = 21.5;
// static final double appVersionID = 50.7;
static final int appChannelId = 3; static final int appChannelId = 3;
static final String appIpAddress = "10.20.10.20"; static final String appIpAddress = "10.20.10.20";
static final String appGeneralId = "Cs2020@2016\$2958"; static final String appGeneralId = "Cs2020@2016\$2958";
@ -312,6 +313,7 @@ class ApiConsts {
// PACKAGES and OFFERS // PACKAGES and OFFERS
var EXA_CART_API_BASE_URL = 'https://mdlaboratories.com/offersdiscounts'; var EXA_CART_API_BASE_URL = 'https://mdlaboratories.com/offersdiscounts';
// var EXA_CART_API_BASE_URL = 'http://10.200.101.75:9000'; // var EXA_CART_API_BASE_URL = 'http://10.200.101.75:9000';
var PACKAGES_CATEGORIES = '/api/categories'; var PACKAGES_CATEGORIES = '/api/categories';
var PACKAGES_STORES = '/api/stores'; var PACKAGES_STORES = '/api/stores';
@ -322,9 +324,11 @@ var PACKAGES_SHOPPING_CART = '/api/shopping_cart_items';
var PACKAGES_ORDERS = '/api/orders'; var PACKAGES_ORDERS = '/api/orders';
var PACKAGES_ORDER_HISTORY = '/api/orders/items'; var PACKAGES_ORDER_HISTORY = '/api/orders/items';
var PACKAGES_TAMARA_OPT = '/api/orders/paymentoptions/tamara'; var PACKAGES_TAMARA_OPT = '/api/orders/paymentoptions/tamara';
// var BASE_URL = 'http://10.50.100.198:2018/'; // var BASE_URL = 'http://10.50.100.198:2018/';
// var BASE_URL = 'https://uat.hmgwebservices.com/'; // var BASE_URL = 'https://uat.hmgwebservices.com/';
var BASE_URL = 'https://hmgwebservices.com/'; var BASE_URL = 'https://hmgwebservices.com/';
// var BASE_URL = 'http://10.201.204.103/'; // var BASE_URL = 'http://10.201.204.103/';
// var BASE_URL = 'https://orash.cloudsolutions.com.sa/'; // var BASE_URL = 'https://orash.cloudsolutions.com.sa/';
// var BASE_URL = 'https://vidauat.cloudsolutions.com.sa/'; // var BASE_URL = 'https://vidauat.cloudsolutions.com.sa/';
@ -420,6 +424,7 @@ var SEND_RAD_REPORT_EMAIL = 'Services/Notifications.svc/REST/SendRadReportEmail'
///Feedback ///Feedback
var SEND_FEEDBACK = 'Services/COCWS.svc/REST/InsertCOCItemInSPList'; var SEND_FEEDBACK = 'Services/COCWS.svc/REST/InsertCOCItemInSPList';
var GET_STATUS_FOR_COCO = 'Services/COCWS.svc/REST/GetStatusforCOC'; var GET_STATUS_FOR_COCO = 'Services/COCWS.svc/REST/GetStatusforCOC';
// var GET_PATIENT_AppointmentHistory = 'Services' // var GET_PATIENT_AppointmentHistory = 'Services'
// '/Doctors.svc/REST/PateintHasAppoimentHistory'; // '/Doctors.svc/REST/PateintHasAppoimentHistory';
@ -664,6 +669,7 @@ var GENERAL_ID = 'Cs2020@2016\$2958';
var IP_ADDRESS = '10.20.10.20'; var IP_ADDRESS = '10.20.10.20';
var SETUP_ID = '91877'; var SETUP_ID = '91877';
var LANGUAGE = 2; var LANGUAGE = 2;
// var PATIENT_OUT_SA = 0; // var PATIENT_OUT_SA = 0;
var SESSION_ID = 'TMRhVmkGhOsvamErw'; var SESSION_ID = 'TMRhVmkGhOsvamErw';
var IS_DENTAL_ALLOWED_BACKEND = false; var IS_DENTAL_ALLOWED_BACKEND = false;
@ -672,6 +678,7 @@ var PATIENT_TYPE_ID = 1;
var DEVICE_TOKEN = ""; var DEVICE_TOKEN = "";
var IS_VOICE_COMMAND_CLOSED = true; var IS_VOICE_COMMAND_CLOSED = true;
var IS_TEXT_COMPLETED = false; var IS_TEXT_COMPLETED = false;
// var DeviceTypeID = Platform.isIOS ? 1 : 2; // var DeviceTypeID = Platform.isIOS ? 1 : 2;
// var LANGUAGE_ID = 2; // var LANGUAGE_ID = 2;
@ -690,6 +697,7 @@ var UPLOAD_INSURANCE_CARD = 'Services/Patients.svc/REST/PatientER_PatientInfoFor
var GET_VACCINES = "Services/Patients.svc/REST/GetDoneVaccinesByPatientID"; var GET_VACCINES = "Services/Patients.svc/REST/GetDoneVaccinesByPatientID";
var GET_VACCINES_EMAIL = "Services/Notifications.svc/REST/SendVaccinesEmail"; var GET_VACCINES_EMAIL = "Services/Notifications.svc/REST/SendVaccinesEmail";
var GET_PAtIENTS_INSURANCE_APPROVALS = "Services/Patients.svc/REST/GetApprovalStatus_Async"; var GET_PAtIENTS_INSURANCE_APPROVALS = "Services/Patients.svc/REST/GetApprovalStatus_Async";
// var GET_PAtIENTS_INSURANCE_APPROVALS = "Services/Patients.svc/REST/GetApprovalStatus"; // var GET_PAtIENTS_INSURANCE_APPROVALS = "Services/Patients.svc/REST/GetApprovalStatus";
var SEARCH_BOT = 'HabibiChatBotApi/BotInterface/GetVoiceCommandResponse'; var SEARCH_BOT = 'HabibiChatBotApi/BotInterface/GetVoiceCommandResponse';
@ -783,6 +791,7 @@ var GET_ANCILLARY_ORDERS_DETAILS = 'Services/Doctors.svc/REST/GetOnlineAncillary
var PHARMACY_AUTORZIE_CUSTOMER = "AutorizeCustomer"; var PHARMACY_AUTORZIE_CUSTOMER = "AutorizeCustomer";
var PHARMACY_VERIFY_CUSTOMER = "VerifyCustomer"; var PHARMACY_VERIFY_CUSTOMER = "VerifyCustomer";
var PHARMACY_GET_COUNTRY = "countries"; var PHARMACY_GET_COUNTRY = "countries";
// var PHARMACY_CREATE_CUSTOMER = "epharmacy/api/CreateCustomer"; // var PHARMACY_CREATE_CUSTOMER = "epharmacy/api/CreateCustomer";
var PHARMACY_CREATE_CUSTOMER = "getorcreateCustomer"; var PHARMACY_CREATE_CUSTOMER = "getorcreateCustomer";
var GET_PHARMACY_BANNER = "promotionbanners"; var GET_PHARMACY_BANNER = "promotionbanners";
@ -923,8 +932,6 @@ var PAYFORT_PROD_URL = 'https://paymentservices.payfort.com/FortAPI/paymentApi';
// Check If InPatient API // Check If InPatient API
var CHECK_IF_INPATIENT = 'Services/Patients.svc/REST/GetInPatientAdmissionInfo'; var CHECK_IF_INPATIENT = 'Services/Patients.svc/REST/GetInPatientAdmissionInfo';
var CHECK_IF_PATIENT_ADMITTED = 'Services/Inps.svc/REST/checkIsPatientAdmittedOrAdmissionRequest';
// Get General Instructions API // Get General Instructions API
var GET_GENERAL_INSTRUCTIONS = 'Services/INPs.svc/REST/getGeneralInstructions'; var GET_GENERAL_INSTRUCTIONS = 'Services/INPs.svc/REST/getGeneralInstructions';
@ -1017,11 +1024,11 @@ const SAVE_SETTING = 'Services/Patients.svc/REST/UpdatePateintInfo';
const GET_PROFILE_IMAGE = 'Services/Patients.svc/REST/Patient_GetProfileImageData'; 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'; const GET_APPOINTMENT_PARKING_QR = 'Services/outps.svc/rest/pms_getParkingTicketByAppointmentNo';

@ -379,6 +379,7 @@ class AppAssets {
static const String invoiceListIcon = '$svgBasePath/invoice_list_icon.svg'; static const String invoiceListIcon = '$svgBasePath/invoice_list_icon.svg';
static const String open_camera = '$svgBasePath/open_camera.svg'; static const String open_camera = '$svgBasePath/open_camera.svg';
static const String gallery = '$svgBasePath/gallery.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 files = '$svgBasePath/files.svg';
static const String online_checkin_icon = '$svgBasePath/online_checkin_icon.svg'; static const String online_checkin_icon = '$svgBasePath/online_checkin_icon.svg';
static const String hmg_pharmacy_logo = '$svgBasePath/hmg_pharmacy_logo.svg'; static const String hmg_pharmacy_logo = '$svgBasePath/hmg_pharmacy_logo.svg';

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; 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/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/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/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/cache_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import '../features/authentication/models/request_models/registration_payload_model.dart'; import '../features/authentication/models/request_models/registration_payload_model.dart';
@ -20,7 +21,7 @@ class AppState {
static const String _profileImageKey = 'profile_image_data'; static const String _profileImageKey = 'profile_image_data';
AppState({required this.navigationService, required this.cacheService}){ AppState({required this.navigationService, required this.cacheService}) {
checkIfGMSAvailableOrNot(); checkIfGMSAvailableOrNot();
_loadProfileImageFromCache(); _loadProfileImageFromCache();
} }
@ -63,7 +64,8 @@ class AppState {
bool isChildLoggedIn = false; bool isChildLoggedIn = false;
bool isGMSAvailable = true; bool isGMSAvailable = true;
bool isAndroid = true; bool isAndroid = true;
bool isRatedVisible =false; bool isRatedVisible = false;
void setAuthenticatedUser(AuthenticatedUser? authenticatedUser, {bool isFamily = false}) { void setAuthenticatedUser(AuthenticatedUser? authenticatedUser, {bool isFamily = false}) {
if (isFamily) { if (isFamily) {
_authenticatedChildUser = authenticatedUser; _authenticatedChildUser = authenticatedUser;
@ -75,10 +77,10 @@ class AppState {
FutureOr<void> checkIfGMSAvailableOrNot() async { FutureOr<void> checkIfGMSAvailableOrNot() async {
isGMSAvailable = (await GmsCheck().checkGmsAvailability()) ?? true; isGMSAvailable = (await GmsCheck().checkGmsAvailability()) ?? true;
if(Platform.isIOS) { if (Platform.isIOS) {
isGMSAvailable = true; isGMSAvailable = true;
isAndroid = false; isAndroid = false;
return; return;
} }
} }
@ -111,16 +113,16 @@ class AppState {
set setProfileImageData(String? value) { set setProfileImageData(String? value) {
// Clear old data first // Clear old data first
if (_profileImageData != null && _profileImageData != value) { 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 // Set new value directly WITHOUT caching
_profileImageData = value; _profileImageData = value;
if (value != null && value.isNotEmpty) { if (value != null && value.isNotEmpty) {
print('✅ Profile image set (length: ${value.length}) - NO CACHE'); log('✅ Profile image set (length: ${value.length}) - NO CACHE');
} else { } else {
print('🗑️ Profile image cleared - NO CACHE'); log('🗑️ Profile image cleared - NO CACHE');
} }
} }
@ -128,14 +130,14 @@ class AppState {
void _loadProfileImageFromCache() { void _loadProfileImageFromCache() {
// DO NOT LOAD FROM CACHE - always fetch fresh from API // DO NOT LOAD FROM CACHE - always fetch fresh from API
_profileImageData = null; _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) /// Clear profile image data (e.g., on logout or user switch)
void clearProfileImageCache() { 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; _profileImageData = null;
print('✅ Profile image data cleared - NO CACHE'); log('✅ Profile image data cleared - NO CACHE');
} }
SelectDeviceByImeiRespModelElement? _selectDeviceByImeiRespModelElement; SelectDeviceByImeiRespModelElement? _selectDeviceByImeiRespModelElement;
@ -170,7 +172,6 @@ class AppState {
set setIsAuthenticated(v) => isAuthenticated = v; set setIsAuthenticated(v) => isAuthenticated = v;
String deviceTypeID = ""; String deviceTypeID = "";
set setDeviceTypeID(v) => deviceTypeID = v; set setDeviceTypeID(v) => deviceTypeID = v;
@ -226,7 +227,7 @@ class AppState {
userLat = 0.0; userLat = 0.0;
} }
setRatedVisible(bool value) { setRatedVisible(bool value) {
isRatedVisible = value; isRatedVisible = value;
} }
@ -281,7 +282,6 @@ class AppState {
} }
} }
dynamic getUserSwitchPayload({required String userType}) { dynamic getUserSwitchPayload({required String userType}) {
return _userSwitchPayloads[userType]; return _userSwitchPayloads[userType];
} }

@ -299,7 +299,7 @@ class ContactUsViewModel extends ChangeNotifier {
requestInsertCOCItem.projectID = 0; requestInsertCOCItem.projectID = 0;
requestInsertCOCItem.tokenID = "C0c@@dm!n?T&A&A@Barcha202029582948"; requestInsertCOCItem.tokenID = "C0c@@dm!n?T&A&A@Barcha202029582948";
requestInsertCOCItem.identificationNo = int.parse(appState.getAuthenticatedUser()!.patientIdentificationNo!); requestInsertCOCItem.identificationNo = int.parse(appState.getAuthenticatedUser()!.patientIdentificationNo!);
if (BASE_URL.contains('uat')) { if (ApiConsts.baseUrl.contains('uat')) {
requestInsertCOCItem.forDemo = true; requestInsertCOCItem.forDemo = true;
} else { } else {
requestInsertCOCItem.forDemo = false; requestInsertCOCItem.forDemo = false;

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

@ -11,7 +11,9 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/
import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/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/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/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_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/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -40,6 +42,41 @@ class HabibWalletViewModel extends ChangeNotifier {
List<RefundableAdvancesItem> refundableAdvancesList = []; List<RefundableAdvancesItem> refundableAdvancesList = [];
bool isSubmittingWithdrawal = false; 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; bool isSearchedFileNumberDataShown = false;
int currentIndex = 0; int currentIndex = 0;
@ -177,6 +214,43 @@ class HabibWalletViewModel extends ChangeNotifier {
return isValid; return isValid;
} }
// ==================== WITHDRAW ATTACHMENTS ====================================
List<String> withdrawAttachmentList = []; // Stores base64 strings
String? _withdrawAttachmentError;
static const int maxAttachments = 4;
static const int maxFileSizeBytes = 2 * 1024 * 1024; // 2MB
String? get withdrawAttachmentError => _withdrawAttachmentError;
void addWithdrawAttachment(String base64String) {
if (withdrawAttachmentList.length >= maxAttachments) {
_withdrawAttachmentError = LocaleKeys.maximumFilesReached.tr();
notifyListeners();
return;
}
withdrawAttachmentList.add(base64String);
_withdrawAttachmentError = null;
notifyListeners();
}
void removeWithdrawAttachment(int index) {
if (index >= 0 && index < withdrawAttachmentList.length) {
withdrawAttachmentList.removeAt(index);
_withdrawAttachmentError = null;
notifyListeners();
}
}
void clearWithdrawAttachmentError() {
_withdrawAttachmentError = null;
notifyListeners();
}
void setWithdrawAttachmentError(String error) {
_withdrawAttachmentError = error;
notifyListeners();
}
// Validate recharge form - shows only first error // Validate recharge form - shows only first error
bool validateRechargeForm(String amount) { bool validateRechargeForm(String amount) {
// Clear previous errors // Clear previous errors
@ -256,26 +330,29 @@ class HabibWalletViewModel extends ChangeNotifier {
required String patientNationalId, required String patientNationalId,
required int projectId, required int projectId,
required String projectName, required String projectName,
required String refundDestination, // '2' for Bank Transfer, '3' for Cash
Function(SubmitRefundResponseModel)? onSuccess, Function(SubmitRefundResponseModel)? onSuccess,
Function(String)? onError, Function(String)? onError,
}) async { }) async {
isSubmittingWithdrawal = true; isSubmittingWithdrawal = true;
notifyListeners(); notifyListeners();
final advances = refundableAdvancesList // Only send the selected advance (not all advances)
.map((a) => SubmitRefundAdvanceItem( final advances = selectedAdvance != null
advanceNo: (a.advanceNo ?? 0).toString(), ? [
paidAmount: a.paidAmount ?? 0.0, SubmitRefundAdvanceItem(
balanceAmount: a.balanceAmount ?? 0.0, advanceNo: (selectedAdvance!.advanceNo ?? 0).toString(),
advanceDate: a.advanceDate ?? "", paidAmount: selectedAdvance!.paidAmount ?? 0.0,
// advanceDate: _formatAdvanceDate(a.advanceDate), balanceAmount: selectedAdvance!.balanceAmount ?? 0.0,
)) advanceDate: selectedAdvance!.advanceDate ?? "",
.toList(); )
]
: <SubmitRefundAdvanceItem>[];
final request = SubmitRefundRequestModel( final request = SubmitRefundRequestModel(
projectId: projectId, projectId: projectId,
isRefundFastTrack: true, isRefundFastTrack: false,
// in case of wallet refund, skip ITG approval // in case of wallet refund, skip ITG approval [need ITG Approval]
refundType: '2', refundType: '2',
// wallet withdrawal // wallet withdrawal
refundProject: projectName, refundProject: projectName,
@ -290,13 +367,14 @@ class HabibWalletViewModel extends ChangeNotifier {
refundDoctorName: '', refundDoctorName: '',
refundClinicId: 0, refundClinicId: 0,
refundClinicName: '', refundClinicName: '',
refundDestination: '2', refundDestination: refundDestination,
// only bank transfer // Use provided refund destination
refundIban: iban, refundIban: iban,
refundRemarks: remarks, refundRemarks: remarks,
refundTotalAmount: totalAdvancesBalance, refundTotalAmount: selectedAdvanceBalance,
refundProceduresList: const [], refundProceduresList: const [],
refundAdvancesList: advances, refundAdvancesList: advances,
refundAttachmentsBase64List: withdrawAttachmentList,
); );
final result = await refundRequestRepo.submitRefund(request: request); final result = await refundRequestRepo.submitRefund(request: request);
@ -364,6 +442,11 @@ class HabibWalletViewModel extends ChangeNotifier {
isSubmittingWithdrawal = false; isSubmittingWithdrawal = false;
_ibanError = null; _ibanError = null;
_withdrawDescriptionError = null; _withdrawDescriptionError = null;
_withdrawAttachmentError = null;
withdrawAttachmentList.clear();
currentWithdrawStep = 0;
selectedAdvance = null;
selectedWithdrawMethod = null;
if (!silent) notifyListeners(); if (!silent) notifyListeners();
} }
@ -474,7 +557,12 @@ class HabibWalletViewModel extends ChangeNotifier {
Function(dynamic)? onSuccess, Function(dynamic)? onSuccess,
Function(String)? onError}) async { Function(String)? onError}) async {
final result = await habibWalletRepo.HISCreateAdvancePayment( 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( result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure), // (failure) async => await errorHandlerService.handleError(failure: failure),
@ -496,7 +584,8 @@ class HabibWalletViewModel extends ChangeNotifier {
); );
} }
Future<void> addAdvanceNumberRequest({required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async { Future<void> addAdvanceNumberRequest(
{required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await habibWalletRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference); final result = await habibWalletRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference);
result.fold( result.fold(
@ -548,7 +637,8 @@ class HabibWalletViewModel extends ChangeNotifier {
lakumAccountInfo = LakumInquiryInformationResponseModel(); lakumAccountInfo = LakumInquiryInformationResponseModel();
notifyListeners(); notifyListeners();
final result = await habibWalletRepo.getLakumAccountInformation(identificationNumber: getIt.get<AppState>().getAuthenticatedUser()!.patientIdentificationNo ?? ""); final result = await habibWalletRepo.getLakumAccountInformation(
identificationNumber: getIt.get<AppState>().getAuthenticatedUser()!.patientIdentificationNo ?? "");
result.fold( result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure), // (failure) async => await errorHandlerService.handleError(failure: failure),
@ -589,4 +679,89 @@ class HabibWalletViewModel extends ChangeNotifier {
}, },
); );
} }
// Check Is Patient Admitted Or Admission Request
bool isCheckingPatientAdmitted = false;
Future<void> checkIsPatientAdmittedOrAdmissionRequest({
required bool isActiveAppointment,
Function(dynamic)? onSuccess,
Function(String)? onError,
}) async {
isCheckingPatientAdmitted = true;
notifyListeners();
final result = await habibWalletRepo.checkIsPatientAdmittedOrAdmissionRequest(
isActiveAppointment: isActiveAppointment,
);
result.fold(
(failure) {
isCheckingPatientAdmitted = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
} else {
errorHandlerService.handleError(failure: failure);
}
},
(apiResponse) {
isCheckingPatientAdmitted = false;
notifyListeners();
if (apiResponse.messageStatus == 1) {
if (onSuccess != null) {
onSuccess(apiResponse.data);
}
} else {
if (onError != null) {
onError(apiResponse.errorMessage ?? LocaleKeys.failed.tr());
}
}
},
);
}
// Threshold Amount
double thresholdAmount = 5000.0;
bool isThresholdAmountLoading = false;
Future<void> getThresholdAmount({
required int projectId,
Function(double)? onSuccess,
Function(String)? onError,
}) async {
isThresholdAmountLoading = true;
notifyListeners();
final result = await refundRequestRepo.getThresholdAmount(projectId: projectId);
result.fold(
(failure) {
isThresholdAmountLoading = false;
thresholdAmount = 5000.0;
notifyListeners();
if (onError != null) {
onError(failure.message);
}
},
(apiResponse) {
isThresholdAmountLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null && apiResponse.data!.thresholdAmountList != null) {
final thresholdItem = apiResponse.data!.thresholdAmountList!.firstWhere(
(item) => item.projectID == projectId,
orElse: () => ThresholdAmountItem(thresholdAvailableAmountByProject: 5000.0),
);
thresholdAmount = thresholdItem.thresholdAvailableAmountByProject ?? 5000.0;
notifyListeners();
if (onSuccess != null) onSuccess(thresholdAmount);
} else {
thresholdAmount = 5000.0;
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? "Failed to get threshold amount");
}
}
},
);
}
} }

@ -19,6 +19,10 @@ class GetInvoicesListResponseModel {
String? doctorImageUrl; String? doctorImageUrl;
int? doctorRate; int? doctorRate;
bool? isAllowRefund; bool? isAllowRefund;
int? invoiceType;
String? invoiceTypeDescription;
bool? isPackageInvoice;
bool? isOutPatient;
int? patientNumber; int? patientNumber;
String? projectName; String? projectName;
@ -41,6 +45,10 @@ class GetInvoicesListResponseModel {
this.doctorImageUrl, this.doctorImageUrl,
this.doctorRate, this.doctorRate,
this.isAllowRefund, this.isAllowRefund,
this.invoiceType,
this.invoiceTypeDescription,
this.isPackageInvoice,
this.isOutPatient,
this.patientNumber, this.patientNumber,
this.projectName, this.projectName,
}); });
@ -68,6 +76,10 @@ class GetInvoicesListResponseModel {
doctorImageUrl: json["DoctorImageURL"], doctorImageUrl: json["DoctorImageURL"],
doctorRate: json["DoctorRate"], doctorRate: json["DoctorRate"],
isAllowRefund: json["IsAllowRefund"], isAllowRefund: json["IsAllowRefund"],
invoiceType: json["InvoiceType"],
invoiceTypeDescription: json["InvoiceTypeDescription"],
isPackageInvoice: json["IsPackageInvoice"],
isOutPatient: json['InvoiceType'] != 75,
patientNumber: json["PatientNumber"], patientNumber: json["PatientNumber"],
projectName: json["ProjectName"], projectName: json["ProjectName"],
); );
@ -91,6 +103,10 @@ class GetInvoicesListResponseModel {
"DoctorImageURL": doctorImageUrl, "DoctorImageURL": doctorImageUrl,
"DoctorRate": doctorRate, "DoctorRate": doctorRate,
"IsAllowRefund": isAllowRefund, "IsAllowRefund": isAllowRefund,
"invoiceType": invoiceType,
"invoiceDescription": invoiceTypeDescription,
"isOutPatient": isOutPatient,
"isPackageInvoice": isPackageInvoice,
"PatientNumber": patientNumber, "PatientNumber": patientNumber,
"ProjectName": projectName, "ProjectName": projectName,
}; };

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

@ -207,7 +207,7 @@ class ProfileSettingsRepoImp implements ProfileSettingsRepo {
Failure? failure; Failure? failure;
await apiClient.post( await apiClient.post(
UPLOAD_PROFILE_IMAGE, uploadProfileImageData,
body: body, body: body,
onFailure: (error, statusCode, {messageStatus, failureType}) { onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType; failure = failureType;

@ -79,6 +79,9 @@ class SubmitRefundRequestModel {
/// Populated only for wallet withdrawal refunds (refundType == '2'). /// Populated only for wallet withdrawal refunds (refundType == '2').
final List<SubmitRefundAdvanceItem> refundAdvancesList; final List<SubmitRefundAdvanceItem> refundAdvancesList;
/// List of base64 encoded images for refund attachments (max 4 files, each max 2MB)
final List<String> refundAttachmentsBase64List;
const SubmitRefundRequestModel({ const SubmitRefundRequestModel({
required this.projectId, required this.projectId,
required this.refundType, required this.refundType,
@ -101,6 +104,7 @@ class SubmitRefundRequestModel {
required this.isRefundFastTrack, required this.isRefundFastTrack,
required this.refundProceduresList, required this.refundProceduresList,
this.refundAdvancesList = const [], this.refundAdvancesList = const [],
this.refundAttachmentsBase64List = const [],
}); });
/// refundType '1' = invoice refund sends Refund_ProceduresList /// refundType '1' = invoice refund sends Refund_ProceduresList
@ -108,20 +112,21 @@ class SubmitRefundRequestModel {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> json = { final Map<String, dynamic> json = {
"ProjectID": projectId, "ProjectID": projectId,
"Refund_RefundType": refundType, "Refund_RefundType": refundType ?? '',
"Refund_Project": refundProject, "Refund_Project": refundProject ?? '',
"Refund_ProjectId": refundProjectId, "Refund_ProjectId": refundProjectId,
"Refund_PatientId": refundPatientId, "Refund_PatientId": refundPatientId ?? '',
"PatientMobileNumber": patientMobileNumber, "PatientMobileNumber": patientMobileNumber ?? '',
"Refund_PatientName": refundPatientName, "Refund_PatientName": refundPatientName ?? '',
"Refund_NationalId": refundNationalId, "Refund_NationalId": refundNationalId ?? '',
"Refund_DoctorId": refundDoctorId, "Refund_DoctorId": refundDoctorId,
"Refund_DoctorName": refundDoctorName, "Refund_DoctorName": refundDoctorName ?? '',
"Refund_RefundDestination": refundDestination, "Refund_RefundDestination": refundDestination ?? '',
"Refund_IBAN": refundIban, "Refund_IBAN": refundIban ?? '',
"Refund_Remarks": refundRemarks, "Refund_Remarks": refundRemarks ?? '',
"Refund_TotalRefundAmount": refundTotalAmount, "Refund_TotalRefundAmount": refundTotalAmount,
"Refund_IsFastTrack": isRefundFastTrack, "Refund_IsFastTrack": isRefundFastTrack,
"Refund_AttachmentsBase64List": refundAttachmentsBase64List,
}; };
if (refundType == '2') { if (refundType == '2') {
@ -129,10 +134,10 @@ class SubmitRefundRequestModel {
json["Refund_AdvancesList"] = refundAdvancesList.map((a) => a.toJson()).toList(); json["Refund_AdvancesList"] = refundAdvancesList.map((a) => a.toJson()).toList();
} else { } else {
// Invoice refund include invoice-specific fields and procedures list // Invoice refund include invoice-specific fields and procedures list
json["Refund_InvoiceNo"] = refundInvoiceNo; json["Refund_InvoiceNo"] = refundInvoiceNo ?? '';
json["Refund_AppointmentNo"] = refundAppointmentNo; json["Refund_AppointmentNo"] = refundAppointmentNo ?? '';
json["Refund_ClinicId"] = refundClinicId; json["Refund_ClinicId"] = refundClinicId;
json["Refund_ClinicName"] = refundClinicName; json["Refund_ClinicName"] = refundClinicName ?? '';
json["Refund_ProceduresList"] = refundProceduresList.map((p) => p.toJson()).toList(); json["Refund_ProceduresList"] = refundProceduresList.map((p) => p.toJson()).toList();
} }

@ -142,6 +142,8 @@ class InvoiceRefundProcedure {
final String? procedureId; final String? procedureId;
final String? procedureName; final String? procedureName;
final String? refundReason; final String? refundReason;
final String? rejectionReason;
final bool? isApproved;
InvoiceRefundProcedure({ InvoiceRefundProcedure({
this.isLocked, this.isLocked,
@ -150,6 +152,8 @@ class InvoiceRefundProcedure {
this.procedureId, this.procedureId,
this.procedureName, this.procedureName,
this.refundReason, this.refundReason,
this.rejectionReason,
this.isApproved,
}); });
factory InvoiceRefundProcedure.fromJson(Map<String, dynamic> json) { factory InvoiceRefundProcedure.fromJson(Map<String, dynamic> json) {
@ -160,6 +164,8 @@ class InvoiceRefundProcedure {
procedureId: json['ProcedureID'] as String?, procedureId: json['ProcedureID'] as String?,
procedureName: json['ProcedureName'] as String?, procedureName: json['ProcedureName'] as String?,
refundReason: json['RefundReason'] 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 String? verificationInitiatedAt;
final bool? verified; final bool? verified;
final String? errorMessage; final String? errorMessage;
final String? rejectReason;
final WalletRequestPayload? walletRequestPayload; final WalletRequestPayload? walletRequestPayload;
final InvoiceRequestPayload? invoiceRequestPayload; final InvoiceRequestPayload? invoiceRequestPayload;
@ -283,6 +290,7 @@ class RefundInvoiceModel {
this.verificationInitiatedAt, this.verificationInitiatedAt,
this.verified, this.verified,
this.errorMessage, this.errorMessage,
this.rejectReason,
this.walletRequestPayload, this.walletRequestPayload,
this.invoiceRequestPayload, this.invoiceRequestPayload,
}); });
@ -303,6 +311,7 @@ class RefundInvoiceModel {
verificationInitiatedAt: json['VerificationInitiatedAt'] as String?, verificationInitiatedAt: json['VerificationInitiatedAt'] as String?,
verified: json['Verified'] as bool?, verified: json['Verified'] as bool?,
errorMessage: json['ErrorMessage'] as String?, errorMessage: json['ErrorMessage'] as String?,
rejectReason: json['RejectReason'] as String?,
walletRequestPayload: walletRequestPayload:
json['WalletRequestPayload'] != null ? WalletRequestPayload.fromJson(json['WalletRequestPayload'] as Map<String, dynamic>) : null, json['WalletRequestPayload'] != null ? WalletRequestPayload.fromJson(json['WalletRequestPayload'] as Map<String, dynamic>) : null,
invoiceRequestPayload: invoiceRequestPayload:

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

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart';
@ -7,6 +9,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/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/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/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'; import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class RefundRequestRepo { abstract class RefundRequestRepo {
@ -33,6 +36,8 @@ abstract class RefundRequestRepo {
required int invoiceNo, required int invoiceNo,
required int projectId, required int projectId,
}); });
Future<Either<Failure, GenericApiModel<ThresholdAmountResponseModel>>> getThresholdAmount({required int projectId});
} }
class RefundRequestRepoImp implements RefundRequestRepo { class RefundRequestRepoImp implements RefundRequestRepo {
@ -287,15 +292,30 @@ class RefundRequestRepoImp implements RefundRequestRepo {
}, },
onSuccess: (response, statusCode, {messageStatus, errorMessage}) { onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try { try {
// Check if MessageStatus indicates success (handle null messageStatus)
if (messageStatus == null || messageStatus != 1) {
// Extract error message from response - ensure we handle nulls properly
final String errorMsg = (response['Message']?.toString() ??
response['message']?.toString() ??
response['ErrorEndUserMessage']?.toString() ??
response['ErrorMessage']?.toString() ??
errorMessage ??
'Unknown error');
loggerService.logError("submitRefund error - messageStatus: $messageStatus, error: $errorMsg");
failure = ServerFailure(errorMsg);
return;
}
final model = SubmitRefundResponseModel.fromJson(response as Map<String, dynamic>); final model = SubmitRefundResponseModel.fromJson(response as Map<String, dynamic>);
apiResponse = GenericApiModel<SubmitRefundResponseModel>( apiResponse = GenericApiModel<SubmitRefundResponseModel>(
messageStatus: 1, messageStatus: messageStatus,
statusCode: statusCode, statusCode: statusCode,
errorMessage: errorMessage, errorMessage: errorMessage,
data: model, data: model,
); );
} catch (e) { } catch (e, stackTrace) {
loggerService.logError("submitRefund parsing error: $e"); loggerService.logError("submitRefund parsing error: $e\nStack trace: $stackTrace");
failure = DataParsingFailure(e.toString()); failure = DataParsingFailure(e.toString());
} }
}, },
@ -305,7 +325,7 @@ class RefundRequestRepoImp implements RefundRequestRepo {
if (apiResponse == null) return Left(ServerFailure("Unknown error")); if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!); return Right(apiResponse!);
} catch (e) { } catch (e) {
loggerService.logError("submitRefund exception: $e"); log("submitRefund exception: $e");
return Left(UnknownFailure(e.toString())); return Left(UnknownFailure(e.toString()));
} }
} }
@ -357,4 +377,46 @@ class RefundRequestRepoImp implements RefundRequestRepo {
return Left(UnknownFailure(e.toString())); return Left(UnknownFailure(e.toString()));
} }
} }
@override
Future<Either<Failure, GenericApiModel<ThresholdAmountResponseModel>>> getThresholdAmount({required int projectId}) async {
final Map<String, dynamic> body = {
"ProjectID": projectId,
};
try {
GenericApiModel<ThresholdAmountResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getThresholdAmount,
body: body,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final ThresholdAmountResponseModel model = ThresholdAmountResponseModel.fromJson(response);
apiResponse = GenericApiModel<ThresholdAmountResponseModel>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: errorMessage,
data: model,
);
} catch (e) {
loggerService.logError("getThresholdAmount parsing error: $e");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
loggerService.logError("getThresholdAmount exception: $e");
return Left(UnknownFailure(e.toString()));
}
}
} }

@ -7,11 +7,12 @@ import 'package:hmg_patient_app_new/features/refund_request/models/req_models/su
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/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/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/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_repo.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.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 { extension RefundTabTypeExtension on RefundTabType {
String toDisplayString() { String toDisplayString() {
@ -20,6 +21,8 @@ extension RefundTabTypeExtension on RefundTabType {
return LocaleKeys.hmgWallet.tr(); return LocaleKeys.hmgWallet.tr();
case RefundTabType.invoices: case RefundTabType.invoices:
return LocaleKeys.bankTransfer.tr(); return LocaleKeys.bankTransfer.tr();
case RefundTabType.cash:
return LocaleKeys.cash.tr();
} }
} }
} }
@ -109,6 +112,7 @@ class RefundRequestViewModel extends ChangeNotifier {
currentStep = 0; currentStep = 0;
procedures.clear(); procedures.clear();
needITGApproval = false; needITGApproval = false;
isWholeInvoiceRefund = false;
selectedPaymentMethod = null; selectedPaymentMethod = null;
selectedRefundReason = null; selectedRefundReason = null;
isProceduresLoading = false; isProceduresLoading = false;
@ -116,6 +120,8 @@ class RefundRequestViewModel extends ChangeNotifier {
invoiceDetails = null; invoiceDetails = null;
_ibanError = null; _ibanError = null;
_descriptionError = null; _descriptionError = null;
refundAttachmentList.clear();
_attachmentError = null;
// Use Future.microtask to avoid calling notifyListeners while tree is locked (e.g. during dispose) // Use Future.microtask to avoid calling notifyListeners while tree is locked (e.g. during dispose)
Future.microtask(() { Future.microtask(() {
if (hasListeners) notifyListeners(); if (hasListeners) notifyListeners();
@ -208,15 +214,20 @@ class RefundRequestViewModel extends ChangeNotifier {
List<RefundableProcedure> procedures = []; List<RefundableProcedure> procedures = [];
bool isProceduresLoading = false; bool isProceduresLoading = false;
bool needITGApproval = false; bool needITGApproval = false;
bool isWholeInvoiceRefund = false; // For Inpatient/Package invoices
void setProcedures(List<RefundableProcedure> list, {bool needsITGApproval = false}) { void setProcedures(List<RefundableProcedure> list, {bool needsITGApproval = false, bool isWholeInvoiceRefund = false}) {
procedures = list; procedures = list;
needITGApproval = needsITGApproval; needITGApproval = needsITGApproval;
this.isWholeInvoiceRefund = isWholeInvoiceRefund;
isProceduresLoading = false; isProceduresLoading = false;
notifyListeners(); notifyListeners();
} }
void toggleProcedureSelection(RefundableProcedure procedure) { void toggleProcedureSelection(RefundableProcedure procedure) {
// Prevent selection toggle for whole invoice refunds
if (isWholeInvoiceRefund) return;
procedure.isSelected = !procedure.isSelected; procedure.isSelected = !procedure.isSelected;
notifyListeners(); notifyListeners();
} }
@ -229,6 +240,9 @@ class RefundRequestViewModel extends ChangeNotifier {
bool get isAllSelected => procedures.isNotEmpty && procedures.every((p) => p.isSelected); bool get isAllSelected => procedures.isNotEmpty && procedures.every((p) => p.isSelected);
void toggleSelectAll() { void toggleSelectAll() {
// Prevent toggle for whole invoice refunds
if (isWholeInvoiceRefund) return;
final selectAll = !isAllSelected; final selectAll = !isAllSelected;
for (final p in procedures) { for (final p in procedures) {
p.isSelected = selectAll; p.isSelected = selectAll;
@ -296,6 +310,47 @@ class RefundRequestViewModel extends ChangeNotifier {
return isValid; return isValid;
} }
// Attachments
List<String> refundAttachmentList = []; // Stores base64 strings
String? _attachmentError;
static const int maxAttachments = 4;
static const int maxFileSizeBytes = 2 * 1024 * 1024; // 2MB
static const double cashRefundMaxAmount = 5000.0; // Maximum amount for cash refund
String? get attachmentError => _attachmentError;
// Check if total refundable amount exceeds cash limit
bool get isCashRefundAmountExceeded => totalRefundableAmount > cashRefundMaxAmount;
void addRefundAttachment(String base64String) {
if (refundAttachmentList.length >= maxAttachments) {
_attachmentError = LocaleKeys.maximumFilesReached.tr();
notifyListeners();
return;
}
refundAttachmentList.add(base64String);
_attachmentError = null;
notifyListeners();
}
void removeRefundAttachment(int index) {
if (index >= 0 && index < refundAttachmentList.length) {
refundAttachmentList.removeAt(index);
_attachmentError = null;
notifyListeners();
}
}
void clearAttachmentError() {
_attachmentError = null;
notifyListeners();
}
void setAttachmentError(String error) {
_attachmentError = error;
notifyListeners();
}
// Payment Method // Payment Method
RefundTabType? selectedPaymentMethod; RefundTabType? selectedPaymentMethod;
@ -570,4 +625,48 @@ class RefundRequestViewModel extends ChangeNotifier {
}, },
); );
} }
// Threshold Amount
double thresholdAmount = 5000.0;
bool isThresholdAmountLoading = false;
Future<void> getThresholdAmount({
required int projectId,
Function(double)? onSuccess,
Function(String)? onError,
}) async {
isThresholdAmountLoading = true;
notifyListeners();
final result = await refundRequestRepo.getThresholdAmount(projectId: projectId);
result.fold(
(failure) {
isThresholdAmountLoading = false;
thresholdAmount = 5000.0;
notifyListeners();
if (onError != null) {
onError(failure.message);
}
},
(apiResponse) {
isThresholdAmountLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null && apiResponse.data!.thresholdAmountList != null) {
final thresholdItem = apiResponse.data!.thresholdAmountList!.firstWhere(
(item) => item.projectID == projectId,
orElse: () => ThresholdAmountItem(thresholdAvailableAmountByProject: 5000.0),
);
thresholdAmount = thresholdItem.thresholdAvailableAmountByProject ?? 5000.0;
notifyListeners();
if (onSuccess != null) onSuccess(thresholdAmount);
} else {
thresholdAmount = 5000.0;
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? "Failed to get threshold amount");
}
}
},
);
}
} }

@ -2,7 +2,7 @@
// ignore_for_file: constant_identifier_names // ignore_for_file: constant_identifier_names
abstract class LocaleKeys { abstract class LocaleKeys {
static const english = 'english'; static const english = 'english';
static const arabic = 'arabic'; static const arabic = 'arabic';
static const login = 'login'; static const login = 'login';
@ -1912,6 +1912,8 @@ abstract class LocaleKeys {
static const bankTransfer = 'bankTransfer'; static const bankTransfer = 'bankTransfer';
static const selectRefundReason = 'selectRefundReason'; static const selectRefundReason = 'selectRefundReason';
static const refundReason = 'refundReason'; static const refundReason = 'refundReason';
static const requestRejectionReason = 'requestRejectionReason';
static const procedureRejectionReason = 'procedureRejectionReason';
static const refundReason1 = 'refundReason1'; static const refundReason1 = 'refundReason1';
static const refundReason2 = 'refundReason2'; static const refundReason2 = 'refundReason2';
static const refundReason3 = 'refundReason3'; static const refundReason3 = 'refundReason3';
@ -1933,12 +1935,23 @@ abstract class LocaleKeys {
static const getParkingQR = 'getParkingQR'; static const getParkingQR = 'getParkingQR';
static const cannotOpenThisFile = 'cannotOpenThisFile'; static const cannotOpenThisFile = 'cannotOpenThisFile';
static const refundDetails = 'refundDetails'; static const refundDetails = 'refundDetails';
static const refund = 'refund';
static const requestNo = 'requestNo'; static const requestNo = 'requestNo';
static const referenceNo = 'referenceNo'; static const referenceNo = 'referenceNo';
static const advanceNumber = 'advanceNumber'; static const advanceNumber = 'advanceNumber';
static const amountOnly = 'amountOnly'; static const amountOnly = 'amountOnly';
static const invoiceHistory = 'invoiceHistory'; 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 parkingQR = 'parkingQR';
static const parkingQRDesc = 'parkingQRDesc'; static const parkingQRDesc = 'parkingQRDesc';
static const livecareUpdateInsuranceSent = 'livecareUpdateInsuranceSent'; static const livecareUpdateInsuranceSent = 'livecareUpdateInsuranceSent';

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

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

@ -161,23 +161,58 @@ class _HabibWalletState extends State<HabibWalletPage> {
}, },
), ),
), ),
// SizedBox(width: 8.w), SizedBox(width: 8.w),
// Flexible( Flexible(
// child: CustomButton( child: CustomButton(
// height: 40.h, height: 40.h,
// icon: AppAssets.refundIcon, icon: AppAssets.refundIcon,
// iconSize: 24.w, iconSize: 24.w,
// backgroundColor: AppColors.successColor, backgroundColor: AppColors.successColor,
// textColor: Colors.white, textColor: Colors.white,
// text: LocaleKeys.withdraw.tr(context: context), text: LocaleKeys.withdraw.tr(context: context),
// borderWidth: 0.w, borderWidth: 0.w,
// fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
// borderColor: Colors.transparent, borderColor: Colors.transparent,
// padding: EdgeInsets.fromLTRB(4, 0, 12, 0), padding: EdgeInsets.fromLTRB(4, 0, 12, 0),
// fontSize: 14.f, fontSize: 14.f,
// onPressed: () => Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())), onPressed: () async {
// ), // Show loader
// ), LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
// Check if patient is admitted
await habibWalletVM.checkIsPatientAdmittedOrAdmissionRequest(
isActiveAppointment: false,
onSuccess: (data) {
LoaderBottomSheet.hideLoader();
// Check if patient is admitted
bool isAdmitted = data['isAdmitted'] ?? false;
if (isAdmitted) {
// Show error message if patient is admitted
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(
loadingText: LocaleKeys.refundCannotBeProcessedPatientAdmitted.tr(context: context),
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
} else {
// Navigate to withdraw page if not admitted
Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage()));
}
},
onError: (error) {
LoaderBottomSheet.hideLoader();
// Navigate to withdraw page even on error (messageStatus == 2)
Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage()));
},
);
},
),
),
], ],
), ),
SizedBox(height: 24.h), SizedBox(height: 24.h),
@ -332,5 +367,3 @@ class _HabibWalletState extends State<HabibWalletPage> {
); );
} }
} }

@ -2,14 +2,19 @@ import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.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/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/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/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/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_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/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/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/habib_wallet/widgets/select_hospital_bottom_sheet.dart';
import 'package:hmg_patient_app_new/presentation/refund_request/refund_history_screen.dart'; import 'package:hmg_patient_app_new/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/image_picker.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.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/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/routes/custom_page_route.dart';
import 'package:hmg_patient_app_new/widgets/stepper/stepper_widget.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class WithdrawRequestCreatePage extends StatefulWidget { class WithdrawRequestCreatePage extends StatefulWidget {
@ -45,6 +52,8 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
_appState = getIt.get<AppState>(); _appState = getIt.get<AppState>();
_habibWalletViewModel = context.read<HabibWalletViewModel>(); _habibWalletViewModel = context.read<HabibWalletViewModel>();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_habibWalletViewModel.currentWithdrawStep = 0;
_habibWalletViewModel.selectedAdvance = null;
_habibWalletViewModel.getProjectsList(); _habibWalletViewModel.getProjectsList();
}); });
} }
@ -59,12 +68,262 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
super.dispose(); super.dispose();
} }
bool get _isFormValid { // Stepper
final vm = _habibWalletViewModel;
return vm.selectedHospital != null && Widget _buildStepper(HabibWalletViewModel vm) {
!vm.isAdvancesLoading && return Padding(
_bankDetailsController.text.trim().isNotEmpty && padding: EdgeInsets.symmetric(horizontal: 24.w),
_descriptionController.text.trim().isNotEmpty; child: LayoutBuilder(
builder: (context, constraints) {
final itemWidth = (constraints.maxWidth - 2 * 8.h) / 2;
return Row(
children: List.generate(2, (index) {
final isActive = index <= vm.currentWithdrawStep;
final isThumb = index == vm.currentWithdrawStep;
return StepperWidget(
itemWidth,
isActive ? AppColors.primaryRedColor : AppColors.greyColor,
isThumb,
6.h,
);
}),
);
},
),
);
}
// Step 1 Select Advance
Widget _buildStep1Content(HabibWalletViewModel vm) {
return Consumer<HabibWalletViewModel>(
builder: (context, walletVM, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.selectAdvance.tr(context: context).toText20(isBold: true),
SizedBox(height: 24.h),
if (vm.isAdvancesLoading)
ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => SizedBox(height: 16.h),
itemCount: 4,
itemBuilder: (_, index) => Container(
height: 80.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 16.h,
),
).toShimmer2(isShow: true, radius: 16.r),
)
else if (vm.refundableAdvancesList.isEmpty)
Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 24.h),
child: LocaleKeys.noAdvancesAvailable.tr().toText14(color: AppColors.greyTextColor),
),
)
else
AnimationLimiter(
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => Divider(
color: AppColors.bottomNAVBorder,
height: 1,
thickness: 1,
).paddingSymmetrical(16.w, 0.w),
itemCount: vm.refundableAdvancesList.length,
itemBuilder: (context, index) {
final advance = vm.refundableAdvancesList[index];
return _buildAdvanceCard(advance, index);
},
),
),
),
],
);
},
);
}
Widget _buildAdvanceCard(RefundableAdvancesItem advance, int index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 400),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _habibWalletViewModel.setSelectedAdvance(advance),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 24.w,
height: 24.w,
child: Radio<int>(
value: advance.advanceNo ?? 0,
groupValue: _habibWalletViewModel.selectedAdvance?.advanceNo ?? -1,
onChanged: (_) => _habibWalletViewModel.setSelectedAdvance(advance),
activeColor: AppColors.primaryRedColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"${LocaleKeys.advanceNo.tr()}: ${advance.advanceNo ?? 0}".toText16(isBold: true),
SizedBox(height: 4.h),
_buildAdvanceDate(advance.advanceDate),
],
),
),
SizedBox(width: 8.w),
Utils.getPaymentAmountWithSymbol(
(advance.balanceAmount ?? 0.0).toStringAsFixed(2).toText20(
isBold: true,
isEnglishOnly: true,
color: AppColors.textColor,
),
AppColors.textColor,
10.f,
isSaudiCurrency: true,
),
],
),
),
),
),
),
),
);
}
Widget _buildAdvanceDate(String? dateString) {
if (dateString == null || dateString.isEmpty) {
return const SizedBox.shrink();
}
try {
final date = DateUtil.convertStringToDate(dateString);
final formattedDate = DateUtil.formatDateToDate(date, false);
return formattedDate.toText12(color: AppColors.greyTextColor);
} catch (e) {
return const SizedBox.shrink();
}
}
// Step 2 Refund Method Selection
Widget _buildRefundMethodSelection(HabibWalletViewModel vm) {
final paymentOptions = [
RefundTabType.invoices, // Bank Transfer
RefundTabType.cash, // Cash
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.selectRefundMethod.tr(context: context).toText20(isBold: true),
SizedBox(height: 24.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: paymentOptions.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final type = paymentOptions[index];
final isCashOption = type == RefundTabType.cash;
final isCashSelected = vm.selectedWithdrawMethod == RefundTabType.cash;
return Column(
children: [
RadioListTileWidget<RefundTabType>(
value: type,
groupValue: vm.selectedWithdrawMethod,
title: type.toDisplayString(),
onChanged: (value) {
if (value != null) vm.setSelectedWithdrawMethod(value);
},
),
// Show cash instructions inside cash tile when selected
if (isCashOption && isCashSelected) ...[
Padding(
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildSvgWithAssets(
icon: AppAssets.alertSquare,
height: 24.h,
width: 24.h,
iconColor: AppColors.textColor,
),
SizedBox(width: 12.w),
Expanded(
child: LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText12(
color: AppColors.greyInfoTextColor,
),
),
],
),
),
],
if (index != paymentOptions.length - 1)
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w),
],
);
},
),
),
if (vm.selectedWithdrawMethod == RefundTabType.cash && vm.isCashWithdrawAmountExceeded) ...[
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(16.h),
decoration: BoxDecoration(
color: AppColors.warningColorYellow.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: AppColors.warningColorYellow, width: 1),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline, color: AppColors.warningColorYellow, size: 20.h),
SizedBox(width: 12.w),
Expanded(
child: LocaleKeys.cashRefundLimitExceeded.tr(context: context).toText14(
color: AppColors.textColor,
),
),
],
),
),
],
],
);
} }
// Bottom bar // Bottom bar
@ -72,10 +331,23 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
Widget _buildBottomBar() { Widget _buildBottomBar() {
return Consumer<HabibWalletViewModel>( return Consumer<HabibWalletViewModel>(
builder: (context, vm, _) { builder: (context, vm, _) {
final isValid = vm.selectedHospital != null && final isStep2 = vm.currentWithdrawStep == 1;
!vm.isAdvancesLoading &&
_bankDetailsController.text.trim().isNotEmpty && // Step 1 validation: advance selected
_descriptionController.text.trim().isNotEmpty; 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( return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
@ -86,15 +358,47 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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( CustomButton(
text: LocaleKeys.submitRequest.tr(context: context), icon: isStep2 ? AppAssets.refundIcon : null,
isDisabled: !isValid, text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context),
isDisabled: isStep2 ? !isStep2Valid : !isStep1Valid,
onPressed: () { onPressed: () {
final isFormValid = vm.validateWithdrawForm( if (!isStep2) {
iban: _bankDetailsController.text, // Move to step 2
description: _descriptionController.text, if (vm.selectedAdvance != null) {
); vm.nextWithdrawStep();
if (isFormValid) { }
} 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); _handleWalletWithdrawalSubmit(vm);
} }
}, },
@ -108,6 +412,8 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
Widget _buildProjectSelectionAndBalance(HabibWalletViewModel habibWalletViewModel) { Widget _buildProjectSelectionAndBalance(HabibWalletViewModel habibWalletViewModel) {
final isArabic = _appState.isArabic(); final isArabic = _appState.isArabic();
final showAmount = habibWalletViewModel.currentWithdrawStep == 1 && habibWalletViewModel.selectedAdvance != null;
return Container( return Container(
padding: EdgeInsets.all(16.h), padding: EdgeInsets.all(16.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r),
@ -137,45 +443,46 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
]), ]),
], ],
), ),
Utils.buildSvgWithAssets( if (habibWalletViewModel.currentWithdrawStep == 0)
icon: AppAssets.arrow_down, Utils.buildSvgWithAssets(
width: 25.h, icon: AppAssets.arrow_down,
height: 25.h, width: 25.h,
), height: 25.h,
),
], ],
).onPress(() { ).onPress(
showCommonBottomSheetWithoutHeight( habibWalletViewModel.currentWithdrawStep == 0
context, ? () {
title: LocaleKeys.selectHospital.tr(context: context), showCommonBottomSheetWithoutHeight(
isDismissible: false, context,
child: SelectHospitalBottomSheet( title: LocaleKeys.selectHospital.tr(context: context),
onHospitalSelected: (hospital) { isDismissible: false,
habibWalletViewModel.setSelectedHospital(hospital); child: SelectHospitalBottomSheet(
habibWalletViewModel.fetchAdvancesForHospital(); onHospitalSelected: (hospital) {
}, habibWalletViewModel.setSelectedHospital(hospital);
), habibWalletViewModel.fetchAdvancesForHospital();
callBackFunc: () {}, if (hospital.iD != null) {
); final projectId = hospital.iD is int ? hospital.iD as int : int.tryParse(hospital.iD.toString());
}), if (projectId != null) {
// Only show divider + amount once hospital is selected habibWalletViewModel.getThresholdAmount(projectId: projectId);
if (habibWalletViewModel.selectedHospital != null) ...[ }
}
},
),
callBackFunc: () {},
);
}
: () {}, // Empty callback for step 2
),
if (showAmount) ...[
Divider(color: AppColors.bottomNAVBorder, thickness: 1).paddingOnly(top: 16.h), Divider(color: AppColors.bottomNAVBorder, thickness: 1).paddingOnly(top: 16.h),
habibWalletViewModel.isAdvancesLoading Utils.getPaymentAmountWithSymbol(
? Container( habibWalletViewModel.selectedAdvanceBalance.toStringAsFixed(2).toText44(isBold: true),
height: 60.h, AppColors.blackColor,
// width: 160.w, 18.w,
decoration: BoxDecoration( isSaudiCurrency: true,
color: AppColors.whiteColor, isExpanded: false,
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,
),
], ],
], ],
), ),
@ -254,42 +561,157 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
); );
} }
Widget _buildImagePickerDropDown() { // Image Upload Section
return Container(
padding: EdgeInsets.all(16.h), Widget _buildImageUploadSection(HabibWalletViewModel vm) {
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), return Consumer<HabibWalletViewModel>(
child: Row( builder: (context, viewModel, _) {
mainAxisAlignment: MainAxisAlignment.spaceBetween, final hasError = viewModel.withdrawAttachmentError != null;
children: [ return Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Utils.buildSvgWithAssets( // Upload button
icon: AppAssets.selectImageIcon, InkWell(
width: 40.h, onTap: () {
height: 40.h, if (viewModel.withdrawAttachmentList.length >= 4) {
applyThemeColor: false, viewModel.setWithdrawAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context));
return;
}
ImageOptions.showImageOptionsNew(
context,
true,
(String base64Image, File file) async {
// Validate file size (2MB max)
final fileSize = await file.length();
if (fileSize > HabibWalletViewModel.maxFileSizeBytes) {
viewModel.setWithdrawAttachmentError(LocaleKeys.fileSizeTooLarge.tr(context: context));
return;
}
// Check max files limit
if (viewModel.withdrawAttachmentList.length >= HabibWalletViewModel.maxAttachments) {
viewModel.setWithdrawAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context));
return;
}
// Add attachment
viewModel.addWithdrawAttachment(base64Image);
},
title: LocaleKeys.selectAttachment.tr(context: context),
allowMultiple: true, // Enable multiple selection
);
},
child: Container(
padding: EdgeInsets.all(16.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.r),
border: hasError ? Border.all(color: AppColors.primaryRedBorderColor, width: 1) : null,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
height: 40.h,
width: 40.h,
padding: EdgeInsets.all(8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
borderRadius: 12.r,
color: AppColors.greyColor,
),
child: Utils.buildSvgWithAssets(icon: AppAssets.selectImageIcon, applyThemeColor: false),
),
SizedBox(width: 12.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.uploadImage.tr(context: context).toText12(
color: AppColors.inputLabelTextColor,
isBold: true,
),
LocaleKeys.selectImage.tr(context: context).toText14(
color: AppColors.inputLabelTextColor,
isBold: true,
),
],
),
],
),
if (hasError) ...[
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
viewModel.withdrawAttachmentError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
fontWeight: FontWeight.w600,
),
),
],
),
],
],
),
),
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h),
],
),
),
),
// Selected images list
if (viewModel.withdrawAttachmentList.isNotEmpty) ...[
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: ListView.separated(
padding: EdgeInsets.all(16.h),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: viewModel.withdrawAttachmentList.length,
separatorBuilder: (context, index) => SizedBox(height: 12.h),
itemBuilder: (BuildContext context, int index) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.attach_file,
color: Color(0xff2B353E),
size: 20.h,
),
SizedBox(width: 8.w),
"${LocaleKeys.image.tr(context: context)} ${index + 1}".toText14(),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.cancel_circle_icon, width: 20.h, height: 20.h).onPress(() {
viewModel.removeWithdrawAttachment(index);
}),
],
);
},
),
), ),
SizedBox(width: 8.h),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
LocaleKeys.uploadImage.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
LocaleKeys.selectImage.tr(context: context).toText14(color: AppColors.textColor, isBold: true),
]),
], ],
), ],
Utils.buildSvgWithAssets(
icon: AppAssets.arrow_down,
width: 25.h,
height: 25.h,
),
],
).onPress(() {
ImageOptions.showImageOptionsNew(
context,
false,
title: LocaleKeys.selectOptions.tr(context: context),
(String image, File file) {},
); );
}), },
); );
} }
@ -310,6 +732,11 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
LoaderBottomSheet.showLoader(); LoaderBottomSheet.showLoader();
final user = _appState.getAuthenticatedUser(); final user = _appState.getAuthenticatedUser();
final hospital = vm.selectedHospital; 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( await vm.submitWalletWithdrawalRequest(
iban: _bankDetailsController.text.trim(), iban: _bankDetailsController.text.trim(),
remarks: _descriptionController.text.trim(), remarks: _descriptionController.text.trim(),
@ -319,9 +746,10 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
patientNationalId: user?.patientIdentificationNo?.toString() ?? '', patientNationalId: user?.patientIdentificationNo?.toString() ?? '',
projectId: hospital?.mainProjectID ?? 0, projectId: hospital?.mainProjectID ?? 0,
projectName: hospital?.name ?? '', projectName: hospital?.name ?? '',
refundDestination: refundDestination,
onSuccess: (response) { onSuccess: (response) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
_showWithdrawalSuccessBottomSheet(response.refNo ?? ''); _showWithdrawalSuccessBottomSheet(response.refNo ?? '', refundDestination);
}, },
onError: (err) { onError: (err) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
@ -340,7 +768,10 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
); );
} }
void _showWithdrawalSuccessBottomSheet(String refNo) { void _showWithdrawalSuccessBottomSheet(String refNo, String refundDestination) {
// refundDestination: '2' = Bank Transfer, '3' = Cash
final isCashRefund = refundDestination == '3';
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
child: Padding( child: Padding(
@ -359,7 +790,11 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
SizedBox(width: 6.w), SizedBox(width: 6.w),
refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true), 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), SizedBox(height: 24.h),
Row( Row(
@ -374,7 +809,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
Navigator.of(context).push( Navigator.of(context).push(
CustomPageRoute( CustomPageRoute(
page: RefundHistoryScreen( page: RefundHistoryScreen(
refundType: 1, // 2 for wallet, 1 for invoice refundType: 2, // 2 for wallet, 1 for invoice
), ),
), ),
); );
@ -407,6 +842,13 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
Expanded( Expanded(
child: CollapsingListView( child: CollapsingListView(
title: LocaleKeys.requestRefund.tr(context: context), title: LocaleKeys.requestRefund.tr(context: context),
leadingCallback: () {
if (habibWalletViewModel.currentWithdrawStep > 0) {
habibWalletViewModel.previousWithdrawStep();
} else {
context.pop();
}
},
requests: () => Navigator.of(context).push( requests: () => Navigator.of(context).push(
CustomPageRoute( CustomPageRoute(
page: RefundHistoryScreen( page: RefundHistoryScreen(
@ -415,17 +857,66 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
), ),
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
padding: EdgeInsets.only(bottom: 32.h, left: 24.w, right: 24.w), padding: EdgeInsets.only(bottom: 32.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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) ...[ if (habibWalletViewModel.selectedHospital != null) ...[
SizedBox(height: 16.h), SizedBox(height: 16.h),
_buildBankInputDetails(), _buildStepper(habibWalletViewModel),
// SizedBox(height: 16.h), SizedBox(height: 16.h),
// _buildImagePickerDropDown(),
], ],
// Step content - show only after hospital selected
if (habibWalletViewModel.selectedHospital != null)
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.05, 0),
end: Offset.zero,
).animate(animation),
child: child,
),
),
child: habibWalletViewModel.currentWithdrawStep == 0
? KeyedSubtree(
key: const ValueKey('step1'),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: _buildStep1Content(habibWalletViewModel),
),
)
: KeyedSubtree(
key: const ValueKey('step2'),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRefundMethodSelection(habibWalletViewModel),
if (habibWalletViewModel.selectedWithdrawMethod == RefundTabType.invoices) ...[
SizedBox(height: 16.h),
_buildBankInputDetails(),
],
if (habibWalletViewModel.selectedWithdrawMethod != null) ...[
SizedBox(height: 16.h),
_buildImageUploadSection(habibWalletViewModel),
],
],
),
),
),
),
], ],
), ),
), ),
@ -439,7 +930,3 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
); );
} }
} }
// I/flutter (13750): uri: https://uat.hmgwebservices.com/Services/COCWS.svc/REST/SubmitRefund
// [log] body: {"ProjectID":15,"Refund_RefundType":"2","Refund_Project":"مستشفى الريان","Refund_ProjectId":15,"Refund_PatientId":"4770714","PatientMobileNumber":"0504278212","Refund_PatientName":"FAIZ UR REHMAN REHMAN","Refund_NationalId":"2530976584","Refund_DoctorId":0,"Refund_DoctorName":"","Refund_RefundDestination":"2","Refund_IBAN":"SA0380000000608010167519","Refund_Remarks":"teskchjvs","Refund_TotalRefundAmount":1714.75,"Refund_AdvancesList":[{"AdvanceNo":"2026000444","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1775682000000+0300)/"},{"AdvanceNo":"2026000392","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1774645200000+0300)/"},{"AdvanceNo":"2026000066","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1770152400000+0300)/"},{"AdvanceNo":"2026000047","PaidAmount":30.0,"BalanceAmount":30.0,"AdvanceDate":"/Date(1769547600000+0300)/"},{"AdvanceNo":"2025000751","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1765746000000+0300)/"},{"AdvanceNo":"2025000414","PaidAmount":115.0,"BalanceAmount":115.0,"AdvanceDate":"/Date(1747083600000+0300)/"},{"AdvanceNo":"2025000210","PaidAmount":120.75,"BalanceAmount":120.75,"AdvanceDate":"/Date(1741726800000+0300)/"}],"PatientType":1,"PatientTypeID":1,"TokenID":"@dm!n","PatientID":4770714,"PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNi0wNS0xOFQwNzo1MzoyMS4wMjQ5MDM2WiIsIlBhdGllbnRJRCI6IjQ3NzIxNjMiLCJQYXRpZW50TWVyZ2VkSURzIjoiNDc3MjE2MyIsIlBhdGllbnRPdXRTQSI6ZmFsc2UsIlNoYXJlZElkcyI6IiJ9LCJleHAiOjE3NzkxMzgwMDB9.VHd_kCya33GfK-DywTHwSU1PMewbQB7UhQD1zXS7X5w","VersionID":"21.2","Channel":"3","IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","LanguageID":"1","Latitude":"0.0","Longitude":"0.0","DeviceTypeID":"2","IsNewFlutterApp":true}
// [log] response: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":"Internal server error occurred.","ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":true,"MealOrderStatus":0,"MealType":0,"MessageStatus":2,"NumberOfResultRecords":0,"PatientBlodType":null,"RequestId":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"ENSAT_GetPatientRequestListResponse":null,"ENSAT_Patient_External_Response":null,"ETQAN_CreateIncident_Response":null,"ETQAN_GetEmployeeOVRRequests_Response":null,"ETQAN_GetIncidentRequest_Response":null,"ETQAN_GetProjects_Response":null,"ITGRequest":null,"Itg_FormAttachmentsList":null,"ListCOCItems":null,"Message":"Refunds API returned status 500. Body: {\"message\":\"Internal server error occurred.\"}","Mohemm_ENSAT_GetItgToken_Response":null,"Mohemm_ITG_DepartmentSectionsList":null,"Mohemm_ITG_Pending_Task_ResponseItem":null,"Mohemm_ITG_ProjectDepartmentsList":null,"Mohemm_ITG_ResponseItem":null,"Mohemm_ITG_SectionTopicsList":null,"Mohemm_ITG_TicketDetailsList":null,"Mohemm_ITG_TicketTransactionsList":null,"Mohemm_ITG_TicketsByEmployeeList":null,"Mohemm_Itg_ProjectsList":null,"Mohemm_Itg_TicketTypesList":null,"ReferenceNumber":null,"Refund_InvoicesList":null,"Refund_PatientIdDisplay":null,"Refund_RefNo":null,"Refund_RefundTypeDisplay":null,"Refund_StatusItem":null,"Refund_TotalInvoices":0,"Refund_VerifyResult":null,"RequestType":null,"TotalCount":0,"statuseCode":500}

@ -89,11 +89,14 @@ class InvoiceListCard extends StatelessWidget {
labelText: LocaleKeys.walkin.tr(), labelText: LocaleKeys.walkin.tr(),
textColor: AppColors.textColor, textColor: AppColors.textColor,
), ),
AppCustomChipWidget( if (getInvoicesListResponseModel.invoiceTypeDescription != null &&
labelText: LocaleKeys.outPatient.tr(context: context), getInvoicesListResponseModel.invoiceTypeDescription!.isNotEmpty) ...[
backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1), AppCustomChipWidget(
textColor: AppColors.warningColorYellow, labelText: getInvoicesListResponseModel.invoiceTypeDescription ?? "",
), backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1),
textColor: AppColors.warningColorYellow,
),
],
], ],
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
@ -212,28 +215,28 @@ class InvoiceListCard extends StatelessWidget {
iconSize: 14.h, iconSize: 14.h,
), ),
), ),
// if (getInvoicesListResponseModel.isAllowRefund == true) ...[ if (getInvoicesListResponseModel.isAllowRefund == true) ...[
// SizedBox(width: 8.w), SizedBox(width: 8.w),
// Expanded( Expanded(
// child: CustomButton( child: CustomButton(
// text: LocaleKeys.requestRefund.tr(context: context), text: LocaleKeys.requestRefund.tr(context: context),
// onPressed: () { onPressed: () {
// _handleRefundInvoiceTap(context); _handleRefundInvoiceTap(context);
// }, },
// icon: AppAssets.refundIcon, icon: AppAssets.refundIcon,
// iconColor: AppColors.caloriesCalculatorColor, iconColor: AppColors.caloriesCalculatorColor,
// backgroundColor: AppColors.blueColor.withValues(alpha: 0.14), backgroundColor: AppColors.blueColor.withValues(alpha: 0.14),
// borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01),
// textColor: AppColors.caloriesCalculatorColor, textColor: AppColors.caloriesCalculatorColor,
// fontSize: 14.f, fontSize: 14.f,
// fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
// borderRadius: 12.r, borderRadius: 12.r,
// padding: EdgeInsets.symmetric(horizontal: 10.w), padding: EdgeInsets.symmetric(horizontal: 10.w),
// height: 40.h, height: 40.h,
// iconSize: 14.h, iconSize: 14.h,
// ), ),
// ), ),
// ] ]
], ],
), ),
], ],

@ -12,7 +12,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/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/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/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/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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/buttons/custom_button.dart';

@ -1,3 +1,6 @@
import 'dart:developer';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.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/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.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/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/input_widget.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.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/radio_list_tile_widget.dart';
@ -61,6 +65,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
_ibanController.addListener(() => _refundRequestViewModel.triggerRebuild()); _ibanController.addListener(() => _refundRequestViewModel.triggerRebuild());
_descriptionController.addListener(() => _refundRequestViewModel.triggerRebuild()); _descriptionController.addListener(() => _refundRequestViewModel.triggerRebuild());
_loadInvoiceDetails(); _loadInvoiceDetails();
// _loadThresholdAmount();
}); });
} }
@ -74,6 +79,20 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
super.dispose(); 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() { void _loadInvoiceDetails() {
final invoiceNo = widget.invoice.invoiceNo; final invoiceNo = widget.invoice.invoiceNo;
final projectId = widget.invoice.projectId; final projectId = widget.invoice.projectId;
@ -90,13 +109,29 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
patientPaidAmount: item.patientPaidAmount ?? 0.0, patientPaidAmount: item.patientPaidAmount ?? 0.0,
apiLineItemNo: item.lineItemNo, apiLineItemNo: item.lineItemNo,
apiProcedureId: item.procedureId, 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(); .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 // Stepper
Widget _buildStepper(RefundRequestViewModel vm) { Widget _buildStepper(RefundRequestViewModel vm) {
@ -123,12 +158,11 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
); );
} }
// Step 1 exactly mirrors RechargeWalletPage hospital row // Step 1 Select Procedures (or whole invoice for Inpatient/Package)
Widget _buildStep1Content(RefundRequestViewModel vm) { Widget _buildStep1Content(RefundRequestViewModel vm) {
return Consumer<RefundRequestViewModel>( return Consumer<RefundRequestViewModel>(
builder: (context, refundVM, _) { builder: (context, refundVM, _) {
final isArabic = _appState.isArabic();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -164,39 +198,41 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Select All row // Select All row - only show for Outpatient/ER invoices
Material( if (_canSelectIndividualProcedures)
color: Colors.transparent, Material(
child: InkWell( color: Colors.transparent,
onTap: () => _refundRequestViewModel.toggleSelectAll(), child: InkWell(
child: Padding( onTap: () => _refundRequestViewModel.toggleSelectAll(),
padding: EdgeInsets.all(16.h), child: Padding(
child: Row( padding: EdgeInsets.all(16.h),
children: [ child: Row(
SizedBox( children: [
width: 24.w, SizedBox(
height: 24.w, width: 24.w,
child: Transform.scale( height: 24.w,
scale: 24.w / 22, child: Transform.scale(
child: Checkbox( scale: 24.w / 22,
value: vm.isAllSelected, child: Checkbox(
onChanged: (_) => _refundRequestViewModel.toggleSelectAll(), value: vm.isAllSelected,
activeColor: AppColors.primaryRedColor, onChanged: (_) => _refundRequestViewModel.toggleSelectAll(),
side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), activeColor: AppColors.primaryRedColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1),
visualDensity: VisualDensity.compact, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.r)), visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.r)),
),
), ),
), ),
), SizedBox(width: 8.w),
SizedBox(width: 8.w), LocaleKeys.selectAll.tr(context: context).toText16(isBold: true),
LocaleKeys.selectAll.tr(context: context).toText16(isBold: true), ],
], ),
), ),
), ),
), ),
), if (_canSelectIndividualProcedures)
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w),
...List.generate( ...List.generate(
vm.procedures.length, vm.procedures.length,
(index) => _buildProcedureCard(vm.procedures[index], index, vm.procedures.length), (index) => _buildProcedureCard(vm.procedures[index], index, vm.procedures.length),
@ -217,6 +253,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
final paymentOptions = [ final paymentOptions = [
RefundTabType.hmgWallet, RefundTabType.hmgWallet,
RefundTabType.invoices, RefundTabType.invoices,
RefundTabType.cash,
]; ];
return Column( return Column(
@ -237,6 +274,9 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final type = paymentOptions[index]; final type = paymentOptions[index];
final isCashOption = type == RefundTabType.cash;
final isCashSelected = vm.selectedPaymentMethod == RefundTabType.cash;
return Column( return Column(
children: [ children: [
RadioListTileWidget<RefundTabType>( RadioListTileWidget<RefundTabType>(
@ -247,6 +287,29 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
if (value != null) vm.setPaymentMethod(value); 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) if (index != paymentOptions.length - 1)
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w),
], ],
@ -254,14 +317,193 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
}, },
), ),
), ),
if (vm.selectedPaymentMethod == RefundTabType.cash && vm.isCashRefundAmountExceeded) ...[
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(16.h),
decoration: BoxDecoration(
color: AppColors.warningColorYellow.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: AppColors.warningColorYellow, width: 1),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline, color: AppColors.warningColorYellow, size: 20.h),
SizedBox(width: 12.w),
Expanded(
child: LocaleKeys.cashRefundLimitExceeded.tr(context: context).toText14(
color: AppColors.textColor,
),
),
],
),
),
],
if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[ if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[
SizedBox(height: 16.h), SizedBox(height: 16.h),
_buildBankInputDetails(vm), _buildBankInputDetails(vm),
], ],
if (vm.selectedPaymentMethod != null) ...[
SizedBox(height: 16.h),
_buildImageUploadSection(vm),
],
], ],
); );
} }
Widget _buildImageUploadSection(RefundRequestViewModel vm) {
return Consumer<RefundRequestViewModel>(
builder: (context, refundVM, _) {
final hasError = refundVM.attachmentError != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Upload button
InkWell(
onTap: () {
if (refundVM.refundAttachmentList.length >= 4) {
refundVM.setAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context));
return;
}
ImageOptions.showImageOptionsNew(
context,
true,
(String base64Image, File file) async {
// Validate file size (2MB max)
final fileSize = await file.length();
if (fileSize > RefundRequestViewModel.maxFileSizeBytes) {
refundVM.setAttachmentError(LocaleKeys.fileSizeTooLarge.tr(context: context));
return;
}
// Check max files limit
if (refundVM.refundAttachmentList.length >= RefundRequestViewModel.maxAttachments) {
refundVM.setAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context));
return;
}
// Add attachment
refundVM.addRefundAttachment(base64Image);
},
title: LocaleKeys.selectAttachment.tr(context: context),
allowMultiple: true, // Enable multiple selection
);
},
child: Container(
padding: EdgeInsets.all(16.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.r),
border: hasError ? Border.all(color: AppColors.primaryRedBorderColor, width: 1) : null,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
height: 40.h,
width: 40.h,
padding: EdgeInsets.all(8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
borderRadius: 12.r,
color: AppColors.greyColor,
),
child: Utils.buildSvgWithAssets(icon: AppAssets.selectImageIcon, applyThemeColor: false),
),
SizedBox(width: 12.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.uploadImage.tr(context: context).toText12(
color: AppColors.inputLabelTextColor,
isBold: true,
),
LocaleKeys.selectImage.tr(context: context).toText14(
color: AppColors.inputLabelTextColor,
isBold: true,
),
],
),
],
),
if (hasError) ...[
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
refundVM.attachmentError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
fontWeight: FontWeight.w600,
),
),
],
),
],
],
),
),
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h),
],
),
),
),
// Selected images list
if (refundVM.refundAttachmentList.isNotEmpty) ...[
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: ListView.separated(
padding: EdgeInsets.all(16.h),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: refundVM.refundAttachmentList.length,
separatorBuilder: (context, index) => SizedBox(height: 12.h),
itemBuilder: (BuildContext context, int index) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.attach_file,
color: Color(0xff2B353E),
size: 20.h,
),
SizedBox(width: 8.w),
"${LocaleKeys.image.tr(context: context)} ${index + 1}".toText14(),
],
),
Utils.buildSvgWithAssets(icon: AppAssets.cancel_circle_icon, width: 20.h, height: 20.h).onPress(() {
refundVM.removeRefundAttachment(index);
}),
],
);
},
),
),
],
],
);
},
);
}
// Bank Transfer Input // Bank Transfer Input
Widget _buildBankInputDetails(RefundRequestViewModel vm) { Widget _buildBankInputDetails(RefundRequestViewModel vm) {
@ -331,11 +573,12 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
); );
} }
// Step 2 procedure checkboxes // Procedure Card
Widget _buildProcedureCard(RefundableProcedure procedure, int index, int length) { Widget _buildProcedureCard(RefundableProcedure procedure, int index, int length) {
final isArabic = _appState.isArabic(); final isArabic = _appState.isArabic();
final name = isArabic && procedure.procedureNameN != null ? procedure.procedureNameN! : procedure.procedureName; final name = isArabic && procedure.procedureNameN != null ? procedure.procedureNameN! : procedure.procedureName;
final canSelect = _canSelectIndividualProcedures;
return AnimationConfiguration.staggeredList( return AnimationConfiguration.staggeredList(
position: index, position: index,
@ -349,81 +592,88 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () => _refundRequestViewModel.toggleProcedureSelection(procedure), // Disable tap for Inpatient/Package invoices
child: Padding( onTap: canSelect ? () => _refundRequestViewModel.toggleProcedureSelection(procedure) : null,
padding: EdgeInsets.all(16.h), child: Opacity(
child: Column( // Reduce opacity for disabled items
crossAxisAlignment: CrossAxisAlignment.start, opacity: canSelect ? 1.0 : 0.7,
children: [ child: Padding(
Row( padding: EdgeInsets.all(16.h),
crossAxisAlignment: CrossAxisAlignment.center, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
SizedBox( children: [
width: 24.w, Row(
height: 24.w, crossAxisAlignment: CrossAxisAlignment.center,
child: Transform.scale( children: [
scale: 24.w / 22, SizedBox(
child: Checkbox( width: 24.w,
value: procedure.isSelected, height: 24.w,
onChanged: (_) => _refundRequestViewModel.toggleProcedureSelection(procedure), child: Transform.scale(
activeColor: AppColors.primaryRedColor, scale: 24.w / 22,
side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), child: Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: procedure.isSelected,
visualDensity: VisualDensity.compact, // Disable checkbox for Inpatient/Package invoices
shape: RoundedRectangleBorder( onChanged: canSelect ? (_) => _refundRequestViewModel.toggleProcedureSelection(procedure) : null,
borderRadius: BorderRadius.circular(4.r), 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),
SizedBox(width: 8.w), Expanded(child: name.toText16(isBold: true)),
Expanded(child: name.toText16(isBold: true)), SizedBox(width: 8.w),
SizedBox(width: 8.w), Utils.getPaymentAmountWithSymbol(
Utils.getPaymentAmountWithSymbol( procedure.patientPaidAmount.toStringAsFixed(2).toText20(
procedure.patientPaidAmount.toStringAsFixed(2).toText20( isBold: true,
isBold: true, isEnglishOnly: true,
isEnglishOnly: true, color: AppColors.textColor,
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),
],
), ),
), AppColors.textColor,
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), 10.f,
], isSaudiCurrency: true,
), ),
), ],
), ),
// Only show refund reason selector for Outpatient/ER invoices
if (canSelect && procedure.isSelected) ...[
InkWell(
onTap: () => showRefundReasonBottomSheet(
context,
selectedReason: procedure.refundReason,
onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason),
),
child: Padding(
padding: EdgeInsets.only(top: 16.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true),
(procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context))
.toText14(color: AppColors.textColor, isBold: true),
],
),
),
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h),
],
),
),
),
],
if (index != length - 1) ...[
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingOnly(top: 16.h),
]
], ],
if (index != length - 1) ...[ ),
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingOnly(top: 16.h),
]
],
), ),
), ),
), ),
@ -491,8 +741,12 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
isDisabled: isStep2 isDisabled: isStep2
? (refundRequestViewModel.selectedPaymentMethod == null || ? (refundRequestViewModel.selectedPaymentMethod == null ||
(refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices && (refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices &&
(_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty))) (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty)) ||
: (refundRequestViewModel.selectedProcedures.isEmpty || !refundRequestViewModel.allSelectedProceduresHaveReasons), (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), ).paddingSymmetrical(24.h, 24.h),
], ],
), ),
@ -523,12 +777,24 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
LoaderBottomSheet.showLoader(); LoaderBottomSheet.showLoader();
final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices; final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices;
final isCash = vm.selectedPaymentMethod == RefundTabType.cash;
final invoice = widget.invoice; final invoice = widget.invoice;
final userPayload = _appState.getAuthenticatedUser(); final userPayload = _appState.getAuthenticatedUser();
final patientId = userPayload!.patientId; final patientId = userPayload!.patientId;
final patientName = "${userPayload.firstName} ${userPayload.lastName}"; final patientName = "${userPayload.firstName} ${userPayload.lastName}";
final patientMobileNumber = "${userPayload.mobileNumber}"; final patientMobileNumber = "${userPayload.mobileNumber}";
final patientNationalId = userPayload.patientIdentificationNo.toString(); 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( final request = SubmitRefundRequestModel(
projectId: invoice.projectId ?? 0, projectId: invoice.projectId ?? 0,
refundType: '1', refundType: '1',
@ -545,10 +811,11 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
refundDoctorName: invoice.doctorName ?? '', refundDoctorName: invoice.doctorName ?? '',
refundClinicId: invoice.clinicId ?? 0, refundClinicId: invoice.clinicId ?? 0,
refundClinicName: invoice.clinicName ?? '', refundClinicName: invoice.clinicName ?? '',
refundDestination: isBankTransfer ? '2' : '1', refundDestination: refundDestination,
refundIban: isBankTransfer ? _ibanController.text.trim() : '', refundIban: isBankTransfer ? _ibanController.text.trim() : '',
refundRemarks: isBankTransfer ? _descriptionController.text.trim() : '', refundRemarks: isBankTransfer ? _descriptionController.text.trim() : '',
refundTotalAmount: vm.totalRefundableAmount, refundTotalAmount: vm.totalRefundableAmount,
refundAttachmentsBase64List: vm.refundAttachmentList,
refundProceduresList: vm.selectedProcedures refundProceduresList: vm.selectedProcedures
.map((p) => SubmitRefundProcedureItem( .map((p) => SubmitRefundProcedureItem(
lineItemNo: p.apiLineItemNo ?? 0, lineItemNo: p.apiLineItemNo ?? 0,
@ -564,7 +831,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
request: request, request: request,
onSuccess: (response) { onSuccess: (response) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
_showSuccessBottomSheet(response.refNo ?? ''); _showSuccessBottomSheet(response.refNo ?? '', refundDestination);
}, },
onError: (err) { onError: (err) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
@ -583,7 +850,11 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
); );
} }
void _showSuccessBottomSheet(String refNo) { void _showSuccessBottomSheet(String refNo, String refundDestination) {
// refundDestination: '1' = HMG Wallet, '2' = Bank Transfer, '3' = Cash
final isCashRefund = refundDestination == '3';
final isPackageInvoice = widget.invoice.isPackageInvoice == true;
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
child: Padding( child: Padding(
@ -604,6 +875,16 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
], ],
).paddingSymmetrical(24.w, 0), ).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), SizedBox(height: 24.h),
Row( Row(
children: [ children: [

@ -186,8 +186,28 @@ class RefundRequestDetailScreen extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
LocaleKeys.procedure.tr(context: context).toText12(color: AppColors.textColor, isBold: true), Expanded(
LocaleKeys.amountOnly.tr(context: context).toText12(color: AppColors.textColor, isBold: true), 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), SizedBox(height: 16.h),
@ -195,6 +215,22 @@ class RefundRequestDetailScreen extends StatelessWidget {
SizedBox(height: 16.h), SizedBox(height: 16.h),
if (invoicePayload.proceduresList != null && invoicePayload.proceduresList!.isNotEmpty) if (invoicePayload.proceduresList != null && invoicePayload.proceduresList!.isNotEmpty)
...invoicePayload.proceduresList!.map((p) => _buildProcedureRow(context, p)), ...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) ...[ if (isWalletType && walletPayload != null) ...[
Row( Row(
@ -213,6 +249,22 @@ class RefundRequestDetailScreen extends StatelessWidget {
SizedBox(height: 16.h), SizedBox(height: 16.h),
if (walletPayload.advancesList != null && walletPayload.advancesList!.isNotEmpty) if (walletPayload.advancesList != null && walletPayload.advancesList!.isNotEmpty)
...walletPayload.advancesList!.map((a) => _buildAdvanceRow(context, a)), ...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) { Widget _buildProcedureRow(BuildContext context, InvoiceRefundProcedure procedure) {
final isApproved = procedure.isApproved ?? false;
final hasRejectionReason = procedure.rejectionReason != null && procedure.rejectionReason!.isNotEmpty;
return Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 6.h), padding: EdgeInsets.symmetric(vertical: 6.h),
child: Row( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Row(
flex: 3, crossAxisAlignment: CrossAxisAlignment.start,
child: (procedure.procedureName ?? '-').toText12(color: AppColors.textColor), 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), if (hasRejectionReason) ...[
(procedure.patientPaidAmount?.toString() ?? '-').toText12(color: AppColors.textColor, isBold: true), Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 10.h),
"${LocaleKeys.procedureRejectionReason.tr(context: context)}:".toText10(
color: AppColors.greyInfoTextColor,
isBold: true,
),
Row(
children: [
Expanded(
child: procedure.rejectionReason!.toText10(
color: AppColors.greyInfoTextColor,
),
),
],
),
],
),
],
], ],
), ),
); );

@ -1,3 +1,4 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:hmg_patient_app_new/core/exceptions/api_exception.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_exception.dart';

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';

@ -1,4 +1,5 @@
import 'package:easy_localization/easy_localization.dart'; 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_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.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/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/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:flutter/material.dart';
class AttachmentOptions extends StatelessWidget { class AttachmentOptions extends StatelessWidget {
final Future<void> Function() onCameraTap; final Future<void> Function() onCameraTap;
@ -16,13 +16,8 @@ class AttachmentOptions extends StatelessWidget {
final VoidCallback onFilesTap; final VoidCallback onFilesTap;
final bool showFilesOption; final bool showFilesOption;
const AttachmentOptions({ const AttachmentOptions({Key? key, required this.onCameraTap, required this.onGalleryTap, required this.onFilesTap, this.showFilesOption = true})
Key? key, : super(key: key);
required this.onCameraTap,
required this.onGalleryTap,
required this.onFilesTap,
this.showFilesOption = true
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -52,7 +47,7 @@ class AttachmentOptions extends StatelessWidget {
if (showFilesOption) if (showFilesOption)
checkInOptionCard( checkInOptionCard(
appState, appState,
AppAssets.file_icon, AppAssets.openFileIcon,
LocaleKeys.openFiles.tr(context: context), LocaleKeys.openFiles.tr(context: context),
LocaleKeys.selectDocumentFiles.tr(context: context), LocaleKeys.selectDocumentFiles.tr(context: context),
).onPress(() async { ).onPress(() async {

@ -22,6 +22,7 @@ class ImageOptions {
bool showFilesOption, bool showFilesOption,
Function(String, File) image, { Function(String, File) image, {
String? title, String? title,
bool allowMultiple = false, // Allow multiple file/image selection
Future<bool> Function()? checkCameraPermission, Future<bool> Function()? checkCameraPermission,
Future<bool> Function()? checkGalleryPermission, Future<bool> Function()? checkGalleryPermission,
}) { }) {
@ -51,7 +52,11 @@ class ImageOptions {
String fileName = _image.path; String fileName = _image.path;
var bytes = File(fileName).readAsBytesSync(); var bytes = File(fileName).readAsBytesSync();
String base64Encode = base64.encode(bytes); 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 { onGalleryTap: () async {
@ -66,21 +71,46 @@ class ImageOptions {
Navigator.pop(context); // Close bottom sheet Navigator.pop(context); // Close bottom sheet
if (Platform.isAndroid) { if (Platform.isAndroid) {
galleryImageAndroid(image); galleryImageAndroid(image, allowMultiple: allowMultiple);
} else { } else {
XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery, imageQuality: 20); if (allowMultiple) {
if (pickedFile == null) return; // iOS/macOS: Pick multiple images
List<XFile> pickedFiles = await picker.pickMultiImage(imageQuality: 20);
if (pickedFiles.isEmpty) return;
File _image = File(pickedFile.path); for (var pickedFile in pickedFiles) {
String fileName = _image.path; File _image = File(pickedFile.path);
var bytes = File(fileName).readAsBytesSync(); var bytes = await _image.readAsBytes();
String base64Encode = base64.encode(bytes); String base64Encode = base64.encode(bytes);
image(base64Encode, _image);
// 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 { onFilesTap: () async {
Navigator.pop(context); // Close bottom sheet
FilePickerResult? result = await FilePicker.platform.pickFiles( FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom, type: FileType.custom,
allowMultiple: allowMultiple, // Use the parameter
allowedExtensions: [ allowedExtensions: [
'jpg', 'jpg',
'jpeg', 'jpeg',
@ -95,9 +125,22 @@ class ImageOptions {
'zip', 'zip',
], ],
); );
if (result == null) return;
List<File> files = result.paths.map((path) => File(path!)).toList(); if (result == null || result.files.isEmpty) return;
image(result.files.first.path.toString(), files.first);
// 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: () {}, 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 // Enable Android Photo Picker if available
if (_pickerPlatform is ImagePickerAndroid) { if (_pickerPlatform is ImagePickerAndroid) {
(_pickerPlatform as ImagePickerAndroid).useAndroidPhotoPicker = true; (_pickerPlatform as ImagePickerAndroid).useAndroidPhotoPicker = true;
} }
try { try {
final XFile? pickedFile = if (allowMultiple) {
await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20)); // Pick multiple images
final List<XFile> pickedFiles = await picker.pickMultiImage(imageQuality: 20);
if (pickedFile == null) return; if (pickedFiles.isEmpty) return;
File _image = File(pickedFile.path); for (var pickedFile in pickedFiles) {
String fileName = _image.path; File _image = File(pickedFile.path);
var bytes = File(fileName).readAsBytesSync(); var bytes = await _image.readAsBytes();
String base64Encode = base64.encode(bytes); String base64Encode = base64.encode(bytes);
image(base64Encode, _image);
// 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) { } catch (e) {
print('Error picking image: $e'); print('Error picking image: $e');
} }
@ -257,12 +323,55 @@ void cameraImageAndroid(Function(String, File) image) async {
String fileName = _image.path; String fileName = _image.path;
var bytes = File(fileName).readAsBytesSync(); var bytes = File(fileName).readAsBytesSync();
String base64Encode = base64.encode(bytes); 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) { } catch (e) {
print('Error taking picture: $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 { class _BottomSheet extends StatelessWidget {
final List<Widget> children; final List<Widget> children;

@ -499,6 +499,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_cache_manager:
dependency: transitive dependency: transitive
description: description:
@ -1641,6 +1649,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" 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: scrollable_positioned_list:
dependency: "direct main" dependency: "direct main"
description: description:

Loading…
Cancel
Save