diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 9cfa8c1a..04b5d9c3 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1873,6 +1873,7 @@ "cash": "نقدي", "selectRefundReason": "اختر سبب الاسترداد", "refundReason": "سبب الاسترداد", + "rejectionReason": "سبب الرفض", "done": "تم", "refundReason1": "تم معالجة مريض الشركة كمدفوع ذاتي وإعادة فوترته", "refundReason2": "المريض لا يريد إجراء سريرياً", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index f367cfe5..69c74a1b 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1865,6 +1865,7 @@ "cash": "Cash", "selectRefundReason": "Select Refund Reason", "refundReason": "Refund Reason", + "rejectionReason": "Rejection Reason", "done": "Done", "refundReason1": "Company patient treated as self-paying and re-invoiced", "refundReason2": "Patient does not want a clinical procedure", diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index 44c0763c..05a3f83e 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -212,6 +212,43 @@ class HabibWalletViewModel extends ChangeNotifier { return isValid; } + // ==================== WITHDRAW ATTACHMENTS ==================================== + List withdrawAttachmentList = []; // Stores base64 strings + String? _withdrawAttachmentError; + static const int maxAttachments = 4; + static const int maxFileSizeBytes = 2 * 1024 * 1024; // 2MB + + String? get withdrawAttachmentError => _withdrawAttachmentError; + + void addWithdrawAttachment(String base64String) { + if (withdrawAttachmentList.length >= maxAttachments) { + _withdrawAttachmentError = LocaleKeys.maximumFilesReached.tr(); + notifyListeners(); + return; + } + withdrawAttachmentList.add(base64String); + _withdrawAttachmentError = null; + notifyListeners(); + } + + void removeWithdrawAttachment(int index) { + if (index >= 0 && index < withdrawAttachmentList.length) { + withdrawAttachmentList.removeAt(index); + _withdrawAttachmentError = null; + notifyListeners(); + } + } + + void clearWithdrawAttachmentError() { + _withdrawAttachmentError = null; + notifyListeners(); + } + + void setWithdrawAttachmentError(String error) { + _withdrawAttachmentError = error; + notifyListeners(); + } + // Validate recharge form - shows only first error bool validateRechargeForm(String amount) { // Clear previous errors @@ -335,6 +372,7 @@ class HabibWalletViewModel extends ChangeNotifier { refundTotalAmount: selectedAdvanceBalance, refundProceduresList: const [], refundAdvancesList: advances, + refundAttachmentsBase64List: withdrawAttachmentList, ); final result = await refundRequestRepo.submitRefund(request: request); @@ -401,8 +439,11 @@ class HabibWalletViewModel extends ChangeNotifier { isSubmittingWithdrawal = false; _ibanError = null; _withdrawDescriptionError = null; + _withdrawAttachmentError = null; + withdrawAttachmentList.clear(); currentWithdrawStep = 0; selectedAdvance = null; + selectedWithdrawMethod = null; if (!silent) notifyListeners(); } diff --git a/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart index 015a7556..aeb75f1c 100644 --- a/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart +++ b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart @@ -142,6 +142,7 @@ class InvoiceRefundProcedure { final String? procedureId; final String? procedureName; final String? refundReason; + final String? rejectionReason; final bool? isApproved; InvoiceRefundProcedure({ @@ -151,6 +152,7 @@ class InvoiceRefundProcedure { this.procedureId, this.procedureName, this.refundReason, + this.rejectionReason, this.isApproved, }); @@ -162,6 +164,7 @@ class InvoiceRefundProcedure { procedureId: json['ProcedureID'] as String?, procedureName: json['ProcedureName'] as String?, refundReason: json['RefundReason'] as String?, + rejectionReason: json['RejectionReason'] as String?, isApproved: json['IsApproved'] as bool?, ); } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index f3341345..751ccb40 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1859,6 +1859,7 @@ abstract class LocaleKeys { static const bankTransfer = 'bankTransfer'; static const selectRefundReason = 'selectRefundReason'; static const refundReason = 'refundReason'; + static const rejectionReason = 'rejectionReason'; static const refundReason1 = 'refundReason1'; static const refundReason2 = 'refundReason2'; static const refundReason3 = 'refundReason3'; diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index 6b8bb6f9..8161cbd8 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -20,6 +22,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/image_picker.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; @@ -293,6 +296,33 @@ class _WithdrawRequestCreatePageState extends State { ), ), ], + if (vm.selectedWithdrawMethod == RefundTabType.cash) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText12( + color: AppColors.greyInfoTextColor, + ), + ), + ], + ), + ), + ], ], ); } @@ -329,7 +359,7 @@ class _WithdrawRequestCreatePageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (isStep2 && vm.selectedAdvance != null) ...[ + if (vm.selectedAdvance != null) ...[ Padding( padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0), child: Row( @@ -351,7 +381,8 @@ class _WithdrawRequestCreatePageState extends State { ), ], CustomButton( - text: isStep2 ? LocaleKeys.submitRequest.tr(context: context) : LocaleKeys.next.tr(context: context), + icon: isStep2 ? AppAssets.refundIcon : null, + text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context), isDisabled: isStep2 ? !isStep2Valid : !isStep1Valid, onPressed: () { if (!isStep2) { @@ -525,6 +556,160 @@ class _WithdrawRequestCreatePageState extends State { ); } + // ── Image Upload Section ────────────────────────────────────────────────── + + Widget _buildImageUploadSection(HabibWalletViewModel vm) { + return Consumer( + builder: (context, viewModel, _) { + final hasError = viewModel.withdrawAttachmentError != null; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Upload button + InkWell( + onTap: () { + if (viewModel.withdrawAttachmentList.length >= 4) { + viewModel.setWithdrawAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context)); + return; + } + ImageOptions.showImageOptionsNew( + context, + true, + (String base64Image, File file) async { + // Validate file size (2MB max) + final fileSize = await file.length(); + if (fileSize > HabibWalletViewModel.maxFileSizeBytes) { + viewModel.setWithdrawAttachmentError(LocaleKeys.fileSizeTooLarge.tr(context: context)); + return; + } + + // Check max files limit + if (viewModel.withdrawAttachmentList.length >= HabibWalletViewModel.maxAttachments) { + viewModel.setWithdrawAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context)); + return; + } + + // Add attachment + viewModel.addWithdrawAttachment(base64Image); + }, + title: LocaleKeys.selectAttachment.tr(context: context), + allowMultiple: true, // Enable multiple selection + ); + }, + child: Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: hasError ? Border.all(color: AppColors.primaryRedBorderColor, width: 1) : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + height: 40.h, + width: 40.h, + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.r, + color: AppColors.greyColor, + ), + child: Utils.buildSvgWithAssets(icon: AppAssets.selectImageIcon, applyThemeColor: false), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.uploadImage.tr(context: context).toText12( + color: AppColors.inputLabelTextColor, + isBold: true, + ), + LocaleKeys.selectImage.tr(context: context).toText14( + color: AppColors.inputLabelTextColor, + isBold: true, + ), + ], + ), + ], + ), + if (hasError) ...[ + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + viewModel.withdrawAttachmentError!, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ), + ), + ), + // Selected images list + if (viewModel.withdrawAttachmentList.isNotEmpty) ...[ + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: ListView.separated( + padding: EdgeInsets.all(16.h), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: viewModel.withdrawAttachmentList.length, + separatorBuilder: (context, index) => SizedBox(height: 12.h), + itemBuilder: (BuildContext context, int index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.attach_file, + color: Color(0xff2B353E), + size: 20.h, + ), + SizedBox(width: 8.w), + "${LocaleKeys.image.tr(context: context)} ${index + 1}".toText14(), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.cancel_circle_icon, width: 20.h, height: 20.h).onPress(() { + viewModel.removeWithdrawAttachment(index); + }), + ], + ); + }, + ), + ), + ], + ], + ); + }, + ); + } + // ── Submit ──────────────────────────────────────────────────────────────── void _handleWalletWithdrawalSubmit(HabibWalletViewModel vm) { @@ -718,6 +903,10 @@ class _WithdrawRequestCreatePageState extends State { SizedBox(height: 16.h), _buildBankInputDetails(), ], + if (habibWalletViewModel.selectedWithdrawMethod != null) ...[ + SizedBox(height: 16.h), + _buildImageUploadSection(habibWalletViewModel), + ], ], ), ), diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index afef99ae..32b96d6b 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -286,7 +286,7 @@ class _RefundRequestCreatePageState extends State { Container( padding: EdgeInsets.all(16.h), decoration: BoxDecoration( - color: AppColors.warningColorYellow.withOpacity(0.1), + color: AppColors.warningColorYellow.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12.r), border: Border.all(color: AppColors.warningColorYellow, width: 1), ), @@ -304,9 +304,38 @@ class _RefundRequestCreatePageState extends State { ), ), ], + if (vm.selectedPaymentMethod == RefundTabType.cash) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText12( + color: AppColors.greyInfoTextColor, + ), + ), + ], + ), + ), + ], if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[ SizedBox(height: 16.h), _buildBankInputDetails(vm), + ], + if (vm.selectedPaymentMethod != null) ...[ SizedBox(height: 16.h), _buildImageUploadSection(vm), ], diff --git a/lib/presentation/refund_request/refund_request_detail_screen.dart b/lib/presentation/refund_request/refund_request_detail_screen.dart index 856f1394..f67a065a 100644 --- a/lib/presentation/refund_request/refund_request_detail_screen.dart +++ b/lib/presentation/refund_request/refund_request_detail_screen.dart @@ -10,9 +10,7 @@ import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/r import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; class RefundRequestDetailScreen extends StatelessWidget { final RefundInvoiceModel invoice; @@ -242,39 +240,65 @@ class RefundRequestDetailScreen extends StatelessWidget { ); } - - Widget _buildProcedureRow(BuildContext context, InvoiceRefundProcedure procedure) { final isApproved = procedure.isApproved ?? false; + final hasRejectionReason = procedure.rejectionReason != null && procedure.rejectionReason!.isNotEmpty; + return Padding( padding: EdgeInsets.symmetric(vertical: 6.h), - child: Row( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - flex: 3, - child: (procedure.procedureName ?? '-').toText12(color: AppColors.textColor), - ), - SizedBox(width: 8.w), - SizedBox( - width: 70.w, - child: (isApproved ? LocaleKeys.yes : LocaleKeys.no).tr(context: context).toText12( + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 3, + child: (procedure.procedureName ?? '-').toText12(color: AppColors.textColor), + ), + SizedBox(width: 8.w), + SizedBox( + width: 70.w, + child: (isApproved ? LocaleKeys.yes : LocaleKeys.no).tr(context: context).toText12( + color: AppColors.textColor, + isBold: true, + textAlignment: TextAlign.center, + ), + ), + SizedBox(width: 8.w), + SizedBox( + width: 60.w, + child: (procedure.patientPaidAmount?.toStringAsFixed(2) ?? '-').toText12( color: AppColors.textColor, isBold: true, - textAlignment: TextAlign.center, + textAlignment: TextAlign.end, ), + ), + ], ), - SizedBox(width: 8.w), - SizedBox( - width: 60.w, - child: (procedure.patientPaidAmount?.toStringAsFixed(2) ?? '-').toText12( - color: AppColors.textColor, - isBold: true, - textAlignment: TextAlign.end, + if (hasRejectionReason) ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 10.h), + "${LocaleKeys.rejectionReason.tr(context: context)}:".toText10( + color: AppColors.greyInfoTextColor, + isBold: true, + ), + Row( + children: [ + Expanded( + child: procedure.rejectionReason!.toText10( + color: AppColors.greyInfoTextColor, + ), + ), + ], + ), + ], ), - ), + ], ], ), );