diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ffe48033..b10aeef0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -168,7 +168,7 @@ android:enabled="true" android:exported="true" android:hardwareAccelerated="true" - android:launchMode="singleTop" + android:launchMode="singleTask" android:showOnLockScreen="true" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize" @@ -205,6 +205,21 @@ + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 85d84646..3df3ba06 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -30,6 +30,8 @@ FIVKSupportVideo + FlutterDeepLinkingEnabled + LSApplicationQueriesSchemes comgooglemaps diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 2c37e774..874ac0d6 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -21,5 +21,10 @@ TAG + com.apple.developer.associated-domains + + applinks:hmgapps.com + applinks:www.hmgapps.com + diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index bf30324e..7623a8e3 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -6,7 +6,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index 25753e1a..7ca730b8 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -51,6 +51,9 @@ class AppState { bool isMyFatoorahEnabled = false; // bool isMyFatoorahEnabled = true; + String? deepLinkJoinSessionId; + bool isSplashInitializationDone = false; + void setIsPaytabsEnabled(bool value) { isPaytabsEnabled = false; // isPaytabsEnabled = true; diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index 46d24eb7..c443ff48 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -25,7 +25,12 @@ import 'models/resp_models/appointment_details_resp_model.dart'; abstract class MyAppointmentsRepo { Future>>> getPatientAppointments( - {required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false, bool isForTimeLine = false}); + {required bool isActiveAppointment, + required bool isArrivedAppointments, + bool isForEyeMeasurement = false, + bool isForTimeLine = false, + bool isAppointmentForHomePage = false, + bool isAppointmentForViewAll = false}); Future>> getPatientShareAppointment( {required int projectID, required int clinicID, required String appointmentNo, required bool isLiveCareAppointment}); @@ -95,6 +100,8 @@ abstract class MyAppointmentsRepo { Future>>> getAppointmentInvoice(num appointmentNum); Future>> getParkingQR(num appointmentNum, int projectID); + + Future>> getAppointmentDetailsByAppointmentNumber(int appointmentID, int projectID, int clinicID); } class MyAppointmentsRepoImp implements MyAppointmentsRepo { @@ -104,8 +111,14 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { MyAppointmentsRepoImp({required this.loggerService, required this.apiClient}); @override - Future>>> getPatientAppointments( - {required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false, bool isForTimeLine = false}) async { + Future>>> getPatientAppointments({ + required bool isActiveAppointment, + required bool isArrivedAppointments, + bool isForEyeMeasurement = false, + bool isForTimeLine = false, + bool isAppointmentForHomePage = false, + bool isAppointmentForViewAll = false, + }) async { Map mapDevice = { "IsActiveAppointment": isActiveAppointment, "IsComingFromCOC": false, @@ -113,6 +126,8 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { "IsForMedicalReport": false, "IsForArrived": isArrivedAppointments, "IsIrisPrescription": isForEyeMeasurement, + "IsForHomePage": isAppointmentForHomePage, + "IsForAppointmentViewAll": isAppointmentForViewAll, "IsForTimeLine": isForTimeLine }; @@ -1225,4 +1240,42 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> getAppointmentDetailsByAppointmentNumber(int appointmentID, int projectID, int clinicID) async { + Map mapDevice = { + "AppointmentNo": appointmentID, + "ProjectID": projectID, + "ClinicID":clinicID, + "IsActiveAppointment": true, + }; + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(GET_PATIENT_APPOINTMENT_HISTORY, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppoimentAllHistoryResultList']; + + final appointmentDetails = PatientAppointmentHistoryResponseModel.fromJson(list[0]); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: appointmentDetails, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: mapDevice); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + } diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index f040fd82..4b2a2550 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -57,6 +57,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { bool isMyDoctorsDataToBeLoaded = true; bool isArrivedAppointmentDataLoaded = false; bool isFullArrivedAppointmentsLoaded = false; + bool isFullUpcomingAppointmentsLoaded = false; bool isEyeMeasurementsAppointmentsLoading = false; @@ -77,6 +78,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { List patientUpcomingAppointmentsHistoryList = []; List patientArrivedAppointmentsHistoryList = []; + List patientHomePageUpcomingAppointmentsHistoryList = []; // List patientAllArrivedAppointmentsHistoryList = []; @@ -111,6 +113,9 @@ class MyAppointmentsViewModel extends ChangeNotifier { Uint8List? parkingQRImageBytes; + late PatientAppointmentHistoryResponseModel currentSelectedAppointmentDetails; + bool isAppointmentDetailsLoading = false; + MyAppointmentsViewModel({required this.myAppointmentsRepo, required this.errorHandlerService, required this.appState}); void onTabChange(int index) { @@ -174,12 +179,14 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); patientArrivedAppointmentsHistoryList.clear(); + patientHomePageUpcomingAppointmentsHistoryList.clear(); // patientAllArrivedAppointmentsHistoryList.clear(); patientEyeMeasurementsAppointmentsHistoryList.clear(); isMyAppointmentsLoading = true; isTimeLineAppointmentsLoading = true; patientMyDoctorsList.clear(); isFullArrivedAppointmentsLoaded = false; + isFullUpcomingAppointmentsLoaded = false; } isTamaraDetailsLoading = true; isAppointmentPatientShareLoading = true; @@ -223,6 +230,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { if (val) { isMyAppointmentsLoading = true; isMyDoctorsDataToBeLoaded = true; + isFullUpcomingAppointmentsLoaded = false; + isFullArrivedAppointmentsLoaded = false; } isAppointmentDataToBeLoaded = val; notifyListeners(); @@ -269,7 +278,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { ); } - Future getPatientAppointments(bool isActiveAppointment, bool isArrivedAppointments, {bool isForTimeLine = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future getPatientAppointments(bool isActiveAppointment, bool isArrivedAppointments, + {bool isAppointmentForHomePage = false, bool isAppointmentForViewAll = false, bool isForTimeLine = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { if (patientArrivedAppointmentsHistoryList.isNotEmpty) { isPatientHasQueueAppointment = false; notifyListeners(); @@ -279,13 +289,13 @@ class MyAppointmentsViewModel extends ChangeNotifier { } } - // Skip API call if full data is already loaded and we're not requesting timeline view - if (!isForTimeLine && isFullArrivedAppointmentsLoaded && !isAppointmentDataToBeLoaded) { + // Retain data and skip API call once all upcoming appointments are loaded + // (or if home page data is already loaded and view-all is not requested) + // until isAppointmentDataToBeLoaded is set to true. + if (!isAppointmentDataToBeLoaded && (isFullUpcomingAppointmentsLoaded || !isAppointmentForViewAll)) { return; } - if (!isAppointmentDataToBeLoaded) return; - patientAppointmentsByClinic.clear(); patientAppointmentsByHospital.clear(); patientAppointmentsViewList.clear(); @@ -294,11 +304,16 @@ class MyAppointmentsViewModel extends ChangeNotifier { filteredAppointmentList.clear(); patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); + if (!isAppointmentForViewAll) { + patientHomePageUpcomingAppointmentsHistoryList.clear(); + } patientArrivedAppointmentsHistoryList.clear(); + setIsAppointmentsHistoryLoading(true); notifyListeners(); final results = await Future.wait([ - myAppointmentsRepo.getPatientAppointments(isActiveAppointment: isActiveAppointment, isArrivedAppointments: isArrivedAppointments), + myAppointmentsRepo.getPatientAppointments( + isActiveAppointment: isActiveAppointment, isArrivedAppointments: isArrivedAppointments, isAppointmentForHomePage: isAppointmentForHomePage, isAppointmentForViewAll: isAppointmentForViewAll), myAppointmentsRepo.getPatientAppointments(isActiveAppointment: false, isArrivedAppointments: true, isForTimeLine: isForTimeLine), ]); @@ -307,8 +322,32 @@ class MyAppointmentsViewModel extends ChangeNotifier { (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + if (isAppointmentForViewAll) { + if (patientHomePageUpcomingAppointmentsHistoryList.isNotEmpty) { + patientUpcomingAppointmentsHistoryList.addAll(patientHomePageUpcomingAppointmentsHistoryList); + } + isFullUpcomingAppointmentsLoaded = true; + } + isMyAppointmentsLoading = false; + isAppointmentDataToBeLoaded = false; + notifyListeners(); } else if (apiResponse.messageStatus == 1) { - patientUpcomingAppointmentsHistoryList = apiResponse.data!; + patientUpcomingAppointmentsHistoryList = List.from(apiResponse.data!); + + if (!isAppointmentForViewAll) { + patientHomePageUpcomingAppointmentsHistoryList = List.from(patientUpcomingAppointmentsHistoryList); + } + if (isAppointmentForViewAll) { + if (patientHomePageUpcomingAppointmentsHistoryList.isNotEmpty) { + final partialAppointmentNos = patientHomePageUpcomingAppointmentsHistoryList.map((e) => e.appointmentNo?.toString()).whereType().toSet(); + patientUpcomingAppointmentsHistoryList.removeWhere( + (appointment) => partialAppointmentNos.contains(appointment.appointmentNo?.toString()), + ); + patientUpcomingAppointmentsHistoryList.insertAll(0, patientHomePageUpcomingAppointmentsHistoryList); + } + isFullUpcomingAppointmentsLoaded = true; + } + isMyAppointmentsLoading = false; isAppointmentDataToBeLoaded = false; notifyListeners(); @@ -324,6 +363,13 @@ class MyAppointmentsViewModel extends ChangeNotifier { (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + isMyAppointmentsLoading = false; + isAppointmentDataToBeLoaded = false; + if (!isForTimeLine) { + isArrivedAppointmentDataLoaded = true; + isFullArrivedAppointmentsLoaded = true; + } + notifyListeners(); } else if (apiResponse.messageStatus == 1) { patientArrivedAppointmentsHistoryList = apiResponse.data!; isMyAppointmentsLoading = false; @@ -343,10 +389,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsHistoryList.addAll(patientUpcomingAppointmentsHistoryList); patientAppointmentsHistoryList.addAll(patientArrivedAppointmentsHistoryList); - filteredAppointmentList.addAll(patientAppointmentsHistoryList); - - // Build grouped list immediately so the UI has data for the default (By Clinic) view. - _groupAppointmentsByClinicAndHospital(); + updateListWRTTab(selectedTabIndex); // if (patientArrivedAppointmentsHistoryList.isNotEmpty) { isPatientHasQueueAppointment = false; @@ -359,7 +402,6 @@ class MyAppointmentsViewModel extends ChangeNotifier { } // } - getFiltersForSelectedAppointmentList(filteredAppointmentList); notifyListeners(); } @@ -1722,4 +1764,31 @@ class MyAppointmentsViewModel extends ChangeNotifier { }, ); } + + updateAppointmentDetailsPage() { + notifyListeners(); + } + + Future getAppointmentDetailsByAppointmentNumber({required int projectID, required int appointmentNo, required int clinicId, Function(dynamic)? onSuccess, Function(String)? onError}) async { + isAppointmentDetailsLoading = true; + notifyListeners(); + + final result = await myAppointmentsRepo.getAppointmentDetailsByAppointmentNumber(appointmentNo, projectID, clinicId); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + } else if (apiResponse.messageStatus == 1) { + currentSelectedAppointmentDetails = apiResponse.data!; + isAppointmentDetailsLoading = false; + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + } diff --git a/lib/features/my_appointments/utils/appointment_type.dart b/lib/features/my_appointments/utils/appointment_type.dart index 13811737..f04a6e26 100644 --- a/lib/features/my_appointments/utils/appointment_type.dart +++ b/lib/features/my_appointments/utils/appointment_type.dart @@ -57,6 +57,8 @@ class AppointmentType { return AppColors.successColor; case 90: return AppColors.alertColor; + case 110: + return AppColors.primaryRedColor; default: return AppColors.successColor; } @@ -76,6 +78,8 @@ class AppointmentType { return AppColors.successColor; case 90: return AppColors.alertColor; + case 110: + return AppColors.primaryRedColor; default: return AppColors.successColor; } @@ -95,6 +99,8 @@ class AppointmentType { return LocaleKeys.confirmLiveCare.tr(); case 90: return LocaleKeys.checkinOption.tr(); + case 110: + return LocaleKeys.viewDetails.tr(); default: return LocaleKeys.upcomingNoAction.tr(); } @@ -114,6 +120,8 @@ class AppointmentType { return AppAssets.confirm_appointment_icon; case 90: return AppAssets.appointment_checkin_icon; + case 110: + return AppAssets.calendar; default: return AppAssets.checkmark_icon; } diff --git a/lib/main.dart b/lib/main.dart index 47416b49..36c59870 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/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/utils.dart'; @@ -55,11 +56,14 @@ import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_mode import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/security_service.dart'; +import 'package:hmg_patient_app_new/splashPage.dart'; import 'package:hmg_patient_app_new/theme/app_theme.dart'; import 'package:hmg_patient_app_new/unsafe_device.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; @@ -142,13 +146,13 @@ Future callInitializations() async { await callAppStateInitializations(); // Initialize Security Service early to catch threats before app logic runs - if (kReleaseMode) { - try { - await getIt.get().initialize(); - } catch (e) { - getIt.get().logError('SecurityService.initialize failed, continuing app startup: $e'); - } - } + // if (kReleaseMode) { + // try { + // await getIt.get().initialize(); + // } catch (e) { + // getIt.get().logError('SecurityService.initialize failed, continuing app startup: $e'); + // } + // } // Set up critical threat callback to navigate to unsafe device page getIt.get().setOnCriticalThreatCallback(() { @@ -386,6 +390,16 @@ class MyApp extends StatelessWidget { } } + String? _extractJoinSessionId(String? routeName) { + if (routeName == null || routeName.isEmpty) return null; + // Matches both "https://hmgapps.com/join/{id}" and "/join/{id}" + final match = RegExp(r'/join/([^?#]+)').firstMatch(routeName); + if (match != null && match.group(1)!.isNotEmpty) { + return Uri.decodeComponent(match.group(1)!); + } + return null; + } + @override Widget build(BuildContext context) { return SafeArea( @@ -422,6 +436,51 @@ class MyApp extends StatelessWidget { locale: context.locale, initialRoute: AppRoutes.initialRoute, routes: AppRoutes.routes, + onGenerateInitialRoutes: (String initialRouteName) { + final defaultRoute = WidgetsBinding.instance.platformDispatcher.defaultRouteName; + final sessionId = _extractJoinSessionId(initialRouteName) ?? _extractJoinSessionId(defaultRoute); + if (sessionId != null) { + getIt.get().deepLinkJoinSessionId = sessionId; + Utils.saveStringFromPrefs(CacheConst.zoomRoomID, sessionId); + debugPrint('DeepLink (Cold Start) Join Session ID: $sessionId'); + } + return [ + MaterialPageRoute(builder: (_) => const SplashPage()), + ]; + }, + onGenerateRoute: (RouteSettings settings) { + final sessionId = _extractJoinSessionId(settings.name); + if (sessionId != null) { + final appState = getIt.get(); + Utils.saveStringFromPrefs(CacheConst.zoomRoomID, sessionId); + debugPrint('DeepLink (Route) Join Session ID: $sessionId'); + // If SplashPage is still initializing, save it so SplashPage navigates when ready + if (!appState.isSplashInitializationDone) { + appState.deepLinkJoinSessionId = sessionId; + return MaterialPageRoute(builder: (_) => const SplashPage()); + } + // Warm Start: App is already initialized, open CallScreen directly + return MaterialPageRoute( + settings: RouteSettings( + name: AppRoutes.zoomCallPage, + arguments: CallArguments(sessionId, "123", "Patient", "40", "0", true, 1), + ), + builder: (_) => const CallScreen(), + ); + } + // Fallback for normal named routes if any reach onGenerateRoute + final builder = AppRoutes.routes[settings.name]; + if (builder != null) { + return MaterialPageRoute(builder: builder, settings: settings); + } + return null; + }, + onUnknownRoute: (RouteSettings settings) { + // Prevents crash when opened via root link https://hmgapps.com + return MaterialPageRoute( + builder: (_) => getIt.get().isSplashInitializationDone ? LandingNavigation() : const SplashPage(), + ); + }, theme: AppTheme.getTheme(isArabic), darkTheme: AppTheme.getDarkTheme(isArabic), themeMode: profileVm.isDarkMode ? ThemeMode.dark : ThemeMode.light, diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 947d3efe..b28a93b9 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -61,9 +61,9 @@ import 'package:provider/provider.dart'; import '../medical_file/widgets/medical_file_card.dart'; class AppointmentDetailsPage extends StatefulWidget { - const AppointmentDetailsPage({super.key, required this.patientAppointmentHistoryResponseModel}); + AppointmentDetailsPage({super.key, required this.patientAppointmentHistoryResponseModel}); - final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; + PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; @override State createState() => _AppointmentDetailsPageState(); @@ -103,6 +103,17 @@ class _AppointmentDetailsPageState extends State { projectID: widget.patientAppointmentHistoryResponseModel.projectID, appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo); } else { + if (widget.patientAppointmentHistoryResponseModel.nextAction == 110) { + myAppointmentsViewModel + .getAppointmentDetailsByAppointmentNumber( + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo, + clinicId: widget.patientAppointmentHistoryResponseModel.clinicID) + .then((val) { + widget.patientAppointmentHistoryResponseModel = myAppointmentsViewModel.currentSelectedAppointmentDetails; + myAppointmentsViewModel.updateAppointmentDetailsPage(); + }); + } Duration difference = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate).difference(DateTime.now()); if (difference.inMinutes > 240) { isAppointmentWithin4Hours = false; @@ -1043,8 +1054,7 @@ class _AppointmentDetailsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 || - widget.patientAppointmentHistoryResponseModel.nextAction == 20) + if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 || widget.patientAppointmentHistoryResponseModel.nextAction == 20) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1052,10 +1062,7 @@ class _AppointmentDetailsPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.amountBeforeTax.tr(context: context).toText18(isBold: true), - Utils.getPaymentAmountWithSymbol( - widget.patientAppointmentHistoryResponseModel.patientShare!.toString().toText16(isBold: true), - AppColors.blackColor, - 13, + Utils.getPaymentAmountWithSymbol(widget.patientAppointmentHistoryResponseModel.patientShare!.toString().toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: true), ], ), @@ -1063,10 +1070,8 @@ class _AppointmentDetailsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: LocaleKeys.upcomingPaymentNow.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor)), - "VAT 15%(${widget.patientAppointmentHistoryResponseModel.patientTaxAmount})" - .toText14(isBold: true, color: AppColors.greyTextColor, letterSpacing: -0.64), + Expanded(child: LocaleKeys.upcomingPaymentNow.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor)), + "VAT 15%(${widget.patientAppointmentHistoryResponseModel.patientTaxAmount})".toText14(isBold: true, color: AppColors.greyTextColor, letterSpacing: -0.64), ], ), SizedBox(height: 18.h), @@ -1077,10 +1082,7 @@ class _AppointmentDetailsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Utils.getPaymentAmountWithSymbol( - widget.patientAppointmentHistoryResponseModel.patientShareWithTax!.toString().toText24(isBold: true), - AppColors.blackColor, - 17, + Utils.getPaymentAmountWithSymbol(widget.patientAppointmentHistoryResponseModel.patientShareWithTax!.toString().toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: true), ], ), @@ -1118,8 +1120,7 @@ class _AppointmentDetailsPageState extends State { handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction); }, backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction), - borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction) - .withValues(alpha: 0.01), + borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), textColor: widget.patientAppointmentHistoryResponseModel.nextAction == 15 ? AppColors.textColor : Colors.white, fontSize: 16.f, fontWeight: FontWeight.w600, @@ -1132,7 +1133,7 @@ class _AppointmentDetailsPageState extends State { ).paddingSymmetrical(16.h, 24.h), ], ), - ), + ).toShimmer2(isShow: myAppointmentsViewModel.isAppointmentDetailsLoading), ), ], ), diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index e15edb47..ec0e4503 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -43,9 +43,11 @@ class _MyAppointmentsPageState extends State { @override void initState() { scheduleMicrotask(() { - if (!myAppointmentsViewModel.isMyAppointmentsLoading) { + if (myAppointmentsViewModel.isAppointmentDataToBeLoaded || !myAppointmentsViewModel.isFullUpcomingAppointmentsLoaded) { myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false); + myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false, isAppointmentForViewAll: true, isAppointmentForHomePage: false); + } else { + myAppointmentsViewModel.updateListWRTTab(myAppointmentsViewModel.selectedTabIndex); } // myAppointmentsViewModel.changeTabToArrived(); }); diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index b9a2f26f..3a89ffc3 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -729,6 +729,7 @@ class _AppointmentCardState extends State { Future handleAppointmentNextAction(nextAction, BuildContext context) async { switch (nextAction) { case 0: + case 110: // No action needed _goToDetails(context); break; diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index a11bd611..048d6afc 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -166,7 +166,7 @@ class _LandingPageState extends State { immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.getPatientLiveCareHistory(); myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true); + myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true, isAppointmentForHomePage: true, isAppointmentForViewAll: false); emergencyServicesViewModel.checkPatientERAdvanceBalance(); // myAppointmentsViewModel.getPatientAppointmentQueueDetails(); notificationsViewModel.initNotificationsViewModel(); @@ -291,7 +291,7 @@ class _LandingPageState extends State { // Refresh Appointments Data myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true); + myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true, isAppointmentForHomePage: true, isAppointmentForViewAll: false); medicalFileViewModel.getPatientAllergiesList(); // Refresh Appointments Data // habibWalletVM.initHabibWalletProvider(); diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index e34a12aa..c8fbdb65 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -346,7 +346,7 @@ class _FamilyCardsState extends State { AppCustomChipWidget( icon: AppAssets.blood_icon, labelText: appState.getUserBloodGroup.isEmpty ? "N/A" : appState.getUserBloodGroup, - iconColor: AppColors.primaryRedColor, + iconColor: AppColors.primaryRedColorBase, labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), padding: EdgeInsets.zero, ), diff --git a/lib/presentation/todo_section/ancillary_procedures_details_page.dart b/lib/presentation/todo_section/ancillary_procedures_details_page.dart index 24a6d97b..50998485 100644 --- a/lib/presentation/todo_section/ancillary_procedures_details_page.dart +++ b/lib/presentation/todo_section/ancillary_procedures_details_page.dart @@ -16,12 +16,15 @@ import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/anc import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_order_payment_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.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/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; @@ -678,20 +681,24 @@ class _AncillaryOrderDetailsListState extends State { onPressed: () { // Navigate to payment page with selected procedures todoSectionViewModel.setIsAncillaryOrdersNeedReloading(true); - Navigator.of(context).push( - CustomPageRoute( - page: AncillaryOrderPaymentPage( - appointmentNoVida: widget.appointmentNoVida, - orderNo: widget.orderNo, - projectID: widget.projectID, - clinicID: widget.clinicID, - selectedProcedures: selectedProcedures, - totalAmount: _getTotalAmount(), - appointmentDate: orderData.appointmentDate, - setupID: widget.setupID, + if (_getTotalAmount() == 0 || _getTotalAmount() == 0.0) { + _autoGenerateInvoice(); + } else { + Navigator.of(context).push( + CustomPageRoute( + page: AncillaryOrderPaymentPage( + appointmentNoVida: widget.appointmentNoVida, + orderNo: widget.orderNo, + projectID: widget.projectID, + clinicID: widget.clinicID, + selectedProcedures: selectedProcedures, + totalAmount: _getTotalAmount(), + appointmentDate: orderData.appointmentDate, + setupID: widget.setupID, + ), ), - ), - ); + ); + } }, isDisabled: !isButtonEnabled, textColor: Colors.white, @@ -704,4 +711,49 @@ class _AncillaryOrderDetailsListState extends State { ).paddingOnly(left: 16.h, top: 24.h, right: 16.h, bottom: 0.h), ); } + + void _autoGenerateInvoice() { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.generatingInvoicePleaseWait.tr(context: context)); + + List selectedProcListAPI = selectedProcedures.map((element) { + return { + "ApprovalLineItemNo": element.approvalLineItemNo, + "OrderLineItemNo": element.orderLineItemNo, + "ProcedureID": element.procedureID, + }; + }).toList(); + + todoSectionViewModel.autoGenerateAncillaryOrdersInvoice( + orderNo: widget.orderNo, + projectID: widget.projectID, + appointmentNo: widget.appointmentNoVida, + selectedProcedures: selectedProcListAPI, + languageID: appState.isArabic() ? 1 : 2, + onSuccess: (response) { + LoaderBottomSheet.hideLoader(); + final invoiceNo = response['AncillaryOrderInvoiceList']?[0]?['InvoiceNo']; + showCommonBottomSheetWithoutHeight(context, + child: Utils.getSuccessWidget(loadingText: "${LocaleKeys.paymentCompletedSuccessfully.tr(context: context)}, ${LocaleKeys.hereIsYourInvoiceNumber.tr(context: context)}$invoiceNo"), + isCloseButtonVisible: true, + isDismissible: true, + isFullScreen: false, + isAutoDismiss: true, callBackFunc: () { + // Pop until we reach the LandingPage + todoSectionViewModel.setIsAncillaryOrdersNeedReloading(true); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }); + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + todoSectionViewModel.setProcessingPayment(false); + showCommonBottomSheetWithoutHeight(context, + child: Utils.getErrorWidget(loadingText: error.toString()), isCloseButtonVisible: true, isDismissible: true, isFullScreen: false, callBackFunc: () {}); + }, + ); + } } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 496be3cb..970e46fb 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -115,6 +115,7 @@ class AppRoutes { static const String refundRequestDetailScreen = '/refundRequestDetailScreen'; static Map get routes => { + '/': (context) => SplashPage(), initialRoute: (context) => SplashPage(), loginScreen: (context) => LoginScreen(), landingScreen: (context) => LandingNavigation(), diff --git a/lib/services/app_lifecycle_service.dart b/lib/services/app_lifecycle_service.dart index 4a0dd947..7bdf7058 100644 --- a/lib/services/app_lifecycle_service.dart +++ b/lib/services/app_lifecycle_service.dart @@ -1,5 +1,12 @@ import 'dart:async'; import 'package:flutter/material.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/presentation/tele_consultation/zoom/call_screen.dart'; +import 'package:hmg_patient_app_new/routes/app_routes.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; /// Service to monitor and track app lifecycle state changes /// This helps detect when app goes to background/foreground @@ -60,7 +67,52 @@ class AppLifecycleService with WidgetsBindingObserver { _stateController.add(state); } - + + String? _extractJoinSessionId(String? routeName) { + if (routeName == null || routeName.isEmpty) return null; + final match = RegExp(r'/join/([^?#]+)').firstMatch(routeName); + if (match != null && match.group(1)!.isNotEmpty) { + return Uri.decodeComponent(match.group(1)!); + } + return null; + } + + Future _handleIncomingDeepLink(String? route) async { + debugPrint('🔗 DeepLink received in AppLifecycleService: $route'); + final sessionId = _extractJoinSessionId(route); + if (sessionId != null) { + final appState = getIt.get(); + await Utils.saveStringFromPrefs(CacheConst.zoomRoomID, sessionId); + debugPrint('🔗 DeepLink Join Session ID parsed: $sessionId (splashDone=${appState.isSplashInitializationDone})'); + + if (!appState.isSplashInitializationDone) { + appState.deepLinkJoinSessionId = sessionId; + return true; + } + + final navState = getIt.get().navigatorKey.currentState; + if (navState != null) { + String callTypeID = await Utils.getStringFromPrefs(CacheConst.callTypeID); + navState.pushNamed( + AppRoutes.zoomCallPage, + arguments: CallArguments(sessionId, "123", "Patient", "40", "0", true, int.tryParse(callTypeID) ?? 1), + ); + return true; + } + } + return false; + } + + @override + Future didPushRouteInformation(RouteInformation routeInformation) { + return _handleIncomingDeepLink(routeInformation.uri.toString()); + } + + @override + Future didPushRoute(String route) { + return _handleIncomingDeepLink(route); + } + /// Initialize the lifecycle observer void initialize() { WidgetsBinding.instance.addObserver(this); diff --git a/lib/splashPage.dart b/lib/splashPage.dart index b1d03c80..3fc7a57e 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -90,8 +90,13 @@ class _SplashScreenState extends State { _initializeClarity(); } + getIt.get().isSplashInitializationDone = true; + if (isAppOpenedFromCall) { navigateToTeleConsult(); + } else if (getIt.get().deepLinkJoinSessionId != null) { + getIt.get().deepLinkJoinSessionId = null; + navigateToTeleConsult(); } else { if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); @@ -166,13 +171,13 @@ class _SplashScreenState extends State { // GetIt.instance().remove(key: CacheConst.isAppOpenedFromCall); Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall); - Navigator.of(GetIt.instance().navigatorKey.currentContext!).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); - Navigator.pushReplacementNamed( + Navigator.of(GetIt.instance().navigatorKey.currentContext!).pushReplacement(FadePage(page: LandingNavigation())); + Navigator.pushNamed( // context, GetIt.instance().navigatorKey.currentContext!, AppRoutes.zoomCallPage, // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), - arguments: CallArguments(roomID, "123", "Patient", "40", "0", true, int.parse(callTypeID)), + arguments: CallArguments(roomID, "123", "Patient", "40", "0", true, int.tryParse(callTypeID) ?? 1), // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), ); }