Merge pull request 'Ancillary Orders Complete Flow (faiz_dev)' (#94) from faiz_dev into master
Reviewed-on: #94pull/99/head
commit
5b7c1c5a41
@ -0,0 +1,115 @@
|
|||||||
|
// Dart model for the "AncillaryOrderList" structure
|
||||||
|
// Uses DateUtil.convertStringToDate and DateUtil.dateToDotNetString from your project to parse/serialize .NET-style dates.
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
|
||||||
|
|
||||||
|
class AncillaryOrderListModel {
|
||||||
|
List<AncillaryOrderGroup>? ancillaryOrderList;
|
||||||
|
|
||||||
|
AncillaryOrderListModel({this.ancillaryOrderList});
|
||||||
|
|
||||||
|
factory AncillaryOrderListModel.fromJson(Map<String, dynamic> json) => AncillaryOrderListModel(
|
||||||
|
ancillaryOrderList: json['AncillaryOrderList'] != null
|
||||||
|
? List<AncillaryOrderGroup>.from(
|
||||||
|
(json['AncillaryOrderList'] as List).map(
|
||||||
|
(x) => AncillaryOrderGroup.fromJson(x as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AncillaryOrderGroup {
|
||||||
|
List<AncillaryOrderItem>? ancillaryOrderList;
|
||||||
|
dynamic errCode;
|
||||||
|
String? message;
|
||||||
|
int? patientID;
|
||||||
|
String? patientName;
|
||||||
|
int? patientType;
|
||||||
|
int? projectID;
|
||||||
|
String? projectName;
|
||||||
|
String? setupID;
|
||||||
|
int? statusCode;
|
||||||
|
|
||||||
|
AncillaryOrderGroup({
|
||||||
|
this.ancillaryOrderList,
|
||||||
|
this.errCode,
|
||||||
|
this.message,
|
||||||
|
this.patientID,
|
||||||
|
this.patientName,
|
||||||
|
this.patientType,
|
||||||
|
this.projectID,
|
||||||
|
this.projectName,
|
||||||
|
this.setupID,
|
||||||
|
this.statusCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AncillaryOrderGroup.fromJson(Map<String, dynamic> json) => AncillaryOrderGroup(
|
||||||
|
ancillaryOrderList: json['AncillaryOrderList'] != null
|
||||||
|
? List<AncillaryOrderItem>.from(
|
||||||
|
(json['AncillaryOrderList'] as List).map(
|
||||||
|
(x) => AncillaryOrderItem.fromJson(x as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
errCode: json['ErrCode'],
|
||||||
|
message: json['Message'] as String?,
|
||||||
|
patientID: json['PatientID'] as int?,
|
||||||
|
patientName: json['PatientName'] as String?,
|
||||||
|
patientType: json['PatientType'] as int?,
|
||||||
|
projectID: json['ProjectID'] as int?,
|
||||||
|
projectName: json['ProjectName'] as String?,
|
||||||
|
setupID: json['SetupID'] as String?,
|
||||||
|
statusCode: json['StatusCode'] as int?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AncillaryOrderItem {
|
||||||
|
dynamic ancillaryProcedureListModels;
|
||||||
|
DateTime? appointmentDate;
|
||||||
|
int? appointmentNo;
|
||||||
|
int? clinicID;
|
||||||
|
String? clinicName;
|
||||||
|
int? doctorID;
|
||||||
|
String? doctorName;
|
||||||
|
int? invoiceNo;
|
||||||
|
bool? isCheckInAllow;
|
||||||
|
bool? isQueued;
|
||||||
|
DateTime? orderDate;
|
||||||
|
int? orderNo;
|
||||||
|
String? projectName; // Added from parent AncillaryOrderGroup
|
||||||
|
int? projectID; // Added from parent AncillaryOrderGroup
|
||||||
|
|
||||||
|
AncillaryOrderItem({
|
||||||
|
this.ancillaryProcedureListModels,
|
||||||
|
this.appointmentDate,
|
||||||
|
this.appointmentNo,
|
||||||
|
this.clinicID,
|
||||||
|
this.clinicName,
|
||||||
|
this.doctorID,
|
||||||
|
this.doctorName,
|
||||||
|
this.invoiceNo,
|
||||||
|
this.isCheckInAllow,
|
||||||
|
this.isQueued,
|
||||||
|
this.orderDate,
|
||||||
|
this.orderNo,
|
||||||
|
this.projectName,
|
||||||
|
this.projectID,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AncillaryOrderItem.fromJson(Map<String, dynamic> json, {String? projectName, int? projectID}) => AncillaryOrderItem(
|
||||||
|
ancillaryProcedureListModels: json['AncillaryProcedureListModels'],
|
||||||
|
appointmentDate: DateUtil.convertStringToDate(json['AppointmentDate']),
|
||||||
|
appointmentNo: json['AppointmentNo'] as int?,
|
||||||
|
clinicID: json['ClinicID'] as int?,
|
||||||
|
clinicName: json['ClinicName'] as String?,
|
||||||
|
doctorID: json['DoctorID'] as int?,
|
||||||
|
doctorName: json['DoctorName'] as String?,
|
||||||
|
invoiceNo: json['Invoiceno'] as int?,
|
||||||
|
isCheckInAllow: json['IsCheckInAllow'] as bool?,
|
||||||
|
isQueued: json['IsQueued'] as bool?,
|
||||||
|
orderDate: DateUtil.convertStringToDate(json['OrderDate']),
|
||||||
|
orderNo: json['OrderNo'] as int?,
|
||||||
|
projectName: projectName,
|
||||||
|
projectID: projectID,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
// Dart model classes for "AncillaryOrderProcList"
|
||||||
|
// Generated for user: faizatflutter
|
||||||
|
// Uses DateUtil.convertStringToDate for parsing .NET-style dates (same approach as your PatientRadiologyResponseModel)
|
||||||
|
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
|
||||||
|
|
||||||
|
class AncillaryOrderProcListModel {
|
||||||
|
List<AncillaryOrderProcedureItem>? ancillaryOrderProcList;
|
||||||
|
|
||||||
|
AncillaryOrderProcListModel({this.ancillaryOrderProcList});
|
||||||
|
|
||||||
|
factory AncillaryOrderProcListModel.fromJson(Map<String, dynamic> json) => AncillaryOrderProcListModel(
|
||||||
|
ancillaryOrderProcList: json['AncillaryOrderProcList'] != null
|
||||||
|
? List<AncillaryOrderProcedureItem>.from(
|
||||||
|
(json['AncillaryOrderProcList'] as List).map(
|
||||||
|
(x) => AncillaryOrderProcedureItem.fromJson(x as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AncillaryOrderProcedureItem {
|
||||||
|
List<AncillaryOrderProcDetail>? ancillaryOrderProcDetailsList;
|
||||||
|
DateTime? appointmentDate;
|
||||||
|
int? appointmentNo;
|
||||||
|
int? clinicID;
|
||||||
|
String? clinicName;
|
||||||
|
int? companyID;
|
||||||
|
String? companyName;
|
||||||
|
int? doctorID;
|
||||||
|
String? doctorName;
|
||||||
|
dynamic errCode;
|
||||||
|
int? groupID;
|
||||||
|
String? insurancePolicyNo;
|
||||||
|
String? message;
|
||||||
|
String? patientCardID;
|
||||||
|
int? patientID;
|
||||||
|
String? patientName;
|
||||||
|
int? patientType;
|
||||||
|
int? policyID;
|
||||||
|
String? policyName;
|
||||||
|
int? projectID;
|
||||||
|
String? setupID;
|
||||||
|
int? statusCode;
|
||||||
|
int? subCategoryID;
|
||||||
|
String? subPolicyNo;
|
||||||
|
|
||||||
|
AncillaryOrderProcedureItem({
|
||||||
|
this.ancillaryOrderProcDetailsList,
|
||||||
|
this.appointmentDate,
|
||||||
|
this.appointmentNo,
|
||||||
|
this.clinicID,
|
||||||
|
this.clinicName,
|
||||||
|
this.companyID,
|
||||||
|
this.companyName,
|
||||||
|
this.doctorID,
|
||||||
|
this.doctorName,
|
||||||
|
this.errCode,
|
||||||
|
this.groupID,
|
||||||
|
this.insurancePolicyNo,
|
||||||
|
this.message,
|
||||||
|
this.patientCardID,
|
||||||
|
this.patientID,
|
||||||
|
this.patientName,
|
||||||
|
this.patientType,
|
||||||
|
this.policyID,
|
||||||
|
this.policyName,
|
||||||
|
this.projectID,
|
||||||
|
this.setupID,
|
||||||
|
this.statusCode,
|
||||||
|
this.subCategoryID,
|
||||||
|
this.subPolicyNo,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AncillaryOrderProcedureItem.fromJson(Map<String, dynamic> json) => AncillaryOrderProcedureItem(
|
||||||
|
ancillaryOrderProcDetailsList: json['AncillaryOrderProcDetailsList'] != null
|
||||||
|
? List<AncillaryOrderProcDetail>.from(
|
||||||
|
(json['AncillaryOrderProcDetailsList'] as List).map(
|
||||||
|
(x) => AncillaryOrderProcDetail.fromJson(x as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
appointmentDate: DateUtil.convertStringToDate(json['AppointmentDate']),
|
||||||
|
appointmentNo: json['AppointmentNo'] as int?,
|
||||||
|
clinicID: json['ClinicID'] as int?,
|
||||||
|
clinicName: json['ClinicName'] as String?,
|
||||||
|
companyID: json['CompanyID'] as int?,
|
||||||
|
companyName: json['CompanyName'] as String?,
|
||||||
|
doctorID: json['DoctorID'] as int?,
|
||||||
|
doctorName: json['DoctorName'] as String?,
|
||||||
|
errCode: json['ErrCode'],
|
||||||
|
groupID: json['GroupID'] as int?,
|
||||||
|
insurancePolicyNo: json['InsurancePolicyNo'] as String?,
|
||||||
|
message: json['Message'] as String?,
|
||||||
|
patientCardID: json['PatientCardID'] as String?,
|
||||||
|
patientID: json['PatientID'] as int?,
|
||||||
|
patientName: json['PatientName'] as String?,
|
||||||
|
patientType: json['PatientType'] as int?,
|
||||||
|
policyID: json['PolicyID'] as int?,
|
||||||
|
policyName: json['PolicyName'] as String?,
|
||||||
|
projectID: json['ProjectID'] as int?,
|
||||||
|
setupID: json['SetupID'] as String?,
|
||||||
|
statusCode: json['StatusCode'] as int?,
|
||||||
|
subCategoryID: json['SubCategoryID'] as int?,
|
||||||
|
subPolicyNo: json['SubPolicyNo'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AncillaryOrderProcDetail {
|
||||||
|
int? approvalLineItemNo;
|
||||||
|
int? approvalNo;
|
||||||
|
String? approvalStatus;
|
||||||
|
int? approvalStatusID;
|
||||||
|
num? companyShare;
|
||||||
|
num? companyShareWithTax;
|
||||||
|
num? companyTaxAmount;
|
||||||
|
num? discountAmount;
|
||||||
|
int? discountCategory;
|
||||||
|
String? discountType;
|
||||||
|
num? discountTypeValue;
|
||||||
|
bool? isApprovalCreated;
|
||||||
|
bool? isApprovalRequired;
|
||||||
|
dynamic isCheckInAllow;
|
||||||
|
bool? isCovered;
|
||||||
|
bool? isLab;
|
||||||
|
DateTime? orderDate;
|
||||||
|
int? orderLineItemNo;
|
||||||
|
int? orderNo;
|
||||||
|
int? partnerID;
|
||||||
|
num? partnerShare;
|
||||||
|
String? partnerShareType;
|
||||||
|
num? patientShare;
|
||||||
|
num? patientShareWithTax;
|
||||||
|
num? patientTaxAmount;
|
||||||
|
num? procPrice;
|
||||||
|
int? procedureCategoryID;
|
||||||
|
String? procedureCategoryName;
|
||||||
|
String? procedureID;
|
||||||
|
String? procedureName;
|
||||||
|
num? taxAmount;
|
||||||
|
num? taxPct;
|
||||||
|
|
||||||
|
AncillaryOrderProcDetail({
|
||||||
|
this.approvalLineItemNo,
|
||||||
|
this.approvalNo,
|
||||||
|
this.approvalStatus,
|
||||||
|
this.approvalStatusID,
|
||||||
|
this.companyShare,
|
||||||
|
this.companyShareWithTax,
|
||||||
|
this.companyTaxAmount,
|
||||||
|
this.discountAmount,
|
||||||
|
this.discountCategory,
|
||||||
|
this.discountType,
|
||||||
|
this.discountTypeValue,
|
||||||
|
this.isApprovalCreated,
|
||||||
|
this.isApprovalRequired,
|
||||||
|
this.isCheckInAllow,
|
||||||
|
this.isCovered,
|
||||||
|
this.isLab,
|
||||||
|
this.orderDate,
|
||||||
|
this.orderLineItemNo,
|
||||||
|
this.orderNo,
|
||||||
|
this.partnerID,
|
||||||
|
this.partnerShare,
|
||||||
|
this.partnerShareType,
|
||||||
|
this.patientShare,
|
||||||
|
this.patientShareWithTax,
|
||||||
|
this.patientTaxAmount,
|
||||||
|
this.procPrice,
|
||||||
|
this.procedureCategoryID,
|
||||||
|
this.procedureCategoryName,
|
||||||
|
this.procedureID,
|
||||||
|
this.procedureName,
|
||||||
|
this.taxAmount,
|
||||||
|
this.taxPct,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AncillaryOrderProcDetail.fromJson(Map<String, dynamic> json) => AncillaryOrderProcDetail(
|
||||||
|
approvalLineItemNo: json['ApprovalLineItemNo'] as int?,
|
||||||
|
approvalNo: json['ApprovalNo'] as int?,
|
||||||
|
approvalStatus: json['ApprovalStatus'] as String?,
|
||||||
|
approvalStatusID: json['ApprovalStatusID'] as int?,
|
||||||
|
companyShare: _toNum(json['CompanyShare']),
|
||||||
|
companyShareWithTax: _toNum(json['CompanyShareWithTax']),
|
||||||
|
companyTaxAmount: _toNum(json['CompanyTaxAmount']),
|
||||||
|
discountAmount: _toNum(json['DiscountAmount']),
|
||||||
|
discountCategory: json['DiscountCategory'] as int?,
|
||||||
|
discountType: json['DiscountType'] as String?,
|
||||||
|
discountTypeValue: _toNum(json['DiscountTypeValue']),
|
||||||
|
isApprovalCreated: json['IsApprovalCreated'] as bool?,
|
||||||
|
isApprovalRequired: json['IsApprovalRequired'] as bool?,
|
||||||
|
isCheckInAllow: json['IsCheckInAllow'],
|
||||||
|
isCovered: json['IsCovered'] as bool?,
|
||||||
|
isLab: json['IsLab'] as bool?,
|
||||||
|
orderDate: DateUtil.convertStringToDate(json['OrderDate']),
|
||||||
|
orderLineItemNo: json['OrderLineItemNo'] as int?,
|
||||||
|
orderNo: json['OrderNo'] as int?,
|
||||||
|
partnerID: json['PartnerID'] as int?,
|
||||||
|
partnerShare: _toNum(json['PartnerShare']),
|
||||||
|
partnerShareType: json['PartnerShareType'] as String?,
|
||||||
|
patientShare: _toNum(json['PatientShare']),
|
||||||
|
patientShareWithTax: _toNum(json['PatientShareWithTax']),
|
||||||
|
patientTaxAmount: _toNum(json['PatientTaxAmount']),
|
||||||
|
procPrice: _toNum(json['ProcPrice']),
|
||||||
|
procedureCategoryID: json['ProcedureCategoryID'] as int?,
|
||||||
|
procedureCategoryName: json['ProcedureCategoryName'] as String?,
|
||||||
|
procedureID: json['ProcedureID'] as String?,
|
||||||
|
procedureName: json['ProcedureName'] as String?,
|
||||||
|
taxAmount: _toNum(json['TaxAmount']),
|
||||||
|
taxPct: _toNum(json['TaxPct']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to safely parse numeric fields that may be int/double/string/null
|
||||||
|
num? _toNum(dynamic v) {
|
||||||
|
if (v == null) return null;
|
||||||
|
if (v is num) return v;
|
||||||
|
if (v is String) return num.tryParse(v);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -0,0 +1,379 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/api/api_client.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/api_consts.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/services/logger_service.dart';
|
||||||
|
|
||||||
|
abstract class TodoSectionRepo {
|
||||||
|
Future<Either<Failure, GenericApiModel<List<AncillaryOrderItem>>>> getOnlineAncillaryOrderList();
|
||||||
|
|
||||||
|
Future<Either<Failure, GenericApiModel<List<AncillaryOrderProcedureItem>>>> getOnlineAncillaryOrderDetailsProceduresList({
|
||||||
|
required int appointmentNoVida,
|
||||||
|
required int orderNo,
|
||||||
|
required int projectID,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Either<Failure, dynamic>> checkPaymentStatus({required String transID});
|
||||||
|
|
||||||
|
Future<Either<Failure, dynamic>> createAdvancePayment({
|
||||||
|
required int projectID,
|
||||||
|
required double paymentAmount,
|
||||||
|
required String paymentReference,
|
||||||
|
required String paymentMethodName,
|
||||||
|
required int patientTypeID,
|
||||||
|
required String patientName,
|
||||||
|
required int patientID,
|
||||||
|
required String setupID,
|
||||||
|
required bool isAncillaryOrder,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Either<Failure, dynamic>> addAdvancedNumberRequest({
|
||||||
|
required String advanceNumber,
|
||||||
|
required String paymentReference,
|
||||||
|
required int appointmentID,
|
||||||
|
required int patientID,
|
||||||
|
required int patientTypeID,
|
||||||
|
required int patientOutSA,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Either<Failure, dynamic>> autoGenerateAncillaryOrdersInvoice({
|
||||||
|
required int orderNo,
|
||||||
|
required int projectID,
|
||||||
|
required int appointmentNo,
|
||||||
|
required List<dynamic> selectedProcedures,
|
||||||
|
required int languageID,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Either<Failure, dynamic>> applePayInsertRequest({required dynamic applePayInsertRequest});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodoSectionRepoImp implements TodoSectionRepo {
|
||||||
|
final ApiClient apiClient;
|
||||||
|
final LoggerService loggerService;
|
||||||
|
|
||||||
|
TodoSectionRepoImp({required this.loggerService, required this.apiClient});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, GenericApiModel<List<AncillaryOrderItem>>>> getOnlineAncillaryOrderList() async {
|
||||||
|
Map<String, dynamic> mapDevice = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
GenericApiModel<List<AncillaryOrderItem>>? apiResponse;
|
||||||
|
Failure? failure;
|
||||||
|
await apiClient.post(
|
||||||
|
ApiConsts.getOnlineAncillaryOrderList,
|
||||||
|
body: mapDevice,
|
||||||
|
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||||
|
failure = failureType;
|
||||||
|
},
|
||||||
|
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||||
|
try {
|
||||||
|
List<AncillaryOrderItem> ancillaryOrders = [];
|
||||||
|
|
||||||
|
// Parse the nested structure
|
||||||
|
if (response['AncillaryOrderList'] != null && response['AncillaryOrderList'] is List) {
|
||||||
|
final groupsList = response['AncillaryOrderList'] as List;
|
||||||
|
|
||||||
|
// Iterate through each group
|
||||||
|
for (var group in groupsList) {
|
||||||
|
if (group is Map<String, dynamic> && group['AncillaryOrderList'] != null) {
|
||||||
|
final ordersList = group['AncillaryOrderList'] as List;
|
||||||
|
final projectName = group['ProjectName'] as String?;
|
||||||
|
final projectID = group['ProjectID'] as int?;
|
||||||
|
|
||||||
|
// Parse each order item in the group
|
||||||
|
for (var orderJson in ordersList) {
|
||||||
|
if (orderJson is Map<String, dynamic>) {
|
||||||
|
ancillaryOrders.add(AncillaryOrderItem.fromJson(orderJson, projectName: projectName, projectID: projectID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiResponse = GenericApiModel<List<AncillaryOrderItem>>(
|
||||||
|
messageStatus: messageStatus,
|
||||||
|
statusCode: statusCode,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
data: ancillaryOrders,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
loggerService.logInfo("Error parsing ancillary orders: ${e.toString()}");
|
||||||
|
failure = DataParsingFailure(e.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (failure != null) return Left(failure!);
|
||||||
|
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
|
||||||
|
return Right(apiResponse!);
|
||||||
|
} catch (e) {
|
||||||
|
loggerService.logError("Unknown error in getOnlineAncillaryOrderList: ${e.toString()}");
|
||||||
|
return Left(UnknownFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, GenericApiModel<List<AncillaryOrderProcedureItem>>>> getOnlineAncillaryOrderDetailsProceduresList({
|
||||||
|
required int appointmentNoVida,
|
||||||
|
required int orderNo,
|
||||||
|
required int projectID,
|
||||||
|
}) async {
|
||||||
|
Map<String, dynamic> mapDevice = {
|
||||||
|
'AppointmentNo_Vida': appointmentNoVida,
|
||||||
|
'OrderNo': orderNo,
|
||||||
|
'ProjectID': projectID,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
GenericApiModel<List<AncillaryOrderProcedureItem>>? apiResponse;
|
||||||
|
Failure? failure;
|
||||||
|
await apiClient.post(
|
||||||
|
ApiConsts.getOnlineAncillaryOrderProcList,
|
||||||
|
body: mapDevice,
|
||||||
|
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||||
|
failure = failureType;
|
||||||
|
},
|
||||||
|
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||||
|
try {
|
||||||
|
List<AncillaryOrderProcedureItem> ancillaryOrdersProcedures = [];
|
||||||
|
|
||||||
|
// Parse the flat array structure (NOT nested like AncillaryOrderList)
|
||||||
|
if (response['AncillaryOrderProcList'] != null && response['AncillaryOrderProcList'] is List) {
|
||||||
|
final procList = response['AncillaryOrderProcList'] as List;
|
||||||
|
|
||||||
|
// Parse each procedure item directly
|
||||||
|
for (var procJson in procList) {
|
||||||
|
if (procJson is Map<String, dynamic>) {
|
||||||
|
ancillaryOrdersProcedures.add(AncillaryOrderProcedureItem.fromJson(procJson));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiResponse = GenericApiModel<List<AncillaryOrderProcedureItem>>(
|
||||||
|
messageStatus: messageStatus,
|
||||||
|
statusCode: statusCode,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
data: ancillaryOrdersProcedures,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
loggerService.logError("Error parsing ancillary Procedures: ${e.toString()}");
|
||||||
|
failure = DataParsingFailure(e.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (failure != null) return Left(failure!);
|
||||||
|
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
|
||||||
|
return Right(apiResponse!);
|
||||||
|
} catch (e) {
|
||||||
|
loggerService.logError("Unknown error in getOnlineAncillaryOrderDetailsProceduresList: ${e.toString()}");
|
||||||
|
return Left(UnknownFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, dynamic>> checkPaymentStatus({required String transID}) async {
|
||||||
|
Map<String, dynamic> mapDevice = {'ClientRequestID': transID};
|
||||||
|
|
||||||
|
try {
|
||||||
|
dynamic apiResponse;
|
||||||
|
Failure? failure;
|
||||||
|
await apiClient.post(
|
||||||
|
ApiConsts.getRequestStatusByRequestID,
|
||||||
|
body: mapDevice,
|
||||||
|
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||||
|
failure = failureType;
|
||||||
|
},
|
||||||
|
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||||
|
apiResponse = response;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (failure != null) return Left(failure!);
|
||||||
|
return Right(apiResponse);
|
||||||
|
} catch (e) {
|
||||||
|
loggerService.logError("Unknown error in checkPaymentStatus: ${e.toString()}");
|
||||||
|
return Left(UnknownFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, dynamic>> createAdvancePayment({
|
||||||
|
required int projectID,
|
||||||
|
required double paymentAmount,
|
||||||
|
required String paymentReference,
|
||||||
|
required String paymentMethodName,
|
||||||
|
required int patientTypeID,
|
||||||
|
required String patientName,
|
||||||
|
required int patientID,
|
||||||
|
required String setupID,
|
||||||
|
required bool isAncillaryOrder,
|
||||||
|
}) async {
|
||||||
|
// //VersionID (number)
|
||||||
|
// // Channel (number)
|
||||||
|
// // IPAdress (string)
|
||||||
|
// // generalid (string)
|
||||||
|
// // LanguageID (number)
|
||||||
|
// // Latitude (number)
|
||||||
|
// // Longitude (number)
|
||||||
|
// // DeviceTypeID (number)
|
||||||
|
// // PatientType (number)
|
||||||
|
// // PatientTypeID (number)
|
||||||
|
// // PatientID (number)
|
||||||
|
// // PatientOutSA (number)
|
||||||
|
// // TokenID (string)
|
||||||
|
// // SessionID (string)
|
||||||
|
|
||||||
|
Map<String, dynamic> mapDevice = {
|
||||||
|
'CustName': patientName,
|
||||||
|
'CustID': patientID,
|
||||||
|
'SetupID': setupID,
|
||||||
|
'ProjectID': projectID,
|
||||||
|
'AccountID': patientID,
|
||||||
|
'PaymentAmount': paymentAmount,
|
||||||
|
'NationalityID': null,
|
||||||
|
'DepositorName': patientName,
|
||||||
|
'CreatedBy': 3,
|
||||||
|
'PaymentMethodName': paymentMethodName,
|
||||||
|
'PaymentReference': paymentReference,
|
||||||
|
'PaymentMethod': paymentMethodName,
|
||||||
|
'IsAncillaryOrder': isAncillaryOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
dynamic apiResponse;
|
||||||
|
Failure? failure;
|
||||||
|
await apiClient.post(
|
||||||
|
ApiConsts.createAdvancePayments,
|
||||||
|
body: mapDevice,
|
||||||
|
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||||
|
failure = failureType;
|
||||||
|
},
|
||||||
|
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||||
|
apiResponse = response;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (failure != null) return Left(failure!);
|
||||||
|
return Right(apiResponse);
|
||||||
|
} catch (e) {
|
||||||
|
loggerService.logError("Unknown error in createAdvancePayment: ${e.toString()}");
|
||||||
|
return Left(UnknownFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, dynamic>> addAdvancedNumberRequest({
|
||||||
|
required String advanceNumber,
|
||||||
|
required String paymentReference,
|
||||||
|
required int appointmentID,
|
||||||
|
required int patientID,
|
||||||
|
required int patientTypeID,
|
||||||
|
required int patientOutSA,
|
||||||
|
}) async {
|
||||||
|
Map<String, dynamic> mapDevice = {
|
||||||
|
'AdvanceNumber': advanceNumber,
|
||||||
|
'PaymentReference': paymentReference,
|
||||||
|
'AppointmentID': appointmentID,
|
||||||
|
'PatientID': patientID,
|
||||||
|
'PatientTypeID': patientTypeID,
|
||||||
|
'PatientOutSA': patientOutSA,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
dynamic apiResponse;
|
||||||
|
Failure? failure;
|
||||||
|
await apiClient.post(
|
||||||
|
ApiConsts.addAdvanceNumberRequest,
|
||||||
|
body: mapDevice,
|
||||||
|
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||||
|
failure = failureType;
|
||||||
|
},
|
||||||
|
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||||
|
apiResponse = response;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (failure != null) return Left(failure!);
|
||||||
|
return Right(apiResponse);
|
||||||
|
} catch (e) {
|
||||||
|
loggerService.logError("Unknown error in addAdvancedNumberRequest: ${e.toString()}");
|
||||||
|
return Left(UnknownFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, dynamic>> autoGenerateAncillaryOrdersInvoice({
|
||||||
|
required int orderNo,
|
||||||
|
required int projectID,
|
||||||
|
required int appointmentNo,
|
||||||
|
required List<dynamic> selectedProcedures,
|
||||||
|
required int languageID,
|
||||||
|
}) async {
|
||||||
|
// Extract procedure IDs from selectedProcedures
|
||||||
|
List<String> procedureOrderIDs = [];
|
||||||
|
selectedProcedures.forEach((element) {
|
||||||
|
procedureOrderIDs.add(element["ProcedureID"].toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> mapDevice = {
|
||||||
|
'LanguageID': languageID,
|
||||||
|
'RequestAncillaryOrderInvoice': [
|
||||||
|
{
|
||||||
|
'MemberID': 102,
|
||||||
|
'ProjectID': projectID,
|
||||||
|
'AppointmentNo': appointmentNo,
|
||||||
|
'OrderNo': orderNo,
|
||||||
|
'AncillaryOrderInvoiceProcList': selectedProcedures,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'ProcedureOrderIds': procedureOrderIDs,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
dynamic apiResponse;
|
||||||
|
Failure? failure;
|
||||||
|
await apiClient.post(
|
||||||
|
ApiConsts.autoGenerateAncillaryOrdersInvoice,
|
||||||
|
body: mapDevice,
|
||||||
|
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||||
|
failure = failureType;
|
||||||
|
},
|
||||||
|
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||||
|
apiResponse = response;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (failure != null) return Left(failure!);
|
||||||
|
return Right(apiResponse);
|
||||||
|
} catch (e) {
|
||||||
|
loggerService.logError("Unknown error in autoGenerateAncillaryOrdersInvoice: ${e.toString()}");
|
||||||
|
return Left(UnknownFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, dynamic>> applePayInsertRequest({required dynamic applePayInsertRequest}) async {
|
||||||
|
Map<String, dynamic> mapDevice = {
|
||||||
|
'ApplePayInsertRequest': applePayInsertRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
dynamic apiResponse;
|
||||||
|
Failure? failure;
|
||||||
|
await apiClient.post(
|
||||||
|
ApiConsts.applePayInsertRequest,
|
||||||
|
body: mapDevice,
|
||||||
|
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||||
|
failure = failureType;
|
||||||
|
},
|
||||||
|
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||||
|
apiResponse = response;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (failure != null) return Left(failure!);
|
||||||
|
return Right(apiResponse);
|
||||||
|
} catch (e) {
|
||||||
|
loggerService.logError("Unknown error in applePayInsertRequest: ${e.toString()}");
|
||||||
|
return Left(UnknownFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,242 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart';
|
||||||
|
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
|
||||||
|
|
||||||
|
class TodoSectionViewModel extends ChangeNotifier {
|
||||||
|
TodoSectionRepo todoSectionRepo;
|
||||||
|
ErrorHandlerService errorHandlerService;
|
||||||
|
|
||||||
|
TodoSectionViewModel({required this.todoSectionRepo, required this.errorHandlerService});
|
||||||
|
|
||||||
|
initializeTodoSectionViewModel() async {
|
||||||
|
patientAncillaryOrdersList.clear();
|
||||||
|
isAncillaryOrdersLoading = true;
|
||||||
|
isAncillaryDetailsProceduresLoading = true;
|
||||||
|
await getPatientOnlineAncillaryOrderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAncillaryOrdersLoading = false;
|
||||||
|
bool isAncillaryDetailsProceduresLoading = false;
|
||||||
|
bool isProcessingPayment = false;
|
||||||
|
List<AncillaryOrderItem> patientAncillaryOrdersList = [];
|
||||||
|
List<AncillaryOrderProcedureItem> patientAncillaryOrderProceduresList = [];
|
||||||
|
|
||||||
|
void setProcessingPayment(bool value) {
|
||||||
|
isProcessingPayment = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> getPatientOnlineAncillaryOrderList({Function(dynamic)? onSuccess, Function(String)? onError}) async {
|
||||||
|
patientAncillaryOrdersList.clear();
|
||||||
|
isAncillaryOrdersLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
final result = await todoSectionRepo.getOnlineAncillaryOrderList();
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) async {
|
||||||
|
isAncillaryOrdersLoading = false;
|
||||||
|
await errorHandlerService.handleError(failure: failure);
|
||||||
|
},
|
||||||
|
(apiResponse) {
|
||||||
|
if (apiResponse.messageStatus == 2) {
|
||||||
|
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
|
||||||
|
} else if (apiResponse.messageStatus == 1) {
|
||||||
|
patientAncillaryOrdersList = apiResponse.data!;
|
||||||
|
isAncillaryOrdersLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
if (onSuccess != null) {
|
||||||
|
onSuccess(apiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> getPatientOnlineAncillaryOrderDetailsProceduresList({
|
||||||
|
Function(dynamic)? onSuccess,
|
||||||
|
Function(String)? onError,
|
||||||
|
required int appointmentNoVida,
|
||||||
|
required int orderNo,
|
||||||
|
required int projectID,
|
||||||
|
}) async {
|
||||||
|
isAncillaryDetailsProceduresLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final result = await todoSectionRepo.getOnlineAncillaryOrderDetailsProceduresList(
|
||||||
|
appointmentNoVida: appointmentNoVida,
|
||||||
|
orderNo: orderNo,
|
||||||
|
projectID: projectID,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) async {
|
||||||
|
isAncillaryDetailsProceduresLoading = false;
|
||||||
|
await errorHandlerService.handleError(failure: failure);
|
||||||
|
},
|
||||||
|
(apiResponse) {
|
||||||
|
if (apiResponse.messageStatus == 2) {
|
||||||
|
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
|
||||||
|
} else if (apiResponse.messageStatus == 1) {
|
||||||
|
patientAncillaryOrderProceduresList = apiResponse.data!;
|
||||||
|
isAncillaryDetailsProceduresLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
if (onSuccess != null) {
|
||||||
|
onSuccess(apiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> checkPaymentStatus({
|
||||||
|
required String transID,
|
||||||
|
Function(dynamic)? onSuccess,
|
||||||
|
Function(String)? onError,
|
||||||
|
}) async {
|
||||||
|
final result = await todoSectionRepo.checkPaymentStatus(transID: transID);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) async {
|
||||||
|
await errorHandlerService.handleError(failure: failure);
|
||||||
|
if (onError != null) {
|
||||||
|
onError(failure.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(response) {
|
||||||
|
if (onSuccess != null) {
|
||||||
|
onSuccess(response);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createAdvancePayment({
|
||||||
|
required int projectID,
|
||||||
|
required double paymentAmount,
|
||||||
|
required String paymentReference,
|
||||||
|
required String paymentMethodName,
|
||||||
|
required int patientTypeID,
|
||||||
|
required String patientName,
|
||||||
|
required int patientID,
|
||||||
|
required String setupID,
|
||||||
|
required bool isAncillaryOrder,
|
||||||
|
Function(dynamic)? onSuccess,
|
||||||
|
Function(String)? onError,
|
||||||
|
}) async {
|
||||||
|
final result = await todoSectionRepo.createAdvancePayment(
|
||||||
|
projectID: projectID,
|
||||||
|
paymentAmount: paymentAmount,
|
||||||
|
paymentReference: paymentReference,
|
||||||
|
paymentMethodName: paymentMethodName,
|
||||||
|
patientTypeID: patientTypeID,
|
||||||
|
patientName: patientName,
|
||||||
|
patientID: patientID,
|
||||||
|
setupID: setupID,
|
||||||
|
isAncillaryOrder: isAncillaryOrder,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) async {
|
||||||
|
await errorHandlerService.handleError(failure: failure);
|
||||||
|
if (onError != null) {
|
||||||
|
onError(failure.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(response) {
|
||||||
|
if (onSuccess != null) {
|
||||||
|
onSuccess(response);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addAdvancedNumberRequest({
|
||||||
|
required String advanceNumber,
|
||||||
|
required String paymentReference,
|
||||||
|
required int appointmentID,
|
||||||
|
required int patientID,
|
||||||
|
required int patientTypeID,
|
||||||
|
required int patientOutSA,
|
||||||
|
Function(dynamic)? onSuccess,
|
||||||
|
Function(String)? onError,
|
||||||
|
}) async {
|
||||||
|
final result = await todoSectionRepo.addAdvancedNumberRequest(
|
||||||
|
advanceNumber: advanceNumber,
|
||||||
|
paymentReference: paymentReference,
|
||||||
|
appointmentID: appointmentID,
|
||||||
|
patientID: patientID,
|
||||||
|
patientTypeID: patientTypeID,
|
||||||
|
patientOutSA: patientOutSA,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) async {
|
||||||
|
await errorHandlerService.handleError(failure: failure);
|
||||||
|
if (onError != null) {
|
||||||
|
onError(failure.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(response) {
|
||||||
|
if (onSuccess != null) {
|
||||||
|
onSuccess(response);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> autoGenerateAncillaryOrdersInvoice({
|
||||||
|
required int orderNo,
|
||||||
|
required int projectID,
|
||||||
|
required int appointmentNo,
|
||||||
|
required List<dynamic> selectedProcedures,
|
||||||
|
required int languageID,
|
||||||
|
Function(dynamic)? onSuccess,
|
||||||
|
Function(String)? onError,
|
||||||
|
}) async {
|
||||||
|
final result = await todoSectionRepo.autoGenerateAncillaryOrdersInvoice(
|
||||||
|
orderNo: orderNo,
|
||||||
|
projectID: projectID,
|
||||||
|
appointmentNo: appointmentNo,
|
||||||
|
selectedProcedures: selectedProcedures,
|
||||||
|
languageID: languageID,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) async {
|
||||||
|
await errorHandlerService.handleError(failure: failure);
|
||||||
|
if (onError != null) {
|
||||||
|
onError(failure.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(response) {
|
||||||
|
if (onSuccess != null) {
|
||||||
|
onSuccess(response);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> applePayInsertRequest({
|
||||||
|
required dynamic applePayInsertRequest,
|
||||||
|
Function(dynamic)? onSuccess,
|
||||||
|
Function(String)? onError,
|
||||||
|
}) async {
|
||||||
|
final result = await todoSectionRepo.applePayInsertRequest(applePayInsertRequest: applePayInsertRequest);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) async {
|
||||||
|
await errorHandlerService.handleError(failure: failure);
|
||||||
|
if (onError != null) {
|
||||||
|
onError(failure.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(response) {
|
||||||
|
if (onSuccess != null) {
|
||||||
|
onSuccess(response);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
|
||||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
|
||||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
|
||||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
|
||||||
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
|
||||||
|
|
||||||
class ToDoPage extends StatefulWidget {
|
|
||||||
const ToDoPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ToDoPage> createState() => _ToDoPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ToDoPageState extends State<ToDoPage> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return CollapsingListView(
|
|
||||||
title: "ToDo List".needTranslation,
|
|
||||||
isLeading: false,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(height: 16.h),
|
|
||||||
"Ancillary Orders".needTranslation.toText18(isBold: true),
|
|
||||||
|
|
||||||
],
|
|
||||||
).paddingSymmetrical(24.w, 0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,644 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
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/cache_consts.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/enums.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
|
||||||
|
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.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/common_bottom_sheet.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class AncillaryOrderPaymentPage extends StatefulWidget {
|
||||||
|
final DateTime? appointmentDate;
|
||||||
|
final int appointmentNoVida;
|
||||||
|
final int orderNo;
|
||||||
|
final int projectID;
|
||||||
|
final List<AncillaryOrderProcDetail> selectedProcedures;
|
||||||
|
final double totalAmount;
|
||||||
|
|
||||||
|
const AncillaryOrderPaymentPage({
|
||||||
|
super.key,
|
||||||
|
required this.appointmentDate,
|
||||||
|
required this.appointmentNoVida,
|
||||||
|
required this.orderNo,
|
||||||
|
required this.projectID,
|
||||||
|
required this.selectedProcedures,
|
||||||
|
required this.totalAmount,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AncillaryOrderPaymentPage> createState() => _AncillaryOrderPaymentPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
|
||||||
|
late PayfortViewModel payfortViewModel;
|
||||||
|
late AppState appState;
|
||||||
|
late TodoSectionViewModel todoSectionViewModel;
|
||||||
|
|
||||||
|
MyInAppBrowser? browser;
|
||||||
|
String selectedPaymentMethod = "";
|
||||||
|
String transID = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
scheduleMicrotask(() {
|
||||||
|
payfortViewModel.initPayfortViewModel();
|
||||||
|
payfortViewModel.setIsApplePayConfigurationLoading(false);
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
appState = getIt.get<AppState>();
|
||||||
|
todoSectionViewModel = Provider.of<TodoSectionViewModel>(context);
|
||||||
|
payfortViewModel = Provider.of<PayfortViewModel>(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.bgScaffoldColor,
|
||||||
|
body: Consumer<TodoSectionViewModel>(
|
||||||
|
builder: (context, todoVM, child) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: CollapsingListView(
|
||||||
|
title: "Select Payment Method".needTranslation,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 24.h),
|
||||||
|
|
||||||
|
// Mada Payment Option
|
||||||
|
Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 20.h,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Image.asset(AppAssets.mada, width: 72.h, height: 25.h).toShimmer2(isShow: todoVM.isProcessingPayment),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
"Mada".needTranslation.toText16(isBold: true).toShimmer2(isShow: todoVM.isProcessingPayment),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.h),
|
||||||
|
const Spacer(),
|
||||||
|
Transform.flip(
|
||||||
|
flipX: appState.isArabic(),
|
||||||
|
child: Utils.buildSvgWithAssets(
|
||||||
|
icon: AppAssets.forward_arrow_icon_small,
|
||||||
|
iconColor: AppColors.blackColor,
|
||||||
|
width: 18.h,
|
||||||
|
height: 13.h,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
).toShimmer2(isShow: todoVM.isProcessingPayment),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(16.h, 16.h),
|
||||||
|
).paddingSymmetrical(24.h, 0.h).onPress(() {
|
||||||
|
if (!todoVM.isProcessingPayment) {
|
||||||
|
selectedPaymentMethod = "MADA";
|
||||||
|
_openPaymentURL("mada");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
// Visa/Mastercard Payment Option
|
||||||
|
Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 20.h,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
|
||||||
|
SizedBox(width: 8.h),
|
||||||
|
Image.asset(AppAssets.Mastercard, width: 40.h, height: 40.h),
|
||||||
|
],
|
||||||
|
).toShimmer2(isShow: todoVM.isProcessingPayment),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
"Visa or Mastercard".needTranslation.toText16(isBold: true).toShimmer2(isShow: todoVM.isProcessingPayment),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.h),
|
||||||
|
const Spacer(),
|
||||||
|
Transform.flip(
|
||||||
|
flipX: appState.isArabic(),
|
||||||
|
child: Utils.buildSvgWithAssets(
|
||||||
|
icon: AppAssets.forward_arrow_icon_small,
|
||||||
|
iconColor: AppColors.blackColor,
|
||||||
|
width: 18.h,
|
||||||
|
height: 13.h,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
).toShimmer2(isShow: todoVM.isProcessingPayment),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(16.h, 16.h),
|
||||||
|
).paddingSymmetrical(24.h, 0.h).onPress(() {
|
||||||
|
if (!todoVM.isProcessingPayment) {
|
||||||
|
selectedPaymentMethod = "VISA";
|
||||||
|
_openPaymentURL("visa");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Payment Summary Footer
|
||||||
|
todoVM.isProcessingPayment ? SizedBox.shrink() : _buildPaymentSummary()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPaymentSummary() {
|
||||||
|
// Calculate amounts
|
||||||
|
double amountBeforeTax = 0.0;
|
||||||
|
double taxAmount = 0.0;
|
||||||
|
|
||||||
|
for (var proc in widget.selectedProcedures) {
|
||||||
|
amountBeforeTax += (proc.patientShare ?? 0);
|
||||||
|
taxAmount += (proc.patientTaxAmount ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 24.h,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
child: Consumer<PayfortViewModel>(builder: (context, payfortVM, child) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 24.h),
|
||||||
|
"Total amount to pay".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h),
|
||||||
|
SizedBox(height: 17.h),
|
||||||
|
|
||||||
|
// Amount before tax
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
"Amount before tax".needTranslation.toText14(isBold: true),
|
||||||
|
Utils.getPaymentAmountWithSymbol(
|
||||||
|
amountBeforeTax.toString().toText16(isBold: true),
|
||||||
|
AppColors.blackColor,
|
||||||
|
13,
|
||||||
|
isSaudiCurrency: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(24.h, 0.h),
|
||||||
|
|
||||||
|
// VAT 15%
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
"VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor),
|
||||||
|
Utils.getPaymentAmountWithSymbol(
|
||||||
|
taxAmount.toString().toText14(isBold: true, color: AppColors.greyTextColor),
|
||||||
|
AppColors.greyTextColor,
|
||||||
|
13,
|
||||||
|
isSaudiCurrency: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(24.h, 0.h),
|
||||||
|
|
||||||
|
SizedBox(height: 17.h),
|
||||||
|
|
||||||
|
// Total Amount
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
"".needTranslation.toText14(isBold: true),
|
||||||
|
Utils.getPaymentAmountWithSymbol(
|
||||||
|
widget.totalAmount.toString().toText24(isBold: true),
|
||||||
|
AppColors.blackColor,
|
||||||
|
17,
|
||||||
|
isSaudiCurrency: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(24.h, 0.h),
|
||||||
|
|
||||||
|
// Apple Pay Button (iOS only)
|
||||||
|
Platform.isIOS && Utils.havePrivilege(103)
|
||||||
|
? Utils.buildSvgWithAssets(
|
||||||
|
icon: AppAssets.apple_pay_button,
|
||||||
|
width: 200.h,
|
||||||
|
height: 80.h,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
).paddingSymmetrical(24.h, 0.h).onPress(() {
|
||||||
|
if (!todoSectionViewModel.isProcessingPayment) {
|
||||||
|
_startApplePay();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: SizedBox(height: 12.h),
|
||||||
|
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openPaymentURL(String paymentMethod) {
|
||||||
|
todoSectionViewModel.setProcessingPayment(true);
|
||||||
|
|
||||||
|
browser = MyInAppBrowser(
|
||||||
|
onExitCallback: _onBrowserExit,
|
||||||
|
onLoadStartCallback: _onBrowserLoadStart,
|
||||||
|
);
|
||||||
|
|
||||||
|
final user = appState.getAuthenticatedUser();
|
||||||
|
transID = Utils.getAdvancePaymentTransID(
|
||||||
|
widget.projectID,
|
||||||
|
user!.patientId!,
|
||||||
|
);
|
||||||
|
|
||||||
|
browser!.openPaymentBrowser(
|
||||||
|
widget.totalAmount,
|
||||||
|
"Ancillary Order Payment",
|
||||||
|
transID,
|
||||||
|
widget.projectID.toString(),
|
||||||
|
user.emailAddress ?? "CustID_${user.patientId}@HMG.com",
|
||||||
|
paymentMethod,
|
||||||
|
user.patientType ?? 1,
|
||||||
|
"${user.firstName} ${user.lastName}",
|
||||||
|
user.patientId,
|
||||||
|
user,
|
||||||
|
browser!,
|
||||||
|
false,
|
||||||
|
"3",
|
||||||
|
ServiceTypeEnum.ancillaryOrder.getIdFromServiceEnum().toString(),
|
||||||
|
context,
|
||||||
|
null,
|
||||||
|
widget.appointmentNoVida,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onBrowserLoadStart(String url) {
|
||||||
|
log("onBrowserLoadStart: $url");
|
||||||
|
|
||||||
|
for (var element in MyInAppBrowser.successURLS) {
|
||||||
|
if (url.contains(element)) {
|
||||||
|
if (browser!.isOpened()) browser!.close();
|
||||||
|
MyInAppBrowser.isPaymentDone = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var element in MyInAppBrowser.errorURLS) {
|
||||||
|
if (url.contains(element)) {
|
||||||
|
if (browser!.isOpened()) browser!.close();
|
||||||
|
MyInAppBrowser.isPaymentDone = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onBrowserExit(bool isPaymentMade) {
|
||||||
|
log("onBrowserExit Called: $isPaymentMade");
|
||||||
|
_checkPaymentStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkPaymentStatus() {
|
||||||
|
LoaderBottomSheet.showLoader(loadingText: "Checking payment status, Please wait...".needTranslation);
|
||||||
|
|
||||||
|
todoSectionViewModel.checkPaymentStatus(
|
||||||
|
transID: transID,
|
||||||
|
onSuccess: (response) {
|
||||||
|
String paymentInfo = response['Response_Message'];
|
||||||
|
|
||||||
|
if (paymentInfo == 'Success') {
|
||||||
|
// Extract payment details from response
|
||||||
|
final paymentAmount = response['Amount'] ?? widget.totalAmount;
|
||||||
|
final fortId = response['Fort_id'] ?? transID;
|
||||||
|
final paymentMethod = response['PaymentMethod'] ?? selectedPaymentMethod;
|
||||||
|
|
||||||
|
// Call createAdvancePayment with the payment details
|
||||||
|
_createAdvancePayment(
|
||||||
|
paymentAmount: paymentAmount is String ? double.parse(paymentAmount) : paymentAmount.toDouble(),
|
||||||
|
paymentReference: fortId,
|
||||||
|
paymentMethod: paymentMethod,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
todoSectionViewModel.setProcessingPayment(false);
|
||||||
|
Utils.showToast(response['Response_Message']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
todoSectionViewModel.setProcessingPayment(false);
|
||||||
|
Utils.showToast(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createAdvancePayment({
|
||||||
|
required double paymentAmount,
|
||||||
|
required String paymentReference,
|
||||||
|
required String paymentMethod,
|
||||||
|
}) {
|
||||||
|
LoaderBottomSheet.showLoader(loadingText: "Processing payment, Please wait...".needTranslation);
|
||||||
|
|
||||||
|
final user = appState.getAuthenticatedUser();
|
||||||
|
|
||||||
|
todoSectionViewModel.createAdvancePayment(
|
||||||
|
projectID: widget.projectID,
|
||||||
|
paymentAmount: paymentAmount,
|
||||||
|
paymentReference: paymentReference,
|
||||||
|
paymentMethodName: paymentMethod,
|
||||||
|
patientTypeID: user!.patientType ?? 1,
|
||||||
|
patientName: "${user.firstName} ${user.lastName}",
|
||||||
|
patientID: user.patientId!,
|
||||||
|
setupID: "010266",
|
||||||
|
isAncillaryOrder: true,
|
||||||
|
onSuccess: (response) {
|
||||||
|
// Extract advance number from response
|
||||||
|
final advanceNumber =
|
||||||
|
response['OnlineCheckInAppointments']?[0]?['AdvanceNumber'] ?? response['OnlineCheckInAppointments']?[0]?['AdvanceNumber_VP'] ?? '';
|
||||||
|
|
||||||
|
if (advanceNumber.isNotEmpty) {
|
||||||
|
_addAdvancedNumberRequest(
|
||||||
|
advanceNumber: advanceNumber.toString(),
|
||||||
|
paymentReference: paymentReference,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
todoSectionViewModel.setProcessingPayment(false);
|
||||||
|
Utils.showToast("Failed to get advance number");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
todoSectionViewModel.setProcessingPayment(false);
|
||||||
|
Utils.showToast(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addAdvancedNumberRequest({
|
||||||
|
required String advanceNumber,
|
||||||
|
required String paymentReference,
|
||||||
|
}) {
|
||||||
|
LoaderBottomSheet.showLoader(loadingText: "Finalizing payment, Please wait...".needTranslation);
|
||||||
|
|
||||||
|
final user = appState.getAuthenticatedUser();
|
||||||
|
|
||||||
|
todoSectionViewModel.addAdvancedNumberRequest(
|
||||||
|
advanceNumber: advanceNumber,
|
||||||
|
paymentReference: paymentReference,
|
||||||
|
appointmentID: 0,
|
||||||
|
patientID: user!.patientId!,
|
||||||
|
patientTypeID: user.patientType ?? 1,
|
||||||
|
patientOutSA: user.outSa ?? 0,
|
||||||
|
onSuccess: (response) {
|
||||||
|
// After adding advance number, generate invoice
|
||||||
|
_autoGenerateInvoice();
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
todoSectionViewModel.setProcessingPayment(false);
|
||||||
|
Utils.showToast(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _autoGenerateInvoice() {
|
||||||
|
LoaderBottomSheet.showLoader(loadingText: "Generating invoice, Please wait...".needTranslation);
|
||||||
|
|
||||||
|
List<dynamic> selectedProcListAPI = widget.selectedProcedures.map((element) {
|
||||||
|
return {
|
||||||
|
"ApprovalLineItemNo": element.approvalLineItemNo,
|
||||||
|
"OrderLineItemNo": element.orderLineItemNo,
|
||||||
|
"ProcedureID": element.procedureID,
|
||||||
|
};
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
todoSectionViewModel.autoGenerateAncillaryOrdersInvoice(
|
||||||
|
orderNo: widget.orderNo,
|
||||||
|
projectID: widget.projectID,
|
||||||
|
appointmentNo: widget.appointmentNoVida,
|
||||||
|
selectedProcedures: selectedProcListAPI,
|
||||||
|
languageID: appState.isArabic() ? 1 : 2,
|
||||||
|
onSuccess: (response) {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
|
||||||
|
final invoiceNo = response['AncillaryOrderInvoiceList']?[0]?['InvoiceNo'];
|
||||||
|
|
||||||
|
_showSuccessDialog(invoiceNo);
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
todoSectionViewModel.setProcessingPayment(false);
|
||||||
|
Utils.showToast(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSuccessDialog(dynamic invoiceNo) {
|
||||||
|
todoSectionViewModel.setProcessingPayment(false);
|
||||||
|
|
||||||
|
log("Ancillary order payment successful! Invoice #: $invoiceNo");
|
||||||
|
|
||||||
|
// Show success message and navigate
|
||||||
|
Utils.showToast("Payment successful! Invoice #: $invoiceNo");
|
||||||
|
// Navigate back to home after a short delay
|
||||||
|
Future.delayed(Duration(seconds: 1), () {
|
||||||
|
showCommonBottomSheetWithoutHeight(
|
||||||
|
context,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
"Here is your invoice #: ".needTranslation.toText14(
|
||||||
|
color: AppColors.textColorLight,
|
||||||
|
weight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
SizedBox(width: 4.w),
|
||||||
|
("12345").toText16(isBold: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 24.h),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: CustomButton(
|
||||||
|
height: 56.h,
|
||||||
|
text: LocaleKeys.ok.tr(),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
CustomPageRoute(
|
||||||
|
page: LandingNavigation(),
|
||||||
|
),
|
||||||
|
(r) => false);
|
||||||
|
},
|
||||||
|
backgroundColor: AppColors.secondaryLightRedColor,
|
||||||
|
borderColor: AppColors.secondaryLightRedColor,
|
||||||
|
textColor: AppColors.primaryRedColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// title: "Payment Completed Successfully".needTranslation,
|
||||||
|
titleWidget: Utils.getSuccessWidget(loadingText: "Payment Completed Successfully".needTranslation),
|
||||||
|
isCloseButtonVisible: false,
|
||||||
|
isDismissible: false,
|
||||||
|
isFullScreen: false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_startApplePay() async {
|
||||||
|
showCommonBottomSheet(
|
||||||
|
context,
|
||||||
|
child: Utils.getLoadingWidget(),
|
||||||
|
callBackFunc: (str) {},
|
||||||
|
title: "",
|
||||||
|
height: ResponsiveExtension.screenHeight * 0.3,
|
||||||
|
isCloseButtonVisible: false,
|
||||||
|
isDismissible: false,
|
||||||
|
isFullScreen: false,
|
||||||
|
);
|
||||||
|
final user = appState.getAuthenticatedUser();
|
||||||
|
transID = Utils.getAdvancePaymentTransID(widget.projectID, user!.patientId!);
|
||||||
|
|
||||||
|
ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest();
|
||||||
|
await payfortViewModel.getPayfortConfigurations(
|
||||||
|
serviceId: ServiceTypeEnum.ancillaryOrder.getIdFromServiceEnum(),
|
||||||
|
projectId: widget.projectID,
|
||||||
|
integrationId: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
applePayInsertRequest.clientRequestID = transID;
|
||||||
|
applePayInsertRequest.clinicID = 0;
|
||||||
|
|
||||||
|
applePayInsertRequest.currency = appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED";
|
||||||
|
applePayInsertRequest.customerEmail = "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com";
|
||||||
|
applePayInsertRequest.customerID = appState.getAuthenticatedUser()!.patientId.toString();
|
||||||
|
applePayInsertRequest.customerName = "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}";
|
||||||
|
|
||||||
|
applePayInsertRequest.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken);
|
||||||
|
applePayInsertRequest.voipToken = await Utils.getStringFromPrefs(CacheConst.voipToken);
|
||||||
|
applePayInsertRequest.doctorID = 0;
|
||||||
|
applePayInsertRequest.projectID = widget.projectID.toString();
|
||||||
|
applePayInsertRequest.serviceID = ServiceTypeEnum.ancillaryOrder.getIdFromServiceEnum().toString();
|
||||||
|
applePayInsertRequest.channelID = 3;
|
||||||
|
applePayInsertRequest.patientID = appState.getAuthenticatedUser()!.patientId.toString();
|
||||||
|
applePayInsertRequest.patientTypeID = appState.getAuthenticatedUser()!.patientType;
|
||||||
|
applePayInsertRequest.patientOutSA = appState.getAuthenticatedUser()!.outSa;
|
||||||
|
applePayInsertRequest.appointmentDate = DateUtil.convertDateToString(widget.appointmentDate ?? DateTime.now());
|
||||||
|
applePayInsertRequest.appointmentNo = widget.appointmentNoVida;
|
||||||
|
applePayInsertRequest.orderDescription = "Ancillary Order Payment";
|
||||||
|
applePayInsertRequest.liveServiceID = "0";
|
||||||
|
applePayInsertRequest.latitude = "0.0";
|
||||||
|
applePayInsertRequest.longitude = "0.0";
|
||||||
|
applePayInsertRequest.amount = widget.totalAmount.toString();
|
||||||
|
applePayInsertRequest.isSchedule = "0";
|
||||||
|
applePayInsertRequest.language = appState.isArabic() ? 'ar' : 'en';
|
||||||
|
applePayInsertRequest.languageID = appState.isArabic() ? 1 : 2;
|
||||||
|
applePayInsertRequest.userName = appState.getAuthenticatedUser()!.patientId;
|
||||||
|
applePayInsertRequest.responseContinueURL = "http://hmg.com/Documents/success.html";
|
||||||
|
applePayInsertRequest.backClickUrl = "http://hmg.com/Documents/success.html";
|
||||||
|
applePayInsertRequest.paymentOption = "ApplePay";
|
||||||
|
|
||||||
|
applePayInsertRequest.isMobSDK = true;
|
||||||
|
applePayInsertRequest.merchantReference = transID;
|
||||||
|
applePayInsertRequest.merchantIdentifier = payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier;
|
||||||
|
applePayInsertRequest.commandType = "PURCHASE";
|
||||||
|
applePayInsertRequest.signature = payfortViewModel.payfortProjectDetailsRespModel!.signature;
|
||||||
|
applePayInsertRequest.accessCode = payfortViewModel.payfortProjectDetailsRespModel!.accessCode;
|
||||||
|
applePayInsertRequest.shaRequestPhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaRequest;
|
||||||
|
applePayInsertRequest.shaResponsePhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaResponse;
|
||||||
|
applePayInsertRequest.returnURL = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest);
|
||||||
|
} catch (error) {
|
||||||
|
log("Apple Pay Insert Request Failed: $error");
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showCommonBottomSheetWithoutHeight(
|
||||||
|
context,
|
||||||
|
child: Utils.getErrorWidget(loadingText: "Failed to initialize Apple Pay. Please try again.".needTranslation),
|
||||||
|
callBackFunc: () {},
|
||||||
|
isFullScreen: false,
|
||||||
|
isCloseButtonVisible: true,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only proceed with Apple Pay if insert was successful
|
||||||
|
payfortViewModel.paymentWithApplePay(
|
||||||
|
customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
|
||||||
|
customerEmail: "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
|
||||||
|
orderDescription: "Ancillary Order Payment",
|
||||||
|
orderAmount: widget.totalAmount,
|
||||||
|
merchantReference: transID,
|
||||||
|
merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier,
|
||||||
|
applePayAccessCode: payfortViewModel.payfortProjectDetailsRespModel!.accessCode,
|
||||||
|
applePayShaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel!.shaRequest,
|
||||||
|
currency: appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED",
|
||||||
|
onFailed: (failureResult) async {
|
||||||
|
log("failureResult: ${failureResult.message.toString()}");
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showCommonBottomSheetWithoutHeight(
|
||||||
|
context,
|
||||||
|
child: Utils.getErrorWidget(loadingText: failureResult.message.toString()),
|
||||||
|
callBackFunc: () {},
|
||||||
|
isFullScreen: false,
|
||||||
|
isCloseButtonVisible: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSucceeded: (successResult) async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
log("successResult: ${successResult.responseMessage.toString()}");
|
||||||
|
selectedPaymentMethod = successResult.paymentOption ?? "VISA";
|
||||||
|
_checkPaymentStatus();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,656 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/app_state.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_order_payment_page.dart';
|
||||||
|
import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.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/routes/custom_page_route.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class AncillaryOrderDetailsList extends StatefulWidget {
|
||||||
|
final int appointmentNoVida;
|
||||||
|
final int orderNo;
|
||||||
|
final int projectID;
|
||||||
|
final String projectName;
|
||||||
|
|
||||||
|
const AncillaryOrderDetailsList({
|
||||||
|
super.key,
|
||||||
|
required this.appointmentNoVida,
|
||||||
|
required this.orderNo,
|
||||||
|
required this.projectID,
|
||||||
|
required this.projectName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AncillaryOrderDetailsList> createState() => _AncillaryOrderDetailsListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AncillaryOrderDetailsListState extends State<AncillaryOrderDetailsList> {
|
||||||
|
late TodoSectionViewModel todoSectionViewModel;
|
||||||
|
late AppState appState;
|
||||||
|
List<AncillaryOrderProcDetail> selectedProcedures = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
appState = getIt.get<AppState>();
|
||||||
|
todoSectionViewModel = context.read<TodoSectionViewModel>();
|
||||||
|
scheduleMicrotask(() async {
|
||||||
|
await todoSectionViewModel.getPatientOnlineAncillaryOrderDetailsProceduresList(
|
||||||
|
appointmentNoVida: widget.appointmentNoVida,
|
||||||
|
orderNo: widget.orderNo,
|
||||||
|
projectID: widget.projectID,
|
||||||
|
onSuccess: (response) {
|
||||||
|
_autoSelectEligibleProcedures();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _autoSelectEligibleProcedures() {
|
||||||
|
selectedProcedures.clear();
|
||||||
|
if (todoSectionViewModel.patientAncillaryOrderProceduresList.isNotEmpty) {
|
||||||
|
final procedures = todoSectionViewModel.patientAncillaryOrderProceduresList[0].ancillaryOrderProcDetailsList;
|
||||||
|
if (procedures != null) {
|
||||||
|
for (var proc in procedures) {
|
||||||
|
if (!_isProcedureDisabled(proc)) {
|
||||||
|
selectedProcedures.add(proc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isProcedureDisabled(AncillaryOrderProcDetail procedure) {
|
||||||
|
// return true;
|
||||||
|
return (procedure.isApprovalRequired == true && procedure.isApprovalCreated == false) ||
|
||||||
|
(procedure.isApprovalCreated == true && procedure.approvalNo == 0) ||
|
||||||
|
(procedure.isApprovalRequired == true && procedure.isApprovalCreated == true && procedure.approvalNo == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isProcedureSelected(AncillaryOrderProcDetail procedure) {
|
||||||
|
return selectedProcedures.contains(procedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleProcedureSelection(AncillaryOrderProcDetail procedure) {
|
||||||
|
setState(() {
|
||||||
|
if (_isProcedureSelected(procedure)) {
|
||||||
|
selectedProcedures.remove(procedure);
|
||||||
|
} else {
|
||||||
|
selectedProcedures.add(procedure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getApprovalStatusText(AncillaryOrderProcDetail procedure) {
|
||||||
|
if (procedure.isApprovalRequired == false) {
|
||||||
|
return "Cash";
|
||||||
|
} else {
|
||||||
|
if (procedure.isApprovalCreated == true && procedure.approvalNo != 0) {
|
||||||
|
return "Approved";
|
||||||
|
} else if (procedure.isApprovalRequired == true && procedure.isApprovalCreated == true && procedure.approvalNo == 0) {
|
||||||
|
return "Approval Rejected - Please visit receptionist";
|
||||||
|
} else {
|
||||||
|
return "Sent For Approval";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getTotalAmount() {
|
||||||
|
double total = 0.0;
|
||||||
|
for (var proc in selectedProcedures) {
|
||||||
|
total += (proc.patientShareWithTax ?? 0);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.bgScaffoldColor,
|
||||||
|
body: Consumer<TodoSectionViewModel>(builder: (context, viewModel, child) {
|
||||||
|
AncillaryOrderProcedureItem? orderData;
|
||||||
|
if (viewModel.patientAncillaryOrderProceduresList.isNotEmpty) {
|
||||||
|
orderData = viewModel.patientAncillaryOrderProceduresList[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: CollapsingListView(
|
||||||
|
title: "Ancillary Order Details".needTranslation,
|
||||||
|
child: viewModel.isAncillaryDetailsProceduresLoading
|
||||||
|
? _buildLoadingShimmer().paddingSymmetrical(24.w, 0)
|
||||||
|
: viewModel.patientAncillaryOrderProceduresList.isEmpty
|
||||||
|
? _buildDefaultEmptyState(context).paddingSymmetrical(24.w, 0)
|
||||||
|
: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
if (orderData != null) _buildPatientInfoCard(orderData),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
if (orderData != null) _buildProceduresSection(orderData),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(24.w, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (orderData != null) _buildStickyPaymentButton(orderData),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingShimmer() {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: 3,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return AncillaryOrderCard(
|
||||||
|
order: AncillaryOrderItem(),
|
||||||
|
isLoading: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDefaultEmptyState(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 40.h),
|
||||||
|
child: Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 12.r,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
child: Utils.getNoDataWidget(
|
||||||
|
context,
|
||||||
|
noDataText: "No Procedures available for the selected order.".needTranslation,
|
||||||
|
isSmallWidget: true,
|
||||||
|
width: 62.w,
|
||||||
|
height: 62.h,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPatientInfoCard(AncillaryOrderProcedureItem orderData) {
|
||||||
|
final user = appState.getAuthenticatedUser();
|
||||||
|
final patientName = orderData.patientName ?? user?.firstName ?? "N/A";
|
||||||
|
final patientMRN = orderData.patientID ?? user?.patientId;
|
||||||
|
final nationalID = user?.patientIdentificationNo ?? "";
|
||||||
|
|
||||||
|
// Determine gender for profile image (assuming 1 = male, 2 = female)
|
||||||
|
final gender = user?.gender ?? 1;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 24.r,
|
||||||
|
hasShadow: true,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header Row with Profile Image, Name, and QR Code
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
gender == 1 ? AppAssets.male_img : AppAssets.femaleImg,
|
||||||
|
width: 56.w,
|
||||||
|
height: 56.h,
|
||||||
|
),
|
||||||
|
SizedBox(width: 12.w),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
patientName.toText18(
|
||||||
|
isBold: true,
|
||||||
|
weight: FontWeight.w600,
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
maxlines: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
|
||||||
|
Wrap(
|
||||||
|
alignment: WrapAlignment.start,
|
||||||
|
spacing: 4.w,
|
||||||
|
runSpacing: 4.h,
|
||||||
|
children: [
|
||||||
|
AppCustomChipWidget(
|
||||||
|
// icon: AppAssets.file_icon,
|
||||||
|
labelText: "MRN: ${patientMRN ?? 'N/A'}",
|
||||||
|
iconSize: 12.w,
|
||||||
|
),
|
||||||
|
|
||||||
|
// National ID
|
||||||
|
if (nationalID.isNotEmpty)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
// icon: AppAssets.card_user,
|
||||||
|
labelText: "ID: $nationalID",
|
||||||
|
iconSize: 12.w,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Appointment Number
|
||||||
|
if (orderData.appointmentNo != null)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
// icon: AppAssets.calendar,
|
||||||
|
labelText: "Appt #: ${orderData.appointmentNo}",
|
||||||
|
iconSize: 12.w,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Order Number
|
||||||
|
if (orderData.ancillaryOrderProcDetailsList?.firstOrNull?.orderNo != null)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: "Order #: ${orderData.ancillaryOrderProcDetailsList!.first.orderNo}",
|
||||||
|
),
|
||||||
|
|
||||||
|
// Blood Group
|
||||||
|
if (user?.bloodGroup != null && user!.bloodGroup!.isNotEmpty)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
// icon: AppAssets.blood_icon,
|
||||||
|
labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w),
|
||||||
|
labelText: "Blood: ${user.bloodGroup}",
|
||||||
|
iconColor: AppColors.primaryRedColor,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Insurance Company (if applicable)
|
||||||
|
if (orderData.companyName != null && orderData.companyName!.isNotEmpty)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
icon: AppAssets.insurance_active_icon,
|
||||||
|
labelText: orderData.companyName!,
|
||||||
|
iconColor: AppColors.successColor,
|
||||||
|
backgroundColor: AppColors.successColor.withValues(alpha: 0.15),
|
||||||
|
iconSize: 12.w,
|
||||||
|
labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Policy Number
|
||||||
|
if (orderData.insurancePolicyNo != null && orderData.insurancePolicyNo!.isNotEmpty)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: "Policy: ${orderData.insurancePolicyNo}",
|
||||||
|
),
|
||||||
|
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: "Doctor: ${orderData.doctorName ?? "N/A"}",
|
||||||
|
),
|
||||||
|
if (widget.projectName.isNotEmpty)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: widget.projectName,
|
||||||
|
),
|
||||||
|
if (orderData.clinicName != null && orderData.clinicName!.isNotEmpty)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: "Clinic: ${orderData.clinicName!}",
|
||||||
|
),
|
||||||
|
if (orderData.clinicName != null && orderData.clinicName!.isNotEmpty)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: "Date: ${DateFormat('MMM dd, yyyy').format(orderData.appointmentDate!)}",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// SizedBox(height: 12.h),
|
||||||
|
//
|
||||||
|
// // Additional Details Section
|
||||||
|
// Container(
|
||||||
|
// padding: EdgeInsets.all(12.h),
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// color: AppColors.bgScaffoldColor,
|
||||||
|
// borderRadius: BorderRadius.circular(12.r),
|
||||||
|
// ),
|
||||||
|
// child: Column(
|
||||||
|
// children: [
|
||||||
|
// _buildInfoRow(
|
||||||
|
// "Doctor".needTranslation,
|
||||||
|
// orderData.doctorName ?? "N/A",
|
||||||
|
// ),
|
||||||
|
// if (orderData.clinicName != null && orderData.clinicName!.isNotEmpty)
|
||||||
|
// _buildInfoRow(
|
||||||
|
// "Clinic".needTranslation,
|
||||||
|
// orderData.clinicName!,
|
||||||
|
// ),
|
||||||
|
// if (orderData.appointmentDate != null)
|
||||||
|
// _buildInfoRow(
|
||||||
|
// "Appointment Date".needTranslation,
|
||||||
|
// DateFormat('MMM dd, yyyy').format(orderData.appointmentDate!),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
).paddingOnly(top: 16.h, right: 16.w, left: 16.w, bottom: 12.h),
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
Container(height: 1, color: AppColors.dividerColor),
|
||||||
|
|
||||||
|
// Summary Section
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSummarySection(AncillaryOrderProcedureItem orderData) {
|
||||||
|
final totalProcedures = orderData.ancillaryOrderProcDetailsList?.length ?? 0;
|
||||||
|
final selectedCount = selectedProcedures.length;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
"Procedures".needTranslation.toText12(
|
||||||
|
color: AppColors.textColorLight,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
"$selectedCount of $totalProcedures selected".toText14(
|
||||||
|
isBold: true,
|
||||||
|
weight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
"Total Amount".needTranslation.toText12(
|
||||||
|
color: AppColors.textColorLight,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Utils.getPaymentAmountWithSymbol(
|
||||||
|
_getTotalAmount().toStringAsFixed(2).toText14(
|
||||||
|
isBold: true,
|
||||||
|
weight: FontWeight.bold,
|
||||||
|
color: AppColors.primaryRedColor,
|
||||||
|
),
|
||||||
|
AppColors.textColorLight,
|
||||||
|
13,
|
||||||
|
isSaudiCurrency: true,
|
||||||
|
),
|
||||||
|
//
|
||||||
|
// _getTotalAmount().toStringAsFixed(2).toText14(
|
||||||
|
// isBold: true,
|
||||||
|
// weight: FontWeight.bold,
|
||||||
|
// color: AppColors.primaryRedColor,
|
||||||
|
// ),
|
||||||
|
// SizedBox(width: 4.w),
|
||||||
|
// "SAR".toText12(color: AppColors.textColorLight),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoRow(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 8.h),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: "$label:".toText12(color: AppColors.textColorLight, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: value.toText12(color: AppColors.textColor, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProceduresSection(AncillaryOrderProcedureItem orderData) {
|
||||||
|
if (orderData.ancillaryOrderProcDetailsList == null || orderData.ancillaryOrderProcDetailsList!.isEmpty) {
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group procedures by category
|
||||||
|
final groupedProcedures = groupBy(
|
||||||
|
orderData.ancillaryOrderProcDetailsList!,
|
||||||
|
(AncillaryOrderProcDetail proc) => proc.procedureCategoryName ?? "Other",
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: groupedProcedures.entries.map((entry) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
entry.key.toText18(isBold: true),
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
...entry.value.map((procedure) => _buildProcedureCard(procedure)),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProcedureCard(AncillaryOrderProcDetail procedure) {
|
||||||
|
final isDisabled = _isProcedureDisabled(procedure);
|
||||||
|
// final isDisabled = _isProcedureDisabled(procedure);
|
||||||
|
final isSelected = _isProcedureSelected(procedure);
|
||||||
|
|
||||||
|
return AnimationConfiguration.staggeredList(
|
||||||
|
position: 0,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
child: SlideAnimation(
|
||||||
|
verticalOffset: 100.0,
|
||||||
|
child: FadeInAnimation(
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
margin: EdgeInsets.only(bottom: 12.h),
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: isDisabled ? AppColors.greyColor : AppColors.whiteColor,
|
||||||
|
borderRadius: 24.h,
|
||||||
|
hasShadow: !isDisabled,
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: isDisabled ? null : () => _toggleProcedureSelection(procedure),
|
||||||
|
borderRadius: BorderRadius.circular(24.h),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(14.h),
|
||||||
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(24.h)),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (!isDisabled)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(right: 8.w),
|
||||||
|
child: Checkbox(
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (v) => _toggleProcedureSelection(procedure),
|
||||||
|
activeColor: AppColors.primaryRedColor,
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
(procedure.procedureName ?? "N/A").toText14(isBold: true, maxlines: 2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 8.h),
|
||||||
|
Wrap(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
spacing: 3.h,
|
||||||
|
runSpacing: 8.h,
|
||||||
|
children: [
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: _getApprovalStatusText(procedure),
|
||||||
|
// backgroundColor: ,
|
||||||
|
),
|
||||||
|
// if (procedure.procedureID != null)
|
||||||
|
// AppCustomChipWidget(
|
||||||
|
// labelText: "ID: ${procedure.procedureID}",
|
||||||
|
// ),
|
||||||
|
if (procedure.isCovered == true)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: "Covered".needTranslation,
|
||||||
|
backgroundColor: AppColors.successColor.withValues(alpha: 0.1),
|
||||||
|
textColor: AppColors.successColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
Container(height: 1, color: AppColors.dividerColor),
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
"Price".needTranslation.toText10(color: AppColors.textColorLight),
|
||||||
|
SizedBox(height: 4.h),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Utils.getPaymentAmountWithSymbol(
|
||||||
|
(procedure.patientShare ?? 0).toStringAsFixed(2).toText13(weight: FontWeight.w600),
|
||||||
|
AppColors.textColorLight,
|
||||||
|
13,
|
||||||
|
isSaudiCurrency: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
"VAT (15%)".needTranslation.toText10(color: AppColors.textColorLight),
|
||||||
|
SizedBox(height: 4.h),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Utils.getPaymentAmountWithSymbol(
|
||||||
|
(procedure.patientTaxAmount ?? 0).toStringAsFixed(2).toText13(weight: FontWeight.w600),
|
||||||
|
AppColors.textColorLight,
|
||||||
|
13,
|
||||||
|
isSaudiCurrency: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
"Total".needTranslation.toText10(color: AppColors.textColorLight),
|
||||||
|
SizedBox(height: 4.h),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Utils.getPaymentAmountWithSymbol(
|
||||||
|
(procedure.patientShareWithTax ?? 0).toStringAsFixed(2).toText13(weight: FontWeight.w600),
|
||||||
|
AppColors.textColorLight,
|
||||||
|
13,
|
||||||
|
isSaudiCurrency: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStickyPaymentButton(orderData) {
|
||||||
|
final isButtonEnabled = selectedProcedures.isNotEmpty;
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
_buildSummarySection(orderData),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
CustomButton(
|
||||||
|
borderWidth: 0,
|
||||||
|
backgroundColor: AppColors.infoLightColor,
|
||||||
|
text: "Proceed to Payment".needTranslation,
|
||||||
|
onPressed: () {
|
||||||
|
// Navigate to payment page with selected procedures
|
||||||
|
Navigator.of(context).push(
|
||||||
|
CustomPageRoute(
|
||||||
|
page: AncillaryOrderPaymentPage(
|
||||||
|
appointmentNoVida: widget.appointmentNoVida,
|
||||||
|
orderNo: widget.orderNo,
|
||||||
|
projectID: widget.projectID,
|
||||||
|
selectedProcedures: selectedProcedures,
|
||||||
|
totalAmount: _getTotalAmount(),
|
||||||
|
appointmentDate: orderData.appointmentDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isDisabled: !isButtonEnabled,
|
||||||
|
textColor: AppColors.whiteColor,
|
||||||
|
borderRadius: 12.r,
|
||||||
|
borderColor: Colors.transparent,
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||||
|
),
|
||||||
|
SizedBox(height: 22.h),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(24.w, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_procedures_details_page.dart';
|
||||||
|
import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class ToDoPage extends StatefulWidget {
|
||||||
|
const ToDoPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ToDoPage> createState() => _ToDoPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToDoPageState extends State<ToDoPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final TodoSectionViewModel todoSectionViewModel = context.read<TodoSectionViewModel>();
|
||||||
|
scheduleMicrotask(() async {
|
||||||
|
await todoSectionViewModel.initializeTodoSectionViewModel();
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingShimmer() {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: 3,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return AncillaryOrderCard(
|
||||||
|
order: AncillaryOrderItem(),
|
||||||
|
isLoading: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CollapsingListView(
|
||||||
|
title: "ToDo List".needTranslation,
|
||||||
|
isLeading: false,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
"Ancillary Orders".needTranslation.toText18(isBold: true),
|
||||||
|
Consumer<TodoSectionViewModel>(
|
||||||
|
builder: (BuildContext context, TodoSectionViewModel todoSectionViewModel, Widget? child) {
|
||||||
|
return todoSectionViewModel.isAncillaryOrdersLoading
|
||||||
|
? _buildLoadingShimmer()
|
||||||
|
: AncillaryOrdersList(
|
||||||
|
orders: todoSectionViewModel.patientAncillaryOrdersList,
|
||||||
|
onCheckIn: (order) => log("Check-in for order: ${order.orderNo}"),
|
||||||
|
onViewDetails: (order) async {
|
||||||
|
Navigator.of(context).push(CustomPageRoute(
|
||||||
|
page: AncillaryOrderDetailsList(
|
||||||
|
appointmentNoVida: order.appointmentNo ?? 0,
|
||||||
|
orderNo: order.orderNo ?? 0,
|
||||||
|
projectID: order.projectID ?? 0,
|
||||||
|
projectName: order.projectName ?? "",
|
||||||
|
)));
|
||||||
|
log("View details for order: ${order.orderNo}");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(24.w, 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,284 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/theme/colors.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';
|
||||||
|
|
||||||
|
class AncillaryOrdersList extends StatelessWidget {
|
||||||
|
final List<AncillaryOrderItem> orders;
|
||||||
|
final Function(AncillaryOrderItem order)? onCheckIn;
|
||||||
|
final Function(AncillaryOrderItem order)? onViewDetails;
|
||||||
|
|
||||||
|
const AncillaryOrdersList({
|
||||||
|
super.key,
|
||||||
|
required this.orders,
|
||||||
|
this.onCheckIn,
|
||||||
|
this.onViewDetails,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Show empty state
|
||||||
|
if (orders.isEmpty) {
|
||||||
|
return _buildDefaultEmptyState(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show orders list
|
||||||
|
return ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: orders.length,
|
||||||
|
separatorBuilder: (BuildContext context, int index) => SizedBox(height: 12.h),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final order = orders[index];
|
||||||
|
|
||||||
|
return AnimationConfiguration.staggeredList(
|
||||||
|
position: index,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
child: SlideAnimation(
|
||||||
|
verticalOffset: 100.0,
|
||||||
|
child: FadeInAnimation(
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
|
||||||
|
child: AncillaryOrderCard(
|
||||||
|
order: order,
|
||||||
|
isLoading: false,
|
||||||
|
onCheckIn: onCheckIn != null ? () => onCheckIn!(order) : null,
|
||||||
|
onViewDetails: onViewDetails != null ? () => onViewDetails!(order) : null,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDefaultEmptyState(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 40.h),
|
||||||
|
child: Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 12.r,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
child: Utils.getNoDataWidget(
|
||||||
|
context,
|
||||||
|
noDataText: "You don't have any ancillary orders yet.".needTranslation,
|
||||||
|
isSmallWidget: true,
|
||||||
|
width: 62.w,
|
||||||
|
height: 62.h,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AncillaryOrderCard extends StatelessWidget {
|
||||||
|
const AncillaryOrderCard({
|
||||||
|
super.key,
|
||||||
|
required this.order,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.onCheckIn,
|
||||||
|
this.onViewDetails,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AncillaryOrderItem order;
|
||||||
|
final bool isLoading;
|
||||||
|
final VoidCallback? onCheckIn;
|
||||||
|
final VoidCallback? onViewDetails;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 12.h),
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 24.h,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(14.h),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header Row with Order Number and Date
|
||||||
|
// Row(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
// children: [
|
||||||
|
// Row(
|
||||||
|
// children: [
|
||||||
|
// if (!isLoading)
|
||||||
|
// "Order #".needTranslation.toText14(
|
||||||
|
// color: AppColors.textColorLight,
|
||||||
|
// weight: FontWeight.w500,
|
||||||
|
// ),
|
||||||
|
// SizedBox(width: 4.w),
|
||||||
|
// (isLoading ? "12345" : "${order.orderNo ?? '-'}").toText16(isBold: true).toShimmer2(isShow: isLoading),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// if (order.orderDate != null || isLoading)
|
||||||
|
// (isLoading ? "Jan 15, 2024" : DateFormat('MMM dd, yyyy').format(order.orderDate!))
|
||||||
|
// .toText12(color: AppColors.textColorLight)
|
||||||
|
// .toShimmer2(isShow: isLoading),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
|
||||||
|
// Doctor and Clinic Info
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (!isLoading) ...[
|
||||||
|
Image.network(
|
||||||
|
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
|
||||||
|
width: 40.w,
|
||||||
|
height: 40.h,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
).circle(100.r),
|
||||||
|
SizedBox(width: 12.w),
|
||||||
|
],
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Doctor Name
|
||||||
|
if (order.doctorName != null || isLoading)
|
||||||
|
(isLoading ? "Dr. John Smith" : order.doctorName!)
|
||||||
|
.toString()
|
||||||
|
.toText14(isBold: true, maxlines: 2)
|
||||||
|
.toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
SizedBox(height: 4.h),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
|
||||||
|
// Chips for Appointment Info and Status
|
||||||
|
Wrap(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
spacing: 3.h,
|
||||||
|
runSpacing: 4.h,
|
||||||
|
children: [
|
||||||
|
// projectName
|
||||||
|
if (order.projectName != null || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: order.projectName ?? '-',
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
// orderNo
|
||||||
|
if (order.orderNo != null || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
// icon: AppAssets.calendar,
|
||||||
|
labelText: "${"Order# :".needTranslation}${order.orderNo ?? '-'}",
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
// Appointment Date
|
||||||
|
if (order.appointmentDate != null || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
icon: AppAssets.calendar,
|
||||||
|
labelText:
|
||||||
|
isLoading ? "Date: Jan 20, 2024" : "Date: ${DateFormat('MMM dd, yyyy').format(order.appointmentDate!)}".needTranslation,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
// Appointment Number
|
||||||
|
if (order.appointmentNo != null || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: isLoading ? "Appt# : 98765" : "Appt #: ${order.appointmentNo}".needTranslation,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
// Invoice Number
|
||||||
|
if (order.invoiceNo != null || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: isLoading ? "Invoice: 45678" : "Invoice: ${order.invoiceNo}".needTranslation,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
// Queued Status
|
||||||
|
if (order.isQueued == true || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: "Queued".needTranslation,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
// Check-in Available Status
|
||||||
|
if (order.isCheckInAllow == true || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: "Check-in Ready".needTranslation,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Check-in Button (if available)
|
||||||
|
if (order.isCheckInAllow == true || isLoading)
|
||||||
|
Expanded(
|
||||||
|
child: CustomButton(
|
||||||
|
text: "Check In".needTranslation,
|
||||||
|
onPressed: () {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
} else if (onCheckIn != null) {
|
||||||
|
onCheckIn!();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: AppColors.primaryRedColor,
|
||||||
|
borderColor: AppColors.primaryRedColor,
|
||||||
|
textColor: AppColors.whiteColor,
|
||||||
|
fontSize: 14.f,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
borderRadius: 10.r,
|
||||||
|
padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0),
|
||||||
|
height: 40.h,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (order.isCheckInAllow == true || isLoading) SizedBox(width: 8.w),
|
||||||
|
|
||||||
|
// View Details Button
|
||||||
|
Expanded(
|
||||||
|
child: CustomButton(
|
||||||
|
text: "View Details".needTranslation,
|
||||||
|
onPressed: () {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
} else if (onViewDetails != null) {
|
||||||
|
onViewDetails!();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: Color(0xffFEE9EA),
|
||||||
|
borderColor: Color(0xffFEE9EA),
|
||||||
|
textColor: Color(0xffED1C2B),
|
||||||
|
fontSize: 14.f,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
borderRadius: 10.r,
|
||||||
|
padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0),
|
||||||
|
height: 40.h,
|
||||||
|
iconSize: 15.h,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,274 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||||
|
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/theme/colors.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';
|
||||||
|
|
||||||
|
class AncillaryProceduresList extends StatelessWidget {
|
||||||
|
final List<AncillaryOrderItem> orders;
|
||||||
|
final Function(AncillaryOrderItem order)? onCheckIn;
|
||||||
|
final Function(AncillaryOrderItem order)? onViewDetails;
|
||||||
|
|
||||||
|
const AncillaryProceduresList({
|
||||||
|
super.key,
|
||||||
|
required this.orders,
|
||||||
|
this.onCheckIn,
|
||||||
|
this.onViewDetails,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Show empty state
|
||||||
|
if (orders.isEmpty) {
|
||||||
|
return _buildDefaultEmptyState(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show orders list
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: orders.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final order = orders[index];
|
||||||
|
|
||||||
|
return AnimationConfiguration.staggeredList(
|
||||||
|
position: index,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
child: SlideAnimation(
|
||||||
|
verticalOffset: 100.0,
|
||||||
|
child: FadeInAnimation(
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
|
||||||
|
child: AncillaryOrderCard(
|
||||||
|
order: order,
|
||||||
|
isLoading: false,
|
||||||
|
onCheckIn: onCheckIn != null ? () => onCheckIn!(order) : null,
|
||||||
|
onViewDetails: onViewDetails != null ? () => onViewDetails!(order) : null,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDefaultEmptyState(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 40.h),
|
||||||
|
child: Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 12.r,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
child: Utils.getNoDataWidget(
|
||||||
|
context,
|
||||||
|
noDataText: "You don't have any ancillary orders yet.".needTranslation,
|
||||||
|
isSmallWidget: true,
|
||||||
|
width: 62.w,
|
||||||
|
height: 62.h,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AncillaryOrderCard extends StatelessWidget {
|
||||||
|
const AncillaryOrderCard({
|
||||||
|
super.key,
|
||||||
|
required this.order,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.onCheckIn,
|
||||||
|
this.onViewDetails,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AncillaryOrderItem order;
|
||||||
|
final bool isLoading;
|
||||||
|
final VoidCallback? onCheckIn;
|
||||||
|
final VoidCallback? onViewDetails;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 12.h),
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 24.h,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(14.h),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header Row with Order Number and Date
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
"Order #".needTranslation.toText14(
|
||||||
|
color: AppColors.textColorLight,
|
||||||
|
weight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
SizedBox(width: 4.w),
|
||||||
|
(isLoading ? "12345" : "${order.orderNo ?? '-'}").toText16(isBold: true).toShimmer2(isShow: isLoading),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (order.orderDate != null || isLoading)
|
||||||
|
(isLoading ? "Jan 15, 2024" : DateFormat('MMM dd, yyyy').format(order.orderDate!))
|
||||||
|
.toText12(color: AppColors.textColorLight)
|
||||||
|
.toShimmer2(isShow: isLoading),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
|
||||||
|
// Doctor and Clinic Info
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Doctor Name
|
||||||
|
if (order.doctorName != null || isLoading)
|
||||||
|
(isLoading ? "Dr. John Smith" : order.doctorName!)
|
||||||
|
.toString()
|
||||||
|
.toText14(isBold: true, maxlines: 2)
|
||||||
|
.toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
SizedBox(height: 4.h),
|
||||||
|
|
||||||
|
// Clinic Name
|
||||||
|
if (order.clinicName != null || isLoading)
|
||||||
|
(isLoading ? "Cardiology Clinic" : order.clinicName!)
|
||||||
|
.toString()
|
||||||
|
.toText12(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppColors.greyTextColor,
|
||||||
|
maxLine: 2,
|
||||||
|
)
|
||||||
|
.toShimmer2(isShow: isLoading),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
|
||||||
|
// Chips for Appointment Info and Status
|
||||||
|
Wrap(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
spacing: 3.h,
|
||||||
|
runSpacing: 4.h,
|
||||||
|
children: [
|
||||||
|
// Appointment Date
|
||||||
|
if (order.appointmentDate != null || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
icon: AppAssets.calendar,
|
||||||
|
labelText:
|
||||||
|
isLoading ? "Date: Jan 20, 2024" : "Date: ${DateFormat('MMM dd, yyyy').format(order.appointmentDate!)}".needTranslation,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
// Appointment Number
|
||||||
|
if (order.appointmentNo != null || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: isLoading ? "Appt #: 98765" : "Appt #: ${order.appointmentNo}".needTranslation,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
// Invoice Number
|
||||||
|
if (order.invoiceNo != null || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: isLoading ? "Invoice: 45678" : "Invoice: ${order.invoiceNo}".needTranslation,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
// Queued Status
|
||||||
|
if (order.isQueued == true || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: "Queued".needTranslation,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
|
||||||
|
// Check-in Available Status
|
||||||
|
if (order.isCheckInAllow == true || isLoading)
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: "Check-in Ready".needTranslation,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Check-in Button (if available)
|
||||||
|
if (order.isCheckInAllow == true || isLoading)
|
||||||
|
Expanded(
|
||||||
|
child: CustomButton(
|
||||||
|
text: "Check In".needTranslation,
|
||||||
|
onPressed: () {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
} else if (onCheckIn != null) {
|
||||||
|
onCheckIn!();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: AppColors.primaryRedColor,
|
||||||
|
borderColor: AppColors.primaryRedColor,
|
||||||
|
textColor: AppColors.whiteColor,
|
||||||
|
fontSize: 14.f,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
borderRadius: 10.r,
|
||||||
|
padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0),
|
||||||
|
height: 40.h,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (order.isCheckInAllow == true || isLoading) SizedBox(width: 8.w),
|
||||||
|
|
||||||
|
// View Details Button
|
||||||
|
Expanded(
|
||||||
|
child: CustomButton(
|
||||||
|
text: "View Details".needTranslation,
|
||||||
|
onPressed: () {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
} else if (onViewDetails != null) {
|
||||||
|
onViewDetails!();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: Color(0xffFEE9EA),
|
||||||
|
borderColor: Color(0xffFEE9EA),
|
||||||
|
textColor: Color(0xffED1C2B),
|
||||||
|
fontSize: 14.f,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
borderRadius: 10.r,
|
||||||
|
padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0),
|
||||||
|
height: 40.h,
|
||||||
|
icon: AppAssets.arrow_forward,
|
||||||
|
iconColor: AppColors.primaryRedColor,
|
||||||
|
iconSize: 15.h,
|
||||||
|
).toShimmer2(isShow: isLoading),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue