Merge branch 'master' into faiz_dev
# Conflicts: # lib/presentation/hmg_services/services_page.dartpull/138/head
commit
460e32d25a
@ -0,0 +1,33 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class Covid19GetTestProceduresResp {
|
||||||
|
String? procedureId;
|
||||||
|
String? procedureName;
|
||||||
|
String? procedureNameN;
|
||||||
|
String? setupId;
|
||||||
|
|
||||||
|
Covid19GetTestProceduresResp({
|
||||||
|
this.procedureId,
|
||||||
|
this.procedureName,
|
||||||
|
this.procedureNameN,
|
||||||
|
this.setupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Covid19GetTestProceduresResp.fromRawJson(String str) => Covid19GetTestProceduresResp.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory Covid19GetTestProceduresResp.fromJson(Map<String, dynamic> json) => Covid19GetTestProceduresResp(
|
||||||
|
procedureId: json["ProcedureID"],
|
||||||
|
procedureName: json["ProcedureName"],
|
||||||
|
procedureNameN: json["ProcedureNameN"],
|
||||||
|
setupId: json["SetupID"],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"ProcedureID": procedureId,
|
||||||
|
"ProcedureName": procedureName,
|
||||||
|
"ProcedureNameN": procedureNameN,
|
||||||
|
"SetupID": setupId,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class Covid19GetPaymentInfo {
|
||||||
|
dynamic propertyChanged;
|
||||||
|
int? cashPriceField;
|
||||||
|
int? cashPriceTaxField;
|
||||||
|
int? cashPriceWithTaxField;
|
||||||
|
int? companyIdField;
|
||||||
|
String? companyNameField;
|
||||||
|
int? companyShareWithTaxField;
|
||||||
|
dynamic errCodeField;
|
||||||
|
int? groupIdField;
|
||||||
|
dynamic insurancePolicyNoField;
|
||||||
|
String? messageField;
|
||||||
|
dynamic patientCardIdField;
|
||||||
|
int? patientShareField;
|
||||||
|
double? patientShareWithTaxField;
|
||||||
|
double? patientTaxAmountField;
|
||||||
|
int? policyIdField;
|
||||||
|
dynamic policyNameField;
|
||||||
|
dynamic procedureIdField;
|
||||||
|
String? procedureNameField;
|
||||||
|
dynamic setupIdField;
|
||||||
|
int? statusCodeField;
|
||||||
|
dynamic subPolicyNoField;
|
||||||
|
|
||||||
|
Covid19GetPaymentInfo({
|
||||||
|
this.propertyChanged,
|
||||||
|
this.cashPriceField,
|
||||||
|
this.cashPriceTaxField,
|
||||||
|
this.cashPriceWithTaxField,
|
||||||
|
this.companyIdField,
|
||||||
|
this.companyNameField,
|
||||||
|
this.companyShareWithTaxField,
|
||||||
|
this.errCodeField,
|
||||||
|
this.groupIdField,
|
||||||
|
this.insurancePolicyNoField,
|
||||||
|
this.messageField,
|
||||||
|
this.patientCardIdField,
|
||||||
|
this.patientShareField,
|
||||||
|
this.patientShareWithTaxField,
|
||||||
|
this.patientTaxAmountField,
|
||||||
|
this.policyIdField,
|
||||||
|
this.policyNameField,
|
||||||
|
this.procedureIdField,
|
||||||
|
this.procedureNameField,
|
||||||
|
this.setupIdField,
|
||||||
|
this.statusCodeField,
|
||||||
|
this.subPolicyNoField,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Covid19GetPaymentInfo.fromRawJson(String str) => Covid19GetPaymentInfo.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory Covid19GetPaymentInfo.fromJson(Map<String, dynamic> json) => Covid19GetPaymentInfo(
|
||||||
|
propertyChanged: json["PropertyChanged"],
|
||||||
|
cashPriceField: json["cashPriceField"],
|
||||||
|
cashPriceTaxField: json["cashPriceTaxField"],
|
||||||
|
cashPriceWithTaxField: json["cashPriceWithTaxField"],
|
||||||
|
companyIdField: json["companyIdField"],
|
||||||
|
companyNameField: json["companyNameField"],
|
||||||
|
companyShareWithTaxField: json["companyShareWithTaxField"],
|
||||||
|
errCodeField: json["errCodeField"],
|
||||||
|
groupIdField: json["groupIDField"],
|
||||||
|
insurancePolicyNoField: json["insurancePolicyNoField"],
|
||||||
|
messageField: json["messageField"],
|
||||||
|
patientCardIdField: json["patientCardIDField"],
|
||||||
|
patientShareField: json["patientShareField"],
|
||||||
|
patientShareWithTaxField: json["patientShareWithTaxField"]?.toDouble(),
|
||||||
|
patientTaxAmountField: json["patientTaxAmountField"]?.toDouble(),
|
||||||
|
policyIdField: json["policyIdField"],
|
||||||
|
policyNameField: json["policyNameField"],
|
||||||
|
procedureIdField: json["procedureIdField"],
|
||||||
|
procedureNameField: json["procedureNameField"],
|
||||||
|
setupIdField: json["setupIDField"],
|
||||||
|
statusCodeField: json["statusCodeField"],
|
||||||
|
subPolicyNoField: json["subPolicyNoField"],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"PropertyChanged": propertyChanged,
|
||||||
|
"cashPriceField": cashPriceField,
|
||||||
|
"cashPriceTaxField": cashPriceTaxField,
|
||||||
|
"cashPriceWithTaxField": cashPriceWithTaxField,
|
||||||
|
"companyIdField": companyIdField,
|
||||||
|
"companyNameField": companyNameField,
|
||||||
|
"companyShareWithTaxField": companyShareWithTaxField,
|
||||||
|
"errCodeField": errCodeField,
|
||||||
|
"groupIDField": groupIdField,
|
||||||
|
"insurancePolicyNoField": insurancePolicyNoField,
|
||||||
|
"messageField": messageField,
|
||||||
|
"patientCardIDField": patientCardIdField,
|
||||||
|
"patientShareField": patientShareField,
|
||||||
|
"patientShareWithTaxField": patientShareWithTaxField,
|
||||||
|
"patientTaxAmountField": patientTaxAmountField,
|
||||||
|
"policyIdField": policyIdField,
|
||||||
|
"policyNameField": policyNameField,
|
||||||
|
"procedureIdField": procedureIdField,
|
||||||
|
"procedureNameField": procedureNameField,
|
||||||
|
"setupIDField": setupIdField,
|
||||||
|
"statusCodeField": statusCodeField,
|
||||||
|
"subPolicyNoField": subPolicyNoField,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,259 @@
|
|||||||
|
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
|
||||||
|
|
||||||
|
class VitalSignResModel {
|
||||||
|
var transNo;
|
||||||
|
var projectID;
|
||||||
|
var weightKg;
|
||||||
|
var heightCm;
|
||||||
|
var temperatureCelcius;
|
||||||
|
var pulseBeatPerMinute;
|
||||||
|
var respirationBeatPerMinute;
|
||||||
|
var bloodPressureLower;
|
||||||
|
var bloodPressureHigher;
|
||||||
|
var sAO2;
|
||||||
|
var fIO2;
|
||||||
|
var painScore;
|
||||||
|
var bodyMassIndex;
|
||||||
|
var headCircumCm;
|
||||||
|
var leanBodyWeightLbs;
|
||||||
|
var idealBodyWeightLbs;
|
||||||
|
var temperatureCelciusMethod;
|
||||||
|
var pulseRhythm;
|
||||||
|
var respirationPattern;
|
||||||
|
var bloodPressureCuffLocation;
|
||||||
|
var bloodPressureCuffSize;
|
||||||
|
var bloodPressurePatientPosition;
|
||||||
|
var painLocation;
|
||||||
|
var painDuration;
|
||||||
|
var painCharacter;
|
||||||
|
var painFrequency;
|
||||||
|
bool? isPainManagementDone;
|
||||||
|
var status;
|
||||||
|
bool? isVitalsRequired;
|
||||||
|
var patientID;
|
||||||
|
var createdOn;
|
||||||
|
var doctorID;
|
||||||
|
var clinicID;
|
||||||
|
var triageCategory;
|
||||||
|
var gCScore;
|
||||||
|
var lineItemNo;
|
||||||
|
DateTime? vitalSignDate;
|
||||||
|
var actualTimeTaken;
|
||||||
|
var sugarLevel;
|
||||||
|
var fBS;
|
||||||
|
var rBS;
|
||||||
|
var observationType;
|
||||||
|
var heartRate;
|
||||||
|
var muscleTone;
|
||||||
|
var reflexIrritability;
|
||||||
|
var bodyColor;
|
||||||
|
var isFirstAssessment;
|
||||||
|
var dateofBirth;
|
||||||
|
var timeOfBirth;
|
||||||
|
var bloodPressure;
|
||||||
|
var bloodPressureCuffLocationDesc;
|
||||||
|
var bloodPressureCuffSizeDesc;
|
||||||
|
var bloodPressurePatientPositionDesc;
|
||||||
|
var clinicName;
|
||||||
|
var doctorImageURL;
|
||||||
|
var doctorName;
|
||||||
|
var painScoreDesc;
|
||||||
|
var pulseRhythmDesc;
|
||||||
|
var respirationPatternDesc;
|
||||||
|
var temperatureCelciusMethodDesc;
|
||||||
|
var time;
|
||||||
|
|
||||||
|
VitalSignResModel(
|
||||||
|
{this.transNo,
|
||||||
|
this.projectID,
|
||||||
|
this.weightKg,
|
||||||
|
this.heightCm,
|
||||||
|
this.temperatureCelcius,
|
||||||
|
this.pulseBeatPerMinute,
|
||||||
|
this.respirationBeatPerMinute,
|
||||||
|
this.bloodPressureLower,
|
||||||
|
this.bloodPressureHigher,
|
||||||
|
this.sAO2,
|
||||||
|
this.fIO2,
|
||||||
|
this.painScore,
|
||||||
|
this.bodyMassIndex,
|
||||||
|
this.headCircumCm,
|
||||||
|
this.leanBodyWeightLbs,
|
||||||
|
this.idealBodyWeightLbs,
|
||||||
|
this.temperatureCelciusMethod,
|
||||||
|
this.pulseRhythm,
|
||||||
|
this.respirationPattern,
|
||||||
|
this.bloodPressureCuffLocation,
|
||||||
|
this.bloodPressureCuffSize,
|
||||||
|
this.bloodPressurePatientPosition,
|
||||||
|
this.painLocation,
|
||||||
|
this.painDuration,
|
||||||
|
this.painCharacter,
|
||||||
|
this.painFrequency,
|
||||||
|
this.isPainManagementDone,
|
||||||
|
this.status,
|
||||||
|
this.isVitalsRequired,
|
||||||
|
this.patientID,
|
||||||
|
this.createdOn,
|
||||||
|
this.doctorID,
|
||||||
|
this.clinicID,
|
||||||
|
this.triageCategory,
|
||||||
|
this.gCScore,
|
||||||
|
this.lineItemNo,
|
||||||
|
this.vitalSignDate,
|
||||||
|
this.actualTimeTaken,
|
||||||
|
this.sugarLevel,
|
||||||
|
this.fBS,
|
||||||
|
this.rBS,
|
||||||
|
this.observationType,
|
||||||
|
this.heartRate,
|
||||||
|
this.muscleTone,
|
||||||
|
this.reflexIrritability,
|
||||||
|
this.bodyColor,
|
||||||
|
this.isFirstAssessment,
|
||||||
|
this.dateofBirth,
|
||||||
|
this.timeOfBirth,
|
||||||
|
this.bloodPressure,
|
||||||
|
this.bloodPressureCuffLocationDesc,
|
||||||
|
this.bloodPressureCuffSizeDesc,
|
||||||
|
this.bloodPressurePatientPositionDesc,
|
||||||
|
this.clinicName,
|
||||||
|
this.doctorImageURL,
|
||||||
|
this.doctorName,
|
||||||
|
this.painScoreDesc,
|
||||||
|
this.pulseRhythmDesc,
|
||||||
|
this.respirationPatternDesc,
|
||||||
|
this.temperatureCelciusMethodDesc,
|
||||||
|
this.time});
|
||||||
|
|
||||||
|
VitalSignResModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
transNo = json['TransNo'];
|
||||||
|
projectID = json['ProjectID'];
|
||||||
|
weightKg = json['WeightKg'];
|
||||||
|
heightCm = json['HeightCm'];
|
||||||
|
temperatureCelcius = json['TemperatureCelcius'];
|
||||||
|
pulseBeatPerMinute = json['PulseBeatPerMinute'];
|
||||||
|
respirationBeatPerMinute = json['RespirationBeatPerMinute'];
|
||||||
|
bloodPressureLower = json['BloodPressureLower'];
|
||||||
|
bloodPressureHigher = json['BloodPressureHigher'];
|
||||||
|
sAO2 = json['SAO2'];
|
||||||
|
fIO2 = json['FIO2'];
|
||||||
|
painScore = json['PainScore'];
|
||||||
|
bodyMassIndex = json['BodyMassIndex'];
|
||||||
|
headCircumCm = json['HeadCircumCm'];
|
||||||
|
leanBodyWeightLbs = json['LeanBodyWeightLbs'];
|
||||||
|
idealBodyWeightLbs = json['IdealBodyWeightLbs'];
|
||||||
|
temperatureCelciusMethod = json['TemperatureCelciusMethod'];
|
||||||
|
pulseRhythm = json['PulseRhythm'];
|
||||||
|
respirationPattern = json['RespirationPattern'];
|
||||||
|
bloodPressureCuffLocation = json['BloodPressureCuffLocation'];
|
||||||
|
bloodPressureCuffSize = json['BloodPressureCuffSize'];
|
||||||
|
bloodPressurePatientPosition = json['BloodPressurePatientPosition'];
|
||||||
|
painLocation = json['PainLocation'];
|
||||||
|
painDuration = json['PainDuration'];
|
||||||
|
painCharacter = json['PainCharacter'];
|
||||||
|
painFrequency = json['PainFrequency'];
|
||||||
|
isPainManagementDone = json['IsPainManagementDone'];
|
||||||
|
status = json['Status'];
|
||||||
|
isVitalsRequired = json['IsVitalsRequired'];
|
||||||
|
patientID = json['PatientID'];
|
||||||
|
createdOn = json['CreatedOn'];
|
||||||
|
doctorID = json['DoctorID'];
|
||||||
|
clinicID = json['ClinicID'];
|
||||||
|
triageCategory = json['TriageCategory'];
|
||||||
|
gCScore = json['GCScore'];
|
||||||
|
lineItemNo = json['LineItemNo'];
|
||||||
|
vitalSignDate = DateUtil.convertStringToDate(json['CreatedOn']);
|
||||||
|
actualTimeTaken = json['ActualTimeTaken'];
|
||||||
|
sugarLevel = json['SugarLevel'];
|
||||||
|
fBS = json['FBS'];
|
||||||
|
rBS = json['RBS'];
|
||||||
|
observationType = json['ObservationType'];
|
||||||
|
heartRate = json['HeartRate'];
|
||||||
|
muscleTone = json['MuscleTone'];
|
||||||
|
reflexIrritability = json['ReflexIrritability'];
|
||||||
|
bodyColor = json['BodyColor'];
|
||||||
|
isFirstAssessment = json['IsFirstAssessment'];
|
||||||
|
dateofBirth = json['DateofBirth'];
|
||||||
|
timeOfBirth = json['TimeOfBirth'];
|
||||||
|
bloodPressure = json['BloodPressure'];
|
||||||
|
bloodPressureCuffLocationDesc = json['BloodPressureCuffLocationDesc'];
|
||||||
|
bloodPressureCuffSizeDesc = json['BloodPressureCuffSizeDesc'];
|
||||||
|
bloodPressurePatientPositionDesc = json['BloodPressurePatientPositionDesc'];
|
||||||
|
clinicName = json['ClinicName'];
|
||||||
|
doctorImageURL = json['DoctorImageURL'];
|
||||||
|
doctorName = json['DoctorName'];
|
||||||
|
painScoreDesc = json['PainScoreDesc'];
|
||||||
|
pulseRhythmDesc = json['PulseRhythmDesc'];
|
||||||
|
respirationPatternDesc = json['RespirationPatternDesc'];
|
||||||
|
temperatureCelciusMethodDesc = json['TemperatureCelciusMethodDesc'];
|
||||||
|
time = json['Time'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||||
|
data['TransNo'] = this.transNo;
|
||||||
|
data['ProjectID'] = this.projectID;
|
||||||
|
data['WeightKg'] = this.weightKg;
|
||||||
|
data['HeightCm'] = this.heightCm;
|
||||||
|
data['TemperatureCelcius'] = this.temperatureCelcius;
|
||||||
|
data['PulseBeatPerMinute'] = this.pulseBeatPerMinute;
|
||||||
|
data['RespirationBeatPerMinute'] = this.respirationBeatPerMinute;
|
||||||
|
data['BloodPressureLower'] = this.bloodPressureLower;
|
||||||
|
data['BloodPressureHigher'] = this.bloodPressureHigher;
|
||||||
|
data['SAO2'] = this.sAO2;
|
||||||
|
data['FIO2'] = this.fIO2;
|
||||||
|
data['PainScore'] = this.painScore;
|
||||||
|
data['BodyMassIndex'] = this.bodyMassIndex;
|
||||||
|
data['HeadCircumCm'] = this.headCircumCm;
|
||||||
|
data['LeanBodyWeightLbs'] = this.leanBodyWeightLbs;
|
||||||
|
data['IdealBodyWeightLbs'] = this.idealBodyWeightLbs;
|
||||||
|
data['TemperatureCelciusMethod'] = this.temperatureCelciusMethod;
|
||||||
|
data['PulseRhythm'] = this.pulseRhythm;
|
||||||
|
data['RespirationPattern'] = this.respirationPattern;
|
||||||
|
data['BloodPressureCuffLocation'] = this.bloodPressureCuffLocation;
|
||||||
|
data['BloodPressureCuffSize'] = this.bloodPressureCuffSize;
|
||||||
|
data['BloodPressurePatientPosition'] = this.bloodPressurePatientPosition;
|
||||||
|
data['PainLocation'] = this.painLocation;
|
||||||
|
data['PainDuration'] = this.painDuration;
|
||||||
|
data['PainCharacter'] = this.painCharacter;
|
||||||
|
data['PainFrequency'] = this.painFrequency;
|
||||||
|
data['IsPainManagementDone'] = this.isPainManagementDone;
|
||||||
|
data['Status'] = this.status;
|
||||||
|
data['IsVitalsRequired'] = this.isVitalsRequired;
|
||||||
|
data['PatientID'] = this.patientID;
|
||||||
|
data['CreatedOn'] = this.createdOn;
|
||||||
|
data['DoctorID'] = this.doctorID;
|
||||||
|
data['ClinicID'] = this.clinicID;
|
||||||
|
data['TriageCategory'] = this.triageCategory;
|
||||||
|
data['GCScore'] = this.gCScore;
|
||||||
|
data['LineItemNo'] = this.lineItemNo;
|
||||||
|
data['VitalSignDate'] = this.vitalSignDate;
|
||||||
|
data['ActualTimeTaken'] = this.actualTimeTaken;
|
||||||
|
data['SugarLevel'] = this.sugarLevel;
|
||||||
|
data['FBS'] = this.fBS;
|
||||||
|
data['RBS'] = this.rBS;
|
||||||
|
data['ObservationType'] = this.observationType;
|
||||||
|
data['HeartRate'] = this.heartRate;
|
||||||
|
data['MuscleTone'] = this.muscleTone;
|
||||||
|
data['ReflexIrritability'] = this.reflexIrritability;
|
||||||
|
data['BodyColor'] = this.bodyColor;
|
||||||
|
data['IsFirstAssessment'] = this.isFirstAssessment;
|
||||||
|
data['DateofBirth'] = this.dateofBirth;
|
||||||
|
data['TimeOfBirth'] = this.timeOfBirth;
|
||||||
|
data['BloodPressure'] = this.bloodPressure;
|
||||||
|
data['BloodPressureCuffLocationDesc'] = this.bloodPressureCuffLocationDesc;
|
||||||
|
data['BloodPressureCuffSizeDesc'] = this.bloodPressureCuffSizeDesc;
|
||||||
|
data['BloodPressurePatientPositionDesc'] =
|
||||||
|
this.bloodPressurePatientPositionDesc;
|
||||||
|
data['ClinicName'] = this.clinicName;
|
||||||
|
data['DoctorImageURL'] = this.doctorImageURL;
|
||||||
|
data['DoctorName'] = this.doctorName;
|
||||||
|
data['PainScoreDesc'] = this.painScoreDesc;
|
||||||
|
data['PulseRhythmDesc'] = this.pulseRhythmDesc;
|
||||||
|
data['RespirationPatternDesc'] = this.respirationPatternDesc;
|
||||||
|
data['TemperatureCelciusMethodDesc'] = this.temperatureCelciusMethodDesc;
|
||||||
|
data['Time'] = this.time;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,565 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
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/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/payfort/payfort_view_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
|
||||||
|
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:smooth_corner/smooth_corner.dart';
|
||||||
|
|
||||||
|
// Added imports required by this file
|
||||||
|
import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
|
||||||
|
import 'package:hmg_patient_app_new/core/cache_consts.dart';
|
||||||
|
|
||||||
|
/// A reusable payment screen for COVID-related payments.
|
||||||
|
///
|
||||||
|
/// This screen re-uses the same UI pattern and payment flow used by
|
||||||
|
/// `AppointmentPaymentPage` (in-app browser, Apple Pay and payfort status checks),
|
||||||
|
/// but it keeps the post-payment handling generic (shows success / failure)
|
||||||
|
/// so it can be safely used for COVID test purchases without appointment-specific APIs.
|
||||||
|
class CovidPaymentScreen extends StatefulWidget {
|
||||||
|
final double amount;
|
||||||
|
final int projectID;
|
||||||
|
final int clinicID;
|
||||||
|
final String procedureId;
|
||||||
|
final double taxAmount;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const CovidPaymentScreen({
|
||||||
|
super.key,
|
||||||
|
required this.amount,
|
||||||
|
required this.projectID,
|
||||||
|
required this.clinicID,
|
||||||
|
required this.procedureId,
|
||||||
|
this.taxAmount = 0.0,
|
||||||
|
this.title = "COVID Payment",
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CovidPaymentScreen> createState() => _CovidPaymentScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CovidPaymentScreenState extends State<CovidPaymentScreen> {
|
||||||
|
late PayfortViewModel payfortViewModel;
|
||||||
|
late AppState appState;
|
||||||
|
|
||||||
|
MyInAppBrowser? browser;
|
||||||
|
String selectedPaymentMethod = "";
|
||||||
|
String transID = "";
|
||||||
|
|
||||||
|
bool isShowTamara = false; // placeholder: could be enabled based on remote config
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// initialize payfort view model when the widget is ready
|
||||||
|
scheduleMicrotask(() {
|
||||||
|
payfortViewModel = Provider.of<PayfortViewModel>(context, listen: false);
|
||||||
|
payfortViewModel.initPayfortViewModel();
|
||||||
|
payfortViewModel.setIsApplePayConfigurationLoading(false);
|
||||||
|
// Optionally compute if Tamara should be shown by calling a remote config API.
|
||||||
|
// For now keep it false (unless the app provides an API for it).
|
||||||
|
// Enable Tamara payment option for COVID screen
|
||||||
|
setState(() {
|
||||||
|
isShowTamara = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
appState = getIt.get<AppState>();
|
||||||
|
payfortViewModel = Provider.of<PayfortViewModel>(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.bgScaffoldColor,
|
||||||
|
body: CollapsingListView(
|
||||||
|
title: widget.title.needTranslation,
|
||||||
|
bottomChild: Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 24.h,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
child: 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),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
"Amount before tax".needTranslation.toText14(isBold: true),
|
||||||
|
Utils.getPaymentAmountWithSymbol(( (widget.amount - widget.taxAmount).toString()).toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: true),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(24.h, 0.h),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
"VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor),
|
||||||
|
// Show VAT amount passed from review screen
|
||||||
|
Utils.getPaymentAmountWithSymbol((widget.taxAmount.toString()).toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, isSaudiCurrency: true),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(24.h, 0.h),
|
||||||
|
SizedBox(height: 17.h),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
"".needTranslation.toText14(isBold: true),
|
||||||
|
Utils.getPaymentAmountWithSymbol(widget.amount.toString().toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: true),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(24.h, 0.h),
|
||||||
|
|
||||||
|
// Apple Pay (iOS)
|
||||||
|
Platform.isIOS
|
||||||
|
? Utils.buildSvgWithAssets(
|
||||||
|
icon: AppAssets.apple_pay_button,
|
||||||
|
width: 200.h,
|
||||||
|
height: 80.h,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
).paddingSymmetrical(24.h, 0.h).onPress(() {
|
||||||
|
if (Utils.havePrivilege(103)) {
|
||||||
|
startApplePay();
|
||||||
|
} else {
|
||||||
|
openPaymentURL("ApplePay");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: SizedBox(height: 12.h),
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
|
||||||
|
// Action buttons: Cancel + Next (Next opens default payment flow - e.g. Visa)
|
||||||
|
// Padding(
|
||||||
|
// padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 24.h),
|
||||||
|
// child: Row(
|
||||||
|
// children: [
|
||||||
|
// Expanded(
|
||||||
|
// child: CustomButton(
|
||||||
|
// height: 56.h,
|
||||||
|
// text: LocaleKeys.cancel.tr(),
|
||||||
|
// onPressed: () {
|
||||||
|
// Navigator.of(context).pop();
|
||||||
|
// },
|
||||||
|
// backgroundColor: AppColors.secondaryLightRedColor,
|
||||||
|
// borderColor: AppColors.secondaryLightRedColor,
|
||||||
|
// textColor: AppColors.primaryRedColor,
|
||||||
|
// icon: AppAssets.cancel,
|
||||||
|
// iconColor: AppColors.primaryRedColor,
|
||||||
|
// borderRadius: 12.r,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// SizedBox(width: 8.h),
|
||||||
|
// Expanded(
|
||||||
|
// child: CustomButton(
|
||||||
|
// height: 56.h,
|
||||||
|
// text: "Next".needTranslation,
|
||||||
|
// onPressed: () {
|
||||||
|
// // Default to Visa for Next
|
||||||
|
// selectedPaymentMethod = "VISA";
|
||||||
|
// openPaymentURL("visa");
|
||||||
|
// },
|
||||||
|
// backgroundColor: AppColors.primaryRedColor,
|
||||||
|
// borderColor: AppColors.primaryRedColor,
|
||||||
|
// textColor: AppColors.whiteColor,
|
||||||
|
// fontSize: 16.f,
|
||||||
|
// fontWeight: FontWeight.w500,
|
||||||
|
// borderRadius: 12.r,
|
||||||
|
// padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||||
|
// icon: AppAssets.add_icon,
|
||||||
|
// iconColor: AppColors.whiteColor,
|
||||||
|
// iconSize: 18.h,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingSymmetrical(0.h, 0.h),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 24.h),
|
||||||
|
|
||||||
|
// MADA tile
|
||||||
|
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),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
"Mada".needTranslation.toText16(isBold: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(16.h, 16.h),
|
||||||
|
).paddingSymmetrical(24.h, 0.h).onPress(() {
|
||||||
|
selectedPaymentMethod = "MADA";
|
||||||
|
openPaymentURL("MADA");
|
||||||
|
}),
|
||||||
|
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
// Visa / Mastercard tile
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
"Visa or Mastercard".needTranslation.toText16(isBold: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(16.h, 16.h),
|
||||||
|
).paddingSymmetrical(24.h, 0.h).onPress(() {
|
||||||
|
selectedPaymentMethod = "VISA";
|
||||||
|
openPaymentURL("VISA");
|
||||||
|
}),
|
||||||
|
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
// Optional Tamara tile (shown only if enabled)
|
||||||
|
isShowTamara
|
||||||
|
? 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.tamaraEng,
|
||||||
|
width: 72.h,
|
||||||
|
height: 25.h,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
// If PNG fails to load for any reason, log and fallback to SVG asset
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
debugPrint('Failed to load Tamara PNG asset: $error');
|
||||||
|
return Utils.buildSvgWithAssets(
|
||||||
|
icon: AppAssets.tamara,
|
||||||
|
width: 72.h,
|
||||||
|
height: 25.h,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
"Tamara".needTranslation.toText16(isBold: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetrical(16.h, 16.h),
|
||||||
|
).paddingSymmetrical(24.h, 0.h).onPress(() {
|
||||||
|
selectedPaymentMethod = "TAMARA";
|
||||||
|
openPaymentURL("TAMARA");
|
||||||
|
})
|
||||||
|
: SizedBox.shrink(),
|
||||||
|
|
||||||
|
SizedBox(height: 24.h),
|
||||||
|
|
||||||
|
// Bottom payment summary
|
||||||
|
|
||||||
|
SizedBox(height: 24.h),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onBrowserLoadStart(String url) {
|
||||||
|
// Generic loader hook: detect success / error patterns from the in-app browser.
|
||||||
|
// Keep parsing defensive: use Uri.parse where possible.
|
||||||
|
try {
|
||||||
|
final uri = Uri.tryParse(url);
|
||||||
|
if (selectedPaymentMethod == "TAMARA" && uri != null) {
|
||||||
|
// tamara returns different query param names depending on platform; defensive checks
|
||||||
|
final params = uri.queryParameters;
|
||||||
|
if (params.isNotEmpty) {
|
||||||
|
// example keys: 'status', 'AuthorizePaymentId' (android) or 'paymentStatus', 'orderId' (iOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('onBrowserLoadStart parse error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
MyInAppBrowser.successURLS.forEach((element) {
|
||||||
|
if (url.contains(element)) {
|
||||||
|
browser?.close();
|
||||||
|
MyInAppBrowser.isPaymentDone = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
MyInAppBrowser.errorURLS.forEach((element) {
|
||||||
|
if (url.contains(element)) {
|
||||||
|
browser?.close();
|
||||||
|
MyInAppBrowser.isPaymentDone = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onBrowserExit(bool isPaymentMade) async {
|
||||||
|
// When browser closes, check payment status using payfort view model
|
||||||
|
await checkPaymentStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> checkPaymentStatus() async {
|
||||||
|
LoaderBottomSheet.showLoader(loadingText: "Checking payment status, Please wait...".needTranslation);
|
||||||
|
try {
|
||||||
|
await payfortViewModel.checkPaymentStatus(transactionID: transID, onSuccess: (apiResponse) async {
|
||||||
|
// treat any successful responseMessage as success; otherwise show generic error
|
||||||
|
final success = payfortViewModel.payfortCheckPaymentStatusResponseModel?.responseMessage?.toLowerCase() == 'success';
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
if (success) {
|
||||||
|
showCommonBottomSheetWithoutHeight(
|
||||||
|
context,
|
||||||
|
child: Utils.getSuccessWidget(loadingText: "Payment successful".needTranslation),
|
||||||
|
callBackFunc: () {},
|
||||||
|
isFullScreen: false,
|
||||||
|
isCloseButtonVisible: true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showCommonBottomSheetWithoutHeight(
|
||||||
|
context,
|
||||||
|
child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation),
|
||||||
|
callBackFunc: () {},
|
||||||
|
isFullScreen: false,
|
||||||
|
isCloseButtonVisible: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
showCommonBottomSheetWithoutHeight(
|
||||||
|
context,
|
||||||
|
child: Utils.getErrorWidget(loadingText: err.toString()),
|
||||||
|
callBackFunc: () {},
|
||||||
|
isFullScreen: false,
|
||||||
|
isCloseButtonVisible: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void openPaymentURL(String paymentMethod) {
|
||||||
|
browser = MyInAppBrowser(onExitCallback: onBrowserExit, onLoadStartCallback: onBrowserLoadStart, context: context);
|
||||||
|
transID = Utils.getAppointmentTransID(widget.projectID, widget.clinicID, DateTime.now().millisecondsSinceEpoch);
|
||||||
|
|
||||||
|
// Open payment browser with essential parameters; many fields are simplified here
|
||||||
|
browser?.openPaymentBrowser(
|
||||||
|
widget.amount,
|
||||||
|
"COVID Test Payment",
|
||||||
|
transID,
|
||||||
|
widget.projectID.toString(),
|
||||||
|
"CustID_${appState.getAuthenticatedUser()?.patientId ?? ''}@HMG.com",
|
||||||
|
selectedPaymentMethod,
|
||||||
|
appState.getAuthenticatedUser()?.patientType.toString() ?? "",
|
||||||
|
appState.getAuthenticatedUser() != null ? "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" : "",
|
||||||
|
appState.getAuthenticatedUser()?.patientId.toString() ?? "",
|
||||||
|
appState.getAuthenticatedUser() ?? (null as dynamic),
|
||||||
|
browser!,
|
||||||
|
false,
|
||||||
|
"2",
|
||||||
|
"",
|
||||||
|
context,
|
||||||
|
DateTime.now().toString(),
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"3",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startApplePay() async {
|
||||||
|
showCommonBottomSheet(
|
||||||
|
context,
|
||||||
|
child: Utils.getLoadingWidget(),
|
||||||
|
callBackFunc: (str) {},
|
||||||
|
title: "",
|
||||||
|
height: ResponsiveExtension.screenHeight * 0.3,
|
||||||
|
isCloseButtonVisible: false,
|
||||||
|
isDismissible: false,
|
||||||
|
isFullScreen: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
transID = Utils.getAppointmentTransID(widget.projectID, widget.clinicID, DateTime.now().millisecondsSinceEpoch);
|
||||||
|
|
||||||
|
// Prepare a minimal apple pay request using payfortViewModel's configuration
|
||||||
|
try {
|
||||||
|
await payfortViewModel.getPayfortConfigurations(serviceId: 0, projectId: widget.projectID, integrationId: 2);
|
||||||
|
|
||||||
|
// Build minimal apple pay request (model omitted here to keep things generic)
|
||||||
|
ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest(
|
||||||
|
clientRequestID: transID,
|
||||||
|
clinicID: widget.clinicID,
|
||||||
|
currency: appState.getAuthenticatedUser() != null && appState.getAuthenticatedUser()!.outSa == 0 ? "SAR" : "AED",
|
||||||
|
customerEmail: "CustID_${appState.getAuthenticatedUser()?.patientId ?? ''}@HMG.com",
|
||||||
|
customerID: appState.getAuthenticatedUser()?.patientId,
|
||||||
|
customerName: appState.getAuthenticatedUser() != null ? "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" : "",
|
||||||
|
deviceToken: await Utils.getStringFromPrefs(CacheConst.pushToken),
|
||||||
|
voipToken: await Utils.getStringFromPrefs(CacheConst.voipToken),
|
||||||
|
doctorID: 0,
|
||||||
|
projectID: widget.projectID.toString(),
|
||||||
|
serviceID: "0",
|
||||||
|
channelID: 3,
|
||||||
|
patientID: appState.getAuthenticatedUser()?.patientId,
|
||||||
|
patientTypeID: appState.getAuthenticatedUser()?.patientType,
|
||||||
|
patientOutSA: appState.getAuthenticatedUser()?.outSa,
|
||||||
|
appointmentDate: DateTime.now().toString(),
|
||||||
|
appointmentNo: 0,
|
||||||
|
orderDescription: "COVID Test Payment",
|
||||||
|
liveServiceID: "0",
|
||||||
|
latitude: "0.0",
|
||||||
|
longitude: "0.0",
|
||||||
|
amount: widget.amount.toString(),
|
||||||
|
isSchedule: "0",
|
||||||
|
language: appState.isArabic() ? 'ar' : 'en',
|
||||||
|
languageID: appState.isArabic() ? 1 : 2,
|
||||||
|
userName: appState.getAuthenticatedUser()?.patientId,
|
||||||
|
responseContinueURL: "http://hmg.com/Documents/success.html",
|
||||||
|
backClickUrl: "http://hmg.com/Documents/success.html",
|
||||||
|
paymentOption: "ApplePay",
|
||||||
|
isMobSDK: true,
|
||||||
|
merchantReference: transID,
|
||||||
|
merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel?.merchantIdentifier,
|
||||||
|
commandType: "PURCHASE",
|
||||||
|
signature: payfortViewModel.payfortProjectDetailsRespModel?.signature,
|
||||||
|
accessCode: payfortViewModel.payfortProjectDetailsRespModel?.accessCode,
|
||||||
|
shaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel?.shaRequest,
|
||||||
|
shaResponsePhrase: payfortViewModel.payfortProjectDetailsRespModel?.shaResponse,
|
||||||
|
returnURL: "",
|
||||||
|
);
|
||||||
|
|
||||||
|
await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) {
|
||||||
|
// Start apple pay flow
|
||||||
|
payfortViewModel.paymentWithApplePay(
|
||||||
|
customerName: appState.getAuthenticatedUser() != null ? "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" : "",
|
||||||
|
customerEmail: "CustID_${appState.getAuthenticatedUser()?.patientId ?? ''}@HMG.com",
|
||||||
|
orderDescription: "COVID Test Payment",
|
||||||
|
orderAmount: widget.amount,
|
||||||
|
merchantReference: transID,
|
||||||
|
merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel?.merchantIdentifier ?? "",
|
||||||
|
applePayAccessCode: payfortViewModel.payfortProjectDetailsRespModel?.accessCode ?? "",
|
||||||
|
applePayShaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel?.shaRequest ?? "",
|
||||||
|
currency: appState.getAuthenticatedUser() != null && appState.getAuthenticatedUser()!.outSa == 0 ? "SAR" : "AED",
|
||||||
|
onFailed: (failureResult) async {
|
||||||
|
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();
|
||||||
|
selectedPaymentMethod = successResult.paymentOption ?? "VISA";
|
||||||
|
await checkPaymentStatus();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).catchError((e) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showCommonBottomSheetWithoutHeight(
|
||||||
|
context,
|
||||||
|
child: Utils.getErrorWidget(loadingText: e.toString()),
|
||||||
|
callBackFunc: () {},
|
||||||
|
isFullScreen: false,
|
||||||
|
isCloseButtonVisible: true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showCommonBottomSheetWithoutHeight(
|
||||||
|
context,
|
||||||
|
child: Utils.getErrorWidget(loadingText: e.toString()),
|
||||||
|
callBackFunc: () {},
|
||||||
|
isFullScreen: false,
|
||||||
|
isCloseButtonVisible: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,441 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
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/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/hmg_services/hmg_services_view_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/covid_questionnare_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
|
||||||
|
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.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';
|
||||||
|
import 'package:hmg_patient_app_new/presentation/covid19test/covid_payment_screen.dart';
|
||||||
|
|
||||||
|
class CovidReviewScreen extends StatefulWidget {
|
||||||
|
|
||||||
|
final HospitalsModel selectedHospital;
|
||||||
|
final List<CovidQuestionnaireModel> qaList;
|
||||||
|
|
||||||
|
const CovidReviewScreen({super.key, required this.selectedHospital, required this.qaList});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CovidReviewScreen> createState() => _CovidReviewScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CovidReviewScreenState extends State<CovidReviewScreen> {
|
||||||
|
|
||||||
|
late HmgServicesViewModel hmgServicesViewModel;
|
||||||
|
bool _acceptedTerms =false;
|
||||||
|
Covid19GetTestProceduresResp? _selectedProcedure;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
hmgServicesViewModel = Provider.of<HmgServicesViewModel>(context, listen: false);
|
||||||
|
scheduleMicrotask(() {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CollapsingListView(
|
||||||
|
title: "COVID-19",
|
||||||
|
bottomChild: Consumer<HmgServicesViewModel>(builder: (context, vm, _) {
|
||||||
|
final info = vm.covidPaymentInfo;
|
||||||
|
return Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 24.r,
|
||||||
|
hasShadow: true,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (info == null) ...[
|
||||||
|
// show a placeholder/loading while payment info is null
|
||||||
|
SizedBox(height: 24.h),
|
||||||
|
Center(child: CircularProgressIndicator()),
|
||||||
|
SizedBox(height: 24.h),
|
||||||
|
] else ...[
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
"Amount before tax".needTranslation.toText18(isBold: true),
|
||||||
|
Utils.getPaymentAmountWithSymbol(
|
||||||
|
(info.patientShareField ?? 0).toString().toText16(isBold: true),
|
||||||
|
AppColors.blackColor,
|
||||||
|
13,
|
||||||
|
isSaudiCurrency: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 4.h),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
"Tax Amount".needTranslation.toText14(isBold: true),
|
||||||
|
Utils.getPaymentAmountWithSymbol(
|
||||||
|
(info.patientTaxAmountField ?? 0).toString().toText16(isBold: true),
|
||||||
|
AppColors.blackColor,
|
||||||
|
13,
|
||||||
|
isSaudiCurrency: true)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 18.h),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 150.h,
|
||||||
|
child: Utils.getPaymentMethods(),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Utils.getPaymentAmountWithSymbol(
|
||||||
|
(info.patientShareWithTaxField ?? 0).toString().toText24(isBold: true),
|
||||||
|
AppColors.blackColor,
|
||||||
|
17,
|
||||||
|
isSaudiCurrency: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddingOnly(left: 16.h, top: 24.h, right: 16.h, bottom: 0.h),
|
||||||
|
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_acceptedTerms = !_acceptedTerms;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 12.r,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 20.w,
|
||||||
|
height: 20.h,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _acceptedTerms
|
||||||
|
? AppColors.primaryRedColor
|
||||||
|
: AppColors.whiteColor,
|
||||||
|
border: Border.all(
|
||||||
|
color: _acceptedTerms
|
||||||
|
? AppColors.primaryRedColor
|
||||||
|
: AppColors.greyTextColor.withValues(alpha: 0.3),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(4.r),
|
||||||
|
),
|
||||||
|
child: _acceptedTerms
|
||||||
|
? Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 14.h,
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
SizedBox(width: 12.w),
|
||||||
|
Expanded(
|
||||||
|
child: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.f,
|
||||||
|
color: AppColors.textColor,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
const TextSpan(text: "I agree to the "),
|
||||||
|
TextSpan(
|
||||||
|
text: "terms and conditions",
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.primaryRedColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: AppColors.primaryRedColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Two-button layout: Cancel (left) and Next (right)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 24.h),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: CustomButton(
|
||||||
|
height: 56.h,
|
||||||
|
text: LocaleKeys.cancel.tr(),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
backgroundColor: AppColors.secondaryLightRedColor,
|
||||||
|
borderColor: AppColors.secondaryLightRedColor,
|
||||||
|
textColor: AppColors.primaryRedColor,
|
||||||
|
// icon: AppAssets.cancel,
|
||||||
|
// iconColor: AppColors.primaryRedColor,
|
||||||
|
borderRadius: 12.r,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.h),
|
||||||
|
Expanded(
|
||||||
|
child: CustomButton(
|
||||||
|
height: 56.h,
|
||||||
|
text: "Next".needTranslation,
|
||||||
|
onPressed: () async {
|
||||||
|
// Validate selection and payment info
|
||||||
|
if (_selectedProcedure == null) {
|
||||||
|
showCommonBottomSheetWithoutHeight(
|
||||||
|
context,
|
||||||
|
child: Utils.getErrorWidget(loadingText: "Please select a procedure".needTranslation),
|
||||||
|
callBackFunc: () {},
|
||||||
|
isFullScreen: false,
|
||||||
|
isCloseButtonVisible: true,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info == null) {
|
||||||
|
// If payment info missing, attempt to fetch
|
||||||
|
LoaderBottomSheet.showLoader();
|
||||||
|
try {
|
||||||
|
await hmgServicesViewModel.getPaymentInfo(procedureID: _selectedProcedure!.procedureId!);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('getPaymentInfo error: $e');
|
||||||
|
} finally {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hmgServicesViewModel.covidPaymentInfo == null) {
|
||||||
|
showCommonBottomSheetWithoutHeight(
|
||||||
|
context,
|
||||||
|
child: Utils.getErrorWidget(loadingText: "Payment information not available".needTranslation),
|
||||||
|
callBackFunc: () {},
|
||||||
|
isFullScreen: false,
|
||||||
|
isCloseButtonVisible: true,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute amount and project/clinic IDs defensively
|
||||||
|
final paymentInfo = hmgServicesViewModel.covidPaymentInfo ?? info!;
|
||||||
|
final double amount = paymentInfo.patientShareWithTaxField ?? (paymentInfo.patientShareField?.toDouble() ?? 0.0);
|
||||||
|
|
||||||
|
// projectID may be int or string; parse defensively
|
||||||
|
int projectID = 0;
|
||||||
|
try {
|
||||||
|
final p = widget.selectedHospital.mainProjectID;
|
||||||
|
if (p is int) projectID = p;
|
||||||
|
else if (p is String) projectID = int.tryParse(p) ?? 0;
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
int clinicID = 0;
|
||||||
|
try {
|
||||||
|
clinicID = int.tryParse(widget.selectedHospital.setupID ?? '') ?? int.tryParse(widget.selectedHospital.iD?.toString() ?? '') ?? 0;
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Navigate to payment screen
|
||||||
|
Navigator.of(context).push(
|
||||||
|
CustomPageRoute(
|
||||||
|
page: CovidPaymentScreen(
|
||||||
|
amount: amount,
|
||||||
|
projectID: projectID,
|
||||||
|
clinicID: clinicID,
|
||||||
|
procedureId: _selectedProcedure!.procedureId ?? '',
|
||||||
|
taxAmount: paymentInfo.patientTaxAmountField?.toDouble() ?? 0.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
backgroundColor: AppColors.primaryRedColor,
|
||||||
|
borderColor: AppColors.primaryRedColor,
|
||||||
|
textColor: AppColors.whiteColor,
|
||||||
|
fontSize: 16.f,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
borderRadius: 12.r,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||||
|
// icon: AppAssets.add_icon,
|
||||||
|
// iconColor: AppColors.whiteColor,
|
||||||
|
iconSize: 18.h,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(24.h),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
|
||||||
|
"Please select the procedure:".toText14(
|
||||||
|
color: AppColors.textColor,
|
||||||
|
weight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
Consumer<HmgServicesViewModel>(
|
||||||
|
builder: (context, vm, _) {
|
||||||
|
final procedures = vm.covidTestProcedureList;
|
||||||
|
|
||||||
|
// ensure default selection is first item (once available)
|
||||||
|
if (_selectedProcedure == null && procedures.isNotEmpty) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedProcedure = procedures[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
// also fetch payment info for default selection and show loader while loading
|
||||||
|
if (procedures[0].procedureId != null) {
|
||||||
|
LoaderBottomSheet.showLoader();
|
||||||
|
hmgServicesViewModel
|
||||||
|
.getPaymentInfo(procedureID: procedures[0].procedureId!)
|
||||||
|
.whenComplete(() {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a shrink-wrapped ListView.separated and match prescription styling
|
||||||
|
return Container(
|
||||||
|
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 20.h,
|
||||||
|
hasShadow: false,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.h),
|
||||||
|
child: ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: procedures.length,
|
||||||
|
separatorBuilder: (context, index) => Divider(
|
||||||
|
height: 1.h,
|
||||||
|
thickness: 1.h,
|
||||||
|
color: AppColors.borderOnlyColor.withValues(alpha: 0.05),
|
||||||
|
).paddingOnly(top:8,bottom:16.h),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = procedures[index];
|
||||||
|
// Let the radio option widget manage its own padding to avoid doubled spacing
|
||||||
|
return _buildRadioOption(value: item, title: item.procedureName ?? '');
|
||||||
|
},
|
||||||
|
).paddingOnly(bottom:16.h)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRadioOption({
|
||||||
|
required Covid19GetTestProceduresResp value,
|
||||||
|
required String title,
|
||||||
|
}) {
|
||||||
|
final bool isSelected = _selectedProcedure?.procedureId == value.procedureId;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
_selectedProcedure = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// show bottomsheet loader while fetching payment info
|
||||||
|
if (value.procedureId != null) {
|
||||||
|
LoaderBottomSheet.showLoader();
|
||||||
|
try {
|
||||||
|
await hmgServicesViewModel.getPaymentInfo(procedureID: value.procedureId!, projectID: widget.selectedHospital.mainProjectID);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('getPaymentInfo error: $e');
|
||||||
|
} finally {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 20.h,
|
||||||
|
height: 20.h,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected
|
||||||
|
? AppColors.primaryRedColor
|
||||||
|
: AppColors.greyTextColor.withValues(alpha: 0.3),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: isSelected
|
||||||
|
? Center(
|
||||||
|
child: Container(
|
||||||
|
width: 10.h,
|
||||||
|
height: 10.h,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: AppColors.primaryRedColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
SizedBox(width: 12.h),
|
||||||
|
Expanded(
|
||||||
|
child: title.toText14(
|
||||||
|
color: AppColors.textColor,
|
||||||
|
weight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
// Keep only bottom padding here and rely on the surrounding container's left/right inset
|
||||||
|
.paddingOnly(left: 0.h, right: 0.h, top: 0.h, bottom: 12.h),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,308 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.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/hmg_services/hmg_services_view_model.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.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/chip/app_custom_chip_widget.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class VitalSignPage extends StatefulWidget {
|
||||||
|
const VitalSignPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VitalSignPage> createState() => _VitalSignPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VitalSignPageState extends State<VitalSignPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final HmgServicesViewModel hmgServicesViewModel = context.read<HmgServicesViewModel>();
|
||||||
|
|
||||||
|
scheduleMicrotask(() async {
|
||||||
|
LoaderBottomSheet.showLoader(loadingText: 'Loading Vital Signs...');
|
||||||
|
await hmgServicesViewModel.getPatientVitalSign(
|
||||||
|
onSuccess: (_) {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
},
|
||||||
|
onError: (_) {
|
||||||
|
LoaderBottomSheet.hideLoader();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.bgScaffoldColor,
|
||||||
|
body: CollapsingListView(
|
||||||
|
title: 'Vital Signs',
|
||||||
|
child: Consumer<HmgServicesViewModel>(
|
||||||
|
builder: (context, viewModel, child) {
|
||||||
|
|
||||||
|
// Get the latest vital sign data (first item in the list)
|
||||||
|
VitalSignResModel? latestVitalSign = viewModel.vitalSignList.isNotEmpty
|
||||||
|
? viewModel.vitalSignList.first
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
// Main content with body image
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24.h),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Left side - Vital Sign Cards
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// BMI Card
|
||||||
|
_buildVitalSignCard(
|
||||||
|
icon: AppAssets.activity,
|
||||||
|
iconColor: AppColors.successColor,
|
||||||
|
iconBgColor: AppColors.successColor.withValues(alpha: 0.1),
|
||||||
|
label: 'BMI',
|
||||||
|
value: latestVitalSign?.bodyMassIndex?.toString() ?? '--',
|
||||||
|
unit: '',
|
||||||
|
chipText: _getBMIStatus(latestVitalSign?.bodyMassIndex),
|
||||||
|
chipBgColor: AppColors.successColor.withValues(alpha: 0.1),
|
||||||
|
chipTextColor: AppColors.successColor,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
// Height Card
|
||||||
|
_buildVitalSignCard(
|
||||||
|
icon: AppAssets.height,
|
||||||
|
iconColor: AppColors.infoColor,
|
||||||
|
iconBgColor: AppColors.infoColor.withValues(alpha: 0.1),
|
||||||
|
label: 'Height',
|
||||||
|
value: latestVitalSign?.heightCm?.toString() ?? '--',
|
||||||
|
unit: 'cm',
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
// Weight Card
|
||||||
|
_buildVitalSignCard(
|
||||||
|
icon: AppAssets.weight,
|
||||||
|
iconColor: AppColors.successColor,
|
||||||
|
iconBgColor: AppColors.successColor.withValues(alpha: 0.1),
|
||||||
|
label: 'Weight',
|
||||||
|
value: latestVitalSign?.weightKg?.toString() ?? '--',
|
||||||
|
unit: 'kg',
|
||||||
|
chipText: 'Normal',
|
||||||
|
chipBgColor: AppColors.successColor.withValues(alpha: 0.1),
|
||||||
|
chipTextColor: AppColors.successColor,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
// Blood Pressure Card
|
||||||
|
_buildVitalSignCard(
|
||||||
|
icon: AppAssets.activity,
|
||||||
|
iconColor: AppColors.warningColor,
|
||||||
|
iconBgColor: AppColors.warningColor.withValues(alpha: 0.1),
|
||||||
|
label: 'Blood Pressure',
|
||||||
|
value: latestVitalSign != null &&
|
||||||
|
latestVitalSign.bloodPressureHigher != null &&
|
||||||
|
latestVitalSign.bloodPressureLower != null
|
||||||
|
? '${latestVitalSign.bloodPressureHigher}/${latestVitalSign.bloodPressureLower}'
|
||||||
|
: '--',
|
||||||
|
unit: '',
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
// Temperature Card
|
||||||
|
_buildVitalSignCard(
|
||||||
|
icon: AppAssets.activity,
|
||||||
|
iconColor: AppColors.errorColor,
|
||||||
|
iconBgColor: AppColors.errorColor.withValues(alpha: 0.1),
|
||||||
|
label: 'Temperature',
|
||||||
|
value: latestVitalSign?.temperatureCelcius?.toString() ?? '--',
|
||||||
|
unit: '°C',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(width: 16.h),
|
||||||
|
|
||||||
|
// Right side - Body Image and Heart Rate + Respiratory Rate
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Body anatomy image
|
||||||
|
Container(
|
||||||
|
height: 280.h,
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 20.h,
|
||||||
|
hasShadow: true,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Image.asset(
|
||||||
|
AppAssets.fullBodyFront,
|
||||||
|
height: 260.h,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
// Heart Rate Card
|
||||||
|
_buildVitalSignCard(
|
||||||
|
icon: AppAssets.heart,
|
||||||
|
iconColor: AppColors.errorColor,
|
||||||
|
iconBgColor: AppColors.errorColor.withValues(alpha: 0.1),
|
||||||
|
label: 'Heart Rate',
|
||||||
|
value: latestVitalSign?.heartRate?.toString() ??
|
||||||
|
latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--',
|
||||||
|
unit: 'bpm',
|
||||||
|
chipText: 'Normal',
|
||||||
|
chipBgColor: AppColors.successColor.withValues(alpha: 0.1),
|
||||||
|
chipTextColor: AppColors.successColor,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
|
||||||
|
// Respiratory rate Card
|
||||||
|
_buildVitalSignCard(
|
||||||
|
icon: AppAssets.activity,
|
||||||
|
iconColor: AppColors.successColor,
|
||||||
|
iconBgColor: AppColors.successColor.withValues(alpha: 0.1),
|
||||||
|
label: 'Respiratory rate',
|
||||||
|
value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--',
|
||||||
|
unit: 'bpm',
|
||||||
|
chipText: 'Normal',
|
||||||
|
chipBgColor: AppColors.successColor.withValues(alpha: 0.1),
|
||||||
|
chipTextColor: AppColors.successColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 60.h),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _getBMIStatus(dynamic bmi) {
|
||||||
|
if (bmi == null) return null;
|
||||||
|
double bmiValue = double.tryParse(bmi.toString()) ?? 0;
|
||||||
|
if (bmiValue < 18.5) return 'Underweight';
|
||||||
|
if (bmiValue < 25) return 'Normal';
|
||||||
|
if (bmiValue < 30) return 'Overweight';
|
||||||
|
return 'Obese';
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVitalSignCard({
|
||||||
|
required String icon,
|
||||||
|
required Color iconColor,
|
||||||
|
required Color iconBgColor,
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
required String unit,
|
||||||
|
String? chipText,
|
||||||
|
Color? chipBgColor,
|
||||||
|
Color? chipTextColor,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||||
|
color: AppColors.whiteColor,
|
||||||
|
borderRadius: 20.h,
|
||||||
|
hasShadow: true,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(12.h),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Icon with background
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8.h),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: iconBgColor,
|
||||||
|
borderRadius: BorderRadius.circular(12.r),
|
||||||
|
),
|
||||||
|
child: Utils.buildSvgWithAssets(
|
||||||
|
icon: icon,
|
||||||
|
width: 16.w,
|
||||||
|
height: 16.h,
|
||||||
|
iconColor: iconColor,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
Expanded(
|
||||||
|
child: label.toText10(
|
||||||
|
color: AppColors.textColorLight,
|
||||||
|
weight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Forward arrow
|
||||||
|
Utils.buildSvgWithAssets(
|
||||||
|
icon: AppAssets.arrow_forward,
|
||||||
|
width: 16.w,
|
||||||
|
height: 16.h,
|
||||||
|
iconColor: AppColors.textColorLight,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
|
||||||
|
// Value
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
value.toText18(
|
||||||
|
isBold: true,
|
||||||
|
color: AppColors.textColor,
|
||||||
|
),
|
||||||
|
if (unit.isNotEmpty) ...[
|
||||||
|
SizedBox(width: 4.w),
|
||||||
|
unit.toText12(
|
||||||
|
color: AppColors.textColorLight,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Chip if available
|
||||||
|
if (chipText != null) ...[
|
||||||
|
SizedBox(height: 8.h),
|
||||||
|
AppCustomChipWidget(
|
||||||
|
labelText: chipText,
|
||||||
|
backgroundColor: chipBgColor,
|
||||||
|
textColor: chipTextColor,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue