From f28762a5c55127e756490f9bc59cce6ebcf3ff29 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Sun, 14 Dec 2025 10:01:05 +0300 Subject: [PATCH 1/5] last appointment rated done. --- lib/core/dependencies.dart | 4 + .../authentication/authentication_repo.dart | 2 +- .../appointment_rating_view_model.dart | 134 ++++++++++ .../req_model/appointment_rate_req_model.dart | 100 ++++++++ .../appointment_details_resp_model.dart | 64 +++++ .../rate_appointment_resp_model.dart | 160 ++++++++++++ .../my_appointments/my_appointments_repo.dart | 184 +++++++++++++- .../my_appointments_view_model.dart | 4 + lib/main.dart | 4 + .../widgets/appointment_card.dart | 7 +- lib/presentation/home/landing_page.dart | 50 +++- .../rate_appointment_clinic.dart | 213 ++++++++++++++++ .../rate_appointment_doctor.dart | 234 ++++++++++++++++++ .../rate_appointment/widget/doctor_row.dart | 96 +++++++ 14 files changed, 1243 insertions(+), 13 deletions(-) create mode 100644 lib/features/my_appointments/appointment_rating_view_model.dart create mode 100644 lib/features/my_appointments/models/req_model/appointment_rate_req_model.dart create mode 100644 lib/features/my_appointments/models/resp_models/appointment_details_resp_model.dart create mode 100644 lib/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart create mode 100644 lib/presentation/rate_appointment/rate_appointment_clinic.dart create mode 100644 lib/presentation/rate_appointment/rate_appointment_doctor.dart create mode 100644 lib/presentation/rate_appointment/widget/doctor_row.dart diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 2a5c749..cba52c6 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -27,6 +27,7 @@ import 'package:hmg_patient_app_new/features/location/location_repo.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; @@ -137,6 +138,9 @@ class AppDependencies { getIt.registerLazySingleton( () => MyAppointmentsViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt())); + getIt.registerLazySingleton( + () => AppointmentRatingViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt())); + getIt.registerLazySingleton( () => PayfortViewModel( payfortRepo: getIt(), diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index 6ecf4b1..7fdec14 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -266,7 +266,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { newRequest.forRegisteration = newRequest.isRegister ?? false; newRequest.isRegister = false; //silent login case removed token and login token - if(newRequest.logInTokenID.isEmpty && newRequest.isSilentLogin == true) { + if(newRequest.logInTokenID.isEmpty && newRequest.isSilentLogin == true && (newRequest.loginType==1 || newRequest.loginType==4)) { newRequest.logInTokenID = null; newRequest.deviceToken = null; } diff --git a/lib/features/my_appointments/appointment_rating_view_model.dart b/lib/features/my_appointments/appointment_rating_view_model.dart new file mode 100644 index 0000000..a192df1 --- /dev/null +++ b/lib/features/my_appointments/appointment_rating_view_model.dart @@ -0,0 +1,134 @@ +// dart +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/appointment_details_resp_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +import 'models/resp_models/rate_appointment_resp_model.dart'; + +class AppointmentRatingViewModel extends ChangeNotifier { + final MyAppointmentsRepo myAppointmentsRepo; + final ErrorHandlerService errorHandlerService; + final AppState appState; + List appointmentRatedList = []; + AppointmentDetails? appointmentDetails; + AppointmentRatingViewModel({ + required this.myAppointmentsRepo, + required this.errorHandlerService, + required this.appState, + }); + + + String title = ""; + String subTitle = ""; + bool isRateClinic = false; + + Future getLastRatingAppointment({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.getLastRatingAppointment(); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + appointmentRatedList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future getAppointmentDetails(int appointmentID, int projectID, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.getAppointmentDetails(appointmentID, projectID); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + appointmentDetails = apiResponse.data ?? AppointmentDetails(); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + Future submitDoctorRating( {required int docRate, required String docNote,Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.sendDoctorRate( + docRate, + appointmentDetails!.appointmentNo!, + appointmentDetails!.projectID!, + appointmentDetails!.doctorID!, + appointmentDetails!.clinicID!, + docNote, + appointmentDetails!.appointmentDate!, + appointmentDetails!.doctorName, + appointmentDetails!.projectName, + appointmentDetails!.clinicName + ); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + + notifyListeners(); + if (onSuccess != null) { + // onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future submitClinicRating( { required int clinicRate, required String clinicNote, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.sendAppointmentRate( + clinicRate, + appointmentDetails!.appointmentNo!, + appointmentDetails!.projectID!, + appointmentDetails!.doctorID!, + appointmentDetails!.clinicID!, + clinicNote + ); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + + notifyListeners(); + if (onSuccess != null) { + // onSuccess(apiResponse.data); + } + } + }, + ); + } + + void setSubTitle(String value) { + this.subTitle = value; + notifyListeners(); + } + + void setTitle(String value) { + this.title = value; + notifyListeners(); + } + void setClinicOrDoctor(bool value){ + this.isRateClinic = value; + notifyListeners(); + } +} diff --git a/lib/features/my_appointments/models/req_model/appointment_rate_req_model.dart b/lib/features/my_appointments/models/req_model/appointment_rate_req_model.dart new file mode 100644 index 0000000..91070f2 --- /dev/null +++ b/lib/features/my_appointments/models/req_model/appointment_rate_req_model.dart @@ -0,0 +1,100 @@ +class AppointmentRate { + int? rate; + int? appointmentNo; + int? projectID; + int? doctorID; + int? clinicID; + String? note; + String? mobileNumber; + int? createdBy; + int? editedBy; + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + int? patientOutSA; + String? sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + int? patientID; + String? tokenID; + int? patientTypeID; + int? patientType; + + AppointmentRate( + {this.rate, + this.appointmentNo, + this.projectID, + this.doctorID, + this.clinicID, + this.note, + this.mobileNumber, + this.createdBy, + this.editedBy, + this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.patientOutSA, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID, + this.patientID, + this.tokenID, + this.patientTypeID, + this.patientType}); + + AppointmentRate.fromJson(Map json) { + rate = json['Rate']; + appointmentNo = json['AppointmentNo']; + projectID = json['ProjectID']; + doctorID = json['DoctorID']; + clinicID = json['ClinicID']; + note = json['Note']; + mobileNumber = json['MobileNumber']; + createdBy = json['CreatedBy']; + editedBy = json['EditedBy']; + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + patientOutSA = json['PatientOutSA']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + patientID = json['PatientID']; + tokenID = json['TokenID']; + patientTypeID = json['PatientTypeID']; + patientType = json['PatientType']; + } + + Map toJson() { + final Map data = new Map(); + data['Rate'] = this.rate; + data['AppointmentNo'] = this.appointmentNo; + data['ProjectID'] = this.projectID; + data['DoctorID'] = this.doctorID; + data['ClinicID'] = this.clinicID; + data['Note'] = this.note; + data['MobileNumber'] = this.mobileNumber; + data['CreatedBy'] = this.createdBy; + data['EditedBy'] = this.editedBy; + data['VersionID'] = this.versionID; + data['Channel'] = this.channel; + data['LanguageID'] = this.languageID; + data['IPAdress'] = this.iPAdress; + data['generalid'] = this.generalid; + data['PatientOutSA'] = this.patientOutSA; + data['SessionID'] = this.sessionID; + data['isDentalAllowedBackend'] = this.isDentalAllowedBackend; + data['DeviceTypeID'] = this.deviceTypeID; + data['PatientID'] = this.patientID; + data['TokenID'] = this.tokenID; + data['PatientTypeID'] = this.patientTypeID; + data['PatientType'] = this.patientType; + return data; + } +} diff --git a/lib/features/my_appointments/models/resp_models/appointment_details_resp_model.dart b/lib/features/my_appointments/models/resp_models/appointment_details_resp_model.dart new file mode 100644 index 0000000..2900bce --- /dev/null +++ b/lib/features/my_appointments/models/resp_models/appointment_details_resp_model.dart @@ -0,0 +1,64 @@ +class AppointmentDetails { + String? setupID; + int? projectID; + int? patientID; + int? appointmentNo; + int? clinicID; + int? doctorID; + dynamic startTime; + dynamic endTime; + dynamic appointmentDate; + dynamic clinicName; + dynamic doctorImageURL; + dynamic doctorName; + dynamic projectName; + + AppointmentDetails( + {this.setupID, + this.projectID, + this.patientID, + this.appointmentNo, + this.clinicID, + this.doctorID, + this.startTime, + this.endTime, + this.appointmentDate, + this.clinicName, + this.doctorImageURL, + this.doctorName, + this.projectName}); + + AppointmentDetails.fromJson(Map json) { + setupID = json['SetupID']; + projectID = json['ProjectID']; + patientID = json['PatientID']; + appointmentNo = json['AppointmentNo']; + clinicID = json['ClinicID']; + doctorID = json['DoctorID']; + startTime = json['StartTime']; + endTime = json['EndTime']; + appointmentDate = json['AppointmentDate']; + clinicName = json['ClinicName']; + doctorImageURL = json['DoctorImageURL']; + doctorName = json['DoctorName']; + projectName = json['ProjectName']; + } + + Map toJson() { + final Map data = new Map(); + data['SetupID'] = this.setupID; + data['ProjectID'] = this.projectID; + data['PatientID'] = this.patientID; + data['AppointmentNo'] = this.appointmentNo; + data['ClinicID'] = this.clinicID; + data['DoctorID'] = this.doctorID; + data['StartTime'] = this.startTime; + data['EndTime'] = this.endTime; + data['AppointmentDate'] = this.appointmentDate; + data['ClinicName'] = this.clinicName; + data['DoctorImageURL'] = this.doctorImageURL; + data['DoctorName'] = this.doctorName; + data['ProjectName'] = this.projectName; + return data; + } +} diff --git a/lib/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart b/lib/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart new file mode 100644 index 0000000..877b7bc --- /dev/null +++ b/lib/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart @@ -0,0 +1,160 @@ +class RateAppointmentRespModel { + String? setupID; + int? projectID; + int? appointmentNo; + String? appointmentDate; + String? appointmentDateN; + int? appointmentType; + String? bookDate; + int? patientType; + int? patientID; + int? clinicID; + int? doctorID; + String? endDate; + String? startTime; + String? endTime; + int? status; + int? visitType; + int? visitFor; + int? patientStatusType; + int? companyID; + int? bookedBy; + String? bookedOn; + int? confirmedBy; + String? confirmedOn; + int? arrivalChangedBy; + String? arrivedOn; + int? editedBy; + String? editedOn; + dynamic doctorName; + String? doctorNameN; + String? statusDesc; + String? statusDescN; + bool? vitalStatus; + dynamic vitalSignAppointmentNo; + int? episodeID; + String? doctorTitle; + bool? isAppoitmentLiveCare; + + RateAppointmentRespModel( + {this.setupID, + this.projectID, + this.appointmentNo, + this.appointmentDate, + this.appointmentDateN, + this.appointmentType, + this.bookDate, + this.patientType, + this.patientID, + this.clinicID, + this.doctorID, + this.endDate, + this.startTime, + this.endTime, + this.status, + this.visitType, + this.visitFor, + this.patientStatusType, + this.companyID, + this.bookedBy, + this.bookedOn, + this.confirmedBy, + this.confirmedOn, + this.arrivalChangedBy, + this.arrivedOn, + this.editedBy, + this.editedOn, + this.doctorName, + this.doctorNameN, + this.statusDesc, + this.statusDescN, + this.vitalStatus, + this.vitalSignAppointmentNo, + this.episodeID, + this.doctorTitle, + this.isAppoitmentLiveCare}); + + RateAppointmentRespModel.fromJson(Map json) { + try { + setupID = json['SetupID']; + projectID = json['ProjectID']; + appointmentNo = json['AppointmentNo']; + appointmentDate = json['AppointmentDate']; + appointmentDateN = json['AppointmentDateN']; + appointmentType = json['AppointmentType']; + bookDate = json['BookDate']; + patientType = json['PatientType']; + patientID = json['PatientID']; + clinicID = json['ClinicID']; + doctorID = json['DoctorID']; + endDate = json['EndDate']; + startTime = json['StartTime']; + endTime = json['EndTime']; + status = json['Status']; + visitType = json['VisitType']; + visitFor = json['VisitFor']; + patientStatusType = json['PatientStatusType']; + companyID = json['CompanyID']; + bookedBy = json['BookedBy']; + bookedOn = json['BookedOn']; + confirmedBy = json['ConfirmedBy']; + confirmedOn = json['ConfirmedOn']; + arrivalChangedBy = json['ArrivalChangedBy']; + arrivedOn = json['ArrivedOn']; + editedBy = json['EditedBy']; + editedOn = json['EditedOn']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + statusDesc = json['StatusDesc']; + statusDescN = json['StatusDescN']; + vitalStatus = json['VitalStatus']; + vitalSignAppointmentNo = json['VitalSignAppointmentNo']; + episodeID = json['EpisodeID']; + doctorTitle = json['DoctorTitle']; + isAppoitmentLiveCare = json['IsAppoitmentLiveCare']; + } catch (e) { + print(e); + } + } + + Map toJson() { + final Map data = new Map(); + data['SetupID'] = this.setupID; + data['ProjectID'] = this.projectID; + data['AppointmentNo'] = this.appointmentNo; + data['AppointmentDate'] = this.appointmentDate; + data['AppointmentDateN'] = this.appointmentDateN; + data['AppointmentType'] = this.appointmentType; + data['BookDate'] = this.bookDate; + data['PatientType'] = this.patientType; + data['PatientID'] = this.patientID; + data['ClinicID'] = this.clinicID; + data['DoctorID'] = this.doctorID; + data['EndDate'] = this.endDate; + data['StartTime'] = this.startTime; + data['EndTime'] = this.endTime; + data['Status'] = this.status; + data['VisitType'] = this.visitType; + data['VisitFor'] = this.visitFor; + data['PatientStatusType'] = this.patientStatusType; + data['CompanyID'] = this.companyID; + data['BookedBy'] = this.bookedBy; + data['BookedOn'] = this.bookedOn; + data['ConfirmedBy'] = this.confirmedBy; + data['ConfirmedOn'] = this.confirmedOn; + data['ArrivalChangedBy'] = this.arrivalChangedBy; + data['ArrivedOn'] = this.arrivedOn; + data['EditedBy'] = this.editedBy; + data['EditedOn'] = this.editedOn; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['StatusDesc'] = this.statusDesc; + data['StatusDescN'] = this.statusDescN; + data['VitalStatus'] = this.vitalStatus; + data['VitalSignAppointmentNo'] = this.vitalSignAppointmentNo; + data['EpisodeID'] = this.episodeID; + data['DoctorTitle'] = this.doctorTitle; + data['IsAppoitmentLiveCare'] = this.isAppoitmentLiveCare; + return data; + } +} diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index 87ec10d..72ed332 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -8,6 +8,7 @@ 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/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart' show HospitalsModel; @@ -15,6 +16,9 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; +import 'models/req_model/appointment_rate_req_model.dart'; +import 'models/resp_models/appointment_details_resp_model.dart'; + abstract class MyAppointmentsRepo { Future>>> getPatientAppointments({required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}); @@ -58,6 +62,15 @@ abstract class MyAppointmentsRepo { Future>> sendAskDocCallRequest( {required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, required String requestType, required String remarks, required String userMobileNumber}); + + Future>>> getLastRatingAppointment(); + + Future>> getAppointmentDetails(int appointmentID, int projectID); + + + Future>> sendAppointmentRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note); + + Future>> sendDoctorRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note, String appoDate, String docName, String projectName, String clinicName); } class MyAppointmentsRepoImp implements MyAppointmentsRepo { @@ -67,7 +80,8 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { MyAppointmentsRepoImp({required this.loggerService, required this.apiClient}); @override - Future>>> getPatientAppointments({required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}) async { + Future>>> getPatientAppointments( + {required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}) async { Map mapDevice = { "IsActiveAppointment": isActiveAppointment, "IsComingFromCOC": false, @@ -176,7 +190,9 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { "AppointmentNo": appointmentNo, "PaymentMethodName": paymentMethodName, "PaymentAmount": payedAmount == 0 ? "0" : payedAmount.toString(), - "PaymentDate": payedAmount == 0 ? "" : "/Date(${DateTime.now().millisecondsSinceEpoch})/", + "PaymentDate": payedAmount == 0 ? "" : "/Date(${DateTime + .now() + .millisecondsSinceEpoch})/", "PaymentReferenceNumber": payedAmount == 0 ? "" : paymentReference, "ProjectID": projectID, "PatientID": patientID, @@ -746,4 +762,166 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } -} + + @override + Future>>> getLastRatingAppointment() async { + Map mapDevice = {}; + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post(IS_LAST_APPOINTMENT_RATED, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['IsLastAppoitmentRatedList']; + + final lstRatingAppointmentList = list.map((item) => RateAppointmentRespModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: lstRatingAppointmentList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: mapDevice); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getAppointmentDetails(int appointmentID, int projectID) async { + Map mapDevice = { + "AppointmentNumber": appointmentID, + "ProjectID": projectID, + }; + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(GET_APPOINTMENT_DETAILS_BY_NO, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppointmentDetails']; + + final appointmentDetails = AppointmentDetails.fromJson(list); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: appointmentDetails, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: mapDevice); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + + @override + Future>> sendAppointmentRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note) async { + + AppointmentRate appointmentRate = AppointmentRate(); + appointmentRate.rate = rate; + appointmentRate.appointmentNo = appointmentNo; + appointmentRate.projectID = projectID; + appointmentRate.doctorID = doctorID; + appointmentRate.clinicID = clinicID; + appointmentRate.note = note; + appointmentRate.createdBy = 2; + appointmentRate.editedBy = 2; + + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(NEW_RATE_APPOINTMENT_URL, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppointmentRated']; + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: list, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: appointmentRate.toJson()); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> sendDoctorRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note, String appoDate, String docName, String projectName, String clinicName) async { + Map request; + + request = { + "DoctorID": doctorID, + "Rate": rate, + "ClinicID": clinicID, + "ProjectID": projectID, + "AppointmentNo": appointmentNo, + "Note": note, + // "MobileNumber": authenticatedUserObject.user!.mobileNumber, + "AppointmentDate": appoDate, + "DoctorName": docName, + "ProjectName": projectName, + "COCTypeName": 1, + // "PatientName": authenticatedUserObject.user!.firstName! + " " + authenticatedUserObject.user!.lastName!, + // "PatientOutSA": authenticatedUserObject.user!.outSA, + // "PatientTypeID": authenticatedUserObject.user!.patientType, + "ClinicName": clinicName, + // "PatientIdentificationID": authenticatedUserObject.user!.patientIdentificationNo + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(NEW_RATE_DOCTOR_URL, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppointmentRated']; + + // final appointmentDetails = AppointmentDetails.fromJson(list); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: list, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: request); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + +} \ No newline at end of file diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index fa1ad22..0958c8e 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -37,6 +37,9 @@ class MyAppointmentsViewModel extends ChangeNotifier { DateTime? start = null; DateTime? end = null; + + + List patientAppointmentsHistoryList = []; List filteredAppointmentList = []; @@ -679,4 +682,5 @@ class MyAppointmentsViewModel extends ChangeNotifier { }, ); } + } diff --git a/lib/main.dart b/lib/main.dart index 1af80b6..c8fa667 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,6 +21,7 @@ import 'package:hmg_patient_app_new/features/lab/history/lab_history_viewmodel.d import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; @@ -104,6 +105,9 @@ void main() async { ChangeNotifierProvider( create: (_) => getIt.get(), ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), ChangeNotifierProvider( create: (_) => getIt.get(), ), diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 3cf56a6..61174c0 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -38,7 +38,7 @@ class AppointmentCard extends StatelessWidget { final MedicalFileViewModel? medicalFileViewModel; final ContactUsViewModel? contactUsViewModel; final BookAppointmentsViewModel bookAppointmentsViewModel; - + final bool isForRate; const AppointmentCard({ super.key, required this.patientAppointmentHistoryResponseModel, @@ -51,6 +51,7 @@ class AppointmentCard extends StatelessWidget { this.isForFeedback = false, this.medicalFileViewModel, this.contactUsViewModel, + this.isForRate =false }); @override @@ -63,11 +64,11 @@ class AppointmentCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildHeader(context, appState), + isForRate ? SizedBox(): _buildHeader(context, appState), SizedBox(height: 16.h), _buildDoctorRow(context), SizedBox(height: 16.h), - _buildActionArea(context, appState), + isForRate ? SizedBox(): _buildActionArea(context, appState), ], ), ), diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 1007547..6e0de4c 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -20,6 +20,7 @@ import 'package:hmg_patient_app_new/features/emergency_services/emergency_servic import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; @@ -39,6 +40,7 @@ import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/profile_settings.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointment_doctor.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -64,7 +66,7 @@ class _LandingPageState extends State { late MyAppointmentsViewModel myAppointmentsViewModel; late PrescriptionsViewModel prescriptionsViewModel; final CacheService cacheService = GetIt.instance(); - + late AppointmentRatingViewModel appointmentRatingViewModel; late InsuranceViewModel insuranceViewModel; late ImmediateLiveCareViewModel immediateLiveCareViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; @@ -76,6 +78,7 @@ class _LandingPageState extends State { void initState() { authVM = context.read(); habibWalletVM = context.read(); + appointmentRatingViewModel = context.read(); // myAppointmentsViewModel = context.read(); // prescriptionsViewModel = context.read(); // insuranceViewModel = context.read(); @@ -99,6 +102,17 @@ class _LandingPageState extends State { immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.getPatientLiveCareHistory(); emergencyServicesViewModel.checkPatientERAdvanceBalance(); + appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) { + if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { + appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!, + onSuccess: ((response) { + appointmentRatingViewModel.setClinicOrDoctor(false); + appointmentRatingViewModel.setTitle("Rate Doctor".needTranslation); + appointmentRatingViewModel.setSubTitle("How was your last visit with doctor?".needTranslation); + openLastRating(); + })); + } + }); } }); super.initState(); @@ -271,17 +285,14 @@ class _LandingPageState extends State { ) : Container( width: double.infinity, - decoration: RoundedRectangleBorder() - .toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), child: Padding( padding: EdgeInsets.all(12.h), child: Column( children: [ Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), SizedBox(height: 12.h), - "You do not have any upcoming appointment. Please book an appointment" - .needTranslation - .toText12(isCenter: true), + "You do not have any upcoming appointment. Please book an appointment".needTranslation.toText12(isCenter: true), SizedBox(height: 12.h), CustomButton( text: LocaleKeys.bookAppo.tr(context: context), @@ -595,4 +606,31 @@ class _LandingPageState extends State { }, ); } + + openLastRating() { + showCommonBottomSheetWithoutHeight( + context, + titleWidget: Selector( + selector: (_, vm) => vm.title, + builder: (context, title, child) { + final displayTitle = title ?? ''; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + displayTitle.toText20(weight: FontWeight.w600), + (context.select((vm) => vm.subTitle) ?? '').toText12(), + ], + ); + }, + ), + isCloseButtonVisible: true, + child: StatefulBuilder( + builder: (context, setState) { + + return RateAppointmentDoctor(); + }, + ), + isFullScreen: false, + ); + } } diff --git a/lib/presentation/rate_appointment/rate_appointment_clinic.dart b/lib/presentation/rate_appointment/rate_appointment_clinic.dart new file mode 100644 index 0000000..a33628e --- /dev/null +++ b/lib/presentation/rate_appointment/rate_appointment_clinic.dart @@ -0,0 +1,213 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.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/my_appointments/appointment_rating_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/widget/doctor_row.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/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class RateAppointmentClinic extends StatefulWidget { + + late final String? doctorNote; + late final int? doctorRate; + + RateAppointmentClinic({this.doctorRate, this.doctorNote}); + + @override + _RateAppointmentClinicState createState() => _RateAppointmentClinicState(); +} + +class _RateAppointmentClinicState extends State { + final formKey = GlobalKey(); + String note = ""; + int rating = 5; + AppointmentRatingViewModel? appointmentRatingViewModel; + MyAppointmentsViewModel? myAppointmentsViewModel; + + @override + Widget build(BuildContext context) { + myAppointmentsViewModel = Provider.of(context, listen: false); + appointmentRatingViewModel = Provider.of(context, listen: false); + + // Make the sheet a fixed height and keep content scrollable while pinning buttons to bottom + final sheetHeight = ResponsiveExtension.screenHeight * 0.60; + + return SizedBox( + height: sheetHeight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Scrollable content + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 0.0, left: 0, right: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Doctor row + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: BuildDoctorRow( + isForClinic: true, + appointmentDetails: appointmentRatingViewModel!.appointmentDetails, + ), + ), + SizedBox(height: 16), + + // Rate clinic box + SizedBox( + width: double.infinity, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Rate Clinic", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xff2B353E), letterSpacing: -0.64, height: 23 / 16), + ), + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...List.generate( + 5, + (index) => rating == (index + 1) + ? Container( + margin: EdgeInsets.only(left: 3.0, right: 3.0), + child: IconButton( + onPressed: () { + setState(() { + rating = index + 1; + }); + }, + iconSize: 35, + icon: SvgPicture.asset('assets/images/svg/rate_${index + 1}.svg', colorFilter: getColors(rating)), + ), + ) + : IconButton( + onPressed: () { + setState(() { + rating = index + 1; + }); + }, + iconSize: 35, + icon: SvgPicture.asset('assets/images/svg/rate_${index + 1}.svg'), + ), + ), + ], + ), + ], + ), + ), + ), + ), + + SizedBox(height: 12), + + // Extra content area (keeps any other widgets that were previously below) + Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Placeholder for in-content widgets if needed in future + ], + ), + ), + + // Add bottom spacing so last content isn't obscured by the fixed buttons + SizedBox(height: 12), + ], + ), + ), + + ), + + // Bottom action buttons pinned to bottom of the sheet + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric( vertical: 12.0), + child: Row( + children: [ + Expanded( + child: CustomButton( + text: "Back".needTranslation, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + onPressed: () { + appointmentRatingViewModel!.setTitle("Rate Doctor".needTranslation); + appointmentRatingViewModel!.setSubTitle("How was your last visit with doctor?".needTranslation); + appointmentRatingViewModel!.setClinicOrDoctor(false); + setState(() { + + }); + }, + ), + ), + SizedBox(width: 10), + Expanded( + child: CustomButton( + text: "Submit".needTranslation, + onPressed: () { + + submitRating(); + + }, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + ColorFilter getColors(int rating){ + + switch(rating){ + case 5: + return ColorFilter.mode(AppColors.bgGreenColor, BlendMode.srcIn); + case 4: + return ColorFilter.mode(Colors.greenAccent, BlendMode.srcIn); + case 3: + return ColorFilter.mode(AppColors.warningLightColor, BlendMode.srcIn); + case 2: + return ColorFilter.mode(Colors.orange, BlendMode.srcIn); + case 1: + return ColorFilter.mode(AppColors.primaryRedColor, BlendMode.srcIn); + + default: + return ColorFilter.mode(AppColors.greyColor, BlendMode.srcIn); + } + } + + submitRating() async{ + LoaderBottomSheet.showLoader(); + await appointmentRatingViewModel!.submitDoctorRating(docRate: widget.doctorRate!, docNote: widget.doctorNote!); + await appointmentRatingViewModel!.submitClinicRating(clinicRate: rating, clinicNote: note); + LoaderBottomSheet.hideLoader(); + Navigator.pop(context); + } + +} diff --git a/lib/presentation/rate_appointment/rate_appointment_doctor.dart b/lib/presentation/rate_appointment/rate_appointment_doctor.dart new file mode 100644 index 0000000..4bff683 --- /dev/null +++ b/lib/presentation/rate_appointment/rate_appointment_doctor.dart @@ -0,0 +1,234 @@ +import 'package:flutter/cupertino.dart'; +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/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointment_clinic.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/widget/doctor_row.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/transitions/fade_page.dart'; +import 'package:provider/provider.dart'; + +class RateAppointmentDoctor extends StatefulWidget { + + bool isFromRegistration; + + RateAppointmentDoctor({Key? key, this.isFromRegistration = false}) : super(key: key); + + @override + _RateAppointmentDoctorState createState() => _RateAppointmentDoctorState(); +} + +class _RateAppointmentDoctorState extends State { + final formKey = GlobalKey(); + String note = ""; + int rating = 5; + + // ProjectViewModel? projectViewModel; + AppointmentRatingViewModel? appointmentRatingViewModel; + MyAppointmentsViewModel? myAppointmentsViewModel; + + @override + void initState() { + + super.initState(); + } + + + @override + Widget build(BuildContext context) { + + myAppointmentsViewModel = Provider.of(context, listen: false); + appointmentRatingViewModel = Provider.of(context, listen: false); + + final sheetHeight = ResponsiveExtension.screenHeight * 0.60; + + return Selector( + selector: (_, vm) => vm.isRateClinic, + builder: (context, isRateClinic, child) => isRateClinic + ? RateAppointmentClinic(doctorNote: note, doctorRate: rating,) + : SizedBox( + height: sheetHeight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Scrollable main content + Expanded( + + child: Padding( + padding: const EdgeInsets.only(top: 0.0, left: 0, right: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Doctor row + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: BuildDoctorRow( + isForClinic: false, + appointmentDetails: appointmentRatingViewModel!.appointmentDetails, + )), + + SizedBox(height: 16), + + // Rating box + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Please rate the doctor", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xff2B353E), + letterSpacing: -0.64, + height: 23 / 16), + ), + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...List.generate( + 5, + (index) => AnimatedSwitcher( + duration: Duration(milliseconds: 1000), + switchInCurve: Curves.elasticOut, + switchOutCurve: Curves.elasticIn, + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(child: child, scale: animation); + }, + child: Container( + key: ValueKey(rating), + child: IconButton( + iconSize: 45.0, + onPressed: () { + setState(() { + rating = index + 1; + }); + }, + color: rating >= (index + 1) + ? Color.fromRGBO(255, 186, 0, 1.0) + : Colors.grey[400], + icon: Icon(rating >= (index + 1) ? Icons.star : Icons.star)), + ), + ), + ) + ], + ), + ], + ), + ), + ), + + SizedBox(height: 12), + + // Note text field + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.0), + child: TextField( + + maxLines: 5, + decoration: InputDecoration.collapsed( + hintText: "Notes".needTranslation, + hintStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xff2B353E), + letterSpacing: -0.64, + height: 23 / 16)), + onChanged: (value) { + setState(() { + note = value; + }); + }, + ))), + + SizedBox(height: 12), + + // Optional extra spacing so content doesn't touch buttons + SizedBox(height: 12), + ], + ), + ), + + ), + + // Bottom action buttons pinned to bottom + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + children: [ + Expanded( + child: CustomButton( + text: "Later".needTranslation, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + SizedBox(width: 10), + Expanded( + child: CustomButton( + text: "Next".needTranslation, + onPressed: () { + // Set up clinic rating and show clinic rating view + appointmentRatingViewModel!.setTitle("Rate Clinic".needTranslation); + appointmentRatingViewModel!.setSubTitle("How was your appointment?".needTranslation); + appointmentRatingViewModel!.setClinicOrDoctor(true); + + setState(() {}); + }, + ), + ), + ], + ), + ), + ), + ], + ), + )); + + // DoctorList getDoctorObject(AppointmentRateViewModel model) { + // DoctorList doctor = new DoctorList(); + // + // doctor.name = model.appointmentDetails.doctorName; + // doctor.doctorImageURL = model.appointmentDetails.doctorImageURL; + // doctor.clinicName = model.appointmentDetails.clinicName; + // doctor.projectName = model.appointmentDetails.projectName; + // doctor.date = model.appointmentDetails.appointmentDate; + // doctor.actualDoctorRate = 5; + // + // return doctor; + // } + } + + +} diff --git a/lib/presentation/rate_appointment/widget/doctor_row.dart b/lib/presentation/rate_appointment/widget/doctor_row.dart new file mode 100644 index 0000000..f38e15d --- /dev/null +++ b/lib/presentation/rate_appointment/widget/doctor_row.dart @@ -0,0 +1,96 @@ + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.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/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/appointment_details_resp_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class BuildDoctorRow extends StatelessWidget { + bool isForClinic = false; + AppointmentDetails? appointmentDetails; + + BuildDoctorRow({super.key, required this.isForClinic, this.appointmentDetails}); + + @override + Widget build(BuildContext context) { + + return Padding(padding: EdgeInsets.all(16),child:Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + isForClinic ? 'https://hmgwebservices.com/Images/Hospitals/${appointmentDetails!.projectID}.jpg' : appointmentDetails!.doctorImageURL , + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 16.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (isForClinic ? appointmentDetails!.projectName : appointmentDetails!.doctorName)!.toString() + .toText16(isBold: true, maxlines: 1), + + SizedBox(height: 8.h), + + + isForClinic ? Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + + labelText: + appointmentDetails!.clinicName.toString(), + + ), + AppCustomChipWidget( + icon: AppAssets.ic_date_filter, + labelText: + DateUtil.formatDateToDate(DateUtil.convertStringToDate(appointmentDetails!.appointmentDate), false), + + ), + + AppCustomChipWidget( + icon: AppAssets.appointment_time_icon, + labelText: + appointmentDetails!.startTime.substring(0, appointmentDetails!.startTime.length - 3), + + ), + + ] + ) : Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + + labelText: + appointmentDetails!.projectName.toString(), + + + ), + AppCustomChipWidget( + + labelText: + appointmentDetails!.clinicName.toString(), + + ) + + ] + ) + ], + ), + ), + ], + )); + } + +} \ No newline at end of file From 1d49308cc73cc085b681b3e166d952ab7eedadbc Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Sun, 14 Dec 2025 14:56:14 +0300 Subject: [PATCH 2/5] last appointment rated. --- .../rate_appointment_clinic.dart | 7 +++---- .../rate_appointment_doctor.dart | 17 ++++------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/lib/presentation/rate_appointment/rate_appointment_clinic.dart b/lib/presentation/rate_appointment/rate_appointment_clinic.dart index a33628e..0913297 100644 --- a/lib/presentation/rate_appointment/rate_appointment_clinic.dart +++ b/lib/presentation/rate_appointment/rate_appointment_clinic.dart @@ -78,10 +78,9 @@ class _RateAppointmentClinicState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Rate Clinic", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xff2B353E), letterSpacing: -0.64, height: 23 / 16), - ), + + "Rate Clinic".needTranslation.toText16(isBold: true), + SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/presentation/rate_appointment/rate_appointment_doctor.dart b/lib/presentation/rate_appointment/rate_appointment_doctor.dart index 4bff683..782927d 100644 --- a/lib/presentation/rate_appointment/rate_appointment_doctor.dart +++ b/lib/presentation/rate_appointment/rate_appointment_doctor.dart @@ -92,15 +92,9 @@ class _RateAppointmentDoctorState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Please rate the doctor", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color(0xff2B353E), - letterSpacing: -0.64, - height: 23 / 16), - ), + + "Please rate the doctor".needTranslation.toText16(isBold: true), + SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -150,7 +144,7 @@ class _RateAppointmentDoctorState extends State { padding: EdgeInsets.all(16.0), child: TextField( - maxLines: 5, + maxLines: 4, decoration: InputDecoration.collapsed( hintText: "Notes".needTranslation, hintStyle: TextStyle( @@ -166,10 +160,7 @@ class _RateAppointmentDoctorState extends State { }, ))), - SizedBox(height: 12), - // Optional extra spacing so content doesn't touch buttons - SizedBox(height: 12), ], ), ), From eff6cbbaad0e51e7ccb16b113598d761f69b6e80 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 15 Dec 2025 09:17:17 +0300 Subject: [PATCH 3/5] updates & fixes --- .../appointments/appointment_queue_page.dart | 2 +- .../appointment_checkin_bottom_sheet.dart | 4 +- .../hmg_services/services_page.dart | 3 +- lib/presentation/home/landing_page.dart | 2 +- .../smartwatches/huawei_health_example.dart | 1563 +++++++++++++++++ lib/routes/app_routes.dart | 3 + pubspec.yaml | 1 + 7 files changed, 1573 insertions(+), 5 deletions(-) create mode 100644 lib/presentation/smartwatches/huawei_health_example.dart diff --git a/lib/presentation/appointments/appointment_queue_page.dart b/lib/presentation/appointments/appointment_queue_page.dart index d9e5832..d8acac6 100644 --- a/lib/presentation/appointments/appointment_queue_page.dart +++ b/lib/presentation/appointments/appointment_queue_page.dart @@ -72,7 +72,7 @@ class AppointmentQueuePage extends StatelessWidget { myAppointmentsVM.currentPatientQueueDetails.queueNo!.toText32(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), SizedBox(height: 8.h), CustomButton( - text: Utils.getCardButtonText(myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo!), + text: Utils.getCardButtonText(myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo ?? ""), onPressed: () {}, backgroundColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus), borderColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.01), diff --git a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart index cd43b17..1fbd22e 100644 --- a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart +++ b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart @@ -148,8 +148,8 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { checkInType: 2, onSuccess: (apiResponse) { LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight(context, title: "Success".needTranslation, child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), callBackFunc: () { - myAppointmentsViewModel.getPatientAppointmentQueueDetails(appointmentNo: patientAppointmentHistoryResponseModel.appointmentNo, patientID: patientAppointmentHistoryResponseModel.patientID); + showCommonBottomSheetWithoutHeight(context, title: "Success".needTranslation, child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), callBackFunc: () async { + await myAppointmentsViewModel.getPatientAppointmentQueueDetails(appointmentNo: patientAppointmentHistoryResponseModel.appointmentNo, patientID: patientAppointmentHistoryResponseModel.patientID); Navigator.of(context).pop(); Navigator.pushAndRemoveUntil( context, diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 1d4131a..56519af 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -155,7 +155,8 @@ class ServicesPage extends StatelessWidget { AppAssets.smartwatch_icon, bgColor: AppColors.whiteColor, true, - route: AppRoutes.smartWatches, + // route: AppRoutes.smartWatches, + route: AppRoutes.huaweiHealthExample, ), ]; diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 1c1a77d..f9ea1ee 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -325,7 +325,7 @@ class _LandingPageState extends State { ) : SizedBox(height: 12.h), SizedBox(height: 8.h), CustomButton( - text: Utils.getCardButtonText(myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo!), + text: Utils.getCardButtonText(myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo ?? ""), onPressed: () {}, backgroundColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus), borderColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.01), diff --git a/lib/presentation/smartwatches/huawei_health_example.dart b/lib/presentation/smartwatches/huawei_health_example.dart new file mode 100644 index 0000000..4163d8b --- /dev/null +++ b/lib/presentation/smartwatches/huawei_health_example.dart @@ -0,0 +1,1563 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:huawei_health/huawei_health.dart'; + +const String packageName = 'com.ejada.hmg'; + +class HuaweiHealthExample extends StatefulWidget { + const HuaweiHealthExample({Key? key}) : super(key: key); + + @override + State createState() => _HuaweiHealthExampleState(); +} + +class _HuaweiHealthExampleState extends State { + /// Styles + static const TextStyle cardTitleTextStyle = TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18, + ); + static const EdgeInsets componentPadding = EdgeInsets.all(8.0); + + /// Text Controllers for showing the logs of different modules + final TextEditingController _activityTextController = TextEditingController(); + final TextEditingController _dataTextController = TextEditingController(); + final TextEditingController _settingTextController = TextEditingController(); + final TextEditingController _autoRecorderTextController = TextEditingController(); + final TextEditingController _consentTextController = TextEditingController(); + final TextEditingController _healthTextController = TextEditingController(); + + /// Data controller reference to initialize at startup. + late DataController _dataController; + + String? accessToken = ''; + + @override + void initState() { + super.initState(); + if (!mounted) return; + // Initialize Event Callbacks + AutoRecorderController.autoRecorderStream.listen(_onAutoRecorderEvent); + // Initialize a DataController + initDataController(); + } + + /// Prints the specified text on both the console and the specified text controller. + void log( + String methodName, + TextEditingController controller, + LogOptions logOption, { + String? result = '', + String? error = '', + }) { + String log = ''; + switch (logOption) { + case LogOptions.call: + log = '$methodName called'; + break; + case LogOptions.success: + log = '$methodName [Success: $result] '; + break; + case LogOptions.error: + log = '$methodName [Error: $error] [Error Description: ${HiHealthStatusCodes.getStatusCodeMessage(error ?? '')}]'; + break; + case LogOptions.custom: + log = methodName; // Custom text + break; + } + debugPrint(log); + setState(() { + controller.text = '$log\n${controller.text}'; + }); + } + + /// Authorizes Huawei Health Kit for the user, with defined scopes. + void signIn() async { + // List of scopes to ask for authorization. + // + // Note: These scopes should also be authorized on the Huawei Developer Console. + final List scopes = [ + Scope.HEALTHKIT_ACTIVITY_READ, + Scope.HEALTHKIT_ACTIVITY_WRITE, + Scope.HEALTHKIT_BLOODGLUCOSE_READ, + Scope.HEALTHKIT_BLOODGLUCOSE_WRITE, + Scope.HEALTHKIT_CALORIES_READ, + Scope.HEALTHKIT_CALORIES_WRITE, + Scope.HEALTHKIT_DISTANCE_READ, + Scope.HEALTHKIT_DISTANCE_WRITE, + Scope.HEALTHKIT_HEARTRATE_READ, + Scope.HEALTHKIT_HEARTRATE_WRITE, + Scope.HEALTHKIT_HEIGHTWEIGHT_READ, + Scope.HEALTHKIT_HEIGHTWEIGHT_WRITE, + Scope.HEALTHKIT_LOCATION_READ, + Scope.HEALTHKIT_LOCATION_WRITE, + Scope.HEALTHKIT_PULMONARY_READ, + Scope.HEALTHKIT_PULMONARY_WRITE, + Scope.HEALTHKIT_SLEEP_READ, + Scope.HEALTHKIT_SLEEP_WRITE, + Scope.HEALTHKIT_SPEED_READ, + Scope.HEALTHKIT_SPEED_WRITE, + Scope.HEALTHKIT_STEP_READ, + Scope.HEALTHKIT_STEP_WRITE, + Scope.HEALTHKIT_STRENGTH_READ, + Scope.HEALTHKIT_STRENGTH_WRITE, + Scope.HEALTHKIT_BODYFAT_READ, + Scope.HEALTHKIT_BODYFAT_WRITE, + Scope.HEALTHKIT_NUTRITION_READ, + Scope.HEALTHKIT_NUTRITION_WRITE, + Scope.HEALTHKIT_BLOODPRESSURE_READ, + Scope.HEALTHKIT_BLOODPRESSURE_WRITE, + Scope.HEALTHKIT_BODYTEMPERATURE_READ, + Scope.HEALTHKIT_BODYTEMPERATURE_WRITE, + Scope.HEALTHKIT_OXYGENSTATURATION_READ, + Scope.HEALTHKIT_OXYGENSTATURATION_WRITE, + Scope.HEALTHKIT_REPRODUCTIVE_READ, + Scope.HEALTHKIT_REPRODUCTIVE_WRITE, + Scope.HEALTHKIT_ACTIVITY_RECORD_READ, + Scope.HEALTHKIT_ACTIVITY_RECORD_WRITE, + Scope.HEALTHKIT_HEARTRATE_REALTIME, + Scope.HEALTHKIT_STEP_REALTIME, + Scope.HEALTHKIT_HEARTHEALTH_WRITE, + Scope.HEALTHKIT_HEARTHEALTH_READ, + Scope.HEALTHKIT_STRESS_WRITE, + Scope.HEALTHKIT_STRESS_READ, + Scope.HEALTHKIT_OXYGEN_SATURATION_WRITE, + Scope.HEALTHKIT_OXYGEN_SATURATION_READ, + Scope.HEALTHKIT_HISTORYDATA_OPEN_WEEK, + Scope.HEALTHKIT_HISTORYDATA_OPEN_MONTH, + Scope.HEALTHKIT_HISTORYDATA_OPEN_YEAR, + ]; + try { + AuthHuaweiId? result = await HealthAuth.signIn(scopes); + debugPrint( + 'Granted Scopes for User(${result?.displayName}): ${result?.grantedScopes?.toString()}', + ); + showSnackBar( + 'Authorization Success.', + color: Colors.green, + ); + setState(() => accessToken = result?.accessToken); + } on PlatformException catch (e) { + debugPrint('Error on authorization, Error:${e.toString()}'); + showSnackBar( + 'Error on authorization, Error:${e.toString()}, Error Description: ' + '${HiHealthStatusCodes.getStatusCodeMessage(e.message ?? '')}', + ); + } + } + + // ActivityRecordsController + // + /// Adds an ActivityRecord with an ActivitySummary, time range is 2 hours from now. + Future addActivityRecord() async { + log( + 'addActivityRecord', + _activityTextController, + LogOptions.call, + ); + DateTime startTime = DateTime.now().subtract(const Duration(hours: 2)); + DateTime endTime = DateTime.now(); + // Build an ActivityRecord object + ActivityRecord activityRecord = ActivityRecord( + startTime: startTime, + endTime: endTime, + id: 'ActivityRecordId0', + name: 'AddActivityRecord', + activityTypeId: HiHealthActivities.running, + description: 'This is a test for ActivityRecord', + activitySummary: ActivitySummary( + paceSummary: PaceSummary( + avgPace: 247.27626, + bestPace: 212.0, + britishPaceMap: { + '102802480': 365.0, + }, + britishPartTimeMap: { + '1.0': 263.0, + }, + partTimeMap: { + '1.0': 456.0, + }, + paceMap: { + '1.0': 263.0, + }, + ), + dataSummary: [ + SamplePoint( + dataType: DataType.DT_CONTINUOUS_DISTANCE_TOTAL, + startTime: startTime.add(Duration(seconds: 1)), + endTime: endTime.subtract(Duration(seconds: 1)), + fieldValueOptions: FieldFloat(Field.FIELD_DISTANCE, 400), + timeUnit: TimeUnit.MILLISECONDS, + ), + SamplePoint( + dataType: DataType.POLYMERIZE_CONTINUOUS_SPEED_STATISTICS, + fieldValueOptions: FieldFloat(Field.FIELD_AVG, 60.0), + startTime: startTime.add(Duration(seconds: 1)), + endTime: endTime.subtract(Duration(seconds: 1)), + timeUnit: TimeUnit.MILLISECONDS, + ) + ..setFieldValue(Field.FIELD_MIN, 40.0) + ..setFieldValue(Field.FIELD_MAX, 80.0), + ]), + ); + + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + dataType: DataType.DT_INSTANTANEOUS_STEPS_RATE, + name: 'AddActivityRecord1923', + ); + + // You can use sampleSets to add more sample points to the sampling dataset. + // Build a list of sampling point objects and add it to the sampling dataSet + List samplePoints = [ + SamplePoint( + dataCollector: dataCollector, + startTime: startTime.add(Duration(seconds: 1)), + endTime: endTime.subtract(Duration(seconds: 1)), + fieldValueOptions: FieldFloat(Field.FIELD_STEP_RATE, 10.0), + timeUnit: TimeUnit.MILLISECONDS, + ), + ]; + SampleSet sampleSet = SampleSet( + dataCollector, + samplePoints, + ); + + try { + await ActivityRecordsController.addActivityRecord( + ActivityRecordInsertOptions( + activityRecord: activityRecord, + sampleSets: [ + sampleSet, + ], + ), + ); + log( + 'addActivityRecord', + _activityTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'addActivityRecord', + _activityTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Obtains saved ActivityRecords between yesterday and now, + /// with the DT_CONTINUOUS_STEPS_DELTA data type + void getActivityRecord() async { + log( + 'getActivityRecord', + _activityTextController, + LogOptions.call, + ); + // Create start time that will be used to read activity record. + DateTime startTime = DateTime.now().subtract(const Duration(days: 1)); + + // Create end time that will be used to read activity record. + DateTime endTime = DateTime.now().add(const Duration(hours: 3)); + + ActivityRecordReadOptions activityRecordReadOptions = ActivityRecordReadOptions( + activityRecordId: "ActivityRecordId0", + activityRecordName: null, + startTime: startTime, + endTime: endTime, + timeUnit: TimeUnit.MILLISECONDS, + dataType: DataType.DT_INSTANTANEOUS_STEPS_RATE, + ); + try { + List result = await ActivityRecordsController.getActivityRecord( + activityRecordReadOptions, + ); + log( + 'getActivityRecord', + _activityTextController, + LogOptions.success, + result: '[IDs: ${result.map((ActivityRecord e) => e.id).toList()}]', + ); + } on PlatformException catch (e) { + log( + 'getActivityRecord', + _activityTextController, + LogOptions.error, + result: e.message, + ); + } + } + + /// Starts the ActivityRecord with the id:`ActivityRecordRun1` + void beginActivityRecord() async { + try { + log( + 'beginActivityRecord', + _activityTextController, + LogOptions.call, + ); + // Build an ActivityRecord object + ActivityRecord activityRecord = ActivityRecord( + id: 'ActivityRecordRun0', + name: 'BeginActivityRecord', + description: 'This is ActivityRecord begin test!', + activityTypeId: HiHealthActivities.running, + startTime: DateTime.now().subtract(const Duration(hours: 1)), + ); + await ActivityRecordsController.beginActivityRecord( + activityRecord, + ); + log( + 'beginActivityRecord', + _activityTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'beginActivityRecord', + _activityTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Stops the ActivityRecord with the id:`ActivityRecordRun1` + void endActivityRecord() async { + try { + log( + 'endActivityRecord', + _activityTextController, + LogOptions.call, + ); + final List result = await ActivityRecordsController.endActivityRecord( + 'ActivityRecordRun0', + ); + // Return the list of activity records that have stopped + log( + 'endActivityRecord', + _activityTextController, + LogOptions.success, + result: result.toString(), + ); + } on PlatformException catch (e) { + log( + 'endActivityRecord', + _activityTextController, + LogOptions.error, + result: e.message, + ); + } + } + + /// Ends all the ongoing activity records. + /// + /// Result list will be null if there is no ongoing activity record. + void endAllActivityRecords() async { + try { + log( + 'endAllActivityRecords', + _activityTextController, + LogOptions.call, + ); + // Return the list of activity records that have stopped + List result = await ActivityRecordsController.endAllActivityRecords(); + log( + 'endAllActivityRecords', + _activityTextController, + LogOptions.success, + result: '[IDs: ${result.map((ActivityRecord e) => e.id).toList()}]', + ); + } on PlatformException catch (e) { + log( + 'endAllActivityRecords', + _activityTextController, + LogOptions.error, + result: e.message, + ); + } + } + + // + // + // End of ActivityRecordsController Methods + + // DataController Methods + // + // + /// Initializes a DataController instance with a list of HiHealtOptions. + void initDataController() async { + if (!mounted) return; + log( + 'init', + _dataTextController, + LogOptions.call, + ); + try { + _dataController = await DataController.init(); + log( + 'init', + _dataTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'init', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Clears all the data inserted by the app. + void clearAll() async { + log('clearAll', _dataTextController, LogOptions.call); + try { + await _dataController.clearAll(); + log('clearAll', _dataTextController, LogOptions.success); + } on PlatformException catch (e) { + log('clearAll', _dataTextController, LogOptions.error, error: e.message); + } + } + + /// Deletes DT_CONTINUOUS_STEPS_DELTA type data by the specified time range. + void delete() async { + log( + 'delete', + _dataTextController, + LogOptions.call, + ); + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + dataStreamName: 'STEPS_DELTA', + ); + + // Build the time range for the deletion: start time and end time. + DeleteOptions deleteOptions = DeleteOptions( + dataCollectors: [dataCollector], + startTime: DateTime.parse('2020-10-10 08:00:00'), + endTime: DateTime.parse('2020-10-10 12:30:00'), + ); + + // Call the api with the constructed DeleteOptions instance. + try { + _dataController.delete(deleteOptions); + log( + 'delete', + _dataTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'delete', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Inserts a sampling set with the DT_CONTINUOUS_STEPS_DELTA data type at the + /// specified start and end dates. + void insert() async { + log( + 'insert', + _dataTextController, + LogOptions.call, + ); + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataStreamName: 'STEPS_DELTA', + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + // You can use sampleSets to add more sampling points to the sampling dataset. + SampleSet sampleSet = SampleSet( + dataCollector, + [ + SamplePoint( + dataCollector: dataCollector, + startTime: DateTime.parse('2020-10-10 12:00:00'), + endTime: DateTime.parse('2020-10-10 12:12:00'), + fieldValueOptions: FieldInt( + Field.FIELD_STEPS_DELTA, + 100, + ), + ), + ], + ); + // Call the api with the constructed sample set. + try { + _dataController.insert(sampleSet); + log( + 'insert', + _dataTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'insert', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // Reads the user data between the specified start and end dates. + void read() async { + log( + 'read', + _dataTextController, + LogOptions.call, + ); + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + dataStreamName: 'STEPS_DELTA', + ); + + // Build the time range for the query: start time and end time. + ReadOptions readOptions = ReadOptions( + dataCollectors: [ + dataCollector, + ], + startTime: DateTime.parse('2020-10-10 12:00:00'), + endTime: DateTime.parse('2020-10-10 12:12:00'), + )..groupByTime(10000); + + // Call the api with the constructed ReadOptions instance. + try { + ReadReply? readReply = await _dataController.read(readOptions); + log( + 'read', + _dataTextController, + LogOptions.success, + result: readReply.toString(), + ); + } on PlatformException catch (e) { + log( + 'read', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Reads the daily summation between the dates: `2020.10.02` to `2020.12.15` for multiple data types. + /// Note that the time format is different for this method. + void readDailySummationList() async { + log( + 'readDailySummationList', + _dataTextController, + LogOptions.call, + ); + try { + List? sampleSets = await _dataController.readDailySummationList( + [DataType.DT_CONTINUOUS_STEPS_DELTA, DataType.DT_CONTINUOUS_CALORIES_BURNT], + 20201002, + 20201003, + ); + log( + 'readDailySummationList', + _dataTextController, + LogOptions.success, + result: sampleSets.toString(), + ); + } on PlatformException catch (e) { + log( + 'readDailySummationList', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Reads the steps summation for today. + void readTodaySummation() async { + log( + 'readTodaySummation', + _dataTextController, + LogOptions.call, + ); + try { + SampleSet? sampleSet = await _dataController.readTodaySummation( + DataType.DT_CONTINUOUS_STEPS_DELTA, + ); + log( + 'readTodaySummation', + _dataTextController, + LogOptions.success, + result: sampleSet.toString(), + ); + } on PlatformException catch (e) { + log( + 'readTodaySummation', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Updates DT_CONTINUOUS_STEPS_DELTA for the specified dates. + void update() async { + log( + 'update', + _dataTextController, + LogOptions.call, + ); + + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataStreamName: 'STEPS_DELTA', + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + + // You can use sampleSets to add more sampling points to the sampling dataset. + SampleSet sampleSet = SampleSet( + dataCollector, + [ + SamplePoint( + dataCollector: dataCollector, + startTime: DateTime.parse('2020-12-12 09:00:00'), + endTime: DateTime.parse('2020-12-12 09:05:00'), + fieldValueOptions: FieldInt( + Field.FIELD_STEPS_DELTA, + 120, + ), + ), + ], + ); + + // Build a parameter object for the update. + // Note: (1) The start time of the modified object updateOptions can not be greater than the minimum + // value of the start time of all sample data points in the modified data sample set + // (2) The end time of the modified object updateOptions can not be less than the maximum value of the + // end time of all sample data points in the modified data sample set + UpdateOptions updateOptions = UpdateOptions( + startTime: DateTime.parse('2020-12-12 08:00:00'), + endTime: DateTime.parse('2020-12-12 09:25:00'), + sampleSet: sampleSet, + ); + try { + await _dataController.update(updateOptions); + log( + 'update', + _dataTextController, + LogOptions.success, + result: sampleSet.toString(), + ); + } on PlatformException catch (e) { + log( + 'update', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of DataController Methods + + // SettingController Methods + // + /// Adds a custom DataType with the FIELD_ALTITUDE. + void addDataType() async { + log( + 'addDataType', + _settingTextController, + LogOptions.call, + ); + try { + // The name of the created data type must be prefixed with the package name + // of the app. Otherwise, the creation fails. If the same data type is tried to + // be added again an exception will be thrown. + DataTypeAddOptions options = DataTypeAddOptions( + '$packageName.myCustomDataType', + [ + const Field.newIntField('myIntField'), + Field.FIELD_ALTITUDE, + ], + ); + final DataType dataTypeResult = await SettingController.addDataType( + options, + ); + log( + 'addDataType', + _settingTextController, + LogOptions.success, + result: dataTypeResult.toString(), + ); + } on PlatformException catch (e) { + log( + 'addDataType', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Reads the inserted data type on the [addDataType] method. + void readDataType() async { + log( + 'readDataType', + _settingTextController, + LogOptions.call, + ); + try { + final DataType dataTypeResult = await SettingController.readDataType( + '$packageName.myCustomDataType', + ); + log( + 'readDataType', + _settingTextController, + LogOptions.success, + result: dataTypeResult.toString(), + ); + } on PlatformException catch (e) { + log( + 'readDataType', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Disables the Health Kit function, cancels user authorization, and cancels + /// all data records. (The task takes effect in 24 hours.) + void disableHiHealth() async { + log( + 'disableHiHealth', + _settingTextController, + LogOptions.call, + ); + try { + await SettingController.disableHiHealth(); + log( + 'disableHiHealth', + _settingTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'disableHiHealth', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Checks the user privacy authorization to Health Kit. Redirects the user to + /// the Authorization screen if the permissions are not given. + void checkHealthAppAuthorization() async { + log( + 'checkHealthAppAuthorization', + _settingTextController, + LogOptions.call, + ); + try { + await SettingController.checkHealthAppAuthorization(); + log( + 'checkHealthAppAuthorization', + _settingTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'checkHealthAppAuthorization', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Checks the user privacy authorization to Health Kit. If authorized `true` + /// value would be returned. + void getHealthAppAuthorization() async { + log( + 'getHealthAppAuthorization', + _settingTextController, + LogOptions.call, + ); + try { + final bool result = await SettingController.getHealthAppAuthorization(); + log( + 'getHealthAppAuthorization', + _settingTextController, + LogOptions.success, + result: result.toString(), + ); + } on PlatformException catch (e) { + log( + 'getHealthAppAuthorization', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void requestAuth() async { + final HealthKitAuthResult res = await SettingController.requestAuthorizationIntent( + [ + Scope.HEALTHKIT_STEP_READ, + Scope.HEALTHKIT_STEP_WRITE, + Scope.HEALTHKIT_HEIGHTWEIGHT_READ, + Scope.HEALTHKIT_HEIGHTWEIGHT_WRITE, + Scope.HEALTHKIT_HEARTRATE_READ, + Scope.HEALTHKIT_HEARTRATE_WRITE, + Scope.HEALTHKIT_ACTIVITY_RECORD_READ, + Scope.HEALTHKIT_ACTIVITY_RECORD_WRITE, + Scope.HEALTHKIT_HEARTHEALTH_READ, + Scope.HEALTHKIT_HEARTHEALTH_WRITE, + ], + true, + ); + debugPrint(res.authAccount?.accessToken); + } + + // + // + // End of SettingController Methods + + // AutoRecorderController Methods + // + // + // Callback function for AutoRecorderStream event. + void _onAutoRecorderEvent(SamplePoint? res) { + log( + '[AutoRecorderEvent] obtained, SamplePoint Field Value is ${res?.fieldValues?.toString()}', + _autoRecorderTextController, + LogOptions.custom, + ); + } + + /// Starts an Android Foreground Service to count the steps of the user. + /// The steps will be emitted to the AutoRecorderStream. + void startRecord() async { + log( + 'startRecord', + _autoRecorderTextController, + LogOptions.call, + ); + try { + await AutoRecorderController.startRecord( + DataType.DT_CONTINUOUS_STEPS_TOTAL, + NotificationProperties( + title: 'HMS Flutter Health Demo', + text: 'Counting steps', + subText: 'this is a subtext', + ticker: 'this is a ticker', + showChronometer: true, + ), + ); + log( + 'startRecord', + _autoRecorderTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'startRecord', + _autoRecorderTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Ends the Foreground service and stops the step count events. + void stopRecord() async { + log( + 'endRecord', + _autoRecorderTextController, + LogOptions.call, + ); + try { + await AutoRecorderController.stopRecord( + DataType.DT_CONTINUOUS_STEPS_TOTAL, + ); + log( + 'endRecord', + _autoRecorderTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'endRecord', + _autoRecorderTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of AutoRecorderController Methods + + // ConsentController Methods + // + /// Obtains the application id from the agconnect-services.json file. + void getAppId() async { + log( + 'getAppId', + _consentTextController, + LogOptions.call, + ); + try { + final String appId = await ConsentsController.getAppId(); + log( + 'getAppId', + _consentTextController, + LogOptions.success, + result: appId, + ); + } on PlatformException catch (e) { + log( + 'getAppId', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Gets the granted permission scopes for the app. + void getScopes() async { + log( + 'getScopes', + _consentTextController, + LogOptions.call, + ); + try { + final String appId = await ConsentsController.getAppId(); + final ScopeLangItem scopeLangItem = await ConsentsController.getScopes( + 'en-gb', + appId, + ); + log( + 'getScopes', + _consentTextController, + LogOptions.success, + result: scopeLangItem.toString(), + ); + } on PlatformException catch (e) { + log( + 'getScopes', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Revokes all the permissions that authorized for this app. + void revoke() async { + log( + 'revoke', + _consentTextController, + LogOptions.call, + ); + try { + final String appId = await ConsentsController.getAppId(); + await ConsentsController.revoke(appId); + log( + 'revoke', + _consentTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'revoke', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Revokes the distance read/write permissions for the app. + void revokeWithScopes() async { + log( + 'revokeWithScopes', + _consentTextController, + LogOptions.call, + ); + try { + // Obtain the application id. + final String appId = await ConsentsController.getAppId(); + // Call the revokeWithScopes method with desired scopes. + await ConsentsController.revokeWithScopes( + appId, + [ + Scope.HEALTHKIT_DISTANCE_WRITE, + Scope.HEALTHKIT_DISTANCE_READ, + ], + ); + log( + 'revokeWithScopes', + _consentTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'revokeWithScopes', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of ConsentController Methods + + // HealthController Methods + // + void addHealthRecord() async { + log( + 'addHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + final DateTime startTime = DateTime(2023, 5, 11); + final DateTime endTime = DateTime(2023, 5, 13); + + DataCollector contDataCollector = DataCollector( + dataStreamName: 'contDataCollector', + packageName: packageName, + dataType: DataType.POLYMERIZE_CONTINUOUS_HEART_RATE_STATISTICS, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + + DataCollector instDataCollector = DataCollector( + dataStreamName: 'instDataCollector', + packageName: packageName, + dataType: DataType.DT_INSTANTANEOUS_HEART_RATE, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + + List subDataDetails = [ + SampleSet(instDataCollector, [ + SamplePoint( + dataCollector: instDataCollector, + ) + ..setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) + ..setFieldValue(Field.FIELD_BPM, 88.0) + ]) + ]; + + List subDataSummary = [ + SamplePoint( + dataCollector: contDataCollector, + ) + ..setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) + ..setFieldValue(Field.FIELD_AVG, 90.0) + ..setFieldValue(Field.FIELD_MAX, 100.0) + ..setFieldValue(Field.FIELD_MIN, 80.0) + ..setFieldValue(Field.LAST, 85.0) + ]; + + final HealthRecord healthRecord = HealthRecord( + startTime: startTime, + endTime: endTime, + metadata: 'Data', + dataCollector: DataCollector( + dataStreamName: 'such as step count', + packageName: packageName, + dataType: HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ), + ) + ..setSubDataSummary(subDataSummary) + ..setSubDataDetails(subDataDetails) + ..setFieldValue(HealthFields.FIELD_THRESHOLD, 42.0) + ..setFieldValue(HealthFields.FIELD_MAX_HEART_RATE, 48.0) + ..setFieldValue(HealthFields.FIELD_MIN_HEART_RATE, 42.0) + ..setFieldValue(HealthFields.FIELD_AVG_HEART_RATE, 45.0); + + final String? result = await HealthRecordController.addHealthRecord( + HealthRecordInsertOptions( + healthRecord: healthRecord, + ), + ); + log( + 'addHealthRecord', + _healthTextController, + LogOptions.success, + result: result.toString(), + ); + } on PlatformException catch (e) { + log( + 'addHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void getHealthRecord() async { + log( + 'getHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + final DateTime startTime = DateTime(2023, 5, 11); + final DateTime endTime = DateTime(2023, 5, 13); + + HealthRecordReply result = await HealthRecordController.getHealthRecord( + HealthRecordReadOptions( + packageName: packageName, + ) + ..setSubDataTypeList( + [ + DataType.DT_INSTANTANEOUS_HEART_RATE, + ], + ) + ..setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS, + ) + ..readByDataType( + HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, + ) + ..readHealthRecordsFromAllApps(), + ); + log( + 'getHealthRecord', + _healthTextController, + LogOptions.success, + result: result.healthRecords[0].toJson(), + ); + } on PlatformException catch (e) { + log( + 'getHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void updateHealthRecord() async { + log( + 'updateHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + final DateTime startTime = DateTime(2022, 10, 11); + final DateTime endTime = DateTime(2022, 10, 12); + final HealthRecord healthRecord = HealthRecord( + startTime: startTime, + endTime: endTime, + metadata: 'Data', + dataCollector: DataCollector( + dataStreamName: 'such as step count', + packageName: packageName, + dataType: HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ), + ) + ..setFieldValue(HealthFields.FIELD_THRESHOLD, 41.9) + ..setFieldValue(HealthFields.FIELD_MAX_HEART_RATE, 49.1) + ..setFieldValue(HealthFields.FIELD_MIN_HEART_RATE, 41.1) + ..setFieldValue(HealthFields.FIELD_AVG_HEART_RATE, 45.1); + await HealthRecordController.updateHealthRecord( + HealthRecordUpdateOptions( + healthRecord: healthRecord, + healthRecordId: '', + ), + ); + log( + 'updateHealthRecord', + _healthTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'updateHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void deleteHealthRecord() async { + log( + 'deleteHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + await HealthRecordController.deleteHealthRecord( + HealthRecordDeleteOptions( + startTime: DateTime.now().subtract(const Duration(days: 14)), + endTime: DateTime.now(), + )..setHealthRecordIds( + [ + '', + ], + ), + ); + log( + 'deleteHealthRecord', + _healthTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'deleteHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of HealthController Methods + + // App's widgets. + // + // + Widget expansionCard({ + required String titleText, + required List children, + }) { + return Card( + margin: componentPadding, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: ExpansionTile( + title: Text( + titleText, + style: cardTitleTextStyle, + ), + children: children, + ), + ); + } + + Widget loggingArea( + TextEditingController moduleTextController, + ) { + return Column( + children: [ + Container( + margin: componentPadding, + padding: const EdgeInsets.all(8.0), + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.0), + border: Border.all(color: Colors.black12), + ), + child: TextField( + readOnly: true, + maxLines: 15, + controller: moduleTextController, + decoration: const InputDecoration( + enabledBorder: InputBorder.none, + ), + ), + ), + TextButton( + child: const Text('Clear Log'), + onPressed: () => setState(() { + moduleTextController.text = ''; + }), + ) + ], + ); + } + + void showSnackBar( + String text, { + Color color = Colors.blue, + }) { + final SnackBar snackBar = SnackBar( + content: Text(text), + backgroundColor: color, + action: SnackBarAction( + label: 'Close', + textColor: Colors.white, + onPressed: () { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + }, + ), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: const Text( + 'Huawei Health Kit', + style: TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + elevation: 0.0, + actions: [ + IconButton( + onPressed: requestAuth, + icon: const Icon(Icons.ac_unit), + ), + ], + ), + body: Builder( + builder: (BuildContext context) { + return ListView( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + children: [ + // Sign In Widgets + Card( + margin: componentPadding, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: componentPadding, + child: Text( + 'Tap to SignIn button to obtain the HMS Account to complete ' + 'login and authorization, and then use other buttons ' + 'to try the related API functions.', + textAlign: TextAlign.center, + ), + ), + const Padding( + padding: componentPadding, + child: Text( + 'Note: If the login page is not displayed, change the package ' + 'name, AppID, and configure the signature file by referring ' + 'to the developer guide on the official website.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.blue, + ), + ), + ), + Container( + padding: componentPadding, + width: double.infinity, + child: OutlinedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Colors.blue, + ), + ), + child: const Text( + 'SignIn', + style: TextStyle( + color: Colors.white, + ), + ), + onPressed: () => signIn(), + ), + ), + ], + ), + ), + + // ActivityRecordsController + expansionCard( + titleText: 'ActivityRecords Controller', + children: [ + loggingArea(_activityTextController), + ListTile( + title: const Text('AddActivityRecord'), + onTap: () => addActivityRecord(), + ), + ListTile( + title: const Text('GetActivityRecord'), + onTap: () => getActivityRecord(), + ), + ListTile( + title: const Text('beginActivityRecord'), + onTap: () => beginActivityRecord(), + ), + ListTile( + title: const Text('endActivityRecord'), + onTap: () => endActivityRecord(), + ), + ListTile( + title: const Text('endAllActivityRecords'), + onTap: () => endAllActivityRecords(), + ), + ], + ), + // DataController Widgets + expansionCard( + titleText: 'DataController', + children: [ + loggingArea(_dataTextController), + ListTile( + title: const Text('readTodaySummation'), + onTap: () => readTodaySummation(), + ), + ListTile( + title: const Text('readDailySummationList'), + onTap: () => readDailySummationList(), + ), + ListTile( + title: const Text('insert'), + onTap: () => insert(), + ), + ListTile( + title: const Text('read'), + onTap: () => read(), + ), + ListTile( + title: const Text('update'), + onTap: () => update(), + ), + ListTile( + title: const Text('delete'), + onTap: () => delete(), + ), + ListTile( + title: const Text('clearAll'), + onTap: () => clearAll(), + ), + ], + ), + // SettingController Widgets. + expansionCard( + titleText: 'SettingController', + children: [ + loggingArea(_settingTextController), + ListTile( + title: const Text('addDataType'), + onTap: () => addDataType(), + ), + ListTile( + title: const Text('readDataType'), + onTap: () => readDataType(), + ), + ListTile( + title: const Text('disableHiHealth'), + onTap: () => disableHiHealth(), + ), + ListTile( + title: const Text('checkHealthAppAuthorization'), + onTap: () => checkHealthAppAuthorization(), + ), + ListTile( + title: const Text('getHealthAppAuthorization'), + onTap: () => getHealthAppAuthorization(), + ), + ], + ), + // AutoRecorderController Widgets + expansionCard( + titleText: 'AutoRecorderController', + children: [ + loggingArea(_autoRecorderTextController), + ListTile( + title: const Text('startRecord'), + onTap: () => startRecord(), + ), + ListTile( + title: const Text('stopRecord'), + onTap: () => stopRecord(), + ), + ], + ), + // Consent Controller Widgets + expansionCard( + titleText: 'ConsentController', + children: [ + loggingArea(_consentTextController), + ListTile( + title: const Text('getAppId'), + onTap: () => getAppId(), + ), + ListTile( + title: const Text('getScopes'), + onTap: () => getScopes(), + ), + ListTile( + title: const Text('revoke'), + onTap: () => revoke(), + ), + ListTile( + title: const Text('revokeWithScopes'), + onTap: () => revokeWithScopes(), + ), + ], + ), + + // Health Controller Widgets + expansionCard( + titleText: 'HealthController', + children: [ + loggingArea(_healthTextController), + ListTile( + title: const Text('addHealthRecord'), + onTap: () => addHealthRecord(), + ), + ListTile( + title: const Text('getHealthRecord'), + onTap: () => getHealthRecord(), + ), + ListTile( + title: const Text('updateHealthRecord'), + onTap: () => updateHealthRecord(), + ), + ListTile( + title: const Text('deleteHealthRecord'), + onTap: () => deleteHealthRecord(), + ), + ], + ), + ], + ); + }, + ), + ); + } +} + +/// Options for logging. +enum LogOptions { + call, + success, + error, + custom, +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index f356310..61f0af2 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -9,6 +9,7 @@ import 'package:hmg_patient_app_new/presentation/e_referral/new_e_referral.dart' import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/huawei_health_example.dart'; import 'package:hmg_patient_app_new/presentation/smartwatches/smartwatch_instructions_page.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/possible_conditions_screen.dart'; @@ -34,6 +35,7 @@ class AppRoutes { static const String zoomCallPage = '/zoomCallPage'; static const String bloodDonationPage = '/bloodDonationPage'; static const String smartWatches = '/smartWatches'; + static const String huaweiHealthExample = '/huaweiHealthExample'; //appointments static const String bookAppointmentPage = '/bookAppointmentPage'; @@ -72,6 +74,7 @@ class AppRoutes { userInfoSelection: (context) => UserInfoSelectionScreen(), userInfoFlowManager: (context) => UserInfoFlowManager(), smartWatches: (context) => SmartwatchInstructionsPage(), + huaweiHealthExample: (context) => HuaweiHealthExample(), // }; diff --git a/pubspec.yaml b/pubspec.yaml index ecfc5ef..5590c9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,6 +86,7 @@ dependencies: location: ^8.0.1 gms_check: ^1.0.4 huawei_location: ^6.14.2+301 + huawei_health: ^6.16.0+300 intl: ^0.20.2 flutter_widget_from_html: ^0.17.1 huawei_map: From 3060c572019843d7b444da7732a912e32ba8071e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 15 Dec 2025 10:59:54 +0300 Subject: [PATCH 4/5] updates --- android/app/build.gradle.kts | 4 ++-- android/settings.gradle.kts | 3 ++- lib/core/api_consts.dart | 2 +- lib/core/utils/size_utils.dart | 4 ++-- .../appointments/widgets/appointment_card.dart | 12 +++++++----- .../widgets/appointment_doctor_card.dart | 2 +- .../book_appointment/book_appointment_page.dart | 2 +- lib/presentation/contact_us/feedback_page.dart | 2 +- lib/presentation/hmg_services/services_page.dart | 4 ++-- lib/presentation/home/landing_page.dart | 2 +- .../widgets/medical_file_appointment_card.dart | 2 +- lib/routes/app_routes.dart | 1 - lib/splashPage.dart | 2 +- 13 files changed, 22 insertions(+), 20 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 226d4dd..0ffb97d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "com.ejada.hmg" // minSdk = 24 minSdk = 26 - targetSdk = 35 - compileSdk = 35 + targetSdk = 36 + compileSdk = 36 // targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index ab39a10..3e6502f 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -18,7 +18,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.3" apply false +// id("com.android.application") version "8.7.3" apply false + id("com.android.application") version "8.9.3" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false } diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index d284b5c..acfbbc0 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -703,7 +703,7 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/utils/size_utils.dart b/lib/core/utils/size_utils.dart index fdd0d30..8a0703e 100644 --- a/lib/core/utils/size_utils.dart +++ b/lib/core/utils/size_utils.dart @@ -27,7 +27,7 @@ extension ResponsiveExtension on num { double clamp; if (SizeUtils.deviceType == DeviceType.tablet || _isFoldable) { // More conservative scaling for tablets and foldables - clamp = (aspectRatio > 1.5 || aspectRatio < 0.67) ? 1.4 : 1.1; + clamp = (aspectRatio > 1.5 || aspectRatio < 0.67) ? 1.6 : 1.4; } else { // Original logic for phones clamp = (aspectRatio > 1.3 || aspectRatio < 0.77) ? 1.6 : 1.2; @@ -68,7 +68,7 @@ extension ResponsiveExtension on num { double get r { double baseScale = (this * _screenWidth) / figmaDesignWidth; - if (_isFoldable) { + if (_isFoldable || isTablet) { // Use the same logic as enhanced width for foldables double scale = _screenWidth / figmaDesignWidth; scale = scale.clamp(0.8, 1.4); diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index b4d327b..9bf16f3 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -129,7 +129,7 @@ class AppointmentCard extends StatelessWidget { children: [ Image.network( isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : patientAppointmentHistoryResponseModel.doctorImageURL!, - width: 63.w, + width: 63.h, height: 63.h, fit: BoxFit.cover, ).circle(100.r).toShimmer2(isShow: isLoading), @@ -238,7 +238,8 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + // height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppAssets.checkmark_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, @@ -264,7 +265,7 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction), iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), iconSize: 15.h, @@ -336,7 +337,8 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + // height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppAssets.ask_doctor_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, @@ -353,7 +355,7 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppAssets.rebook_appointment_icon, iconColor: AppColors.blackColor, iconSize: 16.h, diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 405aa14..6b5dde7 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -47,7 +47,7 @@ class AppointmentDoctorCard extends StatelessWidget { children: [ Image.network( patientAppointmentHistoryResponseModel.doctorImageURL!, - width: 63.w, + width: 63.h, height: 63.h, fit: BoxFit.cover, ).circle(100.r), diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index a27e76d..4d24da7 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -144,7 +144,7 @@ class _BookAppointmentPageState extends State { children: [ Image.network( myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, - width: 64.w, + width: 64.h, height: 64.h, fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: false, radius: 50.r), diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index db7c218..7160179 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -349,7 +349,7 @@ class FeedbackPage extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 10.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppAssets.file_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 76f0269..001af4c 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -179,7 +179,7 @@ class ServicesPage extends StatelessWidget { SizedBox(height: 16.h), GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, // 4 icons per row + crossAxisCount: (isFoldable || isTablet) ? 6 : 4, // 4 icons per row crossAxisSpacing: 12.w, mainAxisSpacing: 18.h, childAspectRatio: 0.8, @@ -342,7 +342,7 @@ class ServicesPage extends StatelessWidget { SizedBox(height: 16.h), GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, // 4 icons per row + crossAxisCount:(isFoldable || isTablet) ? 6 : 4, // 4 icons per row crossAxisSpacing: 12.w, mainAxisSpacing: 18.h, childAspectRatio: 0.8, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index f9ea1ee..ae2e4a6 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -478,7 +478,7 @@ class _LandingPageState extends State { padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), icon: AppAssets.add_icon, iconColor: AppColors.primaryRedColor, - height: 46.h, + height: 40.h, ), ], ), diff --git a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart index 1b09dd5..fbe79bb 100644 --- a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart @@ -173,7 +173,7 @@ class MedicalFileAppointmentCard extends StatelessWidget { backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, - fontSize: 14, + fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index f33199d..d25a9de 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -79,7 +79,6 @@ class AppRoutes { huaweiHealthExample: (context) => HuaweiHealthExample(), // - zoomCallPage: (context) => CallScreen(), healthCalculatorsPage: (context) => HealthCalculatorsPage() }; diff --git a/lib/splashPage.dart b/lib/splashPage.dart index b5b752b..9aa16ee 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -218,7 +218,7 @@ class _SplashScreenState extends State { // AppSharedPreferences().setString(APP_LANGUAGE, projectProvider.isArabic ? "ar" : "en"); // var themeNotifier = Provider.of(context, listen: false); // themeNotifier.setTheme(defaultTheme(fontName: projectProvider.isArabic ? 'Cairo' : 'Poppins')); - PushNotificationHandler().init(context); // Asyncronously + // PushNotificationHandler().init(context); // Asyncronously } @override From fe294ae8f62ed677b5486470d7a18803de78078a Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 15 Dec 2025 11:16:44 +0300 Subject: [PATCH 5/5] updates --- .../health_calculator_result.dart | 16 ++++++++++++++++ .../health_calculators_page.dart | 13 ------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/presentation/health_calculators/health_calculator_result.dart b/lib/presentation/health_calculators/health_calculator_result.dart index 5315a89..5abe410 100644 --- a/lib/presentation/health_calculators/health_calculator_result.dart +++ b/lib/presentation/health_calculators/health_calculator_result.dart @@ -1,3 +1,4 @@ +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/enums.dart'; @@ -5,8 +6,10 @@ 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/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'; class HealthCalculatorResultPage extends StatelessWidget { HealthCalculatorsTypeEnum calculatorType; @@ -18,6 +21,19 @@ class HealthCalculatorResultPage extends StatelessWidget { Widget build(BuildContext context) { return CollapsingListView( title: "Your ${calculatorType.name.toCamelCase}", + bottomChild: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.only(topLeft: Radius.circular(24.r), topRight: Radius.circular(24.r))), + padding: EdgeInsets.symmetric(vertical: 20.h, horizontal: 20.h), + child: CustomButton( + text: LocaleKeys.bookAppo.tr(), + onPressed: () {}, + icon: null, + fontSize: 16.f, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + borderRadius: 12.r, + fontWeight: FontWeight.w500), + ), child: getCalculatorResultWidget(type: calculatorType, calculatedResult: calculatedResult).paddingSymmetrical(18.w, 24.h), ); } diff --git a/lib/presentation/health_calculators/health_calculators_page.dart b/lib/presentation/health_calculators/health_calculators_page.dart index ab37c2c..8ac7a22 100644 --- a/lib/presentation/health_calculators/health_calculators_page.dart +++ b/lib/presentation/health_calculators/health_calculators_page.dart @@ -10,25 +10,12 @@ 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/get_cmc_all_orders_resp_model.dart'; -import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart'; -import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/cmc_order_detail_page.dart'; -import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/cmc_selection_review_page.dart'; -import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/widgets/cmc_ui_selection_helper.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators/health_calculator_detailed_page.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators/widgets/health_card.dart'; -import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart'; import 'package:hmg_patient_app_new/services/dialog_service.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/media_viewer/full_screen_image_viewer.dart'; -import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:provider/provider.dart'; -import 'package:shimmer/shimmer.dart'; class HealthCalculatorsPage extends StatefulWidget { const HealthCalculatorsPage({super.key});