From 94313b8870fb2376e4694de314adc3d1ad179862 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Thu, 25 Sep 2025 14:49:49 +0300 Subject: [PATCH 01/14] Improved Error Handling --- ios/Runner.xcodeproj/project.pbxproj | 68 ++++++------- lib/core/api/api_client.dart | 21 ++-- lib/core/dependencies.dart | 16 +-- lib/core/exceptions/api_failure.dart | 24 ++++- .../authentication_view_model.dart | 6 +- .../medical_file/medical_file_page.dart | 2 +- .../{my_Family.dart => my_family.dart} | 11 +-- .../my_family/widget/family_cards.dart | 20 ++-- .../my_family/widget/my_family_sheet.dart | 6 +- lib/services/dialog_service.dart | 99 ++++++------------- lib/services/error_handler_service.dart | 33 ++++--- lib/services/navigation_service.dart | 12 +++ .../bottomsheet/exception_bottom_sheet.dart | 17 ++-- lib/widgets/common_bottom_sheet.dart | 4 +- 14 files changed, 161 insertions(+), 178 deletions(-) rename lib/presentation/my_family/{my_Family.dart => my_family.dart} (96%) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a982556b..3a65395b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,12 +10,12 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 40721B3F0DC8DB0598443DBF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E824BDA50EF77318022F59D /* Pods_Runner.framework */; }; 478CFA942E638C8E0064F3D7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - B976FB9C47411C32B24D5E01 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ACE60DF9393168FD748550B3 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -44,16 +44,16 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1E824BDA50EF77318022F59D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 478CFA952E6E20A60064F3D7 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 62D069322AC3B532E0B4F137 /* 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 = ""; }; 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 = ""; }; 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 = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -61,8 +61,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - ACE60DF9393168FD748550B3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D6BB17A036DF7FCE75271203 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + B35471C63C8DD1B1DECDB3A5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + D0FB40CE52522242F351743B /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -70,7 +70,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B976FB9C47411C32B24D5E01 /* Pods_Runner.framework in Frameworks */, + 40721B3F0DC8DB0598443DBF /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -88,9 +88,9 @@ 79DD2093A1D9674C94359FC8 /* Pods */ = { isa = PBXGroup; children = ( - 8E12CEEB8E334EE22D5259D7 /* Pods-Runner.debug.xcconfig */, - 7595037DD52211B91157B0F3 /* Pods-Runner.release.xcconfig */, - D6BB17A036DF7FCE75271203 /* Pods-Runner.profile.xcconfig */, + 62D069322AC3B532E0B4F137 /* Pods-Runner.debug.xcconfig */, + D0FB40CE52522242F351743B /* Pods-Runner.release.xcconfig */, + B35471C63C8DD1B1DECDB3A5 /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -114,7 +114,7 @@ 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 79DD2093A1D9674C94359FC8 /* Pods */, - A07D637C76A0ABB38659D189 /* Frameworks */, + C1312CE4ABEB9ED47FF21174 /* Frameworks */, ); sourceTree = ""; }; @@ -144,10 +144,10 @@ path = Runner; sourceTree = ""; }; - A07D637C76A0ABB38659D189 /* Frameworks */ = { + C1312CE4ABEB9ED47FF21174 /* Frameworks */ = { isa = PBXGroup; children = ( - ACE60DF9393168FD748550B3 /* Pods_Runner.framework */, + 1E824BDA50EF77318022F59D /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -176,15 +176,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - BFED6CCFE59BB148875A533B /* [CP] Check Pods Manifest.lock */, + E3E41B3FF6C30233949963A9 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 8372B02399CDF54531650AD4 /* [CP] Embed Pods Frameworks */, - 81DE7C26F41956799E954FCE /* [CP] Copy Pods Resources */, + 968C3FBB62CE1E0E307ECBCE /* [CP] Embed Pods Frameworks */, + CC3E3923FDE40035D0CA6762 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -273,24 +273,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 81DE7C26F41956799E954FCE /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 8372B02399CDF54531650AD4 /* [CP] Embed Pods Frameworks */ = { + 968C3FBB62CE1E0E307ECBCE /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -322,7 +305,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - BFED6CCFE59BB148875A533B /* [CP] Check Pods Manifest.lock */ = { + CC3E3923FDE40035D0CA6762 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E3E41B3FF6C30233949963A9 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 62339bfd..903fc4dc 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -9,7 +9,6 @@ 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/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; -import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:http/http.dart' as http; @@ -80,14 +79,11 @@ abstract class ApiClient { class ApiClientImp implements ApiClient { final _analytics = getIt(); - final LoggerService _loggerService; final AppState _appState; ApiClientImp({ - required LoggerService loggerService, required AppState appState, - }) : _appState = appState, - _loggerService = loggerService; + }) : _appState = appState; @override post( @@ -176,7 +172,7 @@ class ApiClientImp implements ApiClient { body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; } - // body['TokenID'] = "@dm!n"; + // body['TokenID'] = "dlfjhagldfgahjsdf"; // body['PatientID'] = 3628599; } @@ -200,7 +196,6 @@ class ApiClientImp implements ApiClient { final int statusCode = response.statusCode; 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")); logApiEndpointError(endPoint, 'Error While Fetching data', statusCode); } else { @@ -218,6 +213,7 @@ class ApiClientImp implements ApiClient { } else { if (parsed['ErrorType'] == 4) { //TODO : handle app update + onFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'], statusCode, failureType: AppUpdateFailure("parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']")); logApiEndpointError(endPoint, parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'], statusCode); } if (parsed['ErrorType'] == 2) { @@ -225,9 +221,9 @@ class ApiClientImp implements ApiClient { onFailure( parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'], statusCode, - failureType: MessageStatusFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']), + failureType: UnAuthenticatedUserFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'] ?? "User is not Authenticated", url: url), ); - // logApiEndpointError(endPoint, "session logged out", statusCode); + // logApiEndpointError(endPoint, "session logged out", statusCode); } if (isAllowAny) { onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); @@ -248,6 +244,12 @@ class ApiClientImp implements ApiClient { } } else if (parsed['MessageStatus'] == 1 || parsed['SMSLoginRequired'] == true) { onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + } else if (parsed['IsAuthenticated'] == false) { + onFailure( + "User is not Authenticated", + statusCode, + failureType: UnAuthenticatedUserFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'] ?? "User is not Authenticated", url: url), + ); } else if (parsed['MessageStatus'] == 2 && parsed['IsAuthenticated']) { if (parsed['SameClinicApptList'] != null) { onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); @@ -277,7 +279,6 @@ class ApiClientImp implements ApiClient { logApiEndpointError(endPoint, parsed['message'] ?? parsed['message'], statusCode); } } - } else if (!parsed['IsAuthenticated']) { } else { if (parsed['SameClinicApptList'] != null) { onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 4de7c5fb..c9cca966 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -77,7 +77,7 @@ class AppDependencies { final sharedPreferences = await SharedPreferences.getInstance(); getIt.registerLazySingleton(() => CacheServiceImp(sharedPreferences: sharedPreferences, loggerService: getIt())); - getIt.registerLazySingleton(() => ApiClientImp(loggerService: getIt(), appState: getIt())); + getIt.registerLazySingleton(() => ApiClientImp(appState: getIt())); // Repositories getIt.registerLazySingleton(() => CommonRepoImp(loggerService: getIt())); @@ -125,11 +125,7 @@ class AppDependencies { ); getIt.registerLazySingleton( - () => MyAppointmentsViewModel( - myAppointmentsRepo: getIt(), - errorHandlerService: getIt(), - appState: getIt() - ), + () => MyAppointmentsViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt()), ); getIt.registerLazySingleton( @@ -154,13 +150,7 @@ class AppDependencies { ); getIt.registerLazySingleton( - () => BookAppointmentsViewModel( - bookAppointmentsRepo: getIt(), - errorHandlerService: getIt(), - navigationService: getIt(), - myAppointmentsViewModel: getIt(), - locationUtils: getIt() - ), + () => BookAppointmentsViewModel(bookAppointmentsRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt(), myAppointmentsViewModel: getIt(), locationUtils: getIt()), ); getIt.registerLazySingleton( diff --git a/lib/core/exceptions/api_failure.dart b/lib/core/exceptions/api_failure.dart index c9501956..0c5bc662 100644 --- a/lib/core/exceptions/api_failure.dart +++ b/lib/core/exceptions/api_failure.dart @@ -7,7 +7,9 @@ abstract class Failure extends Equatable implements Exception { } class ServerFailure extends Failure { - const ServerFailure(super.message); + final String url; + + const ServerFailure(super.message, {this.url = ""}); @override List get props => [message]; @@ -27,6 +29,13 @@ class MessageStatusFailure extends Failure { List get props => [message]; } +class AppUpdateFailure extends Failure { + const AppUpdateFailure(super.message); + + @override + List get props => [message]; +} + class StatusCodeFailure extends Failure { const StatusCodeFailure(super.message); @@ -34,6 +43,15 @@ class StatusCodeFailure extends Failure { List get props => [message]; } +class UnAuthenticatedUserFailure extends Failure { + final String url; + + const UnAuthenticatedUserFailure(super.message, {this.url = ""}); + + @override + List get props => [message]; +} + class ConnectivityFailure extends Failure { const ConnectivityFailure(super.message); @@ -56,7 +74,9 @@ class DataParsingFailure extends Failure { } class UnknownFailure extends Failure { - const UnknownFailure(super.message); + final String url; + + const UnknownFailure(super.message, {this.url = ""}); @override List get props => [message]; diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index c8adf783..b43858c2 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -1,16 +1,16 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; import 'package:get_it/get_it.dart'; import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.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/common_models/nationality_country_model.dart'; import 'package:hmg_patient_app_new/core/common_models/privilege/HMCProjectListModel.dart'; -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/enums.dart'; @@ -18,7 +18,6 @@ import 'package:hmg_patient_app_new/core/utils/loading_utils.dart'; import 'package:hmg_patient_app_new/core/utils/request_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; -import 'package:hmg_patient_app_new/extensions/context_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_repo.dart'; import 'package:hmg_patient_app_new/features/authentication/models/request_models/check_activation_code_register_request_model.dart'; @@ -37,7 +36,6 @@ import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/localauth_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; -import 'package:hmg_patient_app_new/widgets/bottomsheet/exception_bottom_sheet.dart'; import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; import 'models/request_models/get_user_mobile_device_data.dart'; diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 18b6085b..29da51c4 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -36,7 +36,7 @@ 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/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'; diff --git a/lib/presentation/my_family/my_Family.dart b/lib/presentation/my_family/my_family.dart similarity index 96% rename from lib/presentation/my_family/my_Family.dart rename to lib/presentation/my_family/my_family.dart index 6e4ef913..25f5f3c2 100644 --- a/lib/presentation/my_family/my_Family.dart +++ b/lib/presentation/my_family/my_family.dart @@ -1,11 +1,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.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/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/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'; @@ -13,7 +11,6 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_ 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/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/buttons/custom_button.dart'; @@ -27,10 +24,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(); @@ -85,9 +82,9 @@ class _FamilyMedicalScreenState extends State { ); } - void showModelSheet() { + Future showModelSheet() async { AuthenticationViewModel authVm = getIt.get(); - return showCommonBottomSheetWithoutHeight(context, + return await showCommonBottomSheetWithoutHeight(context, title: "Add Family Member", child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 480f91fe..306692bc 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -43,9 +43,7 @@ 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), @@ -73,13 +71,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 +100,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), diff --git a/lib/presentation/my_family/widget/my_family_sheet.dart b/lib/presentation/my_family/widget/my_family_sheet.dart index a119906b..da822b11 100644 --- a/lib/presentation/my_family/widget/my_family_sheet.dart +++ b/lib/presentation/my_family/widget/my_family_sheet.dart @@ -1,15 +1,13 @@ 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/medical_file/models/family_file_response_model.dart'; -import 'package:hmg_patient_app_new/presentation/my_family/my_Family.dart'; import 'package:hmg_patient_app_new/presentation/my_family/widget/family_cards.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; class MyFamilySheet { - static void show(BuildContext context, List familyLists, Function(FamilyFileResponseModelLists) onSelect) { - return showCommonBottomSheetWithoutHeight( + static Future show(BuildContext context, List familyLists, Function(FamilyFileResponseModelLists) onSelect) async { + return await showCommonBottomSheetWithoutHeight( context, titleWidget: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 29aee3db..6ee0ddc2 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -4,18 +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/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.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:provider/provider.dart'; abstract class DialogService { - Future showErrorBottomSheet({required String message, Function()? onOkPressed}); + Future showErrorBottomSheet({String title = "", required String message, Function()? onOkPressed, Function()? onCancelPressed}); Future showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}); @@ -28,21 +25,40 @@ abstract class DialogService { class DialogServiceImp implements DialogService { final NavigationService navigationService; + bool _isErrorSheetShowing = false; + DialogServiceImp({required this.navigationService}); @override - Future showErrorBottomSheet({required String message, Function()? onOkPressed}) async { + Future showErrorBottomSheet({String title = "", required String message, Function()? onOkPressed, Function()? onCancelPressed}) async { + if (_isErrorSheetShowing) return; final context = navigationService.navigatorKey.currentContext; if (context == null) return; + _isErrorSheetShowing = true; await showModalBottomSheet( context: context, isScrollControlled: false, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), - builder: (_) => _ErrorBottomSheet(message: message, onOkPressed: onOkPressed ?? () {}), + builder: (_) => ExceptionBottomSheet( + // title: title, + message: message, + showCancel: onCancelPressed != null ? true : false, + onOkPressed: () { + if (onOkPressed != null) { + onOkPressed(); + } + }, + onCancelPressed: () { + if (onCancelPressed != null) { + context.pop(); + } + }, + ), ); + _isErrorSheetShowing = false; } @override @@ -73,9 +89,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 @@ -90,10 +105,10 @@ class DialogServiceImp implements DialogService { Widget exceptionBottomSheetWidget({required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) { return Column( children: [ - (message ?? "").toText16(isBold: false, color: AppColors.textColor), + (message).toText16(isBold: false, color: AppColors.textColor), SizedBox(height: 10.h), SizedBox(height: 24.h), - if (onOkPressed != null && onCancelPressed != null) + if (onCancelPressed != null) Row( children: [ Expanded( @@ -112,7 +127,7 @@ Widget exceptionBottomSheetWidget({required BuildContext context, required Strin SizedBox(width: 5.h), Expanded( child: CustomButton( - text: onCancelPressed != null ? LocaleKeys.confirm.tr() : LocaleKeys.ok.tr(), + text: LocaleKeys.confirm.tr(), onPressed: onOkPressed, backgroundColor: AppColors.bgGreenColor, borderColor: AppColors.bgGreenColor, @@ -122,14 +137,14 @@ Widget exceptionBottomSheetWidget({required BuildContext context, required Strin ), ], ), - if (onOkPressed != null && onCancelPressed == null) + if (onCancelPressed == null) Padding( padding: EdgeInsets.only(bottom: 10.h), child: CustomButton( text: LocaleKeys.ok.tr(), - onPressed: (onOkPressed != null && onCancelPressed == null) + onPressed: (onCancelPressed == null) ? () { - Navigator.of(context).pop(); + context.pop(); } : onOkPressed, backgroundColor: AppColors.primaryRedColor, @@ -144,7 +159,6 @@ Widget exceptionBottomSheetWidget({required BuildContext context, required Strin Widget showPhoneNumberPickerWidget({required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) { return StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { - AuthenticationViewModel authViewModel = context.read(); return Column( children: [ (message ?? "").toText16(isBold: false, color: AppColors.textColor), @@ -202,56 +216,3 @@ Widget showPhoneNumberPickerWidget({required BuildContext context, String? messa // ); }); } - -class _ErrorBottomSheet extends StatelessWidget { - final String message; - final Function()? onOkPressed; - - const _ErrorBottomSheet({required this.message, this.onOkPressed}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.error_outline, color: Colors.red, size: 40), - const SizedBox(height: 12), - Text( - "Error", - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Colors.red, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - message, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - if (onOkPressed != null) { - onOkPressed!(); - } else { - context.pop(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text("OK", style: TextStyle(color: Colors.white)).onPress(() { - Navigator.of(context).pop(); - }), - ), - ], - ), - ); - } -} diff --git a/lib/services/error_handler_service.dart b/lib/services/error_handler_service.dart index c29dc176..aeefefb0 100644 --- a/lib/services/error_handler_service.dart +++ b/lib/services/error_handler_service.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:hmg_patient_app_new/core/exceptions/api_exception.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/loading_utils.dart'; -import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -28,46 +27,54 @@ class ErrorHandlerServiceImp implements ErrorHandlerService { if (failure is APIException) { loggerService.errorLogs("API Exception: ${failure.message}"); } else if (failure is ServerFailure) { - loggerService.errorLogs("Server Failure: ${failure.message}"); - await _showDialog(failure); + loggerService.errorLogs("URL: ${failure.url} \n Server Failure: ${failure.message}"); + await _showDialog(failure, title: "Server Failure"); } else if (failure is DataParsingFailure) { loggerService.errorLogs("Data Parsing Failure: ${failure.message}"); await _showDialog(failure, title: "Data Error"); } else if (failure is StatusCodeFailure) { loggerService.errorLogs("StatusCode Failure: ${failure.message}"); - await _showDialog(failure, title: "Status Code Failure Error"); + await _showDialog(failure, title: "StatusCodeFailure"); + } else if (failure is ConnectivityFailure) { + loggerService.errorLogs("ConnectivityFailure : ${failure.message}"); + await _showDialog(failure, title: "ConnectivityFailure ", onOkPressed: () {}); + } else if (failure is UnAuthenticatedUserFailure) { + loggerService.errorLogs("URL: ${failure.url} \n UnAuthenticatedUser Failure: ${failure.message}"); + await _showDialog(failure, title: "UnAuthenticatedUser Failure", onOkPressed: () => navigationService.replaceAllRoutesAndNavigateToLanding()); + } else if (failure is AppUpdateFailure) { + loggerService.errorLogs("AppUpdateFailure : ${failure.message}"); + await _showDialog(failure, title: "AppUpdateFailure Error", onOkPressed: () => navigationService.replaceAllRoutesAndNavigateToLanding()); } else if (failure is HttpException) { loggerService.errorLogs("Http Exception: ${failure.message}"); await _showDialog(failure, title: "Network Error"); } else if (failure is UnknownFailure) { - loggerService.errorLogs("Unknown Failure: ${failure.message}"); - await _showDialog(failure, title: "Unknown Error"); + loggerService.errorLogs("URL: ${failure.url} \n Unknown Failure: ${failure.message}"); + await _showDialog(failure, title: "Unknown Failure"); } else if (failure is InvalidCredentials) { loggerService.errorLogs("Invalid Credentials : ${failure.message}"); - await _showDialog(failure, title: "Unknown Error"); + await _showDialog(failure, title: "Invalid Credentials "); } else if (failure is UserIntimationFailure) { if (onUnHandledFailure != null) { onUnHandledFailure(failure); } else { - await _showDialog(failure, title: "Error", onOkPressed: onOkPressed); + await _showDialog(failure, title: "UserIntimationFailure", onOkPressed: onOkPressed); } } else if (failure is MessageStatusFailure) { if (onMessageStatusFailure != null) { onMessageStatusFailure(failure); } else { - await _showDialog(failure, title: "Error", onOkPressed: onOkPressed); + await _showDialog(failure, title: "MessageStatusFailure", onOkPressed: onOkPressed); } } else { loggerService.errorLogs("Unhandled failure type: $failure"); - await _showDialog(failure, title: "Error", onOkPressed: onOkPressed); + await _showDialog(failure, title: "Unhandled Error", onOkPressed: onOkPressed); } } - Future _showDialog(Failure failure, {String title = "Error", Function()? onOkPressed}) async { + Future _showDialog(Failure failure, {required String title, Function()? onOkPressed}) async { if (LoadingUtils.isLoading) { LoadingUtils.hideFullScreenLoader(); } - - await dialogService.showErrorBottomSheet(message: failure.message, onOkPressed: onOkPressed); + await dialogService.showErrorBottomSheet(title: title, message: failure.message, onOkPressed: onOkPressed); } } diff --git a/lib/services/navigation_service.dart b/lib/services/navigation_service.dart index 420c4cfb..bfdedeff 100644 --- a/lib/services/navigation_service.dart +++ b/lib/services/navigation_service.dart @@ -1,5 +1,8 @@ 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/authentication/widgets/otp_verification_screen.dart'; +import 'package:hmg_patient_app_new/routes/app_routes.dart'; class NavigationService { final GlobalKey navigatorKey = GlobalKey(); @@ -18,6 +21,15 @@ class NavigationService { navigatorKey.currentState?.popUntil(ModalRoute.withName(routeName)); } + void replaceAllRoutesAndNavigateToLanding() { + AppState appState = getIt.get(); + appState.isAuthenticated = false; + navigatorKey.currentState?.pushNamedAndRemoveUntil( + AppRoutes.landingScreen, + (Route route) => false, + ); + } + void pushAndReplace(String routeName) { navigatorKey.currentState?.pushReplacementNamed(routeName); } diff --git a/lib/widgets/bottomsheet/exception_bottom_sheet.dart b/lib/widgets/bottomsheet/exception_bottom_sheet.dart index f0ea651a..c11dec17 100644 --- a/lib/widgets/bottomsheet/exception_bottom_sheet.dart +++ b/lib/widgets/bottomsheet/exception_bottom_sheet.dart @@ -11,13 +11,14 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; class ExceptionBottomSheet extends StatelessWidget { - String message; - bool showOKButton; - bool showCancel; - Function() onOkPressed; - Function()? onCancelPressed; + final String? title; + final String message; + final bool showOKButton; + final bool showCancel; + final Function() onOkPressed; + final Function()? onCancelPressed; - ExceptionBottomSheet({Key? key, required this.message, this.showOKButton = true, this.showCancel = false, required this.onOkPressed, this.onCancelPressed}) : super(key: key); + const ExceptionBottomSheet({super.key, this.title, required this.message, this.showOKButton = true, this.showCancel = false, required this.onOkPressed, this.onCancelPressed}); @override Widget build(BuildContext context) { @@ -44,7 +45,7 @@ class ExceptionBottomSheet extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.notice.tr().toText28(), + (title ?? LocaleKeys.notice.tr()).toText28(), InkWell( onTap: () { Navigator.of(context).pop(); @@ -54,7 +55,7 @@ class ExceptionBottomSheet extends StatelessWidget { ], ), SizedBox(height: 10.h), - (message ?? "").toText16(isBold: false, color: AppColors.textColor), + (message).toText16(isBold: false, color: AppColors.textColor), SizedBox(height: 10.h), SizedBox(height: 24.h), if (showOKButton && showCancel) diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index c50ffada..29df19b2 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -104,7 +104,7 @@ class ButtonSheetContent extends StatelessWidget { } } -void showCommonBottomSheetWithoutHeight( +Future showCommonBottomSheetWithoutHeight( BuildContext context, { required Widget child, required VoidCallback callBackFunc, @@ -113,7 +113,7 @@ void showCommonBottomSheetWithoutHeight( bool isFullScreen = true, bool isDismissible = true, Widget? titleWidget, -}) { +}) async { showModalBottomSheet( sheetAnimationStyle: AnimationStyle( duration: Duration(milliseconds: 500), // Custom animation duration From 1af2db5c098b826bb71216aa15b5fddd110d3fcd Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 29 Sep 2025 15:45:54 +0300 Subject: [PATCH 02/14] 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 a4e76fc9..29b41fd3 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 fa02b2e2..5886dfa9 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 8319634f..3129fbc3 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 cb2f76f4..e78d7bdb 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 cf8b7d7f..06cfd045 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 58273fc9..70f20b10 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 4cc4a7b0d9beaa3bbcfc7b25f541ab33eff6dfeb Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Mon, 29 Sep 2025 15:51:41 +0300 Subject: [PATCH 03/14] merged main --- lib/presentation/lab/lab_results/lab_result_details.dart | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index e3ffaea2..3e9558ca 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -1,29 +1,23 @@ -import 'dart:math'; -import 'package:dartz/dartz.dart'; 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/common_models/data_points.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/history/lab_history_viewmodel.dart'; 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; import '../../../widgets/common_bottom_sheet.dart' show showCommonBottomSheetWithoutHeight; -import '../../book_appointment/widgets/appointment_calendar.dart' - show AppointmentCalendar; class LabResultDetails extends StatelessWidget { final LabResult recentLabResult; From 2de02102c26571b014d686c7863b363b49d3b50b Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 29 Sep 2025 17:30:13 +0300 Subject: [PATCH 04/14] 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 e3ffaea2..9935c0de 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 70f20b10..e5ec389a 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 05/14] 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 5886dfa9..fa02b2e2 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 65f97ae7..d3675095 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 3129fbc3..0bcf30f3 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 6b83f335..658d045a 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 2e528dec..061392e6 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 99f7c7d6..eb8611dd 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 152436a1..d2877957 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 41640b73..bc56473e 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 de5581cb..1c7b1b96 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 aa04d8c9..08e252ba 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 06cfd045..b89d3e87 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 28045fb8..2a9fcfe0 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 9baf538c..00000000 --- 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 00000000..8f891e2a --- /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 00000000..c08c750b --- /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 3bdaa4af..72e75f82 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 06/14] 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 29b41fd3..5d4da2b9 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 fa02b2e2..1c6cec17 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 658d045a..7077fd05 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 061392e6..95c5d63f 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 d2877957..6d157a56 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 2a9fcfe0..7fd9138e 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 c4980d98..e8cff72b 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 5cd25470..fed7d152 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 8f891e2a..9d1bcb19 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 c08c750b..71abcb78 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 40152494..291f81db 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 2f8a9ecd..a4bec4cb 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 a4db1721..16db798a 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 4a2322c6..38e5829c 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 07/14] 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 00000000..058b4cae --- /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 00000000..e063de89 --- /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 31b4f329..446bbc24 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 5e995853..18b53309 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 5ef653ae..ab4ca56f 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 00000000..5c5b9006 --- /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 6ba14f53..012a8dbb 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 00000000..8ec6e4b3 --- /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 00000000..689dc9b7 --- /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 00000000..1642d603 --- /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 10b77206f6060940f08d758061fac5e09043f346 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 1 Oct 2025 17:29:59 +0300 Subject: [PATCH 08/14] 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 00000000..3f82a2e3 --- /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 446bbc24..016e42df 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 4568a15f..f59d6571 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 efa01d05..8830b78c 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 18b53309..0a137cee 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 ab4ca56f..120aa3bf 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 00000000..9a5861e2 --- /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 6976188a..54353a44 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 9d6b5572..7334369e 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 00000000..76f6ad1d --- /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 00000000..d4a108c5 --- /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 8ec6e4b3..69101137 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 aa97fdfa..41d2b15d 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 09/14] 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 d8a3d51d..e4fd2543 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 00000000..c97ff8be --- /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 00000000..40e86453 --- /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 a982556b..be64c530 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 016e42df..44f979c2 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 b42706f4..33fa0dfe 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 8830b78c..f3a53b91 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 0a137cee..43a7eb3b 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 120aa3bf..8a46f91e 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 00000000..eee4adba --- /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 00000000..599bc39c --- /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 00000000..0077d830 --- /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 3690c724..eff60cca 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 6d157a56..3a651245 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 012a8dbb..516aff9e 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 76f6ad1d..af7d053d 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 d4a108c5..d89ef33b 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 69101137..8181d25d 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 10/14] 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 00000000..bcf9f909 --- /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 00000000..521f6fac --- /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 00000000..615b42d9 --- /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 8945fee5..b08e7830 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 843daf89..adab1d5c 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 31b4f329..716448db 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 5ef653ae..bd292ecd 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 00000000..889ab5a2 --- /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 3d12e5bb..9447bd11 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 3690c724..4dfba4be 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 00000000..58a0d009 --- /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 00000000..c81e5485 --- /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 00000000..155d8e36 --- /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 00000000..0d5ba764 --- /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 00000000..1a772f76 --- /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 00000000..f416ed4d --- /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 8f611dcf..233aff94 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 fd6e6918..ec17c618 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 a4db1721..084f85b9 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 4a2322c6..4b060964 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 11/14] 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 00000000..177ef651 --- /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 44f979c2..8ca73b56 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 fafcafe4..8d69eb4d 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 12/14] 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 ec17c618..59756317 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 13/14] 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 233aff94..5949ef58 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 14/14] 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 90b6cb1d..2d671cfa 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 3e7b7c99..434bad24 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 fed7d152..1f6c0bfc 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, ), ),