From 34a55396b05e017e8d688762bb834232b03e24c7 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Wed, 10 Jun 2026 16:26:18 +0300 Subject: [PATCH] Added invoice type checks --- lib/core/api/api_client.dart | 3 +- lib/core/api_consts.dart | 4 +- .../get_invoices_list_response_model.dart | 16 ++ .../refund_request_list_response_model.dart | 3 + .../refund_request_view_model.dart | 11 +- .../widgets/invoice_list_card.dart | 18 +- .../refund_request_create_page.dart | 236 ++++++++++-------- .../refund_request_detail_screen.dart | 44 +++- 8 files changed, 219 insertions(+), 116 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index b1fc0e5b..8b373737 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -209,7 +209,8 @@ class ApiClientImp implements ApiClient { } body['TokenID'] = "@dm!n"; - body['PatientID'] = 4774167; + // body['PatientID'] = 4770714; + body['PatientID'] = 4771727; // body['PatientID'] = 89754444; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 24872c32..c7081528 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { 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 @@ -292,7 +292,7 @@ class ApiConsts { // ************ static values for Api **************** // static final double appVersionID = 21.2; - static final double appVersionID = 50.7; + static final double appVersionID = 21.4; static final int appChannelId = 3; static final String appIpAddress = "10.20.10.20"; static final String appGeneralId = "Cs2020@2016\$2958"; diff --git a/lib/features/my_invoices/models/get_invoices_list_response_model.dart b/lib/features/my_invoices/models/get_invoices_list_response_model.dart index a3c9858c..1aa580e7 100644 --- a/lib/features/my_invoices/models/get_invoices_list_response_model.dart +++ b/lib/features/my_invoices/models/get_invoices_list_response_model.dart @@ -19,6 +19,10 @@ class GetInvoicesListResponseModel { String? doctorImageUrl; int? doctorRate; bool? isAllowRefund; + int? invoiceType; + String? invoiceTypeDescription; + bool? isPackageInvoice; + bool? isOutPatient; int? patientNumber; String? projectName; @@ -41,6 +45,10 @@ class GetInvoicesListResponseModel { this.doctorImageUrl, this.doctorRate, this.isAllowRefund, + this.invoiceType, + this.invoiceTypeDescription, + this.isPackageInvoice, + this.isOutPatient, this.patientNumber, this.projectName, }); @@ -68,6 +76,10 @@ class GetInvoicesListResponseModel { doctorImageUrl: json["DoctorImageURL"], doctorRate: json["DoctorRate"], isAllowRefund: json["IsAllowRefund"], + invoiceType: json["InvoiceType"], + invoiceTypeDescription: json["InvoiceTypeDescription"], + isPackageInvoice: json["IsPackageInvoice"], + isOutPatient: json['InvoiceType'] != 75, patientNumber: json["PatientNumber"], projectName: json["ProjectName"], ); @@ -91,6 +103,10 @@ class GetInvoicesListResponseModel { "DoctorImageURL": doctorImageUrl, "DoctorRate": doctorRate, "IsAllowRefund": isAllowRefund, + "invoiceType": invoiceType, + "invoiceDescription": invoiceTypeDescription, + "isOutPatient": isOutPatient, + "isPackageInvoice": isPackageInvoice, "PatientNumber": patientNumber, "ProjectName": projectName, }; diff --git a/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart index b17dd5aa..015a7556 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 bool? isApproved; InvoiceRefundProcedure({ this.isLocked, @@ -150,6 +151,7 @@ class InvoiceRefundProcedure { this.procedureId, this.procedureName, this.refundReason, + this.isApproved, }); factory InvoiceRefundProcedure.fromJson(Map json) { @@ -160,6 +162,7 @@ class InvoiceRefundProcedure { procedureId: json['ProcedureID'] as String?, procedureName: json['ProcedureName'] as String?, refundReason: json['RefundReason'] as String?, + isApproved: json['IsApproved'] as bool?, ); } } diff --git a/lib/features/refund_request/refund_request_view_model.dart b/lib/features/refund_request/refund_request_view_model.dart index 828e3d0a..d539d97f 100644 --- a/lib/features/refund_request/refund_request_view_model.dart +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -109,6 +109,7 @@ class RefundRequestViewModel extends ChangeNotifier { currentStep = 0; procedures.clear(); needITGApproval = false; + isWholeInvoiceRefund = false; selectedPaymentMethod = null; selectedRefundReason = null; isProceduresLoading = false; @@ -208,15 +209,20 @@ class RefundRequestViewModel extends ChangeNotifier { List procedures = []; bool isProceduresLoading = false; bool needITGApproval = false; + bool isWholeInvoiceRefund = false; // For Inpatient/Package invoices - void setProcedures(List list, {bool needsITGApproval = false}) { + void setProcedures(List list, {bool needsITGApproval = false, bool isWholeInvoiceRefund = false}) { procedures = list; needITGApproval = needsITGApproval; + this.isWholeInvoiceRefund = isWholeInvoiceRefund; isProceduresLoading = false; notifyListeners(); } void toggleProcedureSelection(RefundableProcedure procedure) { + // Prevent selection toggle for whole invoice refunds + if (isWholeInvoiceRefund) return; + procedure.isSelected = !procedure.isSelected; notifyListeners(); } @@ -229,6 +235,9 @@ class RefundRequestViewModel extends ChangeNotifier { bool get isAllSelected => procedures.isNotEmpty && procedures.every((p) => p.isSelected); void toggleSelectAll() { + // Prevent toggle for whole invoice refunds + if (isWholeInvoiceRefund) return; + final selectAll = !isAllSelected; for (final p in procedures) { p.isSelected = selectAll; diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index dc5414cd..3b0ea71d 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -24,9 +24,9 @@ import 'package:provider/provider.dart'; class InvoiceListCard extends StatelessWidget { final GetInvoicesListResponseModel getInvoicesListResponseModel; - Function? onTap; + final Function? onTap; - InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap}); + const InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap}); void _handleRefundInvoiceTap(BuildContext context) { final invoiceNo = getInvoicesListResponseModel.invoiceNo; @@ -70,7 +70,6 @@ class InvoiceListCard extends StatelessWidget { @override Widget build(BuildContext context) { - AppState appState = getIt(); return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: Padding( @@ -90,11 +89,14 @@ class InvoiceListCard extends StatelessWidget { labelText: LocaleKeys.walkin.tr(), textColor: AppColors.textColor, ), - AppCustomChipWidget( - labelText: appState.isArabic() ? "العيادات الخارجيه" : "Out Patient", - backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1), - textColor: AppColors.warningColorYellow, - ), + if (getInvoicesListResponseModel.invoiceTypeDescription != null && + getInvoicesListResponseModel.invoiceTypeDescription!.isNotEmpty) ...[ + AppCustomChipWidget( + labelText: getInvoicesListResponseModel.invoiceTypeDescription ?? "", + backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1), + textColor: AppColors.warningColorYellow, + ), + ], ], ), SizedBox(height: 16.h), diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index 1d0f723f..a795cf16 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -74,6 +74,20 @@ class _RefundRequestCreatePageState extends State { super.dispose(); } + /// Determines if the invoice allows individual procedure selection + /// Returns true for Outpatient and ER invoices only + /// Returns false for Inpatient and Package invoices (whole invoice refund only) + bool get _canSelectIndividualProcedures { + // Package invoices: no individual procedure selection + if (widget.invoice.isPackageInvoice == true) return false; + + // Inpatient invoices: no individual procedure selection + if (widget.invoice.isOutPatient == false) return false; + + // Outpatient and ER invoices: allow individual procedure selection + return true; + } + void _loadInvoiceDetails() { final invoiceNo = widget.invoice.invoiceNo; final projectId = widget.invoice.projectId; @@ -90,9 +104,17 @@ class _RefundRequestCreatePageState extends State { patientPaidAmount: item.patientPaidAmount ?? 0.0, apiLineItemNo: item.lineItemNo, apiProcedureId: item.procedureId, + // For Inpatient/Package invoices: auto-select all procedures (can't deselect) + isSelected: !_canSelectIndividualProcedures, + // For Inpatient/Package invoices: set empty string as refund reason (mandatory field) + refundReason: !_canSelectIndividualProcedures ? "Inpatient/Package invoice" : null, )) .toList(); - _refundRequestViewModel.setProcedures(procedures, needsITGApproval: details.needITGApproval ?? false); + _refundRequestViewModel.setProcedures( + procedures, + needsITGApproval: details.needITGApproval ?? false, + isWholeInvoiceRefund: !_canSelectIndividualProcedures, + ); }, ); } @@ -123,12 +145,11 @@ class _RefundRequestCreatePageState extends State { ); } - // ── Step 1 — exactly mirrors RechargeWalletPage hospital row ───────────── + // ── Step 1 — Select Procedures (or whole invoice for Inpatient/Package) ── Widget _buildStep1Content(RefundRequestViewModel vm) { return Consumer( builder: (context, refundVM, _) { - final isArabic = _appState.isArabic(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -164,39 +185,41 @@ class _RefundRequestCreatePageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Select All row - Material( - color: Colors.transparent, - child: InkWell( - onTap: () => _refundRequestViewModel.toggleSelectAll(), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - SizedBox( - width: 24.w, - height: 24.w, - child: Transform.scale( - scale: 24.w / 22, - child: Checkbox( - value: vm.isAllSelected, - onChanged: (_) => _refundRequestViewModel.toggleSelectAll(), - activeColor: AppColors.primaryRedColor, - side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.r)), + // Select All row - only show for Outpatient/ER invoices + if (_canSelectIndividualProcedures) + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _refundRequestViewModel.toggleSelectAll(), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + SizedBox( + width: 24.w, + height: 24.w, + child: Transform.scale( + scale: 24.w / 22, + child: Checkbox( + value: vm.isAllSelected, + onChanged: (_) => _refundRequestViewModel.toggleSelectAll(), + activeColor: AppColors.primaryRedColor, + side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.r)), + ), ), ), - ), - SizedBox(width: 8.w), - LocaleKeys.selectAll.tr(context: context).toText16(isBold: true), - ], + SizedBox(width: 8.w), + LocaleKeys.selectAll.tr(context: context).toText16(isBold: true), + ], + ), ), ), ), - ), - Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), + if (_canSelectIndividualProcedures) + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), ...List.generate( vm.procedures.length, (index) => _buildProcedureCard(vm.procedures[index], index, vm.procedures.length), @@ -331,11 +354,12 @@ class _RefundRequestCreatePageState extends State { ); } - // ── Step 2 — procedure checkboxes ───────────────────────────────────────── + // ── Procedure Card ──────────────────────────────────────────────────────── Widget _buildProcedureCard(RefundableProcedure procedure, int index, int length) { final isArabic = _appState.isArabic(); final name = isArabic && procedure.procedureNameN != null ? procedure.procedureNameN! : procedure.procedureName; + final canSelect = _canSelectIndividualProcedures; return AnimationConfiguration.staggeredList( position: index, @@ -349,81 +373,88 @@ class _RefundRequestCreatePageState extends State { child: Material( color: Colors.transparent, child: InkWell( - onTap: () => _refundRequestViewModel.toggleProcedureSelection(procedure), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 24.w, - height: 24.w, - child: Transform.scale( - scale: 24.w / 22, - child: Checkbox( - value: procedure.isSelected, - onChanged: (_) => _refundRequestViewModel.toggleProcedureSelection(procedure), - activeColor: AppColors.primaryRedColor, - side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4.r), + // Disable tap for Inpatient/Package invoices + onTap: canSelect ? () => _refundRequestViewModel.toggleProcedureSelection(procedure) : null, + child: Opacity( + // Reduce opacity for disabled items + opacity: canSelect ? 1.0 : 0.7, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 24.w, + height: 24.w, + child: Transform.scale( + scale: 24.w / 22, + child: Checkbox( + value: procedure.isSelected, + // Disable checkbox for Inpatient/Package invoices + onChanged: canSelect ? (_) => _refundRequestViewModel.toggleProcedureSelection(procedure) : null, + activeColor: AppColors.primaryRedColor, + side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.r), + ), ), ), ), - ), - SizedBox(width: 8.w), - Expanded(child: name.toText16(isBold: true)), - SizedBox(width: 8.w), - Utils.getPaymentAmountWithSymbol( - procedure.patientPaidAmount.toStringAsFixed(2).toText20( - isBold: true, - isEnglishOnly: true, - color: AppColors.textColor, - ), - AppColors.textColor, - 10.f, - isSaudiCurrency: true, - ), - ], - ), - if (procedure.isSelected) ...[ - InkWell( - onTap: () => showRefundReasonBottomSheet( - context, - selectedReason: procedure.refundReason, - onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason), - ), - child: Padding( - padding: EdgeInsets.only(top: 16.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), - (procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context)) - .toText14(color: AppColors.textColor, isBold: true), - ], + SizedBox(width: 8.w), + Expanded(child: name.toText16(isBold: true)), + SizedBox(width: 8.w), + Utils.getPaymentAmountWithSymbol( + procedure.patientPaidAmount.toStringAsFixed(2).toText20( + isBold: true, + isEnglishOnly: true, + color: AppColors.textColor, ), - ), - Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), - ], + AppColors.textColor, + 10.f, + isSaudiCurrency: true, ), - ), + ], ), + // Only show refund reason selector for Outpatient/ER invoices + if (canSelect && procedure.isSelected) ...[ + InkWell( + onTap: () => showRefundReasonBottomSheet( + context, + selectedReason: procedure.refundReason, + onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason), + ), + child: Padding( + padding: EdgeInsets.only(top: 16.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), + (procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context)) + .toText14(color: AppColors.textColor, isBold: true), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ), + ), + ), + ], + if (index != length - 1) ...[ + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingOnly(top: 16.h), + ] ], - if (index != length - 1) ...[ - Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingOnly(top: 16.h), - ] - ], + ), ), ), ), @@ -492,7 +523,10 @@ class _RefundRequestCreatePageState extends State { ? (refundRequestViewModel.selectedPaymentMethod == null || (refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices && (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty))) - : (refundRequestViewModel.selectedProcedures.isEmpty || !refundRequestViewModel.allSelectedProceduresHaveReasons), + : (refundRequestViewModel.selectedProcedures.isEmpty || + // For Outpatient/ER: require refund reasons + // For Inpatient/Package: skip refund reason validation + (_canSelectIndividualProcedures && !refundRequestViewModel.allSelectedProceduresHaveReasons)), ).paddingSymmetrical(24.h, 24.h), ], ), diff --git a/lib/presentation/refund_request/refund_request_detail_screen.dart b/lib/presentation/refund_request/refund_request_detail_screen.dart index a28aa4dd..85f399b4 100644 --- a/lib/presentation/refund_request/refund_request_detail_screen.dart +++ b/lib/presentation/refund_request/refund_request_detail_screen.dart @@ -186,8 +186,28 @@ class RefundRequestDetailScreen extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.procedure.tr(context: context).toText12(color: AppColors.textColor, isBold: true), - LocaleKeys.amountOnly.tr(context: context).toText12(color: AppColors.textColor, isBold: true), + Expanded( + flex: 3, + child: LocaleKeys.procedure.tr(context: context).toText12(color: AppColors.textColor, isBold: true), + ), + SizedBox(width: 8.w), + SizedBox( + width: 70.w, + child: LocaleKeys.approved.tr(context: context).toText12( + color: AppColors.textColor, + isBold: true, + textAlignment: TextAlign.center, + ), + ), + SizedBox(width: 8.w), + SizedBox( + width: 60.w, + child: LocaleKeys.amountOnly.tr(context: context).toText12( + color: AppColors.textColor, + isBold: true, + textAlignment: TextAlign.end, + ), + ), ], ), SizedBox(height: 16.h), @@ -221,6 +241,8 @@ class RefundRequestDetailScreen extends StatelessWidget { } Widget _buildProcedureRow(BuildContext context, InvoiceRefundProcedure procedure) { + final isApproved = procedure.isApproved ?? false; + return Padding( padding: EdgeInsets.symmetric(vertical: 6.h), child: Row( @@ -232,7 +254,23 @@ class RefundRequestDetailScreen extends StatelessWidget { child: (procedure.procedureName ?? '-').toText12(color: AppColors.textColor), ), SizedBox(width: 8.w), - (procedure.patientPaidAmount?.toString() ?? '-').toText12(color: AppColors.textColor, isBold: true), + 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, + ), + ), ], ), );