Added invoice type checks

faiz_dev
faizatflutter 3 days ago
parent 7ec72460e7
commit 34a55396b0

@ -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;

@ -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";

@ -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,
};

@ -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<String, dynamic> 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?,
);
}
}

@ -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<RefundableProcedure> procedures = [];
bool isProceduresLoading = 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;
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;

@ -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<AppState>();
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),

@ -74,6 +74,20 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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<RefundRequestCreatePage> {
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<RefundRequestCreatePage> {
);
}
// Step 1 exactly mirrors RechargeWalletPage hospital row
// Step 1 Select Procedures (or whole invoice for Inpatient/Package)
Widget _buildStep1Content(RefundRequestViewModel vm) {
return Consumer<RefundRequestViewModel>(
builder: (context, refundVM, _) {
final isArabic = _appState.isArabic();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -164,39 +185,41 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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<RefundRequestCreatePage> {
);
}
// 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<RefundRequestCreatePage> {
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<RefundRequestCreatePage> {
? (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),
],
),

@ -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,
),
),
],
),
);

Loading…
Cancel
Save