From 3d4609a1a9122b8fabcc1536dfb2c26153f8c707 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Fri, 12 Jun 2026 12:54:08 +0300 Subject: [PATCH] completed refund CR --- assets/images/svg/open_file_icon.svg | 4 + assets/langs/ar-SA.json | 10 +- assets/langs/en-US.json | 10 +- lib/core/app_assets.dart | 1 + .../habib_wallet/habib_wallet_repo.dart | 38 ++++ .../habib_wallet/habib_wallet_view_model.dart | 57 ++++- .../submit_refund_request_model.dart | 5 + .../refund_request_view_model.dart | 47 +++- lib/generated/locale_keys.g.dart | 9 +- .../contact_us/feedback_page.dart | 3 +- .../widget/e_referral_other_details.dart | 1 + .../habib_wallet/habib_wallet_page.dart | 39 +++- .../withdraw_request_create_page.dart | 145 +++++++++--- .../refund_request/refund_history_screen.dart | 2 +- .../refund_request_create_page.dart | 210 +++++++++++++++++- .../refund_request_detail_screen.dart | 4 + lib/widgets/attachment_options.dart | 13 +- lib/widgets/image_picker.dart | 152 +++++++++++-- 18 files changed, 677 insertions(+), 73 deletions(-) create mode 100644 assets/images/svg/open_file_icon.svg diff --git a/assets/images/svg/open_file_icon.svg b/assets/images/svg/open_file_icon.svg new file mode 100644 index 00000000..91d64fe5 --- /dev/null +++ b/assets/images/svg/open_file_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 2c312334..9cfa8c1a 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1870,6 +1870,7 @@ "selectRefundMethod": "اختر طريقة الاسترداد", "hmgWallet": "محفظة HMG", "bankTransfer": "تحويل بنكي", + "cash": "نقدي", "selectRefundReason": "اختر سبب الاسترداد", "refundReason": "سبب الاسترداد", "done": "تم", @@ -1898,5 +1899,12 @@ "invoiceHistory": "سجل الفواتير", "selectAdvance": "اختر الدفعة المقدمة", "advanceNo": "رقم الدفعة", - "noAdvancesAvailable": "لا توجد دفعات متاحة للاسترداد" + "noAdvancesAvailable": "لا توجد دفعات متاحة للاسترداد", + "maximumFilesReached": "الحد الأقصى 4 ملفات مسموح به", + "fileSizeTooLarge": "يجب ألا يتجاوز حجم الملف 2 ميغابايت", + "image": "صورة", + "refundCannotBeProcessedPatientAdmitted": "لا يمكن معالجة الاسترداد أثناء إقامة المريض", + "cashRefundLimitExceeded": "يمكنك فقط التقدم بطلب استرداد نقدي إذا كان المبلغ أقل من 5000. لمزيد من المعلومات يرجى زيارتنا.", + "cashRefundInstructions": "بمجرد الموافقة على الطلب، يرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد. عند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.", + "cashRefundApprovalInstructions": "بمجرد الموافقة على الطلب،\nيرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد.\nعند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح." } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 2fe76d05..f367cfe5 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1862,6 +1862,7 @@ "selectRefundMethod": "Select Refund Method", "hmgWallet": "HMG Wallet", "bankTransfer": "Bank Transfer", + "cash": "Cash", "selectRefundReason": "Select Refund Reason", "refundReason": "Refund Reason", "done": "Done", @@ -1889,7 +1890,14 @@ "invoiceHistory": "Invoice History", "selectAdvance": "Select Advance", "advanceNo": "Advance No", - "noAdvancesAvailable": "No advances available for refund" + "noAdvancesAvailable": "No advances available for refund", + "maximumFilesReached": "Maximum 4 files allowed", + "fileSizeTooLarge": "File size must not exceed 2 MB", + "image": "Image", + "refundCannotBeProcessedPatientAdmitted": "Refund cannot be processed while the patient is admitted", + "cashRefundLimitExceeded": "You can only apply for cash refund if the amount is less than 5000. For more information please visit us.", + "cashRefundInstructions": "Once the request is approved, Please visit HMG Hospital or HMG Primary Care cashier to receive cash. At the cashier counter, you will receive an OTP on your registered mobile number. Provide the OTP to the cashier for validation. Cash will be released only after successful validation.", + "cashRefundApprovalInstructions": "Once the request is approved,\nPlease visit HMG Hospital or HMG Primary Care cashier to receive cash.\nAt the cashier counter, you will receive an OTP on your registered mobile number. Provide the OTP to the cashier for validation. Cash will be released only after successful validation." } diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 37f9c4fd..31c2473c 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -377,6 +377,7 @@ class AppAssets { static const String invoiceListIcon = '$svgBasePath/invoice_list_icon.svg'; static const String open_camera = '$svgBasePath/open_camera.svg'; static const String gallery = '$svgBasePath/gallery.svg'; + static const String openFileIcon = '$svgBasePath/open_file_icon.svg'; static const String files = '$svgBasePath/files.svg'; static const String online_checkin_icon = '$svgBasePath/online_checkin_icon.svg'; static const String hmg_pharmacy_logo = '$svgBasePath/hmg_pharmacy_logo.svg'; diff --git a/lib/features/habib_wallet/habib_wallet_repo.dart b/lib/features/habib_wallet/habib_wallet_repo.dart index ff255092..b5f8d30c 100644 --- a/lib/features/habib_wallet/habib_wallet_repo.dart +++ b/lib/features/habib_wallet/habib_wallet_repo.dart @@ -22,6 +22,8 @@ abstract class HabibWalletRepo { Future>> getLakumAccountInformation({required String identificationNumber}); Future>> getLakumAccountInquiryInformation({required String lakumAccountNumber}); + + Future>> checkIsPatientAdmittedOrAdmissionRequest({required bool isActiveAppointment}); } class HabibWalletRepoImp implements HabibWalletRepo { @@ -306,4 +308,40 @@ class HabibWalletRepoImp implements HabibWalletRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> checkIsPatientAdmittedOrAdmissionRequest({required bool isActiveAppointment}) async { + Map requestBody = { + "IsActiveAppointment": isActiveAppointment, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + CHECK_IF_PATIENT_ADMITTED, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index ce18bf09..44c0763c 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/features/refund_request/models/req_models/su import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; @@ -42,6 +43,8 @@ class HabibWalletViewModel extends ChangeNotifier { // ── Wallet Withdrawal Step Management ───────────────────────────────────── int currentWithdrawStep = 0; RefundableAdvancesItem? selectedAdvance; + RefundTabType? selectedWithdrawMethod; // Bank Transfer or Cash + static const double cashRefundMaxAmount = 5000.0; // Maximum amount for cash refund void nextWithdrawStep() { if (currentWithdrawStep < 1) { @@ -62,8 +65,16 @@ class HabibWalletViewModel extends ChangeNotifier { notifyListeners(); } + void setSelectedWithdrawMethod(RefundTabType? method) { + selectedWithdrawMethod = method; + notifyListeners(); + } + double get selectedAdvanceBalance => selectedAdvance?.balanceAmount ?? 0.0; + // Check if selected advance balance exceeds cash limit + bool get isCashWithdrawAmountExceeded => selectedAdvanceBalance > cashRefundMaxAmount; + bool isSearchedFileNumberDataShown = false; int currentIndex = 0; @@ -280,6 +291,7 @@ class HabibWalletViewModel extends ChangeNotifier { required String patientNationalId, required int projectId, required String projectName, + required String refundDestination, // '2' for Bank Transfer, '3' for Cash Function(SubmitRefundResponseModel)? onSuccess, Function(String)? onError, }) async { @@ -316,8 +328,8 @@ class HabibWalletViewModel extends ChangeNotifier { refundDoctorName: '', refundClinicId: 0, refundClinicName: '', - refundDestination: '2', - // only bank transfer + refundDestination: refundDestination, + // Use provided refund destination refundIban: iban, refundRemarks: remarks, refundTotalAmount: selectedAdvanceBalance, @@ -604,4 +616,45 @@ class HabibWalletViewModel extends ChangeNotifier { }, ); } + + // ── Check Is Patient Admitted Or Admission Request ──────────────────────── + bool isCheckingPatientAdmitted = false; + + Future checkIsPatientAdmittedOrAdmissionRequest({ + required bool isActiveAppointment, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isCheckingPatientAdmitted = true; + notifyListeners(); + + final result = await habibWalletRepo.checkIsPatientAdmittedOrAdmissionRequest( + isActiveAppointment: isActiveAppointment, + ); + + result.fold( + (failure) { + isCheckingPatientAdmitted = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isCheckingPatientAdmitted = false; + notifyListeners(); + if (apiResponse.messageStatus == 1) { + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } else { + if (onError != null) { + onError(apiResponse.errorMessage ?? LocaleKeys.failed.tr()); + } + } + }, + ); + } } diff --git a/lib/features/refund_request/models/req_models/submit_refund_request_model.dart b/lib/features/refund_request/models/req_models/submit_refund_request_model.dart index 93eaadca..ae8ac20e 100644 --- a/lib/features/refund_request/models/req_models/submit_refund_request_model.dart +++ b/lib/features/refund_request/models/req_models/submit_refund_request_model.dart @@ -79,6 +79,9 @@ class SubmitRefundRequestModel { /// Populated only for wallet withdrawal refunds (refundType == '2'). final List refundAdvancesList; + /// List of base64 encoded images for refund attachments (max 4 files, each max 2MB) + final List refundAttachmentsBase64List; + const SubmitRefundRequestModel({ required this.projectId, required this.refundType, @@ -101,6 +104,7 @@ class SubmitRefundRequestModel { required this.isRefundFastTrack, required this.refundProceduresList, this.refundAdvancesList = const [], + this.refundAttachmentsBase64List = const [], }); /// refundType '1' = invoice refund → sends Refund_ProceduresList @@ -122,6 +126,7 @@ class SubmitRefundRequestModel { "Refund_Remarks": refundRemarks, "Refund_TotalRefundAmount": refundTotalAmount, "Refund_IsFastTrack": isRefundFastTrack, + "Refund_AttachmentsBase64List": refundAttachmentsBase64List, }; if (refundType == '2') { diff --git a/lib/features/refund_request/refund_request_view_model.dart b/lib/features/refund_request/refund_request_view_model.dart index d539d97f..1b77c007 100644 --- a/lib/features/refund_request/refund_request_view_model.dart +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -11,7 +11,7 @@ import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo. import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; -enum RefundTabType { invoices, hmgWallet } +enum RefundTabType { invoices, hmgWallet, cash } extension RefundTabTypeExtension on RefundTabType { String toDisplayString() { @@ -20,6 +20,8 @@ extension RefundTabTypeExtension on RefundTabType { return LocaleKeys.hmgWallet.tr(); case RefundTabType.invoices: return LocaleKeys.bankTransfer.tr(); + case RefundTabType.cash: + return LocaleKeys.cash.tr(); } } } @@ -117,6 +119,8 @@ class RefundRequestViewModel extends ChangeNotifier { invoiceDetails = null; _ibanError = null; _descriptionError = null; + refundAttachmentList.clear(); + _attachmentError = null; // Use Future.microtask to avoid calling notifyListeners while tree is locked (e.g. during dispose) Future.microtask(() { if (hasListeners) notifyListeners(); @@ -305,6 +309,47 @@ class RefundRequestViewModel extends ChangeNotifier { return isValid; } + // ── Attachments ─────────────────────────────────────────────────────────── + List refundAttachmentList = []; // Stores base64 strings + String? _attachmentError; + static const int maxAttachments = 4; + static const int maxFileSizeBytes = 2 * 1024 * 1024; // 2MB + static const double cashRefundMaxAmount = 5000.0; // Maximum amount for cash refund + + String? get attachmentError => _attachmentError; + + // Check if total refundable amount exceeds cash limit + bool get isCashRefundAmountExceeded => totalRefundableAmount > cashRefundMaxAmount; + + void addRefundAttachment(String base64String) { + if (refundAttachmentList.length >= maxAttachments) { + _attachmentError = LocaleKeys.maximumFilesReached.tr(); + notifyListeners(); + return; + } + refundAttachmentList.add(base64String); + _attachmentError = null; + notifyListeners(); + } + + void removeRefundAttachment(int index) { + if (index >= 0 && index < refundAttachmentList.length) { + refundAttachmentList.removeAt(index); + _attachmentError = null; + notifyListeners(); + } + } + + void clearAttachmentError() { + _attachmentError = null; + notifyListeners(); + } + + void setAttachmentError(String error) { + _attachmentError = error; + notifyListeners(); + } + // ── Payment Method ──────────────────────────────────────────────────────── RefundTabType? selectedPaymentMethod; diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 1cf39e81..f3341345 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1875,6 +1875,7 @@ abstract class LocaleKeys { static const cannotOpenThisFile = 'cannotOpenThisFile'; static const thisInvoiceIsNotEligibleForRefund = 'thisInvoiceIsNotEligibleForRefund'; static const refundDetails = 'refundDetails'; + static const refund = 'refund'; static const requestNo = 'requestNo'; static const referenceNo = 'referenceNo'; static const advanceNumber = 'advanceNumber'; @@ -1883,6 +1884,12 @@ abstract class LocaleKeys { static const selectAdvance = 'selectAdvance'; static const advanceNo = 'advanceNo'; static const noAdvancesAvailable = 'noAdvancesAvailable'; - static const refund = 'refund'; + static const maximumFilesReached = 'maximumFilesReached'; + static const fileSizeTooLarge = 'fileSizeTooLarge'; + static const image = 'image'; + static const refundCannotBeProcessedPatientAdmitted = 'refundCannotBeProcessedPatientAdmitted'; + static const cashRefundLimitExceeded = 'cashRefundLimitExceeded'; + static const cashRefundInstructions = 'cashRefundInstructions'; + static const cashRefundApprovalInstructions = 'cashRefundApprovalInstructions'; } diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index 00576e9d..743acedf 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -354,8 +354,7 @@ class FeedbackPage extends StatelessWidget { context, true, (String image, file) { - print(image); - print(file); + // Navigator.pop(context); contactUsViewModel.addFeedbackAttachment(image); }, diff --git a/lib/presentation/e_referral/widget/e_referral_other_details.dart b/lib/presentation/e_referral/widget/e_referral_other_details.dart index 043dda21..64dc3730 100644 --- a/lib/presentation/e_referral/widget/e_referral_other_details.dart +++ b/lib/presentation/e_referral/widget/e_referral_other_details.dart @@ -217,6 +217,7 @@ class _OtherDetailsStepState extends State { (String image, File file) { _addInsuranceDocument(image, file, formManager); }, + allowMultiple: true, // Enable multiple selection ); }, ), diff --git a/lib/presentation/habib_wallet/habib_wallet_page.dart b/lib/presentation/habib_wallet/habib_wallet_page.dart index 4f056dad..5463d94e 100644 --- a/lib/presentation/habib_wallet/habib_wallet_page.dart +++ b/lib/presentation/habib_wallet/habib_wallet_page.dart @@ -150,7 +150,42 @@ class _HabibWalletState extends State { borderColor: Colors.transparent, padding: EdgeInsets.fromLTRB(4, 0, 12, 0), fontSize: 14.f, - onPressed: () => Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())), + onPressed: () async { + // Show loader + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); + + // Check if patient is admitted + await habibWalletVM.checkIsPatientAdmittedOrAdmissionRequest( + isActiveAppointment: false, + onSuccess: (data) { + LoaderBottomSheet.hideLoader(); + + // Check if patient is admitted + bool isAdmitted = data['isAdmitted'] ?? false; + + if (isAdmitted) { + // Show error message if patient is admitted + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget( + loadingText: LocaleKeys.refundCannotBeProcessedPatientAdmitted.tr(context: context), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else { + // Navigate to withdraw page if not admitted + Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())); + } + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + // Navigate to withdraw page even on error (messageStatus == 2) + Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())); + }, + ); + }, ), ), ], @@ -307,5 +342,3 @@ class _HabibWalletState extends State { ); } } - - diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index a5909e3a..6b8bb6f9 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart'; import 'package:hmg_patient_app_new/presentation/refund_request/refund_history_screen.dart'; @@ -21,6 +22,7 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/stepper/stepper_widget.dart'; import 'package:provider/provider.dart'; @@ -225,14 +227,74 @@ class _WithdrawRequestCreatePageState extends State { } } - // ── Step 2 — Bank Details ──────────────────────────────────────────────── + // ── Step 2 — Refund Method Selection ───────────────────────────────────── - bool get _isFormValid { - final vm = _habibWalletViewModel; - return vm.selectedHospital != null && - !vm.isAdvancesLoading && - _bankDetailsController.text.trim().isNotEmpty && - _descriptionController.text.trim().isNotEmpty; + Widget _buildRefundMethodSelection(HabibWalletViewModel vm) { + final paymentOptions = [ + RefundTabType.invoices, // Bank Transfer + RefundTabType.cash, // Cash + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.selectRefundMethod.tr(context: context).toText20(isBold: true), + SizedBox(height: 24.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: paymentOptions.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final type = paymentOptions[index]; + return Column( + children: [ + RadioListTileWidget( + value: type, + groupValue: vm.selectedWithdrawMethod, + title: type.toDisplayString(), + onChanged: (value) { + if (value != null) vm.setSelectedWithdrawMethod(value); + }, + ), + if (index != paymentOptions.length - 1) + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), + ], + ); + }, + ), + ), + if (vm.selectedWithdrawMethod == RefundTabType.cash && vm.isCashWithdrawAmountExceeded) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.warningColorYellow.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: AppColors.warningColorYellow, width: 1), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: AppColors.warningColorYellow, size: 20.h), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.cashRefundLimitExceeded.tr(context: context).toText14( + color: AppColors.textColor, + ), + ), + ], + ), + ), + ], + ], + ); } // ── Bottom bar ──────────────────────────────────────────────────────────── @@ -245,9 +307,18 @@ class _WithdrawRequestCreatePageState extends State { // Step 1 validation: advance selected final isStep1Valid = vm.selectedAdvance != null; - // Step 2 validation: bank details filled - final isStep2Valid = _bankDetailsController.text.trim().isNotEmpty && - _descriptionController.text.trim().isNotEmpty; + // Step 2 validation: + // - Refund method must be selected + // - If Bank Transfer selected, IBAN and description required + // - If Cash selected, amount must be <= 5000 SAR + final isBankTransfer = vm.selectedWithdrawMethod == RefundTabType.invoices; + final isCash = vm.selectedWithdrawMethod == RefundTabType.cash; + final isStep2Valid = vm.selectedWithdrawMethod != null && + (isCash + ? !vm.isCashWithdrawAmountExceeded // Cash: check amount limit + : (isBankTransfer && + _bankDetailsController.text.trim().isNotEmpty && + _descriptionController.text.trim().isNotEmpty)); // Bank: check fields return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -280,9 +351,7 @@ class _WithdrawRequestCreatePageState extends State { ), ], CustomButton( - text: isStep2 - ? LocaleKeys.submitRequest.tr(context: context) - : LocaleKeys.next.tr(context: context), + text: isStep2 ? LocaleKeys.submitRequest.tr(context: context) : LocaleKeys.next.tr(context: context), isDisabled: isStep2 ? !isStep2Valid : !isStep1Valid, onPressed: () { if (!isStep2) { @@ -291,14 +360,16 @@ class _WithdrawRequestCreatePageState extends State { vm.nextWithdrawStep(); } } else { - // Submit - final isFormValid = vm.validateWithdrawForm( - iban: _bankDetailsController.text, - description: _descriptionController.text, - ); - if (isFormValid) { - _handleWalletWithdrawalSubmit(vm); + // Submit - only validate bank details if Bank Transfer is selected + final isBankTransfer = vm.selectedWithdrawMethod == RefundTabType.invoices; + if (isBankTransfer) { + final isFormValid = vm.validateWithdrawForm( + iban: _bankDetailsController.text, + description: _descriptionController.text, + ); + if (!isFormValid) return; } + _handleWalletWithdrawalSubmit(vm); } }, ).paddingSymmetrical(24.h, 24.h), @@ -311,8 +382,7 @@ class _WithdrawRequestCreatePageState extends State { Widget _buildProjectSelectionAndBalance(HabibWalletViewModel habibWalletViewModel) { final isArabic = _appState.isArabic(); - final showAmount = habibWalletViewModel.currentWithdrawStep == 1 && - habibWalletViewModel.selectedAdvance != null; + final showAmount = habibWalletViewModel.currentWithdrawStep == 1 && habibWalletViewModel.selectedAdvance != null; return Container( padding: EdgeInsets.all(16.h), @@ -472,6 +542,11 @@ class _WithdrawRequestCreatePageState extends State { LoaderBottomSheet.showLoader(); final user = _appState.getAuthenticatedUser(); final hospital = vm.selectedHospital; + + // Determine refund destination: '2' = Bank Transfer, '3' = Cash + final isCash = vm.selectedWithdrawMethod == RefundTabType.cash; + final refundDestination = isCash ? '3' : '2'; + await vm.submitWalletWithdrawalRequest( iban: _bankDetailsController.text.trim(), remarks: _descriptionController.text.trim(), @@ -481,9 +556,10 @@ class _WithdrawRequestCreatePageState extends State { patientNationalId: user?.patientIdentificationNo?.toString() ?? '', projectId: hospital?.mainProjectID ?? 0, projectName: hospital?.name ?? '', + refundDestination: refundDestination, onSuccess: (response) { LoaderBottomSheet.hideLoader(); - _showWithdrawalSuccessBottomSheet(response.refNo ?? ''); + _showWithdrawalSuccessBottomSheet(response.refNo ?? '', refundDestination); }, onError: (err) { LoaderBottomSheet.hideLoader(); @@ -502,7 +578,10 @@ class _WithdrawRequestCreatePageState extends State { ); } - void _showWithdrawalSuccessBottomSheet(String refNo) { + void _showWithdrawalSuccessBottomSheet(String refNo, String refundDestination) { + // refundDestination: '2' = Bank Transfer, '3' = Cash + final isCashRefund = refundDestination == '3'; + showCommonBottomSheetWithoutHeight( context, child: Padding( @@ -521,7 +600,11 @@ class _WithdrawRequestCreatePageState extends State { SizedBox(width: 6.w), refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], - ).paddingSymmetrical(24.w, 0), + ).paddingSymmetrical(16.w, 0), + ], + if (isCashRefund) ...[ + SizedBox(height: 16.h), + LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText14(color: AppColors.textColor).paddingSymmetrical(16.w, 0), ], SizedBox(height: 24.h), Row( @@ -627,7 +710,16 @@ class _WithdrawRequestCreatePageState extends State { key: const ValueKey('step2'), child: Padding( padding: EdgeInsets.symmetric(horizontal: 24.w), - child: _buildBankInputDetails(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildRefundMethodSelection(habibWalletViewModel), + if (habibWalletViewModel.selectedWithdrawMethod == RefundTabType.invoices) ...[ + SizedBox(height: 16.h), + _buildBankInputDetails(), + ], + ], + ), ), ), ), @@ -644,4 +736,3 @@ class _WithdrawRequestCreatePageState extends State { ); } } - diff --git a/lib/presentation/refund_request/refund_history_screen.dart b/lib/presentation/refund_request/refund_history_screen.dart index 630c5dc0..f689ed4a 100644 --- a/lib/presentation/refund_request/refund_history_screen.dart +++ b/lib/presentation/refund_request/refund_history_screen.dart @@ -12,7 +12,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart' show RefundRequestDetailScreen; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index a795cf16..afef99ae 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -20,6 +22,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/image_picker.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; @@ -240,6 +243,7 @@ class _RefundRequestCreatePageState extends State { final paymentOptions = [ RefundTabType.hmgWallet, RefundTabType.invoices, + RefundTabType.cash, ]; return Column( @@ -277,14 +281,191 @@ class _RefundRequestCreatePageState extends State { }, ), ), + if (vm.selectedPaymentMethod == RefundTabType.cash && vm.isCashRefundAmountExceeded) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.warningColorYellow.withOpacity(0.1), + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: AppColors.warningColorYellow, width: 1), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: AppColors.warningColorYellow, size: 20.h), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.cashRefundLimitExceeded.tr(context: context).toText14( + color: AppColors.textColor, + ), + ), + ], + ), + ), + ], if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[ SizedBox(height: 16.h), _buildBankInputDetails(vm), + SizedBox(height: 16.h), + _buildImageUploadSection(vm), ], ], ); } + Widget _buildImageUploadSection(RefundRequestViewModel vm) { + return Consumer( + builder: (context, refundVM, _) { + final hasError = refundVM.attachmentError != null; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Upload button + InkWell( + onTap: () { + if (refundVM.refundAttachmentList.length >= 4) { + refundVM.setAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context)); + return; + } + ImageOptions.showImageOptionsNew( + context, + true, + (String base64Image, File file) async { + // Validate file size (2MB max) + final fileSize = await file.length(); + if (fileSize > RefundRequestViewModel.maxFileSizeBytes) { + refundVM.setAttachmentError(LocaleKeys.fileSizeTooLarge.tr(context: context)); + return; + } + + // Check max files limit + if (refundVM.refundAttachmentList.length >= RefundRequestViewModel.maxAttachments) { + refundVM.setAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context)); + return; + } + + // Add attachment + refundVM.addRefundAttachment(base64Image); + }, + title: LocaleKeys.selectAttachment.tr(context: context), + allowMultiple: true, // Enable multiple selection + ); + }, + child: Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: hasError ? Border.all(color: AppColors.primaryRedBorderColor, width: 1) : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + height: 40.h, + width: 40.h, + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.r, + color: AppColors.greyColor, + ), + child: Utils.buildSvgWithAssets(icon: AppAssets.selectImageIcon, applyThemeColor: false), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.uploadImage.tr(context: context).toText12( + color: AppColors.inputLabelTextColor, + isBold: true, + ), + LocaleKeys.selectImage.tr(context: context).toText14( + color: AppColors.inputLabelTextColor, + isBold: true, + ), + ], + ), + ], + ), + if (hasError) ...[ + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + refundVM.attachmentError!, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ), + ), + ), + // Selected images list + if (refundVM.refundAttachmentList.isNotEmpty) ...[ + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: ListView.separated( + padding: EdgeInsets.all(16.h), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: refundVM.refundAttachmentList.length, + separatorBuilder: (context, index) => SizedBox(height: 12.h), + itemBuilder: (BuildContext context, int index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.attach_file, + color: Color(0xff2B353E), + size: 20.h, + ), + SizedBox(width: 8.w), + "${LocaleKeys.image.tr(context: context)} ${index + 1}".toText14(), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.cancel_circle_icon, width: 20.h, height: 20.h).onPress(() { + refundVM.removeRefundAttachment(index); + }), + ], + ); + }, + ), + ), + ], + ], + ); + }, + ); + } + // ── Bank Transfer Input ─────────────────────────────────────────────────── Widget _buildBankInputDetails(RefundRequestViewModel vm) { @@ -522,7 +703,8 @@ class _RefundRequestCreatePageState extends State { isDisabled: isStep2 ? (refundRequestViewModel.selectedPaymentMethod == null || (refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices && - (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty))) + (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty)) || + (refundRequestViewModel.selectedPaymentMethod == RefundTabType.cash && refundRequestViewModel.isCashRefundAmountExceeded)) : (refundRequestViewModel.selectedProcedures.isEmpty || // For Outpatient/ER: require refund reasons // For Inpatient/Package: skip refund reason validation @@ -557,12 +739,24 @@ class _RefundRequestCreatePageState extends State { LoaderBottomSheet.showLoader(); final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices; + final isCash = vm.selectedPaymentMethod == RefundTabType.cash; final invoice = widget.invoice; final userPayload = _appState.getAuthenticatedUser(); final patientId = userPayload!.patientId; final patientName = "${userPayload.firstName} ${userPayload.lastName}"; final patientMobileNumber = "${userPayload.mobileNumber}"; final patientNationalId = userPayload.patientIdentificationNo.toString(); + + // Determine refund destination: 1 = HMG Wallet, 2 = Bank Transfer, 3 = Cash + String refundDestination; + if (isCash) { + refundDestination = '3'; + } else if (isBankTransfer) { + refundDestination = '2'; + } else { + refundDestination = '1'; // HMG Wallet + } + final request = SubmitRefundRequestModel( projectId: invoice.projectId ?? 0, refundType: '1', @@ -579,10 +773,11 @@ class _RefundRequestCreatePageState extends State { refundDoctorName: invoice.doctorName ?? '', refundClinicId: invoice.clinicId ?? 0, refundClinicName: invoice.clinicName ?? '', - refundDestination: isBankTransfer ? '2' : '1', + refundDestination: refundDestination, refundIban: isBankTransfer ? _ibanController.text.trim() : '', refundRemarks: isBankTransfer ? _descriptionController.text.trim() : '', refundTotalAmount: vm.totalRefundableAmount, + refundAttachmentsBase64List: vm.refundAttachmentList, refundProceduresList: vm.selectedProcedures .map((p) => SubmitRefundProcedureItem( lineItemNo: p.apiLineItemNo ?? 0, @@ -598,7 +793,7 @@ class _RefundRequestCreatePageState extends State { request: request, onSuccess: (response) { LoaderBottomSheet.hideLoader(); - _showSuccessBottomSheet(response.refNo ?? ''); + _showSuccessBottomSheet(response.refNo ?? '', refundDestination); }, onError: (err) { LoaderBottomSheet.hideLoader(); @@ -617,7 +812,10 @@ class _RefundRequestCreatePageState extends State { ); } - void _showSuccessBottomSheet(String refNo) { + void _showSuccessBottomSheet(String refNo, String refundDestination) { + // refundDestination: '1' = HMG Wallet, '2' = Bank Transfer, '3' = Cash + final isCashRefund = refundDestination == '3'; + showCommonBottomSheetWithoutHeight( context, child: Padding( @@ -638,6 +836,10 @@ class _RefundRequestCreatePageState extends State { ], ).paddingSymmetrical(24.w, 0), ], + if (isCashRefund) ...[ + SizedBox(height: 16.h), + LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText14(color: AppColors.textColor), + ], SizedBox(height: 24.h), Row( children: [ diff --git a/lib/presentation/refund_request/refund_request_detail_screen.dart b/lib/presentation/refund_request/refund_request_detail_screen.dart index 85f399b4..856f1394 100644 --- a/lib/presentation/refund_request/refund_request_detail_screen.dart +++ b/lib/presentation/refund_request/refund_request_detail_screen.dart @@ -10,7 +10,9 @@ import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/r import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; class RefundRequestDetailScreen extends StatelessWidget { final RefundInvoiceModel invoice; @@ -240,6 +242,8 @@ class RefundRequestDetailScreen extends StatelessWidget { ); } + + Widget _buildProcedureRow(BuildContext context, InvoiceRefundProcedure procedure) { final isApproved = procedure.isApproved ?? false; diff --git a/lib/widgets/attachment_options.dart b/lib/widgets/attachment_options.dart index bbc0bc6d..9fe70ddd 100644 --- a/lib/widgets/attachment_options.dart +++ b/lib/widgets/attachment_options.dart @@ -1,4 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; @@ -8,7 +9,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:flutter/material.dart'; class AttachmentOptions extends StatelessWidget { final Future Function() onCameraTap; @@ -16,13 +16,8 @@ class AttachmentOptions extends StatelessWidget { final VoidCallback onFilesTap; final bool showFilesOption; - const AttachmentOptions({ - Key? key, - required this.onCameraTap, - required this.onGalleryTap, - required this.onFilesTap, - this.showFilesOption = true - }) : super(key: key); + const AttachmentOptions({Key? key, required this.onCameraTap, required this.onGalleryTap, required this.onFilesTap, this.showFilesOption = true}) + : super(key: key); @override Widget build(BuildContext context) { @@ -52,7 +47,7 @@ class AttachmentOptions extends StatelessWidget { if (showFilesOption) checkInOptionCard( appState, - AppAssets.file_icon, + AppAssets.openFileIcon, LocaleKeys.openFiles.tr(context: context), LocaleKeys.selectDocumentFiles.tr(context: context), ).onPress(() async { diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart index e849cc96..e92ef282 100644 --- a/lib/widgets/image_picker.dart +++ b/lib/widgets/image_picker.dart @@ -22,6 +22,7 @@ class ImageOptions { bool showFilesOption, Function(String, File) image, { String? title, + bool allowMultiple = false, // Allow multiple file/image selection Future Function()? checkCameraPermission, Future Function()? checkGalleryPermission, }) { @@ -51,7 +52,11 @@ class ImageOptions { String fileName = _image.path; var bytes = File(fileName).readAsBytesSync(); String base64Encode = base64.encode(bytes); - image(base64Encode, _image); + + // Convert to data URI format + String mimeType = _getMimeType(fileName); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); } }, onGalleryTap: () async { @@ -66,21 +71,46 @@ class ImageOptions { Navigator.pop(context); // Close bottom sheet if (Platform.isAndroid) { - galleryImageAndroid(image); + galleryImageAndroid(image, allowMultiple: allowMultiple); } else { - XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery, imageQuality: 20); - if (pickedFile == null) return; + if (allowMultiple) { + // iOS/macOS: Pick multiple images + List pickedFiles = await picker.pickMultiImage(imageQuality: 20); + if (pickedFiles.isEmpty) return; - File _image = File(pickedFile.path); - String fileName = _image.path; - var bytes = File(fileName).readAsBytesSync(); - String base64Encode = base64.encode(bytes); - image(base64Encode, _image); + for (var pickedFile in pickedFiles) { + File _image = File(pickedFile.path); + var bytes = await _image.readAsBytes(); + String base64Encode = base64.encode(bytes); + + // Convert to data URI format + String mimeType = _getMimeType(pickedFile.path); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); + } + } else { + // Single image selection + XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery, imageQuality: 20); + if (pickedFile == null) return; + + File _image = File(pickedFile.path); + String fileName = _image.path; + var bytes = File(fileName).readAsBytesSync(); + String base64Encode = base64.encode(bytes); + + // Convert to data URI format + String mimeType = _getMimeType(fileName); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); + } } }, onFilesTap: () async { + Navigator.pop(context); // Close bottom sheet + FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, + allowMultiple: allowMultiple, // Use the parameter allowedExtensions: [ 'jpg', 'jpeg', @@ -95,8 +125,22 @@ class ImageOptions { 'zip', ], ); - List files = result!.paths.map((path) => File(path!)).toList(); - image(result.files.first.path.toString(), files.first); + + if (result == null || result.files.isEmpty) return; + + // Process selected file(s) + for (var fileData in result.files) { + if (fileData.path != null) { + File selectedFile = File(fileData.path!); + var bytes = await selectedFile.readAsBytes(); + String base64Encode = base64.encode(bytes); + + // Convert to data URI format + String mimeType = _getMimeType(fileData.path!); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, selectedFile); + } + } }, ), callBackFunc: () {}, @@ -217,23 +261,46 @@ class ImageOptions { // } } -void galleryImageAndroid(Function(String, File) image) async { +void galleryImageAndroid(Function(String, File) image, {bool allowMultiple = false}) async { // Enable Android Photo Picker if available if (_pickerPlatform is ImagePickerAndroid) { (_pickerPlatform as ImagePickerAndroid).useAndroidPhotoPicker = true; } try { - final XFile? pickedFile = - await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20)); + if (allowMultiple) { + // Pick multiple images + final List pickedFiles = await picker.pickMultiImage(imageQuality: 20); - if (pickedFile == null) return; + if (pickedFiles.isEmpty) return; - File _image = File(pickedFile.path); - String fileName = _image.path; - var bytes = File(fileName).readAsBytesSync(); - String base64Encode = base64.encode(bytes); - image(base64Encode, _image); + for (var pickedFile in pickedFiles) { + File _image = File(pickedFile.path); + var bytes = await _image.readAsBytes(); + String base64Encode = base64.encode(bytes); + + // Convert to data URI format + String mimeType = _getMimeType(pickedFile.path); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); + } + } else { + // Pick single image + final XFile? pickedFile = + await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20)); + + if (pickedFile == null) return; + + File _image = File(pickedFile.path); + String fileName = _image.path; + var bytes = File(fileName).readAsBytesSync(); + String base64Encode = base64.encode(bytes); + + // Convert to data URI format + String mimeType = _getMimeType(fileName); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); + } } catch (e) { print('Error picking image: $e'); } @@ -256,12 +323,55 @@ void cameraImageAndroid(Function(String, File) image) async { String fileName = _image.path; var bytes = File(fileName).readAsBytesSync(); String base64Encode = base64.encode(bytes); - image(base64Encode, _image); + + // Convert to data URI format + String mimeType = _getMimeType(fileName); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); } catch (e) { print('Error taking picture: $e'); } } +/// Helper function to determine MIME type from file extension +String _getMimeType(String filePath) { + final extension = filePath.split('.').last.toLowerCase(); + + switch (extension) { + case 'jpg': + case 'jpeg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + case 'webp': + return 'image/webp'; + case 'pdf': + return 'application/pdf'; + case 'doc': + return 'application/msword'; + case 'docx': + return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + case 'xls': + return 'application/vnd.ms-excel'; + case 'xlsx': + return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + case 'ppt': + return 'application/vnd.ms-powerpoint'; + case 'pptx': + return 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; + case 'txt': + return 'text/plain'; + case 'zip': + return 'application/zip'; + case 'rar': + return 'application/x-rar-compressed'; + default: + return 'application/octet-stream'; + } +} + class _BottomSheet extends StatelessWidget { final List children;