haroon_dev #402

Merged
Haroon6138 merged 5 commits from haroon_dev into master 1 day ago

@ -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 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="true" />
<!-- Android App Links for hmgapps.com -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:scheme="http" />
<data android:host="hmgapps.com" />
<data android:host="www.hmgapps.com" />
</intent-filter>
</activity> <!-- <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver" android:exported="true"> -->
<!-- <intent-filter> -->
<!-- <action android:name="android.intent.action.BOOT_COMPLETED"/> -->

@ -30,6 +30,8 @@
<true/>
<key>FIVKSupportVideo</key>
<true/>
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>comgooglemaps</string>

@ -21,5 +21,10 @@
<array>
<string>TAG</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:hmgapps.com</string>
<string>applinks:www.hmgapps.com</string>
</array>
</dict>
</plist>

@ -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

@ -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;

@ -25,7 +25,12 @@ import 'models/resp_models/appointment_details_resp_model.dart';
abstract class MyAppointmentsRepo {
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> 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<Either<Failure, GenericApiModel<PatientAppointmentShareResponseModel>>> getPatientShareAppointment(
{required int projectID, required int clinicID, required String appointmentNo, required bool isLiveCareAppointment});
@ -95,6 +100,8 @@ abstract class MyAppointmentsRepo {
Future<Either<Failure, GenericApiModel<List<GetInvoicesListResponseModel>>>> getAppointmentInvoice(num appointmentNum);
Future<Either<Failure, GenericApiModel<AppointmentParkingQRResponseModel>>> getParkingQR(num appointmentNum, int projectID);
Future<Either<Failure, GenericApiModel<PatientAppointmentHistoryResponseModel>>> 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<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientAppointments(
{required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false, bool isForTimeLine = false}) async {
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientAppointments({
required bool isActiveAppointment,
required bool isArrivedAppointments,
bool isForEyeMeasurement = false,
bool isForTimeLine = false,
bool isAppointmentForHomePage = false,
bool isAppointmentForViewAll = false,
}) async {
Map<String, dynamic> 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<Either<Failure, GenericApiModel<PatientAppointmentHistoryResponseModel>>> getAppointmentDetailsByAppointmentNumber(int appointmentID, int projectID, int clinicID) async {
Map<String, dynamic> mapDevice = {
"AppointmentNo": appointmentID,
"ProjectID": projectID,
"ClinicID":clinicID,
"IsActiveAppointment": true,
};
try {
GenericApiModel<PatientAppointmentHistoryResponseModel>? 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<PatientAppointmentHistoryResponseModel>(
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()));
}
}
}

@ -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<PatientAppointmentHistoryResponseModel> patientUpcomingAppointmentsHistoryList = [];
List<PatientAppointmentHistoryResponseModel> patientArrivedAppointmentsHistoryList = [];
List<PatientAppointmentHistoryResponseModel> patientHomePageUpcomingAppointmentsHistoryList = [];
// List<PatientAppointmentHistoryResponseModel> 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<void> getPatientAppointments(bool isActiveAppointment, bool isArrivedAppointments, {bool isForTimeLine = false, Function(dynamic)? onSuccess, Function(String)? onError}) async {
Future<void> 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<PatientAppointmentHistoryResponseModel>.from(apiResponse.data!);
if (!isAppointmentForViewAll) {
patientHomePageUpcomingAppointmentsHistoryList = List<PatientAppointmentHistoryResponseModel>.from(patientUpcomingAppointmentsHistoryList);
}
if (isAppointmentForViewAll) {
if (patientHomePageUpcomingAppointmentsHistoryList.isNotEmpty) {
final partialAppointmentNos = patientHomePageUpcomingAppointmentsHistoryList.map((e) => e.appointmentNo?.toString()).whereType<String>().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<void> 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);
}
}
},
);
}
}

@ -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;
}

@ -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<void> callInitializations() async {
await callAppStateInitializations();
// Initialize Security Service early to catch threats before app logic runs
if (kReleaseMode) {
try {
await getIt.get<SecurityService>().initialize();
} catch (e) {
getIt.get<LoggerService>().logError('SecurityService.initialize failed, continuing app startup: $e');
}
}
// if (kReleaseMode) {
// try {
// await getIt.get<SecurityService>().initialize();
// } catch (e) {
// getIt.get<LoggerService>().logError('SecurityService.initialize failed, continuing app startup: $e');
// }
// }
// Set up critical threat callback to navigate to unsafe device page
getIt.get<SecurityService>().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<AppState>().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<AppState>();
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<AppState>().isSplashInitializationDone ? LandingNavigation() : const SplashPage(),
);
},
theme: AppTheme.getTheme(isArabic),
darkTheme: AppTheme.getDarkTheme(isArabic),
themeMode: profileVm.isDarkMode ? ThemeMode.dark : ThemeMode.light,

@ -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<AppointmentDetailsPage> createState() => _AppointmentDetailsPageState();
@ -103,6 +103,17 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
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<AppointmentDetailsPage> {
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<AppointmentDetailsPage> {
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<AppointmentDetailsPage> {
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<AppointmentDetailsPage> {
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<AppointmentDetailsPage> {
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<AppointmentDetailsPage> {
).paddingSymmetrical(16.h, 24.h),
],
),
),
).toShimmer2(isShow: myAppointmentsViewModel.isAppointmentDetailsLoading),
),
],
),

@ -43,9 +43,11 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
@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();
});

@ -729,6 +729,7 @@ class _AppointmentCardState extends State<AppointmentCard> {
Future<void> handleAppointmentNextAction(nextAction, BuildContext context) async {
switch (nextAction) {
case 0:
case 110:
// No action needed
_goToDetails(context);
break;

@ -166,7 +166,7 @@ class _LandingPageState extends State<LandingPage> {
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<LandingPage> {
// 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();

@ -346,7 +346,7 @@ class _FamilyCardsState extends State<FamilyCards> {
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,
),

@ -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<AncillaryOrderDetailsList> {
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<AncillaryOrderDetailsList> {
).paddingOnly(left: 16.h, top: 24.h, right: 16.h, bottom: 0.h),
);
}
void _autoGenerateInvoice() {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.generatingInvoicePleaseWait.tr(context: context));
List<dynamic> 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: () {});
},
);
}
}

@ -115,6 +115,7 @@ class AppRoutes {
static const String refundRequestDetailScreen = '/refundRequestDetailScreen';
static Map<String, WidgetBuilder> get routes => {
'/': (context) => SplashPage(),
initialRoute: (context) => SplashPage(),
loginScreen: (context) => LoginScreen(),
landingScreen: (context) => LandingNavigation(),

@ -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<bool> _handleIncomingDeepLink(String? route) async {
debugPrint('🔗 DeepLink received in AppLifecycleService: $route');
final sessionId = _extractJoinSessionId(route);
if (sessionId != null) {
final appState = getIt.get<AppState>();
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<NavigationService>().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<bool> didPushRouteInformation(RouteInformation routeInformation) {
return _handleIncomingDeepLink(routeInformation.uri.toString());
}
@override
Future<bool> didPushRoute(String route) {
return _handleIncomingDeepLink(route);
}
/// Initialize the lifecycle observer
void initialize() {
WidgetsBinding.instance.addObserver(this);

@ -90,8 +90,13 @@ class _SplashScreenState extends State<SplashPage> {
_initializeClarity();
}
getIt.get<AppState>().isSplashInitializationDone = true;
if (isAppOpenedFromCall) {
navigateToTeleConsult();
} else if (getIt.get<AppState>().deepLinkJoinSessionId != null) {
getIt.get<AppState>().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<SplashPage> {
// GetIt.instance<CacheService>().remove(key: CacheConst.isAppOpenedFromCall);
Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall);
Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation())));
Navigator.pushReplacementNamed(
Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!).pushReplacement(FadePage(page: LandingNavigation()));
Navigator.pushNamed(
// context,
GetIt.instance<NavigationService>().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!)),
);
}

Loading…
Cancel
Save