diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index bd2eb71..f802818 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -212,7 +212,7 @@ var GET_QR_PARKING = 'Services/SWP.svc/REST/GetQRParkingByID'; //URL to get clinic list var GET_CLINICS_LIST_URL = "Services/lists.svc/REST/GetClinicCentralized"; -var GET_CLINICS_LIST_WRT_HOSPITAL_URL = "Services/Lists.svc/REST/GetClinicFromDoctorSchedule"; +var GET_CLINICS_LIST_WRT_HOSPITAL_ID_URL = "Services/Lists.svc/REST/GetClinicFromDoctorSchedule"; //URL to get active appointment list var GET_ACTIVE_APPOINTMENTS_LIST_URL = "Services/Doctors.svc/Rest/Dr_GetAppointmentActiveNumber"; @@ -723,7 +723,7 @@ const DEACTIVATE_ACCOUNT = 'Services/Patients.svc/REST/PatientAppleActivation_In class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index db86889..c0b20b5 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/material.dart'; @@ -36,6 +37,7 @@ import 'package:hmg_patient_app_new/services/localauth_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/bottomsheet/exception_bottom_sheet.dart'; +import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; import 'models/request_models/get_user_mobile_device_data.dart'; import 'models/request_models/insert_patient_mobile_deviceinfo.dart'; @@ -363,7 +365,7 @@ class AuthenticationViewModel extends ChangeNotifier { ); // TODO: GET APP SMS SIGNATURE HERE - request.sMSSignature = "enKTDcqbOVd"; + request.sMSSignature = await getSignature(); if (checkIsUserComingForRegister(request: payload)) { _appState.setUserRegistrationPayload = RegistrationDataModelPayload.fromJson(payload); @@ -619,8 +621,8 @@ class AuthenticationViewModel extends ChangeNotifier { } else { // authenticated = true; await insertPatientIMEIData(loginTypeEnum.toInt); + LoaderBottomSheet.hideLoader(); } - LoaderBottomSheet.hideLoader(); notifyListeners(); // navigateToHomeScreen(); } else { @@ -875,6 +877,7 @@ class AuthenticationViewModel extends ChangeNotifier { getDeviceLastLogin = deviceInfo.first['LoginType']; await checkActivationCode(otpTypeEnum: OTPTypeEnum.faceIDFingerprint, activationCode: null, onWrongActivationCode: (String? message) {}); await insertPatientIMEIData(loginTypeEnum.toInt); + LoaderBottomSheet.hideLoader(); } if (apiResponse.messageStatus == 2) { LoaderBottomSheet.hideLoader(); @@ -940,5 +943,11 @@ class AuthenticationViewModel extends ChangeNotifier { ); } -// === OTP Widget Logics ===// + Future getSignature() async { + if (Platform.isAndroid) { + return await SmsVerification.getAppSignature(); + } else { + return null; + } + } } diff --git a/lib/features/authentication/widgets/otp_verification_screen.dart b/lib/features/authentication/widgets/otp_verification_screen.dart index 7755ee6..c6f1069 100644 --- a/lib/features/authentication/widgets/otp_verification_screen.dart +++ b/lib/features/authentication/widgets/otp_verification_screen.dart @@ -5,11 +5,15 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.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/appbar/app_bar_widget.dart'; +import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; import 'package:provider/provider.dart'; typedef OnDone = void Function(String text); @@ -94,6 +98,8 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi bool hasFocus = false; AuthenticationViewModel? authVm; + final CacheService cacheService = GetIt.instance(); + @override void didUpdateWidget(OTPWidget oldWidget) { super.didUpdateWidget(oldWidget); @@ -137,6 +143,7 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi } focusNode.addListener(_focusListener); authVm?.otpScreenNotifier.addListener(_onOtpScreenNotifierChanged); + cacheService.remove(key: CacheConst.quickLoginEnabled); } void _controllerListener() { @@ -501,6 +508,7 @@ class _OTPVerificationScreenState extends State { super.initState(); _otpController = TextEditingController(); _startResendTimer(); + checkSignature(); } @override @@ -534,6 +542,30 @@ class _OTPVerificationScreenState extends State { } } + void checkSignature() async { + SmsVerification.startListeningSms().then((message) { + final intRegex = RegExp(r'\d+', multiLine: true); + var otp = SmsVerification.getCode(message, intRegex); + if (otp != null && otp.length == _otpLength) { + autoFillOtp(otp); // Use autoFillOtp to update controller and UI + } + SmsVerification.stopListening(); + }); + } + + void _onAutoOtpChanged(String value) { + setState(() { + _isOtpComplete = value.length == _otpLength; + }); + + if (_isOtpComplete && !_isVerifying) { + _isVerifying = true; + _verifyOtp(value); + } else if (!_isOtpComplete) { + _isVerifying = false; + } + } + void _resendOtp() { if (_resendTime == 0) { setState(() { @@ -547,14 +579,6 @@ class _OTPVerificationScreenState extends State { } } - void _clearOtpAndResetState() { - setState(() { - _isVerifying = false; - _isOtpComplete = false; - }); - _otpController.clear(); - } - String _getMaskedPhoneNumber() { final phone = widget.phoneNumber; return phone.length > 4 ? '05xxxxxx${phone.substring(phone.length - 2)}' : phone; @@ -664,5 +688,7 @@ class _OTPVerificationScreenState extends State { if (otp.length != _otpLength) return; _isVerifying = false; _otpController.text = otp; + setState(() {}); + _onOtpChanged(otp); // Ensure verification and color update } } diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index f41fbc4..7454b80 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -44,6 +44,9 @@ abstract class BookAppointmentsRepo { Future>>> getProjectList(); + + Future>>> getClinicsWithRespectToClinicId(String projectID); + } class BookAppointmentsRepoImp implements BookAppointmentsRepo { @@ -409,4 +412,42 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> getClinicsWithRespectToClinicId(String projectID) async { + Map mapDevice = {"ProjectID": projectID}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_CLINICS_LIST_WRT_HOSPITAL_ID_URL, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['ListClinic']; + + final clinicsList = list.map((item) => GetClinicsListResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: clinicsList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 271db44..c2b9639 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -70,6 +70,10 @@ class BookAppointmentsViewModel extends ChangeNotifier { FacilitySelection currentlySelectedFacility = FacilitySelection.ALL; bool isRegionListLoading = false; + ///this will be used to call the clinic call when navigating from my REGION SELECTION to book appointment screen + bool shouldLoadSpecificClinic = false; + String? currentlySelectedHospitalFromRegionFlow; + BookAppointmentsViewModel({required this.bookAppointmentsRepo, required this.errorHandlerService, required this.navigationService, required this.myAppointmentsViewModel}); void initializeFilteredList() { @@ -109,7 +113,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { clinicsList.clear(); } isClinicsListLoading = value; - notifyListeners(); + // notifyListeners(); } setSelectedClinic(GetClinicsListResponseModel clinic) { @@ -148,7 +152,18 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } - Future getClinics({Function(dynamic)? onSuccess, Function(String)? onError}) async { + /// this function will decide which clinic api to be called + /// either api for region flow or the select clinic api + Future getClinics() async + { + if(shouldLoadSpecificClinic) { + getRegionSelectedClinics(); + } else { + getAllClinics(); + } + } + + Future getAllClinics({Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await bookAppointmentsRepo.getClinics(); result.fold( @@ -173,6 +188,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { Future getDoctorsList( {int projectID = 0, bool isNearest = false, int doctorId = 0, String doctorName = "", isContinueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { doctorsList.clear(); + projectID = currentlySelectedHospitalFromRegionFlow != null?int.parse(currentlySelectedHospitalFromRegionFlow!):projectID; final result = await bookAppointmentsRepo.getDoctorsList(selectedClinic.clinicID ?? 0, projectID, isNearest, doctorId, doctorName); result.fold( @@ -391,6 +407,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { } Future getRegionMappedProjectList() async { + //todo handle the case in the location is switch on if(hospitalList != null && hospitalList!.registeredDoctorMap != null && hospitalList!.registeredDoctorMap!.isNotEmpty){ filteredHospitalList = hospitalList; return; @@ -469,4 +486,33 @@ class BookAppointmentsViewModel extends ChangeNotifier { isLocationEnabled().then((value) => isLocationAvaiable = value); return isLocationAvaiable; } + + void setLoadSpecificClinic(bool status) { + shouldLoadSpecificClinic = status; + } + + void setProjectID(String? mainProjectID) { + currentlySelectedHospitalFromRegionFlow = mainProjectID; + } + + Future getRegionSelectedClinics() async{ + final result = await bookAppointmentsRepo.getClinicsWithRespectToClinicId(currentlySelectedHospitalFromRegionFlow??""); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + clinicsList = apiResponse.data!; + isClinicsListLoading = false; + initializeFilteredList(); + notifyListeners(); + } + }, + ); + } + void resetFilterList(){ + filteredHospitalList = hospitalList; + } } diff --git a/lib/features/my_appointments/appointment_via_region_viewmodel.dart b/lib/features/my_appointments/appointment_via_region_viewmodel.dart index 7bcd0a6..a49e1c4 100644 --- a/lib/features/my_appointments/appointment_via_region_viewmodel.dart +++ b/lib/features/my_appointments/appointment_via_region_viewmodel.dart @@ -1,4 +1,8 @@ import 'package:flutter/foundation.dart' show ChangeNotifier; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; enum AppointmentViaRegionState { REGION_SELECTION, @@ -11,9 +15,13 @@ enum AppointmentViaRegionState { class AppointmentViaRegionViewmodel extends ChangeNotifier { String? selectedRegionId; String? selectedFacilityType; + PatientDoctorAppointmentList? selectedHospital; + final NavigationService navigationService; AppointmentViaRegionState bottomSheetState = AppointmentViaRegionState.REGION_SELECTION; + AppointmentViaRegionViewmodel({required this.navigationService}); + void setSelectedRegionId(String? regionId) { selectedRegionId = regionId; notifyListeners(); @@ -29,10 +37,16 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { notifyListeners(); } + void handleLastStep(){ + navigationService.pop(); + navigationService.push(FadePage( + page: SelectClinicPage(), + ),); + } + void handleBackPress() { switch (bottomSheetState) { case AppointmentViaRegionState.REGION_SELECTION: - // Do nothing or exit the bottom sheet break; case AppointmentViaRegionState.TYPE_SELECTION: setBottomSheetState(AppointmentViaRegionState.REGION_SELECTION); @@ -41,19 +55,6 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { case AppointmentViaRegionState.HOSPITAL_SELECTION: setBottomSheetState(AppointmentViaRegionState.TYPE_SELECTION); break; - - // case AppointmentViaRegionState.HOSPITAL_SELECTION: - // case AppointmentViaRegionState.CLINIC_SELECTION: - // setBottomSheetState(AppointmentViaRegionState.TYPE_SELECTION); - // setFacility(null); - // break; - // case AppointmentViaRegionState.DOCTOR_SELECTION: - // if (selectedFacilityType == 'Hospital') { - // setBottomSheetState(AppointmentViaRegionState.HOSPITAL_SELECTION); - // } else if (selectedFacilityType == 'Medical Center') { - // setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); - // } - // break; default: } } @@ -63,4 +64,8 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { setFacility(null); setBottomSheetState(AppointmentViaRegionState.REGION_SELECTION); } + + void setHospitalModel(PatientDoctorAppointmentList? hospital) { + selectedHospital = hospital; + } } diff --git a/lib/main.dart b/lib/main.dart index 02f8dd1..c75cf91 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -146,7 +146,8 @@ void main() async { ), ), ChangeNotifierProvider( - create: (_) => AppointmentViaRegionViewmodel()) + create: (_) => + AppointmentViaRegionViewmodel(navigationService: getIt())) ], child: MyApp()), ), ); diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart index 242c7ca..71cc07d 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/enums.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_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; @@ -76,21 +77,28 @@ class HospitalBottomSheetBody extends StatelessWidget { SizedBox( height: MediaQuery.sizeOf(context).height * .4, child: ListView.separated( - itemBuilder: (_, index) => HospitalListItem( - hospitalData: regionalViewModel.selectedFacilityType == - FacilitySelection.HMG.name - ? appointmentsViewModel - .filteredHospitalList! - .registeredDoctorMap![ - regionalViewModel.selectedRegionId!]! - .hmgDoctorList![index] - : appointmentsViewModel - .filteredHospitalList - ?.registeredDoctorMap?[ - regionalViewModel.selectedRegionId!] - ?.hmcDoctorList?[index], - isLocationEnabled: appointmentsViewModel.getLocationStatus(), - ), + itemBuilder: (_, index) + { + var hospital = regionalViewModel.selectedFacilityType == + FacilitySelection.HMG.name + ? appointmentsViewModel + .filteredHospitalList! + .registeredDoctorMap![ + regionalViewModel.selectedRegionId!]! + .hmgDoctorList![index] + : appointmentsViewModel + .filteredHospitalList + ?.registeredDoctorMap?[ + regionalViewModel.selectedRegionId!] + ?.hmcDoctorList?[index]; + return HospitalListItem( + hospitalData: hospital, + isLocationEnabled: appointmentsViewModel.getLocationStatus(), + ).onPress(() { + regionalViewModel.setHospitalModel(hospital); + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); + regionalViewModel.handleLastStep(); + });}, separatorBuilder: (_, __) => SizedBox( height: 16.h, ), diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index a6d6c5e..482d864 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -122,6 +122,8 @@ class _BookAppointmentPageState extends State { ], ).onPress(() { bookAppointmentsViewModel.setIsClinicsListLoading(true); + bookAppointmentsViewModel.setLoadSpecificClinic(false); + bookAppointmentsViewModel.setProjectID(null); Navigator.of(context).push( FadePage( page: SelectClinicPage(), @@ -194,6 +196,7 @@ class _BookAppointmentPageState extends State { } void openRegionListBottomSheet(BuildContext context) { + regionalViewModel.flush(); // AppointmentViaRegionViewmodel? viewmodel = null; showCommonBottomSheetWithoutHeight(context, title: "", @@ -203,7 +206,6 @@ class _BookAppointmentPageState extends State { child: Consumer(builder: (_, data, __) { return getRegionalSelectionWidget(data); }), callBackFunc: () { - regionalViewModel.flush(); }); } @@ -212,11 +214,20 @@ class _BookAppointmentPageState extends State { return RegionBottomSheetBody(); } if(data.bottomSheetState == AppointmentViaRegionState.TYPE_SELECTION){ + bookAppointmentsViewModel.resetFilterList(); return FacilityTypeSelectionWidget(selectedRegion: data.selectedRegionId??"",); } if (data.bottomSheetState == AppointmentViaRegionState.HOSPITAL_SELECTION) { return HospitalBottomSheetBody(); - } else { + } + if (data.bottomSheetState == AppointmentViaRegionState.CLINIC_SELECTION) { + // Navigator.of(context).pop(); + bookAppointmentsViewModel.setIsClinicsListLoading(true); + bookAppointmentsViewModel.setLoadSpecificClinic(true); + bookAppointmentsViewModel.setProjectID(regionalViewModel.selectedHospital?.hospitalList.first?.mainProjectID.toString()); + + } + else { SizedBox.shrink(); } return SizedBox.shrink(); diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 2d40869..9f8ae79 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -93,6 +93,7 @@ class _LandingPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + appState.isAuthenticated ? WelcomeWidget( onTap: () { diff --git a/lib/presentation/home/navigation_screen.dart b/lib/presentation/home/navigation_screen.dart index 981363e..8feeb5f 100644 --- a/lib/presentation/home/navigation_screen.dart +++ b/lib/presentation/home/navigation_screen.dart @@ -27,10 +27,10 @@ class _LandingNavigationState extends State { physics: const NeverScrollableScrollPhysics(), children: [ const LandingPage(), - appState.isAuthenticated ? MedicalFilePage() :/* need add feedback page */ const LandingPage(), + appState.isAuthenticated ? MedicalFilePage() : /* need add feedback page */ const LandingPage(), BookAppointmentPage(), const LandingPage(), - appState.isAuthenticated ? /* need add news page */ LandingPage() : const LandingPage(), + appState.isAuthenticated ? /* need add news page */ LandingPage() : const LandingPage(), ], ), bottomNavigationBar: BottomNavigation( diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index 3ece752..85c13f2 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -41,7 +41,6 @@ class AppCustomChipWidget extends StatelessWidget { @override Widget build(BuildContext context) { - print("detected icon: $deleteIcon"); return ChipTheme( data: ChipThemeData( padding: EdgeInsets.all(0.0), diff --git a/pubspec.yaml b/pubspec.yaml index 228376d..e55d241 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: firebase_analytics: ^11.5.1 jiffy: ^6.4.3 hijri_gregorian_calendar: ^0.1.1 + sms_otp_auto_verify: ^2.2.0 web: any flutter_staggered_animations: ^1.1.1