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!)),
);
}