From f9bfc131a8d344ee7b9aebb11c0d496ca07ad5a7 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Mon, 22 Sep 2025 09:55:38 +0300 Subject: [PATCH 01/43] family screen & widgets --- lib/core/api_consts.dart | 3 ++ lib/core/utils/request_utils.dart | 11 ++++ .../authentication/authentication_repo.dart | 21 +++++--- .../authentication_view_model.dart | 39 +++++++++++--- lib/presentation/my_family/my_Family.dart | 54 ++++++------------- lib/services/dialog_service.dart | 5 +- lib/widgets/common_bottom_sheet.dart | 3 ++ 7 files changed, 81 insertions(+), 55 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 5886dfa..9b013a5 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -808,6 +808,9 @@ class ApiConsts { static final String registerUser = 'Services/Authentication.svc/REST/PatientRegistration'; static final String addFamilyFile = 'Services/Patients.svc/REST/ShareFamilyFileService'; + static final String sendFamilyFileActivation = 'Services/Authentication.svc/REST/SendActivationCodeForFamilyFile'; + static final String checkActivationCodeForFamily = 'Services/Authentication.svc/REST/CheckActivationCodeForFamilyFile'; + // static values for Api static final double appVersionID = 18.7; diff --git a/lib/core/utils/request_utils.dart b/lib/core/utils/request_utils.dart index 8b993f1..87f88f5 100644 --- a/lib/core/utils/request_utils.dart +++ b/lib/core/utils/request_utils.dart @@ -125,6 +125,8 @@ class RequestUtils { required bool isForRegister, required bool isFileNo, dynamic payload, + required bool isExcludedUser, + int? responseID, }) { AppState _appState = getIt.get(); var request = SendActivationRequest(); @@ -156,6 +158,15 @@ class RequestUtils { request.isRegister = false; } request.deviceTypeID = request.searchType; + + if (isExcludedUser) { + //INFO: Only for Excluded User Family Member Addition + request.isPatientExcluded = isExcludedUser; + request.responseID = responseID; + request.status = 2; + request.familyRegionID = zipCode == CountryEnum.saudiArabia.countryCode ? 1 : 2; + } + return request; } diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index 2eaeb80..89a9404 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -17,12 +17,9 @@ abstract class AuthenticationRepo { Future>> checkPatientAuthentication({required dynamic checkPatientAuthenticationReq}); - Future>> sendActivationCodeRepo({required dynamic sendActivationCodeReq, String? languageID, bool isRegister = false}); + Future>> sendActivationCodeRepo({required dynamic sendActivationCodeReq, String? languageID, bool isRegister = false, bool isExcludedUser = false}); - Future>> checkActivationCodeRepo( - {required dynamic newRequest, // could be CheckActivationCodeReq or CheckActivationCodeRegisterReq - required String? activationCode, - required bool isRegister}); + Future>> checkActivationCodeRepo({required dynamic newRequest, required String? activationCode, required bool isRegister, bool isExcludedUser = false}); Future>> checkIfUserAgreed({required dynamic commonAuthanticatedRequest}); @@ -134,6 +131,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { required dynamic sendActivationCodeReq, String? languageID, bool isRegister = false, + bool isExcludedUser = false, }) async { int isOutKsa = (sendActivationCodeReq.zipCode == '966' || sendActivationCodeReq.zipCode == '+966') ? 0 : 1; sendActivationCodeReq.patientOutSA = isOutKsa; @@ -144,7 +142,11 @@ class AuthenticationRepoImp implements AuthenticationRepo { Failure? failure; await apiClient.post( - isRegister ? ApiConsts.sendActivationCodeRegister : ApiConsts.sendActivationCode, + isExcludedUser + ? ApiConsts.sendFamilyFileActivation + : isRegister + ? ApiConsts.sendActivationCodeRegister + : ApiConsts.sendActivationCode, body: sendActivationCodeReq.toJson(), onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; @@ -176,6 +178,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { required dynamic newRequest, // could be CheckActivationCodeReq or CheckActivationCodeRegisterReq required String? activationCode, required bool isRegister, + bool isExcludedUser = false, }) async { if (isRegister) { newRequest["activationCode"] = activationCode ?? "0000"; @@ -189,7 +192,11 @@ class AuthenticationRepoImp implements AuthenticationRepo { newRequest.isRegister = false; } - final endpoint = isRegister ? ApiConsts.checkActivationCodeRegister : ApiConsts.checkActivationCode; + final endpoint = isExcludedUser + ? ApiConsts.checkActivationCodeForFamily + : isRegister + ? ApiConsts.checkActivationCodeRegister + : ApiConsts.checkActivationCode; try { GenericApiModel? apiResponse; diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 11374b7..4590a59 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -344,7 +344,14 @@ class AuthenticationViewModel extends ChangeNotifier { } Future sendActivationCode( - {required OTPTypeEnum otpTypeEnum, required String nationalIdOrFileNumber, required String phoneNumber, required bool isForRegister, dynamic payload, bool isComingFromResendOTP = false}) async { + {required OTPTypeEnum otpTypeEnum, + required String nationalIdOrFileNumber, + required String phoneNumber, + required bool isForRegister, + dynamic payload, + bool isComingFromResendOTP = false, + bool isExcludedUser = false, + int? responseID}) async { var request = RequestUtils.getCommonRequestSendActivationCode( otpTypeEnum: otpTypeEnum, mobileNumber: phoneNumber, @@ -360,6 +367,8 @@ class AuthenticationViewModel extends ChangeNotifier { ? false : true, payload: payload, + isExcludedUser: isExcludedUser, + responseID: responseID, ); // TODO: GET APP SMS SIGNATURE HERE @@ -369,7 +378,8 @@ class AuthenticationViewModel extends ChangeNotifier { _appState.setUserRegistrationPayload = RegistrationDataModelPayload.fromJson(payload); } - final resultEither = await _authenticationRepo.sendActivationCodeRepo(sendActivationCodeReq: request, isRegister: checkIsUserComingForRegister(request: payload), languageID: 'er'); + final resultEither = + await _authenticationRepo.sendActivationCodeRepo(sendActivationCodeReq: request, isRegister: checkIsUserComingForRegister(request: payload), languageID: 'er', isExcludedUser: isExcludedUser); resultEither.fold( (failure) async => await _errorHandlerService.handleError(failure: failure), @@ -384,7 +394,9 @@ class AuthenticationViewModel extends ChangeNotifier { } else { if (apiResponse.data != null && apiResponse.data['isSMSSent'] == true) { LoaderBottomSheet.hideLoader(); - if (!isComingFromResendOTP) navigateToOTPScreen(otpTypeEnum: otpTypeEnum, phoneNumber: phoneNumber, isComingFromRegister: checkIsUserComingForRegister(request: payload), payload: payload); + if (!isComingFromResendOTP) + navigateToOTPScreen( + otpTypeEnum: otpTypeEnum, phoneNumber: phoneNumber, isComingFromRegister: checkIsUserComingForRegister(request: payload), payload: payload, isExcludedUser: isExcludedUser); } else { // TODO: Handle isSMSSent false // navigateToOTPScreen(otpTypeEnum: otpTypeEnum, phoneNumber: phoneNumber); @@ -403,7 +415,13 @@ class AuthenticationViewModel extends ChangeNotifier { } Future checkActivationCode( - {required String? activationCode, required OTPTypeEnum otpTypeEnum, required Function(String? message) onWrongActivationCode, Function()? onResendActivation}) async { + {required String? activationCode, + required OTPTypeEnum otpTypeEnum, + required Function(String? message) onWrongActivationCode, + Function()? onResendActivation, + bool isExcludedUser = false, + dynamic requestID, + dynamic responseID}) async { bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1); final request = RequestUtils.getCommonRequestWelcome( @@ -429,6 +447,13 @@ class AuthenticationViewModel extends ChangeNotifier { //TODO: Error Here IN Zip Code. loginType: loginTypeEnum.toInt) .toJson(); + + if (isExcludedUser) { + request['PatientShareRequestID'] = requestID; + request['ResponseID'] = responseID; + request['Status'] = 3; + } + LoaderBottomSheet.showLoader(); if (isForRegister) { if (_appState.getUserRegistrationPayload.patientOutSa == 0) request['DOB'] = _appState.getUserRegistrationPayload.dob; @@ -464,7 +489,8 @@ class AuthenticationViewModel extends ChangeNotifier { } }); } else { - final resultEither = await _authenticationRepo.checkActivationCodeRepo(newRequest: CheckActivationCodeRegisterReq.fromJson(request), activationCode: activationCode, isRegister: false); + final resultEither = await _authenticationRepo.checkActivationCodeRepo( + newRequest: CheckActivationCodeRegisterReq.fromJson(request), activationCode: activationCode, isRegister: false, isExcludedUser: isExcludedUser); resultEither.fold( (failure) async => await _errorHandlerService.handleError( @@ -573,12 +599,13 @@ class AuthenticationViewModel extends ChangeNotifier { _navigationService.pushAndReplace(AppRoutes.landingScreen); } - Future navigateToOTPScreen({required OTPTypeEnum otpTypeEnum, required String phoneNumber, required bool isComingFromRegister, dynamic payload}) async { + Future navigateToOTPScreen({required OTPTypeEnum otpTypeEnum, required String phoneNumber, required bool isComingFromRegister, dynamic payload, bool isExcludedUser = false}) async { _navigationService.pushToOtpScreen( phoneNumber: phoneNumber, checkActivationCode: (int activationCode) async { await checkActivationCode( activationCode: activationCode.toString(), + isExcludedUser: isExcludedUser, otpTypeEnum: otpTypeEnum, onWrongActivationCode: (String? value) { onWrongActivationCode(message: value); diff --git a/lib/presentation/my_family/my_Family.dart b/lib/presentation/my_family/my_Family.dart index 6e4ef91..888f364 100644 --- a/lib/presentation/my_family/my_Family.dart +++ b/lib/presentation/my_family/my_Family.dart @@ -89,9 +89,11 @@ class _FamilyMedicalScreenState extends State { AuthenticationViewModel authVm = getIt.get(); return showCommonBottomSheetWithoutHeight(context, title: "Add Family Member", + useSafeArea: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ "Please fill the below field to add a new family member to your profile".toText16(color: AppColors.textColor, weight: FontWeight.w500), SizedBox(height: 20.h), @@ -104,10 +106,7 @@ class _FamilyMedicalScreenState extends State { countryList: CountryEnum.values, onCountryChange: authVm.onCountryChange, ).paddingOnly(top: 8.h, bottom: 16.h), - Divider( - height: 1.h, - color: AppColors.spacerLineColor, - ), + Divider(height: 1.h, color: AppColors.spacerLineColor), TextInputWidget( labelText: LocaleKeys.nationalIdNumber.tr(), hintText: "xxxxxxxxx", @@ -123,13 +122,10 @@ class _FamilyMedicalScreenState extends State { padding: EdgeInsets.symmetric(vertical: 8.h), leadingIcon: AppAssets.student_card, ).paddingOnly(top: 8.h, bottom: 8.h), - Divider( - height: 1.h, - color: AppColors.spacerLineColor, - ), + Divider(height: 1.h, color: AppColors.spacerLineColor), TextInputWidget( labelText: LocaleKeys.phoneNumber.tr(), - hintText: "574345434", + hintText: "", controller: authVm.phoneNumberController, isEnable: true, prefix: authVm.selectedCountrySignup.countryCode, @@ -140,35 +136,7 @@ class _FamilyMedicalScreenState extends State { keyboardType: TextInputType.number, padding: EdgeInsets.symmetric(vertical: 8.h), leadingIcon: AppAssets.smart_phone, - ).paddingOnly(top: 8.h, bottom: 4), - - //TextInputWidget( - // labelText: widget.isForEmail ? LocaleKeys.email.tr() : LocaleKeys.phoneNumber.tr(), - // hintText: widget.isForEmail ? "demo@gmail.com" : "5xxxxxxxx", - // controller: widget.textController!, - // focusNode: _textFieldFocusNode, - // autoFocus: widget.autoFocus, - // padding: EdgeInsets.all(8.h), - // keyboardType: widget.isForEmail ? TextInputType.emailAddress : TextInputType.number, - // onChange: (value) { - // if (widget.onChange != null) { - // widget.onChange!(value); - // } - // }, - // onCountryChange: (value) { - // if (widget.onCountryChange != null) { - // widget.onCountryChange!(value); - // } - // }, - // isEnable: true, - // isReadOnly: widget.isFromSavedLogin, - // prefix: widget.isForEmail ? null : widget.countryCode, - // isBorderAllowed: false, - // isAllowLeadingIcon: true, - // fontSize: 13.h, - // isCountryDropDown: widget.isEnableCountryDropdown, - // leadingIcon: widget.isForEmail ? AppAssets.email : AppAssets.smart_phone, - // ) + ).paddingOnly(top: 8.h, bottom: 4.h), ], ), ), @@ -184,7 +152,15 @@ class _FamilyMedicalScreenState extends State { onOkPress: () { Navigator.of(context).pop(); }, - )) {} + )) { + authVm.sendActivationCode( + otpTypeEnum: OTPTypeEnum.sms, + nationalIdOrFileNumber: authVm.nationalIdController.text, + phoneNumber: authVm.phoneNumberController.text, + isForRegister: false, + isExcludedUser: true, + responseID: 123); + } }, icon: AppAssets.add_icon, height: 56.h, diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 29aee3d..474a0ec 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -73,9 +73,8 @@ class DialogServiceImp implements DialogService { Future showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { final context = navigationService.navigatorKey.currentContext; if (context == null) return; - showCommonBottomSheetWithoutHeight(context, title: label ?? "", child: exceptionBottomSheetWidget(context: context, message: message, onOkPressed: onOkPressed, onCancelPressed: onCancelPressed), - callBackFunc: () { - }); + showCommonBottomSheetWithoutHeight(context, + title: label ?? "", child: exceptionBottomSheetWidget(context: context, message: message, onOkPressed: onOkPressed, onCancelPressed: onCancelPressed), callBackFunc: () {}); } @override diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index c50ffad..286386d 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -113,6 +113,8 @@ void showCommonBottomSheetWithoutHeight( bool isFullScreen = true, bool isDismissible = true, Widget? titleWidget, + bool useSafeArea = false, + }) { showModalBottomSheet( sheetAnimationStyle: AnimationStyle( @@ -124,6 +126,7 @@ void showCommonBottomSheetWithoutHeight( showDragHandle: false, isDismissible: isDismissible, backgroundColor: AppColors.bottomSheetBgColor, + useSafeArea: useSafeArea, builder: (BuildContext context) { return SafeArea( top: false, From 1125437a569d99d7d9e3fef26ffce5ea6ff9193d Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Mon, 22 Sep 2025 14:48:39 +0300 Subject: [PATCH 02/43] family screen & widgets --- lib/core/api_consts.dart | 2 +- lib/core/utils/request_utils.dart | 12 +- .../authentication_view_model.dart | 2 + .../medical_file/medical_file_view_model.dart | 16 +- lib/presentation/my_family/my_Family.dart | 17 +- lib/widgets/common_bottom_sheet.dart | 175 +++++++++++++----- 6 files changed, 163 insertions(+), 61 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 9b013a5..3640cdd 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu 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/core/utils/request_utils.dart b/lib/core/utils/request_utils.dart index 87f88f5..4f66819 100644 --- a/lib/core/utils/request_utils.dart +++ b/lib/core/utils/request_utils.dart @@ -258,8 +258,16 @@ class RequestUtils { }; } - static dynamic getAddFamilyRequest({required String nationalIDorFile, required String mobileNo, required String countryCode, required int loginType}) { + static dynamic getAddFamilyRequest({required String nationalIDorFile, required String mobileNo, required String countryCode}) { var request = {}; + int? loginType = 0; // Default to National ID + + if (countryCode == CountryEnum.saudiArabia.countryCode || countryCode == '+966') { + loginType = (nationalIDorFile.length == 10) ? 1 : 2; + } else if (countryCode == CountryEnum.unitedArabEmirates.countryCode || countryCode == '+971') { + loginType = (nationalIDorFile.length == 15) ? 1 : 2; + } + if (loginType == 1) { request["sharedPatientID"] = 0; request["sharedPatientIdentificationID"] = nationalIDorFile; @@ -272,5 +280,7 @@ class RequestUtils { request["zipCode"] = countryCode; request["isRegister"] = false; request["patientStatus"] = 2; + request["isDentalAllowedBackend"] = false; + return request; } } diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 4590a59..f6c2ec5 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/core/common_models/privilege/HMCProjectListM import 'package:hmg_patient_app_new/core/common_models/privilege/PrivilegeModel.dart'; import 'package:hmg_patient_app_new/core/common_models/privilege/ProjectDetailListModel.dart'; import 'package:hmg_patient_app_new/core/common_models/privilege/VidaPlusProjectListModel.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/loading_utils.dart'; import 'package:hmg_patient_app_new/core/utils/request_utils.dart'; @@ -26,6 +27,7 @@ import 'package:hmg_patient_app_new/features/authentication/models/request_model import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_activation_code_resp_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_user_staus_nhic_response_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/saved_login_screen.dart'; diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 2e528de..a724a10 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/request_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; @@ -293,9 +296,14 @@ class MedicalFileViewModel extends ChangeNotifier { ); } - Future addFamilyFile() async { - final resultEither = await medicalFileRepo.addFamilyFile(request: {}); - resultEither.fold((failure) async => await errorHandlerService.handleError(failure: failure), (apiResponse) async {}); - } + Future addFamilyFile({required OTPTypeEnum otpTypeEnum, required bool isExcludedUser}) async { + AuthenticationViewModel authVM = getIt.get(); + final request = + await RequestUtils.getAddFamilyRequest(nationalIDorFile: authVM.nationalIdController.text, mobileNo: authVM.phoneNumberController.text, countryCode: authVM.selectedCountrySignup.countryCode); + final resultEither = await medicalFileRepo.addFamilyFile(request: request); + resultEither.fold((failure) async => await errorHandlerService.handleError(failure: failure), (apiResponse) async { + print(apiResponse); + }); + } } diff --git a/lib/presentation/my_family/my_Family.dart b/lib/presentation/my_family/my_Family.dart index 888f364..4eefe7a 100644 --- a/lib/presentation/my_family/my_Family.dart +++ b/lib/presentation/my_family/my_Family.dart @@ -10,6 +10,7 @@ import 'package:hmg_patient_app_new/core/utils/validation_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/authentication/authentication_view_model.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/my_family/widget/family_cards.dart'; @@ -38,6 +39,13 @@ class FamilyMedicalScreen extends StatefulWidget { class _FamilyMedicalScreenState extends State { List tabs = [CustomTabBarModel("", LocaleKeys.medicalFile.tr()), CustomTabBarModel("", LocaleKeys.request.tr())]; + MedicalFileViewModel? medicalVM; + + @override + void initState() { + super.initState(); + medicalVM = getIt.get(); + } @override Widget build(BuildContext context) { @@ -153,13 +161,8 @@ class _FamilyMedicalScreenState extends State { Navigator.of(context).pop(); }, )) { - authVm.sendActivationCode( - otpTypeEnum: OTPTypeEnum.sms, - nationalIdOrFileNumber: authVm.nationalIdController.text, - phoneNumber: authVm.phoneNumberController.text, - isForRegister: false, - isExcludedUser: true, - responseID: 123); + // authVm.addFamilyMember(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true); + medicalVM?.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true); } }, icon: AppAssets.add_icon, diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index 286386d..c6db474 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -105,57 +105,136 @@ class ButtonSheetContent extends StatelessWidget { } void showCommonBottomSheetWithoutHeight( - BuildContext context, { - required Widget child, - required VoidCallback callBackFunc, - String title = "", - bool isCloseButtonVisible = true, - bool isFullScreen = true, - bool isDismissible = true, - Widget? titleWidget, - bool useSafeArea = false, - -}) { + BuildContext context, { + required Widget child, + required VoidCallback callBackFunc, + String title = "", + bool isCloseButtonVisible = true, + bool isFullScreen = true, + bool isDismissible = true, + Widget? titleWidget, + bool useSafeArea = false, + }) { showModalBottomSheet( - sheetAnimationStyle: AnimationStyle( - duration: Duration(milliseconds: 500), // Custom animation duration - reverseDuration: Duration(milliseconds: 300), // Custom reverse animation duration - ), - context: context, - isScrollControlled: true, - showDragHandle: false, - isDismissible: isDismissible, - backgroundColor: AppColors.bottomSheetBgColor, - useSafeArea: useSafeArea, - builder: (BuildContext context) { - return SafeArea( - top: false, - left: false, - right: false, - child: isCloseButtonVisible - ? Container( - padding: EdgeInsets.only(left: 24, top: 24, right: 24, bottom: 12), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.bottomSheetBgColor, borderRadius: 24.h), - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: 16.h, + sheetAnimationStyle: AnimationStyle( + duration: Duration(milliseconds: 500), + reverseDuration: Duration(milliseconds: 300), + ), + context: context, + isScrollControlled: true, + showDragHandle: false, + isDismissible: isDismissible, + backgroundColor: AppColors.bottomSheetBgColor, + useSafeArea: useSafeArea, + builder: (BuildContext context) { + return SafeArea( + top: false, + left: false, + right: false, + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + physics: ClampingScrollPhysics(), + child: isCloseButtonVisible + ? Container( + padding: EdgeInsets.only( + left: 24, + top: 24, + right: 24, + bottom: 12, + ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.bottomSheetBgColor, + borderRadius: 24.h, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - titleWidget ?? Expanded(child: title.toText20(weight: FontWeight.w600)), - Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, iconColor: Color(0xff2B353E)).onPress(() { - Navigator.of(context).pop(); - }), - ], - ), - child, + titleWidget ?? + Expanded( + child: title.toText20(weight: FontWeight.w600), + ), + Utils.buildSvgWithAssets( + icon: AppAssets.close_bottom_sheet_icon, + iconColor: Color(0xff2B353E), + ).onPress(() { + Navigator.of(context).pop(); + }), ], - )) - : child, - ); - }).then((value) { + ), + SizedBox(height: 16.h), + child, + ], + ), + ) + : child, + ), + ), + ); + }, + ).then((value) { callBackFunc(); }); } + +// void showCommonBottomSheetWithoutHeight( +// BuildContext context, { +// required Widget child, +// required VoidCallback callBackFunc, +// String title = "", +// bool isCloseButtonVisible = true, +// bool isFullScreen = true, +// bool isDismissible = true, +// Widget? titleWidget, +// bool useSafeArea = false, +// +// }) { +// showModalBottomSheet( +// sheetAnimationStyle: AnimationStyle( +// duration: Duration(milliseconds: 500), // Custom animation duration +// reverseDuration: Duration(milliseconds: 300), // Custom reverse animation duration +// ), +// context: context, +// isScrollControlled: true, +// showDragHandle: false, +// isDismissible: isDismissible, +// backgroundColor: AppColors.bottomSheetBgColor, +// useSafeArea: useSafeArea, +// builder: (BuildContext context) { +// return SafeArea( +// top: false, +// left: false, +// right: false, +// child: isCloseButtonVisible +// ? Container( +// padding: EdgeInsets.only(left: 24, top: 24, right: 24, bottom: 12), +// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.bottomSheetBgColor, borderRadius: 24.h), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// spacing: 16.h, +// children: [ +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// crossAxisAlignment: CrossAxisAlignment.center, +// children: [ +// titleWidget ?? Expanded(child: title.toText20(weight: FontWeight.w600)), +// Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, iconColor: Color(0xff2B353E)).onPress(() { +// Navigator.of(context).pop(); +// }), +// ], +// ), +// child, +// ], +// )) +// : child, +// ); +// }).then((value) { +// callBackFunc(); +// }); +// } From 192d617350216d240506b98284e62751ebf473e2 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Wed, 24 Sep 2025 09:33:33 +0300 Subject: [PATCH 03/43] family screen & widgets --- lib/core/api_consts.dart | 2 +- lib/core/utils/request_utils.dart | 28 ++-- .../authentication/authentication_repo.dart | 6 +- .../authentication_view_model.dart | 40 +++--- .../common/models/family_file_request.dart | 57 ++++++++ .../medical_file/medical_file_repo.dart | 23 ++-- .../medical_file/medical_file_view_model.dart | 32 ++++- .../medical_file/medical_file_page.dart | 7 + lib/presentation/my_family/my_Family.dart | 4 +- lib/widgets/chip/app_custom_chip_widget.dart | 129 +++++++++--------- 10 files changed, 211 insertions(+), 117 deletions(-) create mode 100644 lib/features/common/models/family_file_request.dart diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 3640cdd..9b013a5 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu 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/request_utils.dart b/lib/core/utils/request_utils.dart index 4f66819..cd813ed 100644 --- a/lib/core/utils/request_utils.dart +++ b/lib/core/utils/request_utils.dart @@ -9,6 +9,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/models/request_models/registration_payload_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/request_models/send_activation_request_model.dart'; import 'package:hmg_patient_app_new/features/common/models/commong_authanticated_req_model.dart'; +import 'package:hmg_patient_app_new/features/common/models/family_file_request.dart'; class RequestUtils { static dynamic getPatientAuthenticationRequest({ @@ -126,6 +127,7 @@ class RequestUtils { required bool isFileNo, dynamic payload, required bool isExcludedUser, + required bool isFormFamilyFile, int? responseID, }) { AppState _appState = getIt.get(); @@ -159,7 +161,7 @@ class RequestUtils { } request.deviceTypeID = request.searchType; - if (isExcludedUser) { + if (isFormFamilyFile) { //INFO: Only for Excluded User Family Member Addition request.isPatientExcluded = isExcludedUser; request.responseID = responseID; @@ -258,8 +260,8 @@ class RequestUtils { }; } - static dynamic getAddFamilyRequest({required String nationalIDorFile, required String mobileNo, required String countryCode}) { - var request = {}; + static Future getAddFamilyRequest({required String nationalIDorFile, required String mobileNo, required String countryCode}) async { + FamilyFileRequest request = FamilyFileRequest(); int? loginType = 0; // Default to National ID if (countryCode == CountryEnum.saudiArabia.countryCode || countryCode == '+966') { @@ -269,18 +271,18 @@ class RequestUtils { } if (loginType == 1) { - request["sharedPatientID"] = 0; - request["sharedPatientIdentificationID"] = nationalIDorFile; + request.sharedPatientId = 0; + request.sharedPatientIdentificationId = nationalIDorFile; } else if (loginType == 2) { - request["sharedPatientID"] = int.parse(nationalIDorFile); - request["sharedPatientIdentificationID"] = ''; + request.sharedPatientId = int.parse(nationalIDorFile); + request.sharedPatientIdentificationId = ''; } - request["searchType"] = loginType; - request["sharedPatientMobileNumber"] = mobileNo; - request["zipCode"] = countryCode; - request["isRegister"] = false; - request["patientStatus"] = 2; - request["isDentalAllowedBackend"] = false; + request.searchType = loginType; + request.sharedPatientMobileNumber = mobileNo; + request.zipCode = countryCode; + request.isRegister = false; + request.patientStatus = 2; + request.isDentalAllowedBackend = false; return request; } } diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index 89a9404..8038c43 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -17,7 +17,7 @@ abstract class AuthenticationRepo { Future>> checkPatientAuthentication({required dynamic checkPatientAuthenticationReq}); - Future>> sendActivationCodeRepo({required dynamic sendActivationCodeReq, String? languageID, bool isRegister = false, bool isExcludedUser = false}); + Future>> sendActivationCodeRepo({required dynamic sendActivationCodeReq, String? languageID, bool isRegister = false, bool isFormFamilyFile = false}); Future>> checkActivationCodeRepo({required dynamic newRequest, required String? activationCode, required bool isRegister, bool isExcludedUser = false}); @@ -131,7 +131,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { required dynamic sendActivationCodeReq, String? languageID, bool isRegister = false, - bool isExcludedUser = false, + bool isFormFamilyFile = false, }) async { int isOutKsa = (sendActivationCodeReq.zipCode == '966' || sendActivationCodeReq.zipCode == '+966') ? 0 : 1; sendActivationCodeReq.patientOutSA = isOutKsa; @@ -142,7 +142,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { Failure? failure; await apiClient.post( - isExcludedUser + isFormFamilyFile ? ApiConsts.sendFamilyFileActivation : isRegister ? ApiConsts.sendActivationCodeRegister diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 7c1252c..558f02d 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -354,25 +354,26 @@ class AuthenticationViewModel extends ChangeNotifier { dynamic payload, bool isComingFromResendOTP = false, bool isExcludedUser = false, + bool isFormFamilyFile = false, int? responseID}) async { var request = RequestUtils.getCommonRequestSendActivationCode( - otpTypeEnum: otpTypeEnum, - mobileNumber: phoneNumber, - selectedLoginType: otpTypeEnum.toInt(), - zipCode: selectedCountrySignup.countryCode, - nationalId: int.parse(nationalIdOrFileNumber), - isFileNo: isForRegister ? isPatientHasFile(request: payload) : false, - patientId: 0, - isForRegister: isForRegister, - patientOutSA: isForRegister - ? isPatientOutsideSA(request: payload) - : selectedCountrySignup.countryCode == CountryEnum.saudiArabia - ? false - : true, - payload: payload, - isExcludedUser: isExcludedUser, - responseID: responseID, - ); + otpTypeEnum: otpTypeEnum, + mobileNumber: phoneNumber, + selectedLoginType: otpTypeEnum.toInt(), + zipCode: selectedCountrySignup.countryCode, + nationalId: int.parse(nationalIdOrFileNumber), + isFileNo: isForRegister ? isPatientHasFile(request: payload) : false, + patientId: 0, + isForRegister: isForRegister, + patientOutSA: isForRegister + ? isPatientOutsideSA(request: payload) + : selectedCountrySignup.countryCode == CountryEnum.saudiArabia + ? false + : true, + payload: payload, + isExcludedUser: isExcludedUser, + isFormFamilyFile: isFormFamilyFile, + responseID: responseID); // TODO: GET APP SMS SIGNATURE HERE request.sMSSignature = await getSignature(); @@ -382,7 +383,7 @@ class AuthenticationViewModel extends ChangeNotifier { } final resultEither = - await _authenticationRepo.sendActivationCodeRepo(sendActivationCodeReq: request, isRegister: checkIsUserComingForRegister(request: payload), languageID: 'er', isExcludedUser: isExcludedUser); + await _authenticationRepo.sendActivationCodeRepo(sendActivationCodeReq: request, isRegister: checkIsUserComingForRegister(request: payload), languageID: 'er', isFormFamilyFile: isFormFamilyFile); resultEither.fold( (failure) async => await _errorHandlerService.handleError(failure: failure), @@ -397,9 +398,10 @@ class AuthenticationViewModel extends ChangeNotifier { } else { if (apiResponse.data != null && apiResponse.data['isSMSSent'] == true) { LoaderBottomSheet.hideLoader(); - if (!isComingFromResendOTP) + if (!isComingFromResendOTP) { navigateToOTPScreen( otpTypeEnum: otpTypeEnum, phoneNumber: phoneNumber, isComingFromRegister: checkIsUserComingForRegister(request: payload), payload: payload, isExcludedUser: isExcludedUser); + } } else { // TODO: Handle isSMSSent false // navigateToOTPScreen(otpTypeEnum: otpTypeEnum, phoneNumber: phoneNumber); diff --git a/lib/features/common/models/family_file_request.dart b/lib/features/common/models/family_file_request.dart new file mode 100644 index 0000000..047b9af --- /dev/null +++ b/lib/features/common/models/family_file_request.dart @@ -0,0 +1,57 @@ +import 'dart:convert'; + +class FamilyFileRequest { + int? sharedPatientId; + String? sharedPatientIdentificationId; + int? searchType; + String? sharedPatientMobileNumber; + String? zipCode; + bool? isRegister; + int? patientStatus; + bool? isDentalAllowedBackend; + bool? isPatientExcluded; + int? responseID; + + FamilyFileRequest({ + this.sharedPatientId, + this.sharedPatientIdentificationId, + this.searchType, + this.sharedPatientMobileNumber, + this.zipCode, + this.isRegister, + this.patientStatus, + this.isDentalAllowedBackend, + this.isPatientExcluded, + this.responseID, + }); + + factory FamilyFileRequest.fromRawJson(String str) => FamilyFileRequest.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory FamilyFileRequest.fromJson(Map json) => FamilyFileRequest( + sharedPatientId: json["sharedPatientID"], + sharedPatientIdentificationId: json["sharedPatientIdentificationID"], + searchType: json["searchType"], + sharedPatientMobileNumber: json["sharedPatientMobileNumber"], + zipCode: json["zipCode"], + isRegister: json["isRegister"], + patientStatus: json["patientStatus"], + isDentalAllowedBackend: json["isDentalAllowedBackend"], + isPatientExcluded: json["IsPatientExcluded"], + responseID: json["ReponseID"], + ); + + Map toJson() => { + "SharedPatientID": sharedPatientId, + "SharedPatientIdentificationID": sharedPatientIdentificationId, + "SearchType": searchType, + "SharedPatientMobileNumber": sharedPatientMobileNumber, + "zipCode": zipCode, + "isRegister": isRegister, + "PatientStatus": patientStatus, + "isDentalAllowedBackend": isDentalAllowedBackend, + "IsPatientExcluded": isPatientExcluded, + "ReponseID": responseID, + }; +} diff --git a/lib/features/medical_file/medical_file_repo.dart b/lib/features/medical_file/medical_file_repo.dart index 6b83f33..3042aba 100644 --- a/lib/features/medical_file/medical_file_repo.dart +++ b/lib/features/medical_file/medical_file_repo.dart @@ -26,7 +26,7 @@ abstract class MedicalFileRepo { Future>>> getPatientFamilyFiles(); - Future>>> addFamilyFile({required dynamic request}); + Future>> addFamilyFile({required dynamic request}); } class MedicalFileRepoImp implements MedicalFileRepo { @@ -313,9 +313,9 @@ class MedicalFileRepoImp implements MedicalFileRepo { } @override - Future>>> addFamilyFile({dynamic request}) async { + Future>> addFamilyFile({dynamic request}) async { try { - GenericApiModel>? apiResponse; + GenericApiModel? apiResponse; Failure? failure; await apiClient.post( ApiConsts.addFamilyFile, @@ -325,17 +325,12 @@ class MedicalFileRepoImp implements MedicalFileRepo { }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - print(response); - // final list = response['GetAllSharedRecordsByStatusList']; - // - // final familyLists = list.map((item) => FamilyFileResponseModelLists.fromJson(item as Map)).toList().cast(); - // - // apiResponse = GenericApiModel>( - // messageStatus: messageStatus, - // statusCode: statusCode, - // errorMessage: null, - // data: familyLists, - // ); + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response["ShareFamilyFileObj"] ?? null, + ); } catch (e) { failure = DataParsingFailure(e.toString()); } diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index a724a10..1c3d353 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; @@ -6,6 +8,7 @@ import 'package:hmg_patient_app_new/core/utils/request_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; +import 'package:hmg_patient_app_new/features/common/models/family_file_request.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; @@ -13,6 +16,7 @@ import 'package:hmg_patient_app_new/features/medical_file/models/patient_sicklea import 'package:hmg_patient_app_new/features/medical_file/models/patient_vaccine_response_model.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; class MedicalFileViewModel extends ChangeNotifier { int selectedTabIndex = 0; @@ -298,12 +302,34 @@ class MedicalFileViewModel extends ChangeNotifier { Future addFamilyFile({required OTPTypeEnum otpTypeEnum, required bool isExcludedUser}) async { AuthenticationViewModel authVM = getIt.get(); - final request = + NavigationService navigationService = getIt.get(); + FamilyFileRequest request = await RequestUtils.getAddFamilyRequest(nationalIDorFile: authVM.nationalIdController.text, mobileNo: authVM.phoneNumberController.text, countryCode: authVM.selectedCountrySignup.countryCode); - final resultEither = await medicalFileRepo.addFamilyFile(request: request); + final resultEither = await medicalFileRepo.addFamilyFile(request: request.toJson()); resultEither.fold((failure) async => await errorHandlerService.handleError(failure: failure), (apiResponse) async { - print(apiResponse); + if (apiResponse != null && apiResponse.data != null) { + request.isPatientExcluded = apiResponse.data["IsPatientExcluded"]; + request.responseID = apiResponse.data["ReponseID"]; + _dialogService.showExceptionBottomSheet( + message: apiResponse.data['Message'], + onOkPressed: () { + print("=================== On Press Ok =================="); + authVM.sendActivationCode( + otpTypeEnum: otpTypeEnum, + nationalIdOrFileNumber: request.sharedPatientIdentificationId!, + phoneNumber: request.sharedPatientMobileNumber!, + isForRegister: false, + isExcludedUser: apiResponse.data['IsPatientExcluded'], + responseID: apiResponse.data["ReponseID"], + isFormFamilyFile: true); + + // insertFamilyData(payload: apiResponse.data![0]['ShareFamilyFileObj'], isExcludedPatient: apiResponse.data![0]['ShareFamilyFileObj']['IsPatientExcluded']); + }, + onCancelPressed: () { + navigationService.pop(); + }); + } }); } } diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 9776e5b..3786382 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -143,6 +143,13 @@ class _MedicalFilePageState extends State { AppCustomChipWidget( icon: AppAssets.file_icon, labelText: "${LocaleKeys.fileNo.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}", + onChipTap: () { + navigationService.pushPage( + page: FamilyMedicalScreen( + profiles: medicalFileViewModel.patientFamilyFiles, + onSelect: (FamilyFileResponseModelLists p1) {}, + )); + }, ), AppCustomChipWidget( icon: AppAssets.checkmark_icon, diff --git a/lib/presentation/my_family/my_Family.dart b/lib/presentation/my_family/my_Family.dart index 4eefe7a..7ebd069 100644 --- a/lib/presentation/my_family/my_Family.dart +++ b/lib/presentation/my_family/my_Family.dart @@ -28,10 +28,10 @@ class FamilyMedicalScreen extends StatefulWidget { final Function(FamilyFileResponseModelLists) onSelect; const FamilyMedicalScreen({ - Key? key, + super.key, required this.profiles, required this.onSelect, - }) : super(key: key); + }); @override State createState() => _FamilyMedicalScreenState(); diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index b29f6e6..099062e 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -24,6 +24,7 @@ class AppCustomChipWidget extends StatelessWidget { this.deleteIconColor = AppColors.textColor, this.deleteIconHasColor = false, this.padding = EdgeInsets.zero, + this.onChipTap }); final String? labelText; @@ -40,74 +41,78 @@ class AppCustomChipWidget extends StatelessWidget { final bool deleteIconHasColor; final OutlinedBorder? shape; final EdgeInsets? padding; + final void Function()? onChipTap; @override Widget build(BuildContext context) { - return ChipTheme( - data: ChipThemeData( - padding: EdgeInsets.all(0.0), - shape: SmoothRectangleBorder( - side: BorderSide( - width: 0.0, - color: Colors.transparent, // Crucially, set color to transparent - style: BorderStyle.none, + return GestureDetector( + onTap: onChipTap, + child: ChipTheme( + data: ChipThemeData( + padding: EdgeInsets.all(0.0), + shape: SmoothRectangleBorder( + side: BorderSide( + width: 0.0, + color: Colors.transparent, // Crucially, set color to transparent + style: BorderStyle.none, + ), + borderRadius: BorderRadius.circular(10.0), // Apply a border radius of 16.0 ), - borderRadius: BorderRadius.circular(10.0), // Apply a border radius of 16.0 ), + child: icon.isNotEmpty + ? Chip( + avatar: icon.isNotEmpty + ? Utils.buildSvgWithAssets( + icon: icon, + width: iconSize.h, + height: iconSize.h, + iconColor: iconHasColor ? iconColor : null) + : SizedBox.shrink(), + label: richText ?? + labelText!.toText10( + weight: FontWeight.w500, + letterSpacing: -0.64, + color: textColor), + // padding: EdgeInsets.all(0.0), + padding: padding, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + labelPadding: EdgeInsets.only( + left: -4.h, + right: deleteIcon?.isNotEmpty == true ? 2.h : 8.h), + backgroundColor: backgroundColor, + shape: shape, + deleteIcon: deleteIcon?.isNotEmpty == true + ? Utils.buildSvgWithAssets( + icon: deleteIcon!, + width: deleteIconSize!.width!.h, + height: deleteIconSize!.height.h, + iconColor: deleteIconHasColor ? deleteIconColor : null) + : null, + onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, + ) + : Chip( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + label: richText ?? + labelText!.toText10( + weight: FontWeight.w500, + letterSpacing: -0.64, + color: textColor), + padding: EdgeInsets.all(0.0), + backgroundColor: backgroundColor, + shape: shape, + labelPadding: EdgeInsets.only( + left: 8.h, + right: deleteIcon?.isNotEmpty == true ? -2.h : 8.h), + deleteIcon: deleteIcon?.isNotEmpty == true + ? Utils.buildSvgWithAssets( + icon: deleteIcon!, + width: deleteIconSize!.width.h, + height: deleteIconSize!.height.h, + iconColor: deleteIconHasColor ? deleteIconColor : null) + : null, + onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, + ), ), - child: icon.isNotEmpty - ? Chip( - avatar: icon.isNotEmpty - ? Utils.buildSvgWithAssets( - icon: icon, - width: iconSize.h, - height: iconSize.h, - iconColor: iconHasColor ? iconColor : null) - : SizedBox.shrink(), - label: richText ?? - labelText!.toText10( - weight: FontWeight.w500, - letterSpacing: -0.64, - color: textColor), - // padding: EdgeInsets.all(0.0), - padding: padding, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - labelPadding: EdgeInsets.only( - left: -4.h, - right: deleteIcon?.isNotEmpty == true ? 2.h : 8.h), - backgroundColor: backgroundColor, - shape: shape, - deleteIcon: deleteIcon?.isNotEmpty == true - ? Utils.buildSvgWithAssets( - icon: deleteIcon!, - width: deleteIconSize!.width!.h, - height: deleteIconSize!.height.h, - iconColor: deleteIconHasColor ? deleteIconColor : null) - : null, - onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, - ) - : Chip( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - label: richText ?? - labelText!.toText10( - weight: FontWeight.w500, - letterSpacing: -0.64, - color: textColor), - padding: EdgeInsets.all(0.0), - backgroundColor: backgroundColor, - shape: shape, - labelPadding: EdgeInsets.only( - left: 8.h, - right: deleteIcon?.isNotEmpty == true ? -2.h : 8.h), - deleteIcon: deleteIcon?.isNotEmpty == true - ? Utils.buildSvgWithAssets( - icon: deleteIcon!, - width: deleteIconSize!.width.h, - height: deleteIconSize!.height.h, - iconColor: deleteIconHasColor ? deleteIconColor : null) - : null, - onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, - ), ); } } From 47ce5022d3f54eae135a280953d43e466a221ce8 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Wed, 24 Sep 2025 14:43:10 +0300 Subject: [PATCH 04/43] family screen & widgets --- .../authentication/authentication_repo.dart | 24 +++++- .../authentication_view_model.dart | 74 +++++++++++++------ .../widgets/otp_verification_screen.dart | 10 +-- .../medical_file/medical_file_view_model.dart | 5 +- lib/services/navigation_service.dart | 4 +- 5 files changed, 82 insertions(+), 35 deletions(-) diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index 8038c43..a02ef70 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/common_models/privilege/PrivilegeModel.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; @@ -19,7 +20,8 @@ abstract class AuthenticationRepo { Future>> sendActivationCodeRepo({required dynamic sendActivationCodeReq, String? languageID, bool isRegister = false, bool isFormFamilyFile = false}); - Future>> checkActivationCodeRepo({required dynamic newRequest, required String? activationCode, required bool isRegister, bool isExcludedUser = false}); + Future>> checkActivationCodeRepo( + {required dynamic newRequest, required String? activationCode, required bool isRegister, bool isFormFamilyFile = false, int? patientShareRequestID, int? responseID}); Future>> checkIfUserAgreed({required dynamic commonAuthanticatedRequest}); @@ -178,7 +180,9 @@ class AuthenticationRepoImp implements AuthenticationRepo { required dynamic newRequest, // could be CheckActivationCodeReq or CheckActivationCodeRegisterReq required String? activationCode, required bool isRegister, - bool isExcludedUser = false, + bool isFormFamilyFile = false, + int? patientShareRequestID, + int? responseID, }) async { if (isRegister) { newRequest["activationCode"] = activationCode ?? "0000"; @@ -191,8 +195,16 @@ class AuthenticationRepoImp implements AuthenticationRepo { newRequest.forRegisteration = newRequest.isRegister ?? false; newRequest.isRegister = false; } + Map familyRequest = {}; + if (isFormFamilyFile) { + familyRequest = newRequest.toJson(); + familyRequest['PatientShareRequestID'] = patientShareRequestID; + familyRequest['ResponseID'] = responseID; + familyRequest['Status'] = 3; + familyRequest["PatientID"] = newRequest["PatientID"]; + } - final endpoint = isExcludedUser + final endpoint = isFormFamilyFile ? ApiConsts.checkActivationCodeForFamily : isRegister ? ApiConsts.checkActivationCodeRegister @@ -204,7 +216,11 @@ class AuthenticationRepoImp implements AuthenticationRepo { await apiClient.post( endpoint, - body: isRegister ? newRequest : newRequest.toJson(), + body: isFormFamilyFile + ? familyRequest + : isRegister + ? newRequest + : newRequest.toJson(), onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; }, diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 558f02d..a108835 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -382,12 +382,16 @@ class AuthenticationViewModel extends ChangeNotifier { _appState.setUserRegistrationPayload = RegistrationDataModelPayload.fromJson(payload); } - final resultEither = - await _authenticationRepo.sendActivationCodeRepo(sendActivationCodeReq: request, isRegister: checkIsUserComingForRegister(request: payload), languageID: 'er', isFormFamilyFile: isFormFamilyFile); + final resultEither = await _authenticationRepo.sendActivationCodeRepo( + sendActivationCodeReq: request, isRegister: checkIsUserComingForRegister(request: payload), languageID: 'er', isFormFamilyFile: isFormFamilyFile); resultEither.fold( (failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async { + int? patientShareRequestID = 0; + if (isFormFamilyFile) { + patientShareRequestID = apiResponse.data['PatientShareRequestID']; + } if (apiResponse.messageStatus == 2) { LoaderBottomSheet.hideLoader(); await _dialogService.showCommonBottomSheetWithoutH( @@ -400,7 +404,15 @@ class AuthenticationViewModel extends ChangeNotifier { LoaderBottomSheet.hideLoader(); if (!isComingFromResendOTP) { navigateToOTPScreen( - otpTypeEnum: otpTypeEnum, phoneNumber: phoneNumber, isComingFromRegister: checkIsUserComingForRegister(request: payload), payload: payload, isExcludedUser: isExcludedUser); + otpTypeEnum: otpTypeEnum, + phoneNumber: phoneNumber, + isComingFromRegister: checkIsUserComingForRegister(request: payload), + payload: payload, + isFormFamilyFile: isFormFamilyFile, + isExcludedUser: isExcludedUser, + responseID: responseID, + patientShareRequestID: patientShareRequestID, + ); } } else { // TODO: Handle isSMSSent false @@ -424,8 +436,8 @@ class AuthenticationViewModel extends ChangeNotifier { required OTPTypeEnum otpTypeEnum, required Function(String? message) onWrongActivationCode, Function()? onResendActivation, - bool isExcludedUser = false, - dynamic requestID, + bool isFormFamilyFile = false, + dynamic patientShareRequestID, dynamic responseID}) async { bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1); @@ -453,12 +465,6 @@ class AuthenticationViewModel extends ChangeNotifier { loginType: loginTypeEnum.toInt) .toJson(); - if (isExcludedUser) { - request['PatientShareRequestID'] = requestID; - request['ResponseID'] = responseID; - request['Status'] = 3; - } - LoaderBottomSheet.showLoader(); if (isForRegister) { if (_appState.getUserRegistrationPayload.patientOutSa == 0) request['DOB'] = _appState.getUserRegistrationPayload.dob; @@ -468,7 +474,11 @@ class AuthenticationViewModel extends ChangeNotifier { request["ForRegisteration"] = _appState.getUserRegistrationPayload.isRegister; request["isRegister"] = false; - final resultEither = await _authenticationRepo.checkActivationCodeRepo(newRequest: request, activationCode: activationCode.toString(), isRegister: true); + final resultEither = await _authenticationRepo.checkActivationCodeRepo( + newRequest: request, + activationCode: activationCode.toString(), + isRegister: true, + ); LoaderBottomSheet.hideLoader(); @@ -495,7 +505,12 @@ class AuthenticationViewModel extends ChangeNotifier { }); } else { final resultEither = await _authenticationRepo.checkActivationCodeRepo( - newRequest: CheckActivationCodeRegisterReq.fromJson(request), activationCode: activationCode, isRegister: false, isExcludedUser: isExcludedUser); + newRequest: CheckActivationCodeRegisterReq.fromJson(request), + activationCode: activationCode, + isRegister: false, + isFormFamilyFile: isFormFamilyFile, + patientShareRequestID: patientShareRequestID, + responseID: responseID); resultEither.fold( (failure) async => await _errorHandlerService.handleError( @@ -509,6 +524,8 @@ class AuthenticationViewModel extends ChangeNotifier { LoaderBottomSheet.hideLoader(); await _dialogService.showCommonBottomSheetWithoutH(message: failure.message, label: LocaleKeys.notice.tr(), onOkPressed: () {}); }), (apiResponse) async { + print("API Response Data: ${apiResponse.data}"); + final activation = CheckActivationCode.fromJson(apiResponse.data as Map); if (activation.errorCode == '699') { @@ -605,14 +622,25 @@ class AuthenticationViewModel extends ChangeNotifier { _navigationService.pushAndReplace(AppRoutes.landingScreen); } - Future navigateToOTPScreen({required OTPTypeEnum otpTypeEnum, required String phoneNumber, required bool isComingFromRegister, dynamic payload, bool isExcludedUser = false}) async { + Future navigateToOTPScreen( + {required OTPTypeEnum otpTypeEnum, + required String phoneNumber, + required bool isComingFromRegister, + dynamic payload, + bool isFormFamilyFile = false, + bool isExcludedUser = false, + int? responseID, + int? patientShareRequestID}) async { _navigationService.pushToOtpScreen( phoneNumber: phoneNumber, + isFormFamilyFile: isFormFamilyFile, checkActivationCode: (int activationCode) async { await checkActivationCode( activationCode: activationCode.toString(), - isExcludedUser: isExcludedUser, + isFormFamilyFile: isFormFamilyFile, otpTypeEnum: otpTypeEnum, + responseID: responseID, + patientShareRequestID: patientShareRequestID, onWrongActivationCode: (String? value) { onWrongActivationCode(message: value); }, @@ -620,12 +648,16 @@ class AuthenticationViewModel extends ChangeNotifier { }, onResendOTPPressed: (String phoneNumber) async { await sendActivationCode( - otpTypeEnum: otpTypeEnum, - phoneNumber: phoneNumberController.text, - nationalIdOrFileNumber: nationalIdController.text, - isForRegister: isComingFromRegister, - isComingFromResendOTP: true, - payload: payload); + otpTypeEnum: otpTypeEnum, + phoneNumber: phoneNumberController.text, + nationalIdOrFileNumber: nationalIdController.text, + isForRegister: isComingFromRegister, + isComingFromResendOTP: true, + payload: payload, + isFormFamilyFile: isFormFamilyFile, + isExcludedUser: isExcludedUser, + responseID: responseID, + ); }, ); } diff --git a/lib/features/authentication/widgets/otp_verification_screen.dart b/lib/features/authentication/widgets/otp_verification_screen.dart index e7f8d72..10e34cd 100644 --- a/lib/features/authentication/widgets/otp_verification_screen.dart +++ b/lib/features/authentication/widgets/otp_verification_screen.dart @@ -428,13 +428,9 @@ class OTPVerificationScreen extends StatefulWidget { final String phoneNumber; final Function(int code) checkActivationCode; final Function(String phoneNumber) onResendOTPPressed; + final bool isFormFamilyFile; - const OTPVerificationScreen({ - super.key, - required this.phoneNumber, - required this.checkActivationCode, - required this.onResendOTPPressed, - }); + const OTPVerificationScreen({super.key, required this.phoneNumber, required this.checkActivationCode, required this.onResendOTPPressed, required this.isFormFamilyFile}); @override State createState() => _OTPVerificationScreenState(); @@ -559,7 +555,7 @@ class _OTPVerificationScreenState extends State { LocaleKeys.weHaveSendOTP.tr().toText15(color: AppColors.inputLabelTextColor, letterSpacing: -0.4), _getMaskedPhoneNumber().toText15(color: AppColors.inputLabelTextColor, isBold: true), LocaleKeys.via.tr().toText15(color: AppColors.inputLabelTextColor, letterSpacing: -0.4), - authVM.loginTypeEnum.displayName.toText15(color: AppColors.inputLabelTextColor, isBold: true, letterSpacing: -0.4), + (widget.isFormFamilyFile ? LoginTypeEnum.sms.displayName : authVM.loginTypeEnum.displayName).toText15(color: AppColors.inputLabelTextColor, isBold: true, letterSpacing: -0.4), appState.getUserRegistrationPayload.isRegister != null && appState.getUserRegistrationPayload.isRegister == true ? LocaleKeys.forRegistrationVerification.tr().toText15(color: AppColors.inputLabelTextColor, letterSpacing: -0.4) : LocaleKeys.forLoginVerification.tr().toText15(color: AppColors.inputLabelTextColor, letterSpacing: -0.4), diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 1c3d353..b3b1db1 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -17,6 +17,7 @@ import 'package:hmg_patient_app_new/features/medical_file/models/patient_vaccine import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; class MedicalFileViewModel extends ChangeNotifier { int selectedTabIndex = 0; @@ -301,6 +302,7 @@ class MedicalFileViewModel extends ChangeNotifier { } Future addFamilyFile({required OTPTypeEnum otpTypeEnum, required bool isExcludedUser}) async { + LoaderBottomSheet.showLoader(); AuthenticationViewModel authVM = getIt.get(); NavigationService navigationService = getIt.get(); FamilyFileRequest request = @@ -311,9 +313,11 @@ class MedicalFileViewModel extends ChangeNotifier { if (apiResponse != null && apiResponse.data != null) { request.isPatientExcluded = apiResponse.data["IsPatientExcluded"]; request.responseID = apiResponse.data["ReponseID"]; + LoaderBottomSheet.hideLoader(); _dialogService.showExceptionBottomSheet( message: apiResponse.data['Message'], onOkPressed: () { + LoaderBottomSheet.showLoader(); print("=================== On Press Ok =================="); authVM.sendActivationCode( otpTypeEnum: otpTypeEnum, @@ -324,7 +328,6 @@ class MedicalFileViewModel extends ChangeNotifier { responseID: apiResponse.data["ReponseID"], isFormFamilyFile: true); - // insertFamilyData(payload: apiResponse.data![0]['ShareFamilyFileObj'], isExcludedPatient: apiResponse.data![0]['ShareFamilyFileObj']['IsPatientExcluded']); }, onCancelPressed: () { navigationService.pop(); diff --git a/lib/services/navigation_service.dart b/lib/services/navigation_service.dart index 420c4cf..0c2a901 100644 --- a/lib/services/navigation_service.dart +++ b/lib/services/navigation_service.dart @@ -22,9 +22,9 @@ class NavigationService { navigatorKey.currentState?.pushReplacementNamed(routeName); } - Future pushToOtpScreen({required String phoneNumber, required Function(int code) checkActivationCode, required Function(String phoneNumber) onResendOTPPressed}) { + Future pushToOtpScreen({required String phoneNumber, required Function(int code) checkActivationCode, required Function(String phoneNumber) onResendOTPPressed, bool isFormFamilyFile = false}) { return navigatorKey.currentState!.push( - MaterialPageRoute(builder: (_) => OTPVerificationScreen(phoneNumber: phoneNumber, checkActivationCode: checkActivationCode, onResendOTPPressed: onResendOTPPressed)), + MaterialPageRoute(builder: (_) => OTPVerificationScreen(phoneNumber: phoneNumber, checkActivationCode: checkActivationCode, onResendOTPPressed: onResendOTPPressed, isFormFamilyFile : isFormFamilyFile)), ); } From 3acd549de7ba1c42a37332914a5448e6cb4a055d Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Wed, 24 Sep 2025 15:07:36 +0300 Subject: [PATCH 05/43] no message --- .../medical_file/medical_file_view_model.dart | 40 ++++--------------- lib/presentation/my_family/my_Family.dart | 5 ++- 2 files changed, 11 insertions(+), 34 deletions(-) diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 1c3d353..cb21b90 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -45,7 +45,7 @@ class MedicalFileViewModel extends ChangeNotifier { int selectedMedicalReportsTabIndex = 0; static final DialogService _dialogService = getIt.get(); AppState _appState = getIt(); - + AuthenticationViewModel authVM = getIt.get(); MedicalFileViewModel({required this.medicalFileRepo, required this.errorHandlerService}); initMedicalFileProvider() { @@ -267,41 +267,15 @@ class MedicalFileViewModel extends ChangeNotifier { ); } - Future switchFamilyFiles({Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await medicalFileRepo.getPatientFamilyFiles(); + Future switchFamilyFiles( {Function(dynamic)? onSuccess,int? responseID,int? patientID, String? phoneNumber, Function(String)? onError}) async { + authVM.phoneNumberController.text = phoneNumber!; + + await authVM.checkActivationCode(activationCode: '0000', otpTypeEnum: OTPTypeEnum.sms, onWrongActivationCode: (String? str) {}, responseID: responseID, requestID: patientID, isExcludedUser: true); - result.fold( - (failure) async => await errorHandlerService.handleError( - failure: failure, - onOkPressed: () { - onError!(failure.message); - }, - ), - (apiResponse) { - if (apiResponse.messageStatus == 2) { - _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage!, onOkPressed: () {}); - } else if (apiResponse.messageStatus == 1) { - patientFamilyFiles = apiResponse.data!; - patientFamilyFiles.insert( - 0, - FamilyFileResponseModelLists( - patientId: _appState.getAuthenticatedUser()!.patientId, - patientName: '${_appState.getAuthenticatedUser()!.firstName!} ${_appState.getAuthenticatedUser()!.lastName!}', - isActive: true, - gender: _appState.getAuthenticatedUser()!.gender!, - responseId: _appState.getAuthenticatedUser()!.patientId), - ); - notifyListeners(); - if (onSuccess != null) { - onSuccess(apiResponse); - } - } - }, - ); - } + } Future addFamilyFile({required OTPTypeEnum otpTypeEnum, required bool isExcludedUser}) async { - AuthenticationViewModel authVM = getIt.get(); + NavigationService navigationService = getIt.get(); FamilyFileRequest request = await RequestUtils.getAddFamilyRequest(nationalIDorFile: authVM.nationalIdController.text, mobileNo: authVM.phoneNumberController.text, countryCode: authVM.selectedCountrySignup.countryCode); diff --git a/lib/presentation/my_family/my_Family.dart b/lib/presentation/my_family/my_Family.dart index 7ebd069..25fe55c 100644 --- a/lib/presentation/my_family/my_Family.dart +++ b/lib/presentation/my_family/my_Family.dart @@ -70,7 +70,10 @@ class _FamilyMedicalScreenState extends State { onTabChange: (int index) {}, ), SizedBox(height: 25.h), - FamilyCards(profiles: widget.profiles, onSelect: widget.onSelect, isShowDetails: true), + FamilyCards(profiles: widget.profiles, onSelect: (FamilyFileResponseModelLists profile){ + medicalVM?.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); + + }, isShowDetails: true), SizedBox(height: 20.h), ], ), From 9e8011c34c58100f82657328d9502f8621a3c210 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Wed, 24 Sep 2025 15:26:15 +0300 Subject: [PATCH 06/43] family screen & widgets --- lib/extensions/string_extensions.dart | 5 ++- .../authentication/authentication_repo.dart | 23 +++++----- .../authentication_view_model.dart | 43 +++++++++++++------ lib/routes/app_routes.dart | 3 ++ 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 5465ce3..4432737 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -1,4 +1,6 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:auto_size_text/auto_size_text.dart'; @@ -365,7 +367,8 @@ extension DynamicTextStyleExtension on BuildContext { TextBaseline? textBaseline, FontStyle? fontStyle, bool isLanguageSwitcher = false}) { - final family = FontUtils.getFontFamilyForLanguage(true); + AppState appState = getIt.get(); + final family = appState.getLanguageCode() == "ar" ? 'GESSTwo' : 'Poppins'; return TextStyle( fontFamily: family, fontSize: fontSize, diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index a02ef70..c2a49bf 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/common_models/privilege/PrivilegeModel.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/features/authentication/models/request_models/check_activation_code_register_request_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart'; @@ -21,7 +22,7 @@ abstract class AuthenticationRepo { Future>> sendActivationCodeRepo({required dynamic sendActivationCodeReq, String? languageID, bool isRegister = false, bool isFormFamilyFile = false}); Future>> checkActivationCodeRepo( - {required dynamic newRequest, required String? activationCode, required bool isRegister, bool isFormFamilyFile = false, int? patientShareRequestID, int? responseID}); + {required dynamic newRequest, required String? activationCode, required bool isRegister, bool isFormFamilyFile = false, int? patientShareRequestID, int? responseID, String? familyFileTokenID}); Future>> checkIfUserAgreed({required dynamic commonAuthanticatedRequest}); @@ -176,14 +177,14 @@ class AuthenticationRepoImp implements AuthenticationRepo { } @override - Future>> checkActivationCodeRepo({ - required dynamic newRequest, // could be CheckActivationCodeReq or CheckActivationCodeRegisterReq - required String? activationCode, - required bool isRegister, - bool isFormFamilyFile = false, - int? patientShareRequestID, - int? responseID, - }) async { + Future>> checkActivationCodeRepo( + {required dynamic newRequest, // could be CheckActivationCodeReq or CheckActivationCodeRegisterReq + required String? activationCode, + required bool isRegister, + bool isFormFamilyFile = false, + int? patientShareRequestID, + int? responseID, + String? familyFileTokenID}) async { if (isRegister) { newRequest["activationCode"] = activationCode ?? "0000"; newRequest["isSilentLogin"] = activationCode != null ? false : true; @@ -197,11 +198,13 @@ class AuthenticationRepoImp implements AuthenticationRepo { } Map familyRequest = {}; if (isFormFamilyFile) { + AppState appState = getIt.get(); familyRequest = newRequest.toJson(); familyRequest['PatientShareRequestID'] = patientShareRequestID; familyRequest['ResponseID'] = responseID; familyRequest['Status'] = 3; - familyRequest["PatientID"] = newRequest["PatientID"]; + familyRequest["PatientID"] = appState.getAuthenticatedUser()!.patientId ?? 0; + familyRequest["LogInTokenID"] = familyFileTokenID; } final endpoint = isFormFamilyFile diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index a108835..8db44ab 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -29,6 +29,7 @@ import 'package:hmg_patient_app_new/features/authentication/models/resp_models/c import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_user_staus_nhic_response_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.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/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/saved_login_screen.dart'; @@ -389,8 +390,10 @@ class AuthenticationViewModel extends ChangeNotifier { (failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async { int? patientShareRequestID = 0; + String? familyFileTokenID; if (isFormFamilyFile) { patientShareRequestID = apiResponse.data['PatientShareRequestID']; + familyFileTokenID = apiResponse.data['LogInTokenID']; } if (apiResponse.messageStatus == 2) { LoaderBottomSheet.hideLoader(); @@ -404,15 +407,15 @@ class AuthenticationViewModel extends ChangeNotifier { LoaderBottomSheet.hideLoader(); if (!isComingFromResendOTP) { navigateToOTPScreen( - otpTypeEnum: otpTypeEnum, - phoneNumber: phoneNumber, - isComingFromRegister: checkIsUserComingForRegister(request: payload), - payload: payload, - isFormFamilyFile: isFormFamilyFile, - isExcludedUser: isExcludedUser, - responseID: responseID, - patientShareRequestID: patientShareRequestID, - ); + otpTypeEnum: otpTypeEnum, + phoneNumber: phoneNumber, + isComingFromRegister: checkIsUserComingForRegister(request: payload), + payload: payload, + isFormFamilyFile: isFormFamilyFile, + isExcludedUser: isExcludedUser, + responseID: responseID, + patientShareRequestID: patientShareRequestID, + familyFileTokenID: familyFileTokenID); } } else { // TODO: Handle isSMSSent false @@ -438,7 +441,8 @@ class AuthenticationViewModel extends ChangeNotifier { Function()? onResendActivation, bool isFormFamilyFile = false, dynamic patientShareRequestID, - dynamic responseID}) async { + dynamic responseID, + String? familyFileTokenID}) async { bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1); final request = RequestUtils.getCommonRequestWelcome( @@ -510,7 +514,8 @@ class AuthenticationViewModel extends ChangeNotifier { isRegister: false, isFormFamilyFile: isFormFamilyFile, patientShareRequestID: patientShareRequestID, - responseID: responseID); + responseID: responseID, + familyFileTokenID: familyFileTokenID); resultEither.fold( (failure) async => await _errorHandlerService.handleError( @@ -543,6 +548,18 @@ class AuthenticationViewModel extends ChangeNotifier { // Navigator.popUntil(context, (route) => Utils.route(route, equalsTo: RegisterNew)); return; } else { + if (isFormFamilyFile) { + _dialogService.showCommonBottomSheetWithoutH( + message: "Family File Added Successfully", + onOkPressed: () { + LoaderBottomSheet.showLoader(); + MedicalFileViewModel medicalFileVM = GetIt.instance(); + medicalFileVM.getFamilyFiles(); + LoaderBottomSheet.hideLoader(); + _navigationService.popUntilNamed(AppRoutes.medicalFilePage); + }); + } + if (activation.list != null && activation.list!.isNotEmpty) { _appState.setAuthenticatedUser(activation.list!.first); _appState.setPrivilegeModelList(activation.list!.first.listPrivilege!); @@ -630,7 +647,8 @@ class AuthenticationViewModel extends ChangeNotifier { bool isFormFamilyFile = false, bool isExcludedUser = false, int? responseID, - int? patientShareRequestID}) async { + int? patientShareRequestID, + String? familyFileTokenID}) async { _navigationService.pushToOtpScreen( phoneNumber: phoneNumber, isFormFamilyFile: isFormFamilyFile, @@ -641,6 +659,7 @@ class AuthenticationViewModel extends ChangeNotifier { otpTypeEnum: otpTypeEnum, responseID: responseID, patientShareRequestID: patientShareRequestID, + familyFileTokenID: familyFileTokenID, onWrongActivationCode: (String? value) { onWrongActivationCode(message: value); }, diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 67bf208..5a93216 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -4,6 +4,7 @@ import 'package:hmg_patient_app_new/presentation/authentication/register.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart'; import 'package:hmg_patient_app_new/presentation/home/landing_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; class AppRoutes { @@ -12,6 +13,7 @@ class AppRoutes { static const String register = '/register'; static const String registerStepTwo = '/registerStepTwo'; static const String landingScreen = '/landingScreen'; + static const String medicalFilePage = '/medicalFilePage'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -19,5 +21,6 @@ class AppRoutes { landingScreen: (context) => LandingNavigation(), register: (context) => RegisterNew(), registerStepTwo: (context) => RegisterNewStep2(), + medicalFilePage: (context) => MedicalFilePage(), }; } From e2188b5a6931b38961d1aa1aeff89ae76c588e1f Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Thu, 25 Sep 2025 15:24:15 +0300 Subject: [PATCH 07/43] switch family member with super user --- .../authentication/authentication_repo.dart | 16 +++++++++++++--- .../authentication_view_model.dart | 8 +++++--- .../medical_file/medical_file_view_model.dart | 4 +++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index 7fe9297..2ed43d8 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -28,7 +28,9 @@ abstract class AuthenticationRepo { int? patientShareRequestID, int? responseID, bool isSwitchUser = false, - int? patientID}); + int? patientID, + int? loginType + }); Future>> checkIfUserAgreed({required dynamic commonAuthanticatedRequest}); @@ -192,6 +194,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { int? responseID, bool isSwitchUser = false, int? patientID, + int? loginType }) async { if (isRegister) { newRequest["activationCode"] = activationCode ?? "0000"; @@ -215,12 +218,19 @@ class AuthenticationRepoImp implements AuthenticationRepo { Map switchRequest = {}; if (isSwitchUser) { switchRequest = newRequest.toJson(); - switchRequest['SuperUser'] = patientID; + switchRequest['PatientID'] = responseID; switchRequest['IsSilentLogin'] = true; switchRequest['LogInTokenID'] = null; - switchRequest['DeviceToken'] = null; switchRequest['SearchType'] = 2; + if(loginType != 0) { + switchRequest['SuperUser'] = patientID; + switchRequest['DeviceToken'] = null; + }else{ + switchRequest['LoginType'] = 2; + } + + } final endpoint = isFormFamilyFile diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index d42c8cf..8cb5eed 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -247,7 +247,7 @@ class AuthenticationViewModel extends ChangeNotifier { await selectDeviceImei(onSuccess: (dynamic respData) async { try { if (respData != null) { - dynamic data = SelectDeviceByImeiRespModelElement.fromJson(respData.toJson()); + dynamic data = await SelectDeviceByImeiRespModelElement.fromJson(respData.toJson()); _appState.setSelectDeviceByImeiRespModelElement(data); LoaderBottomSheet.hideLoader(); @@ -440,7 +440,8 @@ class AuthenticationViewModel extends ChangeNotifier { dynamic patientShareRequestID, dynamic responseID, bool isSwitchUser =false, - int? patientID + int? patientID, + }) async { bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1); @@ -515,7 +516,8 @@ class AuthenticationViewModel extends ChangeNotifier { patientShareRequestID: patientShareRequestID, responseID: responseID, isSwitchUser: isSwitchUser, - patientID: patientID + patientID: patientID, + loginType: _appState.superUserID != null ? 0 : 2, ); resultEither.fold( diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 51836a6..aa77700 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -275,7 +275,9 @@ class MedicalFileViewModel extends ChangeNotifier { ? phoneNumber.replaceFirst("0", "") : phoneNumber; - await authVM.checkActivationCode(activationCode: '0000', otpTypeEnum: OTPTypeEnum.sms, onWrongActivationCode: (String? str) {}, responseID: responseID, isFormFamilyFile:false, isSwitchUser: true, patientID: patientID); + + + await authVM.checkActivationCode(activationCode: '0000', otpTypeEnum: OTPTypeEnum.sms, onWrongActivationCode: (String? str) {}, responseID: responseID, isFormFamilyFile:false, isSwitchUser: true, patientID: patientID, ); } From 02433e1bef3e67bd92c0978d0358034302370f88 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Sun, 28 Sep 2025 09:31:49 +0300 Subject: [PATCH 08/43] family screen & widgets --- lib/core/api_consts.dart | 1 + lib/core/app_state.dart | 10 ++ .../authentication/authentication_repo.dart | 36 ++++- .../authentication_view_model.dart | 135 +++++++++--------- .../medical_file/medical_file_repo.dart | 42 ++++++ .../medical_file/medical_file_view_model.dart | 25 +++- lib/presentation/my_family/my_Family.dart | 2 +- 7 files changed, 173 insertions(+), 78 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 9b013a5..e11de15 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -810,6 +810,7 @@ class ApiConsts { static final String addFamilyFile = 'Services/Patients.svc/REST/ShareFamilyFileService'; static final String sendFamilyFileActivation = 'Services/Authentication.svc/REST/SendActivationCodeForFamilyFile'; static final String checkActivationCodeForFamily = 'Services/Authentication.svc/REST/CheckActivationCodeForFamilyFile'; + static final String getAllPendingRecordsByResponseId = 'Services/Authentication.svc/REST/GetAllPendingRecordsByResponseId'; // static values for Api diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index c906374..3e0bbc1 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -97,6 +97,16 @@ class AppState { set setDeviceTypeID(v) => deviceTypeID = v; + + + String _familyFileTokenID = ""; + + String get getFamilyFileTokenID => _familyFileTokenID; + + set setFamilyFileTokenID(String value) { + _familyFileTokenID = value; + } + List vidaPlusProjectList = []; List privilegeModelList = []; List hMCProjectListModel = []; diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index c2a49bf..3ed05c6 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -22,7 +22,7 @@ abstract class AuthenticationRepo { Future>> sendActivationCodeRepo({required dynamic sendActivationCodeReq, String? languageID, bool isRegister = false, bool isFormFamilyFile = false}); Future>> checkActivationCodeRepo( - {required dynamic newRequest, required String? activationCode, required bool isRegister, bool isFormFamilyFile = false, int? patientShareRequestID, int? responseID, String? familyFileTokenID}); + {required dynamic newRequest, required String? activationCode, required bool isRegister, bool isFormFamilyFile = false, int? patientShareRequestID, int? responseID}); Future>> checkIfUserAgreed({required dynamic commonAuthanticatedRequest}); @@ -139,6 +139,16 @@ class AuthenticationRepoImp implements AuthenticationRepo { int isOutKsa = (sendActivationCodeReq.zipCode == '966' || sendActivationCodeReq.zipCode == '+966') ? 0 : 1; sendActivationCodeReq.patientOutSA = isOutKsa; sendActivationCodeReq.isDentalAllowedBackend = false; + final payload = sendActivationCodeReq.toJson(); + if (isFormFamilyFile) { + payload.remove("MobileNo"); + payload.remove("NationalID"); + payload.remove("SMSSignature"); + payload.remove("ResponseID"); + print("=================== Final Payload ==================="); + print(payload); + print("====================================================="); + } try { GenericApiModel? apiResponse; @@ -150,7 +160,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { : isRegister ? ApiConsts.sendActivationCodeRegister : ApiConsts.sendActivationCode, - body: sendActivationCodeReq.toJson(), + body: isFormFamilyFile ? payload : sendActivationCodeReq.toJson(), onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; }, @@ -183,8 +193,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { required bool isRegister, bool isFormFamilyFile = false, int? patientShareRequestID, - int? responseID, - String? familyFileTokenID}) async { + int? responseID}) async { if (isRegister) { newRequest["activationCode"] = activationCode ?? "0000"; newRequest["isSilentLogin"] = activationCode != null ? false : true; @@ -199,12 +208,27 @@ class AuthenticationRepoImp implements AuthenticationRepo { Map familyRequest = {}; if (isFormFamilyFile) { AppState appState = getIt.get(); - familyRequest = newRequest.toJson(); + familyRequest = {}; familyRequest['PatientShareRequestID'] = patientShareRequestID; familyRequest['ResponseID'] = responseID; familyRequest['Status'] = 3; familyRequest["PatientID"] = appState.getAuthenticatedUser()!.patientId ?? 0; - familyRequest["LogInTokenID"] = familyFileTokenID; + familyRequest["LogInTokenID"] = appState.getFamilyFileTokenID; + + // // Remove unnecessary keys from familyRequest + // familyRequest.remove("MobileNo"); + // familyRequest.remove("DeviceToken"); + // familyRequest.remove("ProjectOutSA"); + // familyRequest.remove("LoginType"); + // familyRequest.remove("ZipCode"); + // familyRequest.remove("isRegister"); + // familyRequest.remove("SearchType"); + // familyRequest.remove("NationalID"); + // familyRequest.remove("IsSilentLogin"); + // familyRequest.remove("isDentalAllowedBackend"); + // familyRequest.remove("ForRegisteration"); + + } final endpoint = isFormFamilyFile diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 8db44ab..f314cc4 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -376,6 +376,9 @@ class AuthenticationViewModel extends ChangeNotifier { isFormFamilyFile: isFormFamilyFile, responseID: responseID); + + + // TODO: GET APP SMS SIGNATURE HERE request.sMSSignature = await getSignature(); @@ -390,10 +393,9 @@ class AuthenticationViewModel extends ChangeNotifier { (failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async { int? patientShareRequestID = 0; - String? familyFileTokenID; if (isFormFamilyFile) { patientShareRequestID = apiResponse.data['PatientShareRequestID']; - familyFileTokenID = apiResponse.data['LogInTokenID']; + _appState.setFamilyFileTokenID = apiResponse.data['LogInTokenID']; } if (apiResponse.messageStatus == 2) { LoaderBottomSheet.hideLoader(); @@ -407,15 +409,15 @@ class AuthenticationViewModel extends ChangeNotifier { LoaderBottomSheet.hideLoader(); if (!isComingFromResendOTP) { navigateToOTPScreen( - otpTypeEnum: otpTypeEnum, - phoneNumber: phoneNumber, - isComingFromRegister: checkIsUserComingForRegister(request: payload), - payload: payload, - isFormFamilyFile: isFormFamilyFile, - isExcludedUser: isExcludedUser, - responseID: responseID, - patientShareRequestID: patientShareRequestID, - familyFileTokenID: familyFileTokenID); + otpTypeEnum: otpTypeEnum, + phoneNumber: phoneNumber, + isComingFromRegister: checkIsUserComingForRegister(request: payload), + payload: payload, + isFormFamilyFile: isFormFamilyFile, + isExcludedUser: isExcludedUser, + responseID: responseID, + patientShareRequestID: patientShareRequestID, + ); } } else { // TODO: Handle isSMSSent false @@ -441,8 +443,7 @@ class AuthenticationViewModel extends ChangeNotifier { Function()? onResendActivation, bool isFormFamilyFile = false, dynamic patientShareRequestID, - dynamic responseID, - String? familyFileTokenID}) async { + dynamic responseID}) async { bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1); final request = RequestUtils.getCommonRequestWelcome( @@ -478,11 +479,7 @@ class AuthenticationViewModel extends ChangeNotifier { request["ForRegisteration"] = _appState.getUserRegistrationPayload.isRegister; request["isRegister"] = false; - final resultEither = await _authenticationRepo.checkActivationCodeRepo( - newRequest: request, - activationCode: activationCode.toString(), - isRegister: true, - ); + final resultEither = await _authenticationRepo.checkActivationCodeRepo(newRequest: request, activationCode: activationCode.toString(), isRegister: true); LoaderBottomSheet.hideLoader(); @@ -514,8 +511,7 @@ class AuthenticationViewModel extends ChangeNotifier { isRegister: false, isFormFamilyFile: isFormFamilyFile, patientShareRequestID: patientShareRequestID, - responseID: responseID, - familyFileTokenID: familyFileTokenID); + responseID: responseID); resultEither.fold( (failure) async => await _errorHandlerService.handleError( @@ -549,59 +545,62 @@ class AuthenticationViewModel extends ChangeNotifier { return; } else { if (isFormFamilyFile) { - _dialogService.showCommonBottomSheetWithoutH( - message: "Family File Added Successfully", - onOkPressed: () { - LoaderBottomSheet.showLoader(); - MedicalFileViewModel medicalFileVM = GetIt.instance(); - medicalFileVM.getFamilyFiles(); - LoaderBottomSheet.hideLoader(); - _navigationService.popUntilNamed(AppRoutes.medicalFilePage); - }); - } - - if (activation.list != null && activation.list!.isNotEmpty) { - _appState.setAuthenticatedUser(activation.list!.first); - _appState.setPrivilegeModelList(activation.list!.first.listPrivilege!); - } - _appState.setUserBloodGroup = (activation.patientBlodType ?? ""); - _appState.setAppAuthToken = activation.authenticationTokenId; - final request = RequestUtils.getAuthanticatedCommonRequest().toJson(); - bool isUserAgreedBefore = await checkIfUserAgreedBefore(request: request); - - //updating the last login type in app state to show the fingerprint/face id option on home screen - if (_appState.getSelectDeviceByImeiRespModelElement != null) { - _appState.getSelectDeviceByImeiRespModelElement!.logInType = loginTypeEnum.toInt; - } - LoaderBottomSheet.hideLoader(); - insertPatientIMEIData(loginTypeEnum.toInt); - await clearDefaultInputValues(); - if (isUserAgreedBefore) { - navigateToHomeScreen(); + await navigateToFamilyFilePage(); + // _dialogService.showCommonBottomSheetWithoutH( + // message: "Family File Added Successfully", + // onOkPressed: () async { + // print("navigating to family file page"); + // + // }); } else { - navigateToHomeScreen(); - //Agreement page not designed yet so we are navigating to home screen directly - // getUserAgreementContent(request: request); + if (activation.list != null && activation.list!.isNotEmpty) { + _appState.setAuthenticatedUser(activation.list!.first); + _appState.setPrivilegeModelList(activation.list!.first.listPrivilege!); + } + _appState.setUserBloodGroup = (activation.patientBlodType ?? ""); + _appState.setAppAuthToken = activation.authenticationTokenId; + final request = RequestUtils.getAuthanticatedCommonRequest().toJson(); + bool isUserAgreedBefore = await checkIfUserAgreedBefore(request: request); + + //updating the last login type in app state to show the fingerprint/face id option on home screen + if (_appState.getSelectDeviceByImeiRespModelElement != null) { + _appState.getSelectDeviceByImeiRespModelElement!.logInType = loginTypeEnum.toInt; + } + LoaderBottomSheet.hideLoader(); + insertPatientIMEIData(loginTypeEnum.toInt); + await clearDefaultInputValues(); + if (isUserAgreedBefore) { + navigateToHomeScreen(); + } else { + navigateToHomeScreen(); + //Agreement page not designed yet so we are navigating to home screen directly + // getUserAgreementContent(request: request); + } + // TODO: setPreferences and stuff + // sharedPref.remove(FAMILY_FILE); + // activation.list!.isFamily = false; + // userData = activation.list; + // sharedPref.setString(BLOOD_TYPE, activation.patientBloodType ?? ""); + // authenticatedUserObject.user = activation.list!; + // projectViewModel.setPrivilege(privilegeList: res); + // await sharedPref.setObject(MAIN_USER, activation.list); + // await sharedPref.setObject(USER_PROFILE, activation.list); + // loginTokenID = activation.logInTokenID; + // await sharedPref.setObject(LOGIN_TOKEN_ID, activation.logInTokenID); + // await sharedPref.setString(TOKEN, activation.authenticationTokenID!); + + // projectViewModel.analytics.loginRegistration.login_successful(); } - // TODO: setPreferences and stuff - // sharedPref.remove(FAMILY_FILE); - // activation.list!.isFamily = false; - // userData = activation.list; - // sharedPref.setString(BLOOD_TYPE, activation.patientBloodType ?? ""); - // authenticatedUserObject.user = activation.list!; - // projectViewModel.setPrivilege(privilegeList: res); - // await sharedPref.setObject(MAIN_USER, activation.list); - // await sharedPref.setObject(USER_PROFILE, activation.list); - // loginTokenID = activation.logInTokenID; - // await sharedPref.setObject(LOGIN_TOKEN_ID, activation.logInTokenID); - // await sharedPref.setString(TOKEN, activation.authenticationTokenID!); - - // projectViewModel.analytics.loginRegistration.login_successful(); } }); } } + Future navigateToFamilyFilePage() async { + MedicalFileViewModel medicalFileVM = GetIt.instance(); + medicalFileVM.handleFamilyFileRequestOTPVerification(); + } + Future checkIfUserAgreedBefore({required dynamic request}) async { bool isAgreed = false; if (havePrivilege(109)) { @@ -647,8 +646,7 @@ class AuthenticationViewModel extends ChangeNotifier { bool isFormFamilyFile = false, bool isExcludedUser = false, int? responseID, - int? patientShareRequestID, - String? familyFileTokenID}) async { + int? patientShareRequestID}) async { _navigationService.pushToOtpScreen( phoneNumber: phoneNumber, isFormFamilyFile: isFormFamilyFile, @@ -659,7 +657,6 @@ class AuthenticationViewModel extends ChangeNotifier { otpTypeEnum: otpTypeEnum, responseID: responseID, patientShareRequestID: patientShareRequestID, - familyFileTokenID: familyFileTokenID, onWrongActivationCode: (String? value) { onWrongActivationCode(message: value); }, diff --git a/lib/features/medical_file/medical_file_repo.dart b/lib/features/medical_file/medical_file_repo.dart index 3042aba..0997cbc 100644 --- a/lib/features/medical_file/medical_file_repo.dart +++ b/lib/features/medical_file/medical_file_repo.dart @@ -26,6 +26,8 @@ abstract class MedicalFileRepo { Future>>> getPatientFamilyFiles(); + Future>>> getAllPendingRecordsByResponseId({required Map request}); + Future>> addFamilyFile({required dynamic request}); } @@ -312,6 +314,45 @@ class MedicalFileRepoImp implements MedicalFileRepo { } } + @override + Future>>> getAllPendingRecordsByResponseId({required Map request}) async { + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.getAllPendingRecordsByResponseId, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['GetAllSharedRecordsByStatusList']; + // if (list == null || list.isEmpty) { + // throw Exception("lab list is empty"); + // } + + final familyLists = list.map((item) => FamilyFileResponseModelLists.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: familyLists, + ); + } 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())); + } + } + @override Future>> addFamilyFile({dynamic request}) async { try { @@ -325,6 +366,7 @@ class MedicalFileRepoImp implements MedicalFileRepo { }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { + apiResponse = GenericApiModel( messageStatus: messageStatus, statusCode: statusCode, diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index b3b1db1..59363ba 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -268,6 +268,22 @@ class MedicalFileViewModel extends ChangeNotifier { ); } + Future getAllPendingRecordsByResponseId() async { + AppState appState = getIt(); + final result = await medicalFileRepo.getAllPendingRecordsByResponseId(request: {'ResponseID': appState.getAuthenticatedUser()!.patientId ?? "0", "Status": 2}); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + print("======= Pending Records Response: ${jsonEncode(apiResponse.data)}"); + } + }, + ); + } + Future switchFamilyFiles({Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await medicalFileRepo.getPatientFamilyFiles(); @@ -318,7 +334,6 @@ class MedicalFileViewModel extends ChangeNotifier { message: apiResponse.data['Message'], onOkPressed: () { LoaderBottomSheet.showLoader(); - print("=================== On Press Ok =================="); authVM.sendActivationCode( otpTypeEnum: otpTypeEnum, nationalIdOrFileNumber: request.sharedPatientIdentificationId!, @@ -327,7 +342,6 @@ class MedicalFileViewModel extends ChangeNotifier { isExcludedUser: apiResponse.data['IsPatientExcluded'], responseID: apiResponse.data["ReponseID"], isFormFamilyFile: true); - }, onCancelPressed: () { navigationService.pop(); @@ -335,4 +349,11 @@ class MedicalFileViewModel extends ChangeNotifier { } }); } + + Future handleFamilyFileRequestOTPVerification() async { + LoaderBottomSheet.showLoader(); + await getFamilyFiles(); + await getAllPendingRecordsByResponseId(); + LoaderBottomSheet.hideLoader(); + } } diff --git a/lib/presentation/my_family/my_Family.dart b/lib/presentation/my_family/my_Family.dart index 7ebd069..a4be2bf 100644 --- a/lib/presentation/my_family/my_Family.dart +++ b/lib/presentation/my_family/my_Family.dart @@ -38,7 +38,7 @@ class FamilyMedicalScreen extends StatefulWidget { } class _FamilyMedicalScreenState extends State { - List tabs = [CustomTabBarModel("", LocaleKeys.medicalFile.tr()), CustomTabBarModel("", LocaleKeys.request.tr())]; + List tabs = [CustomTabBarModel(null, LocaleKeys.medicalFile.tr()), CustomTabBarModel(null, LocaleKeys.request.tr())]; MedicalFileViewModel? medicalVM; @override From 288a0018ce424ec726c9fa73070287c1c80a2317 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Mon, 29 Sep 2025 09:46:45 +0300 Subject: [PATCH 09/43] family screen & widgets --- assets/images/png/dummy_user.png | Bin 0 -> 73018 bytes lib/core/api_consts.dart | 1 + lib/core/app_assets.dart | 1 + lib/core/enums.dart | 53 ++++ .../authentication/authentication_repo.dart | 42 ++-- .../medical_file/medical_file_repo.dart | 11 +- .../medical_file/medical_file_view_model.dart | 226 +++++++++++++----- .../medical_file/medical_file_page.dart | 5 +- lib/presentation/my_family/my_Family.dart | 34 ++- .../my_family/widget/family_cards.dart | 28 ++- 10 files changed, 298 insertions(+), 103 deletions(-) create mode 100644 assets/images/png/dummy_user.png diff --git a/assets/images/png/dummy_user.png b/assets/images/png/dummy_user.png new file mode 100644 index 0000000000000000000000000000000000000000..b4bace408fe0553e11eccc275651b680afa66675 GIT binary patch literal 73018 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 literal 0 HcmV?d00001 diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index e11de15..206809d 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -811,6 +811,7 @@ class ApiConsts { static final String sendFamilyFileActivation = 'Services/Authentication.svc/REST/SendActivationCodeForFamilyFile'; static final String checkActivationCodeForFamily = 'Services/Authentication.svc/REST/CheckActivationCodeForFamilyFile'; static final String getAllPendingRecordsByResponseId = 'Services/Authentication.svc/REST/GetAllPendingRecordsByResponseId'; + static final String getAllSharedRecordsByStatus = 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatus'; // static values for Api diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 83fb842..be651b7 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -158,6 +158,7 @@ class AppAssets { static const String tamara_en = '$pngBasePath/tamara_en.png'; static const String visa = '$pngBasePath/visa.png'; static const String lockIcon = '$pngBasePath/lock-icon.png'; + static const String dummy_user = '$pngBasePath/dummy_user.png'; } class AppAnimations { diff --git a/lib/core/enums.dart b/lib/core/enums.dart index bb6c6fe..e94b3e2 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -30,6 +30,8 @@ enum LoginTypeEnum { sms, whatsapp, face, fingerprint } enum AppEnvironmentTypeEnum { dev, uat, preProd, qa, staging, prod } +enum FamilyFileEnum { active, inactive, blocked, deleted, pending } + extension CalenderExtension on CalenderEnum { int get toInt { switch (this) { @@ -138,6 +140,57 @@ extension OTPTypeEnumExtension on OTPTypeEnum { } } +extension FamilyFileEnumExtenshion on FamilyFileEnum { + int get toInt { + switch (this) { + case FamilyFileEnum.active: + return 3; + case FamilyFileEnum.blocked: + return 1; + case FamilyFileEnum.deleted: + return 0; + case FamilyFileEnum.pending: + return 2; + case FamilyFileEnum.inactive: + return 4; + } + } + + String get displayName { + AppState appState = getIt.get(); + bool isArabic = appState.getLanguageID() == 1 ? true : false; + switch (this) { + case FamilyFileEnum.active: + return isArabic ? 'نشط' : 'Active'; + case FamilyFileEnum.inactive: + return isArabic ? 'غير نشط' : 'Inactive'; + case FamilyFileEnum.blocked: + return isArabic ? 'محظور' : 'Blocked'; + case FamilyFileEnum.deleted: + return isArabic ? 'محذوف' : 'Deleted'; + case FamilyFileEnum.pending: + return isArabic ? 'قيد الانتظار' : 'Pending'; + } + } + + static FamilyFileEnum? fromValue(int value) { + switch (value) { + case 0: + return FamilyFileEnum.pending; + case 2: + return FamilyFileEnum.blocked; + case 1: + return FamilyFileEnum.deleted; + case 3: + return FamilyFileEnum.active; + case 4: + return FamilyFileEnum.inactive; + default: + return null; + } + } +} + enum ServiceTypeEnum { advancePayment, //3 ancillaryOrder, //3 diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index 8d1af92..9244e4a 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -30,8 +30,7 @@ abstract class AuthenticationRepo { int? responseID, bool isSwitchUser = false, int? patientID, - int? loginType - }); + int? loginType}); Future>> checkIfUserAgreed({required dynamic commonAuthanticatedRequest}); @@ -196,17 +195,16 @@ class AuthenticationRepoImp implements AuthenticationRepo { } @override - Future>> checkActivationCodeRepo({ - required dynamic newRequest, // could be CheckActivationCodeReq or CheckActivationCodeRegisterReq - required String? activationCode, - required bool isRegister, - bool isFormFamilyFile = false, - int? patientShareRequestID, - int? responseID, - bool isSwitchUser = false, - int? patientID, - int? loginType - }) async { + Future>> checkActivationCodeRepo( + {required dynamic newRequest, // could be CheckActivationCodeReq or CheckActivationCodeRegisterReq + required String? activationCode, + required bool isRegister, + bool isFormFamilyFile = false, + int? patientShareRequestID, + int? responseID, + bool isSwitchUser = false, + int? patientID, + int? loginType}) async { if (isRegister) { newRequest["activationCode"] = activationCode ?? "0000"; newRequest["isSilentLogin"] = activationCode != null ? false : true; @@ -227,7 +225,9 @@ class AuthenticationRepoImp implements AuthenticationRepo { familyRequest['Status'] = 3; familyRequest["PatientID"] = appState.getAuthenticatedUser()!.patientId ?? 0; familyRequest["LogInTokenID"] = appState.getFamilyFileTokenID; - + familyRequest["activationCode"] = activationCode ?? "0000"; + familyRequest["PatientMobileNumber"] = newRequest.patientMobileNumber; + familyRequest["PatientIdentificationID"] = newRequest.patientIdentificationID; } Map switchRequest = {}; if (isSwitchUser) { @@ -237,14 +237,12 @@ class AuthenticationRepoImp implements AuthenticationRepo { switchRequest['IsSilentLogin'] = true; switchRequest['LogInTokenID'] = null; switchRequest['SearchType'] = 2; - if(loginType != 0) { - switchRequest['SuperUser'] = patientID; - switchRequest['DeviceToken'] = null; - }else{ - switchRequest['LoginType'] = 2; - } - - + if (loginType != 0) { + switchRequest['SuperUser'] = patientID; + switchRequest['DeviceToken'] = null; + } else { + switchRequest['LoginType'] = 2; + } } final endpoint = isFormFamilyFile diff --git a/lib/features/medical_file/medical_file_repo.dart b/lib/features/medical_file/medical_file_repo.dart index b90e7fb..9c18696 100644 --- a/lib/features/medical_file/medical_file_repo.dart +++ b/lib/features/medical_file/medical_file_repo.dart @@ -24,7 +24,7 @@ abstract class MedicalFileRepo { Future>> getPatientMedicalReportPDF(PatientMedicalReportResponseModel patientMedicalReportResponseModel, AuthenticatedUser authenticatedUser); - Future>>> getPatientFamilyFiles(int status, int patientId); + Future>>> getPatientFamilyFiles(int? status, int patientId); Future>>> getAllPendingRecordsByResponseId({required Map request}); @@ -276,13 +276,13 @@ class MedicalFileRepoImp implements MedicalFileRepo { } @override - Future>>> getPatientFamilyFiles(int status, int patientID) async { + Future>>> getPatientFamilyFiles(int? status, int patientID) async { try { GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( - FAMILY_FILES, - body: {"Status": status, "PatientID":patientID}, + ApiConsts.getAllSharedRecordsByStatus, + body: {if (status != null) "Status": status, "PatientID": patientID}, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; }, @@ -327,7 +327,7 @@ class MedicalFileRepoImp implements MedicalFileRepo { }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - final list = response['GetAllSharedRecordsByStatusList']; + final list = response['GetAllPendingRecordsList']; // if (list == null || list.isEmpty) { // throw Exception("lab list is empty"); // } @@ -366,7 +366,6 @@ class MedicalFileRepoImp implements MedicalFileRepo { }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - apiResponse = GenericApiModel( messageStatus: messageStatus, statusCode: statusCode, diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 746e592..153828c 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -39,14 +39,27 @@ class MedicalFileViewModel extends ChangeNotifier { List patientMedicalReportCancelledList = []; List patientFamilyFiles = []; + List pendingFamilyFiles = []; String patientSickLeavePDFBase64 = ""; String patientMedicalReportPDFBase64 = ""; int selectedMedicalReportsTabIndex = 0; + int _selectedFamilyFileTabIndex = 0; + + int get getSelectedFamilyFileTabIndex => _selectedFamilyFileTabIndex; + + set setSelectedFamilyFileTabIndex(int value) { + if (_selectedFamilyFileTabIndex != value) { + _selectedFamilyFileTabIndex = value; + notifyListeners(); + } + } + static final DialogService _dialogService = getIt.get(); - AppState _appState = getIt(); + final AppState _appState = getIt(); AuthenticationViewModel authVM = getIt.get(); + MedicalFileViewModel({required this.medicalFileRepo, required this.errorHandlerService}); initMedicalFileProvider() { @@ -67,6 +80,14 @@ class MedicalFileViewModel extends ChangeNotifier { notifyListeners(); } + void onFamilyFileTabChange(int index) { + setSelectedFamilyFileTabIndex = index; + if (index == 1) { + getAllPendingRecordsByResponseId(); + } + notifyListeners(); + } + setIsPatientVaccineListLoading(bool isLoading) { isPatientVaccineListLoading = isLoading; notifyListeners(); @@ -235,8 +256,8 @@ class MedicalFileViewModel extends ChangeNotifier { ); } - Future getFamilyFiles({Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await medicalFileRepo.getPatientFamilyFiles(); + Future getFamilyFiles({int? status, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await medicalFileRepo.getPatientFamilyFiles(status, _appState.superUserID != null ? _appState.superUserID! : _appState.getAuthenticatedUser()!.patientId!); result.fold( (failure) async => await errorHandlerService.handleError( @@ -250,15 +271,80 @@ class MedicalFileViewModel extends ChangeNotifier { _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { patientFamilyFiles = apiResponse.data!; - patientFamilyFiles.insert( - 0, - FamilyFileResponseModelLists( + if (apiResponse.data != null) { + patientFamilyFiles.insert( + 0, + FamilyFileResponseModelLists( patientId: _appState.getAuthenticatedUser()!.patientId, patientName: '${_appState.getAuthenticatedUser()!.firstName!} ${_appState.getAuthenticatedUser()!.lastName!}', isActive: true, gender: _appState.getAuthenticatedUser()!.gender!, - responseId: _appState.getAuthenticatedUser()!.patientId), - ); + responseId: _appState.getAuthenticatedUser()!.patientId, + age: _appState.getAuthenticatedUser()!.age, + mobileNumber: _appState.getAuthenticatedUser()!.mobileNumber, + patientIdenficationNumber: _appState.getAuthenticatedUser()!.patientIdentificationNo, + emaiLAddress: _appState.getAuthenticatedUser()!.emailAddress, + genderDescription: _appState.getAuthenticatedUser()!.genderDescription, + ), + ); + + final List activeFamilyFiles = []; + final List tempPendingFamilyFiles = []; + for (var element in apiResponse.data!) { + if (element.status != null && element.status == FamilyFileEnum.active.toInt) { + activeFamilyFiles.add(FamilyFileResponseModelLists( + patientId: element.patientId, + patientName: element.patientName!, + isActive: element.status == FamilyFileEnum.active.toInt ? true : false, + gender: element.gender!, + responseId: element.patientId, + mobileNumber: element.mobileNumber, + age: element.age, + patientIdenficationNumber: element.patientIdenficationNumber, + relationship: element.relationship, + relationshipId: element.relationshipId, + relationshipN: element.relationshipN, + status: element.status, + statusDescription: element.statusDescription, + createdOn: element.createdOn, + editedOn: element.editedOn, + patientDataVerified: element.patientDataVerified, + regionId: element.regionId, + familyRegionId: element.familyRegionId, + genderDescription: element.genderDescription, + genderImage: element.genderImage, + emaiLAddress: element.emaiLAddress)); + } + + if (element.status != null && element.status == FamilyFileEnum.pending.toInt) { + tempPendingFamilyFiles.add(FamilyFileResponseModelLists( + patientId: element.patientId, + patientName: element.patientName!, + isActive: element.status == FamilyFileEnum.active.toInt ? true : false, + gender: element.gender!, + responseId: element.patientId, + mobileNumber: element.mobileNumber, + age: element.age, + patientIdenficationNumber: element.patientIdenficationNumber, + relationship: element.relationship, + relationshipId: element.relationshipId, + relationshipN: element.relationshipN, + status: element.status, + statusDescription: element.statusDescription, + createdOn: element.createdOn, + editedOn: element.editedOn, + patientDataVerified: element.patientDataVerified, + regionId: element.regionId, + familyRegionId: element.familyRegionId, + genderDescription: element.genderDescription, + genderImage: element.genderImage, + emaiLAddress: element.emaiLAddress)); + } + } + patientFamilyFiles.addAll(activeFamilyFiles.where((element) => !patientFamilyFiles.any((e) => e.patientId == element.patientId))); + pendingFamilyFiles.addAll(tempPendingFamilyFiles); + } + notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -278,61 +364,93 @@ class MedicalFileViewModel extends ChangeNotifier { if (apiResponse.messageStatus == 2) { _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { - print("======= Pending Records Response: ${jsonEncode(apiResponse.data)}"); + if (apiResponse.data != null) { + final List tempPendingFamilyFiles = []; + for (var element in apiResponse.data!) { + if (element.status != null && element.status == FamilyFileEnum.pending.toInt) { + tempPendingFamilyFiles.add(FamilyFileResponseModelLists( + patientId: element.patientId, + patientName: element.patientName!, + isActive: element.status == FamilyFileEnum.active.toInt ? true : false, + gender: element.gender, + responseId: element.patientId, + mobileNumber: element.mobileNumber, + age: element.age, + patientIdenficationNumber: element.patientIdenficationNumber, + relationship: element.relationship, + relationshipId: element.relationshipId, + relationshipN: element.relationshipN, + status: element.status, + statusDescription: element.statusDescription, + createdOn: element.createdOn, + editedOn: element.editedOn, + patientDataVerified: element.patientDataVerified, + regionId: element.regionId, + familyRegionId: element.familyRegionId, + genderDescription: element.genderDescription, + genderImage: element.genderImage, + emaiLAddress: element.emaiLAddress)); + } + } + pendingFamilyFiles.addAll(tempPendingFamilyFiles.where((element) => !pendingFamilyFiles.any((e) => e.patientId == element.patientId))); + } + notifyListeners(); } }, ); } - Future switchFamilyFiles({Function(dynamic)? onSuccess, Function(String)? onError}) async { - // final result = await medicalFileRepo.getPatientFamilyFiles(); - final result = await medicalFileRepo.getPatientFamilyFiles(3,_appState.superUserID !=null ? _appState.superUserID! : _appState.getAuthenticatedUser()!.patientId!); - - result.fold( - (failure) async => await errorHandlerService.handleError( - failure: failure, - onOkPressed: () { - onError!(failure.message); - }, - ), - (apiResponse) { - if (apiResponse.messageStatus == 2) { - _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage!, onOkPressed: () {}); - } else if (apiResponse.messageStatus == 1) { - patientFamilyFiles = apiResponse.data!; - patientFamilyFiles.insert( - 0, - FamilyFileResponseModelLists( - patientId: _appState.getAuthenticatedUser()!.patientId, - patientName: '${_appState.getAuthenticatedUser()!.firstName!} ${_appState.getAuthenticatedUser()!.lastName!}', - isActive: true, - gender: _appState.getAuthenticatedUser()!.gender!, - age: _appState.getAuthenticatedUser()!.age, - mobileNumber: _appState.getAuthenticatedUser()!.mobileNumber, - responseId: _appState.getAuthenticatedUser()!.patientId), - ); - notifyListeners(); - if (onSuccess != null) { - onSuccess(apiResponse); - } - } - }, + // Future switchFamilyFiles({Function(dynamic)? onSuccess, Function(String)? onError}) async { + // // final result = await medicalFileRepo.getPatientFamilyFiles(); + // final result = await medicalFileRepo.getPatientFamilyFiles(3, _appState.superUserID != null ? _appState.superUserID! : _appState.getAuthenticatedUser()!.patientId!); + // + // result.fold( + // (failure) async => await errorHandlerService.handleError( + // failure: failure, + // onOkPressed: () { + // onError!(failure.message); + // }, + // ), + // (apiResponse) { + // if (apiResponse.messageStatus == 2) { + // _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage!, onOkPressed: () {}); + // } else if (apiResponse.messageStatus == 1) { + // patientFamilyFiles = apiResponse.data!; + // patientFamilyFiles.insert( + // 0, + // FamilyFileResponseModelLists( + // patientId: _appState.getAuthenticatedUser()!.patientId, + // patientName: '${_appState.getAuthenticatedUser()!.firstName!} ${_appState.getAuthenticatedUser()!.lastName!}', + // isActive: true, + // gender: _appState.getAuthenticatedUser()!.gender!, + // age: _appState.getAuthenticatedUser()!.age, + // mobileNumber: _appState.getAuthenticatedUser()!.mobileNumber, + // responseId: _appState.getAuthenticatedUser()!.patientId), + // ); + // notifyListeners(); + // if (onSuccess != null) { + // onSuccess(apiResponse); + // } + // } + // }, + // ); + // } + + Future switchFamilyFiles({Function(dynamic)? onSuccess, int? responseID, int? patientID, String? phoneNumber, Function(String)? onError}) async { + authVM.phoneNumberController.text = phoneNumber!.startsWith("0") ? phoneNumber.replaceFirst("0", "") : phoneNumber; + + await authVM.checkActivationCode( + activationCode: '0000', + otpTypeEnum: OTPTypeEnum.sms, + onWrongActivationCode: (String? str) {}, + responseID: responseID, + isFormFamilyFile: false, + isSwitchUser: true, + patientID: patientID, ); } - Future switchFamilyFiles( {Function(dynamic)? onSuccess,int? responseID,int? patientID, String? phoneNumber, Function(String)? onError}) async { - authVM.phoneNumberController.text =phoneNumber!.startsWith("0") - ? phoneNumber.replaceFirst("0", "") - : phoneNumber; - - - - await authVM.checkActivationCode(activationCode: '0000', otpTypeEnum: OTPTypeEnum.sms, onWrongActivationCode: (String? str) {}, responseID: responseID, isFormFamilyFile:false, isSwitchUser: true, patientID: patientID, ); - - - } Future addFamilyFile({required OTPTypeEnum otpTypeEnum, required bool isExcludedUser}) async { - LoaderBottomSheet.showLoader(); AuthenticationViewModel authVM = getIt.get(); NavigationService navigationService = getIt.get(); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index ea0b86b..afea4e6 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -78,7 +78,9 @@ class _MedicalFilePageState extends State { insuranceViewModel.initInsuranceProvider(); medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.getPatientSickLeaveList(); - medicalFileViewModel.getFamilyFiles(); + medicalFileViewModel.getFamilyFiles(); //TODO: Remove status: 1 by Aamir Need to Discuss With Sultan + medicalFileViewModel.getAllPendingRecordsByResponseId(); //TODO: Added By Aamir + medicalFileViewModel.onTabChanged(0); } }); @@ -198,7 +200,6 @@ class _MedicalFilePageState extends State { CustomTabBarModel(AppAssets.more, "More".needTranslation), ], onTabChange: (index) { - print(index); medicalFileVM.onTabChanged(index); }, ).paddingSymmetrical(24.h, 0.0), diff --git a/lib/presentation/my_family/my_Family.dart b/lib/presentation/my_family/my_Family.dart index cb49933..1944704 100644 --- a/lib/presentation/my_family/my_Family.dart +++ b/lib/presentation/my_family/my_Family.dart @@ -22,6 +22,7 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:provider/provider.dart'; class FamilyMedicalScreen extends StatefulWidget { final List profiles; @@ -44,7 +45,7 @@ class _FamilyMedicalScreenState extends State { @override void initState() { super.initState(); - medicalVM = getIt.get(); + medicalVM = context.read(); } @override @@ -67,13 +68,12 @@ class _FamilyMedicalScreenState extends State { SizedBox(height: 25.h), CustomTabBar( tabs: tabs, - onTabChange: (int index) {}, + onTabChange: (index) { + medicalVM!.onFamilyFileTabChange(index); + }, ), SizedBox(height: 25.h), - FamilyCards(profiles: widget.profiles, onSelect: (FamilyFileResponseModelLists profile){ - medicalVM?.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); - - }, isShowDetails: true), + Consumer(builder: (context, medicalVM, child) => getFamilyTabs(index: medicalVM.getSelectedFamilyFileTabIndex)), SizedBox(height: 20.h), ], ), @@ -96,6 +96,28 @@ class _FamilyMedicalScreenState extends State { ); } + Widget getFamilyTabs({required int index}) { + switch (index) { + case 0: + return FamilyCards( + profiles: medicalVM!.patientFamilyFiles, + onSelect: (FamilyFileResponseModelLists profile) { + medicalVM!.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); + }, + isShowDetails: true, + ); + case 1: + return FamilyCards( + profiles: medicalVM!.pendingFamilyFiles, + onSelect: (FamilyFileResponseModelLists profile) { + // medicalVM!.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); + }, + isShowDetails: true, + ); default: + return SizedBox.shrink(); + } + } + void showModelSheet() { AuthenticationViewModel authVm = getIt.get(); return showCommonBottomSheetWithoutHeight(context, diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 480f91f..40a2d8c 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -43,20 +43,22 @@ class _FamilyCardsState extends State { ), itemBuilder: (context, index) { final profile = widget.profiles[index]; - final isActive = (profile.responseId == appState - .getAuthenticatedUser() - ?.patientId); + final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId); return Container( padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 15.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), child: Opacity( - opacity: isActive ? 0.4 : 1.0, // Fade all content if active + opacity: isActive || profile.status == FamilyFileEnum.pending.toInt ? 0.4 : 1.0, // Fade all content if active child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 5.h), Utils.buildImgWithAssets( - icon: profile.gender == 1 ? ((profile.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.male_img) : (profile.age! < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg), + icon: profile.gender == null + ? AppAssets.dummy_user + : profile.gender == 1 + ? ((profile.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.male_img) + : (profile.age! < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg), width: 80.h, height: 78.h), SizedBox(height: 8.h), @@ -73,13 +75,13 @@ class _FamilyCardsState extends State { widget.isShowDetails ? SizedBox(height: 4.h) : SizedBox(), widget.isShowDetails ? CustomChipWidget( - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: "Age: ${profile.age ?? "N/A"} Years", - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor, - ) + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "Age: ${profile.age ?? "N/A"} Years", + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor, + ) : SizedBox(), widget.isShowDetails ? SizedBox(height: 8.h) : SizedBox(), Spacer(), @@ -102,7 +104,7 @@ class _FamilyCardsState extends State { borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, fontSize: 13.h, - icon: widget.isBottomSheet ? null : AppAssets.heart, + icon: widget.isBottomSheet ? null : AppAssets.heart, iconColor: AppColors.primaryRedColor, padding: EdgeInsets.symmetric(vertical: 0, horizontal: 0), ).paddingOnly(top: 0, bottom: 0), From 1af2db5c098b826bb71216aa15b5fddd110d3fcd Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 29 Sep 2025 15:45:54 +0300 Subject: [PATCH 10/43] patient profile settings page integration implementation contd. --- lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 2 +- .../insurance/insurance_view_model.dart | 18 ++++- lib/presentation/home/landing_page.dart | 5 ++ .../widgets/patient_insurance_card.dart | 2 +- .../profile_settings/profile_settings.dart | 69 +++++++++++++++---- 6 files changed, 78 insertions(+), 20 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index a4e76fc..29b41fd 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -177,7 +177,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 4767477; + // body['PatientID'] = 1018977; } body.removeWhere((key, value) => value == null); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index fa02b2e..5886dfa 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu 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/features/insurance/insurance_view_model.dart b/lib/features/insurance/insurance_view_model.dart index 8319634..3129fbc 100644 --- a/lib/features/insurance/insurance_view_model.dart +++ b/lib/features/insurance/insurance_view_model.dart @@ -12,6 +12,8 @@ class InsuranceViewModel extends ChangeNotifier { bool isInsuranceDetailsLoading = false; bool isInsuranceUpdateDetailsLoading = false; + bool isInsuranceDataToBeLoaded = true; + InsuranceRepo insuranceRepo; ErrorHandlerService errorHandlerService; @@ -23,13 +25,15 @@ class InsuranceViewModel extends ChangeNotifier { InsuranceViewModel({required this.insuranceRepo, required this.errorHandlerService}); initInsuranceProvider() { - patientInsuranceList.clear(); + if (isInsuranceDataToBeLoaded) { + patientInsuranceList.clear(); + isInsuranceLoading = true; + getPatientInsuranceDetails(); + } patientInsuranceCardHistoryList.clear(); - isInsuranceLoading = true; isInsuranceHistoryLoading = true; isInsuranceDetailsLoading = true; isInsuranceUpdateDetailsLoading = true; - getPatientInsuranceDetails(); notifyListeners(); } @@ -48,7 +52,14 @@ class InsuranceViewModel extends ChangeNotifier { notifyListeners(); } + setIsInsuranceDataToBeLoaded(bool val) { + isInsuranceDataToBeLoaded = val; + notifyListeners(); + } + Future getPatientInsuranceDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { + if (!isInsuranceDataToBeLoaded) return; + final result = await insuranceRepo.getPatientInsuranceDetails(); result.fold( @@ -62,6 +73,7 @@ class InsuranceViewModel extends ChangeNotifier { } else if (apiResponse.messageStatus == 1) { patientInsuranceList = apiResponse.data!; isInsuranceLoading = false; + isInsuranceDataToBeLoaded = false; notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index cb2f76f..e78d7bd 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -16,6 +16,7 @@ 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/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/insurance/insurance_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'; @@ -61,6 +62,8 @@ class _LandingPageState extends State { late PrescriptionsViewModel prescriptionsViewModel; final CacheService cacheService = GetIt.instance(); + late InsuranceViewModel insuranceViewModel; + final SwiperController _controller = SwiperController(); @override @@ -81,6 +84,7 @@ class _LandingPageState extends State { myAppointmentsViewModel.getPatientAppointments(true, false); myAppointmentsViewModel.getPatientMyDoctors(); prescriptionsViewModel.initPrescriptionsViewModel(); + insuranceViewModel.initInsuranceProvider(); } }); super.initState(); @@ -92,6 +96,7 @@ class _LandingPageState extends State { NavigationService navigationService = getIt.get(); myAppointmentsViewModel = Provider.of(context, listen: false); prescriptionsViewModel = Provider.of(context, listen: false); + insuranceViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: SingleChildScrollView( diff --git a/lib/presentation/insurance/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index cf8b7d7..06cfd04 100644 --- a/lib/presentation/insurance/widgets/patient_insurance_card.dart +++ b/lib/presentation/insurance/widgets/patient_insurance_card.dart @@ -55,7 +55,7 @@ class PatientInsuranceCard extends StatelessWidget { icon: isInsuranceExpired ? AppAssets.cancel_circle_icon : AppAssets.insurance_active_icon, iconColor: isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, iconSize: 13.h, - text: isInsuranceExpired ? "Insurance Expired" : "Insurance Active", + text: isInsuranceExpired ? "Insurance Expired".needTranslation : "Insurance Active".needTranslation, onPressed: () {}, backgroundColor: isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.15) : AppColors.successColor.withOpacity(0.15), borderColor: isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.01) : AppColors.successColor.withOpacity(0.01), diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 58273fc..70f20b1 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -3,12 +3,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_swiper_view/flutter_swiper_view.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.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/extensions/int_extensions.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/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; @@ -21,6 +25,8 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; +import '../../core/dependencies.dart' show getIt; + class ProfileSettings extends StatefulWidget { ProfileSettings({Key? key}) : super(key: key); @@ -75,7 +81,7 @@ class _ProfileSettingsState extends State { builder: DotSwiperPaginationBuilder(color: Color(0xffD9D9D9), activeColor: AppColors.blackBgColor), ), itemBuilder: (BuildContext context, int index) { - return FamilyCardWidget().paddingOnly(right: 16); + return FamilyCardWidget(isRootUser: true).paddingOnly(right: 16); }, ), GridView( @@ -224,10 +230,14 @@ class _ProfileSettingsState extends State { } class FamilyCardWidget extends StatelessWidget { - FamilyCardWidget(); + FamilyCardWidget({this.isRootUser = true, Key? key}) : super(key: key); + + bool isRootUser; + late AppState appState; @override Widget build(BuildContext context) { + appState = getIt.get(); return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -243,17 +253,18 @@ class FamilyCardWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: 8.h, children: [ - Image.asset(true ? AppAssets.male_img : AppAssets.femaleImg, width: 56.h, height: 56.h), + Image.asset(appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, width: 56.h, height: 56.h), Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 0.h, mainAxisSize: MainAxisSize.min, children: [ - "Mahmoud Shrouf Shrouf".toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1), + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" + .toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1), AppCustomChipWidget( icon: AppAssets.file_icon, - labelText: "File no: 3423443", - iconSize: 14, + labelText: "${LocaleKeys.fileNo.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}", + iconSize: 12, ), ], ).expanded, @@ -266,22 +277,52 @@ class FamilyCardWidget extends StatelessWidget { alignment: WrapAlignment.start, spacing: 8.h, children: [ - AppCustomChipWidget(labelText: "35 Years Old"), - AppCustomChipWidget(labelText: "Blood: A+"), + AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), AppCustomChipWidget( - icon: AppAssets.insurance_active_icon, - labelText: "Insurance Active", - iconColor: AppColors.bgGreenColor, - iconSize: 14, - backgroundColor: AppColors.bgGreenColor.withValues(alpha: 0.15), + icon: AppAssets.blood_icon, + labelText: "${LocaleKeys.bloodType.tr(context: context)}: ${appState.getUserBloodGroup}", + iconColor: AppColors.primaryRedColor, ), + Consumer(builder: (context, insuranceVM, child) { + return AppCustomChipWidget( + icon: insuranceVM.isInsuranceLoading + ? AppAssets.cancel_circle_icon + : (DateTime.now().isAfter( + DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), + )) + ? AppAssets.cancel_circle_icon + : AppAssets.insurance_active_icon, + labelText: insuranceVM.isInsuranceLoading + ? "Insurance" + : (DateTime.now().isAfter( + DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), + ) + ? "Insurance Expired".needTranslation + : "Insurance Active".needTranslation), + iconColor: insuranceVM.isInsuranceLoading + ? AppColors.primaryRedColor + : (DateTime.now().isAfter( + DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), + )) + ? AppColors.primaryRedColor + : AppColors.successColor, + iconSize: 14, + backgroundColor: insuranceVM.isInsuranceLoading + ? AppColors.primaryRedColor + : (DateTime.now().isAfter( + DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), + )) + ? AppColors.primaryRedColor.withValues(alpha: 0.15) + : AppColors.successColor.withValues(alpha: 0.15), + ).toShimmer2(isShow: insuranceVM.isInsuranceLoading); + }), ], ), ), ], ).paddingOnly(top: 16, right: 16, left: 16, bottom: 12).expanded, 1.divider, - CustomButton(icon: AppAssets.add_family, text: "Add a new family member".needTranslation, onPressed: () {}).paddingOnly(top: 12, right: 16, left: 16, bottom: 16), + CustomButton(icon: AppAssets.add_family, text: "Add a new family member".needTranslation, height: 40.h, fontSize: 14, onPressed: () {}).paddingOnly(top: 12, right: 16, left: 16, bottom: 16), ], ), ); From 1335d3a7c8a010c7fb0a00e7087fe4d7fee02c04 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Mon, 29 Sep 2025 16:20:51 +0300 Subject: [PATCH 11/43] family screen & widgets --- assets/images/svg/switch_user.svg | 8 + lib/core/app_assets.dart | 1 + lib/core/app_state.dart | 2 +- .../authentication/authentication_repo.dart | 11 + .../authentication_view_model.dart | 2 +- .../medical_file/medical_file_view_model.dart | 198 +++++++++++++----- .../medical_file/medical_file_page.dart | 8 +- lib/presentation/my_family/my_Family.dart | 107 ++++++---- .../my_family/widget/family_cards.dart | 7 +- 9 files changed, 249 insertions(+), 95 deletions(-) create mode 100644 assets/images/svg/switch_user.svg diff --git a/assets/images/svg/switch_user.svg b/assets/images/svg/switch_user.svg new file mode 100644 index 0000000..fa8e47e --- /dev/null +++ b/assets/images/svg/switch_user.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index dd4bce4..424b90c 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -141,6 +141,7 @@ class AppAssets { static const String ic_normal_result = '$svgBasePath/normal_result.svg'; static const String ic_low_result = '$svgBasePath/low_result.svg'; static const String ic_critical_low_result = '$svgBasePath/critical_low_result.svg'; + static const String switch_user = '$svgBasePath/switch_user.svg'; //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index 206adc1..030cc36 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -57,7 +57,7 @@ class AppState { } } - int? get superUserID => _superUserID; + int? get getSuperUserID => _superUserID; set setSuperUserID(int? value) => _superUserID = value; diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index 9244e4a..96bdc0b 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -205,6 +205,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { bool isSwitchUser = false, int? patientID, int? loginType}) async { + AppState appState = getIt.get(); if (isRegister) { newRequest["activationCode"] = activationCode ?? "0000"; newRequest["isSilentLogin"] = activationCode != null ? false : true; @@ -243,6 +244,16 @@ class AuthenticationRepoImp implements AuthenticationRepo { } else { switchRequest['LoginType'] = 2; } + + if (appState.getSuperUserID == responseID) { + switchRequest['LoginType'] = 3; + switchRequest['PatientIdentificationID'] = ""; + // switchRequest['ProjectOutSA'] = newRequest.zipCode == '966' ? false : true; + switchRequest.remove('NationalID'); + switchRequest.remove('isDentalAllowedBackend'); + switchRequest.remove('ProjectOutSA'); + switchRequest.remove('ForRegisteration'); + } } final endpoint = isFormFamilyFile diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 1bdc09e..0a11eda 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -518,7 +518,7 @@ class AuthenticationViewModel extends ChangeNotifier { responseID: responseID, isSwitchUser: isSwitchUser, patientID: patientID, - loginType: _appState.superUserID != null ? 0 : 2, + loginType: _appState.getSuperUserID != null ? 0 : 2, ); resultEither.fold( diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 153828c..64075b4 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -257,7 +257,7 @@ class MedicalFileViewModel extends ChangeNotifier { } Future getFamilyFiles({int? status, Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await medicalFileRepo.getPatientFamilyFiles(status, _appState.superUserID != null ? _appState.superUserID! : _appState.getAuthenticatedUser()!.patientId!); + final result = await medicalFileRepo.getPatientFamilyFiles(status, _appState.getSuperUserID != null ? _appState.getSuperUserID! : _appState.getAuthenticatedUser()!.patientId!); result.fold( (failure) async => await errorHandlerService.handleError( @@ -288,61 +288,157 @@ class MedicalFileViewModel extends ChangeNotifier { ), ); + // final List activeFamilyFiles = []; + // final List tempPendingFamilyFiles = []; + // for (var element in apiResponse.data!) { + // if (element.status != null && element.status == FamilyFileEnum.active.toInt) { + // activeFamilyFiles.add(FamilyFileResponseModelLists( + // patientId: element.patientId, + // patientName: element.patientName!, + // isActive: element.status == FamilyFileEnum.active.toInt ? true : false, + // gender: element.gender!, + // responseId: element.patientId, + // mobileNumber: element.mobileNumber, + // age: element.age, + // patientIdenficationNumber: element.patientIdenficationNumber, + // relationship: element.relationship, + // relationshipId: element.relationshipId, + // relationshipN: element.relationshipN, + // status: element.status, + // statusDescription: element.statusDescription, + // createdOn: element.createdOn, + // editedOn: element.editedOn, + // patientDataVerified: element.patientDataVerified, + // regionId: element.regionId, + // familyRegionId: element.familyRegionId, + // genderDescription: element.genderDescription, + // genderImage: element.genderImage, + // emaiLAddress: element.emaiLAddress)); + // } + // + // if (element.status != null && element.status == FamilyFileEnum.pending.toInt) { + // tempPendingFamilyFiles.add(FamilyFileResponseModelLists( + // patientId: element.patientId, + // patientName: element.patientName!, + // isActive: element.status == FamilyFileEnum.active.toInt ? true : false, + // gender: element.gender!, + // responseId: element.patientId, + // mobileNumber: element.mobileNumber, + // age: element.age, + // patientIdenficationNumber: element.patientIdenficationNumber, + // relationship: element.relationship, + // relationshipId: element.relationshipId, + // relationshipN: element.relationshipN, + // status: element.status, + // statusDescription: element.statusDescription, + // createdOn: element.createdOn, + // editedOn: element.editedOn, + // patientDataVerified: element.patientDataVerified, + // regionId: element.regionId, + // familyRegionId: element.familyRegionId, + // genderDescription: element.genderDescription, + // genderImage: element.genderImage, + // emaiLAddress: element.emaiLAddress)); + // } + // } + // patientFamilyFiles.addAll(activeFamilyFiles.where((element) => !patientFamilyFiles.any((e) => e.patientId == element.patientId))); + // pendingFamilyFiles.addAll(tempPendingFamilyFiles); + final List activeFamilyFiles = []; final List tempPendingFamilyFiles = []; - for (var element in apiResponse.data!) { - if (element.status != null && element.status == FamilyFileEnum.active.toInt) { - activeFamilyFiles.add(FamilyFileResponseModelLists( - patientId: element.patientId, - patientName: element.patientName!, - isActive: element.status == FamilyFileEnum.active.toInt ? true : false, - gender: element.gender!, - responseId: element.patientId, - mobileNumber: element.mobileNumber, - age: element.age, - patientIdenficationNumber: element.patientIdenficationNumber, - relationship: element.relationship, - relationshipId: element.relationshipId, - relationshipN: element.relationshipN, - status: element.status, - statusDescription: element.statusDescription, - createdOn: element.createdOn, - editedOn: element.editedOn, - patientDataVerified: element.patientDataVerified, - regionId: element.regionId, - familyRegionId: element.familyRegionId, - genderDescription: element.genderDescription, - genderImage: element.genderImage, - emaiLAddress: element.emaiLAddress)); - } + // final Set pendingIds = {}; + + // for (var element in apiResponse.data!) { + // if (element.status != null && element.status == FamilyFileEnum.pending.toInt) { + // tempPendingFamilyFiles.add(FamilyFileResponseModelLists( + // patientId: element.patientId, + // patientName: element.patientName!, + // isActive: false, + // gender: element.gender!, + // responseId: element.patientId, + // mobileNumber: element.mobileNumber, + // age: element.age, + // patientIdenficationNumber: element.patientIdenficationNumber, + // relationship: element.relationship, + // relationshipId: element.relationshipId, + // relationshipN: element.relationshipN, + // status: element.status, + // statusDescription: element.statusDescription, + // createdOn: element.createdOn, + // editedOn: element.editedOn, + // patientDataVerified: element.patientDataVerified, + // regionId: element.regionId, + // familyRegionId: element.familyRegionId, + // genderDescription: element.genderDescription, + // genderImage: element.genderImage, + // emaiLAddress: element.emaiLAddress)); + // } else if (element.status != null && element.status == FamilyFileEnum.active.toInt) { + // activeFamilyFiles.add(FamilyFileResponseModelLists( + // patientId: element.patientId, + // patientName: element.patientName!, + // isActive: element.status == FamilyFileEnum.active.toInt ? true : false, + // gender: element.gender!, + // responseId: element.patientId, + // mobileNumber: element.mobileNumber, + // age: element.age, + // patientIdenficationNumber: element.patientIdenficationNumber, + // relationship: element.relationship, + // relationshipId: element.relationshipId, + // relationshipN: element.relationshipN, + // status: element.status, + // statusDescription: element.statusDescription, + // createdOn: element.createdOn, + // editedOn: element.editedOn, + // patientDataVerified: element.patientDataVerified, + // regionId: element.regionId, + // familyRegionId: element.familyRegionId, + // genderDescription: element.genderDescription, + // genderImage: element.genderImage, + // emaiLAddress: element.emaiLAddress)); + // } + // } - if (element.status != null && element.status == FamilyFileEnum.pending.toInt) { - tempPendingFamilyFiles.add(FamilyFileResponseModelLists( - patientId: element.patientId, - patientName: element.patientName!, - isActive: element.status == FamilyFileEnum.active.toInt ? true : false, - gender: element.gender!, - responseId: element.patientId, - mobileNumber: element.mobileNumber, - age: element.age, - patientIdenficationNumber: element.patientIdenficationNumber, - relationship: element.relationship, - relationshipId: element.relationshipId, - relationshipN: element.relationshipN, - status: element.status, - statusDescription: element.statusDescription, - createdOn: element.createdOn, - editedOn: element.editedOn, - patientDataVerified: element.patientDataVerified, - regionId: element.regionId, - familyRegionId: element.familyRegionId, - genderDescription: element.genderDescription, - genderImage: element.genderImage, - emaiLAddress: element.emaiLAddress)); + for (var element in apiResponse.data!) { + if (element.status == null) continue; + + final isPending = element.status == FamilyFileEnum.pending.toInt; + final isActive = element.status == FamilyFileEnum.active.toInt; + + if (!isPending && !isActive) continue; + + final familyFile = FamilyFileResponseModelLists( + patientId: element.patientId, + patientName: element.patientName!, + isActive: isActive, + gender: element.gender!, + responseId: element.patientId, + mobileNumber: element.mobileNumber, + age: element.age, + patientIdenficationNumber: element.patientIdenficationNumber, + relationship: element.relationship, + relationshipId: element.relationshipId, + relationshipN: element.relationshipN, + status: element.status, + statusDescription: element.statusDescription, + createdOn: element.createdOn, + editedOn: element.editedOn, + patientDataVerified: element.patientDataVerified, + regionId: element.regionId, + familyRegionId: element.familyRegionId, + genderDescription: element.genderDescription, + genderImage: element.genderImage, + emaiLAddress: element.emaiLAddress, + ); + + if (isPending) { + tempPendingFamilyFiles.add(familyFile); + } else if (isActive) { + activeFamilyFiles.add(familyFile); } } - patientFamilyFiles.addAll(activeFamilyFiles.where((element) => !patientFamilyFiles.any((e) => e.patientId == element.patientId))); - pendingFamilyFiles.addAll(tempPendingFamilyFiles); + + patientFamilyFiles.addAll(activeFamilyFiles.where((element) => patientFamilyFiles.every((e) => e.patientId != element.patientId || e.status != FamilyFileEnum.active.toInt))); + pendingFamilyFiles.addAll(tempPendingFamilyFiles.where((element) => pendingFamilyFiles.every((e) => e.patientId != element.patientId))); } notifyListeners(); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index afea4e6..dcd83c6 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -73,13 +73,16 @@ class _MedicalFilePageState extends State { @override void initState() { + appState = getIt.get(); scheduleMicrotask(() { if (appState.isAuthenticated) { insuranceViewModel.initInsuranceProvider(); medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.getPatientSickLeaveList(); - medicalFileViewModel.getFamilyFiles(); //TODO: Remove status: 1 by Aamir Need to Discuss With Sultan - medicalFileViewModel.getAllPendingRecordsByResponseId(); //TODO: Added By Aamir + if (appState.getSuperUserID == null) { + medicalFileViewModel.getFamilyFiles(status: 3); //TODO: Remove status: 1 by Aamir Need to Discuss With Sultan + medicalFileViewModel.getAllPendingRecordsByResponseId(); //TODO: Added By Aamir + } medicalFileViewModel.onTabChanged(0); } @@ -93,7 +96,6 @@ class _MedicalFilePageState extends State { myAppointmentsViewModel = Provider.of(context, listen: false); medicalFileViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); - appState = getIt.get(); NavigationService navigationService = getIt.get(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, diff --git a/lib/presentation/my_family/my_Family.dart b/lib/presentation/my_family/my_Family.dart index 1944704..c0d85c0 100644 --- a/lib/presentation/my_family/my_Family.dart +++ b/lib/presentation/my_family/my_Family.dart @@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_ import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/my_family/widget/family_cards.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -51,33 +52,23 @@ class _FamilyMedicalScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.scaffoldBgColor, - appBar: CustomAppBar( - onBackPressed: () { - Navigator.of(context).pop(); - }, - onLanguageChanged: (lang) {}, - hideLogoAndLang: true, - ), - body: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.myMedicalFile.tr().toText26(color: AppColors.textColor, weight: FontWeight.w600, letterSpacing: -2), - SizedBox(height: 25.h), - CustomTabBar( - tabs: tabs, - onTabChange: (index) { - medicalVM!.onFamilyFileTabChange(index); - }, - ), - SizedBox(height: 25.h), - Consumer(builder: (context, medicalVM, child) => getFamilyTabs(index: medicalVM.getSelectedFamilyFileTabIndex)), - SizedBox(height: 20.h), - ], - ), - ).paddingSymmetrical(20, 0), + body: CollapsingListView( + title: "My Medical File".needTranslation, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTabBar( + tabs: tabs, + onTabChange: (index) { + medicalVM!.onFamilyFileTabChange(index); + }, + ), + SizedBox(height: 25.h), + Consumer(builder: (context, medicalVM, child) => getFamilyTabs(index: medicalVM.getSelectedFamilyFileTabIndex)), + SizedBox(height: 20.h), + ], + ).paddingSymmetrical(20, 0)), bottomSheet: Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -85,15 +76,58 @@ class _FamilyMedicalScreenState extends State { ), padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 20.h), child: CustomButton( - text: "Add a new family member", - onPressed: () { - showModelSheet(); - }, - icon: AppAssets.add_icon, - height: 56.h, - fontWeight: FontWeight.w600, - )), + text: "Add a new family member", + onPressed: () { + showModelSheet(); + }, + icon: AppAssets.add_icon, + height: 56.h, + fontWeight: FontWeight.w600)), ); + // return Scaffold( + // backgroundColor: AppColors.scaffoldBgColor, + // appBar: CustomAppBar( + // onBackPressed: () { + // Navigator.of(context).pop(); + // }, + // onLanguageChanged: (lang) {}, + // hideLogoAndLang: true, + // ), + // body: SingleChildScrollView( + // child: Column( + // mainAxisSize: MainAxisSize.min, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // LocaleKeys.myMedicalFile.tr().toText26(color: AppColors.textColor, weight: FontWeight.w600, letterSpacing: -2), + // SizedBox(height: 25.h), + // CustomTabBar( + // tabs: tabs, + // onTabChange: (index) { + // medicalVM!.onFamilyFileTabChange(index); + // }, + // ), + // SizedBox(height: 25.h), + // Consumer(builder: (context, medicalVM, child) => getFamilyTabs(index: medicalVM.getSelectedFamilyFileTabIndex)), + // SizedBox(height: 20.h), + // ], + // ).paddingSymmetrical(20, 0), + // ), + // bottomSheet: Container( + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + // color: AppColors.whiteColor, + // customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + // ), + // padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 20.h), + // child: CustomButton( + // text: "Add a new family member", + // onPressed: () { + // showModelSheet(); + // }, + // icon: AppAssets.add_icon, + // height: 56.h, + // fontWeight: FontWeight.w600, + // )), + // ); } Widget getFamilyTabs({required int index}) { @@ -113,7 +147,8 @@ class _FamilyMedicalScreenState extends State { // medicalVM!.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); }, isShowDetails: true, - ); default: + ); + default: return SizedBox.shrink(); } } diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 40a2d8c..3916756 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -33,7 +33,7 @@ class _FamilyCardsState extends State { Widget build(BuildContext context) { return GridView.builder( shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), + physics: NeverScrollableScrollPhysics(), itemCount: widget.profiles.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, @@ -41,6 +41,7 @@ class _FamilyCardsState extends State { mainAxisSpacing: 10.h, childAspectRatio: widget.isShowDetails ? 0.56.h : 0.74.h, ), + padding: EdgeInsets.only(bottom: 80.h), itemBuilder: (context, index) { final profile = widget.profiles[index]; final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId); @@ -99,12 +100,12 @@ class _FamilyCardsState extends State { CustomButton( height: 40.h, onPressed: () => widget.onSelect(profile), - text: LocaleKeys.select.tr(), + text: LocaleKeys.switchAccount.tr(), backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, fontSize: 13.h, - icon: widget.isBottomSheet ? null : AppAssets.heart, + icon: widget.isBottomSheet ? null : AppAssets.switch_user, iconColor: AppColors.primaryRedColor, padding: EdgeInsets.symmetric(vertical: 0, horizontal: 0), ).paddingOnly(top: 0, bottom: 0), From 2de02102c26571b014d686c7863b363b49d3b50b Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 29 Sep 2025 17:30:13 +0300 Subject: [PATCH 12/43] profile settings update --- .../lab/lab_results/lab_result_details.dart | 2 +- .../profile_settings/profile_settings.dart | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index e3ffaea..9935c0d 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -13,10 +13,10 @@ import 'package:hmg_patient_app_new/features/lab/history/lab_history_viewmodel.d import 'package:hmg_patient_app_new/features/lab/lab_range_view_model.dart' show LabRangeViewModel; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart'; -import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_calender.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_list_item.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; import 'package:provider/provider.dart' show Consumer, Provider; diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 70f20b1..e5ec389 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -322,7 +322,21 @@ class FamilyCardWidget extends StatelessWidget { ], ).paddingOnly(top: 16, right: 16, left: 16, bottom: 12).expanded, 1.divider, - CustomButton(icon: AppAssets.add_family, text: "Add a new family member".needTranslation, height: 40.h, fontSize: 14, onPressed: () {}).paddingOnly(top: 12, right: 16, left: 16, bottom: 16), + //TODO: Add family file switch logic here + isRootUser + ? CustomButton(icon: AppAssets.add_family, text: "Add a new family member".needTranslation, height: 40.h, fontSize: 14, onPressed: () {}) + .paddingOnly(top: 12, right: 16, left: 16, bottom: 16) + : CustomButton( + icon: AppAssets.add_family, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + iconColor: AppColors.primaryRedColor, + text: "Switch to this medical file".needTranslation, + height: 40.h, + fontSize: 14, + onPressed: () {}) + .paddingOnly(top: 12, right: 16, left: 16, bottom: 16), ], ), ); From 8bb0e495cc8feada8f26fca6e799824e3dd3373a Mon Sep 17 00:00:00 2001 From: Haroon Amjad <> Date: Mon, 29 Sep 2025 22:59:10 +0300 Subject: [PATCH 13/43] request medical report implementation contd. --- lib/core/api_consts.dart | 2 +- lib/features/insurance/insurance_repo.dart | 4 +- .../insurance/insurance_view_model.dart | 13 +- .../medical_file/medical_file_repo.dart | 50 +++++ .../medical_file/medical_file_view_model.dart | 35 +++- .../my_appointments/my_appointments_repo.dart | 5 +- .../widgets/appointment_card.dart | 163 +++++++++------- .../insurance/insurance_home_page.dart | 81 +++++--- .../insurance/widgets/insurance_history.dart | 131 +++++++------ .../insurance_update_details_card.dart | 101 +++++----- .../widgets/patient_insurance_card.dart | 6 +- .../medical_file/medical_file_page.dart | 87 ++++++--- .../medical_file/medical_reports_page.dart | 94 ---------- .../medical_report_request_page.dart | 61 ++++++ .../medical_report/medical_reports_page.dart | 176 ++++++++++++++++++ .../widgets/patient_medical_report_card.dart | 0 pubspec.yaml | 2 +- 17 files changed, 674 insertions(+), 337 deletions(-) delete mode 100644 lib/presentation/medical_file/medical_reports_page.dart create mode 100644 lib/presentation/medical_report/medical_report_request_page.dart create mode 100644 lib/presentation/medical_report/medical_reports_page.dart rename lib/presentation/{medical_file => medical_report}/widgets/patient_medical_report_card.dart (100%) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 5886dfa..fa02b2e 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu 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/insurance/insurance_repo.dart b/lib/features/insurance/insurance_repo.dart index 65f97ae..d367509 100644 --- a/lib/features/insurance/insurance_repo.dart +++ b/lib/features/insurance/insurance_repo.dart @@ -11,7 +11,7 @@ import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class InsuranceRepo { Future>>> getPatientInsuranceDetails(); - Future>>> getPatientInsuranceCardHistory({required String patientId}); + Future>>> getPatientInsuranceCardHistory(); Future>> getPatientInsuranceDetailsForUpdate({required String patientId, required String identificationNo}); } @@ -64,7 +64,7 @@ class InsuranceRepoImp implements InsuranceRepo { } @override - Future>>> getPatientInsuranceCardHistory({required String patientId}) async { + Future>>> getPatientInsuranceCardHistory() async { Map mapDevice = {}; try { diff --git a/lib/features/insurance/insurance_view_model.dart b/lib/features/insurance/insurance_view_model.dart index 3129fbc..0bcf30f 100644 --- a/lib/features/insurance/insurance_view_model.dart +++ b/lib/features/insurance/insurance_view_model.dart @@ -66,6 +66,7 @@ class InsuranceViewModel extends ChangeNotifier { // (failure) async => await errorHandlerService.handleError(failure: failure), (failure) async { isInsuranceLoading = false; + notifyListeners(); }, (apiResponse) { if (apiResponse.messageStatus == 2) { @@ -84,10 +85,13 @@ class InsuranceViewModel extends ChangeNotifier { } Future getPatientInsuranceCardHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await insuranceRepo.getPatientInsuranceCardHistory(patientId: "1231755"); + final result = await insuranceRepo.getPatientInsuranceCardHistory(); result.fold( - (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + isInsuranceHistoryLoading = false; + notifyListeners(); + }, (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); @@ -108,7 +112,10 @@ class InsuranceViewModel extends ChangeNotifier { final result = await insuranceRepo.getPatientInsuranceDetailsForUpdate(patientId: patientID, identificationNo: identificationNo); result.fold( - (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + isInsuranceUpdateDetailsLoading = false; + notifyListeners(); + }, (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); diff --git a/lib/features/medical_file/medical_file_repo.dart b/lib/features/medical_file/medical_file_repo.dart index 6b83f33..658d045 100644 --- a/lib/features/medical_file/medical_file_repo.dart +++ b/lib/features/medical_file/medical_file_repo.dart @@ -8,6 +8,7 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_vaccine_response_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/services/logger_service.dart'; import '../authentication/models/resp_models/authenticated_user_resp_model.dart'; @@ -27,6 +28,8 @@ abstract class MedicalFileRepo { Future>>> getPatientFamilyFiles(); Future>>> addFamilyFile({required dynamic request}); + + Future>>> getPatientAppointmentsForMedicalReport(); } class MedicalFileRepoImp implements MedicalFileRepo { @@ -348,4 +351,51 @@ class MedicalFileRepoImp implements MedicalFileRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> getPatientAppointmentsForMedicalReport() async { + Map mapDevice = { + "IsActiveAppointment": false, + "IsComingFromCOC": false, + "isForUpcomming": false, + "IsForMedicalReport": true, + "IsForArrived": false, + }; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_PATIENT_APPOINTMENT_HISTORY, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppoimentAllHistoryResultList']; + // if (list == null || list.isEmpty) { + // throw Exception("Appointments list is empty"); + // } + + final appointmentsList = list.map((item) => PatientAppointmentHistoryResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: appointmentsList, + ); + } 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/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 2e528de..061392e 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -8,6 +8,7 @@ import 'package:hmg_patient_app_new/features/medical_file/models/family_file_res import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_vaccine_response_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/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; @@ -30,6 +31,9 @@ class MedicalFileViewModel extends ChangeNotifier { List patientMedicalReportReadyList = []; List patientMedicalReportCancelledList = []; + List patientMedicalReportAppointmentHistoryList = []; + PatientAppointmentHistoryResponseModel? patientMedicalReportSelectedAppointment; + List patientFamilyFiles = []; String patientSickLeavePDFBase64 = ""; @@ -42,6 +46,7 @@ class MedicalFileViewModel extends ChangeNotifier { MedicalFileViewModel({required this.medicalFileRepo, required this.errorHandlerService}); initMedicalFileProvider() { + patientMedicalReportAppointmentHistoryList.clear(); isPatientVaccineListLoading = true; isPatientMedicalReportsListLoading = true; notifyListeners(); @@ -87,6 +92,11 @@ class MedicalFileViewModel extends ChangeNotifier { notifyListeners(); } + setSelectedMedicalReportAppointment(PatientAppointmentHistoryResponseModel? val) { + patientMedicalReportSelectedAppointment = val; + notifyListeners(); + } + void onTabChanged(int index) { selectedTabIndex = index; notifyListeners(); @@ -293,9 +303,32 @@ class MedicalFileViewModel extends ChangeNotifier { ); } + Future getPatientMedicalReportAppointmentsList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + patientMedicalReportAppointmentHistoryList.clear(); + notifyListeners(); + + final result = await medicalFileRepo.getPatientAppointmentsForMedicalReport(); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + patientMedicalReportAppointmentHistoryList = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + Future addFamilyFile() async { final resultEither = await medicalFileRepo.addFamilyFile(request: {}); resultEither.fold((failure) async => await errorHandlerService.handleError(failure: failure), (apiResponse) async {}); } - } diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index 99f7c7d..eb8611d 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -56,13 +56,10 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { Future>>> getPatientAppointments({required bool isActiveAppointment, required bool isArrivedAppointments}) async { Map mapDevice = { "IsActiveAppointment": isActiveAppointment, - "isDentalAllowedBackend": false, - "PatientTypeID": 1, "IsComingFromCOC": false, - "PatientType": 1, "isForUpcomming": false, + "IsForMedicalReport": false, "IsForArrived": isArrivedAppointments, - "PatientOutSA": 0 }; try { diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 152436a..d287795 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -8,6 +8,7 @@ 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/medical_file/medical_file_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/my_appointments/utils/appointment_type.dart'; @@ -21,12 +22,21 @@ import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:smooth_corner/smooth_corner.dart'; class AppointmentCard extends StatefulWidget { - AppointmentCard({super.key, required this.patientAppointmentHistoryResponseModel, required this.myAppointmentsViewModel, this.isLoading = false, this.isFromHomePage = false}); + AppointmentCard( + {super.key, + required this.patientAppointmentHistoryResponseModel, + required this.myAppointmentsViewModel, + this.isLoading = false, + this.isFromHomePage = false, + this.isFromMedicalReport = false, + this.medicalFileViewModel}); PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; MyAppointmentsViewModel myAppointmentsViewModel; bool isLoading; bool isFromHomePage; + bool isFromMedicalReport; + MedicalFileViewModel? medicalFileViewModel; @override State createState() => _AppointmentCardState(); @@ -171,75 +181,94 @@ class _AppointmentCardState extends State { ], ), SizedBox(height: 16.h), - Row( - children: [ - Expanded( - flex: 6, - child: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) - ? getArrivedAppointmentButton().toShimmer2(isShow: widget.isLoading) - : CustomButton( - text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction), - onPressed: () { - Navigator.of(context) - .push(CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), - )) - .then((val) { - widget.myAppointmentsViewModel.initAppointmentsViewModel(); - widget.myAppointmentsViewModel.getPatientAppointments(true, false); - }); - }, - backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.15), - borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01), - textColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), - fontSize: 14, - fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction), - iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), - iconSize: 15.h, - ).toShimmer2(isShow: widget.isLoading), - ), - SizedBox(width: 8.h), - Expanded( - flex: 1, - child: Container( + widget.isFromMedicalReport + ? CustomButton( + text: "Select appointment".needTranslation, + onPressed: () { + widget.medicalFileViewModel!.setSelectedMedicalReportAppointment(widget.patientAppointmentHistoryResponseModel); + Navigator.pop(context, false); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, - width: 40.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.textColor, - borderRadius: 10.h, - ), - child: Padding( - padding: EdgeInsets.all(10.h), - child: Transform.flip( - flipX: appState.isArabic() ? true : false, - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, - iconColor: AppColors.whiteColor, - width: 10.h, - height: 10.h, - fit: BoxFit.contain, - ), + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + ) + : Row( + children: [ + Expanded( + flex: 6, + child: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) + ? getArrivedAppointmentButton().toShimmer2(isShow: widget.isLoading) + : CustomButton( + text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction), + onPressed: () { + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) { + widget.myAppointmentsViewModel.initAppointmentsViewModel(); + widget.myAppointmentsViewModel.getPatientAppointments(true, false); + }); + }, + backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.15), + borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01), + textColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction), + iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), + iconSize: 15.h, + ).toShimmer2(isShow: widget.isLoading), ), - ), - ).toShimmer2(isShow: widget.isLoading).onPress(() { - Navigator.of(context) - .push( - CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + SizedBox(width: 8.h), + Expanded( + flex: 1, + child: Container( + height: 40.h, + width: 40.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.textColor, + borderRadius: 10.h, + ), + child: Padding( + padding: EdgeInsets.all(10.h), + child: Transform.flip( + flipX: appState.isArabic() ? true : false, + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.whiteColor, + width: 10.h, + height: 10.h, + fit: BoxFit.contain, + ), + ), + ), + ).toShimmer2(isShow: widget.isLoading).onPress(() { + Navigator.of(context) + .push( + CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + ), + ) + .then((val) { + widget.myAppointmentsViewModel.initAppointmentsViewModel(); + widget.myAppointmentsViewModel.getPatientAppointments(true, false); + }); + }), ), - ) - .then((val) { - widget.myAppointmentsViewModel.initAppointmentsViewModel(); - widget.myAppointmentsViewModel.getPatientAppointments(true, false); - }); - }), - ), - ], - ), + ], + ), ], ), ), diff --git a/lib/presentation/insurance/insurance_home_page.dart b/lib/presentation/insurance/insurance_home_page.dart index 41640b7..bc56473 100644 --- a/lib/presentation/insurance/insurance_home_page.dart +++ b/lib/presentation/insurance/insurance_home_page.dart @@ -3,12 +3,17 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/patient_insurance_card.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -30,6 +35,8 @@ class InsuranceHomePage extends StatefulWidget { class _InsuranceHomePageState extends State { late InsuranceViewModel insuranceViewModel; + late AppState appState; + @override void initState() { scheduleMicrotask(() { @@ -40,6 +47,7 @@ class _InsuranceHomePageState extends State { @override Widget build(BuildContext context) { + appState = getIt.get(); insuranceViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, @@ -48,44 +56,55 @@ class _InsuranceHomePageState extends State { history: () { insuranceViewModel.setIsInsuranceHistoryLoading(true); insuranceViewModel.getPatientInsuranceCardHistory(); - showCommonBottomSheet(context, - child: InsuranceHistory(), callBackFunc: (str) {}, title: "", height: ResponsiveExtension.screenHeight * 0.65, isCloseButtonVisible: false, isFullScreen: false); + showCommonBottomSheetWithoutHeight(context, child: InsuranceHistory(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); }, child: SingleChildScrollView( child: Consumer(builder: (context, insuranceVM, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // "${LocaleKeys.insurance.tr(context: context)} ${LocaleKeys.updateInsurance.tr(context: context)}".toText24(isBold: true), - // CustomButton( - // icon: AppAssets.insurance_history_icon, - // iconColor: AppColors.primaryRedColor, - // iconSize: 21.h, - // text: LocaleKeys.history.tr(context: context), - // onPressed: () { - // }, - // backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), - // borderColor: AppColors.primaryRedColor.withOpacity(0.0), - // textColor: AppColors.primaryRedColor, - // fontSize: 14, - // fontWeight: FontWeight.w600, - // borderRadius: 12, - // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - // height: 40.h, - // ), - // ], - // ).paddingSymmetrical(24.h, 24.h), insuranceVM.isInsuranceLoading - ? const MoviesShimmerWidget().paddingSymmetrical(24.h, 0) - : Padding( - padding: EdgeInsets.only(top: 24.h), - child: PatientInsuranceCard( - insuranceCardDetailsModel: insuranceVM.patientInsuranceList.first, - isInsuranceExpired: DateTime.now().isAfter(DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo))), - ), + ? LabResultItemView( + onTap: () {}, + labOrder: null, + index: 0, + isLoading: true, + ).paddingSymmetrical(24.h, 24.h) + : insuranceVM.patientInsuranceList.isNotEmpty + ? Padding( + padding: EdgeInsets.only(top: 24.h), + child: PatientInsuranceCard( + insuranceCardDetailsModel: insuranceVM.patientInsuranceList.first, + isInsuranceExpired: DateTime.now().isAfter(DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo))), + ) + : Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).size.height * 0.12), + child: Utils.getNoDataWidget( + context, + noDataText: "You don't have insurance registered with HMG.".needTranslation, + callToActionButton: CustomButton( + icon: AppAssets.update_insurance_card_icon, + iconColor: AppColors.successColor, + iconSize: 15.h, + text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}", + onPressed: () { + insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true); + insuranceViewModel.getPatientInsuranceDetailsForUpdate( + appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); + showCommonBottomSheetWithoutHeight(context, + child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); + }, + backgroundColor: AppColors.bgGreenColor.withOpacity(0.20), + borderColor: AppColors.bgGreenColor.withOpacity(0.0), + textColor: AppColors.bgGreenColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ).paddingSymmetrical(64.h, 0.h), + ), + ), ], ); }), diff --git a/lib/presentation/insurance/widgets/insurance_history.dart b/lib/presentation/insurance/widgets/insurance_history.dart index de5581c..1c7b1b9 100644 --- a/lib/presentation/insurance/widgets/insurance_history.dart +++ b/lib/presentation/insurance/widgets/insurance_history.dart @@ -8,6 +8,7 @@ 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/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; @@ -25,6 +26,7 @@ class InsuranceHistory extends StatelessWidget { return Consumer(builder: (context, insuranceVM, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -36,72 +38,85 @@ class InsuranceHistory extends StatelessWidget { ], ).paddingSymmetrical(24.h, 24.h), insuranceVM.isInsuranceHistoryLoading - ? const MoviesShimmerWidget().paddingSymmetrical(24.h, 24.h) - : ListView.separated( - itemCount: insuranceVM.patientInsuranceCardHistoryList.length, - shrinkWrap: true, - padding: const EdgeInsets.only(left: 0, right: 8), - itemBuilder: (context, index) { - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: Container( - // height: 120.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - CustomButton( - text: insuranceVM.patientInsuranceCardHistoryList[index].statusDescription!, - onPressed: () {}, - backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), - borderColor: AppColors.primaryRedColor.withOpacity(0.0), - textColor: AppColors.primaryRedColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 30.h, - ), - ], + ? LabResultItemView( + onTap: () {}, + labOrder: null, + index: 0, + isLoading: true, + ).paddingSymmetrical(24.h, 24.h) + : insuranceVM.patientInsuranceCardHistoryList.isNotEmpty + ? ListView.separated( + itemCount: insuranceVM.patientInsuranceCardHistoryList.length, + shrinkWrap: true, + padding: const EdgeInsets.only(left: 0, right: 8), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: Container( + // height: 120.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24, ), - SizedBox(height: 8.h), - // "Haroon Amjad".toText16(weight: FontWeight.w600), - SizedBox(height: 8.h), - Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Wrap( - direction: Axis.horizontal, - spacing: 4.h, - runSpacing: 4.h, + Row( children: [ - AppCustomChipWidget( - labelText: "File No.: ${insuranceVM.patientInsuranceCardHistoryList[index].patientID}", + CustomButton( + text: insuranceVM.patientInsuranceCardHistoryList[index].statusDescription!, + onPressed: () {}, + backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), + borderColor: AppColors.primaryRedColor.withOpacity(0.0), + textColor: AppColors.primaryRedColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 30.h, ), - AppCustomChipWidget( - labelText: insuranceVM.patientInsuranceCardHistoryList[index].createdOn!, + ], + ), + SizedBox(height: 8.h), + // "Haroon Amjad".toText16(weight: FontWeight.w600), + SizedBox(height: 8.h), + Row( + children: [ + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: "File No.: ${insuranceVM.patientInsuranceCardHistoryList[index].patientID}", + ), + AppCustomChipWidget( + labelText: insuranceVM.patientInsuranceCardHistoryList[index].createdOn!, + ), + ], ), ], ), ], - ), - ], - ).paddingSymmetrical(16.h, 16.h), - ).paddingSymmetrical(24.h, 0.h), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ) + : Utils.getNoDataWidget( + context, + noDataText: "No insurance update requests found.".needTranslation, + // isSmallWidget: true, + // width: 62, + // height: 62, + ), ], ); }); diff --git a/lib/presentation/insurance/widgets/insurance_update_details_card.dart b/lib/presentation/insurance/widgets/insurance_update_details_card.dart index aa04d8c..08e252b 100644 --- a/lib/presentation/insurance/widgets/insurance_update_details_card.dart +++ b/lib/presentation/insurance/widgets/insurance_update_details_card.dart @@ -8,6 +8,7 @@ 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/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; @@ -23,6 +24,7 @@ class PatientInsuranceCardUpdateCard extends StatelessWidget { Widget build(BuildContext context) { insuranceViewModel = Provider.of(context); return Column( + mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -34,57 +36,63 @@ class PatientInsuranceCardUpdateCard extends StatelessWidget { ], ).paddingSymmetrical(24.h, 24.h), insuranceViewModel.isInsuranceUpdateDetailsLoading - ? const MoviesShimmerWidget().paddingSymmetrical(24.h, 24.h) - : Container( - // height: 120.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Haroon Amjad".toText16(weight: FontWeight.w600), - "Policy: ${insuranceViewModel.patientInsuranceUpdateResponseModel!.policyNumber}".toText12(isBold: true, color: AppColors.lightGrayColor), - SizedBox(height: 8.h), - Row( - children: [ - insuranceViewModel.patientInsuranceUpdateResponseModel!.companyName!.toText12(isBold: true), - SizedBox( - width: 6.h, - ), - Container( - padding: EdgeInsets.symmetric(horizontal: 6.h, vertical: 3.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.infoColor, - borderRadius: 50, - ), - child: insuranceViewModel.patientInsuranceUpdateResponseModel!.subCategory!.toText8(isBold: true, color: AppColors.whiteColor), - ), - ], + ? LabResultItemView( + onTap: () {}, + labOrder: null, + index: 0, + isLoading: true, + ).paddingSymmetrical(24.h, 24.h) + : insuranceViewModel.patientInsuranceUpdateResponseModel != null + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24, ), - SizedBox(height: 8.h), - Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Wrap( - direction: Axis.horizontal, - spacing: 4.h, - runSpacing: 4.h, + "Haroon Amjad".toText16(weight: FontWeight.w600), + "Policy: ${insuranceViewModel.patientInsuranceUpdateResponseModel!.policyNumber}".toText12(isBold: true, color: AppColors.lightGrayColor), + SizedBox(height: 8.h), + Row( children: [ - AppCustomChipWidget( - icon: AppAssets.doctor_calendar_icon, - labelText: "${LocaleKeys.expiryOn.tr(context: context)} ${insuranceViewModel.patientInsuranceUpdateResponseModel!.effectiveTo}", + insuranceViewModel.patientInsuranceUpdateResponseModel!.companyName!.toText12(isBold: true), + SizedBox( + width: 6.h, + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 6.h, vertical: 3.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.infoColor, + borderRadius: 50, + ), + child: insuranceViewModel.patientInsuranceUpdateResponseModel!.subCategory!.toText8(isBold: true, color: AppColors.whiteColor), ), - AppCustomChipWidget( - labelText: "Member ID: ${insuranceViewModel.patientInsuranceUpdateResponseModel!.memberID!}", + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + icon: AppAssets.doctor_calendar_icon, + labelText: "${LocaleKeys.expiryOn.tr(context: context)} ${insuranceViewModel.patientInsuranceUpdateResponseModel!.effectiveTo}", + ), + AppCustomChipWidget( + labelText: "Member ID: ${insuranceViewModel.patientInsuranceUpdateResponseModel!.memberID!}", + ), + ], ), ], ), ], - ), - ], - ).paddingSymmetrical(16.h, 16.h), - ).paddingSymmetrical(24.h, 0.h), + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h) + : Utils.getNoDataWidget(context, noDataText: "No insurance data found...".needTranslation), SizedBox( height: 24.h, ), @@ -92,9 +100,9 @@ class PatientInsuranceCardUpdateCard extends StatelessWidget { icon: AppAssets.insurance_active_icon, iconColor: AppColors.whiteColor, iconSize: 20.h, - text: "Update Insurance", + text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}", onPressed: () {}, - backgroundColor: AppColors.successColor, + backgroundColor: insuranceViewModel.patientInsuranceUpdateResponseModel != null ? AppColors.successColor : AppColors.lightGrayBGColor, borderColor: AppColors.successColor.withOpacity(0.01), textColor: AppColors.whiteColor, fontSize: 16, @@ -103,9 +111,6 @@ class PatientInsuranceCardUpdateCard extends StatelessWidget { padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 56.h, ).paddingSymmetrical(24.h, 0.h), - SizedBox( - height: 24.h, - ), ], ); } diff --git a/lib/presentation/insurance/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index 06cfd04..b89d3e8 100644 --- a/lib/presentation/insurance/widgets/patient_insurance_card.dart +++ b/lib/presentation/insurance/widgets/patient_insurance_card.dart @@ -94,11 +94,11 @@ class PatientInsuranceCard extends StatelessWidget { insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true); insuranceViewModel.getPatientInsuranceDetailsForUpdate( appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); - showCommonBottomSheet(context, + showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), - callBackFunc: (str) {}, + callBackFunc: () {}, title: "", - height: ResponsiveExtension.screenHeight * 0.42, + // height: ResponsiveExtension.screenHeight * 0.42, isCloseButtonVisible: false, isFullScreen: false); }, diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 28045fb..2a9fcfe 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -28,9 +28,11 @@ import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.da import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; +import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/patient_insurance_card.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/presentation/medical_file/medical_reports_page.dart'; +import 'package:hmg_patient_app_new/presentation/medical_report/medical_reports_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/patient_sickleaves_list_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/vaccine_list_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/widgets/lab_rad_card.dart'; @@ -566,28 +568,28 @@ class _MedicalFilePageState extends State { horizontalOffset: 100.0, child: FadeInAnimation( child: SizedBox( - width: 80.h, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.network( - myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, - width: 64.h, - height: 64.h, - fit: BoxFit.fill, - ).circle(100).toShimmer2(isShow: false, radius: 50.h), - SizedBox(height: 8.h), - Expanded( - child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) - .toString() - .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) - .toShimmer2(isShow: false), + width: 80.h, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + width: 64.h, + height: 64.h, + fit: BoxFit.fill, + ).circle(100).toShimmer2(isShow: false, radius: 50.h), + SizedBox(height: 8.h), + Expanded( + child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: false), + ), + ], ), - ], + ), ), ), - ), - ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any completed visits yet.".needTranslation, isSmallWidget: true, width: 62, height: 62) .paddingSymmetrical(24.h, 0.h); @@ -646,7 +648,12 @@ class _MedicalFilePageState extends State { children: [ Consumer(builder: (context, insuranceVM, child) { return insuranceVM.isInsuranceLoading - ? const MoviesShimmerWidget().paddingSymmetrical(24.h, 0.0) + ? LabResultItemView( + onTap: () {}, + labOrder: null, + index: index, + isLoading: true, + ).paddingSymmetrical(24.h, 0.0) : insuranceVM.patientInsuranceList.isNotEmpty ? PatientInsuranceCard( insuranceCardDetailsModel: insuranceVM.patientInsuranceList.first, @@ -654,7 +661,33 @@ class _MedicalFilePageState extends State { DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), ), ) - : SizedBox.shrink(); + : Utils.getNoDataWidget( + context, + noDataText: "You don't have insurance registered with HMG.".needTranslation, + isSmallWidget: true, + width: 62, + height: 62, + callToActionButton: CustomButton( + icon: AppAssets.update_insurance_card_icon, + iconColor: AppColors.successColor, + iconSize: 15.h, + text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}", + onPressed: () { + insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true); + insuranceViewModel.getPatientInsuranceDetailsForUpdate( + appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); + showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); + }, + backgroundColor: AppColors.bgGreenColor.withOpacity(0.20), + borderColor: AppColors.bgGreenColor.withOpacity(0.0), + textColor: AppColors.bgGreenColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ).paddingSymmetrical(64.h, 0.h), + ); }), SizedBox(height: 10.h), GridView( @@ -718,11 +751,17 @@ class _MedicalFilePageState extends State { patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first, isLoading: false, ).paddingSymmetrical(24.h, 0.0) - : SizedBox.shrink(); + : Utils.getNoDataWidget( + context, + noDataText: "You don't have any sick leaves yet.".needTranslation, + isSmallWidget: true, + width: 62, + height: 62, + ); }), SizedBox(height: 16.h), GridView( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 13, mainAxisSpacing: 13), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 6.5, mainAxisSpacing: 6.5), physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, shrinkWrap: true, diff --git a/lib/presentation/medical_file/medical_reports_page.dart b/lib/presentation/medical_file/medical_reports_page.dart deleted file mode 100644 index 9baf538..0000000 --- a/lib/presentation/medical_file/medical_reports_page.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_staggered_animations/flutter_staggered_animations.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/medical_file/medical_file_view_model.dart'; -import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/presentation/medical_file/widgets/patient_medical_report_card.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; -import 'package:provider/provider.dart'; - -class MedicalReportsPage extends StatefulWidget { - const MedicalReportsPage({super.key}); - - @override - State createState() => _MedicalReportsPageState(); -} - -class _MedicalReportsPageState extends State { - late MedicalFileViewModel medicalFileViewModel; - - @override - Widget build(BuildContext context) { - medicalFileViewModel = Provider.of(context, listen: false); - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: "Medical Reports".needTranslation, - child: SingleChildScrollView( - child: Column( - children: [ - SizedBox(height: 16.h), - CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), - tabs: [ - CustomTabBarModel(null, "Requested".needTranslation), - CustomTabBarModel(null, "Ready".needTranslation), - CustomTabBarModel(null, "Cancelled".needTranslation), - ], - onTabChange: (index) { - medicalFileViewModel.onMedicalReportTabChange(index); - }, - ).paddingSymmetrical(24.h, 0.h), - Consumer(builder: (context, medicalFileVM, child) { - return ListView.separated( - padding: EdgeInsets.only(top: 24.h), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: medicalFileViewModel.isPatientMedicalReportsListLoading ? 3 : medicalFileViewModel.patientMedicalReportList.length, - // medicalFileViewModel.patientMedicalReportList.isNotEmpty - // ? medicalFileViewModel.patientMedicalReportList.length - // : 1, - itemBuilder: (context, index) { - return medicalFileViewModel.isPatientMedicalReportsListLoading - ? PatientMedicalReportCard( - patientMedicalReportResponseModel: PatientMedicalReportResponseModel(), - medicalFileViewModel: medicalFileVM, - isLoading: true, - ).paddingSymmetrical(24.h, 0.h) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: PatientMedicalReportCard( - patientMedicalReportResponseModel: medicalFileVM.patientMedicalReportList[index], - medicalFileViewModel: medicalFileVM, - - isLoading: false, - ), - ).paddingSymmetrical(24.h, 0.h), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ); - }), - SizedBox(height: 24.h), - ], - ), - ), - ), - ); - } -} diff --git a/lib/presentation/medical_report/medical_report_request_page.dart b/lib/presentation/medical_report/medical_report_request_page.dart new file mode 100644 index 0000000..8f891e2 --- /dev/null +++ b/lib/presentation/medical_report/medical_report_request_page.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.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/medical_file/medical_file_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/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:provider/provider.dart'; + +class MedicalReportRequestPage extends StatelessWidget { + MedicalReportRequestPage({super.key}); + + late MedicalFileViewModel medicalFileViewModel; + + @override + Widget build(BuildContext context) { + medicalFileViewModel = Provider.of(context, listen: false); + return CollapsingListView( + title: "Medical Reports".needTranslation, + isClose: true, + child: Column( + children: [ + ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: medicalFileViewModel.patientMedicalReportAppointmentHistoryList.length, + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: medicalFileViewModel.patientMedicalReportAppointmentHistoryList[index], + myAppointmentsViewModel: Provider.of(context, listen: false), + medicalFileViewModel: medicalFileViewModel, + isLoading: false, + isFromHomePage: false, + isFromMedicalReport: true, + ), + ).paddingSymmetrical(24.h, 0.h), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart new file mode 100644 index 0000000..c08c750 --- /dev/null +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -0,0 +1,176 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/medical_report/medical_report_request_page.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/presentation/medical_report/widgets/patient_medical_report_card.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/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class MedicalReportsPage extends StatefulWidget { + const MedicalReportsPage({super.key}); + + @override + State createState() => _MedicalReportsPageState(); +} + +class _MedicalReportsPageState extends State { + late MedicalFileViewModel medicalFileViewModel; + + @override + Widget build(BuildContext context) { + medicalFileViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Medical Reports".needTranslation, + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 16.h), + CustomTabBar( + activeTextColor: Color(0xffED1C2B), + activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + tabs: [ + CustomTabBarModel(null, "Requested".needTranslation), + CustomTabBarModel(null, "Ready".needTranslation), + CustomTabBarModel(null, "Cancelled".needTranslation), + ], + onTabChange: (index) { + medicalFileViewModel.onMedicalReportTabChange(index); + }, + ).paddingSymmetrical(24.h, 0.h), + Consumer(builder: (context, medicalFileVM, child) { + return ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: medicalFileViewModel.isPatientMedicalReportsListLoading + ? 3 + : medicalFileViewModel.patientMedicalReportList.isNotEmpty + ? medicalFileViewModel.patientMedicalReportList.length + : 1, + itemBuilder: (context, index) { + return medicalFileViewModel.isPatientMedicalReportsListLoading + ? PatientMedicalReportCard( + patientMedicalReportResponseModel: PatientMedicalReportResponseModel(), + medicalFileViewModel: medicalFileVM, + isLoading: true, + ).paddingSymmetrical(24.h, 0.h) + : medicalFileViewModel.patientMedicalReportList.isNotEmpty + ? AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: PatientMedicalReportCard( + patientMedicalReportResponseModel: medicalFileVM.patientMedicalReportList[index], + medicalFileViewModel: medicalFileVM, + isLoading: false, + ), + ).paddingSymmetrical(24.h, 0.h), + ), + ), + ) + : Utils.getNoDataWidget(context, noDataText: "You don't have any medical reports yet.".needTranslation).paddingSymmetrical(24.h, 0.h); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ); + }), + SizedBox(height: 24.h), + ], + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: CustomButton( + text: "Request medical report".needTranslation, + onPressed: () async { + LoaderBottomSheet.showLoader(); + await medicalFileViewModel.getPatientMedicalReportAppointmentsList(onSuccess: (val) async { + LoaderBottomSheet.hideLoader(); + bool? value = await Navigator.of(context).push( + CustomPageRoute( + page: MedicalReportRequestPage(), + fullScreenDialog: true, + direction: AxisDirection.down, + ), + ); + if (value != null) { + showConfirmRequestMedicalReportBottomSheet(); + } + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "You do not have any appointments to request a medical report.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 45.h, + icon: AppAssets.requests, + iconColor: AppColors.whiteColor, + iconSize: 20.h, + ).paddingSymmetrical(24.h, 24.h), + ), + ], + ), + ); + } + + showConfirmRequestMedicalReportBottomSheet() { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to request a medical report for this appointment?".needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + Navigator.pop(context); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } +} diff --git a/lib/presentation/medical_file/widgets/patient_medical_report_card.dart b/lib/presentation/medical_report/widgets/patient_medical_report_card.dart similarity index 100% rename from lib/presentation/medical_file/widgets/patient_medical_report_card.dart rename to lib/presentation/medical_report/widgets/patient_medical_report_card.dart diff --git a/pubspec.yaml b/pubspec.yaml index 3bdaa4a..72e75f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,7 +55,7 @@ dependencies: uuid: ^4.5.1 health: ^13.1.3 # health: 12.0.1 - fl_chart: ^1.0.0 + fl_chart: 1.0.0 geolocator: ^14.0.2 dropdown_search: ^6.0.2 google_maps_flutter: ^2.12.3 From 093e645f6009184af50315aec3259fa03f1a9264 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 30 Sep 2025 11:57:37 +0300 Subject: [PATCH 14/43] request medical report implementation completed. --- lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 2 +- .../medical_file/medical_file_repo.dart | 55 ++++++- .../medical_file/medical_file_view_model.dart | 23 ++- .../widgets/appointment_card.dart | 8 +- .../medical_file/medical_file_page.dart | 134 +++++++++++------- .../widgets/medical_file_card.dart | 4 +- .../widgets/patient_sick_leave_card.dart | 8 +- .../medical_report_request_page.dart | 1 + .../medical_report/medical_reports_page.dart | 110 +++++++++++--- lib/theme/colors.dart | 2 +- lib/widgets/buttons/custom_button.dart | 2 +- lib/widgets/chip/app_custom_chip_widget.dart | 4 +- lib/widgets/input_widget.dart | 4 +- 14 files changed, 263 insertions(+), 96 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 29b41fd..5d4da2b 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -177,7 +177,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1018977; + // body['PatientID'] = 4770714; } body.removeWhere((key, value) => value == null); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index fa02b2e..1c6cec1 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -119,7 +119,7 @@ var GET_STATUS_FOR_COCO = 'Services/COCWS.svc/REST/GetStatusforCOC'; // var GET_PATIENT_AppointmentHistory = 'Services' // '/Doctors.svc/REST/PateintHasAppoimentHistory'; -var GET_PATIENT_AppointmentHistory = 'Services' +var GET_PATIENT_APPOINTMENT_HISTORY_ASYNC = 'Services' '/Doctors.svc/REST/PateintHasAppoimentHistory_Async'; ///VITAL SIGN diff --git a/lib/features/medical_file/medical_file_repo.dart b/lib/features/medical_file/medical_file_repo.dart index 658d045..7077fd0 100644 --- a/lib/features/medical_file/medical_file_repo.dart +++ b/lib/features/medical_file/medical_file_repo.dart @@ -30,6 +30,8 @@ abstract class MedicalFileRepo { Future>>> addFamilyFile({required dynamic request}); Future>>> getPatientAppointmentsForMedicalReport(); + + Future>> insertRequestForMedicalReport({required PatientAppointmentHistoryResponseModel appointmentHistoryResponseModel}); } class MedicalFileRepoImp implements MedicalFileRepo { @@ -366,7 +368,7 @@ class MedicalFileRepoImp implements MedicalFileRepo { GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( - GET_PATIENT_APPOINTMENT_HISTORY, + GET_PATIENT_APPOINTMENT_HISTORY_ASYNC, body: mapDevice, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; @@ -374,9 +376,6 @@ class MedicalFileRepoImp implements MedicalFileRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { final list = response['AppoimentAllHistoryResultList']; - // if (list == null || list.isEmpty) { - // throw Exception("Appointments list is empty"); - // } final appointmentsList = list.map((item) => PatientAppointmentHistoryResponseModel.fromJson(item as Map)).toList().cast(); @@ -398,4 +397,52 @@ class MedicalFileRepoImp implements MedicalFileRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> insertRequestForMedicalReport({required PatientAppointmentHistoryResponseModel appointmentHistoryResponseModel}) async { + Map mapDevice = { + "ClinicID": appointmentHistoryResponseModel.clinicID, + "DoctorID": appointmentHistoryResponseModel.doctorID, + "SetupID": appointmentHistoryResponseModel.setupID, + "EncounterNo": appointmentHistoryResponseModel.appointmentNo, + "EncounterType": 1, + "IsActive": appointmentHistoryResponseModel.isActiveDoctor, + "ProjectID": appointmentHistoryResponseModel.projectID, + "Remarks": "", + "ProcedureId": "", + "RequestType": 1, + "Source": 2, + "Status": 1, + "CreatedBy": 102 + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + INSERT_REQUEST_FOR_MEDICAL_REPORT, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } 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/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 061392e..95c5d63 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/utils/request_utils.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; @@ -54,6 +53,7 @@ class MedicalFileViewModel extends ChangeNotifier { void onMedicalReportTabChange(int index) { selectedMedicalReportsTabIndex = index; + print("Selected Medical Report Tab Index: $selectedMedicalReportsTabIndex"); if (index == 0) { patientMedicalReportList = patientMedicalReportRequestedList; } else if (index == 1) { @@ -85,6 +85,7 @@ class MedicalFileViewModel extends ChangeNotifier { setIsPatientMedicalReportsLoading(bool val) { if (val) { + onMedicalReportTabChange(0); patientMedicalReportList.clear(); patientMedicalReportPDFBase64 = ""; } @@ -327,6 +328,26 @@ class MedicalFileViewModel extends ChangeNotifier { ); } + Future insertRequestForMedicalReport({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await medicalFileRepo.insertRequestForMedicalReport(appointmentHistoryResponseModel: patientMedicalReportSelectedAppointment!); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + Future addFamilyFile() async { final resultEither = await medicalFileRepo.addFamilyFile(request: {}); resultEither.fold((failure) async => await errorHandlerService.handleError(failure: failure), (apiResponse) async {}); diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index d287795..6d157a5 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -167,9 +167,11 @@ class _AppointmentCardState extends State { labelText: widget.isLoading ? "Cardiology" : DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)) .toShimmer2(isShow: widget.isLoading), - AppCustomChipWidget( - icon: AppAssets.appointment_time_icon, - labelText: widget.isLoading + widget.isFromMedicalReport + ? SizedBox.shrink() + : AppCustomChipWidget( + icon: AppAssets.appointment_time_icon, + labelText: widget.isLoading ? "Cardiology" : DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)) .toShimmer2(isShow: widget.isLoading), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 2a9fcfe..7fd9138 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -38,8 +38,6 @@ import 'package:hmg_patient_app_new/presentation/medical_file/vaccine_list_page. import 'package:hmg_patient_app_new/presentation/medical_file/widgets/lab_rad_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/widgets/medical_file_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/widgets/patient_sick_leave_card.dart'; -import 'package:hmg_patient_app_new/presentation/my_family/my_Family.dart'; -import 'package:hmg_patient_app_new/presentation/my_family/widget/my_family_sheet.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -51,7 +49,6 @@ import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import '../prescriptions/prescription_detail_page.dart'; @@ -502,7 +499,14 @@ class _MedicalFilePageState extends State { ), ), ).paddingSymmetrical(24.h, 0.h) - : Utils.getNoDataWidget(context, noDataText: "You don't have any prescriptions yet.".needTranslation, isSmallWidget: true, width: 62, height: 62).paddingSymmetrical(24.h, 0.h); + : Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Utils.getNoDataWidget(context, noDataText: "You don't have any prescriptions yet.".needTranslation, isSmallWidget: true, width: 62, height: 62)) + .paddingSymmetrical(24.h, 0.h); }), SizedBox(height: 24.h), //My Doctor Section @@ -551,7 +555,7 @@ class _MedicalFilePageState extends State { "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 64.h, height: 64.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: true, radius: 50.h), SizedBox(height: 8.h), Expanded( @@ -591,8 +595,14 @@ class _MedicalFilePageState extends State { ), ), ) - : Utils.getNoDataWidget(context, noDataText: "You don't have any completed visits yet.".needTranslation, isSmallWidget: true, width: 62, height: 62) - .paddingSymmetrical(24.h, 0.h); + : Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Utils.getNoDataWidget(context, noDataText: "You don't have any completed visits yet.".needTranslation, isSmallWidget: true, width: 62, height: 62), + ).paddingSymmetrical(24.h, 0.h); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), ), @@ -602,7 +612,12 @@ class _MedicalFilePageState extends State { "Others".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), SizedBox(height: 16.h), GridView( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 13, mainAxisSpacing: 13), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: 130, + ), physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, shrinkWrap: true, @@ -613,15 +628,15 @@ class _MedicalFilePageState extends State { backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon, isLargeText: true, - iconSize: 40.h, + iconSize: 36.h, ), MedicalFileCard( - label: "Allergy Info".needTranslation, + label: "Allergy Info".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.allergy_info_icon, isLargeText: true, - iconSize: 40.h, + iconSize: 36.h, ), MedicalFileCard( label: "Vaccine Info".needTranslation, @@ -629,7 +644,7 @@ class _MedicalFilePageState extends State { backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.vaccine_info_icon, isLargeText: true, - iconSize: 40.h, + iconSize: 36.h, ).onPress(() { Navigator.of(context).push( CustomPageRoute( @@ -661,37 +676,49 @@ class _MedicalFilePageState extends State { DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), ), ) - : Utils.getNoDataWidget( - context, - noDataText: "You don't have insurance registered with HMG.".needTranslation, - isSmallWidget: true, - width: 62, - height: 62, - callToActionButton: CustomButton( - icon: AppAssets.update_insurance_card_icon, - iconColor: AppColors.successColor, - iconSize: 15.h, - text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}", - onPressed: () { - insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true); - insuranceViewModel.getPatientInsuranceDetailsForUpdate( - appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); - showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); - }, - backgroundColor: AppColors.bgGreenColor.withOpacity(0.20), - borderColor: AppColors.bgGreenColor.withOpacity(0.0), - textColor: AppColors.bgGreenColor, - fontSize: 14, - fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ).paddingSymmetrical(64.h, 0.h), - ); + : Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Utils.getNoDataWidget( + context, + noDataText: "You don't have insurance registered with HMG.".needTranslation, + isSmallWidget: true, + width: 62, + height: 62, + callToActionButton: CustomButton( + icon: AppAssets.update_insurance_card_icon, + iconColor: AppColors.successColor, + iconSize: 15.h, + text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}", + onPressed: () { + insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true); + insuranceViewModel.getPatientInsuranceDetailsForUpdate( + appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); + showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); + }, + backgroundColor: AppColors.bgGreenColor.withOpacity(0.20), + borderColor: AppColors.bgGreenColor.withOpacity(0.0), + textColor: AppColors.bgGreenColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ).paddingOnly(left: 12.h, right: 12.h, bottom: 12.h), + ), + ).paddingSymmetrical(24.h, 0.h); }), SizedBox(height: 10.h), GridView( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 13, mainAxisSpacing: 13), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: 140, + ), physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.only(top: 12), shrinkWrap: true, @@ -701,7 +728,7 @@ class _MedicalFilePageState extends State { textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon, - isLargeText: false, + isLargeText: true, iconSize: 36.h) .onPress(() { Navigator.of(context).push( @@ -715,21 +742,21 @@ class _MedicalFilePageState extends State { textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon, - isLargeText: false, + isLargeText: true, iconSize: 36.h), MedicalFileCard( label: "My Invoices List".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon, - isLargeText: false, + isLargeText: true, iconSize: 36.h), MedicalFileCard( label: "Ancillary Orders List".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon, - isLargeText: false, + isLargeText: true, iconSize: 36.h), ], ).paddingSymmetrical(24.h, 0.0), @@ -761,7 +788,12 @@ class _MedicalFilePageState extends State { }), SizedBox(height: 16.h), GridView( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 6.5, mainAxisSpacing: 6.5), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: 140, + ), physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, shrinkWrap: true, @@ -771,16 +803,16 @@ class _MedicalFilePageState extends State { textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon, - isLargeText: false, - iconSize: 40.h, + isLargeText: true, + iconSize: 36.h, ), MedicalFileCard( label: "Medical Reports".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.allergy_info_icon, - isLargeText: false, - iconSize: 40.h, + isLargeText: true, + iconSize: 36.h, ).onPress(() { medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.getPatientMedicalReportList(); @@ -795,8 +827,8 @@ class _MedicalFilePageState extends State { textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.vaccine_info_icon, - isLargeText: false, - iconSize: 40.h, + isLargeText: true, + iconSize: 36.h, ).onPress(() { Navigator.of(context).push( CustomPageRoute( diff --git a/lib/presentation/medical_file/widgets/medical_file_card.dart b/lib/presentation/medical_file/widgets/medical_file_card.dart index c4980d9..e8cff72 100644 --- a/lib/presentation/medical_file/widgets/medical_file_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_card.dart @@ -29,14 +29,14 @@ class MedicalFileCard extends StatelessWidget { borderRadius: 20, ), child: Padding( - padding: EdgeInsets.all(8.h), + padding: EdgeInsets.all(12.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Utils.buildSvgWithAssets(icon: svgIcon, width: iconSize.h, height: iconSize.h, fit: BoxFit.contain), SizedBox(height: 12.h), - isLargeText ? label.toText14(color: textColor, isBold: true, maxlines: 1) : label.toText11(color: textColor, isBold: true, maxLine: 2), + isLargeText ? label.toText13(color: textColor, isBold: true, maxLine: 2) : label.toText11(color: textColor, isBold: true, maxLine: 2), ], ), ), diff --git a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart index 5cd2547..fed7d15 100644 --- a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart +++ b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart @@ -59,7 +59,7 @@ class PatientSickLeaveCard extends StatelessWidget { isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientSickLeavesResponseModel.doctorImageURL!, width: 30.h, height: 30.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: isLoading), SizedBox(width: 16.h), Expanded( @@ -182,11 +182,11 @@ class PatientSickLeaveCard extends StatelessWidget { Color getStatusColor() { Color statusColor = Colors.white; if (patientSickLeavesResponseModel.status == 1) { - statusColor = Color(0xffCC9B14); + statusColor = Color(0xffCC9B14); // TODO change color as per In Queue design } else if (patientSickLeavesResponseModel.status == 2) { - statusColor = Color(0xff359846); + statusColor = AppColors.successColor; } else if (patientSickLeavesResponseModel.status == 3) { - statusColor = Color(0xffD02127); + statusColor = AppColors.primaryRedColor; } else { statusColor = Colors.white; } diff --git a/lib/presentation/medical_report/medical_report_request_page.dart b/lib/presentation/medical_report/medical_report_request_page.dart index 8f891e2..9d1bcb1 100644 --- a/lib/presentation/medical_report/medical_report_request_page.dart +++ b/lib/presentation/medical_report/medical_report_request_page.dart @@ -54,6 +54,7 @@ class MedicalReportRequestPage extends StatelessWidget { }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ), + SizedBox(height: 24.h), ], ), ); diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index c08c750..71abcb7 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -41,23 +41,71 @@ class _MedicalReportsPageState extends State { child: CollapsingListView( title: "Medical Reports".needTranslation, child: SingleChildScrollView( - child: Column( - children: [ - SizedBox(height: 16.h), - CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), - tabs: [ - CustomTabBarModel(null, "Requested".needTranslation), - CustomTabBarModel(null, "Ready".needTranslation), - CustomTabBarModel(null, "Cancelled".needTranslation), - ], - onTabChange: (index) { - medicalFileViewModel.onMedicalReportTabChange(index); - }, - ).paddingSymmetrical(24.h, 0.h), - Consumer(builder: (context, medicalFileVM, child) { - return ListView.separated( + child: Consumer(builder: (context, medicalFileVM, child) { + return Column( + children: [ + SizedBox(height: 16.h), + Row( + children: [ + CustomButton( + text: "Requested".needTranslation, + onPressed: () { + medicalFileViewModel.onMedicalReportTabChange(0); + }, + backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + textColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.ready.tr(context: context), + onPressed: () { + medicalFileViewModel.onMedicalReportTabChange(1); + }, + backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + textColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.cancelled.tr(context: context), + onPressed: () { + medicalFileViewModel.onMedicalReportTabChange(2); + }, + backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + textColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ).paddingSymmetrical(24.h, 0.h), + // CustomTabBar( + // activeTextColor: Color(0xffED1C2B), + // activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + // tabs: [ + // CustomTabBarModel(null, "Requested".needTranslation), + // CustomTabBarModel(null, "Ready".needTranslation), + // CustomTabBarModel(null, "Cancelled".needTranslation), + // ], + // onTabChange: (index) { + // medicalFileViewModel.onMedicalReportTabChange(index); + // }, + // ).paddingSymmetrical(24.h, 0.h), + ListView.separated( padding: EdgeInsets.only(top: 24.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), @@ -93,14 +141,14 @@ class _MedicalReportsPageState extends State { ), ), ) - : Utils.getNoDataWidget(context, noDataText: "You don't have any medical reports yet.".needTranslation).paddingSymmetrical(24.h, 0.h); + : Utils.getNoDataWidget(context, noDataText: "You don't have any medical reports yet.".needTranslation).paddingSymmetrical(24.h, 24.h); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ); - }), - SizedBox(height: 24.h), - ], - ), + ), + SizedBox(height: 24.h), + ], + ); + }), ), ), ), @@ -167,6 +215,22 @@ class _MedicalReportsPageState extends State { }, onConfirmTap: () async { Navigator.pop(context); + LoaderBottomSheet.showLoader(); + await medicalFileViewModel.insertRequestForMedicalReport(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: "Your medical report request has been successfully submitted.".needTranslation), callBackFunc: () { + medicalFileViewModel.setIsPatientMedicalReportsLoading(true); + medicalFileViewModel.onMedicalReportTabChange(0); + medicalFileViewModel.getPatientMedicalReportList(); + }); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight(context, child: Utils.getErrorWidget(loadingText: err), callBackFunc: () { + medicalFileViewModel.setIsPatientMedicalReportsLoading(true); + medicalFileViewModel.onMedicalReportTabChange(0); + medicalFileViewModel.getPatientMedicalReportList(); + }); + }); }), callBackFunc: () {}, isFullScreen: false, diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 4015249..291f81d 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -32,7 +32,7 @@ class AppColors { static const Color textColorLight = Color(0xFF5E5E5E); static const Color borderOnlyColor = Color(0xFF2E3039); static const Color chipBorderColorOpacity20 = Color(0x332E3039); - static const Color dividerColor = Color(0xFFD2D2D2); + static const Color dividerColor = Color(0x40D2D2D2); static const Color warningColorYellow = Color(0xFFF4A308); static const Color blackBgColor = Color(0xFF2E3039); static const blackColor = textColor; diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index 2f8a9ec..a4bec4c 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -78,7 +78,7 @@ class CustomButton extends StatelessWidget { style: context.dynamicTextStyle( fontSize: fontSize.fSize, color: isDisabled ? textColor.withOpacity(0.5) : textColor, - letterSpacing: -0.4, + letterSpacing: 0, fontWeight: fontWeight, ), ), diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index a4db172..16db798 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -58,7 +58,7 @@ class AppCustomChipWidget extends StatelessWidget { child: icon.isNotEmpty ? Chip( avatar: icon.isNotEmpty ? Utils.buildSvgWithAssets(icon: icon, width: iconSize.h, height: iconSize.h, iconColor: iconHasColor ? iconColor : null) : SizedBox.shrink(), - label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: -0.64, color: textColor), + label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), // padding: EdgeInsets.all(0.0), padding: padding, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -77,7 +77,7 @@ class AppCustomChipWidget extends StatelessWidget { ) : Chip( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: -0.64, color: textColor), + label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), padding: EdgeInsets.all(0.0), backgroundColor: backgroundColor, shape: shape ?? SmoothRectangleBorder( diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 4a2322c..38e5829 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -217,7 +217,7 @@ class TextInputWidget extends StatelessWidget { fontSize: 12.fSize, fontWeight: FontWeight.w500, color: labelColor ?? AppColors.inputLabelTextColor, - letterSpacing: -0.2, + letterSpacing: -0, height: 18 / 12, ), ); @@ -246,7 +246,7 @@ class TextInputWidget extends StatelessWidget { decoration: InputDecoration( isDense: true, hintText: hintText, - hintStyle: TextStyle(fontSize: 14.fSize, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -1), + hintStyle: TextStyle(fontSize: 14.fSize, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.75), prefixIconConstraints: BoxConstraints(minWidth: 30.h), prefixIcon: prefix == null ? null : "+${prefix!}".toText14(letterSpacing: -1, color: AppColors.textColor, weight: FontWeight.w500), contentPadding: EdgeInsets.zero, From 2f4d6f5553f976f3d5ad10fa2ec8a49229a2a512 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 30 Sep 2025 17:02:05 +0300 Subject: [PATCH 15/43] immediate livecare consultation implementation contd. --- assets/animations/lottie/Ripple.json | 1 + assets/images/svg/livecare_online_icon.svg | 4 + lib/core/app_assets.dart | 2 + .../book_appointments_repo.dart | 46 +++++ .../book_appointments_view_model.dart | 43 ++++- ...care_immediate_clinics_response_model.dart | 97 +++++++++++ .../book_appointment_page.dart | 16 +- ...select_immediate_livecare_clinic_page.dart | 158 ++++++++++++++++++ .../widgets/select_livecare_call_type.dart | 65 +++++++ .../widgets/livecare_clinic_card.dart | 66 ++++++++ 10 files changed, 489 insertions(+), 9 deletions(-) create mode 100644 assets/animations/lottie/Ripple.json create mode 100644 assets/images/svg/livecare_online_icon.svg create mode 100644 lib/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart create mode 100644 lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart create mode 100644 lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart create mode 100644 lib/presentation/book_appointment/widgets/livecare_clinic_card.dart diff --git a/assets/animations/lottie/Ripple.json b/assets/animations/lottie/Ripple.json new file mode 100644 index 0000000..058b4ca --- /dev/null +++ b/assets/animations/lottie/Ripple.json @@ -0,0 +1 @@ +{"nm":"Comp 1","ddd":0,"h":100,"w":100,"meta":{"g":"@lottiefiles/toolkit-js 0.33.2"},"layers":[{"ty":4,"nm":"Shape Layer 2","sr":1,"st":0,"op":300,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[5.277,-32.723,0],"ix":1},"s":{"a":0,"k":[4.91,4.91,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[50.2,50.18,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 2","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[102.555,102.555],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.0941,0.7608,0.451],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[3.277,-34.527],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":2,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[102.555,102.555],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.0941,0.7608,0.451],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100,100],"t":0},{"s":[295,295],"t":60}],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[3.277,-34.527],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":0},{"s":[0],"t":60}],"ix":7}}]}],"ind":1}],"v":"5.5.7","fr":60,"op":61,"ip":0,"assets":[]} \ No newline at end of file diff --git a/assets/images/svg/livecare_online_icon.svg b/assets/images/svg/livecare_online_icon.svg new file mode 100644 index 0000000..e063de8 --- /dev/null +++ b/assets/images/svg/livecare_online_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 31b4f32..446bbc2 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -145,6 +145,7 @@ class AppAssets { static const String ic_normal_result = '$svgBasePath/normal_result.svg'; static const String ic_low_result = '$svgBasePath/low_result.svg'; static const String ic_critical_low_result = '$svgBasePath/critical_low_result.svg'; + static const String livecare_online_icon = '$svgBasePath/livecare_online_icon.svg'; //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; @@ -184,4 +185,5 @@ class AppAnimations { static const String warningAnimation = '$lottieBasePath/warningAnimation.json'; static const String splashLaunching = '$lottieBasePath/splash_launching.json'; static const String noData = '$lottieBasePath/Nodata.json'; + static const String ripple = '$lottieBasePath/Ripple.json'; } diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index 5e99585..18b5330 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -8,6 +8,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/resp_model import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_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/services/logger_service.dart'; @@ -66,6 +67,8 @@ abstract class BookAppointmentsRepo { required int serviceID, Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>>> getLiveCareImmediateClinicsList(int age, int genderID, {Function(dynamic)? onSuccess, Function(String)? onError}); } class BookAppointmentsRepoImp implements BookAppointmentsRepo { @@ -657,4 +660,47 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> getLiveCareImmediateClinicsList(int age, int genderID, + {Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = { + "Age": age, + "Gender": genderID, + }; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_LIVECARE_CLINICS, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['PatientER_GetClinicsList']; + + final clinicsList = list.map((item) => GetLiveCareClinicListResponseModel.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 5ef653a..ab4ca56 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -15,6 +15,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/free_slot. import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/timeslots.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; @@ -60,6 +61,11 @@ class BookAppointmentsViewModel extends ChangeNotifier { DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel(); GetLiveCareClinicsResponseModel selectedLiveCareClinic = GetLiveCareClinicsResponseModel(); + //Immediate LiveCare + List immediateLiveCareClinicsList = []; + bool isImmediateLiveCareClinicsLoading = false; + int liveCareSelectedCallType = 1; + late DoctorsProfileResponseModel doctorsProfileResponseModel; List slotsList = []; @@ -89,7 +95,6 @@ class BookAppointmentsViewModel extends ChangeNotifier { BookAppointmentsViewModel( {required this.bookAppointmentsRepo, required this.errorHandlerService, required this.navigationService, required this.myAppointmentsViewModel, required this.locationUtils}) { - ; initBookAppointmentViewModel(); } @@ -117,6 +122,9 @@ class BookAppointmentsViewModel extends ChangeNotifier { clinicsList.clear(); doctorsList.clear(); liveCareClinicsList.clear(); + + immediateLiveCareClinicsList.clear(); + isImmediateLiveCareClinicsLoading = true; // getLocation(); notifyListeners(); } @@ -183,6 +191,12 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + + setLiveCareSelectedCallType(int value) { + liveCareSelectedCallType = value; + notifyListeners(); + } + /// this function will decide which clinic api to be called /// either api for region flow or the select clinic api Future getClinics() async { @@ -757,4 +771,31 @@ class BookAppointmentsViewModel extends ChangeNotifier { void getLocation() { locationUtils.getLocation(); } + + Future getLiveCareImmediateClinicsList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + immediateLiveCareClinicsList.clear(); + isImmediateLiveCareClinicsLoading = true; + notifyListeners(); + + final result = await bookAppointmentsRepo.getLiveCareImmediateClinicsList(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!); + + 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) { + immediateLiveCareClinicsList = apiResponse.data!; + + immediateLiveCareClinicsList.sort((a, b) => b.isOnline!.compareTo(a.isOnline!)); + + isImmediateLiveCareClinicsLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } } diff --git a/lib/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart b/lib/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart new file mode 100644 index 0000000..5c5b900 --- /dev/null +++ b/lib/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart @@ -0,0 +1,97 @@ +class GetLiveCareClinicListResponseModel { + int? iD; + int? serviceID; + String? serviceName; + String? serviceNameN; + int? clinicID; + int? age; + bool? isCheckAgeBelow; + int? gender; + bool? isActive; + String? createdOn; + String? createdBy; + int? isOnline; + bool? projectOutSA; + List? shiftTimings; + + GetLiveCareClinicListResponseModel( + {this.iD, + this.serviceID, + this.serviceName, + this.serviceNameN, + this.clinicID, + this.age, + this.isCheckAgeBelow, + this.gender, + this.isActive, + this.createdOn, + this.createdBy, + this.isOnline, + this.projectOutSA, + this.shiftTimings}); + + GetLiveCareClinicListResponseModel.fromJson(Map json) { + iD = json['ID']; + serviceID = json['ServiceID']; + serviceName = json['ServiceName']; + serviceNameN = json['ServiceNameN']; + clinicID = json['ClinicID']; + age = json['Age']; + isCheckAgeBelow = json['IsCheckAgeBelow']; + gender = json['Gender']; + isActive = json['IsActive']; + createdOn = json['CreatedOn']; + createdBy = json['CreatedBy']; + isOnline = json['IsOnline']; + projectOutSA = json['ProjectOutSA']; + if (json['ShiftTimings'] != null) { + shiftTimings = []; + json['ShiftTimings'].forEach((v) { + shiftTimings!.add(new ShiftTimings.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = Map(); + data['ID'] = this.iD; + data['ServiceID'] = this.serviceID; + data['ServiceName'] = this.serviceName; + data['ServiceNameN'] = this.serviceNameN; + data['ClinicID'] = this.clinicID; + data['Age'] = this.age; + data['IsCheckAgeBelow'] = this.isCheckAgeBelow; + data['Gender'] = this.gender; + data['IsActive'] = this.isActive; + data['CreatedOn'] = this.createdOn; + data['CreatedBy'] = this.createdBy; + data['IsOnline'] = this.isOnline; + data['ProjectOutSA'] = this.projectOutSA; + if (this.shiftTimings != null) { + data['ShiftTimings'] = this.shiftTimings!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class ShiftTimings { + String? endTime; + int? shiftID; + String? startTime; + + ShiftTimings({this.endTime, this.shiftID, this.startTime}); + + ShiftTimings.fromJson(Map json) { + endTime = json['EndTime']; + shiftID = json['ShiftID']; + startTime = json['StartTime']; + } + + Map toJson() { + final Map data = Map(); + data['EndTime'] = this.endTime; + data['ShiftID'] = this.shiftID; + data['StartTime'] = this.startTime; + return data; + } +} diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 6ba14f5..012a8db 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -15,6 +15,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart' show RegionBottomSheetBody; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/search_doctor_by_name.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; @@ -230,14 +231,13 @@ class _BookAppointmentPageState extends State { flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h)), ], ).onPress(() { - // bookAppointmentsViewModel.setIsClinicsListLoading(true); - // bookAppointmentsViewModel.setLoadSpecificClinic(false); - // bookAppointmentsViewModel.setProjectID(null); - // Navigator.of(context).push( - // CustomPageRoute( - // page: SelectClinicPage(), - // ), - // ); + //TODO Implement API to check for existing LiveCare Requests + + Navigator.of(context).push( + CustomPageRoute( + page: SelectImmediateLiveCareClinicPage(), + ), + ); }), SizedBox(height: 16.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), diff --git a/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart b/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart new file mode 100644 index 0000000..8ec6e4b --- /dev/null +++ b/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart @@ -0,0 +1,158 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/clinic_card.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/livecare_clinic_card.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/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +class SelectImmediateLiveCareClinicPage extends StatefulWidget { + const SelectImmediateLiveCareClinicPage({super.key}); + + @override + State createState() => _SelectImmediateLiveCareClinicPageState(); +} + +class _SelectImmediateLiveCareClinicPageState extends State { + TextEditingController searchEditingController = TextEditingController(); + FocusNode textFocusNode = FocusNode(); + late AppState appState; + late BookAppointmentsViewModel bookAppointmentsViewModel; + + @override + void initState() { + scheduleMicrotask(() { + bookAppointmentsViewModel.getLiveCareImmediateClinicsList(); + }); + super.initState(); + } + + @override + void dispose() { + textFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + bookAppointmentsViewModel = Provider.of(context, listen: false); + appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "Select LiveCare Clinic".needTranslation, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Consumer(builder: (context, bookAppointmentsVM, child) { + return Column( + children: [ + // SizedBox(height: 16.h), + // TextInputWidget( + // labelText: LocaleKeys.search.tr(context: context), + // hintText: LocaleKeys.clinicName.tr(context: context), + // controller: searchEditingController, + // isEnable: true, + // prefix: null, + // autoFocus: false, + // isBorderAllowed: false, + // keyboardType: TextInputType.text, + // focusNode: textFocusNode, + // suffix: searchEditingController.text.isNotEmpty + // ? GestureDetector( + // onTap: () { + // searchEditingController.clear(); + // bookAppointmentsViewModel.filterClinics(""); + // textFocusNode.unfocus(); + // }, + // child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), + // ) + // : null, + // onChange: (value) { + // bookAppointmentsViewModel.filterClinics(value!); + // }, + // padding: EdgeInsets.symmetric( + // vertical: ResponsiveExtension(10).h, + // horizontal: ResponsiveExtension(15).h, + // ), + // ), + ListView.separated( + padding: EdgeInsets.only(top: 16.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: bookAppointmentsVM.isImmediateLiveCareClinicsLoading ? 5 : bookAppointmentsVM.immediateLiveCareClinicsList.length, + itemBuilder: (context, index) { + return bookAppointmentsVM.isImmediateLiveCareClinicsLoading + ? ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isImmediateLiveCareClinicsLoading, + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: LiveCareClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicListResponseModel: bookAppointmentsVM.immediateLiveCareClinicsList[index], + isLoading: bookAppointmentsVM.isImmediateLiveCareClinicsLoading, + ).onPress(() { + onImmediateLiveCareClinicSelected(bookAppointmentsVM.immediateLiveCareClinicsList[index]); + }), + ), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + ], + ); + }), + ), + ), + ), + ); + } + + onImmediateLiveCareClinicSelected(GetLiveCareClinicListResponseModel liveCareClinic) { + //TODO: add implementation to show clinic schedule + if (liveCareClinic.isOnline == 1) { + showCommonBottomSheetWithoutHeight(context, + child: SelectLiveCareCallType(bookAppointmentsViewModel: bookAppointmentsViewModel), + callBackFunc: () {}, + title: "Select LiveCare call type".needTranslation, + isCloseButtonVisible: true, + isFullScreen: false); + } else { + showCommonBottomSheetWithoutHeight(context, + child: Utils.getErrorWidget( + loadingText: "The selected clinic is only available between ${liveCareClinic.shiftTimings!.first.startTime} & ${liveCareClinic.shiftTimings!.first.endTime} hours.".needTranslation), + callBackFunc: () {}, + title: "", + isCloseButtonVisible: true, + isFullScreen: false); + } + } +} diff --git a/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart b/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart new file mode 100644 index 0000000..689dc9b --- /dev/null +++ b/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart @@ -0,0 +1,65 @@ +import 'package:flutter/cupertino.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/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/presentation/medical_file/widgets/medical_file_card.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class SelectLiveCareCallType extends StatelessWidget { + SelectLiveCareCallType({super.key, required this.bookAppointmentsViewModel}); + + BookAppointmentsViewModel bookAppointmentsViewModel; + + @override + Widget build(BuildContext context) { + //TODO: Replace with actual icons + return GridView( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: 130, + ), + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + MedicalFileCard( + label: "Video Call".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.eye_result_icon, + isLargeText: true, + iconSize: 36.h, + ).onPress(() { + Navigator.of(context).pop(); + bookAppointmentsViewModel.setLiveCareSelectedCallType(1); + }), + MedicalFileCard( + label: "Audio Call".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.allergy_info_icon, + isLargeText: true, + iconSize: 36.h, + ).onPress(() { + Navigator.of(context).pop(); + bookAppointmentsViewModel.setLiveCareSelectedCallType(2); + }), + MedicalFileCard( + label: "Phone Call".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.vaccine_info_icon, + isLargeText: true, + iconSize: 36.h, + ).onPress(() { + Navigator.of(context).pop(); + bookAppointmentsViewModel.setLiveCareSelectedCallType(3); + }), + ], + ); + } +} diff --git a/lib/presentation/book_appointment/widgets/livecare_clinic_card.dart b/lib/presentation/book_appointment/widgets/livecare_clinic_card.dart new file mode 100644 index 0000000..1642d60 --- /dev/null +++ b/lib/presentation/book_appointment/widgets/livecare_clinic_card.dart @@ -0,0 +1,66 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class LiveCareClinicCard extends StatelessWidget { + LiveCareClinicCard({super.key, required this.liveCareClinicListResponseModel, required this.isLoading, required this.bookAppointmentsVM}); + + GetLiveCareClinicListResponseModel liveCareClinicListResponseModel; + bool isLoading; + BookAppointmentsViewModel bookAppointmentsVM; + + @override + Widget build(BuildContext context) { + AppState appState = getIt.get(); + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Column( + children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Utils.buildSvgWithAssets(icon: AppAssets.generic_clinic_icon, width: 24.h, height: 24.h, fit: BoxFit.contain).toShimmer2(isShow: isLoading), + Column( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.livecare_online_icon, + width: 16.h, + height: 16.h, + fit: BoxFit.contain, + iconColor: liveCareClinicListResponseModel.isOnline == 1 ? AppColors.successColor : AppColors.primaryRedColor) + .toShimmer2(isShow: isLoading), + SizedBox(height: 4.h), + liveCareClinicListResponseModel.isOnline == 1 + ? LocaleKeys.online.tr(context: context).toText10(isBold: true, color: AppColors.successColor).toShimmer2(isShow: isLoading) + : "Offline".toText10(isBold: true, color: AppColors.primaryRedColor).toShimmer2(isShow: isLoading), + ], + ), + ]), + SizedBox(height: 8.h), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Expanded( + child: (isLoading ? "Cardiology" : (appState.isArabic() ? liveCareClinicListResponseModel.serviceNameN : liveCareClinicListResponseModel.serviceName))! + .toText16(isBold: true) + .toShimmer2(isShow: isLoading)), + Transform.flip( + flipX: appState.isArabic() ? true : false, + child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor).toShimmer2(isShow: isLoading)), + ]), + ], + ), + ); + } +} From da89c419255de64e192720ae2661f3bc374d16ea Mon Sep 17 00:00:00 2001 From: tahaalam Date: Wed, 1 Oct 2025 09:23:43 +0300 Subject: [PATCH 16/43] lab result by hospital and translations are added --- assets/langs/ar-SA.json | 11 +- assets/langs/en-US.json | 13 +- lib/core/api/api_client.dart | 10 +- lib/core/api_consts.dart | 2 +- lib/features/lab/lab_repo.dart | 115 ++++++++- lib/features/lab/lab_view_model.dart | 85 +++++++ .../lab/models/resp_models/lab_result.dart | 2 + .../patient_lab_special_result.dart | 32 +++ lib/generated/locale_keys.g.dart | 15 ++ lib/presentation/lab/lab_orders_page.dart | 230 +----------------- .../LabResultByHospital.dart | 69 ++++++ .../LabResultList.dart | 40 +++ .../lab_order_result_item.dart | 96 ++++++++ .../lab_order_specialResult.dart | 120 +++++++++ .../lab/lab_results/lab_result_calender.dart | 6 +- .../lab/lab_results/lab_result_details.dart | 15 +- lib/splashPage.dart | 12 +- lib/widgets/graph/custom_graph.dart | 4 +- pubspec.yaml | 1 + 19 files changed, 627 insertions(+), 251 deletions(-) create mode 100644 lib/features/lab/models/resp_models/patient_lab_special_result.dart create mode 100644 lib/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart create mode 100644 lib/presentation/lab/lab_result_via_hospital/LabResultList.dart create mode 100644 lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart create mode 100644 lib/presentation/lab/lab_result_via_hospital/lab_order_specialResult.dart diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 8945fee..fc9d255 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -856,5 +856,14 @@ "onboardingHeading1": "حجز المواعيد لم يكن أسهل من قبل", "onboardingBody1": "ببضع نقرات فقط يمكنك استشارة الطبيب الذي تختاره.", "onboardingHeading2": "الوصول إلى السجل الطبي بين يديك", - "onboardingBody2": "تتبع تاريخك الطبي بما في ذلك الفحوصات المخبرية، الوصفات الطبية، التأمين، وغيرها." + "onboardingBody2": "تتبع تاريخك الطبي بما في ذلك الفحوصات المخبرية، الوصفات الطبية، التأمين، وغيرها.", + "normal": "عادي", + "attention": "انتباه", + "monitor": "مراقبة", + "noSpecialResult": "لا توجد نتائج خاصة", + "setTheDateRange": "تعيين النطاق الزمني", + "historyFlowchart": "مخطط تدفق التاريخ", + "to": "إلى", + "startDate": "تاريخ البدء", + "endDate": "تاريخ الانتهاء" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 843daf8..146d96f 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -852,5 +852,16 @@ "onboardingHeading1": "Booking appointment has never been easy", "onboardingBody1": "In few clicks find yourself having consultation with the doctor of your choice.", "onboardingHeading2": "Access the medical history on finger tips", - "onboardingBody2": "Keep track on your medical history including labs, prescription, insurance, etc" + "onboardingBody2": "Keep track on your medical history including labs, prescription, insurance, etc", + "normal": "Normal", + "attention": "Attention", + "monitor": "Monitor", + "noSpecialResult": "No Special Results", + "setTheDateRange": "Set The Date Range", + "historyFlowchart": "History FlowChart", + "to": "to", + "startDate" : "Start Date", + "endDate": "End Date" + + } \ No newline at end of file diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index a4e76fc..5925641 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -176,8 +176,12 @@ class ApiClientImp implements ApiClient { body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 4767477; + body['TokenID'] = "@dm!n"; + body['PatientID'] = 1018977; + body['PatientTypeID'] = 1; + + body['PatientOutSA'] = 0; + body['SessionID'] = "45786230487560q"; } body.removeWhere((key, value) => value == null); @@ -198,7 +202,7 @@ class ApiClientImp implements ApiClient { final response = await http.post(Uri.parse(url.trim()), body: json.encode(body), headers: headers); final int statusCode = response.statusCode; - log("response.body: ${response.body}"); + // log("response.body: ${response.body}"); if (statusCode < 200 || statusCode >= 400) { var parsed = json.decode(utf8.decode(response.bodyBytes)); onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data")); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index fa02b2e..5886dfa 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu 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/features/lab/lab_repo.dart b/lib/features/lab/lab_repo.dart index 36f9767..2618ab6 100644 --- a/lib/features/lab/lab_repo.dart +++ b/lib/features/lab/lab_repo.dart @@ -4,6 +4,7 @@ import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; +import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_special_result.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'models/resp_models/lab_result.dart' show LabResult; @@ -11,6 +12,14 @@ import 'models/resp_models/lab_result.dart' show LabResult; abstract class LabRepo { Future>>> getPatientLabOrders(); Future>>> getPatientLabResults(PatientLabOrdersResponseModel laborder, bool isVidaPlus, String procedureName); + + Future>>> + getPatientLabResultsByHospitals( + PatientLabOrdersResponseModel laborder, bool isVidaPlus); + + Future>>> + getSpecialLabResult( + PatientLabOrdersResponseModel laborder, bool isVidaPlus); } class LabRepoImp implements LabRepo { @@ -73,7 +82,6 @@ class LabRepoImp implements LabRepo { request['ProjectID'] = laborder.projectID; request['ClinicID'] = laborder.clinicID; request['Procedure'] = procedureName; - request['LanguageID'] = 1; try { GenericApiModel>? apiResponse; Failure? failure; @@ -90,6 +98,58 @@ class LabRepoImp implements LabRepo { throw Exception("lab list is empty"); } + final labOrders = list + .map((item) => LabResult.fromJson(item as Map)) + .toList() + .cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: labOrders, + ); + } 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())); + } + } + + @override + Future>>> + getPatientLabResultsByHospitals( + PatientLabOrdersResponseModel laborder, bool isVidaPlus) async { + Map request = Map(); + request['InvoiceNo_VP'] = isVidaPlus ? laborder!.invoiceNo : "0"; + request['InvoiceNo'] = isVidaPlus ? "0" : laborder!.invoiceNo; + request['OrderNo'] = laborder!.orderNo; + request['isDentalAllowedBackend'] = false; + request['SetupID'] = laborder!.setupID; + request['ProjectID'] = laborder.projectID; + request['ClinicID'] = laborder.clinicID; + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_Patient_LAB_RESULT, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['ListPLR']; + if (list == null || list.isEmpty) { + throw Exception("lab list is empty"); + } + final labOrders = list.map((item) => LabResult.fromJson(item as Map)).toList().cast(); apiResponse = GenericApiModel>( @@ -110,4 +170,57 @@ class LabRepoImp implements LabRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> + getSpecialLabResult( + PatientLabOrdersResponseModel laborder, bool isVidaPlus) async { + Map request = Map(); + request['InvoiceNo_VP'] = isVidaPlus ? laborder!.invoiceNo : "0"; + request['InvoiceNo'] = isVidaPlus ? "0" : laborder!.invoiceNo; + request['OrderNo'] = laborder!.orderNo; + request['isDentalAllowedBackend'] = false; + request['SetupID'] = laborder!.setupID; + request['ProjectID'] = laborder.projectID; + request['ClinicID'] = laborder.clinicID; + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_Patient_LAB_SPECIAL_RESULT, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['ListPLSR']; + if (list == null || list.isEmpty) { + throw Exception("lab list is empty"); + } + + final labOrders = list + .map((item) => PatientLabSpecialResult.fromJson( + item as Map)) + .toList() + .cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: labOrders, + ); + } 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/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 5e89cd3..1bbc27c 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -20,6 +20,8 @@ import 'package:logger/logger.dart'; class LabViewModel extends ChangeNotifier { bool isLabOrdersLoading = false; bool isLabResultsLoading = false; + bool isLabResultByHospitalLoading = false; + bool isSpecialResultsLoading = false; LabRepo labRepo; ErrorHandlerService errorHandlerService; @@ -28,6 +30,11 @@ class LabViewModel extends ChangeNotifier { List patientLabOrders = []; List filteredLabOrders = []; List tempLabOrdersList = []; + String labSpecialResult = ""; + + PatientLabOrdersResponseModel? currentlySelectedPatientOrder; + + List mainLabResultsByHospitals = []; List mainLabResults = []; List mainGraphPoints = []; @@ -138,6 +145,31 @@ class LabViewModel extends ChangeNotifier { }; } + Future getPatientLabResultByHospital( + PatientLabOrdersResponseModel laborder) async { + isLabResultByHospitalLoading = true; + notifyListeners(); + mainLabResultsByHospitals.clear; + + final result = await labRepo.getPatientLabResultsByHospitals(laborder, + Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0"))); + + result.fold( + (failure) async { + isLabResultByHospitalLoading = false; + // await errorHandlerService.handleError(failure: failure); + }, + (apiResponse) { + isLabResultByHospitalLoading = false; + if (apiResponse.messageStatus == 2) { + } else if (apiResponse.messageStatus == 1) { + mainLabResultsByHospitals = apiResponse.data ?? []; + notifyListeners(); + } + }, + ); + } + Future getPatientLabResult( PatientLabOrdersResponseModel laborder, String procedureName) async { LoaderBottomSheet.showLoader(); @@ -199,6 +231,42 @@ class LabViewModel extends ChangeNotifier { ); } + Future getPatientSpecialResult( + PatientLabOrdersResponseModel laborder) async { + isSpecialResultsLoading = true; + labSpecialResult = ""; + notifyListeners(); + final result = await labRepo.getSpecialLabResult( + laborder, + Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0")), + ); + + result.fold( + (failure) async { + isSpecialResultsLoading = false; + notifyListeners(); + // await errorHandlerService.handleError(failure: failure); + }, + (apiResponse) { + isSpecialResultsLoading = false; + if (apiResponse.messageStatus == 2) { + } else if (apiResponse.messageStatus == 1) { + StringBuffer htmlbuffer = StringBuffer(""); + + apiResponse.data?.forEach((element) { + if(element.resultDataHTML != null && element.resultDataHTML?.isNotEmpty == true) + htmlbuffer.write("${element.resultDataHTML}

"); + }); + + labSpecialResult = htmlbuffer.toString(); + + notifyListeners(); + } + notifyListeners(); + }, + ); + } + String resultDate(DateTime date){ @@ -460,4 +528,21 @@ class LabViewModel extends ChangeNotifier { return true; } } + + String getSeverityText(String refernceValue) { + switch (refernceValue) { + case 'N': + return "normal"; + case 'L': + case 'H': + return "monitor"; + case 'CL': + case 'LCL': + case 'CH': + case 'HCH': + return "attention"; + default: + return "normal"; + } + } } diff --git a/lib/features/lab/models/resp_models/lab_result.dart b/lib/features/lab/models/resp_models/lab_result.dart index d4e9223..3f14d54 100644 --- a/lib/features/lab/models/resp_models/lab_result.dart +++ b/lib/features/lab/models/resp_models/lab_result.dart @@ -24,6 +24,7 @@ class LabResult { String? referenceHigh; String? criticalLow; String? referenceLow; + String? packageShortDescription; LabResult( {this.description, @@ -78,6 +79,7 @@ class LabResult { referenceHigh = json['ReferenceHigh']; criticalLow = json['CriticalLow']; referenceLow = json['ReferenceLow']; + packageShortDescription = json['PackageShortDescription']; } Map toJson() { diff --git a/lib/features/lab/models/resp_models/patient_lab_special_result.dart b/lib/features/lab/models/resp_models/patient_lab_special_result.dart new file mode 100644 index 0000000..87301f9 --- /dev/null +++ b/lib/features/lab/models/resp_models/patient_lab_special_result.dart @@ -0,0 +1,32 @@ +class PatientLabSpecialResult { + String? invoiceNo; + String? moduleID; + String? resultData; + String? resultDataHTML; + dynamic resultDataTxt; + + PatientLabSpecialResult( + {this.invoiceNo, + this.moduleID, + this.resultData, + this.resultDataHTML, + this.resultDataTxt}); + + PatientLabSpecialResult.fromJson(Map json) { + invoiceNo = json['InvoiceNo']; + moduleID = json['ModuleID']; + resultData = json['ResultData']; + resultDataHTML = json['ResultDataHTML']; + resultDataTxt = json['ResultDataTxt']; + } + + Map toJson() { + final Map data = new Map(); + data['InvoiceNo'] = this.invoiceNo; + data['ModuleID'] = this.moduleID; + data['ResultData'] = this.resultData; + data['ResultDataHTML'] = this.resultDataHTML; + data['ResultDataTxt'] = this.resultDataTxt; + return data; + } +} diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 3d12e5b..3c382f7 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -849,5 +849,20 @@ abstract class LocaleKeys { static const selectCountry = 'selectCountry'; static const forLoginVerification = 'forLoginVerification'; static const searchHospital = 'searchHospital'; + static const skip = 'skip'; + static const getStarted = 'getStarted'; + static const onboardingHeading1 = 'onboardingHeading1'; + static const onboardingBody1 = 'onboardingBody1'; + static const onboardingHeading2 = 'onboardingHeading2'; + static const onboardingBody2 = 'onboardingBody2'; + static const normal = 'normal'; + static const attention = 'attention'; + static const monitor = 'monitor'; + static const noSpecialResult = 'noSpecialResult'; + static const setTheDateRange = 'setTheDateRange'; + static const historyFlowchart = 'historyFlowchart'; + static const to = 'to'; + static const startDate = 'startDate'; + static const endDate = 'endDate'; } diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 6fd8234..1c78bbd 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1,229 +1 @@ -import 'dart:async'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_staggered_animations/flutter_staggered_animations.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/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/lab/lab_range_view_model.dart'; -import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; -import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; -import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; -import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:provider/provider.dart'; -import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; -import '../../widgets/appbar/collapsing_list_view.dart'; - -class LabOrdersPage extends StatefulWidget { - const LabOrdersPage({super.key}); - - @override - State createState() => _LabOrdersPageState(); -} - -class _LabOrdersPageState extends State { - late LabViewModel labProvider; - late LabRangeViewModel rangeViewModel; - - List?> labSuggestions = []; - int? expandedIndex; - String? selectedFilterText = ''; - int activeIndex = 0; - - @override - void initState() { - scheduleMicrotask(() { - labProvider.initLabProvider(); - }); - super.initState(); - } - - @override - Widget build(BuildContext context) { - labProvider = Provider.of(context); - rangeViewModel = Provider.of(context); - - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: LocaleKeys.labResults.tr(), - search: () async { - final lavVM = Provider.of(context, listen: false); - if (lavVM.isLabOrdersLoading) { - return; - } else { - String? value = await Navigator.of(context).push( - CustomPageRoute( - page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), - fullScreenDialog: true, - direction: AxisDirection.down, - ), - ); - if (value != null) { - selectedFilterText = value; - lavVM.filterLabReports(value); - } - } - }, - child: SingleChildScrollView( - padding: EdgeInsets.all(24.h), - physics: NeverScrollableScrollPhysics(), - child: Consumer( - builder: (context, model, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), - tabs: [ - CustomTabBarModel(null, "By Visit".needTranslation), - CustomTabBarModel(null, "By Test".needTranslation), - // CustomTabBarModel(null, "Completed".needTranslation), - ], - onTabChange: (index) { - activeIndex = index; - setState(() {}); - }, - ), - SizedBox(height: 16.h), - selectedFilterText!.isNotEmpty - ? CustomChipWidget( - chipText: selectedFilterText!, - chipType: ChipTypeEnum.alert, - isSelected: true, - ) - : SizedBox(), - activeIndex == 0 - ? ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - itemCount: model.isLabOrdersLoading - ? 5 - : model.patientLabOrders.isNotEmpty - ? model.patientLabOrders.length - : 1, - itemBuilder: (context, index) { - final isExpanded = expandedIndex == index; - return model.isLabOrdersLoading - ? LabResultItemView( - onTap: () {}, - labOrder: null, - index: index, - isLoading: true, - ) - : model.patientLabOrders.isNotEmpty - ? AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: LabResultItemView( - onTap: () { - setState(() { - expandedIndex = isExpanded ? null : index; - }); - }, - labOrder: model.patientLabOrders[index], - index: index, - isExpanded: isExpanded)), - ), - ) - : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); - }, - ) - : ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().length, - itemBuilder: (context, index) { - final isExpanded = expandedIndex == index; - return model.isLabOrdersLoading - ? LabResultItemView( - onTap: () {}, - labOrder: null, - index: index, - isLoading: true, - ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: LabOrderByTest( - onTap: () { - if(model.uniqueTests.toList()[index].model != null) { - rangeViewModel.flush(); - model.getPatientLabResult( - model.uniqueTests - .toList()[index] - .model!, model.uniqueTests - .toList()[index].description!); - } - }, - tests: model.uniqueTests.toList()[index], - index: index, - isExpanded: isExpanded)), - ), - ); - }, - ) - ], - ); - }, - ), - ), - )); - } - - Color getLabOrderStatusColor(num status) { - switch (status) { - case 44: - return AppColors.warningColorYellow; - case 45: - return AppColors.warningColorYellow; - case 16: - return AppColors.successColor; - case 17: - return AppColors.successColor; - default: - return AppColors.greyColor; - } - } - - String getLabOrderStatusText(num status) { - switch (status) { - case 44: - return LocaleKeys.resultsPending.tr(context: context); - case 45: - return LocaleKeys.resultsPending.tr(context: context); - case 16: - return LocaleKeys.resultsAvailable.tr(context: context); - case 17: - return LocaleKeys.resultsAvailable.tr(context: context); - default: - return ""; - } - } - - getLabSuggestions(LabViewModel model) { - if (model.patientLabOrders.isEmpty) { - return []; - } - return model.patientLabOrders.map((m) => m.testDetails).toList(); - } -} +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.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/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/lab/lab_range_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late LabRangeViewModel rangeViewModel; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context); rangeViewModel = Provider.of(context); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 16.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; scheduleMicrotask(() { labProvider .getPatientLabResultByHospital( model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); }); Navigator.push(context,FadePage( page: LabResultByHospitals(), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().length, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( onTap: () { if(model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult( model.uniqueTests .toList()[index] .model!, model.uniqueTests .toList()[index].description!); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart b/lib/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart new file mode 100644 index 0000000..fb88908 --- /dev/null +++ b/lib/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart @@ -0,0 +1,69 @@ +import 'package:easy_localization/easy_localization.dart' + show tr, StringTranslateExtension; +import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.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/lab/lab_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/LabResultList.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/lab_order_specialResult.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:provider/provider.dart'; + +class LabResultByHospitals extends StatelessWidget { + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: LocaleKeys.labResults.tr(), + child: SingleChildScrollView( + child: Column( + spacing: 8.h, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Selector( + selector: (_, model) => model.isLabResultByHospitalLoading, + builder: (_, isLoading, __) { + if (isLoading) { + return Column( + children: [ + LabResultItemView( + onTap: () {}, + labOrder: null, + index: 0, + isLoading: true, + ), + LabResultItemView( + onTap: () {}, + labOrder: null, + index: 0, + isLoading: true, + ), + LabResultItemView( + onTap: () {}, + labOrder: null, + index: 0, + isLoading: true, + ), + LabResultItemView( + onTap: () {}, + labOrder: null, + index: 0, + isLoading: true, + ), + ], + ); + } else { + return LabResultList(); + } + }, + ), + LabOrderSpecialResult() + ], + ).paddingAll(24.h), + )); + } +} diff --git a/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart b/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart new file mode 100644 index 0000000..9e1f08c --- /dev/null +++ b/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.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/features/lab/lab_view_model.dart'; +import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart'; +import 'package:provider/provider.dart' show Selector, Provider; + +class LabResultList extends StatelessWidget { + late LabViewModel model; + + @override + Widget build(BuildContext context) { + model = Provider.of(context); + return Selector>( + selector: (_, model) => model.mainLabResultsByHospitals, + builder: (__, list, ___) { + if (list.isEmpty) { + return Utils.getNoDataWidget(context, + noDataText: "You don't have any lab results yet." + .needTranslation); + } else { + return ListView.builder( + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) { + var labItem = list[index]; + return LabOrderResultItem(onTap: () { + model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description??""); + }, + tests: labItem, + index: index, + iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"), + severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N")); + }); + } + }, + ); + } +} \ No newline at end of file diff --git a/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart new file mode 100644 index 0000000..59638de --- /dev/null +++ b/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart @@ -0,0 +1,96 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/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/lab/models/resp_models/lab_result.dart'; +import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; + +class LabOrderResultItem extends StatelessWidget { + final VoidCallback onTap; + final int index; + final LabResult? tests; + final String severityText; + final bool isLoading; + final bool isExpanded; + final Color iconColor; + + const LabOrderResultItem({super.key, required this.onTap, this.tests, required this.index, this.isLoading = false, this.isExpanded = false,required this.iconColor, required this.severityText}); + + @override + build(BuildContext context) { + return AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + child: Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ...labOrder!.testDetails!.map((detail) { + Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: '${tests!.description}'.toText14(weight: FontWeight.w500), + ), + '${tests!.packageShortDescription}'.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + // + SizedBox(height: 24.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + spacing: 6.h, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Text(severityText.tr(), + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 10.fSize, + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor + )), + Utils.buildSvgWithAssets( + icon: AppAssets.lab_result_indicator, + width: 21, + height: 23, + iconColor: iconColor + ), + ], + ), + CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + text: LocaleKeys.viewReport.tr(context: context), + onPressed: () { + onTap(); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.bold, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ), + ], + ), + )); + } +} diff --git a/lib/presentation/lab/lab_result_via_hospital/lab_order_specialResult.dart b/lib/presentation/lab/lab_result_via_hospital/lab_order_specialResult.dart new file mode 100644 index 0000000..5f71a7e --- /dev/null +++ b/lib/presentation/lab/lab_result_via_hospital/lab_order_specialResult.dart @@ -0,0 +1,120 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.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/lab/lab_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +class LabOrderSpecialResult extends StatelessWidget { + const LabOrderSpecialResult({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, model) => model.isSpecialResultsLoading, + builder: (_, isLoading, __) { + return Selector( + selector: (_, model) => model.labSpecialResult, + builder: (_, data, __) { + if(isLoading){ + return Container( + margin: EdgeInsets.symmetric(vertical: 8.h), + padding: EdgeInsets.symmetric( + horizontal: 16.h, vertical: 16.h), + width: MediaQuery.sizeOf(context).width - 24, + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true), + child:Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12.h, + children: [ + "loading".toText14().toShimmer2(isShow: isLoading), + "loading".toText14().toShimmer2(isShow: isLoading), + ], + ) + ); + } + if(data.isNotEmpty ) { + return AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 16.h, vertical: 16.h), + width: MediaQuery.sizeOf(context).width - 24, + child: Column( + spacing: 8.h, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ...labOrder!.testDetails!.map((detail) { + LocaleKeys.specialResult + .tr() + .toText14(weight: FontWeight.w500) + .toShimmer2(isShow: isLoading), + + data.isEmpty + ? LocaleKeys.noSpecialResult + .tr() + .toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight) + .toShimmer2(isShow: isLoading) + : HtmlWidget(data).toShimmer2(isShow: isLoading) + + // + ], + ), + )); + } return SizedBox.shrink(); + }); + + }); + } +} + +/*Text( + "Special Results", + style: TextStyle( + fontSize: 18.fSize, + fontWeight: FontWeight.w600, + color: AppColors.blackColor), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true + ), + padding: EdgeInsets.all(16.h), + width: MediaQuery.sizeOf(context).width-24, + child: Selector( + selector: (_, model) => + model.isLabResultByHospitalLoading, + builder: (_, isLoading, __) { + return Selector( + selector: (_, model) => model.labSpecialResult, + builder: (_, data, __) { + return (data.isEmpty) + ? Text("No result available".needTranslation, + style: TextStyle( + fontSize: 12.fSize, + fontWeight: FontWeight.w500, + color: AppColors.textColorLight)) + .toShimmer2(isShow: isLoading) + : HtmlWidget(data) + .toShimmer2(isShow: isLoading); + }); + }))*/ diff --git a/lib/presentation/lab/lab_results/lab_result_calender.dart b/lib/presentation/lab/lab_results/lab_result_calender.dart index b118293..5750fdd 100644 --- a/lib/presentation/lab/lab_results/lab_result_calender.dart +++ b/lib/presentation/lab/lab_results/lab_result_calender.dart @@ -68,7 +68,7 @@ class _LabResultCalenderState extends State { children: [ fromDateComponent(), Text( - "to".needTranslation, + LocaleKeys.to.tr(), style: TextStyle( color: AppColors.calenderTextColor, fontSize: 14.h, @@ -205,7 +205,7 @@ class _LabResultCalenderState extends State { fromDateComponent() { return Consumer( builder: (_, model, __) { - return displayDate("Start Date".needTranslation, + return displayDate(LocaleKeys.startDate.tr(), model.getDateString(model.fromDate), model.fromDate == null); }, ); @@ -214,7 +214,7 @@ class _LabResultCalenderState extends State { toDateComponent() { return Consumer( builder: (_, model, __) { - return displayDate("End Date".needTranslation, + return displayDate(LocaleKeys.endDate.tr(), model.getDateString(model.toDate), model.toDate == null); }, ); diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index e3ffaea..382a846 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -13,10 +13,11 @@ import 'package:hmg_patient_app_new/features/lab/history/lab_history_viewmodel.d import 'package:hmg_patient_app_new/features/lab/lab_range_view_model.dart' show LabRangeViewModel; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart'; -import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_calender.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_list_item.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; import 'package:provider/provider.dart' show Consumer, Provider; @@ -157,7 +158,7 @@ class LabResultDetails extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - model.isGraphVisible?"History FlowChart".needTranslation: "History".needTranslation, + model.isGraphVisible?LocaleKeys.historyFlowchart.tr(): LocaleKeys.history.tr(), style: TextStyle( fontSize: 16, fontFamily: 'Poppins', @@ -183,7 +184,7 @@ class LabResultDetails extends StatelessWidget { height: 24) .onPress(() { showCommonBottomSheetWithoutHeight( - title: "Set The Date Range".needTranslation, + title: LocaleKeys.setTheDateRange.tr(), context, child: LabResultCalender( onRangeSelected: (start, end) { @@ -235,15 +236,17 @@ class LabResultDetails extends StatelessWidget { Widget historyBody(LabRangeViewModel model, LabViewModel labmodel) { if(model.isGraphVisible){ + print("the labmodel.filteredGraphValues.length is ${labmodel.filteredGraphValues.length}"); var graphColor = labmodel.getColor(recentLabResult.calculatedResultFlag??"N"); return CustomGraph( dataPoints: labmodel.filteredGraphValues, // maxY: 100, + makeGraphBasedOnActualValue: true, leftLabelReservedSize: 40, leftLabelInterval: getInterval(labmodel), - maxY: (labmodel.maxY)+(getInterval(labmodel)??0)/2, - + maxY: (labmodel.maxY)+(getInterval(labmodel)??0)/5, + maxX: labmodel.filteredGraphValues.length.toDouble()-.75, leftLabelFormatter: (value) { return leftLabels(value.toStringAsFixed(2).tr()); // switch (value.toInt()) { @@ -263,7 +266,7 @@ class LabResultDetails extends StatelessWidget { // } }, graphColor:graphColor , - graphShadowColor: graphColor.withOpacity(.4), + graphShadowColor: graphColor.withOpacity(.1), graphGridColor: graphColor.withOpacity(.4), bottomLabelFormatter: (value, data) { if(data.isEmpty) return SizedBox.shrink(); diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 3e70742..117db61 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart'; import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; @@ -48,11 +49,12 @@ class _SplashScreenState extends State { Timer(Duration(seconds: 2, milliseconds: 500), () async { LocalNotification.init(onNotificationClick: (payload) {}); - if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { - Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); - } else { - Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); - } + // if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { + // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); + // } else { + // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + // } + Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LabOrdersPage()))); }); var zoom = ZoomVideoSdk(); InitConfig initConfig = InitConfig( diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index fa72b19..1e0bd60 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -236,7 +236,7 @@ class CustomGraph extends StatelessWidget { gradient: LinearGradient( colors: [ graphShadowColor, - Colors.transparent, + Colors.white, ], begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -244,6 +244,8 @@ class CustomGraph extends StatelessWidget { ), ) ]; + var max = LineChartHelper().calculateMaxAxisValues(data); + print("the maxX is the -------> ${max.$2}"); return data; } diff --git a/pubspec.yaml b/pubspec.yaml index 3bdaa4a..fe114dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,6 +85,7 @@ dependencies: gms_check: ^1.0.4 huawei_location: ^6.14.2+301 intl: ^0.20.2 + flutter_widget_from_html: ^0.17.1 dev_dependencies: flutter_test: From 10b77206f6060940f08d758061fac5e09043f346 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 1 Oct 2025 17:29:59 +0300 Subject: [PATCH 17/43] Immediate LiveCare consultation implementation contd. --- assets/images/svg/edit_icon.svg | 5 + lib/core/app_assets.dart | 1 + lib/core/utils/push_notification_handler.dart | 104 ++-- lib/core/utils/utils.dart | 1 - .../book_appointments_repo.dart | 107 ++++ .../book_appointments_view_model.dart | 59 +- ...ivecare_immediate_fees_response_model.dart | 37 ++ .../appointment_details_page.dart | 2 +- .../appointment_payment_page.dart | 4 +- .../immediate_livecare_payment_details.dart | 301 +++++++++++ .../immediate_livecare_payment_page.dart | 510 ++++++++++++++++++ ...select_immediate_livecare_clinic_page.dart | 26 +- .../review_appointment_page.dart | 1 + lib/widgets/loader/bottomsheet_loader.dart | 5 +- 14 files changed, 1096 insertions(+), 67 deletions(-) create mode 100644 assets/images/svg/edit_icon.svg create mode 100644 lib/features/book_appointments/models/resp_models/get_livecare_immediate_fees_response_model.dart create mode 100644 lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart create mode 100644 lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart diff --git a/assets/images/svg/edit_icon.svg b/assets/images/svg/edit_icon.svg new file mode 100644 index 0000000..3f82a2e --- /dev/null +++ b/assets/images/svg/edit_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 446bbc2..016e42d 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -146,6 +146,7 @@ class AppAssets { static const String ic_low_result = '$svgBasePath/low_result.svg'; static const String ic_critical_low_result = '$svgBasePath/critical_low_result.svg'; static const String livecare_online_icon = '$svgBasePath/livecare_online_icon.svg'; + static const String edit_icon = '$svgBasePath/edit_icon.svg'; //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; diff --git a/lib/core/utils/push_notification_handler.dart b/lib/core/utils/push_notification_handler.dart index 4568a15..f59d657 100644 --- a/lib/core/utils/push_notification_handler.dart +++ b/lib/core/utils/push_notification_handler.dart @@ -8,11 +8,14 @@ import 'package:firebase_messaging/firebase_messaging.dart' as fir; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_ios_voip_kit_karmm/call_state_type.dart'; +import 'package:flutter_ios_voip_kit_karmm/flutter_ios_voip_kit.dart'; // import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:hmg_patient_app_new/core/utils/local_notifications.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:permission_handler/permission_handler.dart'; import '../cache_consts.dart'; @@ -139,7 +142,7 @@ class PushNotificationHandler { // late HmsApiAvailability hmsApiAvailability; - // final voIPKit = FlutterIOSVoIPKit.instance; + final voIPKit = FlutterIOSVoIPKit.instance; late Timer timeOutTimer; bool isTalking = false; @@ -188,55 +191,56 @@ class PushNotificationHandler { this.context = context; if (Platform.isIOS) { - // voIPKit.getVoIPToken().then((value) { - // print("APNS VOIP KIT TOKEN: $value"); - // AppSharedPreferences().setString(APNS_TOKEN, value!); - // }); - // - // voIPKit.onDidUpdatePushToken = (String token) { - // print('🎈 example: onDidUpdatePushToken: $token'); - // }; - // - // voIPKit.onDidReceiveIncomingPush = ( - // Map payload, - // ) async { - // print('🎈 example: onDidReceiveIncomingPush $payload'); - // _timeOut(); - // }; - // - // voIPKit.onDidRejectIncomingCall = ( - // String uuid, - // String callerId, - // ) async { - // try { - // print('🎈 example: onDidRejectIncomingCall $uuid - $callerId'); - // timeOutTimer.cancel(); - // } catch (err) {} - // }; - // - // voIPKit.onDidAcceptIncomingCall = ( - // String uuid, - // String callerId, - // ) async { - // print('🎈 example: onDidAcceptIncomingCall $uuid - $callerId'); - // await voIPKit.acceptIncomingCall(callerState: CallStateType.calling); - // await voIPKit.callConnected(); - // await Future.delayed(Duration(seconds: 1)); - // - // Navigator.pushNamed( - // locator().navigatorKey.currentContext!, - // "zoom_call_page", - // arguments: CallArguments("hoover-dam", "123", "Patient", "40", "1", false), - // ); - // - // await voIPKit.endCall(); - // - // // Navigator.pushNamed(navigatorKey.currentContext!, VIDEO_CALL_SCREEN, - // // arguments: VideoArgus( - // // reservationId: int.parse(callerId), token: null, isVideo: true)); - // - // timeOutTimer.cancel(); - // }; + voIPKit.getVoIPToken().then((value) { + print("🎈 APNS VOIP KIT TOKEN: $value"); + Utils.saveStringFromPrefs(CacheConst.voipToken, value ?? ""); + // AppSharedPreferences().setString(APNS_TOKEN, value!); + }); + + voIPKit.onDidUpdatePushToken = (String token) { + print('🎈 example: onDidUpdatePushToken: $token'); + }; + + voIPKit.onDidReceiveIncomingPush = ( + Map payload, + ) async { + print('🎈 example: onDidReceiveIncomingPush $payload'); + // _timeOut(); + }; + + voIPKit.onDidRejectIncomingCall = ( + String uuid, + String callerId, + ) async { + try { + print('🎈 example: onDidRejectIncomingCall $uuid - $callerId'); + timeOutTimer.cancel(); + } catch (err) {} + }; + + voIPKit.onDidAcceptIncomingCall = ( + String uuid, + String callerId, + ) async { + print('🎈 example: onDidAcceptIncomingCall $uuid - $callerId'); + await voIPKit.acceptIncomingCall(callerState: CallStateType.calling); + await voIPKit.callConnected(); + await Future.delayed(Duration(seconds: 1)); + + // Navigator.pushNamed( + // locator().navigatorKey.currentContext!, + // "zoom_call_page", + // arguments: CallArguments("hoover-dam", "123", "Patient", "40", "1", false), + // ); + + await voIPKit.endCall(); + + // Navigator.pushNamed(navigatorKey.currentContext!, VIDEO_CALL_SCREEN, + // arguments: VideoArgus( + // reservationId: int.parse(callerId), token: null, isVideo: true)); + + // timeOutTimer.cancel(); + }; } if (Platform.isAndroid) { diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index efa01d0..8830b78 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -658,7 +658,6 @@ class Utils { static Widget getPaymentAmountWithSymbol(Widget paymentAmountWidget, Color iconColor, double iconSize, {bool isSaudiCurrency = true, bool isExpanded = true}) { return Row( mainAxisAlignment: isExpanded ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, children: [ appState.isArabic() ? Container() diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index 18b5330..0a137ce 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; @@ -9,6 +11,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/resp_model import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_clinics_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_fees_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_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/services/logger_service.dart'; @@ -69,6 +72,13 @@ abstract class BookAppointmentsRepo { Function(String)? onError}); Future>>> getLiveCareImmediateClinicsList(int age, int genderID, {Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>> getLiveCareImmediateAppointmentFees(int age, int genderID, int serviceID, + {Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>> addNewCallRequestForImmediateLiveCare( + int age, int gender, int serviceID, String clientRequestID, int callTypeID, bool isPharma, String deviceToken, String voipToken, + {Function(dynamic)? onSuccess, Function(String)? onError}); } class BookAppointmentsRepoImp implements BookAppointmentsRepo { @@ -703,4 +713,101 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> getLiveCareImmediateAppointmentFees(int age, int genderID, int serviceID, + {Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = { + "Age": age, + "Gender": genderID, + "ServiceID": serviceID, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_ER_APPOINTMENT_FEES, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final respObject = response['GetERAppointmentFeesList']; + + final liveCareFeesObj = LiveCareImmediateAppointmentFeesList.fromJson(respObject); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: liveCareFeesObj, + ); + } 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())); + } + } + + @override + Future> addNewCallRequestForImmediateLiveCare( + int age, int gender, int serviceID, String clientRequestID, int callTypeID, bool isPharma, String deviceToken, String voipToken, + {Function(dynamic p1)? onSuccess, Function(String p1)? onError}) async { + Map mapDevice = { + "IsPharmacy": isPharma, + "ErServiceID": serviceID, + "ClientRequestID": clientRequestID, + "DeviceToken": deviceToken, + "VoipToken": voipToken, + "IsFlutter": true, + "DeviceType": Platform.isIOS ? 'iOS' : 'Android', + "Age": age, + "Gender": gender, + "IsVoip": Platform.isIOS ? true : false, + "CallTypeID": callTypeID + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ADD_NEW_CALL_FOR_PATIENT_ER, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // final respObject = response['GetERAppointmentFeesList']; + + // final liveCareFeesObj = LiveCareImmediateAppointmentFeesList.fromJson(respObject); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: true, + ); + } 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 ab4ca56..120aa3b 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -16,6 +16,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/resp_model import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_fees_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/timeslots.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; @@ -27,8 +28,6 @@ import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; -import 'package:location/location.dart' show Location; import 'models/resp_models/get_livecare_clinics_response_model.dart'; @@ -64,7 +63,9 @@ class BookAppointmentsViewModel extends ChangeNotifier { //Immediate LiveCare List immediateLiveCareClinicsList = []; bool isImmediateLiveCareClinicsLoading = false; - int liveCareSelectedCallType = 1; + int liveCareSelectedCallType = 0; // 1- Video, 2- Audio, 3- Phone + late GetLiveCareClinicListResponseModel immediateLiveCareSelectedClinic; + late LiveCareImmediateAppointmentFeesList liveCareImmediateAppointmentFeesList; late DoctorsProfileResponseModel doctorsProfileResponseModel; @@ -125,6 +126,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { immediateLiveCareClinicsList.clear(); isImmediateLiveCareClinicsLoading = true; + liveCareSelectedCallType = 0; // getLocation(); notifyListeners(); } @@ -191,12 +193,16 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } - setLiveCareSelectedCallType(int value) { liveCareSelectedCallType = value; notifyListeners(); } + setImmediateLiveCareSelectedClinic(GetLiveCareClinicListResponseModel clinic) { + immediateLiveCareSelectedClinic = clinic; + notifyListeners(); + } + /// this function will decide which clinic api to be called /// either api for region flow or the select clinic api Future getClinics() async { @@ -798,4 +804,49 @@ class BookAppointmentsViewModel extends ChangeNotifier { }, ); } + + Future getLiveCareImmediateAppointmentFees({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = + await bookAppointmentsRepo.getLiveCareImmediateAppointmentFees(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!, immediateLiveCareSelectedClinic.serviceID!); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage ?? "Unknown error occurred"); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + liveCareImmediateAppointmentFeesList = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future addNewCallRequestForImmediateLiveCare(String transID, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await bookAppointmentsRepo.addNewCallRequestForImmediateLiveCare(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!, + immediateLiveCareSelectedClinic.serviceID!, transID, liveCareSelectedCallType, false, _appState.deviceToken, await Utils.getStringFromPrefs(CacheConst.voipToken)); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage ?? "Unknown error occurred"); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } } diff --git a/lib/features/book_appointments/models/resp_models/get_livecare_immediate_fees_response_model.dart b/lib/features/book_appointments/models/resp_models/get_livecare_immediate_fees_response_model.dart new file mode 100644 index 0000000..9a5861e --- /dev/null +++ b/lib/features/book_appointments/models/resp_models/get_livecare_immediate_fees_response_model.dart @@ -0,0 +1,37 @@ +class LiveCareImmediateAppointmentFeesList { + String? amount; + String? companyName; + bool? isInsured; + bool? isShowInsuranceUpdateModule; + bool? isCash; + bool? isEligible; + String? tax; + String? total; + String? currency; + + LiveCareImmediateAppointmentFeesList({this.amount, this.companyName, this.isInsured, this.isShowInsuranceUpdateModule, this.tax, this.total, this.currency}); + + LiveCareImmediateAppointmentFeesList.fromJson(Map json) { + amount = json['Amount']; + companyName = json['CompanyName']; + isInsured = json['IsInsured']; + isCash = json['IsCash']; + isEligible = json['IsEligible']; + isShowInsuranceUpdateModule = json['IsShowInsuranceUpdateModule']; + tax = json['Tax']; + total = json['Total']; + currency = json['currency']; + } + + Map toJson() { + final Map data = new Map(); + data['Amount'] = this.amount; + data['CompanyName'] = this.companyName; + data['IsInsured'] = this.isInsured; + data['IsShowInsuranceUpdateModule'] = this.isShowInsuranceUpdateModule; + data['Tax'] = this.tax; + data['Total'] = this.total; + data['currency'] = this.currency; + return data; + } +} diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 6976188..54353a4 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -419,7 +419,7 @@ class _AppointmentDetailsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "Total amount to pay".needTranslation.toText18(isBold: true), + "Amount before tax".needTranslation.toText18(isBold: true), Utils.getPaymentAmountWithSymbol(widget.patientAppointmentHistoryResponseModel.patientShare!.toString().toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: true), ], diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 9d6b557..7334369 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -257,7 +257,7 @@ class _AppointmentPaymentPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "Total amount to pay".needTranslation.toText14(isBold: true), + "Amount before tax".needTranslation.toText14(isBold: true), Utils.getPaymentAmountWithSymbol(myAppointmentsVM.patientAppointmentShareResponseModel!.patientShare!.toString().toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: true), ], @@ -293,7 +293,7 @@ class _AppointmentPaymentPageState extends State { if (Utils.havePrivilege(103)) { startApplePay(); } else { - openPaymentURL(selectedPaymentMethod); + openPaymentURL("ApplePay"); } }) : SizedBox(height: 12.h), diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart new file mode 100644 index 0000000..76f6ad1 --- /dev/null +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -0,0 +1,301 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart'; +import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.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/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_corner/smooth_corner.dart'; + +class ImmediateLiveCarePaymentDetails extends StatelessWidget { + ImmediateLiveCarePaymentDetails({super.key}); + + late BookAppointmentsViewModel bookAppointmentsViewModel; + late AppState appState; + + @override + Widget build(BuildContext context) { + bookAppointmentsViewModel = Provider.of(context, listen: false); + appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Review LiveCare Request".needTranslation, + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + LocaleKeys.patientInfo.tr(context: context).toText16(isBold: true), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Image.asset( + appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, + width: 52.h, + height: 52.h, + ), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true), + SizedBox(height: 8.h), + AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), + ], + ), + ], + ), + ), + ), + SizedBox(height: 24.h), + "Clinic Information".needTranslation.toText16(isBold: true), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.generic_clinic_icon, width: 32.h, height: 32.h, fit: BoxFit.contain), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (appState.isArabic() ? bookAppointmentsViewModel.immediateLiveCareSelectedClinic.serviceNameN : bookAppointmentsViewModel.immediateLiveCareSelectedClinic.serviceName)! + .toText16(isBold: true), + // SizedBox(height: 8.h), + // AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), + ], + ), + ], + ), + ), + ), + SizedBox(height: 24.h), + "Selected LiveCare Type".needTranslation.toText16(isBold: true), + SizedBox(height: 16.h), + Consumer(builder: (context, bookAppointmentsVM, child) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 32.h, height: 32.h, fit: BoxFit.contain), + SizedBox(width: 8.h), + getLiveCareType(bookAppointmentsViewModel.liveCareSelectedCallType).toText16(isBold: true), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.edit_icon, width: 24.h, height: 24.h, fit: BoxFit.contain), + ], + ), + ), + ).onPress(() { + showCommonBottomSheetWithoutHeight(context, child: SelectLiveCareCallType(bookAppointmentsViewModel: bookAppointmentsViewModel), callBackFunc: () async { + debugPrint("Selected Call Type: ${bookAppointmentsViewModel.liveCareSelectedCallType}"); + }, title: "Select LiveCare call type".needTranslation, isCloseButtonVisible: true, isFullScreen: false); + }); + }), + SizedBox(height: 24.h) + ], + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.isCash ?? true) + ? Container( + height: 50.h, + decoration: ShapeDecoration( + color: AppColors.secondaryLightRedBorderColor, + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + smoothness: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Insurance expired or inactive".needTranslation.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500).paddingSymmetrical(24.h, 0.h), + CustomButton( + text: LocaleKeys.updateInsurance.tr(context: context), + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: InsuranceHomePage(), + ), + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.secondaryLightRedBorderColor, + textColor: AppColors.whiteColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(15, 0, 15, 0), + height: 30.h, + ).paddingSymmetrical(24.h, 0.h), + ], + ), + ) + : const SizedBox(), + SizedBox(height: 24.h), + "Total amount to pay".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.amount!.toText16(isBold: true), AppColors.blackColor, 13, + isSaudiCurrency: bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), + ], + ).paddingSymmetrical(24.h, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), + Utils.getPaymentAmountWithSymbol( + bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.tax!.toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, + isSaudiCurrency: bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 150.h, child: Utils.getPaymentMethods()), + Utils.getPaymentAmountWithSymbol(bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.total!.toText24(isBold: true), AppColors.blackColor, 17, + isSaudiCurrency: bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), + ], + ).paddingSymmetrical(24.h, 0.h), + CustomButton( + text: LocaleKeys.payNow.tr(context: context), + onPressed: () async { + await askVideoCallPermission().then((val) { + if (val) { + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePaymentPage(), + ), + ); + } else { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: + "LiveCare requires Camera, Microphone & Location permissions to enable virtual consultation between patient & doctor, Please allow these to proceed.".needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }); + }, + backgroundColor: AppColors.infoColor, + borderColor: AppColors.infoColor, + textColor: AppColors.whiteColor, + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + icon: AppAssets.appointment_pay_icon, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ).paddingSymmetrical(24.h, 24.h), + ], + ), + ), + ], + ), + ); + } + + Future askVideoCallPermission() async { + Map statuses = await [ + Permission.camera, + Permission.microphone, + ].request(); + + if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted) { + // Camera permission granted + return true; + } else { + return false; + } + + // if (!(await Permission.camera.request().isGranted) || !(await Permission.microphone.request().isGranted)) { + // return false; + // } + } + + String getLiveCareType(int callType) { + switch (callType) { + case 1: + return "Video Call".needTranslation; + case 2: + return "Audio Call".needTranslation; + case 3: + return "Phone Call".needTranslation; + default: + return "Video Call".needTranslation; + } + } +} diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart new file mode 100644 index 0000000..d4a108c --- /dev/null +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -0,0 +1,510 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/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/payfort/payfort_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_corner/smooth_corner.dart'; + +class ImmediateLiveCarePaymentPage extends StatefulWidget { + ImmediateLiveCarePaymentPage({super.key}); + + @override + State createState() => _ImmediateLiveCarePaymentPageState(); +} + +class _ImmediateLiveCarePaymentPageState extends State { + late PayfortViewModel payfortViewModel; + late BookAppointmentsViewModel bookAppointmentsViewModel; + late MyAppointmentsViewModel myAppointmentsViewModel; + late AppState appState; + + MyInAppBrowser? browser; + String selectedPaymentMethod = ""; + + String transID = ""; + + @override + void initState() { + scheduleMicrotask(() { + payfortViewModel.initPayfortViewModel(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + myAppointmentsViewModel = Provider.of(context); + bookAppointmentsViewModel = Provider.of(context, listen: false); + payfortViewModel = Provider.of(context); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(builder: (context, myAppointmentsVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Appointment Payment".needTranslation, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(AppAssets.mada, width: 72.h, height: 25.h), + SizedBox(height: 16.h), + "Mada".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic() ? true : false, + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "MADA"; + openPaymentURL("mada"); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset(AppAssets.visa, width: 50.h, height: 50.h), + SizedBox(width: 8.h), + Image.asset(AppAssets.Mastercard, width: 40.h, height: 40.h), + ], + ), + SizedBox(height: 16.h), + "Visa or Mastercard".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic() ? true : false, + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "VISA"; + openPaymentURL("visa"); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(AppAssets.tamara_en, width: 72.h, height: 25.h), + SizedBox(height: 16.h), + "Tamara".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic() ? true : false, + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "TAMARA"; + openPaymentURL("tamara"); + }), + ], + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Consumer(builder: (context, payfortVM, child) { + //TODO: Need to add loading state & animation for Apple Pay Configuration + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.isCash ?? true) + ? Container( + height: 50.h, + decoration: ShapeDecoration( + color: AppColors.secondaryLightRedBorderColor, + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + smoothness: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Insurance expired or inactive".needTranslation.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500).paddingSymmetrical(24.h, 0.h), + CustomButton( + text: LocaleKeys.updateInsurance.tr(context: context), + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: InsuranceHomePage(), + ), + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.secondaryLightRedBorderColor, + textColor: AppColors.whiteColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(15, 0, 15, 0), + height: 30.h, + ).paddingSymmetrical(24.h, 0.h), + ], + ), + ) + : const SizedBox(), + SizedBox(height: 24.h), + "Total amount to pay".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.amount!.toString().toText16(isBold: true), AppColors.blackColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), + Utils.getPaymentAmountWithSymbol( + bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.tax!.toString().toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.total!.toString().toText24(isBold: true), AppColors.blackColor, 17, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Platform.isIOS + ? Utils.buildSvgWithAssets( + icon: AppAssets.apple_pay_button, + width: 200.h, + height: 80.h, + fit: BoxFit.contain, + ).paddingSymmetrical(24.h, 0.h).onPress(() { + // payfortVM.setIsApplePayConfigurationLoading(true); + if (Utils.havePrivilege(103)) { + startApplePay(); + } else { + openPaymentURL("ApplePay"); + } + }) + : SizedBox(height: 12.h), + SizedBox(height: 12.h), + ], + ); + }), + ), + ], + ); + }), + ); + } + + onBrowserLoadStart(String url) { + print("onBrowserLoadStart"); + print(url); + + if (selectedPaymentMethod == "tamara") { + if (Platform.isAndroid) { + Uri uri = new Uri.dataFromString(url); + // tamaraPaymentStatus = uri.queryParameters['status']!; + // tamaraOrderID = uri.queryParameters['AuthorizePaymentId']!; + } else { + Uri uri = new Uri.dataFromString(url); + // tamaraPaymentStatus = uri.queryParameters['paymentStatus']!; + // tamaraOrderID = uri.queryParameters['orderId']!; + } + } + + // if(selectedPaymentMethod != "TAMARA") { + MyInAppBrowser.successURLS.forEach((element) { + if (url.contains(element)) { + browser?.close(); + MyInAppBrowser.isPaymentDone = true; + return; + } + }); + // } + + // if(selectedPaymentMethod != "TAMARA") { + MyInAppBrowser.errorURLS.forEach((element) { + if (url.contains(element)) { + browser?.close(); + MyInAppBrowser.isPaymentDone = false; + return; + } + }); + // } + } + + onBrowserExit(bool isPaymentMade) async { + print("onBrowserExit Called!!!!"); + if (selectedPaymentMethod == "TAMARA") { + // checkTamaraPaymentStatus(transID!, appo); + // if (tamaraPaymentStatus != null && tamaraPaymentStatus.toLowerCase() == "approved") { + // updateTamaraRequestStatus("success", "14", Utils.getAppointmentTransID(appo.projectID, appo.clinicID, appo.appointmentNo), tamaraOrderID, num.parse(selectedInstallments), appo); + // } else { + // updateTamaraRequestStatus("Failed", "00", Utils.getAppointmentTransID(appo.projectID, appo.clinicID, appo.appointmentNo), tamaraOrderID, num.parse(selectedInstallments), appo); + // } + } else { + checkPaymentStatus(); + // checkPaymentStatus(appo); + } + } + + void checkPaymentStatus() async { + LoaderBottomSheet.showLoader(loadingText: "Checking payment status, Please wait...".needTranslation); + await payfortViewModel.checkPaymentStatus( + transactionID: transID, + onSuccess: (apiResponse) async { + debugPrint(apiResponse.data.toString()); + if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") { + await bookAppointmentsViewModel.addNewCallRequestForImmediateLiveCare(transID); + LoaderBottomSheet.hideLoader(); + + // showCommonBottomSheetWithoutHeight( + // context, + // child: Utils.getSuccessWidget(loadingText: "Payment Successful!".needTranslation), + // callBackFunc: () { + // Navigator.of(context).pop(); + // Navigator.of(context).pop(); + // }, + // isFullScreen: false, + // isCloseButtonVisible: true, + // ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }); + } + + openPaymentURL(String paymentMethod) { + browser = MyInAppBrowser(onExitCallback: onBrowserExit, onLoadStartCallback: onBrowserLoadStart, context: context); + transID = Utils.getAppointmentTransID( + bookAppointmentsViewModel.immediateLiveCareSelectedClinic.serviceID!, + ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12, + DateTime.now().millisecondsSinceEpoch, + ); + + //TODO: Need to pass dynamic params to the payment request instead of static values + browser?.openPaymentBrowser( + myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!, + "LiveCare Payment", + transID, + "12", + "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", + selectedPaymentMethod, + appState.getAuthenticatedUser()!.patientType.toString(), + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", + appState.getAuthenticatedUser()!.patientId.toString(), + appState.getAuthenticatedUser()!, + browser!, + false, + "4", + bookAppointmentsViewModel.immediateLiveCareSelectedClinic.serviceID.toString(), + context, + myAppointmentsViewModel.patientAppointmentShareResponseModel!.appointmentDate, + myAppointmentsViewModel.patientAppointmentShareResponseModel!.appointmentNo, + myAppointmentsViewModel.patientAppointmentShareResponseModel!.clinicID, + myAppointmentsViewModel.patientAppointmentShareResponseModel!.doctorID, + "3"); + } + + startApplePay() async { + LoaderBottomSheet.showLoader(loadingText: "Fetching Apple Pay details, Please wait...".needTranslation); + transID = Utils.getAppointmentTransID( + bookAppointmentsViewModel.immediateLiveCareSelectedClinic.serviceID!, + ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12, + DateTime.now().millisecondsSinceEpoch, + ); + + ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest(); + + await payfortViewModel.getPayfortConfigurations( + serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12, integrationId: 2); + + applePayInsertRequest.clientRequestID = transID; + applePayInsertRequest.clinicID = bookAppointmentsViewModel.immediateLiveCareSelectedClinic.serviceID!; + + applePayInsertRequest.currency = appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED"; + applePayInsertRequest.customerEmail = "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com"; + applePayInsertRequest.customerID = appState.getAuthenticatedUser()!.patientId.toString(); + applePayInsertRequest.customerName = "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}"; + + applePayInsertRequest.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken); + applePayInsertRequest.voipToken = await Utils.getStringFromPrefs(CacheConst.voipToken); + applePayInsertRequest.doctorID = 0; + applePayInsertRequest.projectID = "12"; + applePayInsertRequest.serviceID = ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum().toString(); + applePayInsertRequest.channelID = 3; + applePayInsertRequest.patientID = appState.getAuthenticatedUser()!.patientId.toString(); + applePayInsertRequest.patientTypeID = appState.getAuthenticatedUser()!.patientType; + applePayInsertRequest.patientOutSA = appState.getAuthenticatedUser()!.outSa; + applePayInsertRequest.appointmentDate = DateUtil.convertDateToString(DateTime.now()); + applePayInsertRequest.appointmentNo = 0; + applePayInsertRequest.orderDescription = "LiveCare Payment"; + applePayInsertRequest.liveServiceID = bookAppointmentsViewModel.immediateLiveCareSelectedClinic.serviceID!.toString(); + applePayInsertRequest.latitude = "0.0"; + applePayInsertRequest.longitude = "0.0"; + applePayInsertRequest.amount = bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.total.toString(); + applePayInsertRequest.isSchedule = "0"; + applePayInsertRequest.language = appState.isArabic() ? 'ar' : 'en'; + applePayInsertRequest.languageID = appState.isArabic() ? 1 : 2; + applePayInsertRequest.userName = appState.getAuthenticatedUser()!.patientId; + applePayInsertRequest.responseContinueURL = "http://hmg.com/Documents/success.html"; + applePayInsertRequest.backClickUrl = "http://hmg.com/Documents/success.html"; + applePayInsertRequest.paymentOption = "ApplePay"; + + applePayInsertRequest.isMobSDK = true; + applePayInsertRequest.merchantReference = transID; + applePayInsertRequest.merchantIdentifier = payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier; + applePayInsertRequest.commandType = "PURCHASE"; + applePayInsertRequest.signature = payfortViewModel.payfortProjectDetailsRespModel!.signature; + applePayInsertRequest.accessCode = payfortViewModel.payfortProjectDetailsRespModel!.accessCode; + applePayInsertRequest.shaRequestPhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaRequest; + applePayInsertRequest.shaResponsePhrase = payfortViewModel.payfortProjectDetailsRespModel!.shaResponse; + applePayInsertRequest.returnURL = ""; + + //TODO: Need to pass dynamic params to the Apple Pay instead of static values + await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) { + payfortViewModel.paymentWithApplePay( + customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", + // customerEmail: projectViewModel.authenticatedUserObject.user.emailAddress, + customerEmail: "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", + orderDescription: "LiveCare Payment", + orderAmount: double.parse(bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.total!), + merchantReference: transID, + merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier, + applePayAccessCode: payfortViewModel.payfortProjectDetailsRespModel!.accessCode, + applePayShaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel!.shaRequest, + currency: appState.getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED", + onFailed: (failureResult) async { + log("failureResult: ${failureResult.message.toString()}"); + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: failureResult.message.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onSucceeded: (successResult) async { + LoaderBottomSheet.hideLoader(); + log("successResult: ${successResult.responseMessage.toString()}"); + selectedPaymentMethod = successResult.paymentOption ?? "VISA"; + checkPaymentStatus(); + }, + // projectId: appo.projectID, + // serviceTypeEnum: ServiceTypeEnum.appointmentPayment, + ); + }); + } +} diff --git a/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart b/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart index 8ec6e4b..6910113 100644 --- a/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart +++ b/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart @@ -12,12 +12,15 @@ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_clinics_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/clinic_card.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/livecare_clinic_card.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/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; class SelectImmediateLiveCareClinicPage extends StatefulWidget { @@ -37,6 +40,7 @@ class _SelectImmediateLiveCareClinicPageState extends State { ), SizedBox(width: 8.h), Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true), SizedBox(height: 8.h), diff --git a/lib/widgets/loader/bottomsheet_loader.dart b/lib/widgets/loader/bottomsheet_loader.dart index aa97fdf..41d2b15 100644 --- a/lib/widgets/loader/bottomsheet_loader.dart +++ b/lib/widgets/loader/bottomsheet_loader.dart @@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -11,7 +10,7 @@ class LoaderBottomSheet { static final NavigationService _navService = GetIt.I(); static bool _isVisible = false; - static void showLoader() { + static void showLoader({String? loadingText}) { if (_isVisible) return; _isVisible = true; @@ -29,7 +28,7 @@ class LoaderBottomSheet { borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: Center( - child: Utils.getLoadingWidget(), + child: Utils.getLoadingWidget(loadingText: loadingText), ), ); }, From 34184801220d7635e2e649fa46cd2e8382d16640 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Thu, 2 Oct 2025 14:05:07 +0300 Subject: [PATCH 18/43] Immediate LiveCare implementation completed --- assets/images/svg/forward_arrow_icon.svg | 4 +- .../images/svg/forward_arrow_icon_small.svg | 4 + assets/images/svg/waiting_icon.svg | 3 + ios/Runner.xcodeproj/project.pbxproj | 4 +- lib/core/app_assets.dart | 2 + lib/core/dependencies.dart | 12 + lib/core/utils/utils.dart | 2 +- .../book_appointments_repo.dart | 154 --------- .../book_appointments_view_model.dart | 95 ------ .../immediate_livecare_repo.dart | 206 ++++++++++++ .../immediate_livecare_view_model.dart | 162 ++++++++++ ...care_immediate_clinics_response_model.dart | 0 ...ivecare_immediate_fees_response_model.dart | 0 ...tient_livecare_history_response_model.dart | 84 +++++ lib/main.dart | 9 + .../widgets/appointment_card.dart | 19 +- .../book_appointment_page.dart | 42 ++- .../immediate_livecare_payment_details.dart | 29 +- .../immediate_livecare_payment_page.dart | 83 ++--- ...mediate_livecare_pending_request_page.dart | 303 ++++++++++++++++++ ...select_immediate_livecare_clinic_page.dart | 39 +-- .../widgets/livecare_clinic_card.dart | 9 +- .../widgets/select_livecare_call_type.dart | 11 +- .../book_appointment/widgets/clinic_card.dart | 2 +- lib/presentation/home/landing_page.dart | 70 +++- 25 files changed, 990 insertions(+), 358 deletions(-) create mode 100644 assets/images/svg/forward_arrow_icon_small.svg create mode 100644 assets/images/svg/waiting_icon.svg create mode 100644 lib/features/immediate_livecare/immediate_livecare_repo.dart create mode 100644 lib/features/immediate_livecare/immediate_livecare_view_model.dart rename lib/features/{book_appointments => immediate_livecare}/models/resp_models/get_livecare_immediate_clinics_response_model.dart (100%) rename lib/features/{book_appointments => immediate_livecare}/models/resp_models/get_livecare_immediate_fees_response_model.dart (100%) create mode 100644 lib/features/immediate_livecare/models/resp_models/get_patient_livecare_history_response_model.dart create mode 100644 lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart rename lib/presentation/book_appointment/{ => livecare}/widgets/livecare_clinic_card.dart (87%) diff --git a/assets/images/svg/forward_arrow_icon.svg b/assets/images/svg/forward_arrow_icon.svg index d8a3d51..e4fd254 100644 --- a/assets/images/svg/forward_arrow_icon.svg +++ b/assets/images/svg/forward_arrow_icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/svg/forward_arrow_icon_small.svg b/assets/images/svg/forward_arrow_icon_small.svg new file mode 100644 index 0000000..c97ff8b --- /dev/null +++ b/assets/images/svg/forward_arrow_icon_small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/waiting_icon.svg b/assets/images/svg/waiting_icon.svg new file mode 100644 index 0000000..40e8645 --- /dev/null +++ b/assets/images/svg/waiting_icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a982556..be64c53 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7595037DD52211B91157B0F3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 769C9BF82E6F106D009F68A9 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 8E12CEEB8E334EE22D5259D7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -130,6 +131,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 769C9BF82E6F106D009F68A9 /* RunnerDebug.entitlements */, 478CFA952E6E20A60064F3D7 /* Runner.entitlements */, 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */, 97C146FA1CF9000F007C117D /* Main.storyboard */, @@ -635,7 +637,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 3A359E86ZF; ENABLE_BITCODE = NO; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 016e42d..44f979c 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -147,6 +147,8 @@ class AppAssets { static const String ic_critical_low_result = '$svgBasePath/critical_low_result.svg'; static const String livecare_online_icon = '$svgBasePath/livecare_online_icon.svg'; static const String edit_icon = '$svgBasePath/edit_icon.svg'; + static const String waiting_icon = '$svgBasePath/waiting_icon.svg'; + static const String forward_arrow_icon_small = '$svgBasePath/forward_arrow_icon_small.svg'; //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index b42706f..33fa0df 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -10,6 +10,8 @@ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments import 'package:hmg_patient_app_new/features/common/common_repo.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart'; 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_repo.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_repo.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/lab_repo.dart'; @@ -92,6 +94,7 @@ class AppDependencies { getIt.registerLazySingleton(() => LocalAuthService(loggerService: getIt(), localAuth: getIt())); getIt.registerLazySingleton(() => HabibWalletRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => MedicalFileRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => ImmediateLiveCareRepoImp(loggerService: getIt(), apiClient: getIt())); // ViewModels // Global/shared VMs → LazySingleton @@ -164,6 +167,15 @@ class AppDependencies { ), ); + getIt.registerLazySingleton( + () => ImmediateLiveCareViewModel( + immediateLiveCareRepo: getIt(), + errorHandlerService: getIt(), + navigationService: getIt(), + myAppointmentsViewModel: getIt(), + ), + ); + getIt.registerLazySingleton( () => AuthenticationViewModel( authenticationRepo: getIt(), cacheService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), localAuthService: getIt()), diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 8830b78..f3a53b9 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -360,7 +360,7 @@ class Utils { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Lottie.asset(AppAnimations.warningAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 128.h, height: 128.h, fit: BoxFit.fill), + Lottie.asset(AppAnimations.warningAnimation, repeat: false, reverse: false, frameRate: FrameRate(60), width: 128.h, height: 128.h, fit: BoxFit.fill), SizedBox(height: 8.h), (loadingText ?? LocaleKeys.loadingText.tr()).toText14(color: AppColors.blackColor, letterSpacing: 0), SizedBox(height: 16.h), diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index 0a137ce..43a7eb3 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -10,8 +10,6 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/resp_model import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_clinics_response_model.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_fees_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_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/services/logger_service.dart'; @@ -70,15 +68,6 @@ abstract class BookAppointmentsRepo { required int serviceID, Function(dynamic)? onSuccess, Function(String)? onError}); - - Future>>> getLiveCareImmediateClinicsList(int age, int genderID, {Function(dynamic)? onSuccess, Function(String)? onError}); - - Future>> getLiveCareImmediateAppointmentFees(int age, int genderID, int serviceID, - {Function(dynamic)? onSuccess, Function(String)? onError}); - - Future>> addNewCallRequestForImmediateLiveCare( - int age, int gender, int serviceID, String clientRequestID, int callTypeID, bool isPharma, String deviceToken, String voipToken, - {Function(dynamic)? onSuccess, Function(String)? onError}); } class BookAppointmentsRepoImp implements BookAppointmentsRepo { @@ -162,9 +151,6 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { final list = response['DoctorList']; - // if (list == null || list.isEmpty) { - // throw Exception("lab list is empty"); - // } final doctorsList = list.map((item) => DoctorsListResponseModel.fromJson(item as Map)).toList().cast(); @@ -670,144 +656,4 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } - - @override - Future>>> getLiveCareImmediateClinicsList(int age, int genderID, - {Function(dynamic)? onSuccess, Function(String)? onError}) async { - Map mapDevice = { - "Age": age, - "Gender": genderID, - }; - - try { - GenericApiModel>? apiResponse; - Failure? failure; - await apiClient.post( - GET_LIVECARE_CLINICS, - body: mapDevice, - onFailure: (error, statusCode, {messageStatus, failureType}) { - failure = failureType; - onError!(error); - }, - onSuccess: (response, statusCode, {messageStatus, errorMessage}) { - try { - final list = response['PatientER_GetClinicsList']; - - final clinicsList = list.map((item) => GetLiveCareClinicListResponseModel.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())); - } - } - - @override - Future>> getLiveCareImmediateAppointmentFees(int age, int genderID, int serviceID, - {Function(dynamic)? onSuccess, Function(String)? onError}) async { - Map mapDevice = { - "Age": age, - "Gender": genderID, - "ServiceID": serviceID, - }; - - try { - GenericApiModel? apiResponse; - Failure? failure; - await apiClient.post( - GET_ER_APPOINTMENT_FEES, - body: mapDevice, - onFailure: (error, statusCode, {messageStatus, failureType}) { - failure = failureType; - onError!(error); - }, - onSuccess: (response, statusCode, {messageStatus, errorMessage}) { - try { - final respObject = response['GetERAppointmentFeesList']; - - final liveCareFeesObj = LiveCareImmediateAppointmentFeesList.fromJson(respObject); - - apiResponse = GenericApiModel( - messageStatus: messageStatus, - statusCode: statusCode, - errorMessage: null, - data: liveCareFeesObj, - ); - } 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())); - } - } - - @override - Future> addNewCallRequestForImmediateLiveCare( - int age, int gender, int serviceID, String clientRequestID, int callTypeID, bool isPharma, String deviceToken, String voipToken, - {Function(dynamic p1)? onSuccess, Function(String p1)? onError}) async { - Map mapDevice = { - "IsPharmacy": isPharma, - "ErServiceID": serviceID, - "ClientRequestID": clientRequestID, - "DeviceToken": deviceToken, - "VoipToken": voipToken, - "IsFlutter": true, - "DeviceType": Platform.isIOS ? 'iOS' : 'Android', - "Age": age, - "Gender": gender, - "IsVoip": Platform.isIOS ? true : false, - "CallTypeID": callTypeID - }; - - try { - GenericApiModel? apiResponse; - Failure? failure; - await apiClient.post( - ADD_NEW_CALL_FOR_PATIENT_ER, - body: mapDevice, - onFailure: (error, statusCode, {messageStatus, failureType}) { - failure = failureType; - onError!(error); - }, - onSuccess: (response, statusCode, {messageStatus, errorMessage}) { - try { - // final respObject = response['GetERAppointmentFeesList']; - - // final liveCareFeesObj = LiveCareImmediateAppointmentFeesList.fromJson(respObject); - - apiResponse = GenericApiModel( - messageStatus: messageStatus, - statusCode: statusCode, - errorMessage: null, - data: true, - ); - } 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 120aa3b..8a46f91 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -15,8 +15,6 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/free_slot. import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_fees_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/timeslots.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; @@ -60,13 +58,6 @@ class BookAppointmentsViewModel extends ChangeNotifier { DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel(); GetLiveCareClinicsResponseModel selectedLiveCareClinic = GetLiveCareClinicsResponseModel(); - //Immediate LiveCare - List immediateLiveCareClinicsList = []; - bool isImmediateLiveCareClinicsLoading = false; - int liveCareSelectedCallType = 0; // 1- Video, 2- Audio, 3- Phone - late GetLiveCareClinicListResponseModel immediateLiveCareSelectedClinic; - late LiveCareImmediateAppointmentFeesList liveCareImmediateAppointmentFeesList; - late DoctorsProfileResponseModel doctorsProfileResponseModel; List slotsList = []; @@ -123,10 +114,6 @@ class BookAppointmentsViewModel extends ChangeNotifier { clinicsList.clear(); doctorsList.clear(); liveCareClinicsList.clear(); - - immediateLiveCareClinicsList.clear(); - isImmediateLiveCareClinicsLoading = true; - liveCareSelectedCallType = 0; // getLocation(); notifyListeners(); } @@ -193,16 +180,6 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } - setLiveCareSelectedCallType(int value) { - liveCareSelectedCallType = value; - notifyListeners(); - } - - setImmediateLiveCareSelectedClinic(GetLiveCareClinicListResponseModel clinic) { - immediateLiveCareSelectedClinic = clinic; - notifyListeners(); - } - /// this function will decide which clinic api to be called /// either api for region flow or the select clinic api Future getClinics() async { @@ -777,76 +754,4 @@ class BookAppointmentsViewModel extends ChangeNotifier { void getLocation() { locationUtils.getLocation(); } - - Future getLiveCareImmediateClinicsList({Function(dynamic)? onSuccess, Function(String)? onError}) async { - immediateLiveCareClinicsList.clear(); - isImmediateLiveCareClinicsLoading = true; - notifyListeners(); - - final result = await bookAppointmentsRepo.getLiveCareImmediateClinicsList(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!); - - 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) { - immediateLiveCareClinicsList = apiResponse.data!; - - immediateLiveCareClinicsList.sort((a, b) => b.isOnline!.compareTo(a.isOnline!)); - - isImmediateLiveCareClinicsLoading = false; - notifyListeners(); - if (onSuccess != null) { - onSuccess(apiResponse); - } - } - }, - ); - } - - Future getLiveCareImmediateAppointmentFees({Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = - await bookAppointmentsRepo.getLiveCareImmediateAppointmentFees(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!, immediateLiveCareSelectedClinic.serviceID!); - - result.fold( - (failure) async { - onError!(failure.message); - }, - (apiResponse) { - if (apiResponse.messageStatus == 2) { - onError!(apiResponse.errorMessage ?? "Unknown error occurred"); - // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); - } else if (apiResponse.messageStatus == 1) { - liveCareImmediateAppointmentFeesList = apiResponse.data!; - notifyListeners(); - if (onSuccess != null) { - onSuccess(apiResponse); - } - } - }, - ); - } - - Future addNewCallRequestForImmediateLiveCare(String transID, {Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await bookAppointmentsRepo.addNewCallRequestForImmediateLiveCare(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!, - immediateLiveCareSelectedClinic.serviceID!, transID, liveCareSelectedCallType, false, _appState.deviceToken, await Utils.getStringFromPrefs(CacheConst.voipToken)); - - result.fold( - (failure) async { - onError!(failure.message); - }, - (apiResponse) { - if (apiResponse.messageStatus == 2) { - onError!(apiResponse.errorMessage ?? "Unknown error occurred"); - // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); - } else if (apiResponse.messageStatus == 1) { - notifyListeners(); - if (onSuccess != null) { - onSuccess(apiResponse); - } - } - }, - ); - } } diff --git a/lib/features/immediate_livecare/immediate_livecare_repo.dart b/lib/features/immediate_livecare/immediate_livecare_repo.dart new file mode 100644 index 0000000..eee4adb --- /dev/null +++ b/lib/features/immediate_livecare/immediate_livecare_repo.dart @@ -0,0 +1,206 @@ +import 'dart:io'; + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_livecare_immediate_fees_response_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_patient_livecare_history_response_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class ImmediateLiveCareRepo { + Future>>> getLiveCareImmediateClinicsList(int age, int genderID, {Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>> getLiveCareImmediateAppointmentFees(int age, int genderID, int serviceID, + {Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>> addNewCallRequestForImmediateLiveCare( + int age, int gender, int serviceID, String clientRequestID, int callTypeID, bool isPharma, String deviceToken, String voipToken, + {Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>>> getPatientLiveCareHistory({Function(dynamic)? onSuccess, Function(String)? onError}); +} + +class ImmediateLiveCareRepoImp implements ImmediateLiveCareRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + ImmediateLiveCareRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>>> getLiveCareImmediateClinicsList(int age, int genderID, + {Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = { + "Age": age, + "Gender": genderID, + }; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_LIVECARE_CLINICS, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['PatientER_GetClinicsList']; + + final clinicsList = list.map((item) => GetLiveCareClinicListResponseModel.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())); + } + } + + @override + Future>> getLiveCareImmediateAppointmentFees(int age, int genderID, int serviceID, + {Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = { + "Age": age, + "Gender": genderID, + "ServiceID": serviceID, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_ER_APPOINTMENT_FEES, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final respObject = response['GetERAppointmentFeesList']; + + final liveCareFeesObj = LiveCareImmediateAppointmentFeesList.fromJson(respObject); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: liveCareFeesObj, + ); + } 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())); + } + } + + @override + Future> addNewCallRequestForImmediateLiveCare( + int age, int gender, int serviceID, String clientRequestID, int callTypeID, bool isPharma, String deviceToken, String voipToken, + {Function(dynamic p1)? onSuccess, Function(String p1)? onError}) async { + Map mapDevice = { + "IsPharmacy": isPharma, + "ErServiceID": serviceID, + "ClientRequestID": clientRequestID, + "DeviceToken": deviceToken, + "VoipToken": voipToken, + "IsFlutter": true, + "DeviceType": Platform.isIOS ? 'iOS' : 'Android', + "Age": age, + "Gender": gender, + "IsVoip": Platform.isIOS ? true : false, + "CallTypeID": callTypeID + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ADD_NEW_CALL_FOR_PATIENT_ER, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: true, + ); + } 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())); + } + } + + @override + Future>>> getPatientLiveCareHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_LIVECARE_HISTORY, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['ErRequestHistoryList']; + + final liveCareHistoryList = list.map((item) => PatientLiveCareHistory.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: liveCareHistoryList, + ); + } 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/immediate_livecare/immediate_livecare_view_model.dart b/lib/features/immediate_livecare/immediate_livecare_view_model.dart new file mode 100644 index 0000000..599bc39 --- /dev/null +++ b/lib/features/immediate_livecare/immediate_livecare_view_model.dart @@ -0,0 +1,162 @@ +import 'package:flutter/cupertino.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_repo.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_livecare_immediate_fees_response_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_patient_livecare_history_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +import '../../services/navigation_service.dart'; + +class ImmediateLiveCareViewModel extends ChangeNotifier { + ImmediateLiveCareViewModel({ + required this.immediateLiveCareRepo, + required this.errorHandlerService, + required this.navigationService, + required this.myAppointmentsViewModel, + }); + + ImmediateLiveCareRepo immediateLiveCareRepo; + ErrorHandlerService errorHandlerService; + final NavigationService navigationService; + MyAppointmentsViewModel myAppointmentsViewModel; + + List immediateLiveCareClinicsList = []; + bool isImmediateLiveCareClinicsLoading = false; + int liveCareSelectedCallType = 0; // 1- Video, 2- Audio, 3- Phone + late GetLiveCareClinicListResponseModel immediateLiveCareSelectedClinic; + late LiveCareImmediateAppointmentFeesList liveCareImmediateAppointmentFeesList; + + List patientLiveCareHistoryList = []; + bool patientHasPendingLiveCareRequest = false; + + late AppState _appState; + + initImmediateLiveCare() { + _appState = getIt(); + immediateLiveCareClinicsList = []; + patientLiveCareHistoryList = []; + isImmediateLiveCareClinicsLoading = true; + patientHasPendingLiveCareRequest = false; + liveCareSelectedCallType = 0; // 1- Video, 2- Audio, 3- Phone + immediateLiveCareSelectedClinic = GetLiveCareClinicListResponseModel(); + liveCareImmediateAppointmentFeesList = LiveCareImmediateAppointmentFeesList(); + } + + setLiveCareSelectedCallType(int value) { + liveCareSelectedCallType = value; + notifyListeners(); + } + + setImmediateLiveCareSelectedClinic(GetLiveCareClinicListResponseModel clinic) { + immediateLiveCareSelectedClinic = clinic; + notifyListeners(); + } + + Future getLiveCareImmediateClinicsList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + immediateLiveCareClinicsList.clear(); + isImmediateLiveCareClinicsLoading = true; + notifyListeners(); + + final result = await immediateLiveCareRepo.getLiveCareImmediateClinicsList(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!); + + 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) { + immediateLiveCareClinicsList = apiResponse.data!; + + immediateLiveCareClinicsList.sort((a, b) => b.isOnline!.compareTo(a.isOnline!)); + + isImmediateLiveCareClinicsLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getLiveCareImmediateAppointmentFees({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = + await immediateLiveCareRepo.getLiveCareImmediateAppointmentFees(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!, immediateLiveCareSelectedClinic.serviceID!); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage ?? "Unknown error occurred"); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + liveCareImmediateAppointmentFeesList = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future addNewCallRequestForImmediateLiveCare(String transID, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await immediateLiveCareRepo.addNewCallRequestForImmediateLiveCare(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!, + immediateLiveCareSelectedClinic.serviceID!, transID, liveCareSelectedCallType, false, _appState.deviceToken, await Utils.getStringFromPrefs(CacheConst.voipToken)); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage ?? "Unknown error occurred"); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getPatientLiveCareHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await immediateLiveCareRepo.getPatientLiveCareHistory(); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage ?? "Unknown error occurred"); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + patientLiveCareHistoryList = apiResponse.data!; + if (patientLiveCareHistoryList.isNotEmpty) { + if (patientLiveCareHistoryList[0].callStatus! < 4) { + patientHasPendingLiveCareRequest = true; + } else { + patientHasPendingLiveCareRequest = false; + } + } else { + patientHasPendingLiveCareRequest = false; + } + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } +} diff --git a/lib/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart b/lib/features/immediate_livecare/models/resp_models/get_livecare_immediate_clinics_response_model.dart similarity index 100% rename from lib/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart rename to lib/features/immediate_livecare/models/resp_models/get_livecare_immediate_clinics_response_model.dart diff --git a/lib/features/book_appointments/models/resp_models/get_livecare_immediate_fees_response_model.dart b/lib/features/immediate_livecare/models/resp_models/get_livecare_immediate_fees_response_model.dart similarity index 100% rename from lib/features/book_appointments/models/resp_models/get_livecare_immediate_fees_response_model.dart rename to lib/features/immediate_livecare/models/resp_models/get_livecare_immediate_fees_response_model.dart diff --git a/lib/features/immediate_livecare/models/resp_models/get_patient_livecare_history_response_model.dart b/lib/features/immediate_livecare/models/resp_models/get_patient_livecare_history_response_model.dart new file mode 100644 index 0000000..0077d83 --- /dev/null +++ b/lib/features/immediate_livecare/models/resp_models/get_patient_livecare_history_response_model.dart @@ -0,0 +1,84 @@ +class PatientLiveCareHistory { + String? appointmentNo; + String? arrivalTime; + num? callDuration; + int? callStatus; + String? clientRequestID; + String? doctorID; + String? doctorName; + String? doctorNameN; + String? doctorTitle; + String? exWaitingTime; + bool? isAppointmentHaveRating; + int? patCount; + int? projectID; + String? sArrivalTime; + int? serviceID; + String? stringCallStatus; + int? vCID; + int? watingtimeInteger; + + PatientLiveCareHistory( + {this.appointmentNo, + this.arrivalTime, + this.callDuration, + this.callStatus, + this.clientRequestID, + this.doctorID, + this.doctorName, + this.doctorNameN, + this.doctorTitle, + this.exWaitingTime, + this.isAppointmentHaveRating, + this.patCount, + this.projectID, + this.sArrivalTime, + this.serviceID, + this.stringCallStatus, + this.vCID, + this.watingtimeInteger}); + + PatientLiveCareHistory.fromJson(Map json) { + appointmentNo = json['AppointmentNo']; + arrivalTime = json['ArrivalTime']; + callDuration = json['CallDuration']; + callStatus = json['CallStatus']; + clientRequestID = json['ClientRequestID']; + doctorID = json['DoctorID']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + doctorTitle = json['DoctorTitle']; + exWaitingTime = json['Ex_WaitingTime']; + isAppointmentHaveRating = json['IsAppointmentHaveRating']; + patCount = json['Pat_Count']; + projectID = json['ProjectID']; + sArrivalTime = json['SArrivalTime']; + serviceID = json['ServiceID']; + stringCallStatus = json['StringCallStatus']; + vCID = json['VC_ID']; + watingtimeInteger = json['WatingtimeInteger']; + } + + Map toJson() { + final Map data = new Map(); + data['AppointmentNo'] = this.appointmentNo; + data['ArrivalTime'] = this.arrivalTime; + data['CallDuration'] = this.callDuration; + data['CallStatus'] = this.callStatus; + data['ClientRequestID'] = this.clientRequestID; + data['DoctorID'] = this.doctorID; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['DoctorTitle'] = this.doctorTitle; + data['Ex_WaitingTime'] = this.exWaitingTime; + data['IsAppointmentHaveRating'] = this.isAppointmentHaveRating; + data['Pat_Count'] = this.patCount; + data['ProjectID'] = this.projectID; + data['SArrivalTime'] = this.sArrivalTime; + data['ServiceID'] = this.serviceID; + data['StringCallStatus'] = this.stringCallStatus; + data['VC_ID'] = this.vCID; + data['WatingtimeInteger'] = this.watingtimeInteger; + return data; + } +} diff --git a/lib/main.dart b/lib/main.dart index 3690c72..eff60cc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; 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/lab/history/lab_history_viewmodel.dart'; import 'package:hmg_patient_app_new/features/lab/lab_range_view_model.dart'; @@ -137,6 +138,14 @@ void main() async { locationUtils: getIt(), ), ), + ChangeNotifierProvider( + create: (_) => ImmediateLiveCareViewModel( + immediateLiveCareRepo: getIt(), + errorHandlerService: getIt(), + navigationService: getIt(), + myAppointmentsViewModel: getIt(), + ), + ), ChangeNotifierProvider( create: (_) => AuthenticationViewModel( authenticationRepo: getIt(), diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 6d157a5..3a65124 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -243,17 +243,14 @@ class _AppointmentCardState extends State { color: AppColors.textColor, borderRadius: 10.h, ), - child: Padding( - padding: EdgeInsets.all(10.h), - child: Transform.flip( - flipX: appState.isArabic() ? true : false, - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, - iconColor: AppColors.whiteColor, - width: 10.h, - height: 10.h, - fit: BoxFit.contain, - ), + child: Transform.flip( + flipX: appState.isArabic() ? true : false, + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.whiteColor, + width: 40.h, + height: 40.h, + fit: BoxFit.cover, ), ), ).toShimmer2(isShow: widget.isLoading).onPress(() { diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 012a8db..516aff9 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -10,11 +10,13 @@ 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/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_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/resp_models/doctor_list_api_response.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart' show RegionBottomSheetBody; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/search_doctor_by_name.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; @@ -22,6 +24,7 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart' show showCommonBottomSheetWithoutHeight; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; @@ -39,6 +42,7 @@ class _BookAppointmentPageState extends State { late AppState appState; late AppointmentViaRegionViewmodel regionalViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; + late ImmediateLiveCareViewModel immediateLiveCareViewModel; @override void initState() { @@ -46,6 +50,7 @@ class _BookAppointmentPageState extends State { bookAppointmentsViewModel.selectedTabIndex = 0; bookAppointmentsViewModel.initBookAppointmentViewModel(); bookAppointmentsViewModel.getLocation(); + immediateLiveCareViewModel.initImmediateLiveCare(); }); super.initState(); } @@ -53,6 +58,7 @@ class _BookAppointmentPageState extends State { @override Widget build(BuildContext context) { bookAppointmentsViewModel = Provider.of(context, listen: false); + immediateLiveCareViewModel = Provider.of(context, listen: false); appState = getIt.get(); regionalViewModel = Provider.of(context, listen: true); return Scaffold( @@ -120,7 +126,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h)), + flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setIsClinicsListLoading(true); @@ -153,7 +159,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h)), + flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setIsDoctorSearchByNameStarted(false); @@ -184,7 +190,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h)), + flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setProjectID(null); @@ -228,16 +234,28 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h)), + flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], - ).onPress(() { + ).onPress(() async { //TODO Implement API to check for existing LiveCare Requests - Navigator.of(context).push( - CustomPageRoute( - page: SelectImmediateLiveCareClinicPage(), - ), - ); + LoaderBottomSheet.showLoader(); + await immediateLiveCareViewModel.getPatientLiveCareHistory(); + LoaderBottomSheet.hideLoader(); + + if (immediateLiveCareViewModel.patientHasPendingLiveCareRequest) { + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + Navigator.of(context).push( + CustomPageRoute( + page: SelectImmediateLiveCareClinicPage(), + ), + ); + } }), SizedBox(height: 16.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), @@ -259,7 +277,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h)), + flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setIsClinicsListLoading(true); @@ -290,7 +308,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 15.h, height: 15.h)), + flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION); diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart index 76f6ad1..af7d053 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -10,6 +10,7 @@ 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/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart'; @@ -27,12 +28,12 @@ import 'package:smooth_corner/smooth_corner.dart'; class ImmediateLiveCarePaymentDetails extends StatelessWidget { ImmediateLiveCarePaymentDetails({super.key}); - late BookAppointmentsViewModel bookAppointmentsViewModel; + late ImmediateLiveCareViewModel immediateLiveCareViewModel; late AppState appState; @override Widget build(BuildContext context) { - bookAppointmentsViewModel = Provider.of(context, listen: false); + immediateLiveCareViewModel = Provider.of(context, listen: false); appState = getIt.get(); return Scaffold( backgroundColor: AppColors.scaffoldBgColor, @@ -95,7 +96,9 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - (appState.isArabic() ? bookAppointmentsViewModel.immediateLiveCareSelectedClinic.serviceNameN : bookAppointmentsViewModel.immediateLiveCareSelectedClinic.serviceName)! + (appState.isArabic() + ? immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceNameN + : immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceName)! .toText16(isBold: true), // SizedBox(height: 8.h), // AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), @@ -124,7 +127,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { children: [ Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 32.h, height: 32.h, fit: BoxFit.contain), SizedBox(width: 8.h), - getLiveCareType(bookAppointmentsViewModel.liveCareSelectedCallType).toText16(isBold: true), + getLiveCareType(immediateLiveCareViewModel.liveCareSelectedCallType).toText16(isBold: true), ], ), Utils.buildSvgWithAssets(icon: AppAssets.edit_icon, width: 24.h, height: 24.h, fit: BoxFit.contain), @@ -132,8 +135,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { ), ), ).onPress(() { - showCommonBottomSheetWithoutHeight(context, child: SelectLiveCareCallType(bookAppointmentsViewModel: bookAppointmentsViewModel), callBackFunc: () async { - debugPrint("Selected Call Type: ${bookAppointmentsViewModel.liveCareSelectedCallType}"); + showCommonBottomSheetWithoutHeight(context, child: SelectLiveCareCallType(immediateLiveCareViewModel: immediateLiveCareViewModel), callBackFunc: () async { + debugPrint("Selected Call Type: ${immediateLiveCareViewModel.liveCareSelectedCallType}"); }, title: "Select LiveCare call type".needTranslation, isCloseButtonVisible: true, isFullScreen: false); }); }), @@ -152,7 +155,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - (bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.isCash ?? true) + (immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.isCash ?? true) ? Container( height: 50.h, decoration: ShapeDecoration( @@ -195,8 +198,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ "Amount before tax".needTranslation.toText14(isBold: true), - Utils.getPaymentAmountWithSymbol(bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.amount!.toText16(isBold: true), AppColors.blackColor, 13, - isSaudiCurrency: bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), + Utils.getPaymentAmountWithSymbol(immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.amount!.toText16(isBold: true), AppColors.blackColor, 13, + isSaudiCurrency: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), ], ).paddingSymmetrical(24.h, 0.h), Row( @@ -204,8 +207,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { children: [ "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), Utils.getPaymentAmountWithSymbol( - bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.tax!.toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, - isSaudiCurrency: bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), + immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.tax!.toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, + isSaudiCurrency: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), ], ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 17.h), @@ -213,8 +216,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox(width: 150.h, child: Utils.getPaymentMethods()), - Utils.getPaymentAmountWithSymbol(bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.total!.toText24(isBold: true), AppColors.blackColor, 17, - isSaudiCurrency: bookAppointmentsViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), + Utils.getPaymentAmountWithSymbol(immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total!.toText24(isBold: true), AppColors.blackColor, 17, + isSaudiCurrency: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), ], ).paddingSymmetrical(24.h, 0.h), CustomButton( diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index d4a108c..d89ef33 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -22,6 +23,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; @@ -45,7 +47,7 @@ class ImmediateLiveCarePaymentPage extends StatefulWidget { class _ImmediateLiveCarePaymentPageState extends State { late PayfortViewModel payfortViewModel; - late BookAppointmentsViewModel bookAppointmentsViewModel; + late ImmediateLiveCareViewModel immediateLiveCareViewModel; late MyAppointmentsViewModel myAppointmentsViewModel; late AppState appState; @@ -66,7 +68,7 @@ class _ImmediateLiveCarePaymentPageState extends State(); myAppointmentsViewModel = Provider.of(context); - bookAppointmentsViewModel = Provider.of(context, listen: false); + immediateLiveCareViewModel = Provider.of(context, listen: false); payfortViewModel = Provider.of(context); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, @@ -75,7 +77,7 @@ class _ImmediateLiveCarePaymentPageState extends State false); + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unknown error occurred...".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } } else { showCommonBottomSheetWithoutHeight( context, @@ -388,14 +401,14 @@ class _ImmediateLiveCarePaymentPageState extends State createState() => _ImmediateLiveCarePendingRequestPageState(); +} + +class _ImmediateLiveCarePendingRequestPageState extends State { + late ImmediateLiveCareViewModel immediateLiveCareViewModel; + + late AppState appState; + + static Duration countdownDuration = Duration(minutes: 1, seconds: 0); + ValueNotifier durationNotifier = ValueNotifier(countdownDuration); + Timer? timer; + + @override + void initState() { + super.initState(); + scheduleMicrotask(() { + countdownDuration = Duration(minutes: immediateLiveCareViewModel.patientLiveCareHistoryList[0].watingtimeInteger!, seconds: 0); + durationNotifier = ValueNotifier(countdownDuration); + startTimer(); + }); + } + + @override + void dispose() { + timer?.cancel(); + durationNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + immediateLiveCareViewModel = Provider.of(context, listen: false); + appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(builder: (context, immediateLiveCareVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "LiveCare Pending Request".needTranslation, + child: Padding( + padding: EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + side: BorderSide(color: AppColors.ratingColorYellow, width: 3.h), + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Expected waiting time: ".toText16(isBold: true), + SizedBox(height: 8.h), + ValueListenableBuilder( + valueListenable: durationNotifier, + builder: (context, duration, child) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildTime(duration), + ], + ); + }, + ), + SizedBox(height: 8.h), + ], + ), + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + side: BorderSide(color: AppColors.ratingColorYellow, width: 3.h), + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + labelText: immediateLiveCareViewModel.patientLiveCareHistoryList[0].stringCallStatus, + backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20), + textColor: AppColors.alertColor, + ), + Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), + ], + ), + SizedBox(height: 8.h), + "Hala ${appState.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true), + SizedBox(height: 8.h), + AppCustomChipWidget( + icon: AppAssets.appointment_calendar_icon, + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(immediateLiveCareViewModel.patientLiveCareHistoryList[0].arrivalTime), false)), + SizedBox(height: 8.h), + "Your turn is after ${immediateLiveCareViewModel.patientLiveCareHistoryList[0].patCount} patients.".toText16(isBold: true), + SizedBox(height: 8.h), + ], + ), + ), + ) + ], + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: CustomButton( + text: "Call LiveCare Support".needTranslation, + onPressed: () async { + launchUrl(Uri.parse("tel://" + "011 525 9553")); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + icon: AppAssets.call_fill, + iconColor: AppColors.whiteColor, + iconSize: 21.h, + ).paddingSymmetrical(24.h, 24.h), + ), + ], + ); + }), + ); + } + + void startTimer() { + timer = Timer.periodic(const Duration(seconds: 1), (_) => addTime()); + setState(() {}); + } + + void addTime() { + final seconds = durationNotifier.value.inSeconds - 1; + if (seconds < 0) { + timer?.cancel(); + // Handle end of timer here + // showEndMessage(); + } else { + durationNotifier.value = Duration(seconds: seconds); + } + } + + Future _onWillPop() async { + timer?.cancel(); + Navigator.of(context).pop(); + return true; + } + + Widget buildTime(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final hours = twoDigits(duration.inHours); + final minutes = twoDigits(duration.inMinutes.remainder(60)); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildTimeColumn(hours, "Hours".needTranslation), + buildTimeColumn(minutes, "Mins".needTranslation), + buildTimeColumn(seconds, "Secs".needTranslation, isLast: true), + ], + ); + } + + Widget buildTimeColumn(String time, String label, {bool isLast = false}) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + buildDigit(time[0]), + buildDigit(time[1]), + if (!isLast) buildTimeSeparator(), + ], + ), + buildLabel(label), + ], + ); + } + + Widget buildDigit(String digit) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + // margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: ClipRect( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 600), + switchInCurve: Curves.easeOutExpo, + switchOutCurve: Curves.easeInExpo, + transitionBuilder: (Widget child, Animation animation) { + return Stack( + children: [ + SlideTransition( + position: Tween( + begin: const Offset(0, -1), + end: const Offset(0, 1), + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + )), + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + SlideTransition( + position: Tween( + begin: const Offset(0, -1), + end: const Offset(0, 0), + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.bounceIn, + )), + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + ], + ); + }, + child: Text( + digit, + key: ValueKey(digit), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black, + fontSize: 20.fSize, + ), + ), + ), + ), + ); + } + + Widget buildLabel(String label) { + return label.toText14(isBold: true); + } + + Widget buildTimeSeparator() { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 2.0), + child: Text( + ":", + style: TextStyle( + color: Colors.black, + fontSize: 20, + ), + ), + ); + } +} diff --git a/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart b/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart index 6910113..8181d25 100644 --- a/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart +++ b/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart @@ -11,11 +11,12 @@ 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/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_clinics_response_model.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/clinic_card.dart'; -import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/livecare_clinic_card.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/livecare_clinic_card.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/common_bottom_sheet.dart'; @@ -34,13 +35,13 @@ class _SelectImmediateLiveCareClinicPageState extends State(context, listen: false); + immediateLiveCareViewModel = Provider.of(context, listen: false); appState = getIt.get(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, @@ -62,7 +63,7 @@ class _SelectImmediateLiveCareClinicPageState extends State(builder: (context, bookAppointmentsVM, child) { + child: Consumer(builder: (context, immediateLiveCareVM, child) { return Column( children: [ // SizedBox(height: 16.h), @@ -98,14 +99,14 @@ class _SelectImmediateLiveCareClinicPageState extends State(), liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isImmediateLiveCareClinicsLoading, + isLoading: immediateLiveCareVM.isImmediateLiveCareClinicsLoading, ) : AnimationConfiguration.staggeredList( position: index, @@ -118,11 +119,11 @@ class _SelectImmediateLiveCareClinicPageState extends State { final CacheService cacheService = GetIt.instance(); late InsuranceViewModel insuranceViewModel; + late ImmediateLiveCareViewModel immediateLiveCareViewModel; final SwiperController _controller = SwiperController(); @@ -85,6 +89,8 @@ class _LandingPageState extends State { myAppointmentsViewModel.getPatientMyDoctors(); prescriptionsViewModel.initPrescriptionsViewModel(); insuranceViewModel.initInsuranceProvider(); + immediateLiveCareViewModel.initImmediateLiveCare(); + immediateLiveCareViewModel.getPatientLiveCareHistory(); } }); super.initState(); @@ -97,6 +103,7 @@ class _LandingPageState extends State { myAppointmentsViewModel = Provider.of(context, listen: false); prescriptionsViewModel = Provider.of(context, listen: false); insuranceViewModel = Provider.of(context, listen: false); + immediateLiveCareViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: SingleChildScrollView( @@ -124,11 +131,11 @@ class _LandingPageState extends State { backgroundColor: Color(0xffFEE9EA), borderColor: Color(0xffFEE9EA), textColor: Color(0xffED1C2B), - fontSize: 16, + fontSize: 14, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 50, + height: 40, ), Row( mainAxisSize: MainAxisSize.min, @@ -274,7 +281,64 @@ class _LandingPageState extends State { ), ).paddingSymmetrical(24.h, 0.h); }), - SizedBox(height: 12.h), + Consumer(builder: (context, immediateLiveCareVM, child) { + return immediateLiveCareVM.patientHasPendingLiveCareRequest + ? Column( + children: [ + SizedBox(height: 12.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + side: BorderSide(color: AppColors.ratingColorYellow, width: 3.h), + ), + width: double.infinity, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + labelText: immediateLiveCareViewModel.patientLiveCareHistoryList[0].stringCallStatus, + backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20), + textColor: AppColors.alertColor, + ), + Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), + ], + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "You have a pending LiveCare request".needTranslation.toText12(isBold: true), + Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 20.h, + height: 15.h, + fit: BoxFit.contain, + ), + ], + ), + ], + ), + ), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + }), + SizedBox(height: 12.h), + ], + ) + : SizedBox.shrink(); + }), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ From 6ef9ae813de469af6fd80671ef620b954f62c8b8 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Thu, 2 Oct 2025 15:14:44 +0300 Subject: [PATCH 19/43] doctors search filter added --- assets/images/svg/cross_circle.svg | 3 + assets/images/svg/filters.svg | 8 + assets/images/svg/ic_close.svg | 3 + assets/langs/ar-SA.json | 10 +- assets/langs/en-US.json | 10 +- lib/core/app_assets.dart | 3 + .../book_appointments_view_model.dart | 147 +++++++++++ .../doctor_filter_view_model.dart | 110 ++++++++ lib/generated/locale_keys.g.dart | 14 + lib/main.dart | 5 +- ...l_bottom_sheet_body_for_doctor_filter.dart | 66 +++++ .../doctor_filter/RegionChips.dart | 60 +++++ .../doctor_filter/clinic_bottomsheet.dart | 85 ++++++ .../doctor_filter/clinic_item.dart | 52 ++++ .../doctor_filter/doctors_filter.dart | 249 ++++++++++++++++++ .../doctor_filter/facility_Chips.dart | 65 +++++ .../search_doctor_by_name.dart | 122 ++++++--- .../book_appointment/select_doctor_page.dart | 84 ++++-- lib/widgets/chip/app_custom_chip_widget.dart | 2 +- lib/widgets/input_widget.dart | 23 +- 20 files changed, 1050 insertions(+), 71 deletions(-) create mode 100644 assets/images/svg/cross_circle.svg create mode 100644 assets/images/svg/filters.svg create mode 100644 assets/images/svg/ic_close.svg create mode 100644 lib/features/doctor_filter/doctor_filter_view_model.dart create mode 100644 lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart create mode 100644 lib/presentation/book_appointment/doctor_filter/RegionChips.dart create mode 100644 lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart create mode 100644 lib/presentation/book_appointment/doctor_filter/clinic_item.dart create mode 100644 lib/presentation/book_appointment/doctor_filter/doctors_filter.dart create mode 100644 lib/presentation/book_appointment/doctor_filter/facility_Chips.dart diff --git a/assets/images/svg/cross_circle.svg b/assets/images/svg/cross_circle.svg new file mode 100644 index 0000000..bcf9f90 --- /dev/null +++ b/assets/images/svg/cross_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/filters.svg b/assets/images/svg/filters.svg new file mode 100644 index 0000000..521f6fa --- /dev/null +++ b/assets/images/svg/filters.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/svg/ic_close.svg b/assets/images/svg/ic_close.svg new file mode 100644 index 0000000..615b42d --- /dev/null +++ b/assets/images/svg/ic_close.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 8945fee..b08e783 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -856,5 +856,13 @@ "onboardingHeading1": "حجز المواعيد لم يكن أسهل من قبل", "onboardingBody1": "ببضع نقرات فقط يمكنك استشارة الطبيب الذي تختاره.", "onboardingHeading2": "الوصول إلى السجل الطبي بين يديك", - "onboardingBody2": "تتبع تاريخك الطبي بما في ذلك الفحوصات المخبرية، الوصفات الطبية، التأمين، وغيرها." + "onboardingBody2": "تتبع تاريخك الطبي بما في ذلك الفحوصات المخبرية، الوصفات الطبية، التأمين، وغيرها.", + "hmgHospitals": "مستشفيات HMG", + "hmcMedicalClinic": "مراكز HMC الطبية", + "applyFilter": "تطبيق الفلتر", + "facilityAndLocation": "المرفق والموقع", + "regionAndLocation": "المنطقة والمواقع", + "clearAllFilters": "مسح جميع الفلاتر", + "filters": "فلاتر", + "searchClinic": "بحث عن عيادة" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 843daf8..adab1d5 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -852,5 +852,13 @@ "onboardingHeading1": "Booking appointment has never been easy", "onboardingBody1": "In few clicks find yourself having consultation with the doctor of your choice.", "onboardingHeading2": "Access the medical history on finger tips", - "onboardingBody2": "Keep track on your medical history including labs, prescription, insurance, etc" + "onboardingBody2": "Keep track on your medical history including labs, prescription, insurance, etc", + "hmgHospitals": "HMG Hospitals", + "hmcMedicalClinic": "HMC Medical Centers", + "applyFilter": "AppLy Filter", + "facilityAndLocation": "Facility and Location", + "regionAndLocation": "Region And Locations", + "clearAllFilters": "Clear all filters", + "filters": "Filters", + "searchClinic": "Search Clinic" } \ No newline at end of file diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 31b4f32..716448d 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -145,6 +145,9 @@ class AppAssets { static const String ic_normal_result = '$svgBasePath/normal_result.svg'; static const String ic_low_result = '$svgBasePath/low_result.svg'; static const String ic_critical_low_result = '$svgBasePath/critical_low_result.svg'; + static const String ic_filters = '$svgBasePath/filters.svg'; + static const String ic_close = '$svgBasePath/ic_close.svg'; + static const String ic_cross_circle = '$svgBasePath/cross_circle.svg'; //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 5ef653a..bd292ec 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -53,6 +53,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { List get filteredClinicsList => _filteredClinicsList; List doctorsList = []; + List filteredDoctorList = []; List liveCareDoctorsList = []; @@ -87,6 +88,19 @@ class BookAppointmentsViewModel extends ChangeNotifier { bool shouldLoadSpecificClinic = false; String? currentlySelectedHospitalFromRegionFlow; + ///variables for doctor filter + List searchedRegionList = []; + List facilityList = ["hmgHospitals", "hmcMedicalClinic"]; + List searchedHospitalList = []; + List + searchedPatientDoctorAppointmentHospitalsList = []; + List searchedClinicList = []; + + PatientDoctorAppointmentList? selectedHospitalForFilters; + List? selectedFacilityForFilters = [], selectedRegionForFilters = []; + String? selectedClinicForFilters; + bool applyFilters = false; + BookAppointmentsViewModel( {required this.bookAppointmentsRepo, required this.errorHandlerService, required this.navigationService, required this.myAppointmentsViewModel, required this.locationUtils}) { ; @@ -279,8 +293,11 @@ class BookAppointmentsViewModel extends ChangeNotifier { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { doctorsList = apiResponse.data!; + filteredDoctorList = doctorsList; isDoctorsListLoading = false; initializeFilteredList(); + clearSearchFilters(); + getFiltersFromDoctorList(); notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -757,4 +774,134 @@ class BookAppointmentsViewModel extends ChangeNotifier { void getLocation() { locationUtils.getLocation(); } + + void clearSearchFilters() { + searchedRegionList.clear(); + searchedHospitalList.clear(); + searchedPatientDoctorAppointmentHospitalsList.clear(); + searchedClinicList.clear(); + notifyListeners(); + } + + void clearSelection() { + selectedFacilityForFilters = []; + selectedClinicForFilters = null; + selectedHospitalForFilters = null; + selectedRegionForFilters = []; + applyFilters = false; + notifyListeners(); + } + + void setSelections( + List? selectedFacilityForFilters, + List? selectedRegionForFilters, + String? selectedClinicForFilters, + PatientDoctorAppointmentList? selectedHospitalForFilters, + bool applyFilters) { + this.selectedFacilityForFilters = selectedFacilityForFilters; + this.selectedClinicForFilters = selectedClinicForFilters; + this.selectedHospitalForFilters = selectedHospitalForFilters; + this.selectedRegionForFilters = selectedRegionForFilters; + this.applyFilters = applyFilters; + notifyListeners(); + } + + void getFiltersFromDoctorList() { + doctorsList.forEach((element) { + if (!searchedRegionList + .contains(element.getRegionName(_appState.isArabic()))) { + searchedRegionList + .add(element.getRegionName(_appState.isArabic()) ?? ""); + } + if (!searchedHospitalList.contains(element.projectName)) { + searchedPatientDoctorAppointmentHospitalsList + .add(PatientDoctorAppointmentList() + ..filterName = element.projectName + ..isHMC = element.isHMC + ..distanceInKMs = "0"); + searchedHospitalList.add(element.projectName ?? ""); + } + if (!searchedClinicList.contains(element.clinicName)) { + searchedClinicList.add(element.clinicName ?? ""); + } + }); + } + + void updateApplyFilters(bool applyFilters) { + this.applyFilters = applyFilters; + notifyListeners(); + } + + void setSelectedRegion(String region) { + if (selectedRegionForFilters?.contains(region) == true) { + selectedRegionForFilters?.remove(region); + } else { + selectedRegionForFilters?.add(region); + } + notifyListeners(); + } + + void setSelectedHospital(PatientDoctorAppointmentList? hospital) { + selectedHospitalForFilters = hospital; + notifyListeners(); + } + + void setSelectedFacilityForFilter(String facility) { + if (selectedFacilityForFilters?.contains(facility) == true) { + selectedFacilityForFilters?.remove(facility); + } else { + selectedFacilityForFilters?.add(facility); + } + + notifyListeners(); + } + + void setSelectedClinicForFilter(String? clinic) { + selectedClinicForFilters = clinic; + notifyListeners(); + } + + bool isArabic() { + return _appState.isArabic(); + } + + List getDoctorListAsPerSelection() { + if (!applyFilters) return doctorsList; + + if ((selectedRegionForFilters?.isEmpty == true) && + (selectedFacilityForFilters?.isEmpty == true) && + selectedClinicForFilters == null && + selectedHospitalForFilters == null) { + return doctorsList; + } + var list = doctorsList.where((element) { + var isInSelectedRegion = (selectedRegionForFilters?.isEmpty == true) + ? true + : selectedRegionForFilters + ?.any((region) => region == element.getRegionName(isArabic())); + var shouldApplyFacilityFilter = + (selectedFacilityForFilters?.isEmpty == true) ? false : true; + var isHMC = (selectedFacilityForFilters?.isEmpty == true) + ? true + : selectedFacilityForFilters?.any((item) => item.contains("hmc")); + var isInSelectedClinic = (selectedClinicForFilters == null) + ? true + : selectedClinicForFilters == element.clinicName; + var isInSelectedHospital = (selectedHospitalForFilters == null) + ? true + : element.projectName == selectedHospitalForFilters?.filterName; + var facilityFilter = ((shouldApplyFacilityFilter == true) ? isHMC : true); + + return (isInSelectedRegion ?? true) && + (facilityFilter ?? true) && + isInSelectedClinic && + isInSelectedHospital; + }).toList(); + return list; + } + + void updateList() { + filteredDoctorList = getDoctorListAsPerSelection(); + notifyListeners(); + } } diff --git a/lib/features/doctor_filter/doctor_filter_view_model.dart b/lib/features/doctor_filter/doctor_filter_view_model.dart new file mode 100644 index 0000000..889ab5a --- /dev/null +++ b/lib/features/doctor_filter/doctor_filter_view_model.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart' show PatientDoctorAppointmentList; + +class DoctorFilterViewModel extends ChangeNotifier{ + + late AppState appState; + DoctorFilterViewModel(){ + appState = getIt(); + } + List searchedRegionList = []; + List facilityList = ["hmgHospitals", "hmcMedicalClinic"]; + List searchedHospitalList = []; + List + searchedPatientDoctorAppointmentHospitalsList = []; + List searchedClinicList = []; + + PatientDoctorAppointmentList? selectedHospitalForFilters; + List? selectedFacilityForFilters = [] , selectedRegionForFilters = []; + String? selectedClinicForFilters; + bool applyFilters = false; + + void clearSearchFilters() { + searchedRegionList.clear(); + searchedHospitalList.clear(); + searchedPatientDoctorAppointmentHospitalsList.clear(); + searchedClinicList.clear(); + notifyListeners(); + } + + void clearSelection() { + selectedFacilityForFilters = []; + selectedClinicForFilters = null; + selectedHospitalForFilters = null; + selectedRegionForFilters = []; + applyFilters = false; + notifyListeners(); + } + + void getFiltersFromDoctorList(List doctorsList) { + doctorsList.forEach((element) { + if (!searchedRegionList + .contains(element.getRegionName(appState.isArabic()))) { + searchedRegionList + .add(element.getRegionName(appState.isArabic()) ?? ""); + } + if (!searchedHospitalList.contains(element.projectName)) { + searchedPatientDoctorAppointmentHospitalsList + .add(PatientDoctorAppointmentList() + ..filterName = element.projectName + ..isHMC = element.isHMC + ..distanceInKMs = "0"); + searchedHospitalList.add(element.projectName ?? ""); + } + if (!searchedClinicList.contains(element.clinicName)) { + searchedClinicList.add(element.clinicName ?? ""); + } + }); + } + + void updateApplyFilters(bool applyFilters) { + this.applyFilters = applyFilters; + notifyListeners(); + } + + void setSelectedRegion(String region) { + if (selectedRegionForFilters?.contains(region) == true) { + selectedRegionForFilters?.remove(region); + } else { + selectedRegionForFilters?.add(region); + } + notifyListeners(); + } + + void setSelectedHospital(PatientDoctorAppointmentList? hospital) { + selectedHospitalForFilters = hospital; + notifyListeners(); + } + + void setSelectedFacilityForFilter(String facility) { + if (selectedFacilityForFilters?.contains(facility) == true) { + selectedFacilityForFilters?.remove(facility); + } else { + selectedFacilityForFilters?.add(facility); + } + + notifyListeners(); + } + + void setSelectedClinicForFilter(String? clinic) { + selectedClinicForFilters = clinic; + notifyListeners(); + } + + void setSelections( + List? selectedFacilityForFilters, + List? selectedRegionForFilters, + String? selectedClinicForFilters, + PatientDoctorAppointmentList? selectedHospitalForFilters, + bool applyFilters) { + this.selectedFacilityForFilters = selectedFacilityForFilters; + this.selectedClinicForFilters = selectedClinicForFilters; + this.selectedHospitalForFilters = selectedHospitalForFilters; + this.selectedRegionForFilters = selectedRegionForFilters; + this.applyFilters = applyFilters; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 3d12e5b..9447bd1 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -849,5 +849,19 @@ abstract class LocaleKeys { static const selectCountry = 'selectCountry'; static const forLoginVerification = 'forLoginVerification'; static const searchHospital = 'searchHospital'; + static const skip = 'skip'; + static const getStarted = 'getStarted'; + static const onboardingHeading1 = 'onboardingHeading1'; + static const onboardingBody1 = 'onboardingBody1'; + static const onboardingHeading2 = 'onboardingHeading2'; + static const onboardingBody2 = 'onboardingBody2'; + static const hmgHospitals = 'hmgHospitals'; + static const hmcMedicalClinic = 'hmcMedicalClinic'; + static const applyFilter = 'applyFilter'; + static const facilityAndLocation = 'facilityAndLocation'; + static const regionAndLocation = 'regionAndLocation'; + static const clearAllFilters = 'clearAllFilters'; + static const filters = 'filters'; + static const searchClinic = 'searchClinic'; } diff --git a/lib/main.dart b/lib/main.dart index 3690c72..4dfba4b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/history/lab_history_viewmodel.dart'; @@ -154,7 +155,9 @@ void main() async { ChangeNotifierProvider( create: (_) => LabHistoryViewModel()), ChangeNotifierProvider( - create: (_) => LabRangeViewModel()) + create: (_) => LabRangeViewModel()) , + ChangeNotifierProvider( + create: (_) => DoctorFilterViewModel()) ], child: MyApp()), ), ); diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart new file mode 100644 index 0000000..58a0d00 --- /dev/null +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart @@ -0,0 +1,66 @@ +import 'package:easy_localization/easy_localization.dart' + show tr, StringTranslateExtension; +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/doctor_filter/doctor_filter_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'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:provider/provider.dart'; + +class HospitalBottomSheetBodyForDoctorFilter extends StatelessWidget { + late BookAppointmentsViewModel appointmentsViewModel; + late AppointmentViaRegionViewmodel regionalViewModel; + final TextEditingController searchText = TextEditingController(); + + HospitalBottomSheetBodyForDoctorFilter({super.key}); + + @override + Widget build(BuildContext context) { + appointmentsViewModel = Provider.of(context); + regionalViewModel = Provider.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.selectHospital.tr(), + style: TextStyle( + fontSize: 21, + fontWeight: FontWeight.w600, + color: AppColors.blackColor, + ), + ), + SizedBox(height: 24.h), + SizedBox( + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemBuilder: (_, index) + { + var hospital = appointmentsViewModel.searchedPatientDoctorAppointmentHospitalsList[index]; + return HospitalListItem( + hospitalData: hospital, + isLocationEnabled: appointmentsViewModel.isLocationEnabled(), + ).onPress(() { + regionalViewModel.setHospitalModel(hospital); + context.read().setSelectedHospital(hospital); + Navigator.pop(context); + });}, + separatorBuilder: (_, __) => SizedBox( + height: 16.h, + ), + itemCount: appointmentsViewModel.searchedPatientDoctorAppointmentHospitalsList?.length ?? 0), + ) + ], + ); + } +} diff --git a/lib/presentation/book_appointment/doctor_filter/RegionChips.dart b/lib/presentation/book_appointment/doctor_filter/RegionChips.dart new file mode 100644 index 0000000..c81e548 --- /dev/null +++ b/lib/presentation/book_appointment/doctor_filter/RegionChips.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.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/book_appointments/book_appointments_view_model.dart' + show BookAppointmentsViewModel; +import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; + +class RegionChips extends StatelessWidget { + const RegionChips({super.key}); + + @override + Widget build(BuildContext context) { + return Selector>( + selector: (_, model) => model.searchedRegionList, + builder: (__, data, ___) => + Selector?>( + selector: (_, model) => model.selectedRegionForFilters, + builder: (context, selectRegion, ___) => Row( + children: [ + Expanded( + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: data.length, + separatorBuilder: (_, __)=>SizedBox(width: 8.h,), + + itemBuilder: (_, index) => AppCustomChipWidget( + labelText: data[index], + textColor: selectRegion?.any((selectedRegion)=>data[index] == selectedRegion) == true + ? AppColors.primaryRedColor + + : AppColors.textColor, + backgroundColor: + selectRegion?.any((selectedRegion)=>data[index] == selectedRegion) == true + ? AppColors.primaryRedColor + .withOpacity(0.1) + : AppColors.whiteColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: selectRegion?.any((selectedRegion)=>data[index] == selectedRegion) == true + ? AppColors + .primaryRedBorderColor + : AppColors + .chipBorderColorOpacity20, + width: 1, + ), + borderRadius: + BorderRadius.circular(10))) + .onPress(() { + context + .read() + .setSelectedRegion(data[index]); + }))) + ], + ), + )); + } +} diff --git a/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart b/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart new file mode 100644 index 0000000..155d8e3 --- /dev/null +++ b/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart @@ -0,0 +1,85 @@ +import 'package:easy_localization/easy_localization.dart' + show tr, StringTranslateExtension; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.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/doctor_filter/doctor_filter_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'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/clinic_item.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/clinic_card.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:provider/provider.dart'; + +import '../../../features/book_appointments/models/resp_models/get_clinic_list_response_model.dart' show GetClinicsListResponseModel; + +class ClinicBottomSheet extends StatelessWidget { + late BookAppointmentsViewModel appointmentsViewModel; + late AppointmentViaRegionViewmodel regionalViewModel; + final TextEditingController searchText = TextEditingController(); + + ClinicBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + appointmentsViewModel = Provider.of(context); + regionalViewModel = Provider.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.selectClinic.tr(), + style: TextStyle( + fontSize: 21, + fontWeight: FontWeight.w600, + color: AppColors.blackColor, + ), + ), + SizedBox(height: 24.h), + SizedBox( + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemBuilder: (_, index) + { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: ClinicItem( + isArabic: appointmentsViewModel.isArabic(), + clinicName: appointmentsViewModel.searchedClinicList[index], + ), + ), + ), + ), + + ).onPress(() { + context.read() + .setSelectedClinicForFilter(appointmentsViewModel.searchedClinicList[index]); + Navigator.pop(context); + });}, + separatorBuilder: (_, __) => SizedBox( + height: 16.h, + ), + itemCount: appointmentsViewModel.searchedClinicList.length ?? 0), + ) + ], + ); + } +} diff --git a/lib/presentation/book_appointment/doctor_filter/clinic_item.dart b/lib/presentation/book_appointment/doctor_filter/clinic_item.dart new file mode 100644 index 0000000..0d5ba76 --- /dev/null +++ b/lib/presentation/book_appointment/doctor_filter/clinic_item.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; + +class ClinicItem extends StatelessWidget { + final String clinicName; + final bool isArabic; + + ClinicItem({super.key, required this.clinicName, required this.isArabic}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.generic_clinic_icon, + width: 24.h, + height: 24.h, + fit: BoxFit.contain), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: clinicName.toText16(isBold: true)), + Transform.flip( + flipX: isArabic, + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + width: 15.h, + height: 15.h, + fit: BoxFit.contain, + iconColor: AppColors.textColor), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart new file mode 100644 index 0000000..1a772f7 --- /dev/null +++ b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart @@ -0,0 +1,249 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/RegionChips.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/facility_Chips.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:provider/provider.dart'; + +import '../../../widgets/buttons/custom_button.dart'; + +class DoctorsFilters extends StatelessWidget{ + + + TextEditingController hospitalController = TextEditingController(); + TextEditingController clinicController = TextEditingController(); + DoctorsFilters({super.key,}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + appBar: AppBar( + backgroundColor: AppColors.bgScaffoldColor, + + automaticallyImplyLeading: false, + centerTitle: false, + title: Utils.buildSvgWithAssets(icon: AppAssets.ic_close, height: 32.h, width: 32.h).onPress((){ + context.read() + .clearSelection() + ; + Navigator.pop(context); + }) + + + + ), + + body: Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + LocaleKeys.filters.tr(), + style:TextStyle( + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + fontSize: 27.fSize, + color: AppColors.textColor, + letterSpacing: -1 + ) + ), + Text( + LocaleKeys.clearAllFilters.tr(), + style:TextStyle( + fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + fontSize: 14.fSize, + color: AppColors.errorColor + ) + ).onPress((){ + context.read().clearSelection(); + context.read().updateApplyFilters(false); + // context.read().setSelections( + // context.read().selectedFacilityForFilters, + // context.read().selectedRegionForFilters, + // context.read().selectedClinicForFilters, + // context.read().selectedHospitalForFilters, + // context.read().applyFilters); + context.read().updateList(); + }) + ], + ), + titleWidget(LocaleKeys.regionAndLocation.tr()), + SizedBox( + height: 42.h, + child: RegionChips()), + titleWidget(LocaleKeys.facilityAndLocation.tr()), + SizedBox( + height: 42.h, + child: FacilityChip()), + titleWidget(LocaleKeys.hospital.tr()), + TextInputWidget( + controller: TextEditingController()..text =context.watch().selectedHospitalForFilters?.filterName??'', + labelText: LocaleKeys.hospital.tr(context: context), + hintText: LocaleKeys.searchHospital.tr(context: context), + isEnable: false, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + suffix:context.watch().selectedHospitalForFilters != null + ? GestureDetector( + onTap: () { + context.read().setSelectedHospital(null); + }, + child: Utils.buildSvgWithAssets(icon: AppAssets.ic_cross_circle, width: 24.h, height: 24.h, fit: BoxFit.scaleDown), + ) + : null, + onChange: (value) { + // DoctorFilterViewModel.filterClinics(value!); + }, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(8).h, + horizontal: ResponsiveExtension(10).h, + ), + ).onPress((){ + openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION); + }), + + titleWidget(LocaleKeys.clinic.tr()), + TextInputWidget( + controller: TextEditingController()..text =context.watch().selectedClinicForFilters ??'', + labelText: LocaleKeys.clinicName.tr(context: context), + hintText: LocaleKeys.searchClinic.tr().needTranslation, + isEnable: false, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + suffix:context.read().selectedClinicForFilters?.isNotEmpty == true + ? GestureDetector( + onTap: () { + context.read().setSelectedClinicForFilter(null); + }, + child: Utils.buildSvgWithAssets(icon: AppAssets.ic_cross_circle, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), + ) + : null, + onChange: (value) { + // DoctorFilterViewModel.filterClinics(value!); + }, + padding: EdgeInsets.symmetric( + vertical: 8.h, + horizontal: 10.h, + ), + ).onPress((){ + openClinicListBottomSheet(context,); + }), + + + ], + ).paddingSymmetrical(24.h, 0.h), + Spacer(), + DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Colors.white, + customBorder: BorderRadius.only(topLeft: Radius.circular(24.h), topRight: Radius.circular(24.h)) , + + ), + child: CustomButton( + text: LocaleKeys.applyFilter.tr(), + onPressed: () { + context.read().updateApplyFilters(true); + context.read().setSelections( + context.read().selectedFacilityForFilters?.toList()??[], + context.read().selectedRegionForFilters?.toList()??[], + context.read().selectedClinicForFilters, + context.read().selectedHospitalForFilters, + context.read().applyFilters); + context.read().updateList(); + Navigator.pop(context); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + fontSize: 16, + padding: EdgeInsets.zero, + fontWeight: FontWeight.w500, + borderRadius: 12, + icon: AppAssets.add_icon, + iconColor: AppColors.primaryRedColor, + ).paddingAll(24.h), + ), + ], + ), + ); + } + + Widget titleWidget(String title){ + return Column( + children: [ + SizedBox(height: 24.h,), + Text( + title, + style:TextStyle( + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + fontSize: 16.fSize, + color: AppColors.textColor, + letterSpacing:-1 + ) + ), + SizedBox(height: 8.h,), + ], + ); + } + + void openRegionListBottomSheet(BuildContext context, RegionBottomSheetType type) { + context.read().flush(); + context.read().setBottomSheetType(type); + context.read().setBottomSheetState(AppointmentViaRegionState.HOSPITAL_SELECTION); + // AppointmentViaRegionViewmodel? viewmodel = null; + showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer(builder: (_, data, __) => getTitle(data)), isDismissible: false, + child: Consumer(builder: (_, data, __) { + return getRegionalSelectionWidget(data); + }), callBackFunc: () {}); + } + + Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data) { + + if (data.bottomSheetState == AppointmentViaRegionState.HOSPITAL_SELECTION) { + return HospitalBottomSheetBodyForDoctorFilter(); + } + if (data.bottomSheetState == AppointmentViaRegionState.CLINIC_SELECTION) { + } else { + SizedBox.shrink(); + } + return SizedBox.shrink(); + } + + getTitle(AppointmentViaRegionViewmodel data) { + return SizedBox.shrink(); + } + + void openClinicListBottomSheet(BuildContext context) { + showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer(builder: (_, data, __) => getTitle(data)), isDismissible: false, + child: Consumer(builder: (_, data, __) { + return ClinicBottomSheet(); + }), callBackFunc: () {}); + } + +} \ No newline at end of file diff --git a/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart b/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart new file mode 100644 index 0000000..f416ed4 --- /dev/null +++ b/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart @@ -0,0 +1,65 @@ + +import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart' + show BookAppointmentsViewModel; +import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; + +class FacilityChip extends StatelessWidget { + const FacilityChip({super.key}); + + @override + Widget build(BuildContext context) { + return Selector>( + selector: (_, model) => model.facilityList, + builder: (__, data, ___) => + Selector?>( + selector: (_, model) => model.selectedFacilityForFilters, + builder: (context, selectRegion, ___) => Row( + children: [ + Expanded( + child: ListView.separated( + scrollDirection: Axis.horizontal, + + itemCount: data.length, + separatorBuilder: (_, __)=>SizedBox(width: 8.h,), + itemBuilder: (_, index) => AppCustomChipWidget( + icon: data[index].contains("hmg")?AppAssets.hmg: AppAssets.hmc, + iconHasColor: false, + iconSize: 18, + labelText: data[index].tr(), + textColor: selectRegion?.any((selectedRegion)=>data[index] == selectedRegion) == true + ? AppColors.primaryRedColor + : AppColors.textColor, + backgroundColor: + data[index] == selectRegion + ? AppColors.primaryRedColor + .withOpacity(0.1) + : AppColors.whiteColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: selectRegion?.any((selectedRegion)=>data[index] == selectedRegion) == true + ? AppColors + .primaryRedBorderColor + : AppColors + .chipBorderColorOpacity20, + width: 1, + ), + borderRadius: + BorderRadius.circular(10))) + .onPress(() { + context + .read() + .setSelectedFacilityForFilter(data[index]); + }))) + ], + ), + )); + } +} diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index 8f611dc..233aff9 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -9,7 +9,9 @@ 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/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/doctors_filter.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_card.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; @@ -33,16 +35,12 @@ class SearchDoctorByName extends StatefulWidget { class _SearchDoctorByNameState extends State { TextEditingController searchEditingController = TextEditingController(); - FocusNode textFocusNode = FocusNode(); - - late AppState appState; late BookAppointmentsViewModel bookAppointmentsViewModel; @override Widget build(BuildContext context) { bookAppointmentsViewModel = Provider.of(context, listen: false); - appState = getIt.get(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Column( @@ -56,33 +54,85 @@ class _SearchDoctorByNameState extends State { child: Column( children: [ SizedBox(height: 16.h), - TextInputWidget( - labelText: LocaleKeys.search.tr(context: context), - hintText: LocaleKeys.doctorName.tr(context: context), - controller: searchEditingController, - isEnable: true, - prefix: null, - autoFocus: false, - isBorderAllowed: false, - keyboardType: TextInputType.text, - focusNode: textFocusNode, - suffix: searchEditingController.text.isNotEmpty - ? GestureDetector( - onTap: () { - searchEditingController.clear(); - // bookAppointmentsViewModel.filterClinics(""); - textFocusNode.unfocus(); - }, - child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), - ) - : null, - onChange: (value) { - // bookAppointmentsViewModel.filterClinics(value!); - }, - padding: EdgeInsets.symmetric( - vertical: ResponsiveExtension(10).h, - horizontal: ResponsiveExtension(15).h, - ), + Row( + spacing: 8.h, + children: [ + Expanded( + child: TextInputWidget( + labelText: LocaleKeys.search.tr(context: context), + hintText: LocaleKeys.doctorName.tr(context: context), + controller: searchEditingController, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + focusNode: textFocusNode, + suffix: searchEditingController.text.isNotEmpty + ? GestureDetector( + onTap: () { + searchEditingController.clear(); + // bookAppointmentsViewModel.filterClinics(""); + textFocusNode.unfocus(); + }, + child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), + ) + : null, + onChange: (value) { + // bookAppointmentsViewModel.filterClinics(value!); + }, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), + ), + ), + Visibility( + visible: bookAppointmentsViewModel.doctorsList.isNotEmpty, + child: SizedBox( + height: 56.h, + width: 56.h, + child: DecoratedBox(decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 10.h, + hasShadow: false, + ), + child: Utils.buildSvgWithAssets(icon: AppAssets.ic_filters, + height: 24.h, + width: 24.h, ).paddingAll(16.h).onPress((){ + context.read() + ..clearSelection() + ..clearSearchFilters() + ..getFiltersFromDoctorList( + bookAppointmentsViewModel.doctorsList + )..setSelections( + bookAppointmentsViewModel.selectedFacilityForFilters?.toList(), + bookAppointmentsViewModel.selectedRegionForFilters?.toList(), + bookAppointmentsViewModel.selectedClinicForFilters, + bookAppointmentsViewModel.selectedHospitalForFilters, + bookAppointmentsViewModel.applyFilters) ; + Navigator.of(context).push( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => DoctorsFilters(), // Replace YourNewPage with your actual page widget + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(0.0, 1.0); // Start from the bottom (y=1.0) + const end = Offset.zero; // End at the original position (y=0.0) + final tween = Tween(begin: begin, end: end); + final offsetAnimation = animation.drive(tween); + + return SlideTransition( + position: offsetAnimation, + child: child, + ); + }, + transitionDuration: Duration(milliseconds: 200), // Adjust duration as needed + ), + ); + }), + ), + ), + ) + ], ), SizedBox(height: 16.h), Consumer(builder: (context, bookAppointmentsVM, child) { @@ -94,7 +144,7 @@ class _SearchDoctorByNameState extends State { padding: EdgeInsets.only(top: 24.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.doctorsList.length, + itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.filteredDoctorList.length, itemBuilder: (context, index) { return bookAppointmentsVM.isDoctorsListLoading ? DoctorCard( @@ -113,11 +163,11 @@ class _SearchDoctorByNameState extends State { curve: Curves.easeInOut, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), child: DoctorCard( - doctorsListResponseModel: bookAppointmentsVM.doctorsList[index], + doctorsListResponseModel: bookAppointmentsVM.filteredDoctorList[index], isLoading: false, bookAppointmentsViewModel: bookAppointmentsViewModel, ).onPress(() async { - bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.doctorsList[index]); + bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[index]); // bookAppointmentsVM.setSelectedDoctor(DoctorsListResponseModel()); LoaderBottomSheet.showLoader(); await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { @@ -166,7 +216,11 @@ class _SearchDoctorByNameState extends State { text: LocaleKeys.search.tr(context: context), onPressed: () async { textFocusNode.unfocus(); + print("the value is empty ${searchEditingController.text.isNotEmpty}"); if (searchEditingController.text.isNotEmpty) { + bookAppointmentsViewModel.updateApplyFilters(false); + bookAppointmentsViewModel.clearSelection(); + bookAppointmentsViewModel.updateList(); bookAppointmentsViewModel.setIsDoctorSearchByNameStarted(true); bookAppointmentsViewModel.setIsDoctorsListLoading(true); // LoaderBottomSheet.showLoader(); diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index fd6e691..ec17c61 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -68,33 +68,63 @@ class _SelectDoctorPageState extends State { children: [ // TODO: Implement doctor filter functionality SizedBox(height: 16.h), - TextInputWidget( - labelText: LocaleKeys.search.tr(context: context), - hintText: LocaleKeys.doctorName.tr(context: context), - controller: searchEditingController, - isEnable: true, - prefix: null, - autoFocus: false, - isBorderAllowed: false, - keyboardType: TextInputType.text, - focusNode: textFocusNode, - suffix: searchEditingController.text.isNotEmpty - ? GestureDetector( - onTap: () { - searchEditingController.clear(); - bookAppointmentsViewModel.filterClinics(""); - textFocusNode.unfocus(); - }, - child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), - ) - : null, - onChange: (value) { - bookAppointmentsViewModel.filterClinics(value!); - }, - padding: EdgeInsets.symmetric( - vertical: ResponsiveExtension(10).h, - horizontal: ResponsiveExtension(15).h, - ), + Row( + spacing: 8.h, + children: [ + Expanded( + child: TextInputWidget( + labelText: LocaleKeys.search.tr(context: context), + hintText: LocaleKeys.doctorName.tr(context: context), + controller: searchEditingController, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + focusNode: textFocusNode, + suffix: searchEditingController.text.isNotEmpty + ? GestureDetector( + onTap: () { + searchEditingController.clear(); + bookAppointmentsViewModel.filterClinics(""); + textFocusNode.unfocus(); + }, + child: Utils.buildSvgWithAssets( + icon: AppAssets.close_bottom_sheet_icon, + width: 20.h, + height: 20.h, + fit: BoxFit.scaleDown), + ) + : null, + onChange: (value) { + bookAppointmentsViewModel.filterClinics(value!); + }, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), + ), + ), + SizedBox( + height: 56.h, + width: 56.h, + child: DecoratedBox( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 10.h, + hasShadow: false, + ), + child: Utils.buildSvgWithAssets( + icon: AppAssets.ic_filters, + height: 24.h, + width: 24.h, + ).paddingAll(16.h).onPress((){ + + }), + ), + ) + ], ), ListView.separated( padding: EdgeInsets.only(top: 24.h), diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index a4db172..084f85b 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -62,7 +62,7 @@ class AppCustomChipWidget extends StatelessWidget { // padding: EdgeInsets.all(0.0), padding: padding, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - labelPadding: EdgeInsetsDirectional.only(start: -4.h, end: deleteIcon?.isNotEmpty == true ? 2.h : 8.h), + labelPadding: EdgeInsetsDirectional.only(start: 0.h, end: deleteIcon?.isNotEmpty == true ? 2.h : 8.h), backgroundColor: backgroundColor, shape: shape ?? SmoothRectangleBorder( diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 4a2322c..4b06096 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -114,6 +114,7 @@ class TextInputWidget extends StatelessWidget { children: [ Container( padding: padding, + height: 58.h, alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: Colors.white, @@ -134,12 +135,23 @@ class TextInputWidget extends StatelessWidget { // textField: _buildTextField(context), ) : Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( children: [ - _buildLabelText(labelColor).paddingOnly(right: (appState.getLanguageCode() == "ar" ? 10 : 0)), - _buildTextField(context), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLabelText(labelColor).paddingOnly(right: (appState.getLanguageCode() == "ar" ? 10 : 0)), + Row( + children: [ + Expanded(child: _buildTextField(context)), + ], + ), + ], + ), + ), + (suffix!= null )?suffix!:SizedBox.shrink() ], ), ), @@ -218,7 +230,6 @@ class TextInputWidget extends StatelessWidget { fontWeight: FontWeight.w500, color: labelColor ?? AppColors.inputLabelTextColor, letterSpacing: -0.2, - height: 18 / 12, ), ); } From 8a7f46441c11cefbb93eb4b6d84322339f5ddc01 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Thu, 2 Oct 2025 15:31:18 +0300 Subject: [PATCH 20/43] updates --- assets/animations/lottie/pending_loading_animation.json | 1 + lib/core/app_assets.dart | 1 + lib/presentation/home/landing_page.dart | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 assets/animations/lottie/pending_loading_animation.json diff --git a/assets/animations/lottie/pending_loading_animation.json b/assets/animations/lottie/pending_loading_animation.json new file mode 100644 index 0000000..177ef65 --- /dev/null +++ b/assets/animations/lottie/pending_loading_animation.json @@ -0,0 +1 @@ +{"nm":"loader","ddd":0,"h":720,"w":720,"meta":{"g":"@lottiefiles/toolkit-js 0.33.2"},"layers":[{"ty":4,"nm":"bubble","sr":1,"st":30,"op":50,"ip":27,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-23.672,-43.28,0],"ix":1},"s":{"a":0,"k":[150,150,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.667,"y":1},"s":[3.698,50.595,0],"t":27,"ti":[-3.16098022460938,1.98855590820312,0],"to":[4.02357578277588,0.35728123784065,0]},{"o":{"x":0.333,"y":0},"i":{"x":0.833,"y":0.833},"s":[27.839,52.739,0],"t":35,"ti":[12.7697801589966,16.6396007537842,0],"to":[3.16098022460938,-1.98855590820312,0]},{"s":[11.589,25.489,0],"t":50}],"ix":2},"r":{"a":0,"k":-155.955,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":3,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"s":{"a":0,"k":[9.485,9.485],"ix":2}},{"ty":"st","bm":0,"hd":true,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"c":{"a":0,"k":[0.0078,0.0627,0.1882],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.6863,0.0824],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-23.672,-43.28],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1,"parent":6},{"ty":4,"nm":"timeline","sr":1,"st":35,"op":60,"ip":35,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[144,144,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[360,347.04,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.25,1],[1.25,0.5]]}],"t":35},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.236,3.52],[1,56.5]]}],"t":52},{"s":[{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.07,46.291],[1,56.75]]}],"t":59}],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"c":{"a":0,"k":[1,0.6863,0.0824],"ix":3}},{"ty":"fl","bm":0,"hd":true,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.0078,0.0627,0.1882],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":2,"cix":2,"np":1,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.07,44.041],[1,54.25]]},"ix":2}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"top","sr":1,"st":0,"op":60,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[-0.237,-75.448,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"top","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-74.495,-9.31],[74.495,-9.31],[74.495,9.31],[-74.495,9.31]]},"ix":2}},{"ty":"rd","bm":0,"hd":false,"mn":"ADBE Vector Filter - RC","nm":"Round Corners 1","ix":2,"r":{"a":0,"k":9.31,"ix":1}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.0078,0.0627,0.1882],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3,"parent":6},{"ty":4,"nm":"time-down","sr":1,"st":7,"op":60,"ip":53,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,-100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[-1.53,-33,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"time 2","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.667,"y":1},"s":[{"c":true,"i":[[-6.69,0],[0,0],[-0.111,5.244],[1.215,8.14],[7.97,0.125],[2.32,-2.28],[0.22,-5.32],[0,0]],"o":[[0,0],[6.59,0],[-0.5,-6.85],[-0.598,-4.008],[-7.97,-0.125],[0,0],[0,0],[0,6.71]],"v":[[-0.89,25.5],[3.223,25.5],[6.75,20.35],[5.815,7.985],[-0.355,9],[-3.245,12.395],[-5,15.93],[-6,20.35]]}],"t":53},{"s":[{"c":true,"i":[[-6.69,0],[0,0],[-0.111,5.244],[9.57,9.95],[7.97,0.125],[2.32,-2.28],[0.22,-5.32],[0,0]],"o":[[0,0],[6.59,0],[-0.5,-6.85],[-2.81,-2.92],[-7.97,-0.125],[0,0],[0,0],[0,6.71]],"v":[[-18.39,25.5],[20.723,25.5],[30.5,13.35],[16.39,-6.14],[-0.28,-14.5],[-16.795,-5.98],[-30.5,13.18],[-30.5,13.35]]}],"t":59}],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.6863,0.0824],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"time","ix":2,"cix":2,"np":1,"it":[{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.6863,0.0824],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":4,"parent":6},{"ty":4,"nm":"time-up","sr":1,"st":0,"op":53,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[-1.53,32.5,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"time 2","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[{"c":true,"i":[[-6.69,0],[0,0],[-0.125,5.9],[9.57,9.95],[0,0],[2.32,-2.28],[0.22,-5.32],[0,0]],"o":[[0,0],[6.59,0],[0.125,-5.6],[-2.81,-2.92],[-5.98,0],[0,0],[0,0],[0,6.71]],"v":[[-18.39,25.5],[21.015,25.5],[30.5,13.35],[10.41,-21.82],[-0.19,-25.5],[-10.65,-21.99],[-30.5,13.18],[-30.5,13.35]]}],"t":-5},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[{"c":true,"i":[[-6.69,0],[0,0],[-0.111,5.244],[9.57,9.95],[7.97,0.125],[2.32,-2.28],[0.22,-5.32],[0,0]],"o":[[0,0],[6.59,0],[-0.5,-6.85],[-2.81,-2.92],[-7.97,-0.125],[0,0],[0,0],[0,6.71]],"v":[[-18.39,25.5],[20.723,25.5],[30.5,13.35],[16.39,-6.14],[-0.28,-14.5],[-16.795,-5.98],[-30.5,13.18],[-30.5,13.35]]}],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":1},"s":[{"c":true,"i":[[-6.69,0],[0,0],[2.905,4.906],[7.706,11.409],[7.334,-3.123],[3.795,-7.031],[0.22,-5.32],[0,0]],"o":[[0,0],[6.59,0],[-3.838,-8.41],[-6.823,-6.658],[-5.759,2.452],[0,0],[0,0],[0,6.71]],"v":[[-18.39,25.5],[16.753,25.987],[26.53,13.837],[12.972,-9.051],[-7.23,-22.451],[-20.652,-6.377],[-30.5,13.18],[-30.5,13.35]]}],"t":20},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":0.833},"s":[{"c":true,"i":[[-6.69,0],[0,0],[2.905,4.906],[7.706,11.409],[7.742,-1.897],[3.795,-7.031],[0.22,-5.32],[0,0]],"o":[[0,0],[6.59,0],[-3.838,-8.41],[-6.823,-6.658],[-8.432,2.066],[0,0],[0,0],[0,6.71]],"v":[[-15.709,24.889],[20.723,25.5],[30.5,13.35],[18.526,-10.751],[-0.661,-21.798],[-17.97,-6.988],[-27.819,12.569],[-27.819,12.739]]}],"t":25},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[{"c":true,"i":[[-8.78,-0.135],[-6.861,2.709],[-3.043,7.511],[6.829,11.691],[9.846,-0.973],[3.076,-4.716],[0.22,-5.32],[2.646,-6.27]],"o":[[6.522,0.135],[6.397,-2.699],[2.728,-8.693],[-4.868,-4.837],[-10.263,1.059],[0,0],[0,0],[-2.646,6.27]],"v":[[-26.717,26.233],[2.736,24.462],[21.011,11.794],[17.963,-13.07],[-0.928,-23.139],[-18.387,-11.491],[-29.14,7.995],[-29.14,8.165]]}],"t":35},{"o":{"x":0.167,"y":0.167},"i":{"x":0.667,"y":1},"s":[{"c":true,"i":[[-8.78,-0.135],[-4.173,2.699],[-3.043,7.511],[6.829,11.691],[9.846,-0.973],[3.076,-4.716],[3.17,-6.745],[0,0]],"o":[[6.522,0.135],[6.397,-2.699],[2.728,-8.693],[-4.868,-4.837],[-10.263,1.059],[0,0],[0,0],[0,6.71]],"v":[[-23.11,18.54],[6.222,16.368],[20.511,4.794],[17.963,-13.07],[-0.928,-23.139],[-18.387,-11.491],[-26.765,0.995],[-28.89,5.665]]}],"t":40},{"o":{"x":1,"y":0},"i":{"x":0.971,"y":1},"s":[{"c":true,"i":[[-10.45,-3.5],[-5.337,1.625],[0.065,4.325],[5.045,5.015],[12.06,0],[2.32,-2.28],[0.22,-5.32],[0,0]],"o":[[10.45,3.5],[5.337,-1.625],[0.435,-3.575],[-2.81,-2.92],[-12.19,0],[0,0],[0,0],[-2.69,7.15]],"v":[[-23.12,12.5],[8.857,7.25],[24.72,-4.05],[18.36,-16.14],[-0.31,-24.5],[-18.825,-16.23],[-25.78,-3.32],[-24.28,-7.15]]}],"t":46},{"s":[{"c":true,"i":[[0.17,-0.125],[0,0],[-0.095,0.4],[0.046,-0.11],[0.81,-0.125],[0.325,0.105],[0.28,-0.305],[0,0]],"o":[[0,0],[0.182,0.125],[-0.22,0.15],[-0.11,-0.36],[0.185,0],[0,0],[0,0],[-0.095,0.275]],"v":[[-1.67,-24.25],[-0.807,-24.375],[-0.28,-23.525],[0.391,-24.39],[-0.31,-24.5],[-0.294,-24.543],[-1.03,-24.32],[-1.405,-24.4]]}],"t":53}],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.6863,0.0824],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"time","ix":2,"cix":2,"np":1,"it":[{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.0078,0.0627,0.1882],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":5,"parent":6},{"ty":4,"nm":"outline-shape","sr":1,"st":0,"op":60,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[144,144,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[360,360,0],"ix":2},"r":{"a":1,"k":[{"o":{"x":0.185,"y":4.757},"i":{"x":0.575,"y":-11.69},"s":[0],"t":0},{"o":{"x":0.341,"y":0.192},"i":{"x":0.703,"y":1},"s":[7],"t":20},{"o":{"x":0.167,"y":0},"i":{"x":0.703,"y":1},"s":[193],"t":30},{"o":{"x":0.167,"y":0},"i":{"x":0.703,"y":1},"s":[176],"t":39},{"s":[180],"t":43}],"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"shape","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,7.99],[0,0],[0,0],[0,0],[19.09,-27.84],[0,0],[-0.19,-8.53],[0,0],[0,0],[0,0],[0,0],[-17.24,26.68],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,8.5],[0,0],[18.52,27.1],[0,0],[0,0],[0,0],[0,0],[0,-7.99],[0,0],[0,0],[-17.24,-26.68]],"v":[[-50.5,-54.51],[-50.5,-71],[50.5,-71],[50.5,-54.51],[21.86,0],[22.43,0.83],[50.5,54.26],[50.5,54.51],[50.5,71],[-50.5,71],[-50.5,54.51],[-24.64,2.5],[-23.02,0],[-24.64,-2.5]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"c":{"a":0,"k":[0.0078,0.0627,0.1882],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":6},{"ty":4,"nm":"bottom","sr":1,"st":0,"op":60,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[-0.237,75.839,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"bottom","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-74.495,-9.31],[74.495,-9.31],[74.495,9.31],[-74.495,9.31]]},"ix":2}},{"ty":"rd","bm":0,"hd":false,"mn":"ADBE Vector Filter - RC","nm":"Round Corners 1","ix":2,"r":{"a":0,"k":9.31,"ix":1}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.0078,0.0627,0.1882],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":7,"parent":6}],"v":"5.2.1","fr":60,"op":60,"ip":0,"assets":[]} \ No newline at end of file diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 44f979c..8ca73b5 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -189,4 +189,5 @@ class AppAnimations { static const String splashLaunching = '$lottieBasePath/splash_launching.json'; static const String noData = '$lottieBasePath/Nodata.json'; static const String ripple = '$lottieBasePath/Ripple.json'; + static const String pending_loading_animation = '$lottieBasePath/pending_loading_animation.json'; } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index fafcafe..8d69eb4 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -46,6 +46,7 @@ import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/routes/spring_page_route_builder.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; +import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; class LandingPage extends StatefulWidget { @@ -308,6 +309,7 @@ class _LandingPageState extends State { textColor: AppColors.alertColor, ), Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), + // Lottie.asset(AppAnimations.pending_loading_animation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 40.h, height: 40.h, fit: BoxFit.contain), ], ), SizedBox(height: 8.h), From a4a32cca447a325e6ae5c954cf33086df135b1c7 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Thu, 2 Oct 2025 16:31:50 +0300 Subject: [PATCH 21/43] doctor filter icon removed from the doctor listing from clininc --- .../book_appointment/select_doctor_page.dart | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index ec17c61..5975631 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -105,25 +105,6 @@ class _SelectDoctorPageState extends State { ), ), ), - SizedBox( - height: 56.h, - width: 56.h, - child: DecoratedBox( - decoration: - RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 10.h, - hasShadow: false, - ), - child: Utils.buildSvgWithAssets( - icon: AppAssets.ic_filters, - height: 24.h, - width: 24.h, - ).paddingAll(16.h).onPress((){ - - }), - ), - ) ], ), ListView.separated( From 786aa05b19514ce7ac9246ce7e9da0321695366d Mon Sep 17 00:00:00 2001 From: tahaalam Date: Thu, 2 Oct 2025 16:38:46 +0300 Subject: [PATCH 22/43] doctor list handled to be cleared when going back --- .../book_appointment/search_doctor_by_name.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index 233aff9..5949ef5 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -88,7 +88,7 @@ class _SearchDoctorByNameState extends State { ), ), Visibility( - visible: bookAppointmentsViewModel.doctorsList.isNotEmpty, + visible: context.watch().doctorsList.isNotEmpty, child: SizedBox( height: 56.h, width: 56.h, @@ -264,4 +264,9 @@ class _SearchDoctorByNameState extends State { ), ); } + @override + void dispose() { + bookAppointmentsViewModel.doctorsList.clear(); + super.dispose(); + } } From 5e000db55674f1bd16de4ed76b3351281c228574 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 5 Oct 2025 08:56:41 +0300 Subject: [PATCH 23/43] updates --- .../immediate_livecare_pending_request_page.dart | 2 ++ lib/presentation/home/landing_page.dart | 2 +- .../widgets/medical_file_appointment_card.dart | 9 +++++---- .../medical_file/widgets/patient_sick_leave_card.dart | 7 ++++--- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart index 90b6cb1..2d671cf 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart @@ -16,6 +16,7 @@ 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:lottie/lottie.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -121,6 +122,7 @@ class _ImmediateLiveCarePendingRequestPageState extends State { SizedBox(height: 12.h), ], ) - : SizedBox.shrink(); + : SizedBox(height: 12.h); }), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, 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 3e7b7c9..434bad2 100644 --- a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart @@ -121,11 +121,12 @@ class MedicalFileAppointmentCard extends StatelessWidget { child: Padding( padding: EdgeInsets.all(10.h), child: Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, - width: 10.h, - height: 10.h, + iconColor: AppColors.whiteColor, + icon: AppAssets.forward_arrow_icon_small, + width: 40.h, + height: 40.h, fit: BoxFit.contain, ), ), diff --git a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart index fed7d15..1f6c0bf 100644 --- a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart +++ b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart @@ -142,9 +142,10 @@ class PatientSickLeaveCard extends StatelessWidget { child: Transform.flip( flipX: _appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, - width: 10.h, - height: 10.h, + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.whiteColor, + width: 40.h, + height: 40.h, fit: BoxFit.contain, ), ), From 2e9a462a1cbafa507f6bfb7416251444fd2f7ad0 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Sun, 5 Oct 2025 09:42:29 +0300 Subject: [PATCH 24/43] listing resizing if the data is in small numbers. --- lib/presentation/lab/lab_results/lab_result_details.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index 382a846..74557ac 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -148,7 +148,7 @@ class LabResultDetails extends StatelessWidget { borderRadius: 24.h, hasShadow: true, ), - height: 260.h, + height: model.isGraphVisible?260.h:(labmodel.filteredGraphValues.length<3)?(labmodel.filteredGraphValues.length*64)+80.h:260.h, padding: EdgeInsets.all(16.h), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -173,8 +173,8 @@ class LabResultDetails extends StatelessWidget { //todo handle when the graph icon is being displayed Utils.buildSvgWithAssets( icon: model.isGraphVisible?AppAssets.ic_list:AppAssets.ic_graph, - width: 24, - height: 24) + width: 24.h, + height: 24.h) .onPress(() { model.alterGraphVisibility(); }), @@ -291,7 +291,7 @@ class LabResultDetails extends StatelessWidget { Widget labHistoryList(LabRangeViewModel model, LabViewModel labmodel) { return SizedBox( - height: 180.h, + height: labmodel.filteredGraphValues.length<3?labmodel.filteredGraphValues.length*64:180.h, child: ListView.separated( padding: EdgeInsets.zero, itemCount: labmodel.filteredGraphValues.length,itemBuilder: (context, index){ @@ -313,6 +313,7 @@ class LabResultDetails extends StatelessWidget { double? getInterval(LabViewModel labmodel) { var maxX = labmodel.maxY; + if(maxX<1) return .5; if(maxX >1 && maxX < 5) return 1; if(maxX >5 && maxX < 10) return 5; if(maxX >10 && maxX < 50) return 10; From 42abf9ca1b3fcd8be04a112d964f47149dbf0736 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Sun, 5 Oct 2025 09:45:13 +0300 Subject: [PATCH 25/43] login bypass removed --- lib/core/api/api_client.dart | 12 ++++++------ lib/splashPage.dart | 11 +++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 5925641..799942a 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -176,12 +176,12 @@ class ApiClientImp implements ApiClient { body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; } - body['TokenID'] = "@dm!n"; - body['PatientID'] = 1018977; - body['PatientTypeID'] = 1; - - body['PatientOutSA'] = 0; - body['SessionID'] = "45786230487560q"; + // body['TokenID'] = "@dm!n"; + // body['PatientID'] = 1018977; + // body['PatientTypeID'] = 1; + // + // body['PatientOutSA'] = 0; + // body['SessionID'] = "45786230487560q"; } body.removeWhere((key, value) => value == null); diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 117db61..ba7cefd 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -49,12 +49,11 @@ class _SplashScreenState extends State { Timer(Duration(seconds: 2, milliseconds: 500), () async { LocalNotification.init(onNotificationClick: (payload) {}); - // if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { - // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); - // } else { - // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); - // } - Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LabOrdersPage()))); + if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { + Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); + } else { + Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + } }); var zoom = ZoomVideoSdk(); InitConfig initConfig = InitConfig( From 641df8b51b986f8c83c83ceca5913ab07ebf7cf9 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Sun, 5 Oct 2025 09:50:33 +0300 Subject: [PATCH 26/43] logs removed --- assets/langs/ar-SA.json | 2 +- lib/generated/locale_keys.g.dart | 8 ++++++++ lib/widgets/graph/custom_graph.dart | 2 -- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 7b073f4..c434a9e 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -864,7 +864,7 @@ "regionAndLocation": "المنطقة والمواقع", "clearAllFilters": "مسح جميع الفلاتر", "filters": "فلاتر", - "searchClinic": "بحث عن عيادة" + "searchClinic": "بحث عن عيادة", "normal": "عادي", "attention": "انتباه", "monitor": "مراقبة", diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 3c382f7..3ae958c 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -855,6 +855,14 @@ abstract class LocaleKeys { static const onboardingBody1 = 'onboardingBody1'; static const onboardingHeading2 = 'onboardingHeading2'; static const onboardingBody2 = 'onboardingBody2'; + static const hmgHospitals = 'hmgHospitals'; + static const hmcMedicalClinic = 'hmcMedicalClinic'; + static const applyFilter = 'applyFilter'; + static const facilityAndLocation = 'facilityAndLocation'; + static const regionAndLocation = 'regionAndLocation'; + static const clearAllFilters = 'clearAllFilters'; + static const filters = 'filters'; + static const searchClinic = 'searchClinic'; static const normal = 'normal'; static const attention = 'attention'; static const monitor = 'monitor'; diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index 1e0bd60..02ad987 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -244,8 +244,6 @@ class CustomGraph extends StatelessWidget { ), ) ]; - var max = LineChartHelper().calculateMaxAxisValues(data); - print("the maxX is the -------> ${max.$2}"); return data; } From db979977ca9ad6db0cfab4fcab2c6a51f4950b66 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 5 Oct 2025 12:22:33 +0300 Subject: [PATCH 27/43] Tamara implementation & enhancements --- lib/core/api/api_client.dart | 2 + lib/core/api_consts.dart | 16 +++ .../medical_file/medical_file_view_model.dart | 33 +++-- ...a_installments_details_response_model.dart | 83 +++++++++++ .../my_appointments/my_appointments_repo.dart | 40 ++++++ .../my_appointments_view_model.dart | 47 +++++- .../appointment_payment_page.dart | 31 ++-- .../immediate_livecare_payment_page.dart | 25 +++- lib/presentation/home/landing_page.dart | 4 +- .../lab/lab_result_item_view.dart | 135 ++++++++++-------- .../medical_file/medical_file_page.dart | 4 +- .../patient_sickleaves_list_page.dart | 6 +- .../medical_file/vaccine_list_page.dart | 20 +-- .../medical_file/widgets/lab_rad_card.dart | 2 +- .../widgets/patient_sick_leave_card.dart | 5 +- lib/widgets/in_app_browser/InAppBrowser.dart | 18 +-- 16 files changed, 352 insertions(+), 119 deletions(-) create mode 100644 lib/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index a064f11..9f0d230 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -34,6 +34,7 @@ abstract class ApiClient { required Function(dynamic response, int statusCode) onSuccess, required Function(String error, int statusCode) onFailure, Map? queryParams, + bool isAllowAny, bool isExternal, bool isRCService, }); @@ -324,6 +325,7 @@ class ApiClientImp implements ApiClient { {required Function(dynamic response, int statusCode) onSuccess, required Function(String error, int statusCode) onFailure, Map? queryParams, + bool isAllowAny = false, bool isExternal = false, bool isRCService = false}) async { String url; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 1c6cec1..8225796 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -743,6 +743,9 @@ class ApiConsts { static String SERVICE_URL = 'https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx'; //Payfort Payment Gateway URL LIVE // static String SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; // Payfort Payment Gateway URL UAT + static String TAMARA_URL = "https://mdlaboratories.com/tamaralive/Home/Checkout"; + static String GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamaralive/Home/GetInstallments"; + // var payFortEnvironment = FortEnvironment.test; // var applePayMerchantId = "merchant.com.hmgwebservices.uat"; @@ -753,18 +756,25 @@ class ApiConsts { payFortEnvironment = FortEnvironment.production; applePayMerchantId = "merchant.com.hmgwebservices"; SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx"; + TAMARA_URL = "https://mdlaboratories.com/tamaralive/Home/Checkout"; + GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamaralive/Home/GetInstallments"; break; case AppEnvironmentTypeEnum.dev: baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; + TAMARA_URL = "https://mdlaboratories.com/tamara/Home/Checkout"; + GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamara/Home/GetInstallments"; break; case AppEnvironmentTypeEnum.uat: baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; + TAMARA_URL = "https://mdlaboratories.com/tamara/Home/Checkout"; + // GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamara/Home/GetInstallments"; + GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamaralive/Home/GetInstallments"; break; case AppEnvironmentTypeEnum.preProd: @@ -772,18 +782,24 @@ class ApiConsts { payFortEnvironment = FortEnvironment.production; applePayMerchantId = "merchant.com.hmgwebservices"; SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx"; + TAMARA_URL = "https://mdlaboratories.com/tamara/Home/Checkout"; + GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamara/Home/GetInstallments"; break; case AppEnvironmentTypeEnum.qa: baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; + TAMARA_URL = "https://mdlaboratories.com/tamara/Home/Checkout"; + GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamara/Home/GetInstallments"; break; case AppEnvironmentTypeEnum.staging: baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; + TAMARA_URL = "https://mdlaboratories.com/tamara/Home/Checkout"; + GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamara/Home/GetInstallments"; break; } } diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 95c5d63..3f0573f 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -108,12 +108,17 @@ class MedicalFileViewModel extends ChangeNotifier { final result = await medicalFileRepo.getPatientVaccinesList(); result.fold( - (failure) async => await errorHandlerService.handleError( - failure: failure, - onOkPressed: () { - onError!(failure.message); - }, - ), + // (failure) async => await errorHandlerService.handleError( + // failure: failure, + // onOkPressed: () { + // onError!(failure.message); + // }, + // ), + (failure) async { + // onError!(failure.message); + isPatientVaccineListLoading = false; + notifyListeners(); + }, (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); @@ -134,12 +139,16 @@ class MedicalFileViewModel extends ChangeNotifier { final result = await medicalFileRepo.getPatientSickLeavesList(); result.fold( - (failure) async => await errorHandlerService.handleError( - failure: failure, - onOkPressed: () { - onError!(failure.message); - }, - ), + // (failure) async => await errorHandlerService.handleError( + // failure: failure, + // onOkPressed: () { + // onError!(failure.message); + // }, + // ), + (failure) async { + isPatientSickLeaveListLoading = false; + notifyListeners(); + }, (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); diff --git a/lib/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart b/lib/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart new file mode 100644 index 0000000..59ad707 --- /dev/null +++ b/lib/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart @@ -0,0 +1,83 @@ +class GetTamaraInstallmentsDetailsResponseModel { + String? name; + String? description; + MinLimit? minLimit; + MinLimit? maxLimit; + List? supportedInstalments; + + GetTamaraInstallmentsDetailsResponseModel({this.name, this.description, this.minLimit, this.maxLimit, this.supportedInstalments}); + + GetTamaraInstallmentsDetailsResponseModel.fromJson(Map json) { + name = json['name']; + description = json['description']; + minLimit = json['minLimit'] != null ? new MinLimit.fromJson(json['minLimit']) : null; + maxLimit = json['maxLimit'] != null ? new MinLimit.fromJson(json['maxLimit']) : null; + if (json['supportedInstalments'] != null) { + supportedInstalments = []; + json['supportedInstalments'].forEach((v) { + supportedInstalments!.add(new SupportedInstalments.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + data['name'] = this.name; + data['description'] = this.description; + if (this.minLimit != null) { + data['minLimit'] = this.minLimit!.toJson(); + } + if (this.maxLimit != null) { + data['maxLimit'] = this.maxLimit!.toJson(); + } + if (this.supportedInstalments != null) { + data['supportedInstalments'] = this.supportedInstalments!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class MinLimit { + String? currency; + num? amount; + + MinLimit({this.currency, this.amount}); + + MinLimit.fromJson(Map json) { + currency = json['currency']; + amount = json['amount']; + } + + Map toJson() { + final Map data = new Map(); + data['currency'] = this.currency; + data['amount'] = this.amount; + return data; + } +} + +class SupportedInstalments { + int? instalments; + MinLimit? minLimit; + MinLimit? maxLimit; + + SupportedInstalments({this.instalments, this.minLimit, this.maxLimit}); + + SupportedInstalments.fromJson(Map json) { + instalments = json['instalments']; + minLimit = json['minLimit'] != null ? new MinLimit.fromJson(json['minLimit']) : null; + maxLimit = json['maxLimit'] != null ? new MinLimit.fromJson(json['maxLimit']) : null; + } + + Map toJson() { + final Map data = new Map(); + data['instalments'] = this.instalments; + if (this.minLimit != null) { + data['minLimit'] = this.minLimit!.toJson(); + } + if (this.maxLimit != null) { + data['maxLimit'] = this.maxLimit!.toJson(); + } + return data; + } +} diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index eb8611d..b94f004 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/utils.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; 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/models/resp_models/patient_appointment_share_response_model.dart'; @@ -44,6 +45,8 @@ abstract class MyAppointmentsRepo { Future>>> getPatientDoctorsList(); Future>> insertLiveCareVIDARequest({required clientRequestID, required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel}); + + Future>> getTamaraInstallmentsDetails(); } class MyAppointmentsRepoImp implements MyAppointmentsRepo { @@ -543,4 +546,41 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> getTamaraInstallmentsDetails() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.get( + ApiConsts.GET_TAMARA_INSTALLMENTS_URL, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response; + + final tamaraInstallmentsList = GetTamaraInstallmentsDetailsResponseModel.fromJson(list.first); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: tamaraInstallmentsList, + ); + } 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/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index c553f0e..acf0a39 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.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/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_share_response_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 '../../core/utils/doctor_response_mapper.dart' show DoctorMapper; - class MyAppointmentsViewModel extends ChangeNotifier { int selectedTabIndex = 0; @@ -32,6 +31,9 @@ class MyAppointmentsViewModel extends ChangeNotifier { PatientAppointmentShareResponseModel? patientAppointmentShareResponseModel; + GetTamaraInstallmentsDetailsResponseModel? getTamaraInstallmentsDetailsResponseModel; + bool isTamaraDetailsLoading = false; + MyAppointmentsViewModel({required this.myAppointmentsRepo, required this.errorHandlerService, required this.appState}); void onTabChange(int index) { @@ -50,6 +52,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientMyDoctorsList.clear(); isPatientMyDoctorsLoading = true; } + isTamaraDetailsLoading = true; isAppointmentPatientShareLoading = true; notifyListeners(); } @@ -79,6 +82,11 @@ class MyAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + setIsTamaraDetailsLoading(bool val) { + isTamaraDetailsLoading = val; + notifyListeners(); + } + setAppointmentReminder(bool value, PatientAppointmentHistoryResponseModel item) { int index = patientAppointmentsHistoryList.indexOf(item); if (index != -1) { @@ -142,7 +150,13 @@ class MyAppointmentsViewModel extends ChangeNotifier { final result = await myAppointmentsRepo.getPatientShareAppointment(projectID: projectID, clinicID: clinicID, appointmentNo: appointmentNo, isLiveCareAppointment: isLiveCareAppointment); result.fold( - (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + await errorHandlerService.handleError( + failure: failure, + onOkPressed: () { + onError!(failure.message); + }); + }, (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); @@ -337,4 +351,31 @@ class MyAppointmentsViewModel extends ChangeNotifier { }, ); } + + Future getTamaraInstallmentsDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.getTamaraInstallmentsDetails(); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + getTamaraInstallmentsDetailsResponseModel = apiResponse.data!; + isTamaraDetailsLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + + // if (apiResponse.messageStatus == 2) { + // onError!(apiResponse.errorMessage!); + // // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + // } else if (apiResponse.messageStatus == 1) { + // getTamaraInstallmentsDetailsResponseModel = apiResponse.data!; + // notifyListeners(); + // if (onSuccess != null) { + // onSuccess(apiResponse); + // } + // } + }, + ); + } } diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 7334369..ee50ab4 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -51,17 +51,28 @@ class _AppointmentPaymentPageState extends State { String transID = ""; + bool isShowTamara = false; + @override void initState() { scheduleMicrotask(() { payfortViewModel.initPayfortViewModel(); + myAppointmentsViewModel.getTamaraInstallmentsDetails().then((val) { + if (myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! >= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && + myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) { + setState(() { + isShowTamara = true; + }); + } + }); payfortViewModel.setIsApplePayConfigurationLoading(false); myAppointmentsViewModel.getPatientShareAppointment( widget.patientAppointmentHistoryResponseModel.projectID, widget.patientAppointmentHistoryResponseModel.clinicID, - widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), - widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, - ); + widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, onError: (err) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }); }); super.initState(); } @@ -162,11 +173,12 @@ class _AppointmentPaymentPageState extends State { openPaymentURL("visa"); }), SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: false, + isShowTamara + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, ), child: Row( mainAxisSize: MainAxisSize.max, @@ -196,7 +208,8 @@ class _AppointmentPaymentPageState extends State { ).paddingSymmetrical(24.h, 0.h).onPress(() { selectedPaymentMethod = "TAMARA"; openPaymentURL("tamara"); - }), + }) + : SizedBox.shrink(), ], ), ), diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index d89ef33..5437945 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -56,10 +56,20 @@ class _ImmediateLiveCarePaymentPageState extends State= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && + num.parse(immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total!) <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) { + setState(() { + isShowTamara = true; + }); + } + }); }); super.initState(); } @@ -67,9 +77,9 @@ class _ImmediateLiveCarePaymentPageState extends State(); - myAppointmentsViewModel = Provider.of(context); + myAppointmentsViewModel = Provider.of(context, listen: false); immediateLiveCareViewModel = Provider.of(context, listen: false); - payfortViewModel = Provider.of(context); + payfortViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Consumer(builder: (context, myAppointmentsVM, child) { @@ -161,9 +171,10 @@ class _ImmediateLiveCarePaymentPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "Quick Links".toText16(isBold: true), + "Quick Links".needTranslation.toText16(isBold: true), Row( children: [ - "View medical file".toText12(color: AppColors.primaryRedColor), + "View medical file".needTranslation.toText12(color: AppColors.primaryRedColor), SizedBox(width: 2.h), Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), ], diff --git a/lib/presentation/lab/lab_result_item_view.dart b/lib/presentation/lab/lab_result_item_view.dart index 03f721e..57a10ae 100644 --- a/lib/presentation/lab/lab_result_item_view.dart +++ b/lib/presentation/lab/lab_result_item_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.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/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; @@ -49,7 +50,7 @@ class LabResultItemView extends StatelessWidget { backgroundColor: getLabOrderStatusColor(labOrder?.status ?? 0).withOpacity(0.15), textColor: getLabOrderStatusColor(labOrder?.status ?? 0), ).toShimmer2(isShow: isLoading, width: 100), - if (!isLoading) Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + // if (!isLoading) Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), @@ -59,7 +60,7 @@ class LabResultItemView extends StatelessWidget { isLoading ? "" : labOrder!.doctorImageURL!, width: 24.h, height: 24.h, - fit: BoxFit.fill, + fit: BoxFit.cover, errorBuilder: (cxt, child, tr) { return SizedBox(height: 24, width: 24); }, @@ -69,71 +70,83 @@ class LabResultItemView extends StatelessWidget { ], ), SizedBox(height: 8.h), - Wrap( - spacing: 8.h, - runSpacing: 0.h, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - AppCustomChipWidget(labelText: isLoading ? "null" : DateUtil.formatDateToDate(DateUtil.convertStringToDate(labOrder!.createdOn), false)).toShimmer2(isShow: isLoading, width: 70), - AppCustomChipWidget(labelText: isLoading ? "null" : labOrder!.clinicDescription!).toShimmer2(isShow: isLoading, width: 100), + Wrap( + spacing: 8.h, + runSpacing: 0.h, + children: [ + AppCustomChipWidget(labelText: isLoading ? "null" : DateUtil.formatDateToDate(DateUtil.convertStringToDate(labOrder!.createdOn), false)).toShimmer2(isShow: isLoading, width: 70), + AppCustomChipWidget(labelText: isLoading ? "null" : labOrder!.clinicDescription!).toShimmer2(isShow: isLoading, width: 100), + ], + ), + Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.textColor, + width: 20.h, + height: 14.h, + fit: BoxFit.contain, + ), ], ), ], ), ), - AnimatedSwitcher( - duration: Duration(milliseconds: 300), - switchInCurve: Curves.easeIn, - switchOutCurve: Curves.easeOut, - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - axisAlignment: 0.0, - child: child, - ), - ); - }, - child: isExpanded - ? Container( - key: ValueKey(index), - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...labOrder!.testDetails!.map((detail) { - return Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: '● ${detail.description}'.toText14(weight: FontWeight.w500), - ); - }).toList(), - SizedBox(height: 16.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox(), - CustomButton( - icon: AppAssets.view_report_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - text: LocaleKeys.viewReport.tr(context: context), - onPressed: () {}, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14, - fontWeight: FontWeight.bold, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ), - ], - ), - ], - ), - ) - : SizedBox.shrink(key: ValueKey(-index)), - ), + // AnimatedSwitcher( + // duration: Duration(milliseconds: 300), + // switchInCurve: Curves.easeIn, + // switchOutCurve: Curves.easeOut, + // transitionBuilder: (Widget child, Animation animation) { + // return FadeTransition( + // opacity: animation, + // child: SizeTransition( + // sizeFactor: animation, + // axisAlignment: 0.0, + // child: child, + // ), + // ); + // }, + // child: isExpanded + // ? Container( + // key: ValueKey(index), + // padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // ...labOrder!.testDetails!.map((detail) { + // return Padding( + // padding: EdgeInsets.only(bottom: 8.h), + // child: '● ${detail.description}'.toText14(weight: FontWeight.w500), + // ); + // }).toList(), + // SizedBox(height: 16.h), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // SizedBox(), + // CustomButton( + // icon: AppAssets.view_report_icon, + // iconColor: AppColors.primaryRedColor, + // iconSize: 16.h, + // text: LocaleKeys.viewReport.tr(context: context), + // onPressed: () {}, + // backgroundColor: AppColors.secondaryLightRedColor, + // borderColor: AppColors.secondaryLightRedColor, + // textColor: AppColors.primaryRedColor, + // fontSize: 14, + // fontWeight: FontWeight.bold, + // borderRadius: 12, + // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + // height: 40.h, + // ), + // ], + // ), + // ], + // ), + // ) + // : SizedBox.shrink(key: ValueKey(-index)), + // ), ], ), ), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 9358d00..dd80513 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -79,7 +79,7 @@ class _MedicalFilePageState extends State { insuranceViewModel.initInsuranceProvider(); medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.getPatientSickLeaveList(); - medicalFileViewModel.getFamilyFiles(); + // medicalFileViewModel.getFamilyFiles(); medicalFileViewModel.onTabChanged(0); } }); @@ -582,7 +582,7 @@ class _MedicalFilePageState extends State { myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, width: 64.h, height: 64.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: false, radius: 50.h), SizedBox(height: 8.h), Expanded( diff --git a/lib/presentation/medical_file/patient_sickleaves_list_page.dart b/lib/presentation/medical_file/patient_sickleaves_list_page.dart index 215836f..a28f518 100644 --- a/lib/presentation/medical_file/patient_sickleaves_list_page.dart +++ b/lib/presentation/medical_file/patient_sickleaves_list_page.dart @@ -30,10 +30,7 @@ class _PatientSickleavesListPageState extends State { void initState() { scheduleMicrotask(() { medicalFileViewModel.setIsPatientSickLeaveListLoading(true); - medicalFileViewModel.getPatientSickLeaveList(onError: (error) { - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }); + medicalFileViewModel.getPatientSickLeaveList(); }); super.initState(); } @@ -75,6 +72,7 @@ class _PatientSickleavesListPageState extends State { child: PatientSickLeaveCard( patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first, isLoading: false, + isSickLeaveListPage: true, ).paddingSymmetrical(24.h, 0.0), ), ), diff --git a/lib/presentation/medical_file/vaccine_list_page.dart b/lib/presentation/medical_file/vaccine_list_page.dart index 969e3f5..82c9441 100644 --- a/lib/presentation/medical_file/vaccine_list_page.dart +++ b/lib/presentation/medical_file/vaccine_list_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; @@ -28,10 +29,7 @@ class _VaccineListPageState extends State { void initState() { scheduleMicrotask(() { medicalFileViewModel.setIsPatientVaccineListLoading(true); - medicalFileViewModel.getPatientVaccinesList(onError: (error) { - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }); + medicalFileViewModel.getPatientVaccinesList(); }); super.initState(); } @@ -51,7 +49,11 @@ class _VaccineListPageState extends State { SizedBox(height: 16.h), ListView.separated( scrollDirection: Axis.vertical, - itemCount: medicalFileVM.isPatientVaccineListLoading ? 5 : medicalFileVM.patientVaccineList.length, + itemCount: medicalFileVM.isPatientVaccineListLoading + ? 5 + : medicalFileVM.patientVaccineList.isNotEmpty + ? medicalFileVM.patientVaccineList.length + : 1, shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.only(left: 24.h, right: 24.h), @@ -102,8 +104,9 @@ class _VaccineListPageState extends State { ), ), ) - : AnimationConfiguration.staggeredList( - position: index, + : medicalFileVM.patientVaccineList.isNotEmpty + ? AnimationConfiguration.staggeredList( + position: index, duration: const Duration(milliseconds: 1000), child: SlideAnimation( verticalOffset: 100.0, @@ -166,7 +169,8 @@ class _VaccineListPageState extends State { ), ), ), - ); + ) + : Utils.getNoDataWidget(context, noDataText: "No vaccines data found...".needTranslation); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ), diff --git a/lib/presentation/medical_file/widgets/lab_rad_card.dart b/lib/presentation/medical_file/widgets/lab_rad_card.dart index 766ef6d..e396d62 100644 --- a/lib/presentation/medical_file/widgets/lab_rad_card.dart +++ b/lib/presentation/medical_file/widgets/lab_rad_card.dart @@ -54,7 +54,7 @@ class LabRadCard extends StatelessWidget { SizedBox.shrink(), Transform.flip( flipX: appState.isArabic() ? true : false, - child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor) + child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor) .toShimmer2(isShow: false, radius: 12.h), ), ], diff --git a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart index 1f6c0bf..8fc3805 100644 --- a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart +++ b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart @@ -23,11 +23,12 @@ import 'package:open_filex/open_filex.dart'; import 'package:provider/provider.dart'; class PatientSickLeaveCard extends StatelessWidget { - PatientSickLeaveCard({super.key, required this.patientSickLeavesResponseModel, this.isLoading = false}); + PatientSickLeaveCard({super.key, required this.patientSickLeavesResponseModel, this.isLoading = false, this.isSickLeaveListPage = false}); late MedicalFileViewModel medicalFileViewModel; PatientSickLeavesResponseModel patientSickLeavesResponseModel; bool isLoading; + bool isSickLeaveListPage = false; @override Widget build(BuildContext context) { @@ -128,7 +129,7 @@ class PatientSickLeaveCard extends StatelessWidget { ).toShimmer2(isShow: isLoading), ), SizedBox(width: 8.h), - Expanded( + isSickLeaveListPage ? SizedBox.shrink() : Expanded( flex: 1, child: Container( height: 40.h, diff --git a/lib/widgets/in_app_browser/InAppBrowser.dart b/lib/widgets/in_app_browser/InAppBrowser.dart index 3fe0de9..e8370e5 100644 --- a/lib/widgets/in_app_browser/InAppBrowser.dart +++ b/lib/widgets/in_app_browser/InAppBrowser.dart @@ -219,12 +219,12 @@ class MyInAppBrowser extends InAppBrowser { // service.tamaraInsertRequest(tamaraRequestModel, context).then((res) { // // if (context != null) GifLoaderDialogUtils.hideDialog(context); - // generateTamaraURL(amount, orderDesc, transactionID, projId, emailId, paymentMethod, patientType, patientName, patientID, authenticatedUser, isLiveCareAppo, servID, LiveServID, appoDate, - // appoNo, clinicID, doctorID, "", installments) - // .then((value) { - // paymentType = _PAYMENT_TYPE.PATIENT; - // this.browser.openUrlRequest(urlRequest: URLRequest(url: WebUri.uri(Uri.parse(value))), options: _InAppBrowserOptions); - // }); + generateTamaraURL(amount, orderDesc, transactionID, projId, emailId, paymentMethod, patientType, patientName, patientID, authenticatedUser, isLiveCareAppo, servID, LiveServID, appoDate, appoNo, + clinicID, doctorID, "", installments) + .then((value) { + paymentType = _PAYMENT_TYPE.PATIENT; + this.browser.openUrlRequest(urlRequest: URLRequest(url: WebUri.uri(Uri.parse(value))), options: _InAppBrowserOptions); + }); // }).catchError((err) { // print(err); // // if (context != null) GifLoaderDialogUtils.hideDialog(context); @@ -333,12 +333,12 @@ class MyInAppBrowser extends InAppBrowser { form = form.replaceFirst('PROJECT_ID_VALUE', projId); form = form.replaceFirst('PAYMENT_OPTION_VALUE', paymentMethod); form = form.replaceFirst('LANG_VALUE', currentLanguageID); - form = form.replaceFirst('SERVICE_URL_VALUE', "https://mdlaboratories.com/tamaralive/Home/Checkout"); + form = form.replaceFirst('SERVICE_URL_VALUE', ApiConsts.TAMARA_URL); - form = form.replaceFirst('INSTALLMENTS_VALUE', installments); + form = form.replaceFirst('INSTALLMENTS_VALUE', "3"); form = form.replaceFirst('CUSTNATIONALID_VALUE', authUser.patientIdentificationNo!); form = form.replaceFirst('CUSTMOBILE_VALUE', authUser.mobileNumber!); - form = form.replaceFirst('CUSTDOB_VALUE', DateUtil.getDayMonthYearDateFormatted(authUser.strDateofBirth!)); + form = form.replaceFirst('CUSTDOB_VALUE', DateUtil.getDayMonthYearDateFormatted(DateUtil.convertStringToDate(authUser.dateofBirth!))); form = form.replaceFirst('CURRENCY_VALUE', authUser.outSa == 0 ? "SAR" : "AED"); form = form.replaceFirst('COUNTRY_CODE_VALUE', authUser.outSa == 0 ? "966" : "971"); From bbf83d4dc9046061cc5e9a52e4d380e8495a8ec9 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Sun, 5 Oct 2025 13:27:53 +0300 Subject: [PATCH 28/43] bottom handled when dental and laser clinic is selected from clinic page --- .../appointment_via_region_viewmodel.dart | 11 ++++++++++- .../region_bottomsheet/region_list_widget.dart | 2 +- .../book_appointment/select_clinic_page.dart | 15 +++++++++++---- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/features/my_appointments/appointment_via_region_viewmodel.dart b/lib/features/my_appointments/appointment_via_region_viewmodel.dart index 852e678..ffa6ec1 100644 --- a/lib/features/my_appointments/appointment_via_region_viewmodel.dart +++ b/lib/features/my_appointments/appointment_via_region_viewmodel.dart @@ -17,7 +17,8 @@ enum AppointmentViaRegionState { enum RegionBottomSheetType{ FOR_REGION, - FOR_CLINIIC + REGION_FOR_DENTAL_AND_LASER, + FOR_CLINIIC, } class AppointmentViaRegionViewmodel extends ChangeNotifier { @@ -94,4 +95,12 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { page: SelectDoctorPage(), ),); } + + void handleLastStepForDentalAndLaser() { + //todo handle the routing here + navigationService.pop(); + navigationService.push(CustomPageRoute( + page: SelectDoctorPage(), + ),); + } } diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart index 09c7c40..174fe10 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart @@ -24,7 +24,7 @@ class _RegionBottomSheetBodyState extends State { @override void initState() { scheduleMicrotask(() { - if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION) { + if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION || regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER ) { myAppointmentsViewModel.getRegionMappedProjectList(); } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { myAppointmentsViewModel.getMappedDoctors(); diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index 36a490d..2c005d4 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -206,16 +206,17 @@ class _SelectClinicPageState extends State { Navigator.of(context).push( CustomPageRoute( page: SelectLivecareClinicPage(onNegativeClicked: (){ - handleDoctorScreen(); + handleDoctorScreen(clinic); },), ), ); } else { - handleDoctorScreen(); + handleDoctorScreen(clinic); } } - void handleDoctorScreen() { + //17 and 235 + void handleDoctorScreen(GetClinicsListResponseModel clinic) { if (widget.isFromRegionFlow) { Navigator.of(context).push( CustomPageRoute( @@ -223,7 +224,13 @@ class _SelectClinicPageState extends State { ), ); } else { - openRegionListBottomSheet(context, RegionBottomSheetType.FOR_CLINIIC); + + var bottomSheetType = RegionBottomSheetType.FOR_CLINIIC; + print("the cliininc id is ${clinic.clinicID}"); + if(clinic.clinicID == 17 || clinic.clinicID == 235) { + bottomSheetType = RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER; + } + openRegionListBottomSheet(context, bottomSheetType); } } From 903248133eef8f2af1f86b81e205dbddfa3ae8ec Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 5 Oct 2025 13:49:58 +0300 Subject: [PATCH 29/43] Lab order & results enhancements --- lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 2 +- .../book_appointment_page.dart | 107 +++++++++++++++++- .../book_appointment/select_clinic_page.dart | 1 + .../select_livecare_clinic_page.dart | 3 - lib/presentation/lab/lab_order_by_test.dart | 56 ++++----- lib/presentation/lab/lab_orders_page.dart | 2 +- .../lab/lab_result_item_view.dart | 6 +- 8 files changed, 138 insertions(+), 41 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 2efeeed..35d63b4 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -174,7 +174,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1018977; + // body['PatientID'] = 4767477; // body['PatientTypeID'] = 1; // // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 9e1a9ba..8225796 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu 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/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 516aff9..b124438 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -9,6 +9,7 @@ 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/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; @@ -22,6 +23,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/search_doctor_ import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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/common_bottom_sheet.dart' show showCommonBottomSheetWithoutHeight; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; @@ -43,9 +45,11 @@ class _BookAppointmentPageState extends State { late AppointmentViaRegionViewmodel regionalViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; late ImmediateLiveCareViewModel immediateLiveCareViewModel; + late final AuthenticationViewModel authVM; @override void initState() { + authVM = context.read(); scheduleMicrotask(() { bookAppointmentsViewModel.selectedTabIndex = 0; bookAppointmentsViewModel.initBookAppointmentViewModel(); @@ -204,11 +208,12 @@ class _BookAppointmentPageState extends State { ).paddingSymmetrical(24.h, 0.h); case 1: //TODO: Get LiveCare type Select UI from Hussain - return Column( - children: [ - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, + return appState.isAuthenticated + ? Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: false, ), @@ -318,7 +323,8 @@ class _BookAppointmentPageState extends State { ), ), ], - ).paddingSymmetrical(24.h, 0.h); + ).paddingSymmetrical(24.h, 0.h) + : getLiveCareNotLoggedInUI(); default: SizedBox.shrink(); } @@ -375,4 +381,93 @@ class _BookAppointmentPageState extends State { }); } } + + Widget getLiveCareNotLoggedInUI() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.immediate_service_icon, width: 58.h, height: 58.h), + SizedBox(width: 18.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Immediate service".needTranslation.toText18(color: AppColors.textColor, isBold: true), + "No need to wait, you will get medical consultation immediately via video call".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + ), + ], + ), + SizedBox(height: 24.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.no_visit_icon, width: 58.h, height: 58.h), + SizedBox(width: 18.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "No visit required".needTranslation.toText18(color: AppColors.textColor, isBold: true), + LocaleKeys.livecarePoint5.tr(context: context).toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + ), + ], + ), + SizedBox(height: 24.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.doctor_contact_icon, width: 58.h, height: 58.h), + SizedBox(width: 18.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Doctor will contact".needTranslation.toText18(color: AppColors.textColor, isBold: true), + "A specialised doctor will contact you and will be able to view your medical history".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + ), + ], + ), + SizedBox(height: 24.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.free_med_delivery_icon, width: 58.h, height: 58.h), + SizedBox(width: 18.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Free medicine delivery".needTranslation.toText18(color: AppColors.textColor, isBold: true), + "Offers free medicine delivery for the LiveCare appointment".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), + ), + ], + ), + SizedBox(height: 36.h), + CustomButton( + text: "Login to use this service".needTranslation, + onPressed: () async { + await authVM.onLoginPressed(); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + icon: AppAssets.login1, + iconColor: AppColors.whiteColor, + iconSize: 24.h, + ), + ], + ).paddingSymmetrical(24.h, 0.h); + } } diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index 36a490d..869fea3 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -215,6 +215,7 @@ class _SelectClinicPageState extends State { } } + // ClinicID 17 & Clinic ID 253 void handleDoctorScreen() { if (widget.isFromRegionFlow) { Navigator.of(context).push( diff --git a/lib/presentation/book_appointment/select_livecare_clinic_page.dart b/lib/presentation/book_appointment/select_livecare_clinic_page.dart index 1e40357..12a99bc 100644 --- a/lib/presentation/book_appointment/select_livecare_clinic_page.dart +++ b/lib/presentation/book_appointment/select_livecare_clinic_page.dart @@ -6,12 +6,9 @@ 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/presentation/book_appointment/select_doctor_page.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; class SelectLivecareClinicPage extends StatelessWidget { diff --git a/lib/presentation/lab/lab_order_by_test.dart b/lib/presentation/lab/lab_order_by_test.dart index bb0f391..4c0d2c3 100644 --- a/lib/presentation/lab/lab_order_by_test.dart +++ b/lib/presentation/lab/lab_order_by_test.dart @@ -35,41 +35,43 @@ class LabOrderByTest extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ...labOrder!.testDetails!.map((detail) { - Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: '${tests!.description}'.toText14(weight: FontWeight.w500), - ), - - SizedBox(height: 12.h), - + '${tests!.description}'.toText16(isBold: true), + SizedBox(height: 4.h), + //TODO: Add test long description from the Lab Order API + "Measurement of ALT (alanine aminotransferase) level in blood used to assess liver functions".toText12(fontWeight: FontWeight.w500), + SizedBox(height: 8.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AppCustomChipWidget( - richText: '${"Last Tested:".needTranslation} ${ DateUtil.formatDateToDate(DateUtil.convertStringToDate(tests!.createdOn), false)}'.toText12(isBold: true), - // chipType: ChipTypeEnum.lightBg, + richText: '${"Last Tested:".needTranslation} ${DateUtil.formatDateToDate(DateUtil.convertStringToDate(tests!.createdOn), false)}'.toText12(fontWeight: FontWeight.w500), backgroundColor: AppColors.greyLightColor, textColor: AppColors.textColor, - // borderRadius: 5, - ), - CustomButton( - icon: AppAssets.view_report_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - text: LocaleKeys.viewReport.tr(context: context), - onPressed: () { + ], + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded(child: Container()), + Expanded( + child: CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + text: LocaleKeys.viewReport.tr(context: context), + onPressed: () { onTap(); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14, - fontWeight: FontWeight.bold, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), ), ], ), diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 1c78bbd..6b65d5d 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.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/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/lab/lab_range_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late LabRangeViewModel rangeViewModel; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context); rangeViewModel = Provider.of(context); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 16.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; scheduleMicrotask(() { labProvider .getPatientLabResultByHospital( model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); }); Navigator.push(context,FadePage( page: LabResultByHospitals(), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().length, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( onTap: () { if(model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult( model.uniqueTests .toList()[index] .model!, model.uniqueTests .toList()[index].description!); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.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/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/lab/lab_range_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late LabRangeViewModel rangeViewModel; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context); rangeViewModel = Provider.of(context); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 16.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; scheduleMicrotask(() { labProvider .getPatientLabResultByHospital( model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); }); Navigator.push( context, CustomPageRoute( page: LabResultByHospitals(), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().length, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( onTap: () { if(model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult( model.uniqueTests .toList()[index] .model!, model.uniqueTests .toList()[index].description!); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/lab/lab_result_item_view.dart b/lib/presentation/lab/lab_result_item_view.dart index 57a10ae..9269c6d 100644 --- a/lib/presentation/lab/lab_result_item_view.dart +++ b/lib/presentation/lab/lab_result_item_view.dart @@ -81,8 +81,10 @@ class LabResultItemView extends StatelessWidget { AppCustomChipWidget(labelText: isLoading ? "null" : labOrder!.clinicDescription!).toShimmer2(isShow: isLoading, width: 100), ], ), - Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, + isLoading + ? SizedBox.shrink() + : Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.textColor, width: 20.h, height: 14.h, From 35e145ddf94408e8e3c01c6af6b6e298b3050f62 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Sun, 5 Oct 2025 14:20:39 +0300 Subject: [PATCH 30/43] id selection from the appropiate list --- .../hospital_bottom_sheet_body.dart | 5 +++-- .../book_appointment/select_clinic_page.dart | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) 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 44cc43e..34714f4 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 @@ -100,10 +100,11 @@ class HospitalBottomSheetBody extends StatelessWidget { regionalViewModel.setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); regionalViewModel.handleLastStepForRegion(); }else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { - regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); regionalViewModel.handleLastStepForClinic(); - + }else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); + regionalViewModel.handleLastStepForDentalAndLaser(); } });}, separatorBuilder: (_, __) => SizedBox( diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index 2c005d4..285dcfc 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -260,7 +260,21 @@ class _SelectClinicPageState extends State { return HospitalBottomSheetBody(); } if(data.bottomSheetState == AppointmentViaRegionState.DOCTOR_SELECTION){ - bookAppointmentsViewModel.setProjectID(regionalViewModel.selectedHospital?.patientDoctorAppointmentList?.first.projectID.toString()); + //if the region screen is opened for the dental clinic thenthe project id will be in the hospital list as the list is formed form the get project api + var id = ""; + if (data.regionBottomSheetType == + RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { + id = regionalViewModel.selectedHospital?.hospitalList?.first?.iD + ?.toString() ?? + ""; + } else { + id = regionalViewModel.selectedHospital?.patientDoctorAppointmentList + ?.first?.projectID + ?.toString() ?? + ""; + } + bookAppointmentsViewModel.setProjectID(id); + return SizedBox.shrink(); } else { return SizedBox.shrink(); From 415c0eb3c72897bd40f11a796524ef6791da2013 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 5 Oct 2025 17:16:25 +0300 Subject: [PATCH 31/43] Dental appointment booking implementation contd. --- lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 25 ++-- lib/core/utils/utils.dart | 6 +- .../book_appointments_repo.dart | 44 ++++++ .../book_appointments_view_model.dart | 42 +++++- ...et_patient_dental_plan_response_model.dart | 40 ++++++ .../book_appointment/select_clinic_page.dart | 125 +++++++++++++++++- .../book_appointment/select_doctor_page.dart | 103 ++++++++------- lib/presentation/home/landing_page.dart | 4 +- 9 files changed, 317 insertions(+), 74 deletions(-) create mode 100644 lib/features/book_appointments/models/resp_models/get_patient_dental_plan_response_model.dart diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 35d63b4..9140a13 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -174,7 +174,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 4767477; + // body['PatientID'] = 4767884; // body['PatientTypeID'] = 1; // // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 8225796..511cb37 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -746,6 +746,8 @@ class ApiConsts { static String TAMARA_URL = "https://mdlaboratories.com/tamaralive/Home/Checkout"; static String GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamaralive/Home/GetInstallments"; + // static String GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; + // var payFortEnvironment = FortEnvironment.test; // var applePayMerchantId = "merchant.com.hmgwebservices.uat"; @@ -764,42 +766,41 @@ class ApiConsts { payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; - TAMARA_URL = "https://mdlaboratories.com/tamara/Home/Checkout"; - GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamara/Home/GetInstallments"; + TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; + GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; break; case AppEnvironmentTypeEnum.uat: baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; - TAMARA_URL = "https://mdlaboratories.com/tamara/Home/Checkout"; - // GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamara/Home/GetInstallments"; - GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamaralive/Home/GetInstallments"; + TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; + GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; break; case AppEnvironmentTypeEnum.preProd: - + baseUrl = "https://webservices.hmg.com/"; payFortEnvironment = FortEnvironment.production; applePayMerchantId = "merchant.com.hmgwebservices"; SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx"; - TAMARA_URL = "https://mdlaboratories.com/tamara/Home/Checkout"; - GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamara/Home/GetInstallments"; + TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; + GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; break; case AppEnvironmentTypeEnum.qa: baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; - TAMARA_URL = "https://mdlaboratories.com/tamara/Home/Checkout"; - GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamara/Home/GetInstallments"; + TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; + GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; break; case AppEnvironmentTypeEnum.staging: baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; - TAMARA_URL = "https://mdlaboratories.com/tamara/Home/Checkout"; - GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamara/Home/GetInstallments"; + TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; + GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; break; } } diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index f3a53b9..2bff2ac 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -323,7 +323,7 @@ class Utils { children: [ Lottie.asset(AppAnimations.loadingAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), SizedBox(height: 8.h), - (loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor), + (loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor, isCenter: true), SizedBox(height: 8.h), ], ).center; @@ -355,7 +355,7 @@ class Utils { ).center; } - static Widget getWarningWidget({String? loadingText, bool isShowActionButtons = false, Function? onConfirmTap, Function? onCancelTap}) { + static Widget getWarningWidget({String? loadingText, bool isShowActionButtons = false, Widget? bodyWidget, Function? onConfirmTap, Function? onCancelTap}) { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -364,6 +364,8 @@ class Utils { SizedBox(height: 8.h), (loadingText ?? LocaleKeys.loadingText.tr()).toText14(color: AppColors.blackColor, letterSpacing: 0), SizedBox(height: 16.h), + bodyWidget ?? SizedBox.shrink(), + SizedBox(height: 16.h), isShowActionButtons ? Row( children: [ diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index 43a7eb3..f15c5ea 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -10,6 +10,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/resp_model import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_clinics_response_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_patient_dental_plan_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_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/services/logger_service.dart'; @@ -68,6 +69,9 @@ abstract class BookAppointmentsRepo { required int serviceID, Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>>> getPatientDentalEstimation( + {required int projectID, Function(dynamic)? onSuccess, Function(String)? onError}); } class BookAppointmentsRepoImp implements BookAppointmentsRepo { @@ -656,4 +660,44 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> getPatientDentalEstimation( + {required int projectID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = { + "ProjectID": projectID, + }; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + HAS_DENTAL_PLAN, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_IsPatientHasOnGoingEstimation']; + final estimationList = list.map((item) => PatientDentalPlanEstimationResponseModel.fromJson(item as Map)).toList().cast(); + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: estimationList, + ); + } 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 ad59a27..f04394b 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -15,6 +15,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/models/free_slot. import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_patient_dental_plan_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/timeslots.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; @@ -55,6 +56,10 @@ class BookAppointmentsViewModel extends ChangeNotifier { List liveCareDoctorsList = []; + List patientDentalPlanEstimationList = []; + int totalTimeNeededForDentalProcedure = 0; + bool isContinueDentalPlan = false; + GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel(); DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel(); GetLiveCareClinicsResponseModel selectedLiveCareClinic = GetLiveCareClinicsResponseModel(); @@ -128,6 +133,8 @@ class BookAppointmentsViewModel extends ChangeNotifier { clinicsList.clear(); doctorsList.clear(); liveCareClinicsList.clear(); + patientDentalPlanEstimationList.clear(); + isContinueDentalPlan = false; // getLocation(); notifyListeners(); } @@ -275,11 +282,10 @@ class BookAppointmentsViewModel extends ChangeNotifier { } //TODO: Make the API dynamic with parameters for ProjectID, isNearest, languageID, doctorId, doctorName - Future getDoctorsList( - {int projectID = 0, bool isNearest = false, int doctorId = 0, String doctorName = "", isContinueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future getDoctorsList({int projectID = 0, bool isNearest = false, int doctorId = 0, String doctorName = "", 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); + final result = await bookAppointmentsRepo.getDoctorsList(selectedClinic.clinicID ?? 0, projectID, isNearest, doctorId, doctorName, isContinueDentalPlan: isContinueDentalPlan); result.fold( (failure) async { @@ -901,4 +907,34 @@ class BookAppointmentsViewModel extends ChangeNotifier { filteredDoctorList = getDoctorListAsPerSelection(); notifyListeners(); } + + Future getPatientDentalEstimation({required int projectID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + patientDentalPlanEstimationList.clear(); + totalTimeNeededForDentalProcedure = 0; + isContinueDentalPlan = false; + notifyListeners(); + + final result = await bookAppointmentsRepo.getPatientDentalEstimation(projectID: projectID); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + patientDentalPlanEstimationList = apiResponse.data!; + + patientDentalPlanEstimationList.forEach((v) { + totalTimeNeededForDentalProcedure += (v.neededTime ?? 0); + }); + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } } diff --git a/lib/features/book_appointments/models/resp_models/get_patient_dental_plan_response_model.dart b/lib/features/book_appointments/models/resp_models/get_patient_dental_plan_response_model.dart new file mode 100644 index 0000000..6b90199 --- /dev/null +++ b/lib/features/book_appointments/models/resp_models/get_patient_dental_plan_response_model.dart @@ -0,0 +1,40 @@ +class PatientDentalPlanEstimationResponseModel { + dynamic setupID; + dynamic estimationNo; + int? projectID; + String? procedureId; + int? patientID; + int? sequenceNo; + int? neededTime; + String? procedureName; + String? procedureNameN; + + PatientDentalPlanEstimationResponseModel( + {this.setupID, this.estimationNo, this.projectID, this.procedureId, this.patientID, this.sequenceNo, this.neededTime, this.procedureName, this.procedureNameN}); + + PatientDentalPlanEstimationResponseModel.fromJson(Map json) { + setupID = json['SetupID']; + estimationNo = json['EstimationNo']; + projectID = json['ProjectID']; + procedureId = json['ProcedureId']; + patientID = json['PatientID']; + sequenceNo = json['sequenceNo']; + neededTime = json['NeededTime']; + procedureName = json['ProcedureName']; + procedureNameN = json['ProcedureNameN']; + } + + Map toJson() { + final Map data = new Map(); + data['SetupID'] = this.setupID; + data['EstimationNo'] = this.estimationNo; + data['ProjectID'] = this.projectID; + data['ProcedureId'] = this.procedureId; + data['PatientID'] = this.patientID; + data['sequenceNo'] = this.sequenceNo; + data['NeededTime'] = this.neededTime; + data['ProcedureName'] = this.procedureName; + data['ProcedureNameN'] = this.procedureNameN; + return data; + } +} diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index e1bbf1f..18e6b4b 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -23,8 +23,11 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/select_livecar import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/clinic_card.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; @@ -216,16 +219,124 @@ class _SelectClinicPageState extends State { } //17 and 235 - void handleDoctorScreen(GetClinicsListResponseModel clinic) { + void handleDoctorScreen(GetClinicsListResponseModel clinic) async { if (widget.isFromRegionFlow) { - Navigator.of(context).push( - CustomPageRoute( - page: SelectDoctorPage(), - ), - ); + //Dental Clinic Flow + if (clinic.clinicID == 17) { + LoaderBottomSheet.showLoader(loadingText: "Checking for an existing dental plan, Please wait...".needTranslation); + await bookAppointmentsViewModel.getPatientDentalEstimation(projectID: int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")).then((value) { + LoaderBottomSheet.hideLoader(); + if (bookAppointmentsViewModel.patientDentalPlanEstimationList.isNotEmpty) { + showCommonBottomSheetWithoutHeight( + // title: LocaleKeys.notice.tr(context: context), + title: "Dental treatment plan".needTranslation, + context, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "You have an existing treatment plan: ".needTranslation.toText14(weight: FontWeight.w500), + SizedBox(height: 8.h), + Container( + width: double.infinity, + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: bookAppointmentsViewModel.patientDentalPlanEstimationList.length, + separatorBuilder: (_, __) { + return Column( + children: [ + SizedBox(height: 8.h), + Divider(height: 1, color: AppColors.greyColor), + SizedBox(height: 8.h), + ], + ); + }, + itemBuilder: (context, index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + bookAppointmentsViewModel.patientDentalPlanEstimationList[index].procedureName!.toText12(isBold: true), + AppCustomChipWidget(icon: AppAssets.appointment_time_icon, labelText: "${bookAppointmentsViewModel.totalTimeNeededForDentalProcedure} Mins".needTranslation), + ], + ); + }, + ), + SizedBox( + height: 16.h, + ), + Divider(height: 1, color: AppColors.greyColor), + SizedBox( + height: 8.h, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Total time required".needTranslation.toText14(isBold: true), + AppCustomChipWidget(icon: AppAssets.appointment_time_icon, labelText: "30 Mins".needTranslation), + ], + ) + ], + ), + ), + SizedBox(height: 16.h), + "Would you like to continue it?".needTranslation.toText14(weight: FontWeight.w500), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.cancel.tr(), + onPressed: () { + Navigator.of(context).pop(); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + icon: AppAssets.cancel, + iconColor: AppColors.whiteColor, + ), + ), + SizedBox(width: 8.h), + Expanded( + child: CustomButton( + text: LocaleKeys.confirm.tr(), + onPressed: () async {}, + backgroundColor: AppColors.bgGreenColor, + borderColor: AppColors.bgGreenColor, + textColor: Colors.white, + icon: AppAssets.confirm, + ), + ), + ], + ) + ], + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else { + // Navigate to Chief Complaint Screen + } + }); + } else { + Navigator.of(context).push( + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + } } else { var bottomSheetType = RegionBottomSheetType.FOR_CLINIIC; - print("the cliininc id is ${clinic.clinicID}"); if (clinic.clinicID == 17 || clinic.clinicID == 235) { bottomSheetType = RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER; } diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 5975631..8d24b43 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -89,11 +89,7 @@ class _SelectDoctorPageState extends State { bookAppointmentsViewModel.filterClinics(""); textFocusNode.unfocus(); }, - child: Utils.buildSvgWithAssets( - icon: AppAssets.close_bottom_sheet_icon, - width: 20.h, - height: 20.h, - fit: BoxFit.scaleDown), + child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), ) : null, onChange: (value) { @@ -111,8 +107,11 @@ class _SelectDoctorPageState extends State { padding: EdgeInsets.only(top: 24.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemCount: - bookAppointmentsVM.isDoctorsListLoading ? 5 : (bookAppointmentsVM.isLiveCareSchedule ? bookAppointmentsVM.liveCareDoctorsList.length : bookAppointmentsVM.doctorsList.length), + itemCount: bookAppointmentsVM.isDoctorsListLoading + ? 5 + : (bookAppointmentsVM.isLiveCareSchedule + ? (bookAppointmentsVM.liveCareDoctorsList.isNotEmpty ? bookAppointmentsVM.liveCareDoctorsList.length : 1) + : (bookAppointmentsVM.doctorsList.isNotEmpty ? bookAppointmentsVM.doctorsList.length : 1)), itemBuilder: (context, index) { return bookAppointmentsVM.isDoctorsListLoading ? DoctorCard( @@ -120,47 +119,49 @@ class _SelectDoctorPageState extends State { isLoading: true, bookAppointmentsViewModel: bookAppointmentsViewModel, ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: DoctorCard( - doctorsListResponseModel: bookAppointmentsVM.isLiveCareSchedule ? bookAppointmentsVM.liveCareDoctorsList[index] : bookAppointmentsVM.doctorsList[index], - isLoading: false, - bookAppointmentsViewModel: bookAppointmentsViewModel, - ).onPress(() async { - bookAppointmentsVM - .setSelectedDoctor(bookAppointmentsVM.isLiveCareSchedule ? bookAppointmentsVM.liveCareDoctorsList[index] : bookAppointmentsVM.doctorsList[index]); - // bookAppointmentsVM.setSelectedDoctor(DoctorsListResponseModel()); - LoaderBottomSheet.showLoader(); - await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }), + : checkIsDoctorsListEmpty() + ? Utils.getNoDataWidget(context, noDataText: "No Doctor found for selected criteria...".needTranslation) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: DoctorCard( + doctorsListResponseModel: bookAppointmentsVM.isLiveCareSchedule ? bookAppointmentsVM.liveCareDoctorsList[index] : bookAppointmentsVM.doctorsList[index], + isLoading: false, + bookAppointmentsViewModel: bookAppointmentsViewModel, + ).onPress(() async { + bookAppointmentsVM + .setSelectedDoctor(bookAppointmentsVM.isLiveCareSchedule ? bookAppointmentsVM.liveCareDoctorsList[index] : bookAppointmentsVM.doctorsList[index]); + // bookAppointmentsVM.setSelectedDoctor(DoctorsListResponseModel()); + LoaderBottomSheet.showLoader(); + await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }), + ), + ), ), - ), - ), - ); + ); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ), @@ -173,4 +174,12 @@ class _SelectDoctorPageState extends State { ), ); } + + bool checkIsDoctorsListEmpty() { + if (bookAppointmentsViewModel.isLiveCareSchedule) { + return bookAppointmentsViewModel.liveCareDoctorsList.isEmpty; + } else { + return bookAppointmentsViewModel.doctorsList.isEmpty; + } + } } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index f28beed..db8a068 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -135,8 +135,8 @@ class _LandingPageState extends State { fontSize: 14, fontWeight: FontWeight.w500, borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40, + padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), + height: 40.h, ), Row( mainAxisSize: MainAxisSize.min, From 6cf08c04d29106410510435a1cfaf827b2f33040 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Mon, 6 Oct 2025 09:57:12 +0300 Subject: [PATCH 32/43] appointment filters added. --- assets/langs/ar-SA.json | 3 +- assets/langs/en-US.json | 4 +- .../models/appointemnet_filters.dart | 17 + .../my_appointments_view_model.dart | 157 +++++++- lib/generated/locale_keys.g.dart | 1 + lib/main.dart | 6 +- .../appointments/my_appointments_page.dart | 350 +++++++----------- .../widgets/AppointmentFilter.dart | 48 +++ lib/presentation/home/landing_page.dart | 1 - lib/presentation/lab/lab_orders_page.dart | 6 +- .../lab/lab_results/lab_result_details.dart | 12 +- lib/theme/colors.dart | 1 + lib/widgets/chip/app_custom_chip_widget.dart | 4 +- .../date_range_calender.dart} | 40 +- .../viewmodel/date_range_view_model.dart} | 12 +- 15 files changed, 420 insertions(+), 242 deletions(-) create mode 100644 lib/features/my_appointments/models/appointemnet_filters.dart create mode 100644 lib/presentation/appointments/widgets/AppointmentFilter.dart rename lib/{presentation/lab/lab_results/lab_result_calender.dart => widgets/date_range_selector/date_range_calender.dart} (90%) rename lib/{features/lab/lab_range_view_model.dart => widgets/date_range_selector/viewmodel/date_range_view_model.dart} (94%) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index b08e783..cc114c8 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -864,5 +864,6 @@ "regionAndLocation": "المنطقة والمواقع", "clearAllFilters": "مسح جميع الفلاتر", "filters": "فلاتر", - "searchClinic": "بحث عن عيادة" + "searchClinic": "بحث عن عيادة", + "walkin": "زيارة بدون موعد" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index adab1d5..d0069a3 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -860,5 +860,7 @@ "regionAndLocation": "Region And Locations", "clearAllFilters": "Clear all filters", "filters": "Filters", - "searchClinic": "Search Clinic" + "searchClinic": "Search Clinic", + "walkin": "Walk In" + } \ No newline at end of file diff --git a/lib/features/my_appointments/models/appointemnet_filters.dart b/lib/features/my_appointments/models/appointemnet_filters.dart new file mode 100644 index 0000000..e157af4 --- /dev/null +++ b/lib/features/my_appointments/models/appointemnet_filters.dart @@ -0,0 +1,17 @@ +import 'package:hmg_patient_app_new/core/app_assets.dart'; + +enum AppointmentListingFilters{ + WALKIN("walkin", AppAssets.walkin_appointment_icon), + BOOKED("booked", AppAssets.calendar), + CONFIRMED("confirmed", AppAssets.calendar), + ARRIVED("arrived", AppAssets.calendar), + LIVECARE("livecare", AppAssets.small_livecare_icon), + DATESELECTION("",AppAssets.calendar, trailingIcon: AppAssets.arrow_down); + + final String labelText; + final String leadingIcon; + final String trailingIcon; + + const AppointmentListingFilters(this.labelText, this.leadingIcon, + {this.trailingIcon = ""}); +} \ 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 c553f0e..99fbf60 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -1,14 +1,18 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/appointemnet_filters.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/models/resp_models/patient_appointment_share_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import '../../core/utils/doctor_response_mapper.dart' show DoctorMapper; class MyAppointmentsViewModel extends ChangeNotifier { int selectedTabIndex = 0; + int previouslySelectedTab = -1; MyAppointmentsRepo myAppointmentsRepo; ErrorHandlerService errorHandlerService; @@ -21,7 +25,14 @@ class MyAppointmentsViewModel extends ChangeNotifier { bool isAppointmentDataToBeLoaded = true; + List availableFilters = []; + List? selectedFilter = []; + bool isDateFilterSelected = false; + DateTime? start =null; + DateTime? end =null; + List patientAppointmentsHistoryList = []; + List filteredAppointmentList = []; List patientUpcomingAppointmentsHistoryList = []; List patientArrivedAppointmentsHistoryList = []; @@ -35,6 +46,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { MyAppointmentsViewModel({required this.myAppointmentsRepo, required this.errorHandlerService, required this.appState}); void onTabChange(int index) { + previouslySelectedTab = selectedTabIndex; selectedTabIndex = index; notifyListeners(); } @@ -132,10 +144,43 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsHistoryList.addAll(patientUpcomingAppointmentsHistoryList); patientAppointmentsHistoryList.addAll(patientArrivedAppointmentsHistoryList); - + filteredAppointmentList.addAll(patientAppointmentsHistoryList); print('Upcoming Appointments: ${patientUpcomingAppointmentsHistoryList.length}'); print('Arrived Appointments: ${patientArrivedAppointmentsHistoryList.length}'); print('All Appointments: ${patientAppointmentsHistoryList.length}'); + getFiltersForSelectedAppointmentList(filteredAppointmentList); + } + + void getFiltersForSelectedAppointmentList( + List filteredAppointmentList) { + availableFilters.clear(); + if (filteredAppointmentList.isEmpty == true) return; + availableFilters.add(AppointmentListingFilters.DATESELECTION); + if (filteredAppointmentList + .any((element) => element.isLiveCareAppointment == true)) { + availableFilters.add(AppointmentListingFilters.LIVECARE); + } + + if (filteredAppointmentList + .any((element) => element.isLiveCareAppointment == false)) { + availableFilters.add(AppointmentListingFilters.WALKIN); + } + + if (filteredAppointmentList + .any((element) => AppointmentType.isArrived(element) == true)) { + availableFilters.add(AppointmentListingFilters.ARRIVED); + } + + if (filteredAppointmentList + .any((element) => AppointmentType.isBooked(element) == true)) { + availableFilters.add(AppointmentListingFilters.BOOKED); + } + + if (filteredAppointmentList + .any((element) => AppointmentType.isConfirmed(element) == true)) { + availableFilters.add(AppointmentListingFilters.CONFIRMED); + } + notifyListeners(); } Future getPatientShareAppointment(int projectID, int clinicID, String appointmentNo, bool isLiveCareAppointment, {Function(dynamic)? onSuccess, Function(String)? onError}) async { @@ -337,4 +382,114 @@ class MyAppointmentsViewModel extends ChangeNotifier { }, ); } + + void updateListWRTTab(int index) { + isDateFilterSelected = false; + selectedFilter = []; + // if(previouslySelectedTab == selectedTabIndex ) return; + switch (index) { + case 0: + filteredAppointmentList.clear(); + filteredAppointmentList.addAll(patientAppointmentsHistoryList); + break; + case 1: + filteredAppointmentList.clear(); + filteredAppointmentList.addAll(patientUpcomingAppointmentsHistoryList); + break; + case 2: + filteredAppointmentList.clear(); + filteredAppointmentList.addAll(patientArrivedAppointmentsHistoryList); + break; + } + getFiltersForSelectedAppointmentList(filteredAppointmentList); + notifyListeners(); + } + + void setSelectedFilter(AppointmentListingFilters availableFilter) { + if (selectedFilter?.contains(availableFilter) == true) { + selectedFilter?.remove(availableFilter); + notifyListeners(); + + return; + } + selectedFilter?.add(availableFilter) ; + notifyListeners(); + } + + void getSelectedDateRange(DateTime? start, DateTime? end) { + this.start = start; + this.end = end; + isDateFilterSelected = true; + List sourceList = []; + if (selectedTabIndex == 0) { + sourceList = patientAppointmentsHistoryList; + } else if (selectedTabIndex == 1) { + sourceList = patientUpcomingAppointmentsHistoryList; + } else if (selectedTabIndex == 2) { + sourceList = patientArrivedAppointmentsHistoryList; + } + // if (isDateFilterSelected) sourceList = filteredAppointmentList; + if (start == null && end == null) { + isDateFilterSelected = false; + filteredAppointmentList.clear(); + sourceList.forEach((element) { + if (isUnderFilter(element)) { + filteredAppointmentList.add(element); + } + }); + filteredAppointmentList.addAll(sourceList); + } else { + filteredAppointmentList.clear(); + sourceList.forEach((element) { + try { + var dateTime = DateUtil.convertStringToDate(element.appointmentDate); + if (start != null && end == null) { + if (dateTime.isAtSameMomentAs(start)) { + if (isUnderFilter(element)) { + filteredAppointmentList.add(element); + } + } + } else if (start != null && end != null) { + if ((dateTime.isAfter(start)) && (dateTime.isBefore(end))) { + if (isUnderFilter(element)) { + filteredAppointmentList.add(element); + } + } + } + } catch (e) {} + }); + } + notifyListeners(); + } + + void filterTheListAsPerSelection() { + getSelectedDateRange(start, end); + } + + bool isUnderFilter(PatientAppointmentHistoryResponseModel element) { + bool isUnderTheFilter = false; + if (selectedFilter == null || selectedFilter!.isEmpty) return true; + int count = 0; + for (var filter in selectedFilter ?? []) { + switch (filter) { + case AppointmentListingFilters.WALKIN: + if (element.isLiveCareAppointment == false) return true; + case AppointmentListingFilters.BOOKED: + if (AppointmentType.isBooked(element))return true; + + case AppointmentListingFilters.CONFIRMED: + if (AppointmentType.isConfirmed(element))return true; + + case AppointmentListingFilters.ARRIVED: + if (AppointmentType.isArrived(element))return true; + + case AppointmentListingFilters.LIVECARE: + if (element.isLiveCareAppointment == true) return true; + + case AppointmentListingFilters.DATESELECTION: + + } + } + return false; + } } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 9447bd1..724ae2f 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -863,5 +863,6 @@ abstract class LocaleKeys { static const clearAllFilters = 'clearAllFilters'; static const filters = 'filters'; static const searchClinic = 'searchClinic'; + static const walkin = 'walkin'; } diff --git a/lib/main.dart b/lib/main.dart index cdfcc13..ac11f8f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +15,6 @@ import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_mode 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/lab/history/lab_history_viewmodel.dart'; -import 'package:hmg_patient_app_new/features/lab/lab_range_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/lab_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_via_region_viewmodel.dart'; @@ -28,6 +27,7 @@ import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/app_theme.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart' show DateRangeSelectorRangeViewModel; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; @@ -163,8 +163,8 @@ void main() async { navigationService: getIt(), appState: getIt())), ChangeNotifierProvider( create: (_) => LabHistoryViewModel()), - ChangeNotifierProvider( - create: (_) => LabRangeViewModel()) , + ChangeNotifierProvider( + create: (_) => DateRangeSelectorRangeViewModel()) , ChangeNotifierProvider( create: (_) => DoctorFilterViewModel()) ], child: MyApp()), diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index 2518c88..c939234 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -8,19 +8,26 @@ 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/my_appointments/models/appointemnet_filters.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/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/AppointmentFilter.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/date_range_calender.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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/custom_tab_bar.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; import 'package:provider/provider.dart'; +import '../../widgets/common_bottom_sheet.dart' + show showCommonBottomSheetWithoutHeight; + class MyAppointmentsPage extends StatefulWidget { const MyAppointmentsPage({super.key}); @@ -61,6 +68,8 @@ class _MyAppointmentsPageState extends State { ], onTabChange: (index) { myAppointmentsViewModel.onTabChange(index); + myAppointmentsViewModel.updateListWRTTab(index); + context.read().flush(); }, ).paddingSymmetrical(24.h, 0.h), Consumer(builder: (context, myAppointmentsVM, child) { @@ -74,218 +83,147 @@ class _MyAppointmentsPageState extends State { } Widget getSelectedTabData(int index, MyAppointmentsViewModel myAppointmentsVM) { - switch (index) { - case 0: - //All Appointments Tab Data - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Expandable list - ListView.separated( - padding: EdgeInsets.only(top: 24.h), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: myAppointmentsVM.isMyAppointmentsLoading - ? 5 - : myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty - ? myAppointmentsVM.patientAppointmentsHistoryList.length - : 1, - itemBuilder: (context, index) { - return myAppointmentsVM.isMyAppointmentsLoading - ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(), - myAppointmentsViewModel: myAppointmentsViewModel, - isLoading: true, - isFromHomePage: false, - ), - ).paddingSymmetrical(24.h, 0.h) - : myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty - ? AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - isLoading: false, - isFromHomePage: false, - ), - ).paddingSymmetrical(24.h, 0.h), + return getAppointList( + myAppointmentsVM, myAppointmentsVM.filteredAppointmentList); + } + + Widget getAppointList(MyAppointmentsViewModel myAppointmentsVM, + List filteredAppointmentList) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Visibility( + visible: myAppointmentsVM.availableFilters.isNotEmpty, + child: getAppointmentFilters(myAppointmentsVM)), + ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: myAppointmentsVM.isMyAppointmentsLoading + ? 5 + : filteredAppointmentList.isNotEmpty + ? filteredAppointmentList.length + : 1, + itemBuilder: (context, index) { + return myAppointmentsVM.isMyAppointmentsLoading + ? Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: + PatientAppointmentHistoryResponseModel(), + myAppointmentsViewModel: myAppointmentsViewModel, + isLoading: true, + isFromHomePage: false, + ), + ).paddingSymmetrical(24.h, 0.h) + : filteredAppointmentList.isNotEmpty + ? AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: + filteredAppointmentList[index], + myAppointmentsViewModel: + myAppointmentsViewModel, + isLoading: false, + isFromHomePage: false, ), - ), - ) - : Utils.getNoDataWidget( - context, - noDataText: "You don't have any appointments yet.".needTranslation, - callToActionButton: CustomButton( - text: LocaleKeys.bookAppo.tr(context: context), - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: BookAppointmentPage(), - ), - ); - }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), - fontSize: 14, - fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40, - icon: AppAssets.add_icon, - iconColor: AppColors.primaryRedColor, - ).paddingSymmetrical(48.h, 0.h), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), - ], - ); - case 1: - //Upcoming Appointments Tab Data - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Expandable list - ListView.separated( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: myAppointmentsVM.isMyAppointmentsLoading - ? 5 - : myAppointmentsVM.patientUpcomingAppointmentsHistoryList.isNotEmpty - ? myAppointmentsVM.patientUpcomingAppointmentsHistoryList.length - : 1, - itemBuilder: (context, index) { - return myAppointmentsVM.isMyAppointmentsLoading - ? const MoviesShimmerWidget().paddingSymmetrical(24.h, 0.h) - : myAppointmentsVM.patientUpcomingAppointmentsHistoryList.isNotEmpty - ? AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientUpcomingAppointmentsHistoryList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - ), - ).paddingSymmetrical(24.h, 0.h), + ).paddingSymmetrical(24.h, 0.h), + ), + ), + ) + : Utils.getNoDataWidget( + context, + noDataText: "You don't have any appointments yet." + .needTranslation, + callToActionButton: CustomButton( + text: LocaleKeys.bookAppo.tr(context: context), + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: BookAppointmentPage(), ), - ), - ) - : Utils.getNoDataWidget( - context, - noDataText: "You don't have any appointments yet.".needTranslation, - callToActionButton: CustomButton( - text: LocaleKeys.bookAppo.tr(context: context), - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: BookAppointmentPage(), - ), - ); - }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), - fontSize: 14, - fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40, - icon: AppAssets.add_icon, - iconColor: AppColors.primaryRedColor, - ).paddingSymmetrical(48.h, 0.h), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), - ], - ); - case 2: - //Completed Appointments Tab Data - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + ); + }, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40, + icon: AppAssets.add_icon, + iconColor: AppColors.primaryRedColor, + ).paddingSymmetrical(48.h, 0.h), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => + SizedBox(height: 16.h), + ), + ], + ); + } + + Widget getAppointmentFilters(MyAppointmentsViewModel myAppointmentsVM) { + return SizedBox( + height: 56.h, + child: Row( children: [ - // Expandable list - ListView.separated( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: myAppointmentsVM.isMyAppointmentsLoading - ? 5 - : myAppointmentsVM.patientArrivedAppointmentsHistoryList.isNotEmpty - ? myAppointmentsVM.patientArrivedAppointmentsHistoryList.length - : 1, - itemBuilder: (context, index) { - return myAppointmentsVM.isMyAppointmentsLoading - ? const MoviesShimmerWidget().paddingSymmetrical(24.h, 0.h) - : myAppointmentsVM.patientArrivedAppointmentsHistoryList.isNotEmpty - ? AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientArrivedAppointmentsHistoryList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - ), - ).paddingSymmetrical(24.h, 0.h), + Expanded( + child: ListView.separated( + separatorBuilder: (_, index) => SizedBox( + width: 8.h, + ), + scrollDirection: Axis.horizontal, + itemCount: myAppointmentsVM.availableFilters.length, + itemBuilder: (_, index) => AppointmentFilters( + selectedFilter: myAppointmentsVM.selectedFilter, + item: myAppointmentsVM.availableFilters[index], + onClicked: () { + if (myAppointmentsVM.availableFilters[index] == + AppointmentListingFilters.DATESELECTION) { + showCommonBottomSheetWithoutHeight( + title: "Set The Date Range".needTranslation, + context, + child: DateRangeSelector( + onRangeSelected: (start, end) { + // if (start != null) { + myAppointmentsVM.getSelectedDateRange( + start, end); + // } + }, ), - ), - ) - : Utils.getNoDataWidget( - context, - noDataText: "You don't have any appointments yet.".needTranslation, - callToActionButton: CustomButton( - text: LocaleKeys.bookAppo.tr(context: context), - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: BookAppointmentPage(), - ), - ); - }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), - fontSize: 14, - fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40, - icon: AppAssets.add_icon, - iconColor: AppColors.primaryRedColor, - ).paddingSymmetrical(48.h, 0.h), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + isFullScreen: false, + isCloseButtonVisible: true, + callBackFunc: () {}, + ); + } else { + myAppointmentsVM.setSelectedFilter( + myAppointmentsVM.availableFilters[index]); + myAppointmentsVM.filterTheListAsPerSelection(); + } + }, + )), ), ], - ); - default: - return Container(); - } + )).paddingOnly(top: 24.h, left: 24.h, right: 24.h); } } diff --git a/lib/presentation/appointments/widgets/AppointmentFilter.dart b/lib/presentation/appointments/widgets/AppointmentFilter.dart new file mode 100644 index 0000000..fd2a9fe --- /dev/null +++ b/lib/presentation/appointments/widgets/AppointmentFilter.dart @@ -0,0 +1,48 @@ +import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; +import 'package:flutter/material.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/my_appointments/models/appointemnet_filters.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:smooth_corner/smooth_corner.dart'; + +class AppointmentFilters extends StatelessWidget { + final AppointmentListingFilters item; + final List? selectedFilter; + final VoidCallback onClicked; + + const AppointmentFilters( + {super.key, + required this.item, + required this.onClicked, + required this.selectedFilter}); + + @override + Widget build(BuildContext context) { + return AppCustomChipWidget( + backgroundColor: selectedFilter?.contains(item) == true?AppColors.chipSecondaryLightRedColor:AppColors.whiteColor, + icon: item.leadingIcon, + textColor: selectedFilter?.contains(item) == true + ? AppColors.chipPrimaryRedBorderColor: AppColors.blackColor, + labelText: item.labelText.isNotEmpty?item.labelText.tr():"", + iconHasColor: true, + iconColor: selectedFilter?.contains(item) == true + ? AppColors.chipPrimaryRedBorderColor:AppColors.blackColor, + iconSize: 16, + deleteIcon: item.trailingIcon, + labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: 0.h), + padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 8.h), + deleteIconSize: Size(18.h, 18.h), + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.circular(10 ), + smoothness: 10, + side: BorderSide( + color: selectedFilter?.contains(item) == true + ? AppColors.chipPrimaryRedBorderColor + : AppColors.borderGrayColor, + width: 1), + )).onPress(onClicked); + } +} diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 9443ee2..abdaac5 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -32,7 +32,6 @@ import 'package:hmg_patient_app_new/presentation/home/widgets/habib_wallet_card. import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; -import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_calender.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/services/cache_service.dart'; diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 6fd8234..3f6ab99 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -9,7 +9,6 @@ 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/lab/lab_range_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; @@ -18,6 +17,7 @@ import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; @@ -32,7 +32,7 @@ class LabOrdersPage extends StatefulWidget { class _LabOrdersPageState extends State { late LabViewModel labProvider; - late LabRangeViewModel rangeViewModel; + late DateRangeSelectorRangeViewModel rangeViewModel; List?> labSuggestions = []; int? expandedIndex; @@ -50,7 +50,7 @@ class _LabOrdersPageState extends State { @override Widget build(BuildContext context) { labProvider = Provider.of(context); - rangeViewModel = Provider.of(context); + rangeViewModel = Provider.of(context); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index 3e9558c..5030d37 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -6,10 +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/features/lab/lab_range_view_model.dart' show LabRangeViewModel; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart'; -import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_calender.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/date_range_calender.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_list_item.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; @@ -133,7 +133,7 @@ class LabResultDetails extends StatelessWidget { ], )); - Widget LabGraph(BuildContext context) => Consumer( + Widget LabGraph(BuildContext context) => Consumer( builder: (_, model, ___) => Consumer( builder: (_, labmodel, ___) => Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -179,7 +179,7 @@ class LabResultDetails extends StatelessWidget { showCommonBottomSheetWithoutHeight( title: "Set The Date Range".needTranslation, context, - child: LabResultCalender( + child: DateRangeSelector( onRangeSelected: (start, end) { // if (start != null) { @@ -227,7 +227,7 @@ class LabResultDetails extends StatelessWidget { ); } - Widget historyBody(LabRangeViewModel model, LabViewModel labmodel) { + Widget historyBody(DateRangeSelectorRangeViewModel model, LabViewModel labmodel) { if(model.isGraphVisible){ var graphColor = labmodel.getColor(recentLabResult.calculatedResultFlag??"N"); return CustomGraph( @@ -280,7 +280,7 @@ class LabResultDetails extends StatelessWidget { } } - Widget labHistoryList(LabRangeViewModel model, LabViewModel labmodel) { + Widget labHistoryList(DateRangeSelectorRangeViewModel model, LabViewModel labmodel) { return SizedBox( height: 180.h, child: ListView.separated( diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 291f81d..55f2b2a 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -29,6 +29,7 @@ class AppColors { static const Color bgRedLightColor = Color(0xFFFEE9EA); static const Color bgGreenColor = Color(0xFF18C273); static const Color textColor = Color(0xFF2E3039); + static const Color borderGrayColor = Color(0x332E3039); static const Color textColorLight = Color(0xFF5E5E5E); static const Color borderOnlyColor = Color(0xFF2E3039); static const Color chipBorderColorOpacity20 = Color(0x332E3039); diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index d11b8f4..751b01c 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -24,6 +24,7 @@ class AppCustomChipWidget extends StatelessWidget { this.deleteIconColor = AppColors.textColor, this.deleteIconHasColor = false, this.padding = EdgeInsets.zero, + this.labelPadding , }); final String? labelText; @@ -40,6 +41,7 @@ class AppCustomChipWidget extends StatelessWidget { final bool deleteIconHasColor; final OutlinedBorder? shape; final EdgeInsets? padding; + final EdgeInsetsDirectional? labelPadding; @override Widget build(BuildContext context) { @@ -62,7 +64,7 @@ class AppCustomChipWidget extends StatelessWidget { // padding: EdgeInsets.all(0.0), padding: padding, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - labelPadding: EdgeInsetsDirectional.only(start: 0.h, end: deleteIcon?.isNotEmpty == true ? 2.h : 8.h), + labelPadding: labelPadding??EdgeInsetsDirectional.only(start: 0.h, end: deleteIcon?.isNotEmpty == true ? 2.h : 8.h), backgroundColor: backgroundColor, shape: shape ?? SmoothRectangleBorder( diff --git a/lib/presentation/lab/lab_results/lab_result_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart similarity index 90% rename from lib/presentation/lab/lab_results/lab_result_calender.dart rename to lib/widgets/date_range_selector/date_range_calender.dart index b118293..1a551f3 100644 --- a/lib/presentation/lab/lab_results/lab_result_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -7,32 +7,32 @@ import 'package:hmg_patient_app_new/core/app_export.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/lab/lab_range_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/Range.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/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart' show DateRangeSelectorRangeViewModel; import 'package:provider/provider.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; typedef OnRangeSelected = void Function(DateTime? start, DateTime? end); -class LabResultCalender extends StatefulWidget { +class DateRangeSelector extends StatefulWidget { final OnRangeSelected onRangeSelected; - const LabResultCalender({super.key, required this.onRangeSelected}); + const DateRangeSelector({super.key, required this.onRangeSelected}); @override - State createState() => _LabResultCalenderState(); + State createState() => _DateRangeSelectorState(); } -class _LabResultCalenderState extends State { +class _DateRangeSelectorState extends State { late DateRangePickerController _calendarController; DateTime? start; DateTime? end; - late LabRangeViewModel model; + late DateRangeSelectorRangeViewModel model; @override void initState() { _calendarController = DateRangePickerController(); @@ -44,13 +44,17 @@ class _LabResultCalenderState extends State { @override Widget build(BuildContext context) { - model = Provider.of(context); + model = Provider.of(context); + + + _calendarController.selectedRange = PickerDateRange(model.fromDate,model.toDate); + return Padding( padding: EdgeInsets.symmetric(horizontal: 0.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Consumer( + Consumer( builder: (_, model, __) => selectionChip(model), ).paddingOnly(bottom: 16.h), Container( @@ -135,13 +139,19 @@ class _LabResultCalenderState extends State { ), onSelectionChanged: (DateRangePickerSelectionChangedArgs args) { + print("the value is ${args.value}"); if (args.value is PickerDateRange) { final PickerDateRange range = args.value; start = range.startDate; end = range.endDate; model.fromDate = start; model.toDate = end; - model.resetCurrentlySelectedRange(); + if(end == null) { + scheduleMicrotask((){ + model.resetCurrentlySelectedRange(); + }); + + } } }, ), @@ -151,7 +161,7 @@ class _LabResultCalenderState extends State { ), Row( children: [ - Consumer( + Consumer( builder: (_, model, __) => Visibility( visible: (model.fromDate != null || model.toDate != null), child: Expanded( @@ -164,6 +174,8 @@ class _LabResultCalenderState extends State { _calendarController.selectedRange = null; _calendarController.selectedDate = null; model.flush(); + Navigator.of(context).pop(); + widget.onRangeSelected(null, null); }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, @@ -203,7 +215,7 @@ class _LabResultCalenderState extends State { } fromDateComponent() { - return Consumer( + return Consumer( builder: (_, model, __) { return displayDate("Start Date".needTranslation, model.getDateString(model.fromDate), model.fromDate == null); @@ -212,7 +224,7 @@ class _LabResultCalenderState extends State { } toDateComponent() { - return Consumer( + return Consumer( builder: (_, model, __) { return displayDate("End Date".needTranslation, model.getDateString(model.toDate), model.toDate == null); @@ -254,7 +266,7 @@ class _LabResultCalenderState extends State { ), ); - selectionChip(LabRangeViewModel model) { + selectionChip(DateRangeSelectorRangeViewModel model) { return Row( spacing: 8.h, children: [ @@ -311,6 +323,7 @@ class _LabResultCalenderState extends State { _calendarController.selectedRange = null; model.currentlySelectedRange = Range.LAST_6MONTH; model.calculateDatesFromRange(); + }), AppCustomChipWidget( labelText: "Year ${model.getCurrentYear}", @@ -329,6 +342,7 @@ class _LabResultCalenderState extends State { _calendarController.selectedRange = null; model.currentlySelectedRange = Range.THIS_YEAR; model.calculateDatesFromRange(); + }), ], ); diff --git a/lib/features/lab/lab_range_view_model.dart b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart similarity index 94% rename from lib/features/lab/lab_range_view_model.dart rename to lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart index aa91964..42f222b 100644 --- a/lib/features/lab/lab_range_view_model.dart +++ b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart @@ -2,7 +2,7 @@ import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/features/lab/models/Range.dart'; -class LabRangeViewModel extends ChangeNotifier { +class DateRangeSelectorRangeViewModel extends ChangeNotifier { List months = [ 'Jan', 'Feb', @@ -24,8 +24,6 @@ class LabRangeViewModel extends ChangeNotifier { set currentlySelectedRange(Range? value) { _currentlySelectedRange = value; - notifyListeners(); - } DateTime? _toDate; @@ -34,7 +32,7 @@ class LabRangeViewModel extends ChangeNotifier { set toDate(DateTime? value) { _toDate = value; - notifyListeners(); + } DateTime? _fromDate; @@ -43,11 +41,11 @@ class LabRangeViewModel extends ChangeNotifier { set fromDate(DateTime? value) { _fromDate = value; - notifyListeners(); + } - LabRangeViewModel(); + DateRangeSelectorRangeViewModel(); get getCurrentYear => DateTime.now().year; @@ -65,6 +63,7 @@ class LabRangeViewModel extends ChangeNotifier { _fromDate = DateTime(_toDate!.year, DateTime.january, 01); default: } + notifyListeners(); } getDateString(DateTime? date){ @@ -84,6 +83,7 @@ class LabRangeViewModel extends ChangeNotifier { resetCurrentlySelectedRange(){ currentlySelectedRange = null; + notifyListeners(); } alterGraphVisibility(){ From 2b21086cc17d210d91d4b6b22237d859ab0f8906 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 6 Oct 2025 10:19:53 +0300 Subject: [PATCH 33/43] Dental appointment booking flow implementation contd. --- lib/core/api/api_client.dart | 4 ++-- .../book_appointments_view_model.dart | 5 +++++ lib/features/lab/lab_view_model.dart | 20 ++++++++++++------- .../patient_lab_orders_response_model.dart | 9 +++++++-- .../book_appointment/select_clinic_page.dart | 10 +++++++++- lib/presentation/lab/lab_order_by_test.dart | 7 ++++--- lib/presentation/lab/lab_orders_page.dart | 2 +- .../LabResultList.dart | 4 ++-- .../lab_order_result_item.dart | 11 ++-------- .../lab/lab_results/lab_result_details.dart | 3 ++- 10 files changed, 47 insertions(+), 28 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 9140a13..cd7b673 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -173,8 +173,8 @@ class ApiClientImp implements ApiClient { body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 4767884; + body['TokenID'] = "@dm!n"; + body['PatientID'] = 4767884; // body['PatientTypeID'] = 1; // // body['PatientOutSA'] = 0; diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index f04394b..5e0d424 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -196,6 +196,11 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + setIsContinueDentalPlan(bool value) { + isContinueDentalPlan = value; + notifyListeners(); + } + void onTabChanged(int index) { selectedTabIndex = index; notifyListeners(); diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 1bbc27c..e7507b0 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -58,7 +58,7 @@ class LabViewModel extends ChangeNotifier { List get labSuggestions => _labSuggestionsList; - Set uniqueTests = {}; + Set uniqueTests = {}; double maxY = 0.0; double maxX = double.infinity; @@ -78,6 +78,11 @@ class LabViewModel extends ChangeNotifier { } Future getPatientLabOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { + patientLabOrders.clear(); + uniqueTests.clear(); + uniqueTests = {}; + notifyListeners(); + final result = await labRepo.getPatientLabOrders(); result.fold( @@ -132,11 +137,12 @@ class LabViewModel extends ChangeNotifier { } getUniqueTestDescription() { - - uniqueTests = { + uniqueTests = { for (var item in patientLabOrders) if (item.testDetails != null) ...?item.testDetails?.map((test) => TestDetails( + testDescriptionEn: test.testDescriptionEn.toString(), + testDescriptionAr: test.testDescriptionAr.toString(), description: test.description.toString(), testCode: test.testCode.toString(), testID: test.testID, @@ -170,8 +176,7 @@ class LabViewModel extends ChangeNotifier { ); } - Future getPatientLabResult( - PatientLabOrdersResponseModel laborder, String procedureName) async { + Future getPatientLabResult(PatientLabOrdersResponseModel laborder, String procedureName, String testDescription) async { LoaderBottomSheet.showLoader(); mainLabResults.clear(); filteredGraphValues.clear(); @@ -223,8 +228,9 @@ class LabViewModel extends ChangeNotifier { recentResult.verifiedOn = resultDate(DateUtil.convertStringToDate(recentResult.verifiedOnDateTime!)); // filteredGraphValues = [filteredGraphValues.first]; navigationService.push(MaterialPageRoute( - builder: (_) => - LabResultDetails(recentLabResult: recentResult))); + builder: (_) => LabResultDetails(recentLabResult: recentResult, testDescription: testDescription), + ), + ); notifyListeners(); } }, diff --git a/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart b/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart index aadfc76..4143498 100644 --- a/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart +++ b/lib/features/lab/models/resp_models/patient_lab_orders_response_model.dart @@ -224,21 +224,26 @@ class PatientLabOrdersResponseModel { class TestDetails { String? description; + String? testDescriptionEn; + String? testDescriptionAr; String? testCode; String? testID; String? createdOn; PatientLabOrdersResponseModel? model; - TestDetails({this.description, this.testCode, this.testID, this.createdOn, this.model}); + + TestDetails({this.description, this.testDescriptionEn, this.testDescriptionAr, this.testCode, this.testID, this.createdOn, this.model}); TestDetails.fromJson(Map json) { description = json['Description']; + testDescriptionEn = json['TestDescriptionEn'] ?? ""; + testDescriptionAr = json['TestDescriptionAr'] ?? ""; testCode = json['TestCode']; testID = json['TestID']; createdOn = json['CreatedOn']; } Map toJson() { - final Map data = new Map(); + final Map data = {}; data['Description'] = this.description; data['TestCode'] = this.testCode; data['TestID'] = this.testID; diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index 18e6b4b..3326ef5 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -296,6 +296,7 @@ class _SelectClinicPageState extends State { child: CustomButton( text: LocaleKeys.cancel.tr(), onPressed: () { + bookAppointmentsViewModel.setIsContinueDentalPlan(false); Navigator.of(context).pop(); }, backgroundColor: AppColors.primaryRedColor, @@ -309,7 +310,14 @@ class _SelectClinicPageState extends State { Expanded( child: CustomButton( text: LocaleKeys.confirm.tr(), - onPressed: () async {}, + onPressed: () async { + bookAppointmentsViewModel.setIsContinueDentalPlan(true); + Navigator.of(context).push( + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + }, backgroundColor: AppColors.bgGreenColor, borderColor: AppColors.bgGreenColor, textColor: Colors.white, diff --git a/lib/presentation/lab/lab_order_by_test.dart b/lib/presentation/lab/lab_order_by_test.dart index 4c0d2c3..2b791ae 100644 --- a/lib/presentation/lab/lab_order_by_test.dart +++ b/lib/presentation/lab/lab_order_by_test.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -19,8 +20,9 @@ class LabOrderByTest extends StatelessWidget { final TestDetails? tests; final bool isLoading; final bool isExpanded; + final AppState appState; - const LabOrderByTest({super.key, required this.onTap, this.tests, required this.index, this.isLoading = false, this.isExpanded = false}); + const LabOrderByTest({super.key, required this.onTap, required this.appState, this.tests, required this.index, this.isLoading = false, this.isExpanded = false}); @override build(BuildContext context) { @@ -37,8 +39,7 @@ class LabOrderByTest extends StatelessWidget { children: [ '${tests!.description}'.toText16(isBold: true), SizedBox(height: 4.h), - //TODO: Add test long description from the Lab Order API - "Measurement of ALT (alanine aminotransferase) level in blood used to assess liver functions".toText12(fontWeight: FontWeight.w500), + (appState.isArabic() ? tests!.testDescriptionAr : tests!.testDescriptionEn)!.toText12(fontWeight: FontWeight.w500), SizedBox(height: 8.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 6b65d5d..6311451 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.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/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/lab/lab_range_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late LabRangeViewModel rangeViewModel; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context); rangeViewModel = Provider.of(context); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 16.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; scheduleMicrotask(() { labProvider .getPatientLabResultByHospital( model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); }); Navigator.push( context, CustomPageRoute( page: LabResultByHospitals(), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().length, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( onTap: () { if(model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult( model.uniqueTests .toList()[index] .model!, model.uniqueTests .toList()[index].description!); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_range_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late LabRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 16.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); Navigator.push( context, CustomPageRoute( page: LabResultByHospitals(), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().isNotEmpty ? model.uniqueTests.toList().length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.uniqueTests.toList().isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( appState: _appState, onTap: () { if (model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult(model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!)); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart b/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart index 9e1f08c..d7642c2 100644 --- a/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart @@ -26,8 +26,8 @@ class LabResultList extends StatelessWidget { shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) { var labItem = list[index]; return LabOrderResultItem(onTap: () { - model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description??""); - }, + model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.packageShortDescription!); + }, tests: labItem, index: index, iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"), diff --git a/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart index 59638de..7dc4075 100644 --- a/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart +++ b/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart @@ -54,14 +54,7 @@ class LabOrderResultItem extends StatelessWidget { spacing: 6.h, crossAxisAlignment: CrossAxisAlignment.start, children: [ - - Text(severityText.tr(), - style: TextStyle( - fontFamily: 'Poppins', - fontSize: 10.fSize, - fontWeight: FontWeight.w500, - color: AppColors.greyTextColor - )), + severityText.tr().toText10(weight: FontWeight.w500, color: AppColors.greyTextColor), Utils.buildSvgWithAssets( icon: AppAssets.lab_result_indicator, width: 21, @@ -82,7 +75,7 @@ class LabOrderResultItem extends StatelessWidget { borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, fontSize: 14, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index 790199a..4b31d0a 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -25,8 +25,9 @@ class LabResultDetails extends StatelessWidget { // final List graphPoint; late LabViewModel model; + String? testDescription; - LabResultDetails({super.key, required this.recentLabResult}); + LabResultDetails({super.key, required this.recentLabResult, required this.testDescription}); @override Widget build(BuildContext context) { From b05e4e2a159f1b91d3d24a5e19bc02034c78608d Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 6 Oct 2025 10:30:04 +0300 Subject: [PATCH 34/43] Dental appointment booking flow implementation contd. --- .../book_appointments/book_appointments_repo.dart | 5 +++-- .../book_appointments/book_appointments_view_model.dart | 9 ++++++++- .../book_appointment/review_appointment_page.dart | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index f15c5ea..82ca921 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -24,7 +24,7 @@ abstract class BookAppointmentsRepo { Future>> getDoctorProfile(int clinicID, int projectID, int doctorId, {Function(dynamic)? onSuccess, Function(String)? onError}); - Future>> getDoctorFreeSlots(int clinicID, int projectID, int doctorId, bool isBookingForLiveCare, {Function(dynamic)? onSuccess, Function(String)? onError}); + Future>> getDoctorFreeSlots(int clinicID, int projectID, int doctorId, bool isBookingForLiveCare, {bool continueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}); Future>> cancelAppointment({required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel}); @@ -229,7 +229,7 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { //TODO: Implement the logic for Dental & laser clinics @override Future>> getDoctorFreeSlots(int clinicID, int projectID, int doctorId, bool isBookingForLiveCare, - {Function(dynamic)? onSuccess, Function(String)? onError}) async { + {bool continueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { Map mapDevice = { "DoctorID": doctorId, "IsBookingForLiveCare": isBookingForLiveCare, @@ -238,6 +238,7 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { "OriginalClinicID": clinicID, "days": 0, "isReschadual": false, + "ContinueDentalPlan": continueDentalPlan }; try { diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 5e0d424..a6c741f 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -378,7 +378,14 @@ class BookAppointmentsViewModel extends ChangeNotifier { final DateFormat dateFormatter = DateFormat('yyyy-MM-dd'); Map _eventsParsed; - final result = await bookAppointmentsRepo.getDoctorFreeSlots(selectedDoctor.clinicID ?? 0, selectedDoctor.projectID ?? 0, selectedDoctor.doctorID ?? 0, isBookingForLiveCare, onError: onError); + final result = await bookAppointmentsRepo.getDoctorFreeSlots( + selectedDoctor.clinicID ?? 0, + selectedDoctor.projectID ?? 0, + selectedDoctor.doctorID ?? 0, + isBookingForLiveCare, + continueDentalPlan: isContinueDentalPlan, + onError: onError, + ); result.fold( (failure) async { diff --git a/lib/presentation/book_appointment/review_appointment_page.dart b/lib/presentation/book_appointment/review_appointment_page.dart index 280c4ad..77cd249 100644 --- a/lib/presentation/book_appointment/review_appointment_page.dart +++ b/lib/presentation/book_appointment/review_appointment_page.dart @@ -229,6 +229,7 @@ class _ReviewAppointmentPageState extends State { }); }); } else { + //TODO: Add patient Derma package check API Here await bookAppointmentsViewModel.insertSpecificAppointment(onError: (err) { print(err.data["ErrorEndUserMessage"]); LoadingUtils.hideFullScreenLoader(); From 30eb8bf5dea3f8a9a51195c9ce0b3e41c12c362e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 6 Oct 2025 12:34:18 +0300 Subject: [PATCH 35/43] dental booking flow implemented --- lib/core/api/api_client.dart | 4 +- lib/core/api_consts.dart | 2 +- .../book_appointments_repo.dart | 99 +++++++ .../book_appointments_view_model.dart | 68 +++++ ...ental_chief_complaints_response_model.dart | 24 ++ .../facility_selection_item.dart | 2 +- .../region_bottomsheet/region_list_item.dart | 2 +- .../dental_chief_complaints_page.dart | 95 +++++++ .../book_appointment/select_clinic_page.dart | 263 ++++++++++-------- .../book_appointment/select_doctor_page.dart | 8 +- .../widgets/chief_complaint_card.dart | 38 +++ .../book_appointment/widgets/clinic_card.dart | 5 +- 12 files changed, 481 insertions(+), 129 deletions(-) create mode 100644 lib/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart create mode 100644 lib/presentation/book_appointment/dental_chief_complaints_page.dart create mode 100644 lib/presentation/book_appointment/widgets/chief_complaint_card.dart diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index cd7b673..9140a13 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -173,8 +173,8 @@ class ApiClientImp implements ApiClient { body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; } - body['TokenID'] = "@dm!n"; - body['PatientID'] = 4767884; + // body['TokenID'] = "@dm!n"; + // body['PatientID'] = 4767884; // body['PatientTypeID'] = 1; // // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 511cb37..177a541 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu 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/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index 82ca921..437d2c8 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; @@ -72,6 +73,12 @@ abstract class BookAppointmentsRepo { Future>>> getPatientDentalEstimation( {required int projectID, Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>>> getDentalChiefComplaintsList( + {required int projectID, required int clinicID, required int patientID, Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>>> getDentalChiefComplaintDoctorsList(int projectID, int chiefComplaintID, + {Function(dynamic)? onSuccess, Function(String)? onError}); } class BookAppointmentsRepoImp implements BookAppointmentsRepo { @@ -701,4 +708,96 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> getDentalChiefComplaintsList( + {required int projectID, required int clinicID, required int patientID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = { + "PatientID": patientID, + "ClinicID": clinicID, + "ProjectID": projectID, + "isDentalAllowedBackend": true, + "ContinueDentalPlan": false, + "IsSearchAppointmnetByClinicID": false, + }; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_DOCTORS_LIST_URL, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_DentalChiefComplain']; + + final chiefComplaintsList = list.map((item) => DentalChiefComplaintsListResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: chiefComplaintsList, + ); + } 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())); + } + } + + @override + Future>>> getDentalChiefComplaintDoctorsList(int projectID, int chiefComplaintID, + {Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = { + "ProjectID": projectID, + "ChiefComplaintID": chiefComplaintID, + "isDentalAllowedBackend": true, + "IsPublicRequest": true, + }; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_DENTAL_DOCTORS_LIST_URL, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_DentalDoctorChiefComplaintMapping']; + + final doctorsList = list.map((item) => DoctorsListResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: doctorsList, + ); + } 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 a6c741f..26bfe4f 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -12,6 +12,7 @@ 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/features/book_appointments/book_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/free_slot.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; @@ -57,8 +58,11 @@ class BookAppointmentsViewModel extends ChangeNotifier { List liveCareDoctorsList = []; List patientDentalPlanEstimationList = []; + List dentalChiefComplaintsList = []; int totalTimeNeededForDentalProcedure = 0; bool isContinueDentalPlan = false; + bool isChiefComplaintsListLoading = false; + int selectedChiefComplaintID = 0; GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel(); DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel(); @@ -134,7 +138,9 @@ class BookAppointmentsViewModel extends ChangeNotifier { doctorsList.clear(); liveCareClinicsList.clear(); patientDentalPlanEstimationList.clear(); + dentalChiefComplaintsList.clear(); isContinueDentalPlan = false; + isChiefComplaintsListLoading = true; // getLocation(); notifyListeners(); } @@ -201,6 +207,16 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + setIsChiefComplaintsListLoading(bool value) { + isChiefComplaintsListLoading = value; + notifyListeners(); + } + + setSelectedChiefComplaintID(int id) { + selectedChiefComplaintID = id; + notifyListeners(); + } + void onTabChanged(int index) { selectedTabIndex = index; notifyListeners(); @@ -949,4 +965,56 @@ class BookAppointmentsViewModel extends ChangeNotifier { }, ); } + + Future getDentalChiefComplaintsList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + dentalChiefComplaintsList.clear(); + notifyListeners(); + int patientID = _appState.isAuthenticated ? _appState.getAuthenticatedUser()!.patientId ?? -1 : -1; + final result = await bookAppointmentsRepo.getDentalChiefComplaintsList(patientID: patientID, projectID: int.parse(currentlySelectedHospitalFromRegionFlow ?? "0"), clinicID: 17); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + dentalChiefComplaintsList = apiResponse.data!; + isChiefComplaintsListLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getDentalChiefComplaintDoctorsList({int projectID = 0, Function(dynamic)? onSuccess, Function(String)? onError}) async { + doctorsList.clear(); + projectID = currentlySelectedHospitalFromRegionFlow != null ? int.parse(currentlySelectedHospitalFromRegionFlow!) : projectID; + final result = await bookAppointmentsRepo.getDentalChiefComplaintDoctorsList(projectID, selectedChiefComplaintID); + + result.fold( + (failure) async { + onError!("No doctors found for the search criteria...".needTranslation); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + doctorsList = apiResponse.data!; + filteredDoctorList = doctorsList; + isDoctorsListLoading = false; + // initializeFilteredList(); + // clearSearchFilters(); + // getFiltersFromDoctorList(); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } } diff --git a/lib/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart b/lib/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart new file mode 100644 index 0000000..99d5cd7 --- /dev/null +++ b/lib/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart @@ -0,0 +1,24 @@ +class DentalChiefComplaintsListResponseModel { + int? projectID; + int? iD; + String? name; + dynamic nameN; + + DentalChiefComplaintsListResponseModel({this.projectID, this.iD, this.name, this.nameN}); + + DentalChiefComplaintsListResponseModel.fromJson(Map json) { + projectID = json['ProjectID']; + iD = json['ID']; + name = json['Name']; + nameN = json['NameN']; + } + + Map toJson() { + final Map data = new Map(); + data['ProjectID'] = this.projectID; + data['ID'] = this.iD; + data['Name'] = this.name; + data['NameN'] = this.nameN; + return data; + } +} diff --git a/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart b/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart index b267ee4..16dfb1e 100644 --- a/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart +++ b/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart @@ -48,7 +48,7 @@ class FacilitySelectionItem extends StatelessWidget { Transform.flip( flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, + icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, width: 18, height: 13, diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart index ab9cd9b..688580c 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart @@ -45,7 +45,7 @@ class RegionListItem extends StatelessWidget { Transform.flip( flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, + icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, width: 18, height: 13, diff --git a/lib/presentation/book_appointment/dental_chief_complaints_page.dart b/lib/presentation/book_appointment/dental_chief_complaints_page.dart new file mode 100644 index 0000000..4dc3881 --- /dev/null +++ b/lib/presentation/book_appointment/dental_chief_complaints_page.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/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/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/select_doctor_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/chief_complaint_card.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/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class DentalChiefComplaintsPage extends StatefulWidget { + const DentalChiefComplaintsPage({super.key}); + + @override + State createState() => _DentalChiefComplaintsPageState(); +} + +class _DentalChiefComplaintsPageState extends State { + late AppState appState; + late BookAppointmentsViewModel bookAppointmentsViewModel; + + @override + void initState() { + scheduleMicrotask(() { + bookAppointmentsViewModel.getDentalChiefComplaintsList(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + bookAppointmentsViewModel = Provider.of(context, listen: false); + appState = getIt.get(); + return CollapsingListView( + title: "Dental Chief Complaints".needTranslation, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Consumer(builder: (context, bookAppointmentsVM, child) { + return ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: bookAppointmentsVM.isChiefComplaintsListLoading ? 5 : bookAppointmentsVM.dentalChiefComplaintsList.length, + itemBuilder: (context, index) { + return bookAppointmentsVM.isChiefComplaintsListLoading + ? ChiefComplaintCard( + bookAppointmentsVM: bookAppointmentsVM, + dentalChiefComplaintsListResponseModel: DentalChiefComplaintsListResponseModel(), + isLoading: bookAppointmentsVM.isChiefComplaintsListLoading, + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: ChiefComplaintCard( + bookAppointmentsVM: bookAppointmentsVM, + dentalChiefComplaintsListResponseModel: bookAppointmentsVM.dentalChiefComplaintsList[index], + isLoading: bookAppointmentsVM.isChiefComplaintsListLoading, + ).onPress(() { + bookAppointmentsVM.setSelectedChiefComplaintID(bookAppointmentsVM.dentalChiefComplaintsList[index].iD!); + bookAppointmentsViewModel.setIsDoctorsListLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + }), + ), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ); + }), + ), + ), + ); + } +} diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index 1c33dd6..ada4809 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -18,6 +18,7 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/dental_chief_complaints_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_doctor_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_livecare_clinic_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/clinic_card.dart'; @@ -222,124 +223,13 @@ class _SelectClinicPageState extends State { void handleDoctorScreen(GetClinicsListResponseModel clinic) async { if (widget.isFromRegionFlow) { //Dental Clinic Flow - if (clinic.clinicID == 17) { - LoaderBottomSheet.showLoader(loadingText: "Checking for an existing dental plan, Please wait...".needTranslation); - await bookAppointmentsViewModel.getPatientDentalEstimation(projectID: int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")).then((value) { - LoaderBottomSheet.hideLoader(); - if (bookAppointmentsViewModel.patientDentalPlanEstimationList.isNotEmpty) { - showCommonBottomSheetWithoutHeight( - // title: LocaleKeys.notice.tr(context: context), - title: "Dental treatment plan".needTranslation, - context, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "You have an existing treatment plan: ".needTranslation.toText14(weight: FontWeight.w500), - SizedBox(height: 8.h), - Container( - width: double.infinity, - padding: EdgeInsets.all(16.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListView.separated( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsViewModel.patientDentalPlanEstimationList.length, - separatorBuilder: (_, __) { - return Column( - children: [ - SizedBox(height: 8.h), - Divider(height: 1, color: AppColors.greyColor), - SizedBox(height: 8.h), - ], - ); - }, - itemBuilder: (context, index) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - bookAppointmentsViewModel.patientDentalPlanEstimationList[index].procedureName!.toText12(isBold: true), - AppCustomChipWidget(icon: AppAssets.appointment_time_icon, labelText: "${bookAppointmentsViewModel.totalTimeNeededForDentalProcedure} Mins".needTranslation), - ], - ); - }, - ), - SizedBox( - height: 16.h, - ), - Divider(height: 1, color: AppColors.greyColor), - SizedBox( - height: 8.h, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "Total time required".needTranslation.toText14(isBold: true), - AppCustomChipWidget(icon: AppAssets.appointment_time_icon, labelText: "30 Mins".needTranslation), - ], - ) - ], - ), - ), - SizedBox(height: 16.h), - "Would you like to continue it?".needTranslation.toText14(weight: FontWeight.w500), - SizedBox(height: 16.h), - Row( - children: [ - Expanded( - child: CustomButton( - text: LocaleKeys.cancel.tr(), - onPressed: () { - bookAppointmentsViewModel.setIsContinueDentalPlan(false); - Navigator.of(context).pop(); - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - icon: AppAssets.cancel, - iconColor: AppColors.whiteColor, - ), - ), - SizedBox(width: 8.h), - Expanded( - child: CustomButton( - text: LocaleKeys.confirm.tr(), - onPressed: () async { - bookAppointmentsViewModel.setIsContinueDentalPlan(true); - Navigator.of(context).push( - CustomPageRoute( - page: SelectDoctorPage(), - ), - ); - }, - backgroundColor: AppColors.bgGreenColor, - borderColor: AppColors.bgGreenColor, - textColor: Colors.white, - icon: AppAssets.confirm, - ), - ), - ], - ) - ], - ), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - } else { - // Navigate to Chief Complaint Screen - } - }); + if (clinic.clinicID == 17 && appState.isAuthenticated) { + initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")); } else { + bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); Navigator.of(context).push( CustomPageRoute( - page: SelectDoctorPage(), + page: DentalChiefComplaintsPage(), ), ); } @@ -361,7 +251,18 @@ class _SelectClinicPageState extends State { showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer(builder: (_, data, __) => getTitle(data)), isDismissible: false, child: Consumer(builder: (_, data, __) { return getRegionalSelectionWidget(data); - }), callBackFunc: () {}); + }), callBackFunc: () { + if (type == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER && appState.isAuthenticated) { + initDentalAppointmentBookingFlow(regionalViewModel.selectedHospital?.hospitalList.first.iD); + } else { + bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: DentalChiefComplaintsPage(), + ), + ); + } + }); } Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data) { @@ -378,12 +279,12 @@ class _SelectClinicPageState extends State { return HospitalBottomSheetBody(); } if (data.bottomSheetState == AppointmentViaRegionState.DOCTOR_SELECTION) { - //if the region screen is opened for the dental clinic thenthe project id will be in the hospital list as the list is formed form the get project api + //if the region screen is opened for the dental clinic then the project id will be in the hospital list as the list is formed form the get project api var id = ""; if (data.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { - id = regionalViewModel.selectedHospital?.hospitalList?.first?.iD?.toString() ?? ""; + id = regionalViewModel.selectedHospital?.hospitalList.first.iD?.toString() ?? ""; } else { - id = regionalViewModel.selectedHospital?.patientDoctorAppointmentList?.first?.projectID?.toString() ?? ""; + id = regionalViewModel.selectedHospital?.patientDoctorAppointmentList?.first.projectID?.toString() ?? ""; } bookAppointmentsViewModel.setProjectID(id); return SizedBox.shrink(); @@ -410,4 +311,128 @@ class _SelectClinicPageState extends State { }); } } + + void initDentalAppointmentBookingFlow(int projectID) async { + bookAppointmentsViewModel.setProjectID(projectID.toString()); + LoaderBottomSheet.showLoader(loadingText: "Checking for an existing dental plan, Please wait...".needTranslation); + await bookAppointmentsViewModel.getPatientDentalEstimation(projectID: projectID).then((value) { + LoaderBottomSheet.hideLoader(); + if (bookAppointmentsViewModel.patientDentalPlanEstimationList.isNotEmpty) { + showCommonBottomSheetWithoutHeight( + // title: LocaleKeys.notice.tr(context: context), + title: "Dental treatment plan".needTranslation, + context, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "You have an existing treatment plan: ".needTranslation.toText14(weight: FontWeight.w500), + SizedBox(height: 8.h), + Container( + width: double.infinity, + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: bookAppointmentsViewModel.patientDentalPlanEstimationList.length, + separatorBuilder: (_, __) { + return Column( + children: [ + SizedBox(height: 8.h), + Divider(height: 1, color: AppColors.greyColor), + SizedBox(height: 8.h), + ], + ); + }, + itemBuilder: (context, index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + bookAppointmentsViewModel.patientDentalPlanEstimationList[index].procedureName!.toText12(isBold: true), + AppCustomChipWidget(icon: AppAssets.appointment_time_icon, labelText: "${bookAppointmentsViewModel.totalTimeNeededForDentalProcedure} Mins".needTranslation), + ], + ); + }, + ), + SizedBox( + height: 16.h, + ), + Divider(height: 1, color: AppColors.greyColor), + SizedBox( + height: 8.h, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Total time required".needTranslation.toText14(isBold: true), + AppCustomChipWidget(icon: AppAssets.appointment_time_icon, labelText: "30 Mins".needTranslation), + ], + ) + ], + ), + ), + SizedBox(height: 16.h), + "Would you like to continue it?".needTranslation.toText14(weight: FontWeight.w500), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.cancel.tr(), + onPressed: () { + bookAppointmentsViewModel.setIsContinueDentalPlan(false); + bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); + Navigator.of(context).pop(); + Navigator.of(context).push( + CustomPageRoute( + page: DentalChiefComplaintsPage(), + ), + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + icon: AppAssets.cancel, + iconColor: AppColors.whiteColor, + ), + ), + SizedBox(width: 8.h), + Expanded( + child: CustomButton( + text: LocaleKeys.confirm.tr(), + onPressed: () async { + bookAppointmentsViewModel.setIsContinueDentalPlan(true); + Navigator.of(context).pop(); + Navigator.of(context).push( + CustomPageRoute( + page: SelectDoctorPage(), + ), + ); + }, + backgroundColor: AppColors.bgGreenColor, + borderColor: AppColors.bgGreenColor, + textColor: Colors.white, + icon: AppAssets.confirm, + ), + ), + ], + ) + ], + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else { + // Navigate to Chief Complaint Screen + } + }); + } } diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 8d24b43..b0b2a45 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -21,7 +21,6 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; class SelectDoctorPage extends StatefulWidget { @@ -45,7 +44,11 @@ class _SelectDoctorPageState extends State { if (bookAppointmentsViewModel.isLiveCareSchedule) { bookAppointmentsViewModel.getLiveCareDoctorsList(); } else { - bookAppointmentsViewModel.getDoctorsList(); + if (bookAppointmentsViewModel.selectedClinic.clinicID == 17) { + bookAppointmentsViewModel.getDentalChiefComplaintDoctorsList(); + } else { + bookAppointmentsViewModel.getDoctorsList(); + } } }); super.initState(); @@ -66,7 +69,6 @@ class _SelectDoctorPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // TODO: Implement doctor filter functionality SizedBox(height: 16.h), Row( spacing: 8.h, diff --git a/lib/presentation/book_appointment/widgets/chief_complaint_card.dart b/lib/presentation/book_appointment/widgets/chief_complaint_card.dart new file mode 100644 index 0000000..e80596c --- /dev/null +++ b/lib/presentation/book_appointment/widgets/chief_complaint_card.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class ChiefComplaintCard extends StatelessWidget { + ChiefComplaintCard({super.key, required this.isLoading, required this.bookAppointmentsVM, required this.dentalChiefComplaintsListResponseModel}); + + bool isLoading; + BookAppointmentsViewModel bookAppointmentsVM; + DentalChiefComplaintsListResponseModel dentalChiefComplaintsListResponseModel; + + @override + Widget build(BuildContext context) { + AppState appState = getIt.get(); + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Expanded(child: (isLoading ? "Cardiology" : dentalChiefComplaintsListResponseModel.name)!.toText16(isBold: true).toShimmer2(isShow: isLoading)), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, width: 40.h, height: 40.h, fit: BoxFit.contain, iconColor: AppColors.textColor).toShimmer2(isShow: isLoading)), + ]), + ); + } +} diff --git a/lib/presentation/book_appointment/widgets/clinic_card.dart b/lib/presentation/book_appointment/widgets/clinic_card.dart index c157d05..cab3b1a 100644 --- a/lib/presentation/book_appointment/widgets/clinic_card.dart +++ b/lib/presentation/book_appointment/widgets/clinic_card.dart @@ -36,7 +36,8 @@ class ClinicCard extends StatelessWidget { (clinicsListResponseModel.isLiveCareClinicAndOnline ?? true) ? Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 32.h, height: 32.h, fit: BoxFit.contain).toShimmer2(isShow: isLoading) : SizedBox.shrink(), - ]), + ], + ), SizedBox(height: 16.h), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( @@ -44,7 +45,7 @@ class ClinicCard extends StatelessWidget { .toText16(isBold: true) .toShimmer2(isShow: isLoading)), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, width: 40.h, height: 40.h, fit: BoxFit.contain, iconColor: AppColors.textColor).toShimmer2(isShow: isLoading)), ]), ], From d220be72831685cb30c41c464a08550c2c31ddc3 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 6 Oct 2025 14:24:32 +0300 Subject: [PATCH 36/43] Tamara payment flow done --- lib/core/api_consts.dart | 9 +- lib/features/payfort/payfort_repo.dart | 103 +++++++ lib/features/payfort/payfort_view_model.dart | 50 ++++ .../appointment_payment_page.dart | 258 +++++++++++------- .../appointment_checkin_bottom_sheet.dart | 2 +- .../immediate_livecare_payment_page.dart | 128 ++++++--- 6 files changed, 410 insertions(+), 140 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 177a541..98c7ec0 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu 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 @@ -745,6 +745,7 @@ class ApiConsts { static String TAMARA_URL = "https://mdlaboratories.com/tamaralive/Home/Checkout"; static String GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamaralive/Home/GetInstallments"; + static String GET_TAMARA_PAYMENT_STATUS = 'https://mdlaboratories.com/tamaralive/api/OnlineTamara/order_status?orderid='; // static String GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; @@ -760,6 +761,7 @@ class ApiConsts { SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx"; TAMARA_URL = "https://mdlaboratories.com/tamaralive/Home/Checkout"; GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamaralive/Home/GetInstallments"; + GET_TAMARA_PAYMENT_STATUS = 'https://mdlaboratories.com/tamaralive/api/OnlineTamara/order_status?orderid='; break; case AppEnvironmentTypeEnum.dev: baseUrl = "https://uat.hmgwebservices.com/"; @@ -768,6 +770,7 @@ class ApiConsts { SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; + GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; break; case AppEnvironmentTypeEnum.uat: baseUrl = "https://uat.hmgwebservices.com/"; @@ -776,6 +779,7 @@ class ApiConsts { SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; + GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; break; case AppEnvironmentTypeEnum.preProd: @@ -785,6 +789,7 @@ class ApiConsts { SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx"; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; + GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; break; case AppEnvironmentTypeEnum.qa: baseUrl = "https://uat.hmgwebservices.com/"; @@ -793,6 +798,7 @@ class ApiConsts { SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; + GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; break; case AppEnvironmentTypeEnum.staging: baseUrl = "https://uat.hmgwebservices.com/"; @@ -801,6 +807,7 @@ class ApiConsts { SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; + GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; break; } } diff --git a/lib/features/payfort/payfort_repo.dart b/lib/features/payfort/payfort_repo.dart index 9f836d1..74a323a 100644 --- a/lib/features/payfort/payfort_repo.dart +++ b/lib/features/payfort/payfort_repo.dart @@ -18,6 +18,13 @@ abstract class PayfortRepo { Future>> generateSdkSignatureFromAPI({required SdkTokenRequest tokenRequest}); Future>> checkPaymentStatus({required String transactionID}); + + Future>> checkTamaraPaymentStatus({required String transactionID}); + + Future>> markAppointmentAsTamaraPaid({required int projectID, required int appointmentNo}); + + Future>> updateTamaraRequestStatus( + {required String responseMessage, required String status, required String clientRequestID, required String tamaraOrderID}); } class PayfortRepoImp implements PayfortRepo { @@ -147,4 +154,100 @@ class PayfortRepoImp implements PayfortRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future> checkTamaraPaymentStatus({required String transactionID}) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.get( + '${ApiConsts.GET_TAMARA_PAYMENT_STATUS}$transactionID', + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } 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())); + } + } + + @override + Future> updateTamaraRequestStatus({required String responseMessage, required String status, required String clientRequestID, required String tamaraOrderID}) async { + Map body = { + "Response_Message": responseMessage, + "ClientRequestID": clientRequestID, + "Status": status, + "FortID": tamaraOrderID, // Tamara order ID + "LanguageID": 1, + "Installments_Number": 3, + }; + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(UPDATE_TAMARA_STATUS, body: body, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, isAllowAny: true, isPaymentServices: true); + 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> markAppointmentAsTamaraPaid({required int projectID, required int appointmentNo}) async { + Map body = {"ProjectID": projectID, "AppointmentNo": appointmentNo, "LanguageID": 1}; + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(MARK_APPOINTMENT_TAMARA_STATUS, body: body, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, isAllowAny: true, isPaymentServices: true); + 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/payfort/payfort_view_model.dart b/lib/features/payfort/payfort_view_model.dart index 6b67ce9..89effcd 100644 --- a/lib/features/payfort/payfort_view_model.dart +++ b/lib/features/payfort/payfort_view_model.dart @@ -94,6 +94,40 @@ class PayfortViewModel extends ChangeNotifier { ); } + Future checkTamaraPaymentStatus({required String transactionID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.checkTamaraPaymentStatus(transactionID: transactionID); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + print(apiResponse.data); + if (onSuccess != null) { + onSuccess(apiResponse); + } + // } + }, + ); + } + + Future updateTamaraRequestStatus( + {required String responseMessage, required String status, required String clientRequestID, required String tamaraOrderID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.updateTamaraRequestStatus(responseMessage: responseMessage, status: status, clientRequestID: clientRequestID, tamaraOrderID: tamaraOrderID); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + print(apiResponse.data); + if (onSuccess != null) { + onSuccess(apiResponse); + } + }, + ); + } + Future _generateSdkResponse({ String? applePayAccessCode, String? merchantIdentifier, @@ -199,4 +233,20 @@ class PayfortViewModel extends ChangeNotifier { onFailed!(e.toString() as PayFortFailureResult); } } + + Future markAppointmentAsTamaraPaid({required int projectID, required int appointmentNo, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.markAppointmentAsTamaraPaid(projectID: projectID, appointmentNo: appointmentNo); + + result.fold( + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + print(apiResponse.data); + if (onSuccess != null) { + onSuccess(apiResponse); + } + }, + ); + } } diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index ee50ab4..3c475c1 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -52,6 +52,8 @@ class _AppointmentPaymentPageState extends State { String transID = ""; bool isShowTamara = false; + String tamaraPaymentStatus = ""; + String tamaraOrderID = ""; @override void initState() { @@ -117,7 +119,7 @@ class _AppointmentPaymentPageState extends State { Transform.flip( flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, + icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, width: 18.h, height: 13.h, @@ -159,7 +161,7 @@ class _AppointmentPaymentPageState extends State { Transform.flip( flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, + icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, width: 18.h, height: 13.h, @@ -196,18 +198,18 @@ class _AppointmentPaymentPageState extends State { Transform.flip( flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, - iconColor: AppColors.blackColor, - width: 18.h, - height: 13.h, - fit: BoxFit.contain, - ).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), - ), - ], - ).paddingSymmetrical(16.h, 16.h), - ).paddingSymmetrical(24.h, 0.h).onPress(() { - selectedPaymentMethod = "TAMARA"; - openPaymentURL("tamara"); + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "TAMARA"; + openPaymentURL("tamara"); }) : SizedBox.shrink(), ], @@ -328,12 +330,12 @@ class _AppointmentPaymentPageState extends State { if (selectedPaymentMethod == "tamara") { if (Platform.isAndroid) { Uri uri = new Uri.dataFromString(url); - // tamaraPaymentStatus = uri.queryParameters['status']!; - // tamaraOrderID = uri.queryParameters['AuthorizePaymentId']!; + tamaraPaymentStatus = uri.queryParameters['status']!; + tamaraOrderID = uri.queryParameters['AuthorizePaymentId']!; } else { Uri uri = new Uri.dataFromString(url); - // tamaraPaymentStatus = uri.queryParameters['paymentStatus']!; - // tamaraOrderID = uri.queryParameters['orderId']!; + tamaraPaymentStatus = uri.queryParameters['paymentStatus']!; + tamaraOrderID = uri.queryParameters['orderId']!; } } @@ -359,95 +361,155 @@ class _AppointmentPaymentPageState extends State { } onBrowserExit(bool isPaymentMade) async { - print("onBrowserExit Called!!!!"); - if (selectedPaymentMethod == "TAMARA") { - // checkTamaraPaymentStatus(transID!, appo); - // if (tamaraPaymentStatus != null && tamaraPaymentStatus.toLowerCase() == "approved") { - // updateTamaraRequestStatus("success", "14", Utils.getAppointmentTransID(appo.projectID, appo.clinicID, appo.appointmentNo), tamaraOrderID, num.parse(selectedInstallments), appo); - // } else { - // updateTamaraRequestStatus("Failed", "00", Utils.getAppointmentTransID(appo.projectID, appo.clinicID, appo.appointmentNo), tamaraOrderID, num.parse(selectedInstallments), appo); - // } - } else { - checkPaymentStatus(); - // checkPaymentStatus(appo); - } + checkPaymentStatus(); } void checkPaymentStatus() async { - LoaderBottomSheet.showLoader(); - await payfortViewModel.checkPaymentStatus( - transactionID: transID, - onSuccess: (apiResponse) async { - print(apiResponse.data); - if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") { - await myAppointmentsViewModel.createAdvancePayment( - paymentMethodName: selectedPaymentMethod, - projectID: widget.patientAppointmentHistoryResponseModel.projectID, - clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, - appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), - payedAmount: payfortViewModel.payfortCheckPaymentStatusResponseModel!.amount!, - paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!, - patientID: appState.getAuthenticatedUser()!.patientId.toString(), - patientType: appState.getAuthenticatedUser()!.patientType!, - onSuccess: (value) async { - print(value); - await myAppointmentsViewModel.addAdvanceNumberRequest( - advanceNumber: Utils.isVidaPlusProject(widget.patientAppointmentHistoryResponseModel.projectID) - ? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString() - : value.data['OnlineCheckInAppointments'][0]['AdvanceNumber'].toString(), - paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!, - appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), - onSuccess: (value) async { - if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) { - //TODO: Implement LiveCare Check-In API Call - await myAppointmentsViewModel.insertLiveCareVIDARequest( - clientRequestID: transID, - patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, - onSuccess: (apiResponse) { - Future.delayed(Duration(milliseconds: 500), () { - LoaderBottomSheet.hideLoader(); - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }); - }, - onError: (error) {}); - } else { - await myAppointmentsViewModel.generateAppointmentQR( - clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, - projectID: widget.patientAppointmentHistoryResponseModel.projectID, - appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), - isFollowUp: myAppointmentsViewModel.patientAppointmentShareResponseModel!.isFollowup!, - onSuccess: (apiResponse) { - Future.delayed(Duration(milliseconds: 500), () { - LoaderBottomSheet.hideLoader(); - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - // Navigator.of(context).push( - // CustomPageRoute(page: MyAppointmentsPage()), - // ); - }); - }); - } - }); - }); - } else { + LoaderBottomSheet.showLoader(loadingText: "Checking payment status, Please wait...".needTranslation); + if (selectedPaymentMethod == "TAMARA") { + await payfortViewModel.checkTamaraPaymentStatus( + transactionID: transID, + onSuccess: (apiResponse) async { + if (apiResponse.data["status"].toString().toLowerCase() == "success") { + tamaraOrderID = apiResponse.data["tamara_order_id"].toString(); + await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID); + await payfortViewModel.markAppointmentAsTamaraPaid( + projectID: widget.patientAppointmentHistoryResponseModel.projectID, appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo); + await myAppointmentsViewModel.addAdvanceNumberRequest( + advanceNumber: "Tamara-Advance-0000", + paymentReference: tamaraOrderID, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + onSuccess: (value) async { + if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) { + //TODO: Implement LiveCare Check-In API Call + await myAppointmentsViewModel.insertLiveCareVIDARequest( + clientRequestID: tamaraOrderID, + patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, + onSuccess: (apiResponse) { + Future.delayed(Duration(milliseconds: 500), () { + LoaderBottomSheet.hideLoader(); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }); + }, + onError: (error) {}); + } else { + await myAppointmentsViewModel.generateAppointmentQR( + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + isFollowUp: myAppointmentsViewModel.patientAppointmentShareResponseModel!.isFollowup!, + onSuccess: (apiResponse) { + Future.delayed(Duration(milliseconds: 500), () { + LoaderBottomSheet.hideLoader(); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }); + }); + } + }); + } else { + await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID); + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( context, - child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + child: Utils.getErrorWidget(loadingText: err), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, ); - } - }); + }); + } else { + await payfortViewModel.checkPaymentStatus( + transactionID: transID, + onSuccess: (apiResponse) async { + print(apiResponse.data); + if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") { + await myAppointmentsViewModel.createAdvancePayment( + paymentMethodName: selectedPaymentMethod, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + payedAmount: payfortViewModel.payfortCheckPaymentStatusResponseModel!.amount!, + paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!, + patientID: appState.getAuthenticatedUser()!.patientId.toString(), + patientType: appState.getAuthenticatedUser()!.patientType!, + onSuccess: (value) async { + print(value); + await myAppointmentsViewModel.addAdvanceNumberRequest( + advanceNumber: Utils.isVidaPlusProject(widget.patientAppointmentHistoryResponseModel.projectID) + ? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString() + : value.data['OnlineCheckInAppointments'][0]['AdvanceNumber'].toString(), + paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + onSuccess: (value) async { + if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) { + //TODO: Implement LiveCare Check-In API Call + await myAppointmentsViewModel.insertLiveCareVIDARequest( + clientRequestID: transID, + patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, + onSuccess: (apiResponse) { + Future.delayed(Duration(milliseconds: 500), () { + LoaderBottomSheet.hideLoader(); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }); + }, + onError: (error) {}); + } else { + await myAppointmentsViewModel.generateAppointmentQR( + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), + isFollowUp: myAppointmentsViewModel.patientAppointmentShareResponseModel!.isFollowup!, + onSuccess: (apiResponse) { + Future.delayed(Duration(milliseconds: 500), () { + LoaderBottomSheet.hideLoader(); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }); + }); + } + }); + }); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }); + } } openPaymentURL(String paymentMethod) { diff --git a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart index e79f26f..93f2bda 100644 --- a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart +++ b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart @@ -124,7 +124,7 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { Transform.flip( flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, + icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, width: 18.h, height: 13.h, diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index 5437945..95d6302 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -57,6 +57,8 @@ class _ImmediateLiveCarePaymentPageState extends State false); + Navigator.of(context).push( CustomPageRoute( - page: LandingNavigation(), + page: ImmediateLiveCarePendingRequestPage(), ), - (r) => false); - Navigator.of(context).push( - CustomPageRoute( - page: ImmediateLiveCarePendingRequestPage(), - ), - ); + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unknown error occurred...".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } } else { + await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID); + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( context, - child: Utils.getErrorWidget(loadingText: "Unknown error occurred...".needTranslation), + child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, ); } - } else { + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( context, - child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + child: Utils.getErrorWidget(loadingText: err), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, ); - } - }); + }); + } else { + await payfortViewModel.checkPaymentStatus( + transactionID: transID, + onSuccess: (apiResponse) async { + debugPrint(apiResponse.data.toString()); + if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") { + await immediateLiveCareViewModel.addNewCallRequestForImmediateLiveCare(transID); + await immediateLiveCareViewModel.getPatientLiveCareHistory(); + LoaderBottomSheet.hideLoader(); + if (immediateLiveCareViewModel.patientHasPendingLiveCareRequest) { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unknown error occurred...".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }); + } } openPaymentURL(String paymentMethod) { From e5dcabaccee8e88eb8abbfbe5283389ef0e3c71e Mon Sep 17 00:00:00 2001 From: tahaalam Date: Mon, 6 Oct 2025 14:46:43 +0300 Subject: [PATCH 37/43] lab prescription details are added andgraph visibility added to the graph page for result that are IRR --- lib/features/lab/lab_view_model.dart | 14 ++ .../lab/models/resp_models/lab_result.dart | 17 ++ .../LabResultList.dart | 4 +- .../lab_order_result_item.dart | 47 +++- .../lab/lab_results/lab_result_details.dart | 235 ++++++++++-------- .../viewmodel/date_range_view_model.dart | 7 +- 6 files changed, 216 insertions(+), 108 deletions(-) diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index e7507b0..8a397e3 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -22,6 +22,8 @@ class LabViewModel extends ChangeNotifier { bool isLabResultsLoading = false; bool isLabResultByHospitalLoading = false; bool isSpecialResultsLoading = false; + bool isGraphVisible = true; + bool shouldShowGraph = true; LabRepo labRepo; ErrorHandlerService errorHandlerService; @@ -225,6 +227,7 @@ class LabViewModel extends ChangeNotifier { } catch (e) {} }); LabResult recentResult = recentThree.first; + checkIfGraphShouldBeDisplayed(recentResult); recentResult.verifiedOn = resultDate(DateUtil.convertStringToDate(recentResult.verifiedOnDateTime!)); // filteredGraphValues = [filteredGraphValues.first]; navigationService.push(MaterialPageRoute( @@ -237,6 +240,12 @@ class LabViewModel extends ChangeNotifier { ); } + void checkIfGraphShouldBeDisplayed(LabResult recentResult){ + shouldShowGraph = recentResult.checkIfGraphShouldBeDisplayed(); + isGraphVisible = shouldShowGraph; + notifyListeners(); + } + Future getPatientSpecialResult( PatientLabOrdersResponseModel laborder) async { isSpecialResultsLoading = true; @@ -551,4 +560,9 @@ class LabViewModel extends ChangeNotifier { return "normal"; } } + + alterGraphVisibility(){ + isGraphVisible = !isGraphVisible; + notifyListeners(); + } } diff --git a/lib/features/lab/models/resp_models/lab_result.dart b/lib/features/lab/models/resp_models/lab_result.dart index 3f14d54..a075be3 100644 --- a/lib/features/lab/models/resp_models/lab_result.dart +++ b/lib/features/lab/models/resp_models/lab_result.dart @@ -24,6 +24,7 @@ class LabResult { String? referenceHigh; String? criticalLow; String? referenceLow; + num? resultTypeID; String? packageShortDescription; LabResult( @@ -80,6 +81,7 @@ class LabResult { criticalLow = json['CriticalLow']; referenceLow = json['ReferenceLow']; packageShortDescription = json['PackageShortDescription']; + resultTypeID = json['ResultTypeID']; } Map toJson() { @@ -111,6 +113,21 @@ class LabResult { return data; } + bool checkIfGraphShouldBeDisplayed(){ + if (resultTypeID == null) return false; + if (resultTypeID == 6) return false; + if (referanceRange == null || referanceRange == "" || referanceRange == "\n") return false; + bool isDigit = RegExp(r"\\d+").hasMatch("$resultValue"); + if(isDigit) return true; + try { + num.parse(resultValue ?? ""); + } catch (e) { + return false; + } + + return true; + } + @override String toString() { return 'LabOrderResult(flag: $calculatedResultFlag, value: $resultValue, verifiedOn: $verifiedOnDateTime)'; diff --git a/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart b/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart index d7642c2..e94bb5c 100644 --- a/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_hospital/LabResultList.dart @@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart'; -import 'package:provider/provider.dart' show Selector, Provider; +import 'package:provider/provider.dart' show Selector, Provider, ReadContext; class LabResultList extends StatelessWidget { late LabViewModel model; @@ -15,7 +15,7 @@ class LabResultList extends StatelessWidget { return Selector>( selector: (_, model) => model.mainLabResultsByHospitals, builder: (__, list, ___) { - if (list.isEmpty) { + if (list.isEmpty && context.read().labSpecialResult.isEmpty) { return Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet." .needTranslation); diff --git a/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart index 7dc4075..87fd790 100644 --- a/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart +++ b/lib/presentation/lab/lab_result_via_hospital/lab_order_result_item.dart @@ -7,6 +7,7 @@ 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/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart' show LabViewModel; import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; @@ -14,6 +15,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; +import 'package:provider/provider.dart'; class LabOrderResultItem extends StatelessWidget { final VoidCallback onTap; @@ -45,8 +47,49 @@ class LabOrderResultItem extends StatelessWidget { child: '${tests!.description}'.toText14(weight: FontWeight.w500), ), '${tests!.packageShortDescription}'.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), - // - SizedBox(height: 24.h), + SizedBox(height: 12.h), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: Text( + tests?.resultValue ?? "", + style: TextStyle( + fontSize: 24.fSize, + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + color: context.read().getColor( + tests?.calculatedResultFlag ?? "", + ), + letterSpacing: -2, + ), + overflow: TextOverflow.ellipsis, // prevent overflow + maxLines: 1, + softWrap: false, + ), + ), + SizedBox(width: 4.h,), + Expanded( + flex: 2, + child: Visibility( + visible: tests?.referanceRange != null, + child: Text( + "(Reference range ${tests?.referanceRange})".needTranslation, + style: TextStyle( + fontSize: 12.fSize, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + color: AppColors.greyTextColor, + ), + // overflow: TextOverflow.ellipsis, + // maxLines: 2, + softWrap: true, + ), + ), + ), + ], + ), + SizedBox(height: 12.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index 89e86bb..30c1910 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -15,35 +15,38 @@ import 'package:hmg_patient_app_new/presentation/lab/lab_results/lab_result_list import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; -import 'package:provider/provider.dart' show Consumer, Provider; +import 'package:provider/provider.dart' show Consumer, Provider, ReadContext; import '../../../widgets/common_bottom_sheet.dart' show showCommonBottomSheetWithoutHeight; class LabResultDetails extends StatelessWidget { final LabResult recentLabResult; + final String? testDescription; - // final List graphPoint; - late LabViewModel model; - String? testDescription; - - LabResultDetails({super.key, required this.recentLabResult, required this.testDescription}); + const LabResultDetails( + {super.key, + required this.recentLabResult, + required this.testDescription}); @override Widget build(BuildContext context) { - model = Provider.of(context, listen: false); return CollapsingListView( title: 'Lab Result Details'.needTranslation, child: SingleChildScrollView( child: Column( spacing: 16.h, - children: [LabNameAndStatus, LabGraph(context)], + children: [ + LabNameAndStatus(context), + getLabDescription(context), + LabGraph(context) + ], ).paddingAll(24.h), ), ); } - Widget get LabNameAndStatus => Container( + Widget LabNameAndStatus(BuildContext context) => Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 24.h, @@ -80,11 +83,12 @@ class LabResultDetails extends StatelessWidget { //todo change the text color according to the provided test values Row( crossAxisAlignment: CrossAxisAlignment.center, + children: [ Expanded( child: Row( - spacing: 4.h, + mainAxisSize: MainAxisSize.max, children: [ Flexible( child: Text( @@ -93,8 +97,8 @@ class LabResultDetails extends StatelessWidget { fontSize: 24.fSize, fontWeight: FontWeight.w600, fontFamily: 'Poppins', - color: model.getColor( - recentLabResult.calculatedResultFlag ?? "", + color: context.read().getColor( + recentLabResult.calculatedResultFlag ?? "", ), letterSpacing: -2, ), @@ -103,30 +107,37 @@ class LabResultDetails extends StatelessWidget { softWrap: false, ), ), - Visibility( - visible: recentLabResult.referanceRange != null, - child: Text( - "(Reference range ${recentLabResult.referanceRange})".needTranslation, - style: TextStyle( - fontSize: 12.fSize, - fontWeight: FontWeight.w500, - fontFamily: 'Poppins', - color: AppColors.greyTextColor, + SizedBox(width: 4.h,), + Expanded( + flex: 2, + child: Visibility( + visible: recentLabResult.referanceRange != null, + child: Text( + "(Reference range ${recentLabResult.referanceRange})".needTranslation, + style: TextStyle( + fontSize: 12.fSize, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + color: AppColors.greyTextColor, + ), + // overflow: TextOverflow.ellipsis, + // maxLines: 2, + softWrap: true, ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, ), ), ], ), ), - Utils.buildSvgWithAssets( - icon: AppAssets.lab_result_indicator, + SizedBox( width: 21, - height: 23, - iconColor: model.getColor( - recentLabResult.calculatedResultFlag ?? "", + child: Utils.buildSvgWithAssets( + icon: AppAssets.lab_result_indicator, + width: 21, + height: 23, + iconColor: context.read().getColor( + recentLabResult.calculatedResultFlag ?? "", + ), ), ), ], @@ -135,73 +146,80 @@ class LabResultDetails extends StatelessWidget { ], )); - Widget LabGraph(BuildContext context) => Consumer( - builder: (_, model, ___) => Consumer( - builder: (_, labmodel, ___) => Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: true, - ), - height: model.isGraphVisible?260.h:(labmodel.filteredGraphValues.length<3)?(labmodel.filteredGraphValues.length*64)+80.h:260.h, - padding: EdgeInsets.all(16.h), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, + Widget LabGraph(BuildContext context) => Consumer( + builder: (_, labmodel, ___) => Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + height: labmodel.isGraphVisible + ? 260.h + : (labmodel.filteredGraphValues.length < 3) + ? (labmodel.filteredGraphValues.length * 64) + 80.h + : 260.h, + padding: EdgeInsets.all(16.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + //title and filter icon + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - //title and filter icon + Text( + labmodel.isGraphVisible + ? LocaleKeys.historyFlowchart.tr() + : LocaleKeys.history.tr(), + style: TextStyle( + fontSize: 16, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 16.h, children: [ - Text( - model.isGraphVisible?LocaleKeys.historyFlowchart.tr(): LocaleKeys.history.tr(), - style: TextStyle( - fontSize: 16, - fontFamily: 'Poppins', - - fontWeight: FontWeight.w600, - color: AppColors.textColor, - ), - ), - Row( - spacing: 16.h, - children: [ - //todo handle when the graph icon is being displayed - Utils.buildSvgWithAssets( - icon: model.isGraphVisible?AppAssets.ic_list:AppAssets.ic_graph, - width: 24.h, - height: 24.h) - .onPress(() { - model.alterGraphVisibility(); - }), - Utils.buildSvgWithAssets( - icon: AppAssets.ic_date_filter, - width: 24, - height: 24) - .onPress(() { - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.setTheDateRange.tr(), - context, - child: DateRangeSelector( - onRangeSelected: (start, end) { - - // if (start != null) { - labmodel.getSelectedDateRange(start, end); - // } - }, - ), - isFullScreen: false, - isCloseButtonVisible: true, - callBackFunc: () {}, - ); - }), - ], - ) + //todo handle when the graph icon is being displayed + Utils.buildSvgWithAssets( + icon: labmodel.isGraphVisible + ? AppAssets.ic_list + : AppAssets.ic_graph, + width: 24.h, + height: 24.h) + .onPress(() { + if (labmodel.shouldShowGraph) { + labmodel.alterGraphVisibility(); + } + }), + Utils.buildSvgWithAssets( + icon: AppAssets.ic_date_filter, + width: 24, + height: 24) + .onPress(() { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.setTheDateRange.tr(), + context, + child: DateRangeSelector( + onRangeSelected: (start, end) { + // if (start != null) { + labmodel.getSelectedDateRange(start, end); + // } + }, + ), + isFullScreen: false, + isCloseButtonVisible: true, + callBackFunc: () {}, + ); + }), ], - ).paddingOnly(bottom: model.isGraphVisible? 16.h :24.h), - historyBody(model, labmodel) + ) ], - )), - )); + ).paddingOnly(bottom: labmodel.isGraphVisible ? 16.h : 24.h), + historyBody(labmodel) + ], + )), + ); Widget leftLabels(String value) { return Text( @@ -229,17 +247,15 @@ class LabResultDetails extends StatelessWidget { ); } - Widget historyBody(DateRangeSelectorRangeViewModel model, LabViewModel labmodel) { - if(model.isGraphVisible){ + Widget historyBody(LabViewModel labmodel) { + if (labmodel.isGraphVisible && labmodel.shouldShowGraph) { var graphColor = labmodel.getColor(recentLabResult.calculatedResultFlag??"N"); return CustomGraph( dataPoints: labmodel.filteredGraphValues, - // maxY: 100, - makeGraphBasedOnActualValue: true, leftLabelReservedSize: 40, leftLabelInterval: getInterval(labmodel), - maxY: (labmodel.maxY)+(getInterval(labmodel)??0)/5, + maxY: (labmodel.maxY)+(getInterval(labmodel)??0)/2, maxX: labmodel.filteredGraphValues.length.toDouble()-.75, leftLabelFormatter: (value) { return leftLabels(value.toStringAsFixed(2).tr()); @@ -279,11 +295,11 @@ class LabResultDetails extends StatelessWidget { scrollDirection: Axis.horizontal, height: 180.h); }else { - return labHistoryList(model, labmodel); + return labHistoryList(labmodel); } } - Widget labHistoryList(DateRangeSelectorRangeViewModel model, LabViewModel labmodel) { + Widget labHistoryList(LabViewModel labmodel) { return SizedBox( height: labmodel.filteredGraphValues.length<3?labmodel.filteredGraphValues.length*64:180.h, child: ListView.separated( @@ -315,4 +331,27 @@ class LabResultDetails extends StatelessWidget { if(maxX >100 && maxX < 200) return 30; return 50; } + + Widget getLabDescription(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + height: 98.h, + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + "What is this result?" + .needTranslation + .toText16(weight: FontWeight.w600, color: AppColors.textColor), + testDescription?.toText12( + fontWeight: FontWeight.w500, color: AppColors.textColorLight) ?? + SizedBox.shrink() + ], + )); + } } diff --git a/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart index 42f222b..398a809 100644 --- a/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart +++ b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart @@ -17,7 +17,7 @@ class DateRangeSelectorRangeViewModel extends ChangeNotifier { 'Nov', 'Dec' ]; - bool isGraphVisible = true; + Range? _currentlySelectedRange; Range? get currentlySelectedRange => _currentlySelectedRange; @@ -77,7 +77,6 @@ class DateRangeSelectorRangeViewModel extends ChangeNotifier { toDate = null; fromDate = null; currentlySelectedRange = null; - isGraphVisible = true; notifyListeners(); } @@ -86,8 +85,4 @@ class DateRangeSelectorRangeViewModel extends ChangeNotifier { notifyListeners(); } - alterGraphVisibility(){ - isGraphVisible = !isGraphVisible; - notifyListeners(); - } } From 61007302c9dd4fc0fd180bf51844f3250a0026e9 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Mon, 6 Oct 2025 16:21:05 +0300 Subject: [PATCH 38/43] appointment date filter handled --- lib/core/utils/date_util.dart | 8 ++++++++ .../my_appointments/my_appointments_view_model.dart | 8 +++++--- .../viewmodel/date_range_view_model.dart | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/core/utils/date_util.dart b/lib/core/utils/date_util.dart index 1eb3d9f..2fbbd09 100644 --- a/lib/core/utils/date_util.dart +++ b/lib/core/utils/date_util.dart @@ -485,3 +485,11 @@ class DateUtil { return ""; } } + + +extension OnlyDate on DateTime{ + + DateTime provideDateOnly(){ + return DateTime(this.year, month, day); + } +} \ 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 916327c..f278558 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -50,6 +50,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { void onTabChange(int index) { previouslySelectedTab = selectedTabIndex; selectedTabIndex = index; + start = null; + end = null; notifyListeners(); } @@ -451,12 +453,12 @@ class MyAppointmentsViewModel extends ChangeNotifier { filteredAppointmentList.add(element); } }); - filteredAppointmentList.addAll(sourceList); } else { filteredAppointmentList.clear(); sourceList.forEach((element) { try { - var dateTime = DateUtil.convertStringToDate(element.appointmentDate); + var dateTime = DateUtil.convertStringToDate(element.appointmentDate).provideDateOnly(); + if (start != null && end == null) { if (dateTime.isAtSameMomentAs(start)) { if (isUnderFilter(element)) { @@ -464,7 +466,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { } } } else if (start != null && end != null) { - if ((dateTime.isAfter(start)) && (dateTime.isBefore(end))) { + if ((dateTime.isAfter(start)) && ((dateTime.isBefore(end))||((dateTime.isAtSameMomentAs(end))))) { if (isUnderFilter(element)) { filteredAppointmentList.add(element); } diff --git a/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart index 42f222b..7bdb658 100644 --- a/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart +++ b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart @@ -1,5 +1,6 @@ import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/features/lab/models/Range.dart'; class DateRangeSelectorRangeViewModel extends ChangeNotifier { @@ -50,7 +51,7 @@ class DateRangeSelectorRangeViewModel extends ChangeNotifier { get getCurrentYear => DateTime.now().year; calculateDatesFromRange() { - _toDate = DateTime.now(); + _toDate = DateTime.now().provideDateOnly(); switch (_currentlySelectedRange) { case Range.WEEKLY: _fromDate = _toDate!.subtract(Duration(days: 7)); From f1a60a9dfb4ccff7840be4d771e698cfe558ff3d Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 6 Oct 2025 16:35:10 +0300 Subject: [PATCH 39/43] updates --- lib/core/utils/size_utils.dart | 8 +- lib/core/utils/utils.dart | 1 + .../my_appointments_view_model.dart | 1 + lib/main.dart | 81 +++++-------------- .../appointments/my_appointments_page.dart | 1 + lib/presentation/lab/lab_orders_page.dart | 2 +- 6 files changed, 28 insertions(+), 66 deletions(-) diff --git a/lib/core/utils/size_utils.dart b/lib/core/utils/size_utils.dart index 6ad1835..e569021 100644 --- a/lib/core/utils/size_utils.dart +++ b/lib/core/utils/size_utils.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; // These are the Viewport values of your Figma Design. // These are used in the code as a reference to create your UI Responsively. -const num FIGMA_DESIGN_WIDTH = 375; -const num FIGMA_DESIGN_HEIGHT = 667; +final num FIGMA_DESIGN_WIDTH = SizeUtils.width; +final num FIGMA_DESIGN_HEIGHT = SizeUtils.height; const num FIGMA_DESIGN_STATUS_BAR = 0; extension ResponsiveExtension on num { @@ -70,10 +70,10 @@ class SizeUtils { static late DeviceType deviceType; /// Device's Height - static late double height; + static double height = 667; /// Device's Width - static late double width; + static double width = 375; static void setScreenSize( BoxConstraints constraints, diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 2bff2ac..c9eef9d 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -757,4 +757,5 @@ class Utils { } return isHavePrivilege; } + } diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 916327c..0c9509a 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -153,6 +153,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsHistoryList.addAll(patientUpcomingAppointmentsHistoryList); patientAppointmentsHistoryList.addAll(patientArrivedAppointmentsHistoryList); filteredAppointmentList.addAll(patientAppointmentsHistoryList); + print('Upcoming Appointments: ${patientUpcomingAppointmentsHistoryList.length}'); print('Arrived Appointments: ${patientArrivedAppointmentsHistoryList.length}'); print('All Appointments: ${patientAppointmentsHistoryList.length}'); diff --git a/lib/main.dart b/lib/main.dart index ac11f8f..20507d0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -79,94 +79,53 @@ void main() async { fallbackLocale: Locale('en', 'US'), child: MultiProvider(providers: [ ChangeNotifierProvider( - create: (_) => LabViewModel( - labRepo: getIt(), - errorHandlerService: getIt(), - navigationService: getIt()), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => RadiologyViewModel( - radiologyRepo: getIt(), - errorHandlerService: getIt(), - ), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => PrescriptionsViewModel( - prescriptionsRepo: getIt(), - errorHandlerService: getIt(), - ), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => InsuranceViewModel( - insuranceRepo: getIt(), - errorHandlerService: getIt(), - ), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => MedicalFileViewModel( - medicalFileRepo: getIt(), - errorHandlerService: getIt(), - ), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => ProfileSettingsViewModel(), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => MyAppointmentsViewModel( - myAppointmentsRepo: getIt(), - errorHandlerService: getIt(), - appState: getIt(), - ), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => PayfortViewModel( - payfortRepo: getIt(), - errorHandlerService: getIt(), - ), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => HabibWalletViewModel( - habibWalletRepo: getIt(), - errorHandlerService: getIt(), - ), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => BookAppointmentsViewModel( - bookAppointmentsRepo: getIt(), - errorHandlerService: getIt(), - navigationService: getIt(), - myAppointmentsViewModel: getIt(), - locationUtils: getIt(), - ), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => ImmediateLiveCareViewModel( - immediateLiveCareRepo: getIt(), - errorHandlerService: getIt(), - navigationService: getIt(), - myAppointmentsViewModel: getIt(), - ), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => AuthenticationViewModel( - authenticationRepo: getIt(), - appState: getIt(), - dialogService: getIt(), - errorHandlerService: getIt(), - navigationService: getIt(), - cacheService: getIt(), - localAuthService: getIt(), - ), + create: (_) => getIt.get(), ), ChangeNotifierProvider( - create: (_) => AppointmentViaRegionViewmodel( - navigationService: getIt(), appState: getIt())), + create: (_) => getIt.get(), + ), ChangeNotifierProvider( - create: (_) => LabHistoryViewModel()), + create: (_) => getIt.get(), + ), ChangeNotifierProvider( - create: (_) => DateRangeSelectorRangeViewModel()) , + create: (_) => getIt.get(), + ), ChangeNotifierProvider( - create: (_) => DoctorFilterViewModel()) + create: (_) => getIt.get(), + ) ], child: MyApp()), ), ); diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index c939234..150f0bf 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -177,6 +177,7 @@ class _MyAppointmentsPageState extends State { separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ), + SizedBox(height: 24.h), ], ); } diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 4d7066f..882c737 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 16.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); Navigator.push( context, CustomPageRoute( page: LabResultByHospitals(), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().isNotEmpty ? model.uniqueTests.toList().length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.uniqueTests.toList().isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( appState: _appState, onTap: () { if (model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult(model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!)); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_hospital/LabResultByHospital.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 16.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); Navigator.push( context, CustomPageRoute( page: LabResultByHospitals(), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().isNotEmpty ? model.uniqueTests.toList().length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.uniqueTests.toList().isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( appState: _appState, onTap: () { if (model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult(model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!)); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file From 8ff5566f0be5274f66b93071310579e6ab6fc608 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 6 Oct 2025 17:16:24 +0300 Subject: [PATCH 40/43] dependencies update --- lib/core/dependencies.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index f2f6183..638d80f 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -8,6 +8,7 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/common/common_repo.dart'; +import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart'; 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_repo.dart'; @@ -34,6 +35,7 @@ import 'package:hmg_patient_app_new/services/firebase_service.dart'; import 'package:hmg_patient_app_new/services/localauth_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:local_auth/local_auth.dart'; import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -171,6 +173,14 @@ class AppDependencies { authenticationRepo: getIt(), cacheService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), localAuthService: getIt()), ); + getIt.registerLazySingleton( + () => DateRangeSelectorRangeViewModel(), + ); + + getIt.registerLazySingleton( + () => DoctorFilterViewModel(), + ); + // Screen-specific VMs → Factory // getIt.registerFactory( // () => BookAppointmentsViewModel( From d918b6e4645be019499913d1203b2115aee20bdd Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Tue, 7 Oct 2025 08:28:14 +0300 Subject: [PATCH 41/43] family screen & widgets --- assets/images/svg/active-check.svg | 3 + assets/images/svg/alert-square.svg | 3 + assets/images/svg/arrow-right.svg | 3 + assets/images/svg/delete_icon.svg | 3 + lib/core/api/api_client.dart | 62 +-- lib/core/api_consts.dart | 4 +- lib/core/app_assets.dart | 4 + lib/core/app_state.dart | 13 +- lib/core/dependencies.dart | 6 +- lib/core/enums.dart | 6 +- lib/core/utils/request_utils.dart | 2 +- lib/core/utils/utils.dart | 11 +- .../authentication/authentication_repo.dart | 98 +++-- .../authentication_view_model.dart | 85 ++-- .../authenticated_user_resp_model.dart | 307 +++++++------- .../medical_file/medical_file_repo.dart | 78 +++- .../medical_file/medical_file_view_model.dart | 329 +++++++-------- .../models/family_file_response_model.dart | 147 +++---- lib/presentation/home/landing_page.dart | 2 + .../medical_file/medical_file_page.dart | 283 +++++++------ lib/presentation/my_family/my_family.dart | 213 +++------- .../my_family/widget/family_cards.dart | 391 ++++++++++++++---- .../my_family/widget/my_family_sheet.dart | 3 +- .../profile_settings/profile_settings.dart | 142 ++++++- lib/services/dialog_service.dart | 47 +++ lib/widgets/appbar/collapsing_list_view.dart | 6 +- lib/widgets/buttons/custom_button.dart | 65 ++- lib/widgets/chip/custom_chip_widget.dart | 3 + .../family_files/family_file_add_widget.dart | 105 +++++ 29 files changed, 1480 insertions(+), 944 deletions(-) create mode 100644 assets/images/svg/active-check.svg create mode 100644 assets/images/svg/alert-square.svg create mode 100644 assets/images/svg/arrow-right.svg create mode 100644 assets/images/svg/delete_icon.svg create mode 100644 lib/widgets/family_files/family_file_add_widget.dart diff --git a/assets/images/svg/active-check.svg b/assets/images/svg/active-check.svg new file mode 100644 index 0000000..c94a3a0 --- /dev/null +++ b/assets/images/svg/active-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/alert-square.svg b/assets/images/svg/alert-square.svg new file mode 100644 index 0000000..87b52e3 --- /dev/null +++ b/assets/images/svg/alert-square.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/arrow-right.svg b/assets/images/svg/arrow-right.svg new file mode 100644 index 0000000..8818d72 --- /dev/null +++ b/assets/images/svg/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/delete_icon.svg b/assets/images/svg/delete_icon.svg new file mode 100644 index 0000000..2e37d0d --- /dev/null +++ b/assets/images/svg/delete_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index dc8b576..b1d1d87 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -18,25 +18,25 @@ abstract class ApiClient { static final NavigationService _navigationService = getIt.get(); Future post( - String endPoint, { - required Map body, - required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, - required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, - bool isAllowAny, - bool isExternal, - bool isRCService, - bool isPaymentServices, - bool bypassConnectionCheck, - }); + String endPoint, { + required Map body, + required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, + required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, + bool isAllowAny, + bool isExternal, + bool isRCService, + bool isPaymentServices, + bool bypassConnectionCheck, + }); Future get( - String endPoint, { - required Function(dynamic response, int statusCode) onSuccess, - required Function(String error, int statusCode) onFailure, - Map? queryParams, - bool isExternal, - bool isRCService, - }); + String endPoint, { + required Function(dynamic response, int statusCode) onSuccess, + required Function(String error, int statusCode) onFailure, + Map? queryParams, + bool isExternal, + bool isRCService, + }); String getSessionId(String id); @@ -87,16 +87,16 @@ class ApiClientImp implements ApiClient { @override post( - String endPoint, { - required Map body, - required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, - required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, - bool isAllowAny = false, - bool isExternal = false, - bool isRCService = false, - bool isPaymentServices = false, - bool bypassConnectionCheck = false, - }) async { + String endPoint, { + required Map body, + required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, + required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, + bool isAllowAny = false, + bool isExternal = false, + bool isRCService = false, + bool isPaymentServices = false, + bool bypassConnectionCheck = false, + }) async { String url; if (isExternal) { url = endPoint; @@ -322,10 +322,10 @@ class ApiClientImp implements ApiClient { @override get(String endPoint, {required Function(dynamic response, int statusCode) onSuccess, - required Function(String error, int statusCode) onFailure, - Map? queryParams, - bool isExternal = false, - bool isRCService = false}) async { + required Function(String error, int statusCode) onFailure, + Map? queryParams, + bool isExternal = false, + bool isRCService = false}) async { String url; if (isExternal) { url = endPoint; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 9f5abce..fd61480 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu 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 @@ -812,6 +812,8 @@ class ApiConsts { static final String checkActivationCodeForFamily = 'Services/Authentication.svc/REST/CheckActivationCodeForFamilyFile'; static final String getAllPendingRecordsByResponseId = 'Services/Authentication.svc/REST/GetAllPendingRecordsByResponseId'; static final String getAllSharedRecordsByStatus = 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatus'; + static final String removeFileFromFamilyMembers = 'Services/Authentication.svc/REST/ActiveDeactive_PatientFile'; + static final String acceptAndRejectFamilyFile = 'Services/Authentication.svc/REST/Update_FileStatus'; // static values for Api diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 513a52a..89f2e82 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -146,6 +146,8 @@ class AppAssets { static const String ic_low_result = '$svgBasePath/low_result.svg'; static const String ic_critical_low_result = '$svgBasePath/critical_low_result.svg'; static const String switch_user = '$svgBasePath/switch_user.svg'; + static const String activeCheck = '$svgBasePath/active-check.svg'; + static const String deleteIcon = '$svgBasePath/delete_icon.svg'; //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; @@ -157,6 +159,8 @@ class AppAssets { static const String feedback = '$svgBasePath/feedback.svg'; static const String news = '$svgBasePath/news.svg'; static const String heart = '$svgBasePath/heart.svg'; + static const String alertSquare = '$svgBasePath/alert-square.svg'; + static const String arrowRight = '$svgBasePath/arrow-right.svg'; // PNGS // static const String hmg_logo = '$pngBasePath/hmg_logo.png'; diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index 030cc36..404e22b 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -38,15 +38,15 @@ class AppState { AuthenticatedUser? _authenticatedChildUser; int? _superUserID; + bool isChildLoggedIn = false; - void setAuthenticatedUser(AuthenticatedUser authenticatedUser, {bool isFamily = false}) { + void setAuthenticatedUser(AuthenticatedUser? authenticatedUser, {bool isFamily = false}) { if (isFamily) { _authenticatedChildUser = authenticatedUser; } else { setIsAuthenticated = true; _authenticatedRootUser = authenticatedUser; } - } AuthenticatedUser? getAuthenticatedUser({bool isFamily = false}) { @@ -59,8 +59,12 @@ class AppState { int? get getSuperUserID => _superUserID; + bool get getIsChildLoggedIn => isChildLoggedIn; + set setSuperUserID(int? value) => _superUserID = value; + set setIsChildLoggedIn(bool value) => isChildLoggedIn = value; + String _userBloodGroup = ""; String get getUserBloodGroup => _userBloodGroup; @@ -103,8 +107,6 @@ class AppState { set setDeviceTypeID(v) => deviceTypeID = v; - - String _familyFileTokenID = ""; String get getFamilyFileTokenID => _familyFileTokenID; @@ -151,9 +153,8 @@ class AppState { } ///this will be called if there is any problem in getting the user location - void resetLocation(){ + void resetLocation() { userLong = 0.0; userLong = 0.0; } - } diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 1fe2317..a3bb4c7 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -97,11 +97,7 @@ class AppDependencies { // Global/shared VMs → LazySingleton getIt.registerLazySingleton( - () => LabViewModel( - labRepo: getIt(), - errorHandlerService: getIt(), - navigationService: getIt() - ), + () => LabViewModel(labRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt()), ); getIt.registerLazySingleton( diff --git a/lib/core/enums.dart b/lib/core/enums.dart index e94b3e2..4151bed 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -30,7 +30,7 @@ enum LoginTypeEnum { sms, whatsapp, face, fingerprint } enum AppEnvironmentTypeEnum { dev, uat, preProd, qa, staging, prod } -enum FamilyFileEnum { active, inactive, blocked, deleted, pending } +enum FamilyFileEnum { active, inactive, blocked, deleted, pending, rejected } extension CalenderExtension on CalenderEnum { int get toInt { @@ -152,6 +152,8 @@ extension FamilyFileEnumExtenshion on FamilyFileEnum { case FamilyFileEnum.pending: return 2; case FamilyFileEnum.inactive: + return 6; + case FamilyFileEnum.rejected: return 4; } } @@ -170,6 +172,8 @@ extension FamilyFileEnumExtenshion on FamilyFileEnum { return isArabic ? 'محذوف' : 'Deleted'; case FamilyFileEnum.pending: return isArabic ? 'قيد الانتظار' : 'Pending'; + case FamilyFileEnum.rejected: + return isArabic ? 'مرفوض' : 'Rejected'; } } diff --git a/lib/core/utils/request_utils.dart b/lib/core/utils/request_utils.dart index cd813ed..a4ea936 100644 --- a/lib/core/utils/request_utils.dart +++ b/lib/core/utils/request_utils.dart @@ -262,7 +262,7 @@ class RequestUtils { static Future getAddFamilyRequest({required String nationalIDorFile, required String mobileNo, required String countryCode}) async { FamilyFileRequest request = FamilyFileRequest(); - int? loginType = 0; // Default to National ID + int? loginType = 0; if (countryCode == CountryEnum.saudiArabia.countryCode || countryCode == '+966') { loginType = (nationalIDorFile.length == 10) ? 1 : 2; diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index efa01d0..bac2470 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -627,8 +627,17 @@ class Utils { double width = 24, double height = 24, BoxFit fit = BoxFit.cover, + double? border, + double? borderRadius, }) { - return Image.asset(icon, width: width, height: height, fit: fit); + return Container( + decoration: BoxDecoration( + border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null, + borderRadius: border != null ? BorderRadius.circular(borderRadius ?? 0) : null, + ), + child: Image.asset(icon, width: width, height: height, fit: fit), + ); + // return Image.asset(icon, width: width, height: height, fit: fit, ); } /// Widget to build an SVG from network diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index 96bdc0b..c034a69 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -206,38 +206,93 @@ class AuthenticationRepoImp implements AuthenticationRepo { int? patientID, int? loginType}) async { AppState appState = getIt.get(); + // if (isRegister) { + // newRequest["activationCode"] = activationCode ?? "0000"; + // newRequest["isSilentLogin"] = activationCode != null ? false : true; + // } else { + // newRequest.activationCode = activationCode ?? "0000"; + // newRequest.isSilentLogin = activationCode != null ? false : true; + // newRequest.projectOutSA = newRequest.zipCode == '966' ? false : true; + // newRequest.isDentalAllowedBackend = false; + // newRequest.forRegisteration = newRequest.isRegister ?? false; + // newRequest.isRegister = false; + // } + // Map familyRequest = {}; + // if (isFormFamilyFile) { + // AppState appState = getIt.get(); + // familyRequest = {}; + // familyRequest['PatientShareRequestID'] = patientShareRequestID; + // familyRequest['ResponseID'] = responseID; + // familyRequest['Status'] = 3; + // familyRequest["PatientID"] = appState.getAuthenticatedUser()!.patientId ?? 0; + // familyRequest["LogInTokenID"] = appState.getFamilyFileTokenID; + // familyRequest["activationCode"] = activationCode ?? "0000"; + // familyRequest["PatientMobileNumber"] = newRequest.patientMobileNumber; + // familyRequest["PatientIdentificationID"] = newRequest.patientIdentificationID; + // } + // Map switchRequest = {}; + // if (isSwitchUser) { + // switchRequest = newRequest.toJson(); + // + // switchRequest['PatientID'] = responseID; + // switchRequest['IsSilentLogin'] = true; + // switchRequest['LogInTokenID'] = null; + // switchRequest['SearchType'] = 2; + // if (loginType != 0) { + // switchRequest['SuperUser'] = patientID; + // switchRequest['DeviceToken'] = null; + // } else { + // switchRequest["LoginType"] = 2; + // } + // + // if (appState.getSuperUserID == responseID) { + // // switchRequest['LoginType'] = 3; + // switchRequest['PatientIdentificationID'] = ""; + // // switchRequest['ProjectOutSA'] = newRequest.zipCode == '966' ? false : true; + // switchRequest.remove('NationalID'); + // switchRequest.remove('isDentalAllowedBackend'); + // switchRequest.remove('ProjectOutSA'); + // switchRequest.remove('ForRegisteration'); + // appState.setSuperUserID = null; + // } + // } + if (isRegister) { newRequest["activationCode"] = activationCode ?? "0000"; - newRequest["isSilentLogin"] = activationCode != null ? false : true; + newRequest["isSilentLogin"] = activationCode == null; } else { newRequest.activationCode = activationCode ?? "0000"; - newRequest.isSilentLogin = activationCode != null ? false : true; - newRequest.projectOutSA = newRequest.zipCode == '966' ? false : true; + newRequest.isSilentLogin = activationCode == null; + newRequest.projectOutSA = newRequest.zipCode != '966'; newRequest.isDentalAllowedBackend = false; newRequest.forRegisteration = newRequest.isRegister ?? false; newRequest.isRegister = false; } + Map familyRequest = {}; if (isFormFamilyFile) { - AppState appState = getIt.get(); - familyRequest = {}; - familyRequest['PatientShareRequestID'] = patientShareRequestID; - familyRequest['ResponseID'] = responseID; - familyRequest['Status'] = 3; - familyRequest["PatientID"] = appState.getAuthenticatedUser()!.patientId ?? 0; - familyRequest["LogInTokenID"] = appState.getFamilyFileTokenID; - familyRequest["activationCode"] = activationCode ?? "0000"; - familyRequest["PatientMobileNumber"] = newRequest.patientMobileNumber; - familyRequest["PatientIdentificationID"] = newRequest.patientIdentificationID; + familyRequest = { + 'PatientShareRequestID': patientShareRequestID, + 'ResponseID': responseID, + 'Status': 3, + 'PatientID': appState.getAuthenticatedUser()?.patientId ?? 0, + 'LogInTokenID': appState.getFamilyFileTokenID, + 'activationCode': activationCode ?? "0000", + 'PatientMobileNumber': newRequest.patientMobileNumber, + 'PatientIdentificationID': newRequest.patientIdentificationID, + }; } + Map switchRequest = {}; if (isSwitchUser) { switchRequest = newRequest.toJson(); + switchRequest.addAll({ + 'PatientID': responseID, + 'IsSilentLogin': true, + 'LogInTokenID': null, + 'SearchType': 2, + }); - switchRequest['PatientID'] = responseID; - switchRequest['IsSilentLogin'] = true; - switchRequest['LogInTokenID'] = null; - switchRequest['SearchType'] = 2; if (loginType != 0) { switchRequest['SuperUser'] = patientID; switchRequest['DeviceToken'] = null; @@ -246,13 +301,8 @@ class AuthenticationRepoImp implements AuthenticationRepo { } if (appState.getSuperUserID == responseID) { - switchRequest['LoginType'] = 3; switchRequest['PatientIdentificationID'] = ""; - // switchRequest['ProjectOutSA'] = newRequest.zipCode == '966' ? false : true; - switchRequest.remove('NationalID'); - switchRequest.remove('isDentalAllowedBackend'); - switchRequest.remove('ProjectOutSA'); - switchRequest.remove('ForRegisteration'); + switchRequest.removeWhere((key, value) => ['NationalID', 'isDentalAllowedBackend', 'ProjectOutSA', 'ForRegisteration'].contains(key)); } } @@ -303,7 +353,6 @@ class AuthenticationRepoImp implements AuthenticationRepo { @override Future>> checkIfUserAgreed({required dynamic commonAuthanticatedRequest}) async { commonAuthanticatedRequest['Region'] = 1; - print("dsfsdd"); try { GenericApiModel? apiResponse; Failure? failure; @@ -338,7 +387,6 @@ class AuthenticationRepoImp implements AuthenticationRepo { @override Future>> getUserAgreementContent({required dynamic commonAuthanticatedRequest}) async { commonAuthanticatedRequest['Region'] = 1; - print("dsfsdd"); try { GenericApiModel? apiResponse; Failure? failure; diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index b3f9617..467a476 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -366,7 +366,7 @@ class AuthenticationViewModel extends ChangeNotifier { isForRegister: isForRegister, patientOutSA: isForRegister ? isPatientOutsideSA(request: payload) - : selectedCountrySignup.countryCode == CountryEnum.saudiArabia + : selectedCountrySignup.countryCode == CountryEnum.saudiArabia.countryCode ? false : true, payload: payload, @@ -374,9 +374,6 @@ class AuthenticationViewModel extends ChangeNotifier { isFormFamilyFile: isFormFamilyFile, responseID: responseID); - - - // TODO: GET APP SMS SIGNATURE HERE request.sMSSignature = await getSignature(); @@ -434,18 +431,17 @@ class AuthenticationViewModel extends ChangeNotifier { return isUserComingForRegister; } - Future checkActivationCode( - {required String? activationCode, - required OTPTypeEnum otpTypeEnum, - required Function(String? message) onWrongActivationCode, - Function()? onResendActivation, - bool isFormFamilyFile = false, - dynamic patientShareRequestID, - dynamic responseID, - bool isSwitchUser =false, - int? patientID, - - }) async { + Future checkActivationCode({ + required String? activationCode, + required OTPTypeEnum otpTypeEnum, + required Function(String? message) onWrongActivationCode, + Function()? onResendActivation, + bool isFormFamilyFile = false, + dynamic patientShareRequestID, + dynamic responseID, + bool isSwitchUser = false, + int? patientID, + }) async { bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1); final request = RequestUtils.getCommonRequestWelcome( @@ -508,15 +504,15 @@ class AuthenticationViewModel extends ChangeNotifier { }); } else { final resultEither = await _authenticationRepo.checkActivationCodeRepo( - newRequest: CheckActivationCodeRegisterReq.fromJson(request), - activationCode: activationCode, - isRegister: false, - isFormFamilyFile: isFormFamilyFile, - patientShareRequestID: patientShareRequestID, - responseID: responseID, - isSwitchUser: isSwitchUser, - patientID: patientID, - loginType: _appState.getSuperUserID != null ? 0 : 2, + newRequest: CheckActivationCodeRegisterReq.fromJson(request), + activationCode: activationCode, + isRegister: false, + isFormFamilyFile: isFormFamilyFile, + patientShareRequestID: patientShareRequestID, + responseID: responseID, + isSwitchUser: isSwitchUser, + patientID: patientID, + loginType: _appState.getSuperUserID != null ? 0 : 2, ); resultEither.fold( @@ -531,8 +527,6 @@ class AuthenticationViewModel extends ChangeNotifier { LoaderBottomSheet.hideLoader(); await _dialogService.showCommonBottomSheetWithoutH(message: failure.message, label: LocaleKeys.notice.tr(), onOkPressed: () {}); }), (apiResponse) async { - print("API Response Data: ${apiResponse.data}"); - final activation = CheckActivationCode.fromJson(apiResponse.data as Map); if (activation.errorCode == '699') { @@ -551,17 +545,27 @@ class AuthenticationViewModel extends ChangeNotifier { return; } else { if (isFormFamilyFile) { - await navigateToFamilyFilePage(); - // _dialogService.showCommonBottomSheetWithoutH( - // message: "Family File Added Successfully", - // onOkPressed: () async { - // print("navigating to family file page"); - // - // }); + // await navigateToFamilyFilePage(); + MedicalFileViewModel medicalVm = getIt(); + if (!_appState.getIsChildLoggedIn) { + await medicalVm.getFamilyFiles(status: 0); + await medicalVm.getAllPendingRecordsByResponseId(); + _navigationService.popUntilNamed(AppRoutes.medicalFilePage); + } } else { if (activation.list != null && activation.list!.isNotEmpty) { - if(isSwitchUser){ - _appState.setSuperUserID = _appState.getAuthenticatedUser()?.patientId; + if (isSwitchUser) { + if (_appState.getIsChildLoggedIn) { + _appState.setSuperUserID = null; + _appState.setIsChildLoggedIn = false; + activation.list!.first.isParentUser = true; + } else { + _appState.setSuperUserID = _appState.getAuthenticatedUser()?.patientId; + _appState.setIsChildLoggedIn = true; + activation.list!.first.isParentUser = false; + } + } else { + activation.list!.first.isParentUser = true; } _appState.setAuthenticatedUser(activation.list!.first); _appState.setPrivilegeModelList(activation.list!.first.listPrivilege!); @@ -576,7 +580,14 @@ class AuthenticationViewModel extends ChangeNotifier { _appState.getSelectDeviceByImeiRespModelElement!.logInType = loginTypeEnum.toInt; } LoaderBottomSheet.hideLoader(); - insertPatientIMEIData(loginTypeEnum.toInt); +// + if (!isSwitchUser && !_appState.getIsChildLoggedIn) { + MedicalFileViewModel medicalVm = getIt(); + insertPatientIMEIData(loginTypeEnum.toInt); + await medicalVm.getFamilyFiles(status: 0); //TODO: Remove status: 1 by Aamir Need to Discuss With Sultan + // medicalVm.getAllPendingRecordsByResponseId(); + } + await clearDefaultInputValues(); if (isUserAgreedBefore) { navigateToHomeScreen(); diff --git a/lib/features/authentication/models/resp_models/authenticated_user_resp_model.dart b/lib/features/authentication/models/resp_models/authenticated_user_resp_model.dart index e6a8e17..8209a0f 100644 --- a/lib/features/authentication/models/resp_models/authenticated_user_resp_model.dart +++ b/lib/features/authentication/models/resp_models/authenticated_user_resp_model.dart @@ -70,6 +70,8 @@ class AuthenticatedUser { dynamic authenticatedUserPatientType; dynamic authenticatedUserStatus; int? superUser; + bool? isParentUser; + AuthenticatedUser({ this.setupId, this.patientType, @@ -139,7 +141,8 @@ class AuthenticatedUser { this.authenticatedUserPatientPayType, this.authenticatedUserPatientType, this.authenticatedUserStatus, - this.superUser + this.superUser, + this.isParentUser, }); factory AuthenticatedUser.fromRawJson(String str) => AuthenticatedUser.fromJson(json.decode(str)); @@ -147,148 +150,150 @@ class AuthenticatedUser { String toRawJson() => json.encode(toJson()); factory AuthenticatedUser.fromJson(Map json) => AuthenticatedUser( - setupId: json["SetupID"], - patientType: json["PatientType"], - patientId: json["PatientID"], - firstName: json["FirstName"], - middleName: json["MiddleName"], - lastName: json["LastName"], - firstNameN: json["FirstNameN"], - middleNameN: json["MiddleNameN"], - lastNameN: json["LastNameN"], - relationshipId: json["RelationshipID"], - gender: json["Gender"], - dateofBirth: json["DateofBirth"], - dateofBirthN: json["DateofBirthN"], - nationalityId: json["NationalityID"], - phoneResi: json["PhoneResi"], - phoneOffice: json["PhoneOffice"], - mobileNumber: json["MobileNumber"], - faxNumber: json["FaxNumber"], - emailAddress: json["EmailAddress"], - bloodGroup: json["BloodGroup"], - rhFactor: json["RHFactor"], - isEmailAlertRequired: json["IsEmailAlertRequired"], - isSmsAlertRequired: json["IsSMSAlertRequired"], - preferredLanguage: json["PreferredLanguage"], - isPrivilegedMember: json["IsPrivilegedMember"], - memberId: json["MemberID"], - expiryDate: json["ExpiryDate"], - isHmgEmployee: json["IsHmgEmployee"], - employeeId: json["EmployeeID"], - emergencyContactName: json["EmergencyContactName"], - emergencyContactNo: json["EmergencyContactNo"], - patientPayType: json["PatientPayType"], - dhccPatientRefId: json["DHCCPatientRefID"], - isPatientDummy: json["IsPatientDummy"], - status: json["Status"], - isStatusCleared: json["IsStatusCleared"], - patientIdentificationType: json["PatientIdentificationType"], - patientIdentificationNo: json["PatientIdentificationNo"], - projectId: json["ProjectID"], - infoSourceId: json["InfoSourceID"], - address: json["Address"], - age: json["Age"], - ageDesc: json["AgeDesc"], - areaId: json["AreaID"], - crsVerificationStatus: json["CRSVerificationStatus"], - crsVerificationStatusDesc: json["CRSVerificationStatusDesc"], - crsVerificationStatusDescN: json["CRSVerificationStatusDescN"], - createdBy: json["CreatedBy"], - genderDescription: json["GenderDescription"], - healthIdFromNhicViaVida: json["HealthIDFromNHICViaVida"], - ir: json["IR"], - isoCityId: json["ISOCityID"], - isoCountryId: json["ISOCountryID"], - isVerfiedFromNhic: json["IsVerfiedFromNHIC"], - listPrivilege: json["ListPrivilege"] == null ? [] : List.from(json["ListPrivilege"]!.map((x) => ListPrivilege.fromJson(x))), - marital: json["Marital"], - occupationId: json["OccupationID"], - outSa: json["OutSA"], - poBox: json["POBox"], - receiveHealthSummaryReport: json["ReceiveHealthSummaryReport"], - sourceType: json["SourceType"], - strDateofBirth: json["StrDateofBirth"], - tempAddress: json["TempAddress"], - zipCode: json["ZipCode"], - eHealthIdField: json["eHealthIDField"], - authenticatedUserPatientPayType: json["patientPayType"], - authenticatedUserPatientType: json["patientType"], - authenticatedUserStatus: json["status"], - superUser: json["superUser"], - ); + setupId: json["SetupID"], + patientType: json["PatientType"], + patientId: json["PatientID"], + firstName: json["FirstName"], + middleName: json["MiddleName"], + lastName: json["LastName"], + firstNameN: json["FirstNameN"], + middleNameN: json["MiddleNameN"], + lastNameN: json["LastNameN"], + relationshipId: json["RelationshipID"], + gender: json["Gender"], + dateofBirth: json["DateofBirth"], + dateofBirthN: json["DateofBirthN"], + nationalityId: json["NationalityID"], + phoneResi: json["PhoneResi"], + phoneOffice: json["PhoneOffice"], + mobileNumber: json["MobileNumber"], + faxNumber: json["FaxNumber"], + emailAddress: json["EmailAddress"], + bloodGroup: json["BloodGroup"], + rhFactor: json["RHFactor"], + isEmailAlertRequired: json["IsEmailAlertRequired"], + isSmsAlertRequired: json["IsSMSAlertRequired"], + preferredLanguage: json["PreferredLanguage"], + isPrivilegedMember: json["IsPrivilegedMember"], + memberId: json["MemberID"], + expiryDate: json["ExpiryDate"], + isHmgEmployee: json["IsHmgEmployee"], + employeeId: json["EmployeeID"], + emergencyContactName: json["EmergencyContactName"], + emergencyContactNo: json["EmergencyContactNo"], + patientPayType: json["PatientPayType"], + dhccPatientRefId: json["DHCCPatientRefID"], + isPatientDummy: json["IsPatientDummy"], + status: json["Status"], + isStatusCleared: json["IsStatusCleared"], + patientIdentificationType: json["PatientIdentificationType"], + patientIdentificationNo: json["PatientIdentificationNo"], + projectId: json["ProjectID"], + infoSourceId: json["InfoSourceID"], + address: json["Address"], + age: json["Age"], + ageDesc: json["AgeDesc"], + areaId: json["AreaID"], + crsVerificationStatus: json["CRSVerificationStatus"], + crsVerificationStatusDesc: json["CRSVerificationStatusDesc"], + crsVerificationStatusDescN: json["CRSVerificationStatusDescN"], + createdBy: json["CreatedBy"], + genderDescription: json["GenderDescription"], + healthIdFromNhicViaVida: json["HealthIDFromNHICViaVida"], + ir: json["IR"], + isoCityId: json["ISOCityID"], + isoCountryId: json["ISOCountryID"], + isVerfiedFromNhic: json["IsVerfiedFromNHIC"], + listPrivilege: json["ListPrivilege"] == null ? [] : List.from(json["ListPrivilege"]!.map((x) => ListPrivilege.fromJson(x))), + marital: json["Marital"], + occupationId: json["OccupationID"], + outSa: json["OutSA"], + poBox: json["POBox"], + receiveHealthSummaryReport: json["ReceiveHealthSummaryReport"], + sourceType: json["SourceType"], + strDateofBirth: json["StrDateofBirth"], + tempAddress: json["TempAddress"], + zipCode: json["ZipCode"], + eHealthIdField: json["eHealthIDField"], + authenticatedUserPatientPayType: json["patientPayType"], + authenticatedUserPatientType: json["patientType"], + authenticatedUserStatus: json["status"], + superUser: json["superUser"], + isParentUser: json["isParentUser"] ?? false, + ); Map toJson() => { - "SetupID": setupId, - "PatientType": patientType, - "PatientID": patientId, - "FirstName": firstName, - "MiddleName": middleName, - "LastName": lastName, - "FirstNameN": firstNameN, - "MiddleNameN": middleNameN, - "LastNameN": lastNameN, - "RelationshipID": relationshipId, - "Gender": gender, - "DateofBirth": dateofBirth, - "DateofBirthN": dateofBirthN, - "NationalityID": nationalityId, - "PhoneResi": phoneResi, - "PhoneOffice": phoneOffice, - "MobileNumber": mobileNumber, - "FaxNumber": faxNumber, - "EmailAddress": emailAddress, - "BloodGroup": bloodGroup, - "RHFactor": rhFactor, - "IsEmailAlertRequired": isEmailAlertRequired, - "IsSMSAlertRequired": isSmsAlertRequired, - "PreferredLanguage": preferredLanguage, - "IsPrivilegedMember": isPrivilegedMember, - "MemberID": memberId, - "ExpiryDate": expiryDate, - "IsHmgEmployee": isHmgEmployee, - "EmployeeID": employeeId, - "EmergencyContactName": emergencyContactName, - "EmergencyContactNo": emergencyContactNo, - "PatientPayType": patientPayType, - "DHCCPatientRefID": dhccPatientRefId, - "IsPatientDummy": isPatientDummy, - "Status": status, - "IsStatusCleared": isStatusCleared, - "PatientIdentificationType": patientIdentificationType, - "PatientIdentificationNo": patientIdentificationNo, - "ProjectID": projectId, - "InfoSourceID": infoSourceId, - "Address": address, - "Age": age, - "AgeDesc": ageDesc, - "AreaID": areaId, - "CRSVerificationStatus": crsVerificationStatus, - "CRSVerificationStatusDesc": crsVerificationStatusDesc, - "CRSVerificationStatusDescN": crsVerificationStatusDescN, - "CreatedBy": createdBy, - "GenderDescription": genderDescription, - "HealthIDFromNHICViaVida": healthIdFromNhicViaVida, - "IR": ir, - "ISOCityID": isoCityId, - "ISOCountryID": isoCountryId, - "IsVerfiedFromNHIC": isVerfiedFromNhic, - "ListPrivilege": listPrivilege == null ? [] : List.from(listPrivilege!.map((x) => x.toJson())), - "Marital": marital, - "OccupationID": occupationId, - "OutSA": outSa, - "POBox": poBox, - "ReceiveHealthSummaryReport": receiveHealthSummaryReport, - "SourceType": sourceType, - "StrDateofBirth": strDateofBirth, - "TempAddress": tempAddress, - "ZipCode": zipCode, - "eHealthIDField": eHealthIdField, - "patientPayType": authenticatedUserPatientPayType, - "patientType": authenticatedUserPatientType, - "status": authenticatedUserStatus, - "superUser": superUser, - }; + "SetupID": setupId, + "PatientType": patientType, + "PatientID": patientId, + "FirstName": firstName, + "MiddleName": middleName, + "LastName": lastName, + "FirstNameN": firstNameN, + "MiddleNameN": middleNameN, + "LastNameN": lastNameN, + "RelationshipID": relationshipId, + "Gender": gender, + "DateofBirth": dateofBirth, + "DateofBirthN": dateofBirthN, + "NationalityID": nationalityId, + "PhoneResi": phoneResi, + "PhoneOffice": phoneOffice, + "MobileNumber": mobileNumber, + "FaxNumber": faxNumber, + "EmailAddress": emailAddress, + "BloodGroup": bloodGroup, + "RHFactor": rhFactor, + "IsEmailAlertRequired": isEmailAlertRequired, + "IsSMSAlertRequired": isSmsAlertRequired, + "PreferredLanguage": preferredLanguage, + "IsPrivilegedMember": isPrivilegedMember, + "MemberID": memberId, + "ExpiryDate": expiryDate, + "IsHmgEmployee": isHmgEmployee, + "EmployeeID": employeeId, + "EmergencyContactName": emergencyContactName, + "EmergencyContactNo": emergencyContactNo, + "PatientPayType": patientPayType, + "DHCCPatientRefID": dhccPatientRefId, + "IsPatientDummy": isPatientDummy, + "Status": status, + "IsStatusCleared": isStatusCleared, + "PatientIdentificationType": patientIdentificationType, + "PatientIdentificationNo": patientIdentificationNo, + "ProjectID": projectId, + "InfoSourceID": infoSourceId, + "Address": address, + "Age": age, + "AgeDesc": ageDesc, + "AreaID": areaId, + "CRSVerificationStatus": crsVerificationStatus, + "CRSVerificationStatusDesc": crsVerificationStatusDesc, + "CRSVerificationStatusDescN": crsVerificationStatusDescN, + "CreatedBy": createdBy, + "GenderDescription": genderDescription, + "HealthIDFromNHICViaVida": healthIdFromNhicViaVida, + "IR": ir, + "ISOCityID": isoCityId, + "ISOCountryID": isoCountryId, + "IsVerfiedFromNHIC": isVerfiedFromNhic, + "ListPrivilege": listPrivilege == null ? [] : List.from(listPrivilege!.map((x) => x.toJson())), + "Marital": marital, + "OccupationID": occupationId, + "OutSA": outSa, + "POBox": poBox, + "ReceiveHealthSummaryReport": receiveHealthSummaryReport, + "SourceType": sourceType, + "StrDateofBirth": strDateofBirth, + "TempAddress": tempAddress, + "ZipCode": zipCode, + "eHealthIDField": eHealthIdField, + "patientPayType": authenticatedUserPatientPayType, + "patientType": authenticatedUserPatientType, + "status": authenticatedUserStatus, + "superUser": superUser, + "isParentUser": isParentUser, + }; } class ListPrivilege { @@ -309,16 +314,16 @@ class ListPrivilege { String toRawJson() => json.encode(toJson()); factory ListPrivilege.fromJson(Map json) => ListPrivilege( - id: json["ID"], - serviceName: json["ServiceName"], - previlege: json["Previlege"], - region: json["Region"], - ); + id: json["ID"], + serviceName: json["ServiceName"], + previlege: json["Previlege"], + region: json["Region"], + ); Map toJson() => { - "ID": id, - "ServiceName": serviceName, - "Previlege": previlege, - "Region": region, - }; + "ID": id, + "ServiceName": serviceName, + "Previlege": previlege, + "Region": region, + }; } diff --git a/lib/features/medical_file/medical_file_repo.dart b/lib/features/medical_file/medical_file_repo.dart index 9c18696..994f9f9 100644 --- a/lib/features/medical_file/medical_file_repo.dart +++ b/lib/features/medical_file/medical_file_repo.dart @@ -29,6 +29,10 @@ abstract class MedicalFileRepo { Future>>> getAllPendingRecordsByResponseId({required Map request}); Future>> addFamilyFile({required dynamic request}); + + Future>> removeFamilyFile({required int? id}); + + Future>> acceptRejectFamilyFile({required int? id, required int? status}); } class MedicalFileRepoImp implements MedicalFileRepo { @@ -328,10 +332,6 @@ class MedicalFileRepoImp implements MedicalFileRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { final list = response['GetAllPendingRecordsList']; - // if (list == null || list.isEmpty) { - // throw Exception("lab list is empty"); - // } - final familyLists = list.map((item) => FamilyFileResponseModelLists.fromJson(item as Map)).toList().cast(); apiResponse = GenericApiModel>( @@ -384,4 +384,74 @@ class MedicalFileRepoImp implements MedicalFileRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> removeFamilyFile({required int? id}) async { + Map request = {}; + request["ID"] = id; + request['IsActive'] = false; + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.removeFileFromFamilyMembers, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } 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())); + } + } + + @override + Future>> acceptRejectFamilyFile({required int? id, required int? status}) async { + Map request = {}; + request["ID"] = id; + request['Status'] = status; + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.acceptAndRejectFamilyFile, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } 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/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 64075b4..df8027f 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; @@ -83,7 +84,7 @@ class MedicalFileViewModel extends ChangeNotifier { void onFamilyFileTabChange(int index) { setSelectedFamilyFileTabIndex = index; if (index == 1) { - getAllPendingRecordsByResponseId(); + // getAllPendingRecordsByResponseId(); } notifyListeners(); } @@ -270,148 +271,50 @@ class MedicalFileViewModel extends ChangeNotifier { if (apiResponse.messageStatus == 2) { _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { - patientFamilyFiles = apiResponse.data!; if (apiResponse.data != null) { - patientFamilyFiles.insert( - 0, - FamilyFileResponseModelLists( - patientId: _appState.getAuthenticatedUser()!.patientId, - patientName: '${_appState.getAuthenticatedUser()!.firstName!} ${_appState.getAuthenticatedUser()!.lastName!}', - isActive: true, - gender: _appState.getAuthenticatedUser()!.gender!, - responseId: _appState.getAuthenticatedUser()!.patientId, - age: _appState.getAuthenticatedUser()!.age, - mobileNumber: _appState.getAuthenticatedUser()!.mobileNumber, - patientIdenficationNumber: _appState.getAuthenticatedUser()!.patientIdentificationNo, - emaiLAddress: _appState.getAuthenticatedUser()!.emailAddress, - genderDescription: _appState.getAuthenticatedUser()!.genderDescription, - ), + // Add current user as the first active family file + final currentUser = _appState.getAuthenticatedUser()!; + final currentUserFamilyFile = FamilyFileResponseModelLists( + patientId: currentUser.patientId, + patientName: '${currentUser.firstName!} ${currentUser.lastName!}', + isActive: true, + gender: currentUser.gender!, + responseId: currentUser.patientId, + age: currentUser.age, + mobileNumber: currentUser.mobileNumber, + patientIdenficationNumber: currentUser.patientIdentificationNo, + emaiLAddress: currentUser.emailAddress, + genderDescription: currentUser.genderDescription, ); - // final List activeFamilyFiles = []; - // final List tempPendingFamilyFiles = []; - // for (var element in apiResponse.data!) { - // if (element.status != null && element.status == FamilyFileEnum.active.toInt) { - // activeFamilyFiles.add(FamilyFileResponseModelLists( - // patientId: element.patientId, - // patientName: element.patientName!, - // isActive: element.status == FamilyFileEnum.active.toInt ? true : false, - // gender: element.gender!, - // responseId: element.patientId, - // mobileNumber: element.mobileNumber, - // age: element.age, - // patientIdenficationNumber: element.patientIdenficationNumber, - // relationship: element.relationship, - // relationshipId: element.relationshipId, - // relationshipN: element.relationshipN, - // status: element.status, - // statusDescription: element.statusDescription, - // createdOn: element.createdOn, - // editedOn: element.editedOn, - // patientDataVerified: element.patientDataVerified, - // regionId: element.regionId, - // familyRegionId: element.familyRegionId, - // genderDescription: element.genderDescription, - // genderImage: element.genderImage, - // emaiLAddress: element.emaiLAddress)); - // } - // - // if (element.status != null && element.status == FamilyFileEnum.pending.toInt) { - // tempPendingFamilyFiles.add(FamilyFileResponseModelLists( - // patientId: element.patientId, - // patientName: element.patientName!, - // isActive: element.status == FamilyFileEnum.active.toInt ? true : false, - // gender: element.gender!, - // responseId: element.patientId, - // mobileNumber: element.mobileNumber, - // age: element.age, - // patientIdenficationNumber: element.patientIdenficationNumber, - // relationship: element.relationship, - // relationshipId: element.relationshipId, - // relationshipN: element.relationshipN, - // status: element.status, - // statusDescription: element.statusDescription, - // createdOn: element.createdOn, - // editedOn: element.editedOn, - // patientDataVerified: element.patientDataVerified, - // regionId: element.regionId, - // familyRegionId: element.familyRegionId, - // genderDescription: element.genderDescription, - // genderImage: element.genderImage, - // emaiLAddress: element.emaiLAddress)); - // } - // } - // patientFamilyFiles.addAll(activeFamilyFiles.where((element) => !patientFamilyFiles.any((e) => e.patientId == element.patientId))); - // pendingFamilyFiles.addAll(tempPendingFamilyFiles); + // Clear and start fresh with current user + patientFamilyFiles.clear(); + patientFamilyFiles.add(currentUserFamilyFile); final List activeFamilyFiles = []; - final List tempPendingFamilyFiles = []; - // final Set pendingIds = {}; - - // for (var element in apiResponse.data!) { - // if (element.status != null && element.status == FamilyFileEnum.pending.toInt) { - // tempPendingFamilyFiles.add(FamilyFileResponseModelLists( - // patientId: element.patientId, - // patientName: element.patientName!, - // isActive: false, - // gender: element.gender!, - // responseId: element.patientId, - // mobileNumber: element.mobileNumber, - // age: element.age, - // patientIdenficationNumber: element.patientIdenficationNumber, - // relationship: element.relationship, - // relationshipId: element.relationshipId, - // relationshipN: element.relationshipN, - // status: element.status, - // statusDescription: element.statusDescription, - // createdOn: element.createdOn, - // editedOn: element.editedOn, - // patientDataVerified: element.patientDataVerified, - // regionId: element.regionId, - // familyRegionId: element.familyRegionId, - // genderDescription: element.genderDescription, - // genderImage: element.genderImage, - // emaiLAddress: element.emaiLAddress)); - // } else if (element.status != null && element.status == FamilyFileEnum.active.toInt) { - // activeFamilyFiles.add(FamilyFileResponseModelLists( - // patientId: element.patientId, - // patientName: element.patientName!, - // isActive: element.status == FamilyFileEnum.active.toInt ? true : false, - // gender: element.gender!, - // responseId: element.patientId, - // mobileNumber: element.mobileNumber, - // age: element.age, - // patientIdenficationNumber: element.patientIdenficationNumber, - // relationship: element.relationship, - // relationshipId: element.relationshipId, - // relationshipN: element.relationshipN, - // status: element.status, - // statusDescription: element.statusDescription, - // createdOn: element.createdOn, - // editedOn: element.editedOn, - // patientDataVerified: element.patientDataVerified, - // regionId: element.regionId, - // familyRegionId: element.familyRegionId, - // genderDescription: element.genderDescription, - // genderImage: element.genderImage, - // emaiLAddress: element.emaiLAddress)); - // } - // } + final List pendingFamilyFiles = []; for (var element in apiResponse.data!) { - if (element.status == null) continue; + if (element.status == null) { + continue; + } - final isPending = element.status == FamilyFileEnum.pending.toInt; + final isPending = element.status == FamilyFileEnum.pending.toInt || element.status == FamilyFileEnum.rejected.toInt; final isActive = element.status == FamilyFileEnum.active.toInt; - if (!isPending && !isActive) continue; + print("====== Element Status: ${element.status}, isPending: $isPending, isActive: $isActive ============"); + + if (!isPending && !isActive) { + continue; + } final familyFile = FamilyFileResponseModelLists( + id: element.id, patientId: element.patientId, patientName: element.patientName!, - isActive: isActive, + isActive: element.isActive, gender: element.gender!, - responseId: element.patientId, + responseId: element.responseId, mobileNumber: element.mobileNumber, age: element.age, patientIdenficationNumber: element.patientIdenficationNumber, @@ -431,14 +334,22 @@ class MedicalFileViewModel extends ChangeNotifier { ); if (isPending) { - tempPendingFamilyFiles.add(familyFile); - } else if (isActive) { + familyFile.isRequestFromMySide = true; + pendingFamilyFiles.add(familyFile); + } + if (isActive) { activeFamilyFiles.add(familyFile); } } - patientFamilyFiles.addAll(activeFamilyFiles.where((element) => patientFamilyFiles.every((e) => e.patientId != element.patientId || e.status != FamilyFileEnum.active.toInt))); - pendingFamilyFiles.addAll(tempPendingFamilyFiles.where((element) => pendingFamilyFiles.every((e) => e.patientId != element.patientId))); + for (var activeFile in activeFamilyFiles) { + if (!patientFamilyFiles.any((e) => e.responseId == activeFile.responseId)) { + patientFamilyFiles.add(activeFile); + } + } + + this.pendingFamilyFiles.clear(); + this.pendingFamilyFiles.addAll(pendingFamilyFiles); } notifyListeners(); @@ -463,11 +374,12 @@ class MedicalFileViewModel extends ChangeNotifier { if (apiResponse.data != null) { final List tempPendingFamilyFiles = []; for (var element in apiResponse.data!) { - if (element.status != null && element.status == FamilyFileEnum.pending.toInt) { + if (element.status != null && element.status == FamilyFileEnum.pending.toInt || element.status == FamilyFileEnum.active.toInt) { tempPendingFamilyFiles.add(FamilyFileResponseModelLists( + id: element.id, patientId: element.patientId, patientName: element.patientName!, - isActive: element.status == FamilyFileEnum.active.toInt ? true : false, + isActive: element.status, gender: element.gender, responseId: element.patientId, mobileNumber: element.mobileNumber, @@ -488,7 +400,11 @@ class MedicalFileViewModel extends ChangeNotifier { emaiLAddress: element.emaiLAddress)); } } + // pendingFamilyFiles.addAll(tempPendingFamilyFiles.where((element) => !pendingFamilyFiles.any((e) => e.responseId == element.responseId))); pendingFamilyFiles.addAll(tempPendingFamilyFiles.where((element) => !pendingFamilyFiles.any((e) => e.patientId == element.patientId))); + + print("====== Pending Family Length: ${pendingFamilyFiles.length} ============"); + print("====== Pending Family Files: ${jsonEncode(pendingFamilyFiles)} ============"); } notifyListeners(); } @@ -496,42 +412,6 @@ class MedicalFileViewModel extends ChangeNotifier { ); } - // Future switchFamilyFiles({Function(dynamic)? onSuccess, Function(String)? onError}) async { - // // final result = await medicalFileRepo.getPatientFamilyFiles(); - // final result = await medicalFileRepo.getPatientFamilyFiles(3, _appState.superUserID != null ? _appState.superUserID! : _appState.getAuthenticatedUser()!.patientId!); - // - // result.fold( - // (failure) async => await errorHandlerService.handleError( - // failure: failure, - // onOkPressed: () { - // onError!(failure.message); - // }, - // ), - // (apiResponse) { - // if (apiResponse.messageStatus == 2) { - // _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage!, onOkPressed: () {}); - // } else if (apiResponse.messageStatus == 1) { - // patientFamilyFiles = apiResponse.data!; - // patientFamilyFiles.insert( - // 0, - // FamilyFileResponseModelLists( - // patientId: _appState.getAuthenticatedUser()!.patientId, - // patientName: '${_appState.getAuthenticatedUser()!.firstName!} ${_appState.getAuthenticatedUser()!.lastName!}', - // isActive: true, - // gender: _appState.getAuthenticatedUser()!.gender!, - // age: _appState.getAuthenticatedUser()!.age, - // mobileNumber: _appState.getAuthenticatedUser()!.mobileNumber, - // responseId: _appState.getAuthenticatedUser()!.patientId), - // ); - // notifyListeners(); - // if (onSuccess != null) { - // onSuccess(apiResponse); - // } - // } - // }, - // ); - // } - Future switchFamilyFiles({Function(dynamic)? onSuccess, int? responseID, int? patientID, String? phoneNumber, Function(String)? onError}) async { authVM.phoneNumberController.text = phoneNumber!.startsWith("0") ? phoneNumber.replaceFirst("0", "") : phoneNumber; @@ -555,34 +435,107 @@ class MedicalFileViewModel extends ChangeNotifier { final resultEither = await medicalFileRepo.addFamilyFile(request: request.toJson()); resultEither.fold((failure) async => await errorHandlerService.handleError(failure: failure), (apiResponse) async { - if (apiResponse != null && apiResponse.data != null) { - request.isPatientExcluded = apiResponse.data["IsPatientExcluded"]; - request.responseID = apiResponse.data["ReponseID"]; + if (apiResponse.messageStatus == 2) { LoaderBottomSheet.hideLoader(); - _dialogService.showExceptionBottomSheet( - message: apiResponse.data['Message'], + _dialogService.showErrorBottomSheet( + message: apiResponse.errorMessage!, onOkPressed: () { - LoaderBottomSheet.showLoader(); - authVM.sendActivationCode( - otpTypeEnum: otpTypeEnum, - nationalIdOrFileNumber: request.sharedPatientIdentificationId!, - phoneNumber: request.sharedPatientMobileNumber!, - isForRegister: false, - isExcludedUser: apiResponse.data['IsPatientExcluded'], - responseID: apiResponse.data["ReponseID"], - isFormFamilyFile: true); - }, - onCancelPressed: () { navigationService.pop(); }); + } else if (apiResponse.messageStatus == 1) { + if (apiResponse.data != null) { + request.isPatientExcluded = apiResponse.data["IsPatientExcluded"]; + request.responseID = apiResponse.data["ReponseID"]; + LoaderBottomSheet.hideLoader(); + _dialogService.showExceptionBottomSheet( + message: apiResponse.data["Message"], + onOkPressed: () { + LoaderBottomSheet.showLoader(); + authVM.sendActivationCode( + otpTypeEnum: otpTypeEnum, + nationalIdOrFileNumber: request.sharedPatientIdentificationId!, + phoneNumber: request.sharedPatientMobileNumber!, + isForRegister: false, + isExcludedUser: apiResponse.data['IsPatientExcluded'], + responseID: apiResponse.data["ReponseID"], + isFormFamilyFile: true); + }, + onCancelPressed: () { + navigationService.pop(); + }); + } } }); } Future handleFamilyFileRequestOTPVerification() async { LoaderBottomSheet.showLoader(); - await getFamilyFiles(); - await getAllPendingRecordsByResponseId(); + if (!_appState.getIsChildLoggedIn) { + await getFamilyFiles(status: 0); + await getAllPendingRecordsByResponseId(); + } + LoaderBottomSheet.hideLoader(); } + + Future removeFileFromFamilyMembers({int? id}) async { + NavigationService navigationService = getIt.get(); + _dialogService.showExceptionBottomSheet( + message: "Remove this member?", + onOkPressed: () async { + LoaderBottomSheet.showLoader(); + final result = await medicalFileRepo.removeFamilyFile(id: id); + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + LoaderBottomSheet.hideLoader(); + _dialogService.showErrorBottomSheet( + message: apiResponse.errorMessage!, + onOkPressed: () { + navigationService.pop(); + }); + } else if (apiResponse.messageStatus == 1) { + patientFamilyFiles.removeWhere((element) => element.id == id); + LoaderBottomSheet.hideLoader(); + notifyListeners(); + navigationService.pop(); + } + }, + ); + }, + onCancelPressed: () { + navigationService.pop(); + }); + } + + Future acceptRejectFileFromFamilyMembers({int? id, int? status}) async { + NavigationService navigationService = getIt.get(); + LoaderBottomSheet.showLoader(); + final result = await medicalFileRepo.acceptRejectFamilyFile(id: id, status: status); + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + LoaderBottomSheet.hideLoader(); + _dialogService.showErrorBottomSheet( + message: apiResponse.errorMessage!, + onOkPressed: () { + navigationService.pop(); + }); + } else if (apiResponse.messageStatus == 1) { + // FamilyFileResponseModelLists moveProfile = pendingFamilyFiles.firstWhere((element) => element.id == patientID); + // moveProfile.status = 3; + // moveProfile.statusDescription = "Approved"; + // patientFamilyFiles.add(moveProfile); + pendingFamilyFiles.removeWhere((element) => element.id == id); + //TODO: Call Api Here To Load Family Members + getFamilyFiles(status: 0); + getAllPendingRecordsByResponseId(); + LoaderBottomSheet.hideLoader(); + onFamilyFileTabChange(0); + } + }, + ); + } } diff --git a/lib/features/medical_file/models/family_file_response_model.dart b/lib/features/medical_file/models/family_file_response_model.dart index 82fe3d8..f4aa557 100644 --- a/lib/features/medical_file/models/family_file_response_model.dart +++ b/lib/features/medical_file/models/family_file_response_model.dart @@ -23,83 +23,90 @@ class FamilyFileResponseModelLists { String? patientIdenficationNumber; String? patientName; String? statusDescription; + bool? isSuperUser = false; + bool? isRequestFromMySide; - FamilyFileResponseModelLists({ - this.id, - this.patientId, - this.responseId, - this.relationshipId, - this.relationship, - this.relationshipN, - this.regionId, - this.familyRegionId, - this.status, - this.isActive, - this.editedOn, - this.createdOn, - this.age, - this.emaiLAddress, - this.gender, - this.genderDescription, - this.genderImage, - this.mobileNumber, - this.patientDataVerified, - this.patientIdenficationNumber, - this.patientName, - this.statusDescription, - }); + FamilyFileResponseModelLists( + {this.id, + this.patientId, + this.responseId, + this.relationshipId, + this.relationship, + this.relationshipN, + this.regionId, + this.familyRegionId, + this.status, + this.isActive, + this.editedOn, + this.createdOn, + this.age, + this.emaiLAddress, + this.gender, + this.genderDescription, + this.genderImage, + this.mobileNumber, + this.patientDataVerified, + this.patientIdenficationNumber, + this.patientName, + this.statusDescription, + this.isSuperUser, + this.isRequestFromMySide}); factory FamilyFileResponseModelLists.fromRawJson(String str) => FamilyFileResponseModelLists.fromJson(json.decode(str)); String toRawJson() => json.encode(toJson()); factory FamilyFileResponseModelLists.fromJson(Map json) => FamilyFileResponseModelLists( - id: json["ID"], - patientId: json["PatientID"], - responseId: json["ResponseID"], - relationshipId: json["RelationshipID"], - relationship: json["Relationship"], - relationshipN: json["RelationshipN"], - regionId: json["RegionID"], - familyRegionId: json["FamilyRegionID"], - status: json["Status"], - isActive: json["IsActive"], - editedOn: json["EditedOn"], - createdOn: json["CreatedOn"], - age: json["Age"], - emaiLAddress: json["EmaiLAddress"], - gender: json["Gender"], - genderDescription: json["GenderDescription"], - genderImage: json["GenderImage"], - mobileNumber: json["MobileNumber"], - patientDataVerified: json["PatientDataVerified"], - patientIdenficationNumber: json["PatientIdenficationNumber"], - patientName: json["PatientName"], - statusDescription: json["StatusDescription"], - ); + id: json["ID"], + patientId: json["PatientID"], + responseId: json["ResponseID"], + relationshipId: json["RelationshipID"], + relationship: json["Relationship"], + relationshipN: json["RelationshipN"], + regionId: json["RegionID"], + familyRegionId: json["FamilyRegionID"], + status: json["Status"], + isActive: json["IsActive"], + editedOn: json["EditedOn"], + createdOn: json["CreatedOn"], + age: json["Age"], + emaiLAddress: json["EmaiLAddress"], + gender: json["Gender"], + genderDescription: json["GenderDescription"], + genderImage: json["GenderImage"], + mobileNumber: json["MobileNumber"], + patientDataVerified: json["PatientDataVerified"], + patientIdenficationNumber: json["PatientIdenficationNumber"], + patientName: json["PatientName"], + statusDescription: json["StatusDescription"], + isSuperUser: json["isSuperUser"] ?? false, + isRequestFromMySide: json["isRequestFromMySide"] ?? false, + ); Map toJson() => { - "ID": id, - "PatientID": patientId, - "ResponseID": responseId, - "RelationshipID": relationshipId, - "Relationship": relationship, - "RelationshipN": relationshipN, - "RegionID": regionId, - "FamilyRegionID": familyRegionId, - "Status": status, - "IsActive": isActive, - "EditedOn": editedOn, - "CreatedOn": createdOn, - "Age": age, - "EmaiLAddress": emaiLAddress, - "Gender": gender, - "GenderDescription": genderDescription, - "GenderImage": genderImage, - "MobileNumber": mobileNumber, - "PatientDataVerified": patientDataVerified, - "PatientIdenficationNumber": patientIdenficationNumber, - "PatientName": patientName, - "StatusDescription": statusDescription, - }; + "ID": id, + "PatientID": patientId, + "ResponseID": responseId, + "RelationshipID": relationshipId, + "Relationship": relationship, + "RelationshipN": relationshipN, + "RegionID": regionId, + "FamilyRegionID": familyRegionId, + "Status": status, + "IsActive": isActive, + "EditedOn": editedOn, + "CreatedOn": createdOn, + "Age": age, + "EmaiLAddress": emaiLAddress, + "Gender": gender, + "GenderDescription": genderDescription, + "GenderImage": genderImage, + "MobileNumber": mobileNumber, + "PatientDataVerified": patientDataVerified, + "PatientIdenficationNumber": patientIdenficationNumber, + "PatientName": patientName, + "StatusDescription": statusDescription, + "isSuperUser": isSuperUser, + "isRequestFromMySide": isRequestFromMySide, + }; } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index cb2f76f..da19747 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -16,6 +16,7 @@ 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/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_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/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'; @@ -81,6 +82,7 @@ class _LandingPageState extends State { myAppointmentsViewModel.getPatientAppointments(true, false); myAppointmentsViewModel.getPatientMyDoctors(); prescriptionsViewModel.initPrescriptionsViewModel(); + } }); super.initState(); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 4b5259f..b87d657 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -29,6 +29,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointme import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/patient_insurance_card.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_reports_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/patient_sickleaves_list_page.dart'; @@ -80,7 +81,7 @@ class _MedicalFilePageState extends State { medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.getPatientSickLeaveList(); if (appState.getSuperUserID == null) { - medicalFileViewModel.getFamilyFiles(status: 3); //TODO: Remove status: 1 by Aamir Need to Discuss With Sultan + medicalFileViewModel.getFamilyFiles(status: 0); //TODO: Remove status: 1 by Aamir Need to Discuss With Sultan medicalFileViewModel.getAllPendingRecordsByResponseId(); //TODO: Added By Aamir } @@ -97,121 +98,139 @@ class _MedicalFilePageState extends State { medicalFileViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); NavigationService navigationService = getIt.get(); - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: LocaleKeys.medicalFile.tr(context: context), - isLeading: false, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - TextInputWidget( - labelText: LocaleKeys.search.tr(context: context), - hintText: "Type any record", - controller: TextEditingController(), - keyboardType: TextInputType.number, - isEnable: true, - prefix: null, - autoFocus: false, - isBorderAllowed: false, - isAllowLeadingIcon: true, - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), - leadingIcon: AppAssets.student_card, - ).paddingSymmetrical(24.h, 0.0), - SizedBox(height: 16.h), - Container( - width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset(appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, width: 56.h, height: 56.h), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText18(isBold: true), - SizedBox(height: 4.h), - Wrap( - direction: Axis.horizontal, - spacing: 4.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget( - icon: AppAssets.file_icon, - labelText: "${LocaleKeys.fileNo.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}", - onChipTap: () { - navigationService.pushPage( - page: FamilyMedicalScreen( - profiles: medicalFileViewModel.patientFamilyFiles, - onSelect: (FamilyFileResponseModelLists p1) {}, - )); - }, - ), - AppCustomChipWidget( - icon: AppAssets.checkmark_icon, - labelText: LocaleKeys.verified.tr(context: context), - iconColor: AppColors.successColor, - ), - ], - ), - ], - ) - ], - ), - SizedBox(height: 16.h), - Divider(color: AppColors.dividerColor, height: 1.h), - SizedBox(height: 16.h), - Wrap( - direction: Axis.horizontal, - spacing: 4.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget( - labelText: "${appState.getAuthenticatedUser()!.age} Years Old", - ), - AppCustomChipWidget( - icon: AppAssets.blood_icon, - labelText: "${LocaleKeys.bloodType.tr(context: context)}: ${appState.getUserBloodGroup}", - iconColor: AppColors.primaryRedColor, - ), - ], - ), - ], - ), - ), - ).paddingSymmetrical(24.h, 0.0), - SizedBox(height: 16.h), - Consumer(builder: (context, medicalFileVM, child) { - return Column( + return CollapsingListView( + title: "Medical File".needTranslation, + trailing: Row( + children: [ + Wrap(spacing: -15, children: [ + Utils.buildImgWithAssets(icon: AppAssets.babyGirlImg, width: 32.h, height: 32.h, border: 1.5.h, borderRadius: 50.h), + Utils.buildImgWithAssets(icon: AppAssets.femaleImg, width: 32.h, height: 32.h, border: 1.5.h, borderRadius: 50.h), + Utils.buildImgWithAssets(icon: AppAssets.male_img, width: 32.h, height: 32.h, border: 1.5.h, borderRadius: 50.h), + ]), + SizedBox(width: 4.h), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + DialogService dialogService = getIt.get(); + dialogService.showFamilyBottomSheetWithoutH( + label: "Who do you want to book for?".needTranslation, + message: "This clinic or doctor is only available for the below eligible profiles.".needTranslation, + onSwitchPress: (FamilyFileResponseModelLists profile) { + medicalFileViewModel.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); + }, + profiles: medicalFileViewModel.patientFamilyFiles); + }), + isLeading: false, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + TextInputWidget( + labelText: LocaleKeys.search.tr(context: context), + hintText: "Type any record".needTranslation, + controller: TextEditingController(), + keyboardType: TextInputType.number, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), + leadingIcon: AppAssets.student_card, + ).paddingSymmetrical(24.h, 0.0), + SizedBox(height: 16.h), + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), - tabs: [ - CustomTabBarModel(AppAssets.myFilesBottom, LocaleKeys.general.tr(context: context).needTranslation), - CustomTabBarModel(AppAssets.insurance, LocaleKeys.insurance.tr(context: context)), - CustomTabBarModel(AppAssets.requests, LocaleKeys.request.tr(context: context).needTranslation), - CustomTabBarModel(AppAssets.more, "More".needTranslation), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, width: 56.h, height: 56.h), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" + .toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1), + SizedBox(height: 4.h), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + icon: AppAssets.file_icon, + labelText: "${LocaleKeys.fileNo.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}", + onChipTap: () { + navigationService.pushPage( + page: FamilyMedicalScreen( + profiles: medicalFileViewModel.patientFamilyFiles, + onSelect: (FamilyFileResponseModelLists p1) {}, + )); + }, + ), + AppCustomChipWidget( + icon: AppAssets.checkmark_icon, + labelText: LocaleKeys.verified.tr(context: context), + iconColor: AppColors.successColor, + ), + ], + ), + ], + ) + ], + ), + SizedBox(height: 16.h), + Divider(color: AppColors.dividerColor, height: 1.h), + SizedBox(height: 16.h), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: "${appState.getAuthenticatedUser()!.age} Years Old", + ), + AppCustomChipWidget( + icon: AppAssets.blood_icon, + labelText: "${LocaleKeys.bloodType.tr(context: context)}: ${appState.getUserBloodGroup}", + iconColor: AppColors.primaryRedColor, + ), ], - onTabChange: (index) { - medicalFileVM.onTabChanged(index); - }, - ).paddingSymmetrical(24.h, 0.0), - SizedBox(height: 24.h), - getSelectedTabData(medicalFileVM.selectedTabIndex), + ), ], - ); - }), - ], - ), + ), + ), + ).paddingSymmetrical(24.h, 0.0), + SizedBox(height: 16.h), + Consumer(builder: (context, medicalFileVM, child) { + return Column( + children: [ + CustomTabBar( + activeTextColor: Color(0xffED1C2B), + activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + tabs: [ + CustomTabBarModel(AppAssets.myFilesBottom, LocaleKeys.general.tr(context: context).needTranslation), + CustomTabBarModel(AppAssets.insurance, LocaleKeys.insurance.tr(context: context)), + CustomTabBarModel(AppAssets.requests, LocaleKeys.request.tr(context: context).needTranslation), + CustomTabBarModel(AppAssets.more, "More".needTranslation), + ], + onTabChange: (index) { + medicalFileVM.onTabChanged(index); + }, + ).paddingSymmetrical(24.h, 0.0), + SizedBox(height: 24.h), + getSelectedTabData(medicalFileVM.selectedTabIndex), + ], + ); + }), + ], ), ), ); @@ -576,28 +595,28 @@ class _MedicalFilePageState extends State { horizontalOffset: 100.0, child: FadeInAnimation( child: SizedBox( - width: 80.h, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.network( - myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, - width: 64.h, - height: 64.h, - fit: BoxFit.fill, - ).circle(100).toShimmer2(isShow: false, radius: 50.h), - SizedBox(height: 8.h), - Expanded( - child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) - .toString() - .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) - .toShimmer2(isShow: false), + width: 80.h, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + width: 64.h, + height: 64.h, + fit: BoxFit.fill, + ).circle(100).toShimmer2(isShow: false, radius: 50.h), + SizedBox(height: 8.h), + Expanded( + child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: false), + ), + ], ), - ], + ), ), ), - ), - ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any completed visits yet.".needTranslation, isSmallWidget: true, width: 62, height: 62) .paddingSymmetrical(24.h, 0.h); diff --git a/lib/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart index a2bcfaa..7de92f4 100644 --- a/lib/presentation/my_family/my_family.dart +++ b/lib/presentation/my_family/my_family.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; @@ -11,10 +14,10 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_ import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/my_family/widget/family_cards.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/app_bar_widget.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; @@ -48,100 +51,75 @@ class _FamilyMedicalScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: CollapsingListView( - title: "My Medical File".needTranslation, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomTabBar( - tabs: tabs, - onTabChange: (index) { - medicalVM!.onFamilyFileTabChange(index); - }, + AppState appState = getIt.get(); + return CollapsingListView( + title: "My Medical File".needTranslation, + bottomChild: appState.getAuthenticatedUser()!.isParentUser! + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), ), - SizedBox(height: 25.h), - Consumer(builder: (context, medicalVM, child) => getFamilyTabs(index: medicalVM.getSelectedFamilyFileTabIndex)), - SizedBox(height: 20.h), - ], - ).paddingSymmetrical(20, 0)), - bottomSheet: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 20.h), + child: CustomButton( + text: "Add a new family member".needTranslation, + onPressed: () { + DialogService dialogService = getIt.get(); + dialogService.showAddFamilyFileSheet( + label: "Add Family Member".needTranslation, + message: "Please fill the below field to add a new family member to your profile".needTranslation, + onVerificationPress: () { + medicalVM?.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true); + }); + }, + icon: AppAssets.add_icon, + height: 56.h, + fontWeight: FontWeight.w600)) + : SizedBox(), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTabBar( + tabs: tabs, + onTabChange: (index) { + medicalVM!.onFamilyFileTabChange(index); + }, ), - padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 20.h), - child: CustomButton( - text: "Add a new family member", - onPressed: () { - showModelSheet(); - }, - icon: AppAssets.add_icon, - height: 56.h, - fontWeight: FontWeight.w600)), + SizedBox(height: 25.h), + Consumer(builder: (context, medicalVM, child) => getFamilyTabs(index: medicalVM.getSelectedFamilyFileTabIndex)), + SizedBox(height: 20.h), + ], + ).paddingSymmetrical(20, 0), ); - // return Scaffold( - // backgroundColor: AppColors.scaffoldBgColor, - // appBar: CustomAppBar( - // onBackPressed: () { - // Navigator.of(context).pop(); - // }, - // onLanguageChanged: (lang) {}, - // hideLogoAndLang: true, - // ), - // body: SingleChildScrollView( - // child: Column( - // mainAxisSize: MainAxisSize.min, - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // LocaleKeys.myMedicalFile.tr().toText26(color: AppColors.textColor, weight: FontWeight.w600, letterSpacing: -2), - // SizedBox(height: 25.h), - // CustomTabBar( - // tabs: tabs, - // onTabChange: (index) { - // medicalVM!.onFamilyFileTabChange(index); - // }, - // ), - // SizedBox(height: 25.h), - // Consumer(builder: (context, medicalVM, child) => getFamilyTabs(index: medicalVM.getSelectedFamilyFileTabIndex)), - // SizedBox(height: 20.h), - // ], - // ).paddingSymmetrical(20, 0), - // ), - // bottomSheet: Container( - // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - // color: AppColors.whiteColor, - // customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), - // ), - // padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 20.h), - // child: CustomButton( - // text: "Add a new family member", - // onPressed: () { - // showModelSheet(); - // }, - // icon: AppAssets.add_icon, - // height: 56.h, - // fontWeight: FontWeight.w600, - // )), - // ); } Widget getFamilyTabs({required int index}) { switch (index) { case 0: + print(jsonEncode(medicalVM!.patientFamilyFiles)); return FamilyCards( profiles: medicalVM!.patientFamilyFiles, onSelect: (FamilyFileResponseModelLists profile) { medicalVM!.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); }, + onRemove: (FamilyFileResponseModelLists profile) { + medicalVM!.removeFileFromFamilyMembers(id: profile.id); + }, + isLeftAligned: true, isShowDetails: true, + isShowRemoveButton: true, ); case 1: + print(jsonEncode(medicalVM!.pendingFamilyFiles)); return FamilyCards( profiles: medicalVM!.pendingFamilyFiles, + isRequestDesign: true, onSelect: (FamilyFileResponseModelLists profile) { - // medicalVM!.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); + medicalVM!.acceptRejectFileFromFamilyMembers(id: profile.id, status: 3); + }, + onRemove: (FamilyFileResponseModelLists profile) { + medicalVM!.acceptRejectFileFromFamilyMembers(id: profile.id, status: 4); }, isShowDetails: true, ); @@ -149,85 +127,4 @@ class _FamilyMedicalScreenState extends State { return SizedBox.shrink(); } } - - Future showModelSheet() async { - AuthenticationViewModel authVm = getIt.get(); - return await showCommonBottomSheetWithoutHeight(context, - title: "Add Family Member", - useSafeArea: true, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - "Please fill the below field to add a new family member to your profile".toText16(color: AppColors.textColor, weight: FontWeight.w500), - SizedBox(height: 20.h), - Container( - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(24)), - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), - child: Column( - children: [ - CustomCountryDropdown( - countryList: CountryEnum.values, - onCountryChange: authVm.onCountryChange, - ).paddingOnly(top: 8.h, bottom: 16.h), - Divider(height: 1.h, color: AppColors.spacerLineColor), - TextInputWidget( - labelText: LocaleKeys.nationalIdNumber.tr(), - hintText: "xxxxxxxxx", - controller: authVm.nationalIdController, - // focusNode: _nationalIdFocusNode, - isEnable: true, - prefix: null, - isAllowRadius: true, - isBorderAllowed: false, - isAllowLeadingIcon: true, - autoFocus: true, - keyboardType: TextInputType.number, - padding: EdgeInsets.symmetric(vertical: 8.h), - leadingIcon: AppAssets.student_card, - ).paddingOnly(top: 8.h, bottom: 8.h), - Divider(height: 1.h, color: AppColors.spacerLineColor), - TextInputWidget( - labelText: LocaleKeys.phoneNumber.tr(), - hintText: "", - controller: authVm.phoneNumberController, - isEnable: true, - prefix: authVm.selectedCountrySignup.countryCode, - isAllowRadius: true, - isBorderAllowed: false, - isAllowLeadingIcon: true, - autoFocus: true, - keyboardType: TextInputType.number, - padding: EdgeInsets.symmetric(vertical: 8.h), - leadingIcon: AppAssets.smart_phone, - ).paddingOnly(top: 8.h, bottom: 4.h), - ], - ), - ), - SizedBox(height: 20.h), - CustomButton( - text: "Verify the member", - onPressed: () { - FocusScope.of(context).unfocus(); - if (ValidationUtils.isValidatedIdAndPhoneWithCountryValidation( - nationalId: authVm.nationalIdController.text, - selectedCountry: authVm.selectedCountrySignup, - phoneNumber: authVm.phoneNumberController.text, - onOkPress: () { - Navigator.of(context).pop(); - }, - )) { - // authVm.addFamilyMember(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true); - medicalVM?.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true); - } - }, - icon: AppAssets.add_icon, - height: 56.h, - fontWeight: FontWeight.w600), - SizedBox(height: 20.h), - ], - ), - callBackFunc: () {}); - } } diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 3916756..166b06d 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -17,10 +17,23 @@ import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; class FamilyCards extends StatefulWidget { final List profiles; final Function(FamilyFileResponseModelLists) onSelect; + final Function(FamilyFileResponseModelLists) onRemove; final bool isShowDetails; final bool isBottomSheet; + final bool isRequestDesign; + final bool isLeftAligned; + final bool isShowRemoveButton; - const FamilyCards({super.key, required this.profiles, required this.onSelect, this.isShowDetails = false, this.isBottomSheet = false}); + const FamilyCards( + {super.key, + required this.profiles, + required this.onSelect, + required this.onRemove, + this.isShowDetails = false, + this.isBottomSheet = false, + this.isRequestDesign = false, + this.isLeftAligned = false, + this.isShowRemoveButton = false}); @override State createState() => _FamilyCardsState(); @@ -29,91 +42,315 @@ class FamilyCards extends StatefulWidget { class _FamilyCardsState extends State { AppState appState = getIt(); + // bool isShowActions = true; + @override Widget build(BuildContext context) { - return GridView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: widget.profiles.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 10.h, - mainAxisSpacing: 10.h, - childAspectRatio: widget.isShowDetails ? 0.56.h : 0.74.h, - ), - padding: EdgeInsets.only(bottom: 80.h), - itemBuilder: (context, index) { - final profile = widget.profiles[index]; - final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId); - return Container( - padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 15.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), - child: Opacity( - opacity: isActive || profile.status == FamilyFileEnum.pending.toInt ? 0.4 : 1.0, // Fade all content if active - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 5.h), - Utils.buildImgWithAssets( - icon: profile.gender == null - ? AppAssets.dummy_user - : profile.gender == 1 - ? ((profile.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.male_img) - : (profile.age! < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg), - width: 80.h, - height: 78.h), - SizedBox(height: 8.h), - (profile.patientName ?? "Unknown").toText16(isBold: false, isCenter: true, maxlines: 1, weight: FontWeight.w600), - SizedBox(height: 4.h), - CustomChipWidget( - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: "Relation: ${profile.relationship ?? "N/A"}", - iconAsset: AppAssets.heart, - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor), - widget.isShowDetails ? SizedBox(height: 4.h) : SizedBox(), - widget.isShowDetails - ? CustomChipWidget( + if (widget.isRequestDesign) { + return Column( + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.alertSquare), + SizedBox(width: 8.h), + "Sent Requests".needTranslation.toText14(color: AppColors.textColor, isUnderLine: true, weight: FontWeight.w500), + SizedBox(width: 4.h), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight), + ], + ), + SizedBox(height: 24.h), + ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: widget.profiles.where((profile) => profile.isRequestFromMySide ?? false).length, + itemBuilder: (context, index) { + final mySideProfiles = widget.profiles.where((profile) => profile.isRequestFromMySide ?? false).toList(); + FamilyFileResponseModelLists profile = mySideProfiles[index]; + + return Container( + margin: EdgeInsets.only( + bottom: 12.h, + ), + padding: EdgeInsets.symmetric( + vertical: 15.h, + horizontal: 15.h, + ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), + child: Opacity( + opacity: 1.0, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.lightGreenColor + : AppColors.lightGrayBGColor, + chipText: profile.statusDescription ?? "N/A", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.textGreenColor + : AppColors.alertColor, + ), + Wrap( + alignment: WrapAlignment.start, + children: [ + (profile.patientName ?? "").toText16( + isBold: false, + isCenter: true, + maxlines: 1, + weight: FontWeight.w600, + ), + ("has ${(profile.statusDescription ?? "").toLowerCase()} your family member request").toText14( + isBold: false, + isCenter: true, + maxlines: 1, + weight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + ], + ), + SizedBox(height: 4.h), + CustomChipWidget( + height: 30.h, chipType: ChipTypeEnum.alert, backgroundColor: AppColors.lightGrayBGColor, - chipText: "Age: ${profile.age ?? "N/A"} Years", + chipText: "Medical File: ${profile.responseId ?? "N/A"}", + iconAsset: null, isShowBorder: false, borderRadius: 8.h, textColor: AppColors.textColor, - ) - : SizedBox(), - widget.isShowDetails ? SizedBox(height: 8.h) : SizedBox(), - Spacer(), - if (isActive) - CustomButton( - height: 40.h, - onPressed: () {}, - text: LocaleKeys.active.tr(), - backgroundColor: Colors.grey.shade200, - borderColor: Colors.grey.shade200, - textColor: AppColors.greyTextColor, - fontSize: 13.h, - ).paddingOnly(top: 0, bottom: 0) - else - CustomButton( - height: 40.h, - onPressed: () => widget.onSelect(profile), - text: LocaleKeys.switchAccount.tr(), - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 13.h, - icon: widget.isBottomSheet ? null : AppAssets.switch_user, - iconColor: AppColors.primaryRedColor, - padding: EdgeInsets.symmetric(vertical: 0, horizontal: 0), - ).paddingOnly(top: 0, bottom: 0), + ), + ], + ), + ), + ); + }, + ), + SizedBox(height: 20.h), + if (widget.profiles.where((profile) => !(profile.isRequestFromMySide ?? false)).isNotEmpty) + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.alertSquare), + SizedBox(width: 8.h), + "Users who want to view your profile".needTranslation.toText14(color: AppColors.textColor, isUnderLine: true, weight: FontWeight.w500), + SizedBox(width: 4.h), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight), ], ), + + // Items for second group (requests from others) + ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: widget.profiles.where((profile) => !(profile.isRequestFromMySide ?? false)).length, + itemBuilder: (context, index) { + final otherProfiles = widget.profiles.where((profile) => !(profile.isRequestFromMySide ?? false)).toList(); + FamilyFileResponseModelLists profile = otherProfiles[index]; + return Container( + margin: EdgeInsets.only(bottom: 12.h), + padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 15.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), + child: Opacity( + opacity: 1.0, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.lightGreenColor + : AppColors.lightGrayBGColor, + chipText: profile.statusDescription ?? "N/A", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.textGreenColor + : AppColors.alertColor, + ), + Wrap( + alignment: WrapAlignment.start, + children: [ + (profile.patientName ?? "").toText16(isBold: false, isCenter: true, maxlines: 1, weight: FontWeight.w600), + (profile.status == FamilyFileEnum.active.toInt ? "can view your family".needTranslation : "wants to add you as their family member".needTranslation).toText14( + isBold: false, + isCenter: true, + maxlines: 1, + weight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + ], + ), + SizedBox(height: 4.h), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "Medical File: ${profile.patientId ?? "N/A".needTranslation}", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor, + ), + SizedBox(height: 16.h), + Row( + children: [ + profile.status == FamilyFileEnum.active.toInt + ? SizedBox() + : Expanded( + child: CustomButton( + height: 40.h, + text: LocaleKeys.confirm.tr(), + onPressed: () { + widget.onSelect(profile); + }, + backgroundColor: AppColors.lightGreenButtonColor, + borderColor: AppColors.lightGreenButtonColor, + textColor: AppColors.textGreenColor, + icon: null, + ), + ), + profile.status == FamilyFileEnum.active.toInt ? SizedBox() : SizedBox(width: 8.h), + Expanded( + child: CustomButton( + height: 40.h, + text: profile.status == FamilyFileEnum.active.toInt ? LocaleKeys.removeMember.tr() : LocaleKeys.cancel.tr(), + onPressed: () { + widget.onRemove(profile); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + icon: null, + iconColor: AppColors.primaryRedColor, + ), + ), + ], + ), + ], + ), + ), + ); + }, ), - ); - }, - ); + ], + ); + } else { + return GridView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: widget.profiles.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 10.h, + mainAxisSpacing: 10.h, + childAspectRatio: widget.isShowDetails ? 0.56.h : 0.66.h, + ), + padding: EdgeInsets.only(bottom: 20.h), + itemBuilder: (context, index) { + final profile = widget.profiles[index]; + final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId); + final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; + final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID); + return Container( + padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 15.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), + child: Opacity( + opacity: isActive || profile.status == FamilyFileEnum.pending.toInt || !canSwitch ? 0.4 : 1.0, // Fade all content if active + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Utils.buildImgWithAssets( + icon: profile.gender == null + ? AppAssets.dummy_user + : profile.gender == 1 + ? ((profile.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.male_img) + : (profile.age! < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg), + width: 80.h, + height: 78.h), + SizedBox(height: 8.h), + (profile.patientName ?? "Unknown").toText16(isBold: false, isCenter: true, maxlines: 1, weight: FontWeight.w600), + SizedBox(height: 4.h), + CustomChipWidget( + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "Relation:${profile.relationship ?? "N/A"}", + iconAsset: AppAssets.heart, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + widget.isShowDetails ? SizedBox(height: 4.h) : SizedBox(), + widget.isShowDetails + ? CustomChipWidget( + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "Age:${profile.age ?? "N/A"} Years", + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor, + ) + : SizedBox(), + widget.isShowDetails ? SizedBox(height: 8.h) : SizedBox(), + Spacer(), + CustomButton( + height: 40.h, + onPressed: () { + if (canSwitch) widget.onSelect(profile); + }, + text: isActive ? "Active".needTranslation : "Switch".needTranslation, + backgroundColor: isActive || !canSwitch ? Colors.grey.shade200 : AppColors.secondaryLightRedColor, + borderColor: isActive || !canSwitch ? Colors.grey.shade200 : AppColors.secondaryLightRedColor, + textColor: isActive || !canSwitch ? AppColors.greyTextColor : AppColors.primaryRedColor, + fontSize: 13.h, + icon: isActive ? AppAssets.activeCheck : AppAssets.switch_user, + iconColor: isActive || !canSwitch ? (isActive ? null : AppColors.greyTextColor) : AppColors.primaryRedColor, + padding: EdgeInsets.symmetric(vertical: 0, horizontal: 0), + ).paddingOnly(top: 0, bottom: 0), + ], + ), + if (widget.isShowRemoveButton) ...[ + Positioned( + top: 0, + right: 0, + child: Utils.buildSvgWithAssets(icon: AppAssets.deleteIcon).onPress(() { + if (!isActive) widget.onRemove(profile); + }), + ), + ], + ], + ), + ), + ); + }, + ); + } } } diff --git a/lib/presentation/my_family/widget/my_family_sheet.dart b/lib/presentation/my_family/widget/my_family_sheet.dart index da822b1..1c085ca 100644 --- a/lib/presentation/my_family/widget/my_family_sheet.dart +++ b/lib/presentation/my_family/widget/my_family_sheet.dart @@ -7,7 +7,7 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; class MyFamilySheet { static Future show(BuildContext context, List familyLists, Function(FamilyFileResponseModelLists) onSelect) async { - return await showCommonBottomSheetWithoutHeight( + return showCommonBottomSheetWithoutHeight( context, titleWidget: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -22,6 +22,7 @@ class MyFamilySheet { Navigator.of(context).pop(); // Close the bottom sheet onSelect(profile); // Call the onSelect callback }, + onRemove: (profile) {}, isBottomSheet: true), callBackFunc: () {}, ); diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 58273fc..2afa3e0 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -1,16 +1,25 @@ +import 'dart:convert'; +import 'dart:developer'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_swiper_view/flutter_swiper_view.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/int_extensions.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/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/app_language_change.dart'; @@ -48,6 +57,7 @@ class _ProfileSettingsState extends State { @override Widget build(BuildContext context) { + final MedicalFileViewModel medicalFileViewModel = getIt.get(); return CollapsingListView( title: "Profile & Settings".needTranslation, logout: () {}, @@ -57,11 +67,12 @@ class _ProfileSettingsState extends State { physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { + print(jsonEncode(medicalFileViewModel.patientFamilyFiles)); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Swiper( - itemCount: length, + itemCount: medicalFileViewModel.patientFamilyFiles.length, layout: SwiperLayout.STACK, loop: true, itemWidth: MediaQuery.of(context).size.width - 42, @@ -75,7 +86,21 @@ class _ProfileSettingsState extends State { builder: DotSwiperPaginationBuilder(color: Color(0xffD9D9D9), activeColor: AppColors.blackBgColor), ), itemBuilder: (BuildContext context, int index) { - return FamilyCardWidget().paddingOnly(right: 16); + return FamilyCardWidget( + profile: medicalFileViewModel.patientFamilyFiles[index], + onAddFamilyMemberPress: () { + DialogService dialogService = getIt.get(); + dialogService.showAddFamilyFileSheet( + label: "Add Family Member".needTranslation, + message: "Please fill the below field to add a new family member to your profile".needTranslation, + onVerificationPress: () { + medicalFileViewModel.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true); + }); + }, + onFamilySwitchPress: (FamilyFileResponseModelLists profile) { + medicalFileViewModel.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); + }, + ).paddingOnly(right: 16); }, ), GridView( @@ -224,16 +249,20 @@ class _ProfileSettingsState extends State { } class FamilyCardWidget extends StatelessWidget { - FamilyCardWidget(); + final Function() onAddFamilyMemberPress; + final Function(FamilyFileResponseModelLists member) onFamilySwitchPress; + final FamilyFileResponseModelLists profile; + + const FamilyCardWidget({required this.onAddFamilyMemberPress, required this.profile, required this.onFamilySwitchPress(FamilyFileResponseModelLists member)}); @override Widget build(BuildContext context) { + AppState appState = getIt.get(); + final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId); + final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; + final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID); return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: Column( children: [ Column( @@ -243,16 +272,16 @@ class FamilyCardWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: 8.h, children: [ - Image.asset(true ? AppAssets.male_img : AppAssets.femaleImg, width: 56.h, height: 56.h), + Image.asset((profile.gender == 1) ? AppAssets.male_img : AppAssets.femaleImg, width: 56.h, height: 56.h), Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 0.h, mainAxisSize: MainAxisSize.min, children: [ - "Mahmoud Shrouf Shrouf".toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1), + (profile.patientName ?? "").toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1), AppCustomChipWidget( icon: AppAssets.file_icon, - labelText: "File no: 3423443", + labelText: "File no: ${profile.patientId}", iconSize: 14, ), ], @@ -266,11 +295,11 @@ class FamilyCardWidget extends StatelessWidget { alignment: WrapAlignment.start, spacing: 8.h, children: [ - AppCustomChipWidget(labelText: "35 Years Old"), - AppCustomChipWidget(labelText: "Blood: A+"), + AppCustomChipWidget(labelText: "${profile.age} Years Old"), + AppCustomChipWidget(labelText: "Blood: N/A"), AppCustomChipWidget( icon: AppAssets.insurance_active_icon, - labelText: "Insurance Active", + labelText: "Insurance N/A", iconColor: AppColors.bgGreenColor, iconSize: 14, backgroundColor: AppColors.bgGreenColor.withValues(alpha: 0.15), @@ -281,9 +310,92 @@ class FamilyCardWidget extends StatelessWidget { ], ).paddingOnly(top: 16, right: 16, left: 16, bottom: 12).expanded, 1.divider, - CustomButton(icon: AppAssets.add_family, text: "Add a new family member".needTranslation, onPressed: () {}).paddingOnly(top: 12, right: 16, left: 16, bottom: 16), + _buildActionButton(appState), + + // if (appState.getAuthenticatedUser()!.isParentUser ?? false) ...[ + // if (member!.responseId != appState.getAuthenticatedUser()!.patientId) ...[ + // CustomButton( + // icon: AppAssets.switch_user, + // text: "Switch Family File".needTranslation, + // onPressed: () { + // onFamilySwitchPress(member!); + // }, + // ).paddingOnly(top: 12, right: 16, left: 16, bottom: 16), + // ] else + // ...[ + // CustomButton( + // icon: AppAssets.add_family, + // text: "Add a new family member".needTranslation, + // onPressed: () { + // onAddFamilyMemberPress(); + // }, + // ).paddingOnly(top: 12, right: 16, left: 16, bottom: 16), + // ] + // ] else + // ...[ + // if (appState.getSuperUserID != null && appState.getSuperUserID == member!.responseId) ...[ + // CustomButton( + // icon: AppAssets.switch_user, + // text: "Switch Back To Family File".needTranslation, + // onPressed: () { + // onFamilySwitchPress(member!); + // }, + // ).paddingOnly(top: 12, right: 16, left: 16, bottom: 16), + // ] else + // ...[ + // CustomButton( + // icon: AppAssets.switch_user, + // text: "Disabled".needTranslation, + // backgroundColor: Colors.grey.shade200, + // borderColor: Colors.grey.shade200, + // textColor: AppColors.greyTextColor, + // onPressed: () {}, + // iconColor: AppColors.greyTextColor, + // ).paddingOnly(top: 12, right: 16, left: 16, bottom: 16), + // ] + // ] ], ), ); } + + Widget _buildActionButton(AppState appState) { + final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; + final int? currentUserId = appState.getAuthenticatedUser()?.patientId; + final int? superUserId = appState.getSuperUserID; + + if (isParentUser) { + return _buildParentUserButton(currentUserId); + } else { + return _buildNonParentUserButton(superUserId); + } + } + + Widget _buildParentUserButton(int? currentUserId) { + final canSwitch = profile.responseId != currentUserId; + + return CustomButton( + icon: canSwitch ? AppAssets.switch_user : AppAssets.add_family, + text: canSwitch ? "Switch Family File".needTranslation : "Add a new family member".needTranslation, + onPressed: canSwitch ? () => onFamilySwitchPress(profile) : onAddFamilyMemberPress, + backgroundColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor, + borderColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor, + textColor: canSwitch ? AppColors.primaryRedColor : AppColors.whiteColor, + iconColor: canSwitch ? AppColors.primaryRedColor : AppColors.whiteColor, + ).paddingOnly(top: 12, right: 16, left: 16, bottom: 16); + } + + Widget _buildNonParentUserButton(int? superUserId) { + final canSwitchBack = superUserId != null && superUserId == profile.responseId; + + return CustomButton( + icon: AppAssets.switch_user, + text: canSwitchBack ? "Switch Back To Family File".needTranslation : "Switch".needTranslation, + backgroundColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200, + borderColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200, + textColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor, + iconColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor, + onPressed: canSwitchBack ? () => onFamilySwitchPress(profile) : () {}, + ).paddingOnly(top: 12, right: 16, left: 16, bottom: 16); + } } diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 6ee0ddc..4d332b3 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -4,12 +4,15 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/my_family/widget/family_cards.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/bottomsheet/exception_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/family_files/family_file_add_widget.dart'; abstract class DialogService { Future showErrorBottomSheet({String title = "", required String message, Function()? onOkPressed, Function()? onCancelPressed}); @@ -18,7 +21,12 @@ abstract class DialogService { Future showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}); + Future showFamilyBottomSheetWithoutH( + {String? label, required String message, required Function(FamilyFileResponseModelLists response) onSwitchPress, required List profiles}); + Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}); + + Future showAddFamilyFileSheet({String? label, String? message, required Function() onVerificationPress}); // TODO : Need to be Fixed showPhoneNumberPickerSheet ( From Login ADn Signup Bottom Sheet Move Here } @@ -93,6 +101,24 @@ class DialogServiceImp implements DialogService { title: label ?? "", child: exceptionBottomSheetWidget(context: context, message: message, onOkPressed: onOkPressed, onCancelPressed: onCancelPressed), callBackFunc: () {}); } + @override + Future showFamilyBottomSheetWithoutH( + {String? label, required String message, required Function(FamilyFileResponseModelLists response) onSwitchPress, required List profiles}) async { + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + showCommonBottomSheetWithoutHeight(context, + title: label ?? "", + child: FamilyCards( + profiles: profiles, + onSelect: (FamilyFileResponseModelLists profile) { + onSwitchPress(profile); + }, + onRemove: (FamilyFileResponseModelLists profile) {}, + isShowDetails: false, + ), + callBackFunc: () {}); + } + @override Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { final context = navigationService.navigatorKey.currentContext; @@ -100,6 +126,18 @@ class DialogServiceImp implements DialogService { showCommonBottomSheetWithoutHeight(context, title: label ?? "", child: showPhoneNumberPickerWidget(context: context, message: message, onSMSPress: onSMSPress, onWhatsappPress: onWhatsappPress), callBackFunc: () {}); } + + @override + Future showAddFamilyFileSheet({String? label, String? message, required Function() onVerificationPress}) async { + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + showCommonBottomSheetWithoutHeight(context, + title: label ?? "", + child: FamilyFileAddWidget(() { + onVerificationPress(); + }, message ?? ""), + callBackFunc: () {}); + } } Widget exceptionBottomSheetWidget({required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) { @@ -216,3 +254,12 @@ Widget showPhoneNumberPickerWidget({required BuildContext context, String? messa // ); }); } + +// Widget familyMemberAddWidget() { +// AuthenticationViewModel authVm = getIt.get(); +// return showCommonBottomSheetWithoutHeight(context, +// title: "Add Family Member".needTranslation, +// useSafeArea: true, +// child: +// callBackFunc: () {}); +// } diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index b18c6ee..60b0f03 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -22,10 +22,11 @@ class CollapsingListView extends StatelessWidget { VoidCallback? logout; VoidCallback? history; Widget? bottomChild; + Widget? trailing; bool isClose; bool isLeading; - CollapsingListView({required this.title, this.child, this.search, this.isClose = false, this.bottomChild, this.report, this.logout, this.history, this.isLeading = true}); + CollapsingListView({required this.title, this.child, this.search, this.isClose = false, this.bottomChild, this.report, this.logout, this.history, this.isLeading = true, this.trailing}); @override Widget build(BuildContext context) { @@ -97,7 +98,8 @@ class CollapsingListView extends StatelessWidget { if (logout != null) actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(logout!), if (report != null) actionButton(context, t, title: "Report".needTranslation, icon: AppAssets.report_icon).onPress(report!), if (history != null) actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon).onPress(history!), - if (search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(search!).paddingOnly(right: 24) + if (search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(search!).paddingOnly(right: 24), + if (trailing != null) trailing!, ], )), ), diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index 2f8a9ec..b253c36 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -23,28 +23,30 @@ class CustomButton extends StatelessWidget { final double? width; final double iconSize; final TextOverflow? textOverflow; + final BorderSide? borderSide; - CustomButton({ - Key? key, - required this.text, - required this.onPressed, - this.backgroundColor = const Color(0xFFED1C2B), - this.borderColor = const Color(0xFFED1C2B), - this.textColor = Colors.white, - this.borderRadius = 12, - this.borderWidth = 2, - this.padding = const EdgeInsets.fromLTRB(8, 10, 8, 10), - this.fontSize = 16, - this.fontFamily, - this.fontWeight = FontWeight.w500, - this.isDisabled = false, - this.icon, - this.iconColor = Colors.white, - this.height = 56, - this.width, - this.iconSize = 24, - this.textOverflow, - }) : super(key: key); + CustomButton( + {Key? key, + required this.text, + required this.onPressed, + this.backgroundColor = const Color(0xFFED1C2B), + this.borderColor = const Color(0xFFED1C2B), + this.textColor = Colors.white, + this.borderRadius = 12, + this.borderWidth = 2, + this.padding = const EdgeInsets.fromLTRB(8, 10, 8, 10), + this.fontSize = 16, + this.fontFamily, + this.fontWeight = FontWeight.w500, + this.isDisabled = false, + this.icon, + this.iconColor = Colors.white, + this.height = 56, + this.width, + this.iconSize = 24, + this.textOverflow, + this.borderSide}) + : super(key: key); @override Widget build(BuildContext context) { @@ -57,17 +59,15 @@ class CustomButton extends StatelessWidget { decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: isDisabled ? Colors.transparent : backgroundColor, borderRadius: borderRadius, - side: BorderSide( - width: borderWidth.h, - color: isDisabled ? borderColor.withOpacity(0.5) : borderColor, - )), + customBorder: BorderRadius.circular(borderRadius), + side: borderSide ?? BorderSide(width: borderWidth.h, color: isDisabled ? borderColor.withValues(alpha: 0.5) : borderColor)), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ if (icon != null) Padding( - padding: const EdgeInsets.only(right: 8.0, left: 8.0), + padding: EdgeInsets.only(right: 8.h, left: 8.h), child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isDisabled, width: iconSize, height: iconSize), ), Padding( @@ -85,17 +85,6 @@ class CustomButton extends StatelessWidget { ), ], ), - ) - - // .toSmoothContainer( - // smoothness: 1, - // side: BorderSide(width: borderWidth, color: backgroundColor), - // borderRadius: BorderRadius.circular(borderRadius * 1.2), - // foregroundDecoration: BoxDecoration( - // color: isDisabled ? backgroundColor.withOpacity(0.5) : Colors.transparent, - // borderRadius: BorderRadius.circular(borderRadius), - // ), - // ), - ); + )); } } diff --git a/lib/widgets/chip/custom_chip_widget.dart b/lib/widgets/chip/custom_chip_widget.dart index 1d0b5ef..5dee4f0 100644 --- a/lib/widgets/chip/custom_chip_widget.dart +++ b/lib/widgets/chip/custom_chip_widget.dart @@ -17,6 +17,7 @@ class CustomChipWidget extends StatelessWidget { final Color? textColor; final Color? borderColor; final bool isShowBorder; + final double? height; const CustomChipWidget({ super.key, @@ -31,6 +32,7 @@ class CustomChipWidget extends StatelessWidget { this.textColor, this.borderColor, this.isShowBorder = false, + this.height, }); @override @@ -39,6 +41,7 @@ class CustomChipWidget extends StatelessWidget { final hasOnTap = onTap != null || hasIcon; return Container( + height: height, decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), color: isSelected ? chipType.color : backgroundColor ?? chipType.backgroundColor, diff --git a/lib/widgets/family_files/family_file_add_widget.dart b/lib/widgets/family_files/family_file_add_widget.dart new file mode 100644 index 0000000..8e57f91 --- /dev/null +++ b/lib/widgets/family_files/family_file_add_widget.dart @@ -0,0 +1,105 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_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/authentication/authentication_view_model.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; + +class FamilyFileAddWidget extends StatelessWidget { + final Function()? onVerificationPress; + final String message; + + const FamilyFileAddWidget(this.onVerificationPress, this.message, {super.key}); + + @override + Widget build(BuildContext context) { + AuthenticationViewModel authVm = getIt.get(); + MedicalFileViewModel? medicalVM = getIt.get(); + // TODO: implement build + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + message.toText16(color: AppColors.textColor, weight: FontWeight.w500), + SizedBox(height: 20.h), + Container( + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(24)), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + child: Column( + children: [ + CustomCountryDropdown( + countryList: CountryEnum.values, + onCountryChange: authVm.onCountryChange, + ).paddingOnly(top: 8.h, bottom: 16.h), + Divider(height: 1.h, color: AppColors.spacerLineColor), + TextInputWidget( + labelText: LocaleKeys.nationalIdNumber.tr(), + hintText: "xxxxxxxxx", + controller: authVm.nationalIdController, + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + autoFocus: true, + keyboardType: TextInputType.number, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.student_card, + ).paddingOnly(top: 8.h, bottom: 8.h), + Divider(height: 1.h, color: AppColors.spacerLineColor), + TextInputWidget( + labelText: LocaleKeys.phoneNumber.tr(), + hintText: "", + controller: authVm.phoneNumberController, + isEnable: true, + prefix: authVm.selectedCountrySignup.countryCode, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + autoFocus: true, + keyboardType: TextInputType.number, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.smart_phone, + ).paddingOnly(top: 8.h, bottom: 4.h), + ], + ), + ), + SizedBox(height: 20.h), + CustomButton( + text: "Verify the member".needTranslation, + onPressed: () { + FocusScope.of(context).unfocus(); + if (ValidationUtils.isValidatedIdAndPhoneWithCountryValidation( + nationalId: authVm.nationalIdController.text, + selectedCountry: authVm.selectedCountrySignup, + phoneNumber: authVm.phoneNumberController.text, + onOkPress: () { + Navigator.of(context).pop(); + }, + )) { + // authVm.addFamilyMember(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true); + if (onVerificationPress != null) { + onVerificationPress!(); + } + } + }, + icon: AppAssets.add_icon, + height: 56.h, + fontWeight: FontWeight.w600), + SizedBox(height: 20.h), + ], + ); + } +} From 24362f67cbe92690d0c94e6ea1d5f56e0dccf761 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Tue, 7 Oct 2025 15:43:58 +0300 Subject: [PATCH 42/43] family screen & widgets --- lib/core/dependencies.dart | 2 + .../authentication/authentication_repo.dart | 2 +- .../authentication_view_model.dart | 11 +- .../medical_file/medical_file_repo.dart | 2 +- .../medical_file/medical_file_view_model.dart | 10 +- lib/presentation/home/landing_page.dart | 10 +- .../medical_file/medical_file_page.dart | 8 +- lib/presentation/my_family/my_family.dart | 14 +- .../my_family/widget/family_cards.dart | 375 +++++++++--------- .../my_family/widget/my_family_sheet.dart | 11 +- .../profile_settings/profile_settings.dart | 210 +++++++--- lib/services/dialog_service.dart | 14 + lib/widgets/chip/app_custom_chip_widget.dart | 98 ++--- 13 files changed, 448 insertions(+), 319 deletions(-) diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index e2e06d2..0ddada2 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -24,6 +24,7 @@ import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_repo.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; @@ -166,6 +167,7 @@ class AppDependencies { () => AuthenticationViewModel( authenticationRepo: getIt(), cacheService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), localAuthService: getIt()), ); + getIt.registerLazySingleton(() => ProfileSettingsViewModel()); // Screen-specific VMs → Factory // getIt.registerFactory( diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index c034a69..bec5470 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -275,7 +275,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { 'PatientShareRequestID': patientShareRequestID, 'ResponseID': responseID, 'Status': 3, - 'PatientID': appState.getAuthenticatedUser()?.patientId ?? 0, + // 'PatientID': appState.getAuthenticatedUser()?.patientId ?? 0, 'LogInTokenID': appState.getFamilyFileTokenID, 'activationCode': activationCode ?? "0000", 'PatientMobileNumber': newRequest.patientMobileNumber, diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 467a476..444e54e 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -550,7 +550,7 @@ class AuthenticationViewModel extends ChangeNotifier { if (!_appState.getIsChildLoggedIn) { await medicalVm.getFamilyFiles(status: 0); await medicalVm.getAllPendingRecordsByResponseId(); - _navigationService.popUntilNamed(AppRoutes.medicalFilePage); + _navigationService.popUntilNamed(AppRoutes.landingScreen); } } else { if (activation.list != null && activation.list!.isNotEmpty) { @@ -567,12 +567,13 @@ class AuthenticationViewModel extends ChangeNotifier { } else { activation.list!.first.isParentUser = true; } + activation.list!.first.bloodGroup = activation.patientBlodType; _appState.setAuthenticatedUser(activation.list!.first); _appState.setPrivilegeModelList(activation.list!.first.listPrivilege!); } - _appState.setUserBloodGroup = (activation.patientBlodType ?? ""); + // _appState.setUserBloodGroup = (activation.patientBlodType ?? ""); _appState.setAppAuthToken = activation.authenticationTokenId; - final request = RequestUtils.getAuthanticatedCommonRequest().toJson(); + final request = await RequestUtils.getAuthanticatedCommonRequest().toJson(); bool isUserAgreedBefore = await checkIfUserAgreedBefore(request: request); //updating the last login type in app state to show the fingerprint/face id option on home screen @@ -583,9 +584,9 @@ class AuthenticationViewModel extends ChangeNotifier { // if (!isSwitchUser && !_appState.getIsChildLoggedIn) { MedicalFileViewModel medicalVm = getIt(); - insertPatientIMEIData(loginTypeEnum.toInt); + await insertPatientIMEIData(loginTypeEnum.toInt); await medicalVm.getFamilyFiles(status: 0); //TODO: Remove status: 1 by Aamir Need to Discuss With Sultan - // medicalVm.getAllPendingRecordsByResponseId(); + await medicalVm.getAllPendingRecordsByResponseId(); } await clearDefaultInputValues(); diff --git a/lib/features/medical_file/medical_file_repo.dart b/lib/features/medical_file/medical_file_repo.dart index 416c372..a460d9f 100644 --- a/lib/features/medical_file/medical_file_repo.dart +++ b/lib/features/medical_file/medical_file_repo.dart @@ -29,7 +29,7 @@ abstract class MedicalFileRepo { Future>>> getAllPendingRecordsByResponseId({required Map request}); - Future>>> addFamilyFile({required dynamic request}); + Future>> addFamilyFile({required dynamic request}); Future>>> getPatientAppointmentsForMedicalReport(); diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 60d14b0..b5d9d03 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -89,9 +89,6 @@ class MedicalFileViewModel extends ChangeNotifier { void onFamilyFileTabChange(int index) { setSelectedFamilyFileTabIndex = index; - if (index == 1) { - // getAllPendingRecordsByResponseId(); - } notifyListeners(); } @@ -323,8 +320,6 @@ class MedicalFileViewModel extends ChangeNotifier { final isPending = element.status == FamilyFileEnum.pending.toInt || element.status == FamilyFileEnum.rejected.toInt; final isActive = element.status == FamilyFileEnum.active.toInt; - print("====== Element Status: ${element.status}, isPending: $isPending, isActive: $isActive ============"); - if (!isPending && !isActive) { continue; } @@ -423,9 +418,6 @@ class MedicalFileViewModel extends ChangeNotifier { } // pendingFamilyFiles.addAll(tempPendingFamilyFiles.where((element) => !pendingFamilyFiles.any((e) => e.responseId == element.responseId))); pendingFamilyFiles.addAll(tempPendingFamilyFiles.where((element) => !pendingFamilyFiles.any((e) => e.patientId == element.patientId))); - - print("====== Pending Family Length: ${pendingFamilyFiles.length} ============"); - print("====== Pending Family Files: ${jsonEncode(pendingFamilyFiles)} ============"); } notifyListeners(); } @@ -447,7 +439,7 @@ class MedicalFileViewModel extends ChangeNotifier { ); } - Future addFamilyFile({required OTPTypeEnum otpTypeEnum, required bool isExcludedUser}) async { + Future addFamilyFile({required OTPTypeEnum otpTypeEnum}) async { LoaderBottomSheet.showLoader(); AuthenticationViewModel authVM = getIt.get(); NavigationService navigationService = getIt.get(); diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 42932b1..afaa1c8 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -75,6 +75,11 @@ class _LandingPageState extends State { void initState() { authVM = context.read(); habibWalletVM = context.read(); + myAppointmentsViewModel = context.read(); + prescriptionsViewModel = context.read(); + insuranceViewModel = context.read(); + immediateLiveCareViewModel = context.read(); + authVM.savePushTokenToAppState(); if (mounted) { authVM.checkLastLoginStatus(() { @@ -100,11 +105,6 @@ class _LandingPageState extends State { @override Widget build(BuildContext context) { appState = getIt.get(); - NavigationService navigationService = getIt.get(); - myAppointmentsViewModel = Provider.of(context, listen: false); - prescriptionsViewModel = Provider.of(context, listen: false); - insuranceViewModel = Provider.of(context, listen: false); - immediateLiveCareViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: SingleChildScrollView( diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 92396a8..674732a 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -81,10 +81,10 @@ class _MedicalFilePageState extends State { insuranceViewModel.initInsuranceProvider(); medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.getPatientSickLeaveList(); - if (appState.getSuperUserID == null) { - medicalFileViewModel.getFamilyFiles(status: 0); //TODO: Remove status: 1 by Aamir Need to Discuss With Sultan - medicalFileViewModel.getAllPendingRecordsByResponseId(); //TODO: Added By Aamir - } + // if (appState.getSuperUserID == null) { + // medicalFileViewModel.getFamilyFiles(status: 0); //TODO: Remove status: 1 by Aamir Need to Discuss With Sultan + // medicalFileViewModel.getAllPendingRecordsByResponseId(); //TODO: Added By Aamir + // } medicalFileViewModel.onTabChanged(0); } diff --git a/lib/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart index 7de92f4..c1773b8 100644 --- a/lib/presentation/my_family/my_family.dart +++ b/lib/presentation/my_family/my_family.dart @@ -40,7 +40,6 @@ class FamilyMedicalScreen extends StatefulWidget { } class _FamilyMedicalScreenState extends State { - List tabs = [CustomTabBarModel(null, LocaleKeys.medicalFile.tr()), CustomTabBarModel(null, LocaleKeys.request.tr())]; MedicalFileViewModel? medicalVM; @override @@ -52,6 +51,7 @@ class _FamilyMedicalScreenState extends State { @override Widget build(BuildContext context) { AppState appState = getIt.get(); + return CollapsingListView( title: "My Medical File".needTranslation, bottomChild: appState.getAuthenticatedUser()!.isParentUser! @@ -69,7 +69,7 @@ class _FamilyMedicalScreenState extends State { label: "Add Family Member".needTranslation, message: "Please fill the below field to add a new family member to your profile".needTranslation, onVerificationPress: () { - medicalVM?.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true); + medicalVM!.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); }); }, icon: AppAssets.add_icon, @@ -81,13 +81,17 @@ class _FamilyMedicalScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTabBar( - tabs: tabs, + activeBackgroundColor: AppColors.secondaryLightRedColor, + activeTextColor: AppColors.primaryRedColor, + tabs: appState.isChildLoggedIn + ? [CustomTabBarModel(null, LocaleKeys.medicalFile.tr())] + : [CustomTabBarModel(null, LocaleKeys.medicalFile.tr()), CustomTabBarModel(null, LocaleKeys.request.tr())], onTabChange: (index) { medicalVM!.onFamilyFileTabChange(index); }, ), SizedBox(height: 25.h), - Consumer(builder: (context, medicalVM, child) => getFamilyTabs(index: medicalVM.getSelectedFamilyFileTabIndex)), + Selector(selector: (_, model) => model.getSelectedFamilyFileTabIndex, builder: (context, selectedIndex, child) => getFamilyTabs(index: selectedIndex)), SizedBox(height: 20.h), ], ).paddingSymmetrical(20, 0), @@ -97,7 +101,6 @@ class _FamilyMedicalScreenState extends State { Widget getFamilyTabs({required int index}) { switch (index) { case 0: - print(jsonEncode(medicalVM!.patientFamilyFiles)); return FamilyCards( profiles: medicalVM!.patientFamilyFiles, onSelect: (FamilyFileResponseModelLists profile) { @@ -111,7 +114,6 @@ class _FamilyMedicalScreenState extends State { isShowRemoveButton: true, ); case 1: - print(jsonEncode(medicalVM!.pendingFamilyFiles)); return FamilyCards( profiles: medicalVM!.pendingFamilyFiles, isRequestDesign: true, diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 166b06d..92c85af 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -10,6 +10,8 @@ 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/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; @@ -42,10 +44,9 @@ class FamilyCards extends StatefulWidget { class _FamilyCardsState extends State { AppState appState = getIt(); - // bool isShowActions = true; - @override Widget build(BuildContext context) { + DialogService dialogService = getIt.get(); if (widget.isRequestDesign) { return Column( children: [ @@ -53,7 +54,14 @@ class _FamilyCardsState extends State { children: [ Utils.buildSvgWithAssets(icon: AppAssets.alertSquare), SizedBox(width: 8.h), - "Sent Requests".needTranslation.toText14(color: AppColors.textColor, isUnderLine: true, weight: FontWeight.w500), + "Who can view my medical file ?".needTranslation.toText14(color: AppColors.textColor, isUnderLine: true, weight: FontWeight.w500).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Manage Family".needTranslation, + message: "", + child: manageFamily(), + onOkPressed: () {}, + ); + }), SizedBox(width: 4.h), Utils.buildSvgWithAssets(icon: AppAssets.arrowRight), ], @@ -67,100 +75,6 @@ class _FamilyCardsState extends State { itemBuilder: (context, index) { final mySideProfiles = widget.profiles.where((profile) => profile.isRequestFromMySide ?? false).toList(); FamilyFileResponseModelLists profile = mySideProfiles[index]; - - return Container( - margin: EdgeInsets.only( - bottom: 12.h, - ), - padding: EdgeInsets.symmetric( - vertical: 15.h, - horizontal: 15.h, - ), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), - child: Opacity( - opacity: 1.0, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: profile.status == FamilyFileEnum.pending.toInt - ? AppColors.alertLightColor.withValues(alpha: 0.20) - : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor.withValues(alpha: 0.20) - : profile.status == FamilyFileEnum.active.toInt - ? AppColors.lightGreenColor - : AppColors.lightGrayBGColor, - chipText: profile.statusDescription ?? "N/A", - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: profile.status == FamilyFileEnum.pending.toInt - ? AppColors.alertLightColor - : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor - : profile.status == FamilyFileEnum.active.toInt - ? AppColors.textGreenColor - : AppColors.alertColor, - ), - Wrap( - alignment: WrapAlignment.start, - children: [ - (profile.patientName ?? "").toText16( - isBold: false, - isCenter: true, - maxlines: 1, - weight: FontWeight.w600, - ), - ("has ${(profile.statusDescription ?? "").toLowerCase()} your family member request").toText14( - isBold: false, - isCenter: true, - maxlines: 1, - weight: FontWeight.w500, - color: AppColors.greyTextColor, - ), - ], - ), - SizedBox(height: 4.h), - CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: "Medical File: ${profile.responseId ?? "N/A"}", - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor, - ), - ], - ), - ), - ); - }, - ), - SizedBox(height: 20.h), - if (widget.profiles.where((profile) => !(profile.isRequestFromMySide ?? false)).isNotEmpty) - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.alertSquare), - SizedBox(width: 8.h), - "Users who want to view your profile".needTranslation.toText14(color: AppColors.textColor, isUnderLine: true, weight: FontWeight.w500), - SizedBox(width: 4.h), - Utils.buildSvgWithAssets(icon: AppAssets.arrowRight), - ], - ), - - // Items for second group (requests from others) - ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: widget.profiles.where((profile) => !(profile.isRequestFromMySide ?? false)).length, - itemBuilder: (context, index) { - final otherProfiles = widget.profiles.where((profile) => !(profile.isRequestFromMySide ?? false)).toList(); - FamilyFileResponseModelLists profile = otherProfiles[index]; return Container( margin: EdgeInsets.only(bottom: 12.h), padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 15.h), @@ -173,92 +87,49 @@ class _FamilyCardsState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: profile.status == FamilyFileEnum.pending.toInt - ? AppColors.alertLightColor.withValues(alpha: 0.20) - : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor.withValues(alpha: 0.20) - : profile.status == FamilyFileEnum.active.toInt - ? AppColors.lightGreenColor - : AppColors.lightGrayBGColor, - chipText: profile.statusDescription ?? "N/A", - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: profile.status == FamilyFileEnum.pending.toInt - ? AppColors.alertLightColor - : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor - : profile.status == FamilyFileEnum.active.toInt - ? AppColors.textGreenColor - : AppColors.alertColor, - ), - Wrap( - alignment: WrapAlignment.start, - children: [ - (profile.patientName ?? "").toText16(isBold: false, isCenter: true, maxlines: 1, weight: FontWeight.w600), - (profile.status == FamilyFileEnum.active.toInt ? "can view your family".needTranslation : "wants to add you as their family member".needTranslation).toText14( - isBold: false, - isCenter: true, - maxlines: 1, - weight: FontWeight.w500, - color: AppColors.greyTextColor, - ), - ], - ), - SizedBox(height: 4.h), + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.lightGreenColor + : AppColors.lightGrayBGColor, + chipText: profile.statusDescription ?? "N/A", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.textGreenColor + : AppColors.alertColor), + SizedBox(height: 8.h), + Wrap(alignment: WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.start, runAlignment: WrapAlignment.start, spacing: 0.h, children: [ + (profile.patientName ?? "").toText14(isBold: false, isCenter: false, maxlines: 1, weight: FontWeight.w600), + (getStatusTextByRequest(FamilyFileEnum.values.firstWhere((e) => e.toInt == profile.status), profile.isRequestFromMySide ?? false)) + .toText14(isBold: false, isCenter: false, maxlines: 1, weight: FontWeight.w500, color: AppColors.greyTextColor), + ]), + SizedBox(height: 8.h), CustomChipWidget( - height: 30.h, - chipType: ChipTypeEnum.alert, - backgroundColor: AppColors.lightGrayBGColor, - chipText: "Medical File: ${profile.patientId ?? "N/A".needTranslation}", - iconAsset: null, - isShowBorder: false, - borderRadius: 8.h, - textColor: AppColors.textColor, - ), - SizedBox(height: 16.h), - Row( - children: [ - profile.status == FamilyFileEnum.active.toInt - ? SizedBox() - : Expanded( - child: CustomButton( - height: 40.h, - text: LocaleKeys.confirm.tr(), - onPressed: () { - widget.onSelect(profile); - }, - backgroundColor: AppColors.lightGreenButtonColor, - borderColor: AppColors.lightGreenButtonColor, - textColor: AppColors.textGreenColor, - icon: null, - ), - ), - profile.status == FamilyFileEnum.active.toInt ? SizedBox() : SizedBox(width: 8.h), - Expanded( - child: CustomButton( - height: 40.h, - text: profile.status == FamilyFileEnum.active.toInt ? LocaleKeys.removeMember.tr() : LocaleKeys.cancel.tr(), - onPressed: () { - widget.onRemove(profile); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - icon: null, - iconColor: AppColors.primaryRedColor, - ), - ), - ], - ), + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "Medical File: ${profile.responseId ?? "N/A"}", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), ], ), ), ); }, ), + SizedBox(height: 20.h), ], ); } else { @@ -270,7 +141,7 @@ class _FamilyCardsState extends State { crossAxisCount: 2, crossAxisSpacing: 10.h, mainAxisSpacing: 10.h, - childAspectRatio: widget.isShowDetails ? 0.56.h : 0.66.h, + childAspectRatio: widget.isShowDetails ? 0.56.h : 0.65.h, ), padding: EdgeInsets.only(bottom: 20.h), itemBuilder: (context, index) { @@ -297,8 +168,8 @@ class _FamilyCardsState extends State { width: 80.h, height: 78.h), SizedBox(height: 8.h), - (profile.patientName ?? "Unknown").toText16(isBold: false, isCenter: true, maxlines: 1, weight: FontWeight.w600), - SizedBox(height: 4.h), + (profile.patientName ?? "Unknown").toText14(isBold: false, isCenter: true, maxlines: 1, weight: FontWeight.w600), + SizedBox(height: 8.h), CustomChipWidget( chipType: ChipTypeEnum.alert, backgroundColor: AppColors.lightGrayBGColor, @@ -318,7 +189,11 @@ class _FamilyCardsState extends State { textColor: AppColors.textColor, ) : SizedBox(), - widget.isShowDetails ? SizedBox(height: 8.h) : SizedBox(), + widget.isShowDetails + ? SizedBox(height: 8.h) + : SizedBox( + height: 4.h, + ), Spacer(), CustomButton( height: 40.h, @@ -353,4 +228,144 @@ class _FamilyCardsState extends State { ); } } + + Widget manageFamily() { + NavigationService navigationService = getIt(); + return ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsetsGeometry.zero, + itemCount: widget.profiles.where((profile) => !(profile.isRequestFromMySide ?? false)).length, + itemBuilder: (context, index) { + final otherProfiles = widget.profiles.where((profile) => !(profile.isRequestFromMySide ?? false)).toList(); + FamilyFileResponseModelLists profile = otherProfiles[index]; + return Container( + margin: EdgeInsets.only(bottom: 12.h), + padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 15.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), + child: Opacity( + opacity: 1.0, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.lightGreenColor + : AppColors.lightGrayBGColor, + chipText: profile.statusDescription ?? "N/A", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.textGreenColor + : AppColors.alertColor, + ), + SizedBox(height: 8.h), + Wrap( + alignment: WrapAlignment.start, + children: [ + (profile.patientName ?? "").toText14(isBold: false, isCenter: true, maxlines: 1, weight: FontWeight.w600), + (getStatusTextByRequest(FamilyFileEnum.values.firstWhere((e) => e.toInt == profile.status), profile.isRequestFromMySide ?? false)).toText14( + isBold: false, + isCenter: true, + maxlines: 1, + weight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + ], + ), + SizedBox(height: 8.h), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "Medical File: ${profile.patientId ?? "N/A".needTranslation}", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor, + ), + SizedBox(height: 16.h), + Row( + children: [ + profile.status == FamilyFileEnum.active.toInt + ? SizedBox() + : Expanded( + child: CustomButton( + height: 40.h, + text: LocaleKeys.confirm.tr(), + onPressed: () { + navigationService.pop(); + widget.onSelect(profile); + }, + backgroundColor: AppColors.lightGreenButtonColor, + borderColor: AppColors.lightGreenButtonColor, + textColor: AppColors.textGreenColor, + icon: null, + ), + ), + profile.status == FamilyFileEnum.active.toInt ? SizedBox() : SizedBox(width: 8.h), + Expanded( + child: CustomButton( + height: 40.h, + text: profile.status == FamilyFileEnum.active.toInt ? LocaleKeys.removeMember.tr() : LocaleKeys.cancel.tr(), + onPressed: () { + navigationService.pop(); + widget.onRemove(profile); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + icon: null, + iconColor: AppColors.primaryRedColor, + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } + + String getStatusTextByRequest(FamilyFileEnum status, bool isRequestFromMySide) { + switch (status) { + case FamilyFileEnum.active: + if (isRequestFromMySide) { + return "${status.displayName} your request to be your family member".needTranslation; + } else { + return "can view your file".needTranslation; + } + case FamilyFileEnum.pending: + if (isRequestFromMySide) { + return "has a request ${status.displayName} to be your family member".needTranslation; + } else { + return "wants to add you as their family member".needTranslation; + } + case FamilyFileEnum.rejected: + if (isRequestFromMySide) { + return "${status.displayName} your request to be your family member".needTranslation; + } else { + return "${status.displayName} your family member request".needTranslation; + } + case FamilyFileEnum.inactive: + return "Inactive".needTranslation; + default: + return "N/A".needTranslation; + } + } } diff --git a/lib/presentation/my_family/widget/my_family_sheet.dart b/lib/presentation/my_family/widget/my_family_sheet.dart index 1c085ca..d469ab2 100644 --- a/lib/presentation/my_family/widget/my_family_sheet.dart +++ b/lib/presentation/my_family/widget/my_family_sheet.dart @@ -1,26 +1,29 @@ import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/presentation/my_family/widget/family_cards.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; class MyFamilySheet { static Future show(BuildContext context, List familyLists, Function(FamilyFileResponseModelLists) onSelect) async { + NavigationService navigationService = getIt(); return showCommonBottomSheetWithoutHeight( context, titleWidget: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - 'Please select a profile'.toText21(isBold: true), - 'switch from the below list of medical file'.toText16(weight: FontWeight.w100, color: AppColors.greyTextColor), + 'Please select a profile'.needTranslation.toText21(isBold: true), + 'switch from the below list of medical file'.needTranslation.toText16(weight: FontWeight.w100, color: AppColors.greyTextColor), ], ), child: FamilyCards( profiles: familyLists, onSelect: (profile) { - Navigator.of(context).pop(); // Close the bottom sheet - onSelect(profile); // Call the onSelect callback + navigationService.pop(); + onSelect(profile); }, onRemove: (profile) {}, isBottomSheet: true), diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index d40104b..3954dd7 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -59,11 +59,8 @@ class _ProfileSettingsState extends State { int length = 3; final SwiperController _controller = SwiperController(); - int _index = 0; - @override Widget build(BuildContext context) { - final MedicalFileViewModel medicalFileViewModel = getIt.get(); return CollapsingListView( title: "Profile & Settings".needTranslation, logout: () {}, @@ -71,28 +68,41 @@ class _ProfileSettingsState extends State { child: SingleChildScrollView( padding: EdgeInsets.only(top: 24, bottom: 24), physics: NeverScrollableScrollPhysics(), - child: Consumer( - builder: (context, model, child) { - print(jsonEncode(medicalFileViewModel.patientFamilyFiles)); + child: Consumer2( + builder: (context, profileVm, medicalVm, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Swiper( - itemCount: medicalFileViewModel.patientFamilyFiles.length, + itemCount: medicalVm.patientFamilyFiles.length, layout: SwiperLayout.STACK, loop: true, itemWidth: MediaQuery.of(context).size.width - 42, indicatorLayout: PageIndicatorLayout.COLOR, axisDirection: AxisDirection.right, controller: _controller, - itemHeight: 210 + 16, + itemHeight: 220 + 16, pagination: const SwiperPagination( alignment: Alignment.bottomCenter, margin: EdgeInsets.only(top: 210 + 8 + 24), builder: DotSwiperPaginationBuilder(color: Color(0xffD9D9D9), activeColor: AppColors.blackBgColor), ), itemBuilder: (BuildContext context, int index) { - return FamilyCardWidget(isRootUser: true).paddingOnly(right: 16); + return FamilyCardWidget( + profile: medicalVm.patientFamilyFiles[index], + onAddFamilyMemberPress: () { + DialogService dialogService = getIt.get(); + dialogService.showAddFamilyFileSheet( + label: "Add Family Member".needTranslation, + message: "Please fill the below field to add a new family member to your profile".needTranslation, + onVerificationPress: () { + medicalVm.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); + }); + }, + onFamilySwitchPress: (FamilyFileResponseModelLists profile) { + medicalVm.switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); + }, + ).paddingOnly(right: 16); }, ), GridView( @@ -241,15 +251,12 @@ class _ProfileSettingsState extends State { } class FamilyCardWidget extends StatelessWidget { - FamilyCardWidget({this.isRootUser = true, Key? key}) : super(key: key); - - bool isRootUser; late AppState appState; final Function() onAddFamilyMemberPress; final Function(FamilyFileResponseModelLists member) onFamilySwitchPress; final FamilyFileResponseModelLists profile; - const FamilyCardWidget({required this.onAddFamilyMemberPress, required this.profile, required this.onFamilySwitchPress(FamilyFileResponseModelLists member)}); + FamilyCardWidget({super.key, required this.onAddFamilyMemberPress, required this.profile, required this.onFamilySwitchPress(FamilyFileResponseModelLists member)}); @override Widget build(BuildContext context) { @@ -268,17 +275,16 @@ class FamilyCardWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: 8.h, children: [ - Image.asset(appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, width: 56.h, height: 56.h), + Image.asset(profile.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, width: 56.h, height: 56.h), Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 0.h, mainAxisSize: MainAxisSize.min, children: [ - "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" - .toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1), + "${profile.patientName}".toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1), AppCustomChipWidget( icon: AppAssets.file_icon, - labelText: "${LocaleKeys.fileNo.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}", + labelText: "${LocaleKeys.fileNo.tr(context: context)}: ${profile.patientId}", iconSize: 12, ), ], @@ -291,46 +297,137 @@ class FamilyCardWidget extends StatelessWidget { child: Wrap( alignment: WrapAlignment.start, spacing: 8.h, + runSpacing: 8.h, children: [ - AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), + AppCustomChipWidget(labelText: "${profile.age} Years Old"), AppCustomChipWidget( - icon: AppAssets.blood_icon, - labelText: "${LocaleKeys.bloodType.tr(context: context)}: ${appState.getUserBloodGroup}", - iconColor: AppColors.primaryRedColor, - ), - Consumer(builder: (context, insuranceVM, child) { - return AppCustomChipWidget( - icon: insuranceVM.isInsuranceLoading - ? AppAssets.cancel_circle_icon - : (DateTime.now().isAfter( - DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), - )) - ? AppAssets.cancel_circle_icon - : AppAssets.insurance_active_icon, - labelText: insuranceVM.isInsuranceLoading - ? "Insurance" - : (DateTime.now().isAfter( - DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), - ) - ? "Insurance Expired".needTranslation - : "Insurance Active".needTranslation), - iconColor: insuranceVM.isInsuranceLoading - ? AppColors.primaryRedColor - : (DateTime.now().isAfter( - DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), - )) - ? AppColors.primaryRedColor - : AppColors.successColor, - iconSize: 14, - backgroundColor: insuranceVM.isInsuranceLoading - ? AppColors.primaryRedColor - : (DateTime.now().isAfter( - DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), - )) - ? AppColors.primaryRedColor.withValues(alpha: 0.15) - : AppColors.successColor.withValues(alpha: 0.15), - ).toShimmer2(isShow: insuranceVM.isInsuranceLoading); - }), + icon: AppAssets.blood_icon, labelText: "${LocaleKeys.bloodType.tr()}: ${appState.getAuthenticatedUser()!.bloodGroup ?? "N/A"}", iconColor: AppColors.primaryRedColor), + + Selector( + selector: (context, insuranceVM) => ( + isEmpty: insuranceVM.patientInsuranceList.isEmpty, + patientID: insuranceVM.patientInsuranceList.isNotEmpty ? insuranceVM.patientInsuranceList.first.patientID : null, + isLoading: insuranceVM.isInsuranceLoading, + cardValidTo: insuranceVM.patientInsuranceList.isNotEmpty ? insuranceVM.patientInsuranceList.first.cardValidTo : null + ), + builder: (context, data, child) { + if (data.isEmpty) { + return const SizedBox(); + } else if (profile.responseId != data.patientID) { + return SizedBox(); + } + + final isLoading = data.isLoading; + final isExpired = !isLoading && DateTime.now().isAfter(DateUtil.convertStringToDate(data.cardValidTo)); + + final String icon; + final String labelText; + final Color iconColor; + final Color backgroundColor; + + if (isLoading) { + icon = AppAssets.cancel_circle_icon; + labelText = "Insurance"; + iconColor = AppColors.primaryRedColor; + backgroundColor = AppColors.primaryRedColor; + } else if (isExpired) { + icon = AppAssets.cancel_circle_icon; + labelText = "Insurance Expired".needTranslation; + iconColor = AppColors.primaryRedColor; + backgroundColor = AppColors.primaryRedColor.withValues(alpha: 0.15); + } else { + icon = AppAssets.insurance_active_icon; + labelText = "Insurance Active".needTranslation; + iconColor = AppColors.successColor; + backgroundColor = AppColors.successColor.withValues(alpha: 0.15); + } + + return AppCustomChipWidget( + icon: icon, + labelText: labelText, + iconColor: iconColor, + iconSize: 12, + backgroundColor: backgroundColor, + // padding: EdgeInsets.zero, + ).toShimmer2(isShow: isLoading); + }, + ) + + // Consumer(builder: (context, insuranceVM, child) { + // if (insuranceVM.patientInsuranceList.isEmpty) { + // return const SizedBox(); + // } else if (profile.responseId != insuranceVM.patientInsuranceList.first.patientID) { + // return SizedBox(); + // } + // + // final isLoading = insuranceVM.isInsuranceLoading; + // final isExpired = !isLoading && DateTime.now().isAfter(DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo)); + // + // final String icon; + // final String labelText; + // final Color iconColor; + // final Color backgroundColor; + // + // if (isLoading) { + // icon = AppAssets.cancel_circle_icon; + // labelText = "Insurance"; + // iconColor = AppColors.primaryRedColor; + // backgroundColor = AppColors.primaryRedColor; + // } else if (isExpired) { + // icon = AppAssets.cancel_circle_icon; + // labelText = "Insurance Expired".needTranslation; + // iconColor = AppColors.primaryRedColor; + // backgroundColor = AppColors.primaryRedColor.withValues(alpha: 0.15); + // } else { + // icon = AppAssets.insurance_active_icon; + // labelText = "Insurance Active".needTranslation; + // iconColor = AppColors.successColor; + // backgroundColor = AppColors.successColor.withValues(alpha: 0.15); + // } + // + // return AppCustomChipWidget( + // icon: icon, + // labelText: labelText, + // iconColor: iconColor, + // iconSize: 12, + // backgroundColor: backgroundColor, + // // padding: EdgeInsets.zero, + // ).toShimmer2(isShow: isLoading); + // }) + + // Consumer(builder: (context, insuranceVM, child) { + // return insuranceVM.patientInsuranceList. isNotEmpty ? AppCustomChipWidget( + // icon: insuranceVM.isInsuranceLoading + // ? AppAssets.cancel_circle_icon + // : (DateTime.now().isAfter( + // DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), + // )) + // ? AppAssets.cancel_circle_icon + // : AppAssets.insurance_active_icon, + // labelText: insuranceVM.isInsuranceLoading + // ? "Insurance" + // : (DateTime.now().isAfter( + // DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), + // ) + // ? "Insurance Expired".needTranslation + // : "Insurance Active".needTranslation), + // iconColor: insuranceVM.isInsuranceLoading + // ? AppColors.primaryRedColor + // : (DateTime.now().isAfter( + // DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), + // )) + // ? AppColors.primaryRedColor + // : AppColors.successColor, + // iconSize: 14, + // backgroundColor: insuranceVM.isInsuranceLoading + // ? AppColors.primaryRedColor + // : (DateTime.now().isAfter( + // DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo), + // )) + // ? AppColors.primaryRedColor.withValues(alpha: 0.15) + // : AppColors.successColor.withValues(alpha: 0.15), + // ).toShimmer2(isShow: insuranceVM.isInsuranceLoading) : SizedBox(); + // }), ], ), ), @@ -426,8 +523,7 @@ class FamilyCardWidget extends StatelessWidget { ).paddingOnly(top: 12, right: 16, left: 16, bottom: 16); } - - // //TODO: Add family file switch logic here +// //TODO: Add family file switch logic here // isRootUser // ? CustomButton(icon: AppAssets.add_family, text: "Add a new family member".needTranslation, height: 40.h, fontSize: 14, onPressed: () {}) // .paddingOnly(top: 12, right: 16, left: 16, bottom: 16) diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 4d332b3..7003a31 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -24,6 +24,8 @@ abstract class DialogService { Future showFamilyBottomSheetWithoutH( {String? label, required String message, required Function(FamilyFileResponseModelLists response) onSwitchPress, required List profiles}); + Future showFamilyBottomSheetWithoutHWithChild({String? label, required String message, Widget? child, required Function() onOkPressed, Function()? onCancelPressed}); + Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}); Future showAddFamilyFileSheet({String? label, String? message, required Function() onVerificationPress}); @@ -119,6 +121,18 @@ class DialogServiceImp implements DialogService { callBackFunc: () {}); } + @override + Future showFamilyBottomSheetWithoutHWithChild({String? label, required String message, Widget? child, required Function() onOkPressed, Function()? onCancelPressed}) async { + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + showCommonBottomSheetWithoutHeight( + context, + title: label ?? "", + child: child ?? SizedBox(), + callBackFunc: () {}, + ); + } + @override Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { final context = navigationService.navigatorKey.currentContext; diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index 38552fb..10b98bc 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -24,8 +24,8 @@ class AppCustomChipWidget extends StatelessWidget { this.deleteIconColor = AppColors.textColor, this.deleteIconHasColor = false, this.padding = EdgeInsets.zero, - this.onChipTap - this.labelPadding , + this.onChipTap, + this.labelPadding, }); final String? labelText; @@ -47,54 +47,58 @@ class AppCustomChipWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return ChipTheme( - data: ChipThemeData( - padding: EdgeInsets.all(0.0), - shape: SmoothRectangleBorder( - side: BorderSide( - width: 10.0, - color: Colors.transparent, // Crucially, set color to transparent - style: BorderStyle.none, + return GestureDetector( + onTap: onChipTap, + child: ChipTheme( + data: ChipThemeData( + padding: EdgeInsets.all(0.0), + shape: SmoothRectangleBorder( + side: BorderSide( + width: 10.0, + color: Colors.transparent, // Crucially, set color to transparent + style: BorderStyle.none, + ), + borderRadius: BorderRadius.circular(8.0), // Apply a border radius of 16.0 ), - borderRadius: BorderRadius.circular(8.0), // Apply a border radius of 16.0 ), - ), - child: icon.isNotEmpty - ? Chip( - avatar: icon.isNotEmpty ? Utils.buildSvgWithAssets(icon: icon, width: iconSize.h, height: iconSize.h, iconColor: iconHasColor ? iconColor : null) : SizedBox.shrink(), - label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), - // padding: EdgeInsets.all(0.0), - padding: padding, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - labelPadding: labelPadding??EdgeInsetsDirectional.only(start: 0.h, end: deleteIcon?.isNotEmpty == true ? 2.h : 8.h), - backgroundColor: backgroundColor, - shape: shape ?? - SmoothRectangleBorder( - borderRadius: BorderRadius.circular(8 ?? 0), - smoothness: 10, - side: BorderSide(color: AppColors.transparent, width: 1.5), - ), - deleteIcon: deleteIcon?.isNotEmpty == true - ? Utils.buildSvgWithAssets(icon: deleteIcon!, width: deleteIconSize!.width!.h, height: deleteIconSize!.height.h, iconColor: deleteIconHasColor ? deleteIconColor : null) - : null, - onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, - ) - : Chip( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), - padding: EdgeInsets.all(0.0), - backgroundColor: backgroundColor, - shape: shape ?? SmoothRectangleBorder( - borderRadius: BorderRadius.circular(8 ?? 0), - smoothness: 10, - side: BorderSide(color: AppColors.transparent, width: 1.5), + child: icon.isNotEmpty + ? Chip( + avatar: icon.isNotEmpty ? Utils.buildSvgWithAssets(icon: icon, width: iconSize.h, height: iconSize.h, iconColor: iconHasColor ? iconColor : null) : SizedBox.shrink(), + label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), + // padding: EdgeInsets.all(0.0), + padding: padding, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + labelPadding: labelPadding ?? EdgeInsetsDirectional.only(start: 0.h, end: deleteIcon?.isNotEmpty == true ? 2.h : 8.h), + backgroundColor: backgroundColor, + shape: shape ?? + SmoothRectangleBorder( + borderRadius: BorderRadius.circular(8 ?? 0), + smoothness: 10, + side: BorderSide(color: AppColors.transparent, width: 1.5), + ), + deleteIcon: deleteIcon?.isNotEmpty == true + ? Utils.buildSvgWithAssets(icon: deleteIcon!, width: deleteIconSize!.width!.h, height: deleteIconSize!.height.h, iconColor: deleteIconHasColor ? deleteIconColor : null) + : null, + onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, + ) + : Chip( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), + padding: EdgeInsets.all(0.0), + backgroundColor: backgroundColor, + shape: shape ?? + SmoothRectangleBorder( + borderRadius: BorderRadius.circular(8 ?? 0), + smoothness: 10, + side: BorderSide(color: AppColors.transparent, width: 1.5), + ), + labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: deleteIcon?.isNotEmpty == true ? -2.h : 8.h), + deleteIcon: deleteIcon?.isNotEmpty == true + ? Utils.buildSvgWithAssets(icon: deleteIcon!, width: deleteIconSize!.width.h, height: deleteIconSize!.height.h, iconColor: deleteIconHasColor ? deleteIconColor : null) + : null, + onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, ), - labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: deleteIcon?.isNotEmpty == true ? -2.h : 8.h), - deleteIcon: deleteIcon?.isNotEmpty == true - ? Utils.buildSvgWithAssets(icon: deleteIcon!, width: deleteIconSize!.width.h, height: deleteIconSize!.height.h, iconColor: deleteIconHasColor ? deleteIconColor : null) - : null, - onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, - ), + ), ); } } From 25fa1acc9e64b1ae79bf5206c0c3cbb126f10b9f Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 7 Oct 2025 15:49:23 +0300 Subject: [PATCH 43/43] Medical file widgets, Insurance approval implementation contd. --- lib/core/api/api_client.dart | 4 +- lib/core/dependencies.dart | 5 + lib/features/insurance/insurance_repo.dart | 41 +- .../insurance/insurance_view_model.dart | 33 ++ ...ent_insurance_approval_response_model.dart | 465 ++++++++++++++++++ lib/features/lab/lab_view_model.dart | 30 +- .../appointment_details_page.dart | 2 +- .../appointment_payment_page.dart | 6 +- .../appointments/my_appointments_page.dart | 6 +- .../appointments/my_doctors_page.dart | 6 +- .../widgets/AppointmentFilter.dart | 4 +- .../widgets/appointment_card.dart | 55 ++- .../appointment_checkin_bottom_sheet.dart | 2 +- .../widgets/appointment_doctor_card.dart | 2 +- .../facility_selection_item.dart | 2 +- .../hospital_list_items.dart | 2 +- .../region_bottomsheet/region_list_item.dart | 2 +- .../book_appointment_page.dart | 12 +- .../book_appointment/doctor_profile_page.dart | 4 +- .../immediate_livecare_payment_page.dart | 6 +- .../widgets/livecare_clinic_card.dart | 2 +- .../review_appointment_page.dart | 4 +- .../wallet_payment_confirm_page.dart | 4 +- .../widgets/hospital_list_item.dart | 2 +- lib/presentation/home/landing_page.dart | 6 + .../insurance/insurance_approvals_page.dart | 109 ++++ .../insurance/insurance_home_page.dart | 1 - .../widgets/insurance_approval_card.dart | 129 +++++ .../insurance_approval_details_page.dart | 135 +++++ .../medical_file/medical_file_page.dart | 89 ++-- .../medical_file/vaccine_list_page.dart | 2 +- .../medical_file/widgets/lab_rad_card.dart | 27 +- .../widgets/patient_sick_leave_card.dart | 2 +- .../medical_report_request_page.dart | 2 + .../prescriptions_list_page.dart | 5 +- lib/widgets/appbar/collapsing_list_view.dart | 2 +- lib/widgets/chip/app_custom_chip_widget.dart | 2 +- 37 files changed, 1121 insertions(+), 91 deletions(-) create mode 100644 lib/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart create mode 100644 lib/presentation/insurance/insurance_approvals_page.dart create mode 100644 lib/presentation/insurance/widgets/insurance_approval_card.dart create mode 100644 lib/presentation/insurance/widgets/insurance_approval_details_page.dart diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 9140a13..a36f554 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -173,8 +173,8 @@ class ApiClientImp implements ApiClient { body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 4767884; + body['TokenID'] = "@dm!n"; + body['PatientID'] = 4772172; // body['PatientTypeID'] = 1; // // body['PatientOutSA'] = 0; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 638d80f..3ce8a19 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -19,6 +19,7 @@ import 'package:hmg_patient_app_new/features/lab/lab_repo.dart'; import 'package:hmg_patient_app_new/features/lab/lab_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_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'; import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart'; @@ -181,6 +182,10 @@ class AppDependencies { () => DoctorFilterViewModel(), ); + getIt.registerLazySingleton( + () => AppointmentViaRegionViewmodel(navigationService: getIt(), appState: getIt()), + ); + // Screen-specific VMs → Factory // getIt.registerFactory( // () => BookAppointmentsViewModel( diff --git a/lib/features/insurance/insurance_repo.dart b/lib/features/insurance/insurance_repo.dart index d367509..1aeb769 100644 --- a/lib/features/insurance/insurance_repo.dart +++ b/lib/features/insurance/insurance_repo.dart @@ -3,6 +3,7 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_card_history.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_details_response_model.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_update_response_model.dart'; @@ -14,6 +15,8 @@ abstract class InsuranceRepo { Future>>> getPatientInsuranceCardHistory(); Future>> getPatientInsuranceDetailsForUpdate({required String patientId, required String identificationNo}); + + Future>>> getPatientInsuranceApprovalsList(); } class InsuranceRepoImp implements InsuranceRepo { @@ -66,7 +69,6 @@ class InsuranceRepoImp implements InsuranceRepo { @override Future>>> getPatientInsuranceCardHistory() async { Map mapDevice = {}; - try { GenericApiModel>? apiResponse; Failure? failure; @@ -144,4 +146,41 @@ class InsuranceRepoImp implements InsuranceRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> getPatientInsuranceApprovalsList() async { + Map mapDevice = {}; + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_PAtIENTS_INSURANCE_APPROVALS, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['HIS_Approval_List']; + + final insuranceCardsList = list.map((item) => InsuranceApprovalResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: insuranceCardsList, + ); + } 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/insurance/insurance_view_model.dart b/lib/features/insurance/insurance_view_model.dart index 0bcf30f..363ad28 100644 --- a/lib/features/insurance/insurance_view_model.dart +++ b/lib/features/insurance/insurance_view_model.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_repo.dart'; +import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_card_history.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_details_response_model.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_update_response_model.dart'; @@ -14,11 +15,14 @@ class InsuranceViewModel extends ChangeNotifier { bool isInsuranceDataToBeLoaded = true; + bool isInsuranceApprovalsLoading = false; + InsuranceRepo insuranceRepo; ErrorHandlerService errorHandlerService; List patientInsuranceList = []; List patientInsuranceCardHistoryList = []; + List patientInsuranceApprovalsList = []; PatientInsuranceUpdateResponseModel? patientInsuranceUpdateResponseModel; @@ -34,6 +38,7 @@ class InsuranceViewModel extends ChangeNotifier { isInsuranceHistoryLoading = true; isInsuranceDetailsLoading = true; isInsuranceUpdateDetailsLoading = true; + isInsuranceApprovalsLoading = true; notifyListeners(); } @@ -57,6 +62,11 @@ class InsuranceViewModel extends ChangeNotifier { notifyListeners(); } + setIsInsuranceApprovalsLoading(bool val) { + isInsuranceApprovalsLoading = val; + notifyListeners(); + } + Future getPatientInsuranceDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { if (!isInsuranceDataToBeLoaded) return; @@ -130,4 +140,27 @@ class InsuranceViewModel extends ChangeNotifier { }, ); } + + Future getPatientInsuranceApprovalsList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await insuranceRepo.getPatientInsuranceApprovalsList(); + + result.fold( + (failure) async { + isInsuranceApprovalsLoading = false; + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + patientInsuranceApprovalsList = apiResponse.data!; + isInsuranceApprovalsLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } } diff --git a/lib/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart b/lib/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart new file mode 100644 index 0000000..f272f2d --- /dev/null +++ b/lib/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart @@ -0,0 +1,465 @@ +class InsuranceApprovalResponseModel { + String? setupID; + int? projectID; + int? approvalNo; + String? approvalDate; + int? patientType; + int? patientID; + int? companyID; + int? subCategoryID; + int? doctorID; + int? clinicID; + int? approvalType; + String? inpatientApprovalSubType; + String? validFrom; + String? vaildDays; + String? validTo; + bool? isApprovalOnGross; + bool? isPackage; + int? requestedAmount; + int? actualApprovedAmount; + int? aviliableAmount; + String? companyApprovalNo; + int? progNoteOrderNo; + String? submitOn; + String? receiptOn; + String? remarks1; + String? remarks2; + int? status; + String? feedbackStatusBy; + String? feedbackStatus; + String? feedbackStatusOn; + int? authorizerID; + String? expiryDate; + int? appointmentNo; + int? admissionNo; + int? admissionRequestNo; + int? createdBy; + String? createdOn; + int? editedBy; + String? editedOn; + String? rowVer; + bool? isAddlDiscApplied; + int? inProgressReasonID; + String? extendedBy; + String? extendedOn; + int? subPolicyNo; + int? noOrderAuthorizerID; + bool? isVerbalApproval; + String? subStatus; + bool? isNotificationSend; + String? eApprovalStatus; + String? eApprovalRemarks; + bool? isEmailSentOnDelayApproval; + int? eAuthorizationID; + InsuranceApprovalDetails? apporvalDetails; + String? approvalStatusDescption; + String? clinicName; + String? clinicNameN; + String? companyName; + String? doctorImageURL; + String? doctorName; + String? doctorNameN; + int? doctorRate; + String? doctorTitle; + int? gender; + String? genderDescription; + bool? isActiveDoctorProfile; + bool? isExecludeDoctor; + bool? isInOutPatient; + String? isInOutPatientDescription; + String? isInOutPatientDescriptionN; + bool? isLiveCareAppointment; + String? projectName; + String? projectNameN; + String? qR; + List? speciality; + String? strApprovalDate; + String? strExpiryDate; + String? strSubmitDate; + int? totaUnUsedCount; + int? unUsedCount; + + InsuranceApprovalResponseModel( + {this.setupID, + this.projectID, + this.approvalNo, + this.approvalDate, + this.patientType, + this.patientID, + this.companyID, + this.subCategoryID, + this.doctorID, + this.clinicID, + this.approvalType, + this.inpatientApprovalSubType, + this.validFrom, + this.vaildDays, + this.validTo, + this.isApprovalOnGross, + this.isPackage, + this.requestedAmount, + this.actualApprovedAmount, + this.aviliableAmount, + this.companyApprovalNo, + this.progNoteOrderNo, + this.submitOn, + this.receiptOn, + this.remarks1, + this.remarks2, + this.status, + this.feedbackStatusBy, + this.feedbackStatus, + this.feedbackStatusOn, + this.authorizerID, + this.expiryDate, + this.appointmentNo, + this.admissionNo, + this.admissionRequestNo, + this.createdBy, + this.createdOn, + this.editedBy, + this.editedOn, + this.rowVer, + this.isAddlDiscApplied, + this.inProgressReasonID, + this.extendedBy, + this.extendedOn, + this.subPolicyNo, + this.noOrderAuthorizerID, + this.isVerbalApproval, + this.subStatus, + this.isNotificationSend, + this.eApprovalStatus, + this.eApprovalRemarks, + this.isEmailSentOnDelayApproval, + this.eAuthorizationID, + this.apporvalDetails, + this.approvalStatusDescption, + this.clinicName, + this.clinicNameN, + this.companyName, + this.doctorImageURL, + this.doctorName, + this.doctorNameN, + this.doctorRate, + this.doctorTitle, + this.gender, + this.genderDescription, + this.isActiveDoctorProfile, + this.isExecludeDoctor, + this.isInOutPatient, + this.isInOutPatientDescription, + this.isInOutPatientDescriptionN, + this.isLiveCareAppointment, + this.projectName, + this.projectNameN, + this.qR, + this.speciality, + this.strApprovalDate, + this.strExpiryDate, + this.strSubmitDate, + this.totaUnUsedCount, + this.unUsedCount}); + + InsuranceApprovalResponseModel.fromJson(Map json) { + setupID = json['SetupID']; + projectID = json['ProjectID']; + approvalNo = json['ApprovalNo']; + approvalDate = json['ApprovalDate']; + patientType = json['PatientType']; + patientID = json['PatientID']; + companyID = json['CompanyID']; + subCategoryID = json['SubCategoryID']; + doctorID = json['DoctorID']; + clinicID = json['ClinicID']; + approvalType = json['ApprovalType']; + inpatientApprovalSubType = json['InpatientApprovalSubType']; + validFrom = json['ValidFrom']; + vaildDays = json['VaildDays']; + validTo = json['ValidTo']; + isApprovalOnGross = json['IsApprovalOnGross']; + isPackage = json['IsPackage']; + requestedAmount = json['RequestedAmount']; + actualApprovedAmount = json['ActualApprovedAmount']; + aviliableAmount = json['AviliableAmount']; + companyApprovalNo = json['CompanyApprovalNo']; + progNoteOrderNo = json['ProgNoteOrderNo']; + submitOn = json['SubmitOn']; + receiptOn = json['ReceiptOn']; + remarks1 = json['Remarks1']; + remarks2 = json['Remarks2']; + status = json['Status']; + feedbackStatusBy = json['FeedbackStatusBy']; + feedbackStatus = json['FeedbackStatus']; + feedbackStatusOn = json['FeedbackStatusOn']; + authorizerID = json['AuthorizerID']; + expiryDate = json['ExpiryDate']; + appointmentNo = json['AppointmentNo']; + admissionNo = json['AdmissionNo']; + admissionRequestNo = json['AdmissionRequestNo']; + createdBy = json['CreatedBy']; + createdOn = json['CreatedOn']; + editedBy = json['EditedBy']; + editedOn = json['EditedOn']; + rowVer = json['RowVer']; + isAddlDiscApplied = json['IsAddlDiscApplied']; + inProgressReasonID = json['InProgressReasonID']; + extendedBy = json['ExtendedBy']; + extendedOn = json['ExtendedOn']; + subPolicyNo = json['SubPolicyNo']; + noOrderAuthorizerID = json['NoOrderAuthorizerID']; + isVerbalApproval = json['isVerbalApproval']; + subStatus = json['SubStatus']; + isNotificationSend = json['IsNotificationSend']; + eApprovalStatus = json['EApprovalStatus']; + eApprovalRemarks = json['EApprovalRemarks']; + isEmailSentOnDelayApproval = json['IsEmailSentOnDelayApproval']; + eAuthorizationID = json['EAuthorizationID']; + if (json['ApporvalDetails'] != null && json['ApporvalDetails'].length != 0) { + apporvalDetails = InsuranceApprovalDetails.fromJson(json['ApporvalDetails'][0]); + } + approvalStatusDescption = json['ApprovalStatusDescption']; + clinicName = json['ClinicName']; + clinicNameN = json['ClinicNameN']; + companyName = json['CompanyName']; + doctorImageURL = json['DoctorImageURL']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + doctorRate = json['DoctorRate']; + doctorTitle = json['DoctorTitle']; + gender = json['Gender']; + genderDescription = json['GenderDescription']; + isActiveDoctorProfile = json['IsActiveDoctorProfile']; + isExecludeDoctor = json['IsExecludeDoctor']; + isInOutPatient = json['IsInOutPatient']; + isInOutPatientDescription = json['IsInOutPatientDescription']; + isInOutPatientDescriptionN = json['IsInOutPatientDescriptionN']; + isLiveCareAppointment = json['IsLiveCareAppointment']; + projectName = json['ProjectName']; + projectNameN = json['ProjectNameN']; + qR = json['QR']; + if (json['Speciality'] != null) speciality = json['Speciality'].cast(); + strApprovalDate = json['StrApprovalDate']; + strExpiryDate = json['StrExpiryDate']; + strSubmitDate = json['StrSubmitDate']; + totaUnUsedCount = json['TotaUnUsedCount']; + unUsedCount = json['UnUsedCount']; + } + + Map toJson() { + final Map data = new Map(); + data['SetupID'] = this.setupID; + data['ProjectID'] = this.projectID; + data['ApprovalNo'] = this.approvalNo; + data['ApprovalDate'] = this.approvalDate; + data['PatientType'] = this.patientType; + data['PatientID'] = this.patientID; + data['CompanyID'] = this.companyID; + data['SubCategoryID'] = this.subCategoryID; + data['DoctorID'] = this.doctorID; + data['ClinicID'] = this.clinicID; + data['ApprovalType'] = this.approvalType; + data['InpatientApprovalSubType'] = this.inpatientApprovalSubType; + data['ValidFrom'] = this.validFrom; + data['VaildDays'] = this.vaildDays; + data['ValidTo'] = this.validTo; + data['IsApprovalOnGross'] = this.isApprovalOnGross; + data['IsPackage'] = this.isPackage; + data['RequestedAmount'] = this.requestedAmount; + data['ActualApprovedAmount'] = this.actualApprovedAmount; + data['AviliableAmount'] = this.aviliableAmount; + data['CompanyApprovalNo'] = this.companyApprovalNo; + data['ProgNoteOrderNo'] = this.progNoteOrderNo; + data['SubmitOn'] = this.submitOn; + data['ReceiptOn'] = this.receiptOn; + data['Remarks1'] = this.remarks1; + data['Remarks2'] = this.remarks2; + data['Status'] = this.status; + data['FeedbackStatusBy'] = this.feedbackStatusBy; + data['FeedbackStatus'] = this.feedbackStatus; + data['FeedbackStatusOn'] = this.feedbackStatusOn; + data['AuthorizerID'] = this.authorizerID; + data['ExpiryDate'] = this.expiryDate; + data['AppointmentNo'] = this.appointmentNo; + data['AdmissionNo'] = this.admissionNo; + data['AdmissionRequestNo'] = this.admissionRequestNo; + data['CreatedBy'] = this.createdBy; + data['CreatedOn'] = this.createdOn; + data['EditedBy'] = this.editedBy; + data['EditedOn'] = this.editedOn; + data['RowVer'] = this.rowVer; + data['IsAddlDiscApplied'] = this.isAddlDiscApplied; + data['InProgressReasonID'] = this.inProgressReasonID; + data['ExtendedBy'] = this.extendedBy; + data['ExtendedOn'] = this.extendedOn; + data['SubPolicyNo'] = this.subPolicyNo; + data['NoOrderAuthorizerID'] = this.noOrderAuthorizerID; + data['isVerbalApproval'] = this.isVerbalApproval; + data['SubStatus'] = this.subStatus; + data['IsNotificationSend'] = this.isNotificationSend; + data['EApprovalStatus'] = this.eApprovalStatus; + data['EApprovalRemarks'] = this.eApprovalRemarks; + data['IsEmailSentOnDelayApproval'] = this.isEmailSentOnDelayApproval; + data['EAuthorizationID'] = this.eAuthorizationID; + if (this.apporvalDetails != null) { + data['ApporvalDetails'] = this.apporvalDetails!.toJson; + } + data['ApprovalStatusDescption'] = this.approvalStatusDescption; + data['ClinicName'] = this.clinicName; + data['ClinicNameN'] = this.clinicNameN; + data['CompanyName'] = this.companyName; + data['DoctorImageURL'] = this.doctorImageURL; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['DoctorRate'] = this.doctorRate; + data['DoctorTitle'] = this.doctorTitle; + data['Gender'] = this.gender; + data['GenderDescription'] = this.genderDescription; + data['IsActiveDoctorProfile'] = this.isActiveDoctorProfile; + data['IsExecludeDoctor'] = this.isExecludeDoctor; + data['IsInOutPatient'] = this.isInOutPatient; + data['IsInOutPatientDescription'] = this.isInOutPatientDescription; + data['IsInOutPatientDescriptionN'] = this.isInOutPatientDescriptionN; + data['IsLiveCareAppointment'] = this.isLiveCareAppointment; + data['ProjectName'] = this.projectName; + data['ProjectNameN'] = this.projectNameN; + data['QR'] = this.qR; + data['Speciality'] = this.speciality; + data['StrApprovalDate'] = this.strApprovalDate; + data['StrExpiryDate'] = this.strExpiryDate; + data['StrSubmitDate'] = this.strSubmitDate; + data['TotaUnUsedCount'] = this.totaUnUsedCount; + data['UnUsedCount'] = this.unUsedCount; + return data; + } +} + +class InsuranceApprovalDetails { + int? setupID; + int? projectID; + int? approvalNo; + int? lineItemNo; + int? orderType; + int? procedureID; + int? toothNo; + int? price; + int? approvedAmount; + String? unapprovedPatientShare; + int? waivedAmount; + String? discountType; + int? discountValue; + String? shareType; + String? patientShareTypeValue; + String? companyShareTypeValue; + String? patientShare; + String? companyShare; + int? deductableAmount; + String? disapprovedRemarks; + int? progNoteOrderNo; + int? progNoteLineItemNo; + String? invoiceTransactionType; + int? invoiceNo; + String? procedureName; + String? procedureNameN; + String? status; + bool? isInvoiced; + String? isInvoicedDesc; + + InsuranceApprovalDetails( + {this.setupID, + this.projectID, + this.approvalNo, + this.lineItemNo, + this.orderType, + this.procedureID, + this.toothNo, + this.price, + this.approvedAmount, + this.unapprovedPatientShare, + this.waivedAmount, + this.discountType, + this.discountValue, + this.shareType, + this.patientShareTypeValue, + this.companyShareTypeValue, + this.patientShare, + this.companyShare, + this.deductableAmount, + this.disapprovedRemarks, + this.progNoteOrderNo, + this.progNoteLineItemNo, + this.invoiceTransactionType, + this.invoiceNo, + this.procedureName, + this.procedureNameN, + this.status, + this.isInvoiced, + this.isInvoicedDesc}); + + InsuranceApprovalDetails.fromJson(Map json) { + setupID = json['SetupID']; + projectID = json['ProjectID']; + approvalNo = json['ApprovalNo']; + lineItemNo = json['LineItemNo']; + orderType = json['OrderType']; + procedureID = json['ProcedureID']; + toothNo = json['ToothNo']; + price = json['Price']; + approvedAmount = json['ApprovedAmount']; + unapprovedPatientShare = json['UnapprovedPatientShare']; + waivedAmount = json['WaivedAmount']; + discountType = json['DiscountType']; + discountValue = json['DiscountValue']; + shareType = json['ShareType']; + patientShareTypeValue = json['PatientShareTypeValue']; + companyShareTypeValue = json['CompanyShareTypeValue']; + patientShare = json['PatientShare']; + companyShare = json['CompanyShare']; + deductableAmount = json['DeductableAmount']; + disapprovedRemarks = json['DisapprovedRemarks']; + progNoteOrderNo = json['ProgNoteOrderNo']; + progNoteLineItemNo = json['ProgNoteLineItemNo']; + invoiceTransactionType = json['InvoiceTransactionType']; + invoiceNo = json['InvoiceNo']; + procedureName = json['ProcedureName']; + procedureNameN = json['ProcedureNameN']; + status = json['Status']; + isInvoiced = json['IsInvoiced']; + isInvoicedDesc = json['IsInvoicedDesc']; + } + + Map toJson() { + final Map data = new Map(); + data['SetupID'] = this.setupID; + data['ProjectID'] = this.projectID; + data['ApprovalNo'] = this.approvalNo; + data['LineItemNo'] = this.lineItemNo; + data['OrderType'] = this.orderType; + data['ProcedureID'] = this.procedureID; + data['ToothNo'] = this.toothNo; + data['Price'] = this.price; + data['ApprovedAmount'] = this.approvedAmount; + data['UnapprovedPatientShare'] = this.unapprovedPatientShare; + data['WaivedAmount'] = this.waivedAmount; + data['DiscountType'] = this.discountType; + data['DiscountValue'] = this.discountValue; + data['ShareType'] = this.shareType; + data['PatientShareTypeValue'] = this.patientShareTypeValue; + data['CompanyShareTypeValue'] = this.companyShareTypeValue; + data['PatientShare'] = this.patientShare; + data['CompanyShare'] = this.companyShare; + data['DeductableAmount'] = this.deductableAmount; + data['DisapprovedRemarks'] = this.disapprovedRemarks; + data['ProgNoteOrderNo'] = this.progNoteOrderNo; + data['ProgNoteLineItemNo'] = this.progNoteLineItemNo; + data['InvoiceTransactionType'] = this.invoiceTransactionType; + data['InvoiceNo'] = this.invoiceNo; + data['ProcedureName'] = this.procedureName; + data['ProcedureNameN'] = this.procedureNameN; + data['Status'] = this.status; + data['IsInvoiced'] = this.isInvoiced; + data['IsInvoicedDesc'] = this.isInvoicedDesc; + return data; + } +} diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 8a397e3..9f713ac 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -25,6 +25,8 @@ class LabViewModel extends ChangeNotifier { bool isGraphVisible = true; bool shouldShowGraph = true; + bool isLabNeedToLoad = true; + LabRepo labRepo; ErrorHandlerService errorHandlerService; NavigationService navigationService; @@ -33,6 +35,7 @@ class LabViewModel extends ChangeNotifier { List filteredLabOrders = []; List tempLabOrdersList = []; String labSpecialResult = ""; + List labOrderTests = []; PatientLabOrdersResponseModel? currentlySelectedPatientOrder; @@ -71,17 +74,29 @@ class LabViewModel extends ChangeNotifier { required this.navigationService}); initLabProvider() { - patientLabOrders.clear(); - filteredLabOrders.clear(); - isLabOrdersLoading = true; - isLabResultsLoading = true; - getPatientLabOrders(); + if (isLabNeedToLoad) { + patientLabOrders.clear(); + filteredLabOrders.clear(); + labOrderTests.clear(); + isLabOrdersLoading = true; + isLabResultsLoading = true; + getPatientLabOrders(); + } + notifyListeners(); + } + + setIsLabNeedToLoad(bool value) { + isLabNeedToLoad = value; notifyListeners(); } Future getPatientLabOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { + if (!isLabNeedToLoad) return; + + isLabOrdersLoading = true; patientLabOrders.clear(); uniqueTests.clear(); + labOrderTests.clear(); uniqueTests = {}; notifyListeners(); @@ -105,6 +120,7 @@ class LabViewModel extends ChangeNotifier { isLabResultsLoading = false; filterSuggestions(); getUniqueTestDescription(); + isLabNeedToLoad = false; notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -151,6 +167,9 @@ class LabViewModel extends ChangeNotifier { createdOn: item.createdOn, model: item)) }; + for (var element in uniqueTests) { + labOrderTests.add(element.description ?? ""); + } } Future getPatientLabResultByHospital( @@ -165,6 +184,7 @@ class LabViewModel extends ChangeNotifier { result.fold( (failure) async { isLabResultByHospitalLoading = false; + notifyListeners(); // await errorHandlerService.handleError(failure: failure); }, (apiResponse) { diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 54353a4..c87039c 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -313,7 +313,7 @@ class _AppointmentDetailsPageState extends State { ), SizedBox(width: 68.h), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon, iconColor: AppColors.blackColor, diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index 3c475c1..467a480 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -117,7 +117,7 @@ class _AppointmentPaymentPageState extends State { SizedBox(width: 8.h), const Spacer(), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, @@ -159,7 +159,7 @@ class _AppointmentPaymentPageState extends State { SizedBox(width: 8.h), const Spacer(), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, @@ -196,7 +196,7 @@ class _AppointmentPaymentPageState extends State { SizedBox(width: 8.h), const Spacer(), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index 150f0bf..1305ea0 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -8,6 +8,7 @@ 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/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/appointemnet_filters.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'; @@ -37,6 +38,7 @@ class MyAppointmentsPage extends StatefulWidget { class _MyAppointmentsPageState extends State { late MyAppointmentsViewModel myAppointmentsViewModel; + late BookAppointmentsViewModel bookAppointmentsViewModel; @override void initState() { @@ -50,6 +52,7 @@ class _MyAppointmentsPageState extends State { @override Widget build(BuildContext context) { myAppointmentsViewModel = Provider.of(context, listen: false); + bookAppointmentsViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( @@ -92,7 +95,6 @@ class _MyAppointmentsPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Visibility( visible: myAppointmentsVM.availableFilters.isNotEmpty, child: getAppointmentFilters(myAppointmentsVM)), @@ -117,6 +119,7 @@ class _MyAppointmentsPageState extends State { patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(), myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, isLoading: true, isFromHomePage: false, ), @@ -141,6 +144,7 @@ class _MyAppointmentsPageState extends State { filteredAppointmentList[index], myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, isLoading: false, isFromHomePage: false, ), diff --git a/lib/presentation/appointments/my_doctors_page.dart b/lib/presentation/appointments/my_doctors_page.dart index 2fedfd8..06257c7 100644 --- a/lib/presentation/appointments/my_doctors_page.dart +++ b/lib/presentation/appointments/my_doctors_page.dart @@ -61,7 +61,7 @@ class MyDoctorsPage extends StatelessWidget { "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 63.h, height: 63.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: true), SizedBox(width: 16.h), Expanded( @@ -112,7 +112,7 @@ class MyDoctorsPage extends StatelessWidget { myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, width: 63.h, height: 63.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: false), SizedBox(width: 16.h), Expanded( @@ -140,7 +140,7 @@ class MyDoctorsPage extends StatelessWidget { children: [ "".toText16(), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), ], ), diff --git a/lib/presentation/appointments/widgets/AppointmentFilter.dart b/lib/presentation/appointments/widgets/AppointmentFilter.dart index fd2a9fe..98da22d 100644 --- a/lib/presentation/appointments/widgets/AppointmentFilter.dart +++ b/lib/presentation/appointments/widgets/AppointmentFilter.dart @@ -36,8 +36,8 @@ class AppointmentFilters extends StatelessWidget { padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 8.h), deleteIconSize: Size(18.h, 18.h), shape: SmoothRectangleBorder( - borderRadius: BorderRadius.circular(10 ), - smoothness: 10, + borderRadius: BorderRadius.circular(10.h), + smoothness: 10.h, side: BorderSide( color: selectedFilter?.contains(item) == true ? AppColors.chipPrimaryRedBorderColor diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 3a65124..aa67a76 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -8,15 +8,20 @@ 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/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_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/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/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/appointment_details_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:smooth_corner/smooth_corner.dart'; @@ -26,6 +31,7 @@ class AppointmentCard extends StatefulWidget { {super.key, required this.patientAppointmentHistoryResponseModel, required this.myAppointmentsViewModel, + required this.bookAppointmentsViewModel, this.isLoading = false, this.isFromHomePage = false, this.isFromMedicalReport = false, @@ -37,6 +43,7 @@ class AppointmentCard extends StatefulWidget { bool isFromHomePage; bool isFromMedicalReport; MedicalFileViewModel? medicalFileViewModel; + BookAppointmentsViewModel bookAppointmentsViewModel; @override State createState() => _AppointmentCardState(); @@ -140,7 +147,7 @@ class _AppointmentCardState extends State { widget.isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : widget.patientAppointmentHistoryResponseModel.doctorImageURL!, width: 63.h, height: 63.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: widget.isLoading), SizedBox(width: 16.h), Expanded( @@ -244,7 +251,7 @@ class _AppointmentCardState extends State { borderRadius: 10.h, ), child: Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon, iconColor: AppColors.whiteColor, @@ -293,7 +300,9 @@ class _AppointmentCardState extends State { ) : CustomButton( text: "Rebook with same doctor".needTranslation, - onPressed: () {}, + onPressed: () { + openDoctorScheduleCalendar(); + }, backgroundColor: AppColors.greyColor, borderColor: AppColors.greyColor, textColor: AppColors.blackColor, @@ -308,5 +317,45 @@ class _AppointmentCardState extends State { ); } + openDoctorScheduleCalendar() async { + DoctorsListResponseModel doctor = DoctorsListResponseModel( + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, + doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL, + doctorTitle: widget.patientAppointmentHistoryResponseModel.doctorTitle, + name: widget.patientAppointmentHistoryResponseModel.doctorNameObj, + nationalityFlagURL: "https://hmgwebservices.com/Images/flag/SYR.png", + speciality: [], + clinicName: widget.patientAppointmentHistoryResponseModel.clinicName, + projectName: widget.patientAppointmentHistoryResponseModel.projectName, + ); + widget.bookAppointmentsViewModel.setSelectedDoctor(doctor); + LoaderBottomSheet.showLoader(); + await widget.bookAppointmentsViewModel.getDoctorFreeSlots( + isBookingForLiveCare: false, + onSuccess: (dynamic respData) async { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + title: "Pick a Date".needTranslation, + context, + child: AppointmentCalendar(), + isFullScreen: false, + isCloseButtonVisible: true, + callBackFunc: () {}, + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } + void performAppointmentNextAction(nextAction) {} } diff --git a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart index 93f2bda..1863875 100644 --- a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart +++ b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart @@ -122,7 +122,7 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { ), ), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index b35305f..efc5a6b 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -40,7 +40,7 @@ class AppointmentDoctorCard extends StatelessWidget { patientAppointmentHistoryResponseModel.doctorImageURL!, width: 63.h, height: 63.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100), SizedBox(width: 16.h), Expanded( diff --git a/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart b/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart index 16dfb1e..c7ef3a9 100644 --- a/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart +++ b/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart @@ -46,7 +46,7 @@ class FacilitySelectionItem extends StatelessWidget { children: [ info, Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart index cad0309..31dc74e 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart @@ -39,7 +39,7 @@ class HospitalListItem extends StatelessWidget { ), ), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart index 688580c..8b35921 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart @@ -43,7 +43,7 @@ class RegionListItem extends StatelessWidget { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index b124438..b39c51e 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -130,7 +130,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setIsClinicsListLoading(true); @@ -163,7 +163,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setIsDoctorSearchByNameStarted(false); @@ -194,7 +194,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setProjectID(null); @@ -239,7 +239,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() async { //TODO Implement API to check for existing LiveCare Requests @@ -282,7 +282,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setIsClinicsListLoading(true); @@ -313,7 +313,7 @@ class _BookAppointmentPageState extends State { ], ), Transform.flip( - flipX: appState.isArabic() ? true : false, child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION); diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index 6084c3d..1223842 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -53,7 +53,7 @@ class DoctorProfilePage extends StatelessWidget { bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL!, width: 63.h, height: 63.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Column( @@ -73,7 +73,7 @@ class DoctorProfilePage extends StatelessWidget { bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL!, width: 32.h, height: 32.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ), ], ), diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index 95d6302..914ac4a 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -115,7 +115,7 @@ class _ImmediateLiveCarePaymentPageState extends State { bookAppointmentsViewModel.selectedDoctor.doctorImageURL!, width: 50.h, height: 50.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Column( @@ -88,7 +88,7 @@ class _ReviewAppointmentPageState extends State { bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!, width: 20.h, height: 15.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ), ], ), diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index 63aa313..b414799 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -88,7 +88,7 @@ class _WalletPaymentConfirmPageState extends State { SizedBox(width: 8.h), const Spacer(), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon, iconColor: AppColors.blackColor, @@ -130,7 +130,7 @@ class _WalletPaymentConfirmPageState extends State { SizedBox(width: 8.h), const Spacer(), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon, iconColor: AppColors.blackColor, diff --git a/lib/presentation/habib_wallet/widgets/hospital_list_item.dart b/lib/presentation/habib_wallet/widgets/hospital_list_item.dart index c5583e2..8e10e28 100644 --- a/lib/presentation/habib_wallet/widgets/hospital_list_item.dart +++ b/lib/presentation/habib_wallet/widgets/hospital_list_item.dart @@ -36,7 +36,7 @@ class HospitalListItemAdvancePayment extends StatelessWidget { ), ), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon, iconColor: AppColors.blackColor, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 258e8e0..d235f4d 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -15,6 +15,7 @@ import 'package:hmg_patient_app_new/extensions/int_extensions.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/authentication/authentication_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; 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'; @@ -67,6 +68,7 @@ class _LandingPageState extends State { late InsuranceViewModel insuranceViewModel; late ImmediateLiveCareViewModel immediateLiveCareViewModel; + late BookAppointmentsViewModel bookAppointmentsViewModel; final SwiperController _controller = SwiperController(); @@ -100,6 +102,7 @@ class _LandingPageState extends State { Widget build(BuildContext context) { appState = getIt.get(); NavigationService navigationService = getIt.get(); + bookAppointmentsViewModel = Provider.of(context, listen: false); myAppointmentsViewModel = Provider.of(context, listen: false); prescriptionsViewModel = Provider.of(context, listen: false); insuranceViewModel = Provider.of(context, listen: false); @@ -200,6 +203,7 @@ class _LandingPageState extends State { child: AppointmentCard( patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(), myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, isLoading: true, isFromHomePage: true, ), @@ -211,6 +215,7 @@ class _LandingPageState extends State { child: AppointmentCard( patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList.first, myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, isLoading: false, isFromHomePage: true, ), @@ -239,6 +244,7 @@ class _LandingPageState extends State { child: AppointmentCard( patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, isLoading: false, isFromHomePage: true, ), diff --git a/lib/presentation/insurance/insurance_approvals_page.dart b/lib/presentation/insurance/insurance_approvals_page.dart new file mode 100644 index 0000000..50e659c --- /dev/null +++ b/lib/presentation/insurance/insurance_approvals_page.dart @@ -0,0 +1,109 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; +import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_approval_card.dart'; +import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_approval_details_page.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/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class InsuranceApprovalsPage extends StatefulWidget { + const InsuranceApprovalsPage({super.key}); + + @override + State createState() => _InsuranceApprovalsPageState(); +} + +class _InsuranceApprovalsPageState extends State { + late InsuranceViewModel insuranceViewModel; + late AppState appState; + + @override + void initState() { + scheduleMicrotask(() { + insuranceViewModel.setIsInsuranceApprovalsLoading(true); + insuranceViewModel.getPatientInsuranceApprovalsList(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + insuranceViewModel = Provider.of(context, listen: false); + return CollapsingListView( + title: "${LocaleKeys.insurance.tr(context: context)} ${LocaleKeys.approvals.tr(context: context)}", + child: SingleChildScrollView( + child: Consumer(builder: (context, insuranceVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: insuranceVM.isInsuranceApprovalsLoading + ? 5 + : insuranceVM.patientInsuranceApprovalsList.isNotEmpty + ? insuranceVM.patientInsuranceApprovalsList.length + : 1, + itemBuilder: (context, index) { + return insuranceVM.isInsuranceApprovalsLoading + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: InsuranceApprovalCard( + insuranceApprovalResponseModel: InsuranceApprovalResponseModel(), + appState: appState, + isLoading: true, + ), + ).paddingSymmetrical(24.h, 0.h) + : insuranceVM.patientInsuranceApprovalsList.isNotEmpty + ? AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: InsuranceApprovalCard( + insuranceApprovalResponseModel: insuranceVM.patientInsuranceApprovalsList[index], + appState: appState, + isLoading: false, + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: InsuranceApprovalDetailsPage(insuranceApprovalResponseModel: insuranceVM.patientInsuranceApprovalsList[index]), + ), + ); + }), + ).paddingSymmetrical(24.h, 0.h), + ), + ), + ) + : Utils.getNoDataWidget(context, noDataText: "You don't have any insurance approvals yet.".needTranslation); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + SizedBox(height: 24.h), + ], + ); + }), + ), + ); + } +} diff --git a/lib/presentation/insurance/insurance_home_page.dart b/lib/presentation/insurance/insurance_home_page.dart index bc56473..bd195c3 100644 --- a/lib/presentation/insurance/insurance_home_page.dart +++ b/lib/presentation/insurance/insurance_home_page.dart @@ -34,7 +34,6 @@ class InsuranceHomePage extends StatefulWidget { class _InsuranceHomePageState extends State { late InsuranceViewModel insuranceViewModel; - late AppState appState; @override diff --git a/lib/presentation/insurance/widgets/insurance_approval_card.dart b/lib/presentation/insurance/widgets/insurance_approval_card.dart new file mode 100644 index 0000000..71fce58 --- /dev/null +++ b/lib/presentation/insurance/widgets/insurance_approval_card.dart @@ -0,0 +1,129 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_approval_response_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/utils/appointment_type.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/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class InsuranceApprovalCard extends StatelessWidget { + InsuranceApprovalCard({super.key, required this.insuranceApprovalResponseModel, required this.isLoading, required this.appState}); + + InsuranceApprovalResponseModel insuranceApprovalResponseModel; + AppState appState; + bool isLoading; + + @override + Widget build(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + alignment: WrapAlignment.start, + direction: Axis.horizontal, + spacing: 6.h, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + labelText: isLoading ? "Approved" : insuranceApprovalResponseModel.approvalStatusDescption, + backgroundColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor.withOpacity(0.1) : AppColors.primaryRedColor.withOpacity(0.1), + textColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor : AppColors.primaryRedColor, + ).toShimmer2(isShow: isLoading), + AppCustomChipWidget( + icon: isLoading ? AppAssets.walkin_appointment_icon : (!insuranceApprovalResponseModel.isLiveCareAppointment! ? AppAssets.walkin_appointment_icon : AppAssets.small_livecare_icon), + iconColor: isLoading + ? AppColors.textColor + : !insuranceApprovalResponseModel.isLiveCareAppointment! + ? AppColors.textColor + : AppColors.whiteColor, + labelText: isLoading + ? "Walk In" + : insuranceApprovalResponseModel.isLiveCareAppointment! + ? LocaleKeys.livecare.tr(context: context) + : "Walk In".needTranslation, + backgroundColor: isLoading ? AppColors.greyColor : (!insuranceApprovalResponseModel.isLiveCareAppointment! ? AppColors.greyColor : AppColors.successColor), + textColor: isLoading ? AppColors.textColor : (!insuranceApprovalResponseModel.isLiveCareAppointment! ? AppColors.textColor : AppColors.whiteColor), + ).toShimmer2(isShow: isLoading), + AppCustomChipWidget( + labelText: isLoading + ? "OutPatient" + : appState.isArabic() + ? insuranceApprovalResponseModel.isInOutPatientDescriptionN! + : insuranceApprovalResponseModel.isInOutPatientDescription!, + backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), + textColor: AppColors.primaryRedColor, + ).toShimmer2(isShow: isLoading), + ], + ).toShimmer2(isShow: isLoading), + SizedBox(height: 16.h), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + isLoading ? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png" : insuranceApprovalResponseModel.doctorImageURL!, + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: isLoading), + SizedBox(width: 16.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (isLoading ? "John Smith" : insuranceApprovalResponseModel.doctorName!).toText16(isBold: true).toShimmer2(isShow: isLoading), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + // AppCustomChipWidget(labelText: isLoading ? "Approved" : insuranceApprovalResponseModel.approvalStatusDescption!, + // backgroundColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.primaryRedColor.withValues(alpha: 0.1), + // textColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor : AppColors.primaryRedColor, + // ).toShimmer2(isShow: isLoading), + AppCustomChipWidget(labelText: isLoading ? "Cardiology" : insuranceApprovalResponseModel.clinicName!).toShimmer2(isShow: isLoading), + AppCustomChipWidget( + icon: AppAssets.doctor_calendar_icon, + labelText: isLoading ? "Cardiology" : DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.submitOn), false)) + .toShimmer2(isShow: isLoading), + isLoading + ? SizedBox.shrink() + : AppCustomChipWidget( + labelText: isLoading ? LocaleKeys.approvalNo.tr(context: context) : "${LocaleKeys.approvalNo.tr(context: context)} ${insuranceApprovalResponseModel.approvalNo}") + .toShimmer2(isShow: isLoading), + ], + ), + ], + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Transform.flip( + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/insurance/widgets/insurance_approval_details_page.dart b/lib/presentation/insurance/widgets/insurance_approval_details_page.dart new file mode 100644 index 0000000..c9b1517 --- /dev/null +++ b/lib/presentation/insurance/widgets/insurance_approval_details_page.dart @@ -0,0 +1,135 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.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/insurance/insurance_view_model.dart'; +import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; + +class InsuranceApprovalDetailsPage extends StatelessWidget { + InsuranceApprovalDetailsPage({super.key, required this.insuranceApprovalResponseModel}); + + InsuranceApprovalResponseModel insuranceApprovalResponseModel; + late InsuranceViewModel insuranceViewModel; + late AppState appState; + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + insuranceViewModel = Provider.of(context, listen: false); + return CollapsingListView( + title: "${LocaleKeys.insurance.tr(context: context)} ${LocaleKeys.approvals.tr(context: context)}", + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + alignment: WrapAlignment.start, + direction: Axis.horizontal, + spacing: 6.h, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + labelText: insuranceApprovalResponseModel.approvalStatusDescption, + backgroundColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor.withOpacity(0.1) : AppColors.primaryRedColor.withOpacity(0.1), + textColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor : AppColors.primaryRedColor, + ), + AppCustomChipWidget( + icon: (!insuranceApprovalResponseModel.isLiveCareAppointment! ? AppAssets.walkin_appointment_icon : AppAssets.small_livecare_icon), + iconColor: !insuranceApprovalResponseModel.isLiveCareAppointment! ? AppColors.textColor : AppColors.whiteColor, + labelText: insuranceApprovalResponseModel.isLiveCareAppointment! ? LocaleKeys.livecare.tr(context: context) : "Walk In".needTranslation, + backgroundColor: (!insuranceApprovalResponseModel.isLiveCareAppointment! ? AppColors.greyColor : AppColors.successColor), + textColor: (!insuranceApprovalResponseModel.isLiveCareAppointment! ? AppColors.textColor : AppColors.whiteColor), + ), + AppCustomChipWidget( + labelText: appState.isArabic() ? insuranceApprovalResponseModel.isInOutPatientDescriptionN! : insuranceApprovalResponseModel.isInOutPatientDescription!, + backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), + textColor: AppColors.primaryRedColor, + ), + ], + ), + SizedBox(height: 16.h), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + insuranceApprovalResponseModel.doctorImageURL!, + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 16.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (insuranceApprovalResponseModel.doctorName!).toText16(isBold: true), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget(labelText: insuranceApprovalResponseModel.clinicName!), + AppCustomChipWidget(labelText: "${LocaleKeys.approvalNo.tr(context: context)} ${insuranceApprovalResponseModel.approvalNo}"), + AppCustomChipWidget(labelText: "${LocaleKeys.unusedCount.tr(context: context)} ${insuranceApprovalResponseModel.unUsedCount}"), + AppCustomChipWidget(labelText: "${LocaleKeys.companyName.tr(context: context)} ${insuranceApprovalResponseModel.companyName}"), + AppCustomChipWidget( + labelText: + "${LocaleKeys.receiptOn.tr(context: context)} ${DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.receiptOn), false)}"), + AppCustomChipWidget( + labelText: + "${LocaleKeys.expiryOn.tr(context: context)} ${DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.expiryDate), false)}"), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + SizedBox(height: 16.h), + Container( + width: double.infinity, + height: 200.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Approval Details".toText16(isBold: true), + ], + ), + ), + ) + ], + ).paddingSymmetrical(24.h, 24.h), + ); + } +} diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index dd80513..8ff081f 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -8,7 +8,6 @@ import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; -import 'package:hmg_patient_app_new/core/utils/request_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -16,8 +15,8 @@ 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/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; +import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; -import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_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'; @@ -27,10 +26,13 @@ import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_pa import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; +import 'package:hmg_patient_app_new/presentation/insurance/insurance_approvals_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/patient_insurance_card.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; +import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/medical_report/medical_reports_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/patient_sickleaves_list_page.dart'; @@ -38,8 +40,6 @@ import 'package:hmg_patient_app_new/presentation/medical_file/vaccine_list_page. import 'package:hmg_patient_app_new/presentation/medical_file/widgets/lab_rad_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/widgets/medical_file_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/widgets/patient_sick_leave_card.dart'; -import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart'; -import 'package:hmg_patient_app_new/presentation/my_family/widget/my_family_sheet.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -69,6 +69,7 @@ class _MedicalFilePageState extends State { late MyAppointmentsViewModel myAppointmentsViewModel; late MedicalFileViewModel medicalFileViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; + late LabViewModel labViewModel; int currentIndex = 0; @@ -76,10 +77,10 @@ class _MedicalFilePageState extends State { void initState() { scheduleMicrotask(() { if (appState.isAuthenticated) { + labViewModel.initLabProvider(); insuranceViewModel.initInsuranceProvider(); medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.getPatientSickLeaveList(); - // medicalFileViewModel.getFamilyFiles(); medicalFileViewModel.onTabChanged(0); } }); @@ -88,6 +89,7 @@ class _MedicalFilePageState extends State { @override Widget build(BuildContext context) { + labViewModel = Provider.of(context, listen: false); insuranceViewModel = Provider.of(context, listen: false); myAppointmentsViewModel = Provider.of(context, listen: false); medicalFileViewModel = Provider.of(context, listen: false); @@ -106,7 +108,7 @@ class _MedicalFilePageState extends State { SizedBox(height: 16.h), TextInputWidget( labelText: LocaleKeys.search.tr(context: context), - hintText: "Type any record", + hintText: "Type any record".needTranslation, controller: TextEditingController(), keyboardType: TextInputType.number, isEnable: true, @@ -183,8 +185,8 @@ class _MedicalFilePageState extends State { return Column( children: [ CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), tabs: [ CustomTabBarModel(AppAssets.myFilesBottom, LocaleKeys.general.tr(context: context).needTranslation), CustomTabBarModel(AppAssets.insurance, LocaleKeys.insurance.tr(context: context)), @@ -192,7 +194,6 @@ class _MedicalFilePageState extends State { CustomTabBarModel(AppAssets.more, "More".needTranslation), ], onTabChange: (index) { - print(index); medicalFileVM.onTabChanged(index); }, ).paddingSymmetrical(24.h, 0.0), @@ -361,16 +362,32 @@ class _MedicalFilePageState extends State { child: LabRadCard( icon: AppAssets.lab_result_icon, labelText: LocaleKeys.labResults.tr(context: context), - labOrderTests: ["Complete blood count", "Creatinine", "Blood Sugar"], - ), + // labOrderTests: ["Complete blood count", "Creatinine", "Blood Sugar"], + labOrderTests: labViewModel.isLabOrdersLoading ? [] : labViewModel.labOrderTests, + isLoading: labViewModel.isLabOrdersLoading, + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: LabOrdersPage(), + ), + ); + }), ), SizedBox(width: 16.h), Expanded( child: LabRadCard( icon: AppAssets.radiology_icon, labelText: LocaleKeys.radiology.tr(context: context), - labOrderTests: ["Chest X-ray", "Abdominal Ultrasound", "Dental X-ray"], - ), + // labOrderTests: ["Chest X-ray", "Abdominal Ultrasound", "Dental X-ray"], + labOrderTests: [], + isLoading: false, + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: RadiologyOrdersPage(), + ), + ); + }), ), ], ).paddingSymmetrical(24.h, 0.h), @@ -408,7 +425,7 @@ class _MedicalFilePageState extends State { prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!, width: 40.h, height: 40.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100), SizedBox(width: 16.h), Expanded( @@ -434,8 +451,9 @@ class _MedicalFilePageState extends State { ), // SizedBox(width: 40.h), Transform.flip( - flipX: appState.isArabic() ? true : false, - child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), ], ).onPress(() { prescriptionVM.setPrescriptionsDetailsLoading(); @@ -469,7 +487,7 @@ class _MedicalFilePageState extends State { backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, - fontSize: 13, + fontSize: 12.3, fontWeight: FontWeight.w500, borderRadius: 12.h, height: 40.h, @@ -505,7 +523,7 @@ class _MedicalFilePageState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 20.h, - hasShadow: true, + hasShadow: false, ), child: Utils.getNoDataWidget(context, noDataText: "You don't have any prescriptions yet.".needTranslation, isSmallWidget: true, width: 62, height: 62)) .paddingSymmetrical(24.h, 0.h); @@ -601,7 +619,7 @@ class _MedicalFilePageState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 20.h, - hasShadow: true, + hasShadow: false, ), child: Utils.getNoDataWidget(context, noDataText: "You don't have any completed visits yet.".needTranslation, isSmallWidget: true, width: 62, height: 62), ).paddingSymmetrical(24.h, 0.h); @@ -682,7 +700,7 @@ class _MedicalFilePageState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 20.h, - hasShadow: true, + hasShadow: false, ), child: Utils.getNoDataWidget( context, @@ -740,12 +758,18 @@ class _MedicalFilePageState extends State { ); }), MedicalFileCard( - label: "Insurance Approvals".needTranslation, + label: "${LocaleKeys.insurance.tr(context: context)} ${LocaleKeys.approvals.tr(context: context)}", textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.eye_result_icon, isLargeText: true, - iconSize: 36.h), + iconSize: 36.h).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: InsuranceApprovalsPage(), + ), + ); + }), MedicalFileCard( label: "My Invoices List".needTranslation, textColor: AppColors.blackColor, @@ -780,13 +804,20 @@ class _MedicalFilePageState extends State { patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first, isLoading: false, ).paddingSymmetrical(24.h, 0.0) - : Utils.getNoDataWidget( - context, - noDataText: "You don't have any sick leaves yet.".needTranslation, - isSmallWidget: true, - width: 62, - height: 62, - ); + : Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Utils.getNoDataWidget( + context, + noDataText: "You don't have any sick leaves yet.".needTranslation, + isSmallWidget: true, + width: 62, + height: 62, + ), + ).paddingSymmetrical(24.h, 0.h); }), SizedBox(height: 16.h), GridView( diff --git a/lib/presentation/medical_file/vaccine_list_page.dart b/lib/presentation/medical_file/vaccine_list_page.dart index 82c9441..777426f 100644 --- a/lib/presentation/medical_file/vaccine_list_page.dart +++ b/lib/presentation/medical_file/vaccine_list_page.dart @@ -77,7 +77,7 @@ class _VaccineListPageState extends State { "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 63.h, height: 63.h, - fit: BoxFit.fill, + fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: true), SizedBox(width: 16.h), Expanded( diff --git a/lib/presentation/medical_file/widgets/lab_rad_card.dart b/lib/presentation/medical_file/widgets/lab_rad_card.dart index e396d62..c7fd7dd 100644 --- a/lib/presentation/medical_file/widgets/lab_rad_card.dart +++ b/lib/presentation/medical_file/widgets/lab_rad_card.dart @@ -9,11 +9,12 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; class LabRadCard extends StatelessWidget { - LabRadCard({super.key, required this.icon, required this.labelText, required this.labOrderTests}); + LabRadCard({super.key, required this.icon, required this.labelText, required this.labOrderTests, required this.isLoading}); String icon; String labelText; List labOrderTests = []; + bool isLoading; @override Widget build(BuildContext context) { @@ -36,24 +37,26 @@ class LabRadCard extends StatelessWidget { ], ), SizedBox(height: 16.h), - ListView.separated( - scrollDirection: Axis.vertical, - padding: EdgeInsets.zero, - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, + labOrderTests.isNotEmpty + ? ListView.separated( + scrollDirection: Axis.vertical, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, itemBuilder: (cxt, index) { - return labOrderTests[index].toText12(isBold: true, maxLine: 1).toShimmer2(isShow: false, radius: 6.h, height: 24.h, width: 120.h); - }, - separatorBuilder: (cxt, index) => SizedBox(height: 8.h), - itemCount: labOrderTests.length, - ), + return labOrderTests[index].toText12(isBold: true, maxLine: 1).toShimmer2(isShow: false, radius: 6.h, height: 24.h, width: 120.h).toShimmer2(isShow: isLoading); + }, + separatorBuilder: (cxt, index) => SizedBox(height: 8.h), + itemCount: 3, + ) + : "You don't have any records yet".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, isCenter: true), SizedBox(height: 16.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox.shrink(), Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor) .toShimmer2(isShow: false, radius: 12.h), ), diff --git a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart index 8fc3805..87794ef 100644 --- a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart +++ b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart @@ -141,7 +141,7 @@ class PatientSickLeaveCard extends StatelessWidget { child: Padding( padding: EdgeInsets.all(10.h), child: Transform.flip( - flipX: _appState.isArabic() ? true : false, + flipX: _appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.whiteColor, diff --git a/lib/presentation/medical_report/medical_report_request_page.dart b/lib/presentation/medical_report/medical_report_request_page.dart index 9d1bcb1..8eabcdd 100644 --- a/lib/presentation/medical_report/medical_report_request_page.dart +++ b/lib/presentation/medical_report/medical_report_request_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.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/medical_file/medical_file_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'; @@ -42,6 +43,7 @@ class MedicalReportRequestPage extends StatelessWidget { child: AppointmentCard( patientAppointmentHistoryResponseModel: medicalFileViewModel.patientMedicalReportAppointmentHistoryList[index], myAppointmentsViewModel: Provider.of(context, listen: false), + bookAppointmentsViewModel: Provider.of(context, listen: false), medicalFileViewModel: medicalFileViewModel, isLoading: false, isFromHomePage: false, diff --git a/lib/presentation/prescriptions/prescriptions_list_page.dart b/lib/presentation/prescriptions/prescriptions_list_page.dart index 87f880d..1293c9c 100644 --- a/lib/presentation/prescriptions/prescriptions_list_page.dart +++ b/lib/presentation/prescriptions/prescriptions_list_page.dart @@ -264,9 +264,10 @@ class _PrescriptionsListPageState extends State { child: Padding( padding: EdgeInsets.all(8.h), child: Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.whiteColor, width: 10.h, height: 10.h, fit: BoxFit.contain, diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index b18c6ee..951732b 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -46,7 +46,7 @@ class CollapsingListView extends StatelessWidget { backgroundColor: AppColors.bgScaffoldColor, leading: isLeading ? Transform.flip( - flipX: appState.isArabic() ? true : false, + flipX: appState.isArabic(), child: IconButton( icon: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h), padding: EdgeInsets.only(left: 12), diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index 751b01c..783a83f 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -59,7 +59,7 @@ class AppCustomChipWidget extends StatelessWidget { ), child: icon.isNotEmpty ? Chip( - avatar: icon.isNotEmpty ? Utils.buildSvgWithAssets(icon: icon, width: iconSize.h, height: iconSize.h, iconColor: iconHasColor ? iconColor : null) : SizedBox.shrink(), + avatar: icon.isNotEmpty ? Utils.buildSvgWithAssets(icon: icon, width: iconSize.h, height: iconSize.h, iconColor: iconHasColor ? iconColor : null, fit: BoxFit.contain) : SizedBox.shrink(), label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), // padding: EdgeInsets.all(0.0), padding: padding,