Image Functionality & Discounts & Offers

pull/319/head
Aamir Muhammad 4 weeks ago
parent 285287e596
commit 7cd1d84ff6

@ -41,10 +41,6 @@
android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"
tools:node="remove" />
<!-- Gallery/Photo permissions for Android 13+ (API 33+) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove" />
<!-- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />-->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove" />
<!-- Gallery/Photo permissions for Android 12 and below -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.9995 12C20.9995 12 20.9995 12 20.9995 12C20.9995 11.5568 20.8047 11.162 20.6411 10.8906C20.461 10.5918 20.2225 10.2892 19.9677 10.0008C19.4559 9.4216 18.7926 8.80434 18.1551 8.25084C17.5127 7.69303 16.8731 7.18002 16.396 6.80767C16.1593 6.62291 15.7406 6.30672 15.5962 6.19767L15.5925 6.19487C15.1478 5.86736 14.5218 5.96236 14.1943 6.40706C13.8668 6.85173 13.9618 7.47768 14.4064 7.80521C14.5396 7.90576 14.9359 8.20515 15.1656 8.38437C15.6259 8.74365 16.2364 9.23357 16.8439 9.76105C17.3225 10.1765 17.7853 10.6034 18.1679 11L3.99951 11C3.44723 11 2.99951 11.4477 2.99951 12C2.99951 12.5523 3.44723 13 3.99951 13L18.1679 13C17.7853 13.3966 17.3225 13.8235 16.8439 14.2389C16.2364 14.7664 15.6259 15.2564 15.1656 15.6156C14.9359 15.7948 14.5396 16.0942 14.4064 16.1948C13.9618 16.5223 13.8668 17.1483 14.1943 17.5929C14.5218 18.0376 15.1478 18.1326 15.5925 17.8051L15.5965 17.8021C15.7409 17.6931 16.1593 17.3771 16.396 17.1923C16.8731 16.82 17.5127 16.307 18.1551 15.7492C18.7926 15.1957 19.4559 14.5784 19.9677 13.9992C20.2225 13.7108 20.461 13.4082 20.6411 13.1094C20.8047 12.8381 20.9995 12.4431 20.9995 12Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.7163 5.99998C10.8032 6 10.8902 6.00001 10.9771 6.00001L10.9771 14C10.9771 14.5523 11.4351 15 12.0001 15C12.5651 15 13.0231 14.5523 13.0231 14L13.0231 6.00001C13.11 6.00001 13.1969 6 13.2837 5.99998C13.4694 5.99994 13.655 5.9999 13.8407 6.00004C14.073 6.00021 14.3505 6.00041 14.5774 5.96723C14.8128 5.93278 15.3669 5.81137 15.6344 5.24525C15.9005 4.68223 15.641 4.19357 15.5138 3.99322C15.3933 3.80322 15.2141 3.59781 15.0652 3.42709C15.0546 3.41493 15.0442 3.40295 15.0339 3.39116C14.481 2.75629 13.6973 1.89011 13.1169 1.41106C12.8307 1.17478 12.47 1.01767 12.0681 1.0014C11.6161 0.983094 11.2039 1.14606 10.883 1.41077C10.3023 1.88992 9.51848 2.75645 8.96575 3.39141C8.9555 3.40319 8.94507 3.41515 8.93448 3.4273C8.78565 3.59799 8.60654 3.80342 8.48601 3.99344C8.35889 4.19385 8.0995 4.68246 8.3656 5.24535C8.63317 5.81135 9.18709 5.93277 9.42259 5.96723C9.6494 6.00041 9.92688 6.00021 10.1591 6.00004C10.3449 5.9999 10.5306 5.99994 10.7163 5.99998Z" fill="white"/>
<path d="M6.20302 7.97913C6.7438 7.86701 7.0913 7.33772 6.97918 6.79694C6.86705 6.25615 6.33777 5.90866 5.79698 6.02078C4.89779 6.20722 4.12408 6.54004 3.48187 7.15175C2.67092 7.92418 2.31966 8.90199 2.15626 10.0596C1.99994 11.167 1.99997 12.5725 2 14.3041V14.4558C1.99997 16.1873 1.99994 17.5928 2.15626 18.7003C2.31966 19.8579 2.67092 20.8357 3.48187 21.6081C4.28597 22.374 5.29233 22.6998 6.48389 22.8524C7.63674 23 9.1038 23 10.9298 23H13.0702C14.8962 23 16.3633 23 17.5161 22.8524C18.7077 22.6998 19.714 22.374 20.5181 21.6081C21.3291 20.8357 21.6803 19.8579 21.8437 18.7003C22.0001 17.5928 22 16.1873 22 14.4558V14.3041C22 12.5725 22.0001 11.167 21.8437 10.0596C21.6803 8.90199 21.3291 7.92418 20.5181 7.15175C19.8759 6.54004 19.1022 6.20722 18.203 6.02078C17.6622 5.90866 17.133 6.25615 17.0208 6.79694C16.9087 7.33772 17.2562 7.86701 17.797 7.97913C18.4468 8.11386 18.8417 8.31705 19.1387 8.59994C19.4993 8.94343 19.7339 9.42162 19.8634 10.3391C19.9977 11.2905 20 12.5542 20 14.3799C20 16.2056 19.9977 17.4694 19.8634 18.4208C19.7339 19.3383 19.4993 19.8164 19.1387 20.1599C18.7713 20.51 18.249 20.7422 17.2621 20.8686C16.2511 20.998 14.9126 21 13 21H11C9.08742 21 7.74887 20.998 6.73794 20.8686C5.75099 20.7422 5.22875 20.51 4.86128 20.1599C4.50066 19.8164 4.26614 19.3383 4.13663 18.4208C4.00234 17.4694 4 16.2056 4 14.3799C4 12.5542 4.00234 11.2905 4.13663 10.3391C4.26614 9.42162 4.50066 8.94343 4.86128 8.59994C5.15828 8.31705 5.55322 8.11386 6.20302 7.97913Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1,3 @@
<svg width="21" height="22" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.447715 0.447715 0 1 0L1.93845 0C3.31505 0 4.515 0.936891 4.84887 2.27239L4.85349 2.29084L7.31185 14H7.38C7.39152 14 7.40299 14.0002 7.41441 14.0006L13.7632 14.0006C14.7838 14.0006 15.515 13.8399 16.0562 13.5888C16.5861 13.3429 16.9837 12.9875 17.3096 12.5118C18.0068 11.4943 18.3556 9.98374 18.7807 7.8723C18.9056 7.25167 18.9778 6.88274 18.9961 6.61741C19.002 6.53142 18.983 6.52996 18.9117 6.52452L18.884 6.52225C18.7781 6.51262 18.6477 6.5068 18.4816 6.50374C17.9294 6.49357 17.49 6.03769 17.5002 5.4855C17.5103 4.93331 17.9662 4.49391 18.5184 4.50408C18.8759 4.51066 19.258 4.52934 19.6007 4.61974C19.9829 4.72056 20.3604 4.92167 20.641 5.30777C20.9858 5.78217 21.0219 6.31071 20.9914 6.75472C20.963 7.16653 20.8624 7.66613 20.753 8.20908L20.7414 8.26701C20.3387 10.2673 19.9324 12.2223 18.9595 13.6422C18.4504 14.3853 17.7842 14.9917 16.8981 15.4029C16.0234 15.8088 14.9865 16.0006 13.7632 16.0006L6.92903 16.0006C6.34562 16.0177 5.81269 16.4121 5.59895 17L16 17L15.9984 17.0026C15.9982 17.003 15.9979 17.0035 15.9976 17.0039C15.9984 17.0039 15.9992 17.0039 16 17.0039C17.2426 17.0039 18.25 18.0113 18.25 19.2539C18.25 20.4965 17.2426 21.5039 16 21.5039C14.7574 21.5039 13.75 20.4965 13.75 19.2539C13.75 19.1681 13.7548 19.0833 13.7642 19H13.764H11.2358C11.2452 19.0833 11.25 19.1681 11.25 19.2539C11.25 20.4965 10.2426 21.5039 9 21.5039C7.75736 21.5039 6.75 20.4965 6.75 19.2539C6.75 19.1681 6.75481 19.0833 6.76417 19H4.91143C4.09384 19 3.5 18.3215 3.5 17.5714C3.5 16.2195 4.24057 15.0121 5.35392 14.4081L2.90464 2.74218C2.78811 2.30517 2.39205 2 1.93845 2L1 2C0.447715 2 0 1.55228 0 1ZM12.5 1.5C12.5 0.947715 12.0523 0.5 11.5 0.5C10.9477 0.5 10.5 0.947715 10.5 1.5L10.5 4.5L7.5 4.5C6.94772 4.5 6.5 4.94772 6.5 5.5C6.5 6.05228 6.94772 6.5 7.5 6.5L10.5 6.5V9.5C10.5 10.0523 10.9477 10.5 11.5 10.5C12.0523 10.5 12.5 10.0523 12.5 9.5V6.5L15.5 6.5C16.0523 6.5 16.5 6.05228 16.5 5.5C16.5 4.94772 16.0523 4.5 15.5 4.5L12.5 4.5L12.5 1.5ZM8.25 19.2539C8.25 18.8397 8.58579 18.5039 9 18.5039C9.41421 18.5039 9.75 18.8397 9.75 19.2539C9.75 19.6681 9.41421 20.0039 9 20.0039C8.58579 20.0039 8.25 19.6681 8.25 19.2539ZM16 18.5039C15.5858 18.5039 15.25 18.8397 15.25 19.2539C15.25 19.6681 15.5858 20.0039 16 20.0039C16.4142 20.0039 16.75 19.6681 16.75 19.2539C16.75 18.8397 16.4142 18.5039 16 18.5039Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -557,7 +557,7 @@
"remeberthat": "Remember that",
"loginToUseService": "You need to login to use this service",
"offersAndPromotions": "OFFERS & SPECIAL PROMOTIONS",
"offers": "OFFERS",
"offers": "Offers",
"myPrescriptions": "MY PRESCRIPTIONS",
"searchAndScanMedication": "SEARCH & SCAN FOR MEDICATION",
"shopByBrands": "Shop by Brands",

@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts {
static const maxSmallScreen = 660;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.preProd;
// static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT
@ -680,6 +680,11 @@ var GET_PATIENT_SICK_LEAVE_STATUS = 'Services/Patients.svc/REST/GetPatientSickLe
var GET_SERVICES_PRICE_LIST = 'Services/OUTPs.svc/REST/GetServicesPriceList';
// Offers and Discounts
//TODO: Need to Be Changes Once Apis Provided By Vendor ---- Aamir
var GET_OFFERS_AND_DISCOUNTS = 'Services/Patients.svc/REST/GetOffersAndDiscounts';
var GET_OFFERS_AND_DISCOUNTS_HISTORY = 'Services/Patients.svc/REST/GetOffersAndDiscountsHistory';
var SendSickLeaveEmail = 'Services/Notifications.svc/REST/SendSickLeaveEmail';
var GET_PATIENT_AdVANCE_BALANCE_AMOUNT = 'Services/Patients.svc/REST/GetPatientAdvanceBalanceAmount';

@ -236,6 +236,9 @@ class AppAssets {
static const String h_calc_selected = '$svgBasePath/h_calc_selected.svg';
static const String weatherBottom = '$svgBasePath/weather_bottom.svg';
static const String weatherBottomFill = '$svgBasePath/weather_bottom_fill.svg';
static const String shoppingCart = '$svgBasePath/shoppingcart.svg';
static const String share = '$svgBasePath/share.svg';
static const String nextSwiper = '$svgBasePath/arrow-right-02.svg';
static const String height = '$svgBasePath/height.svg';
static const String weight = '$svgBasePath/weight.svg';

@ -46,10 +46,13 @@ import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_repo.dart';
import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart';
import 'package:hmg_patient_app_new/features/notifications/notifications_repo.dart';
import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_repo.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart';
import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart';
import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_repo.dart';
import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_picture/profile_picture_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_repo.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart';
@ -77,6 +80,7 @@ import 'package:hmg_patient_app_new/services/localauth_service.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/services/notification_service.dart';
import 'package:hmg_patient_app_new/services/permission_service.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart';
import 'package:local_auth/local_auth.dart';
@ -149,6 +153,8 @@ class AppDependencies {
() => LocalAuthService(loggerService: getIt<LoggerService>(), localAuth: getIt<LocalAuthentication>()),
);
getIt.registerLazySingleton<PermissionService>(() => PermissionService());
// Repositories
getIt.registerLazySingleton<CommonRepo>(() => CommonRepoImp(loggerService: getIt()));
getIt.registerLazySingleton<AuthenticationRepo>(() => AuthenticationRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
@ -188,6 +194,7 @@ class AppDependencies {
getIt.registerLazySingleton<AskDoctorRepo>(() => AskDoctorRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<ServicesPriceListRepo>(() => ServicesPriceListRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<ProfileSettingsRepo>(() => ProfileSettingsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<OffersAndDiscountsRepo>(() => OffersAndDiscountsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
// ViewModels
// Global/shared VMs LazySingleton
@ -246,6 +253,11 @@ class AppDependencies {
errorHandlerService: getIt<ErrorHandlerService>(),
));
getIt.registerLazySingleton<ProfilePictureViewModel>(() => ProfilePictureViewModel(
appState: getIt<AppState>(),
profileSettingsViewModel: getIt<ProfileSettingsViewModel>(),
));
getIt.registerLazySingleton<DateRangeSelectorRangeViewModel>(() => DateRangeSelectorRangeViewModel());
getIt.registerLazySingleton<DoctorFilterViewModel>(() => DoctorFilterViewModel());
@ -326,6 +338,9 @@ class AppDependencies {
getIt.registerLazySingleton<AskDoctorViewModel>(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<ServicesPriceListViewModel>(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<OffersAndDiscountsViewModel>(() => OffersAndDiscountsViewModel(offersAndDiscountsRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<DateRangCalenderModel>(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()));
}
}

@ -805,8 +805,13 @@ class MedicalFileViewModel extends ChangeNotifier {
}
if (updated) {
// Create new list instances to trigger Selector rebuild
patientFamilyFiles = List.from(patientFamilyFiles);
pendingFamilyFiles = List.from(pendingFamilyFiles);
// Notify listeners to update UI
notifyListeners();
print("🔄 Created new list instances and notified listeners");
} else {
print("⚠️ Family member not found in cache for patientID: $patientID");
}

@ -324,7 +324,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
// if (patientArrivedAppointmentsHistoryList.isNotEmpty) {
isPatientHasQueueAppointment = false;
notifyListeners();
if(patientArrivedAppointmentsHistoryList.isNotEmpty) {
if (patientArrivedAppointmentsHistoryList.isNotEmpty) {
if (Utils.isDateToday(DateUtil.convertStringToDate(patientArrivedAppointmentsHistoryList.first.appointmentDate))) {
// getPatientAppointmentQueueDetails(appointmentNo: patientArrivedAppointmentsHistoryList.first.appointmentNo, patientID: patientArrivedAppointmentsHistoryList.first.patientID);
getPatientAppointmentQueueDetails();
@ -943,7 +943,8 @@ class MyAppointmentsViewModel extends ChangeNotifier {
final result = await myAppointmentsRepo.getPatientAppointmentQueueDetails(
clinicID: patientArrivedAppointmentsHistoryList.first.clinicID,
appointmentNo: patientArrivedAppointmentsHistoryList.first.appointmentNo, patientID: patientArrivedAppointmentsHistoryList.first.patientID);
appointmentNo: patientArrivedAppointmentsHistoryList.first.appointmentNo,
patientID: patientArrivedAppointmentsHistoryList.first.patientID);
isAppointmentQueueDetailsLoading = false;
@ -1198,7 +1199,8 @@ class MyAppointmentsViewModel extends ChangeNotifier {
switch (reminderType) {
case ReminderType.appointment:
eventTitle = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}";
eventTitle =
"Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}";
eventDescription = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} in ${patientAppointmentHistoryResponseModel.projectName}";
break;
case ReminderType.payment:
@ -1431,7 +1433,8 @@ class MyAppointmentsViewModel extends ChangeNotifier {
switch (reminderType) {
case ReminderType.appointment:
eventTitle = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}";
eventTitle =
"Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}";
eventDescription = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} in ${patientAppointmentHistoryResponseModel.projectName}";
break;
case ReminderType.payment:
@ -1502,7 +1505,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
required BuildContext context,
required bool shouldCreateReminder,
required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel,
required ReminderType reminderType ,
required ReminderType reminderType,
Function(bool)? onSuccess,
Function(String)? onError,
}) async {

@ -0,0 +1,52 @@
class OffersAndDiscountsResponseModel {
int? id;
String? title;
String? description;
String? imageUrl;
String? facilityType; // HMC, HMG, or Both
String? startDate;
String? endDate;
String? discount;
bool? isActive;
OffersAndDiscountsResponseModel({
this.id,
this.title,
this.description,
this.imageUrl,
this.facilityType,
this.startDate,
this.endDate,
this.discount,
this.isActive,
});
factory OffersAndDiscountsResponseModel.fromJson(Map<String, dynamic> json) {
return OffersAndDiscountsResponseModel(
id: json['ID'] as int?,
title: json['Title'] as String?,
description: json['Description'] as String?,
imageUrl: json['ImageURL'] as String?,
facilityType: json['FacilityType'] as String?,
startDate: json['StartDate'] as String?,
endDate: json['EndDate'] as String?,
discount: json['Discount'] as String?,
isActive: json['IsActive'] as bool?,
);
}
Map<String, dynamic> toJson() {
return {
'ID': id,
'Title': title,
'Description': description,
'ImageURL': imageUrl,
'FacilityType': facilityType,
'StartDate': startDate,
'EndDate': endDate,
'Discount': discount,
'IsActive': isActive,
};
}
}

@ -0,0 +1,102 @@
import 'package:dartz/dartz.dart';
import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class OffersAndDiscountsRepo {
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscounts();
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscountsHistory();
}
class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
final ApiClient apiClient;
final LoggerService loggerService;
OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient});
@override
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscounts() async {
Map<String, dynamic> mapDevice = {};
try {
GenericApiModel<List<OffersAndDiscountsResponseModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_OFFERS_AND_DISCOUNTS, // TODO: Replace with actual API endpoint
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final list = response['List_OffersAndDiscounts'];
if (list == null || list.isEmpty) {
throw Exception("offers and discounts list is empty");
}
final offers = list.map((item) => OffersAndDiscountsResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<OffersAndDiscountsResponseModel>();
apiResponse = GenericApiModel<List<OffersAndDiscountsResponseModel>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: offers,
);
} catch (e) {
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscountsHistory() async {
Map<String, dynamic> mapDevice = {};
try {
GenericApiModel<List<OffersAndDiscountsResponseModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_OFFERS_AND_DISCOUNTS_HISTORY, // TODO: Replace with actual API endpoint
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final list = response['List_OffersAndDiscountsHistory'];
if (list == null || list.isEmpty) {
throw Exception("offers and discounts history list is empty");
}
final historyOffers = list.map((item) => OffersAndDiscountsResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<OffersAndDiscountsResponseModel>();
apiResponse = GenericApiModel<List<OffersAndDiscountsResponseModel>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: historyOffers,
);
} catch (e) {
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
}

@ -0,0 +1,449 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
class OffersAndDiscountsViewModel extends ChangeNotifier {
bool isOffersLoading = false;
bool isHistoryLoading = false;
List<String> selectedFacilities = ['All Offers']; // Default to 'All Offers'
String searchQuery = '';
OffersAndDiscountsRepo offersAndDiscountsRepo;
ErrorHandlerService errorHandlerService;
List<OffersAndDiscountsResponseModel> offersList = [];
List<OffersAndDiscountsResponseModel> historyList = [];
OffersAndDiscountsViewModel({
required this.offersAndDiscountsRepo,
required this.errorHandlerService,
}) {
_initializeDummyData();
}
// Initialize with dummy data for testing
void _initializeDummyData() {
offersList = [
OffersAndDiscountsResponseModel(
id: 1,
title: 'Summer Health Checkup Package',
description: 'Complete health screening with blood tests, X-ray, and consultation. Get 30% off on all diagnostic services.',
imageUrl: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=800',
facilityType: 'Female',
startDate: '2026-05-01',
endDate: '2026-08-31',
discount: '30% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 2,
title: 'Maternity Care Special',
description: 'Comprehensive prenatal care package including ultrasound, consultations, and postnatal support.',
imageUrl: 'https://images.unsplash.com/photo-1584820927498-cfe5211fd8bf?w=800',
facilityType: 'OB-Gyne',
startDate: '2026-05-01',
endDate: '2026-12-31',
discount: '25% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 3,
title: 'Skin Treatment Package',
description: 'Advanced dermatology services including acne treatment, anti-aging procedures, and skin rejuvenation.',
imageUrl: 'https://images.unsplash.com/photo-1570172619644-dfd03ed5d881?w=800',
facilityType: 'Dermatology',
startDate: '2026-05-15',
endDate: '2026-07-15',
discount: '40% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 4,
title: 'CT & MRI Scanning Discount',
description: 'State-of-the-art imaging services with latest technology. Book your scan today!',
imageUrl: 'https://images.unsplash.com/photo-1516549655169-df83a0774514?w=800',
facilityType: 'Radiology',
startDate: '2026-05-01',
endDate: '2026-06-30',
discount: '20% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 5,
title: 'Women\'s Health Screening',
description: 'Comprehensive health package designed specifically for women including mammography, bone density scan, and hormone tests.',
imageUrl: 'https://images.unsplash.com/photo-1559757175-5700dde675bc?w=800',
facilityType: 'Female',
startDate: '2026-05-10',
endDate: '2026-09-30',
discount: '35% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 6,
title: 'Gynecology Consultation Special',
description: 'Free follow-up consultation with every initial gynecology visit. Expert care for all women\'s health needs.',
imageUrl: 'https://images.unsplash.com/photo-1631217868264-e5b90bb7e133?w=800',
facilityType: 'OB-Gyne',
startDate: '2026-05-01',
endDate: '2026-07-31',
discount: 'FREE Follow-up',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 7,
title: 'Laser Hair Removal Package',
description: 'Professional laser hair removal treatment with latest technology. Multiple sessions available.',
imageUrl: 'https://images.unsplash.com/photo-1612349317150-e413f6a5b16d?w=800',
facilityType: 'Dermatology',
startDate: '2026-05-20',
endDate: '2026-10-20',
discount: '50% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 8,
title: 'X-Ray & Ultrasound Combo',
description: 'Get both X-ray and ultrasound services at a discounted price. Quick results guaranteed.',
imageUrl: 'https://images.unsplash.com/photo-1530497610245-94d3c16cda28?w=800',
facilityType: 'Radiology',
startDate: '2026-05-01',
endDate: '2026-08-15',
discount: '15% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 9,
title: 'Complete Wellness Package',
description: 'Full body checkup including all major tests, consultations, and health assessment report.',
imageUrl: 'https://images.unsplash.com/photo-1505751172876-fa1923c5c528?w=800',
facilityType: 'Both',
startDate: '2026-05-01',
endDate: '2026-12-31',
discount: '45% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 10,
title: 'Botox & Filler Treatment',
description: 'Anti-aging treatments with certified dermatologists. Natural-looking results guaranteed.',
imageUrl: 'https://images.unsplash.com/photo-1515377905703-c4788e51af15?w=800',
facilityType: 'Dermatology',
startDate: '2026-05-15',
endDate: '2026-06-15',
discount: '30% OFF',
isActive: true,
),
];
// Past offers for history
historyList = [
OffersAndDiscountsResponseModel(
id: 101,
title: 'Winter Health Campaign',
description: 'Flu shots and winter wellness packages that were offered during the winter season.',
imageUrl: 'https://images.unsplash.com/photo-1584308666744-24d5c474f2ae?w=800',
facilityType: 'Both',
startDate: '2025-12-01',
endDate: '2026-02-28',
discount: '25% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 102,
title: 'Valentine\'s Day Couple Checkup',
description: 'Special couple health screening packages offered for Valentine\'s Day.',
imageUrl: 'https://images.unsplash.com/photo-1516549655169-df83a0774514?w=800',
facilityType: 'Both',
startDate: '2026-02-01',
endDate: '2026-02-14',
discount: '40% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 103,
title: 'Spring Skin Renewal',
description: 'Spring special dermatology treatments for skin rejuvenation.',
imageUrl: 'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=800',
facilityType: 'Dermatology',
startDate: '2026-03-01',
endDate: '2026-04-30',
discount: '30% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 104,
title: 'Summer Wellness Package - Active',
description: 'Active offer for comprehensive summer health checkup with multiple tests.',
imageUrl: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=800',
facilityType: 'Female',
startDate: '2026-05-01',
endDate: '2026-08-31',
discount: '35% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 105,
title: 'Kids Health Screening - Active',
description: 'Currently active pediatric health screening package for children.',
imageUrl: 'https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=800',
facilityType: 'Both',
startDate: '2026-05-05',
endDate: '2026-09-30',
discount: '20% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 106,
title: 'Laser Treatment Offer - Expired',
description: 'Laser hair removal package that has expired.',
imageUrl: 'https://images.unsplash.com/photo-1612349317150-e413f6a5b16d?w=800',
facilityType: 'Dermatology',
startDate: '2026-01-01',
endDate: '2026-03-31',
discount: '50% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 107,
title: 'Cardiology Consultation Package',
description: 'Heart health checkup with ECG and consultation.',
imageUrl: 'https://images.unsplash.com/photo-1628348068343-c6a848d2b6dd?w=800',
facilityType: 'Both',
startDate: '2025-11-01',
endDate: '2026-01-31',
discount: '25% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 108,
title: 'Dental Care Special - Active',
description: 'Comprehensive dental checkup and cleaning package.',
imageUrl: 'https://images.unsplash.com/photo-1606811971618-4486d14f3f99?w=800',
facilityType: 'Both',
startDate: '2026-04-01',
endDate: '2026-12-31',
discount: '30% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 109,
title: 'Eye Care Package - Expired',
description: 'Complete eye examination with vision testing.',
imageUrl: 'https://images.unsplash.com/photo-1516534775068-ba3e7458af70?w=800',
facilityType: 'Both',
startDate: '2025-10-01',
endDate: '2025-12-31',
discount: '20% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 110,
title: 'Maternity Care Premium',
description: 'Premium prenatal and postnatal care package.',
imageUrl: 'https://images.unsplash.com/photo-1584820927498-cfe5211fd8bf?w=800',
facilityType: 'OB-Gyne',
startDate: '2026-01-15',
endDate: '2026-06-30',
discount: '40% OFF',
isActive: true,
),
OffersAndDiscountsResponseModel(
id: 111,
title: 'Blood Test Special - Expired',
description: 'Comprehensive blood work panel at discounted rates.',
imageUrl: 'https://images.unsplash.com/photo-1579154204601-01588f351e67?w=800',
facilityType: 'Both',
startDate: '2025-09-01',
endDate: '2025-11-30',
discount: '15% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 112,
title: 'Vaccination Drive',
description: 'Special vaccination package for adults and children.',
imageUrl: 'https://images.unsplash.com/photo-1587854692152-cbe660dbde88?w=800',
facilityType: 'Both',
startDate: '2026-03-01',
endDate: '2026-05-05',
discount: '10% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 113,
title: 'Radiology Imaging Package - Active',
description: 'CT, MRI, and X-ray services at reduced prices.',
imageUrl: 'https://images.unsplash.com/photo-1516549655169-df83a0774514?w=800',
facilityType: 'Radiology',
startDate: '2026-05-01',
endDate: '2026-10-31',
discount: '25% OFF',
isActive: false, // Changed to false - will show as "Availed"
),
OffersAndDiscountsResponseModel(
id: 114,
title: 'Physiotherapy Sessions',
description: 'Package of 10 physiotherapy sessions.',
imageUrl: 'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=800',
facilityType: 'Both',
startDate: '2025-12-01',
endDate: '2026-04-01',
discount: '35% OFF',
isActive: false,
),
OffersAndDiscountsResponseModel(
id: 115,
title: 'Nutrition Consultation Bundle - Active',
description: 'Series of nutrition and diet consultation sessions.',
imageUrl: 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?w=800',
facilityType: 'Both',
startDate: '2026-05-01',
endDate: '2026-11-30',
discount: '20% OFF',
isActive: true,
),
];
}
// For loading state compatibility with existing code
bool get isRegionListLoading => isOffersLoading;
initOffersAndDiscounts() {
// Don't clear the list - keep dummy data until API returns
// Don't show loading state since we have dummy data
// offersList.clear();
// isOffersLoading = true;
notifyListeners();
getOffersAndDiscounts();
}
setIsOffersLoading(bool val) {
isOffersLoading = val;
notifyListeners();
}
setIsHistoryLoading(bool val) {
isHistoryLoading = val;
notifyListeners();
}
setSelectedFacility(List<String> facilities) {
selectedFacilities = facilities;
notifyListeners();
}
setSearchQuery(String query) {
searchQuery = query;
notifyListeners();
}
Future<void> getOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await offersAndDiscountsRepo.getOffersAndDiscounts();
result.fold(
(failure) async {
isOffersLoading = false;
notifyListeners();
// Keep dummy data if API fails - don't clear the list
// await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(response) {
isOffersLoading = false;
if (response.data != null && response.data!.isNotEmpty) {
offersList = response.data!;
}
// If API returns empty or null, keep the dummy data
notifyListeners();
if (onSuccess != null) {
onSuccess(response);
}
},
);
}
Future<void> getOffersAndDiscountsHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async {
isHistoryLoading = true;
notifyListeners();
final result = await offersAndDiscountsRepo.getOffersAndDiscountsHistory();
result.fold(
(failure) async {
isHistoryLoading = false;
notifyListeners();
// Keep dummy data if API fails - don't clear the list
// await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(response) {
isHistoryLoading = false;
if (response.data != null && response.data!.isNotEmpty) {
historyList = response.data!;
}
// If API returns empty or null, keep the dummy data
notifyListeners();
if (onSuccess != null) {
onSuccess(response);
}
},
);
}
List<OffersAndDiscountsResponseModel> get filteredOffers {
List<OffersAndDiscountsResponseModel> filtered = offersList;
// Filter by facility types (multi-select support)
if (!selectedFacilities.contains('All Offers')) {
filtered = filtered.where((offer) {
// Check if offer's facility type matches any of the selected types
return selectedFacilities.any((selectedType) =>
offer.facilityType == selectedType || offer.facilityType == 'Both'
);
}).toList();
}
// Filter by search query
if (searchQuery.isNotEmpty) {
filtered = filtered.where((offer) {
final titleMatch = offer.title?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
final descriptionMatch = offer.description?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
return titleMatch || descriptionMatch;
}).toList();
}
return filtered;
}
List<OffersAndDiscountsResponseModel> get filteredHistory {
List<OffersAndDiscountsResponseModel> filtered = historyList;
// Filter by facility types (multi-select support)
if (!selectedFacilities.contains('All Offers')) {
filtered = filtered.where((offer) {
// Check if offer's facility type matches any of the selected types
return selectedFacilities.any((selectedType) =>
offer.facilityType == selectedType || offer.facilityType == 'Both'
);
}).toList();
}
// Filter by search query
if (searchQuery.isNotEmpty) {
filtered = filtered.where((offer) {
final titleMatch = offer.title?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
final descriptionMatch = offer.description?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
return titleMatch || descriptionMatch;
}).toList();
}
return filtered;
}
}

@ -381,14 +381,20 @@ class ProfileSettingsViewModel extends ChangeNotifier {
(response) {
isUploadingProfileImage = false;
profileImageData = imageData;
// Store in AppState for global access
// Store in AppState for global access FIRST
GetIt.instance<AppState>().setProfileImageData = imageData;
// Update the family files cache with the new profile image
// Update the family files cache with the new profile image (after AppState update)
try {
final medicalFileViewModel = GetIt.instance.get<MedicalFileViewModel>();
medicalFileViewModel.updateFamilyMemberProfileImage(patientID, imageData);
print("✅ Updated profile image in family files cache for patient: $patientID");
// Only update if family files are loaded
if (medicalFileViewModel.patientFamilyFiles.isNotEmpty ||
medicalFileViewModel.pendingFamilyFiles.isNotEmpty) {
medicalFileViewModel.updateFamilyMemberProfileImage(patientID, imageData);
} else {
print(" Family files not loaded yet, skipping cache update");
}
} catch (e) {
print("⚠️ Could not update family files cache: $e");
}

@ -34,8 +34,10 @@ import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_reg
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart';
import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart';
import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_picture/profile_picture_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_view_model.dart';
import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart';
@ -168,6 +170,9 @@ void main() async {
ChangeNotifierProvider<ProfileSettingsViewModel>(
create: (_) => getIt.get<ProfileSettingsViewModel>(),
),
ChangeNotifierProvider<ProfilePictureViewModel>(
create: (_) => getIt.get<ProfilePictureViewModel>(),
),
ChangeNotifierProvider<MyAppointmentsViewModel>(
create: (_) => getIt.get<MyAppointmentsViewModel>(),
),
@ -257,7 +262,10 @@ void main() async {
),
ChangeNotifierProvider<ServicesPriceListViewModel>(
create: (_) => getIt.get<ServicesPriceListViewModel>(),
) ,
),
ChangeNotifierProvider<OffersAndDiscountsViewModel>(
create: (_) => getIt.get<OffersAndDiscountsViewModel>(),
),
ChangeNotifierProvider<WeatherMonitorViewModel>(
create: (_) => getIt.get<WeatherMonitorViewModel>(),
), ChangeNotifierProvider<DateRangCalenderModel>(

@ -29,6 +29,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_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/appointments/appointment_queue_page.dart';
@ -49,6 +50,8 @@ import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_upd
import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart';
import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart';
import 'package:hmg_patient_app_new/presentation/notifications/notifications_list_page.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_page.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/widgets/offers_and_discounts.dart';
import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointment_doctor.dart';
import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_procedures_details_page.dart';
import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart';
@ -147,11 +150,14 @@ class _LandingPageState extends State<LandingPage> {
notificationsViewModel.initNotificationsViewModel();
insuranceViewModel.initInsuranceProvider();
if(!appState.isRatedVisible) {
appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) {
if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) {
appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!,
onSuccess: ((response) {
if (!appState.isRatedVisible) {
appointmentRatingViewModel.getLastRatingAppointment(
onSuccess: (response) {
if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) {
appointmentRatingViewModel.getAppointmentDetails(
appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!,
appointmentRatingViewModel.appointmentRatedList.last.projectID!,
onSuccess: ((response) {
appointmentRatingViewModel.setClinicOrDoctor(false);
appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr(context: context));
appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context));
@ -210,10 +216,7 @@ class _LandingPageState extends State<LandingPage> {
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(
top: (appState.isAuthenticated &&
!insuranceVM.isInsuranceLoading &&
insuranceVM.isInsuranceExpired &&
insuranceVM.isInsuranceExpiryBannerShown)
top: (appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown)
? (MediaQuery.paddingOf(context).top + 70.h)
: kToolbarHeight + 0.h,
bottom: 24),
@ -237,11 +240,20 @@ class _LandingPageState extends State<LandingPage> {
);
},
name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'),
imageWidget: UserAvatarWidget(
width: 42.w,
height: 42.h,
fit: BoxFit.cover,
isCircular: true,
imageWidget: Selector<ProfileSettingsViewModel, String?>(
selector: (_, profileVM) => profileVM.profileImageData ?? appState.getProfileImageData,
shouldRebuild: (previous, next) => previous != next,
builder: (context, profileImageData, child) {
// Only rebuild when profile image data changes
return UserAvatarWidget(
key: ValueKey('landing_avatar_${profileImageData?.hashCode ?? 0}'),
width: 42.w,
height: 42.h,
fit: BoxFit.cover,
isCircular: true,
customProfileImageData: profileImageData,
);
},
),
).expanded
: CustomButton(
@ -333,12 +345,8 @@ class _LandingPageState extends State<LandingPage> {
// }),
!appState.isAuthenticated
? Row(children: [
SizedBox(
width: 24.w,
),
Utils.buildSvgWithAssets(
icon: appState.isArabic() ? AppAssets.enLangIcon : AppAssets.arLangIcon, height: 24.h, width: 24.h)
.onPress(() {
SizedBox(width: 24.w),
Utils.buildSvgWithAssets(icon: appState.isArabic() ? AppAssets.enLangIcon : AppAssets.arLangIcon, height: 24.h, width: 24.h).onPress(() {
context.setLocale(appState.isArabic() ? Locale('en', 'US') : Locale('ar', 'SA'));
})
])
@ -393,6 +401,36 @@ class _LandingPageState extends State<LandingPage> {
),
).paddingSymmetrical(24.w, 0.h)
: SizedBox.shrink(),
// Offers And Discounts Carousel - Auto-scrolling from right to left
// appState.isAuthenticated
// ?
// Column(
// children: [
// SizedBox(height: 12.h),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// ("${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}").toText16(isBold: true),
// Row(
// children: [
// LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true),
// SizedBox(width: 2.h),
// Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h),
// ],
// ),
// ],
// ).paddingSymmetrical(24.h, 0.h).onPress(() {
// Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsPage()));
// }),
// SizedBox(height: 16.h),
// OffersAndDiscountsCarousel().paddingSymmetrical(24.h, 0.h),
// SizedBox(height: 18.h),
// ],
// )
// : SizedBox.shrink(),
// ,
appState.isAuthenticated
? Column(
children: [
@ -482,9 +520,7 @@ class _LandingPageState extends State<LandingPage> {
),
)
: SizedBox(
height: 255.h +
20 +
30, // itemHeight + shadow padding (10 top + 10 bottom) + pagination dots space
height: 255.h + 20 + 30, // itemHeight + shadow padding (10 top + 10 bottom) + pagination dots space
child: Builder(
builder: (context) {
final int swiperItemCount = myAppointmentsVM.isMyAppointmentsLoading
@ -529,8 +565,7 @@ class _LandingPageState extends State<LandingPage> {
? _buildLiveCareRequestCard().paddingSymmetrical(24.h, 16.h)
: Container(
width: double.infinity,
decoration: RoundedRectangleBorder()
.toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
@ -630,9 +665,7 @@ class _LandingPageState extends State<LandingPage> {
LocaleKeys.quickLinks.tr(context: context).toText16(isBold: true),
Row(
children: [
LocaleKeys.viewMedicalFileLandingPage
.tr(context: context)
.toText14(color: AppColors.primaryRedColor, isBold: true),
LocaleKeys.viewMedicalFileLandingPage.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true),
SizedBox(width: 2.h),
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h),
],
@ -660,10 +693,7 @@ class _LandingPageState extends State<LandingPage> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.pendingAncillaryOrders
.tr(context: context)
.toText14(color: AppColors.eReferralCardColor, isBold: true)
.paddingSymmetrical(24.h, 0.h),
LocaleKeys.pendingAncillaryOrders.tr(context: context).toText14(color: AppColors.eReferralCardColor, isBold: true).paddingSymmetrical(24.h, 0.h),
CustomButton(
text: LocaleKeys.view.tr(context: context),
onPressed: () {
@ -694,10 +724,7 @@ class _LandingPageState extends State<LandingPage> {
trackColor: Color(0xffD9D9D9),
trackBorderColor: Colors.transparent,
trackRadius: Radius.circular(10.0),
padding: EdgeInsets.only(
top: 92.h + 32.h,
left: MediaQuery.sizeOf(context).width / 2.5 - 10,
right: MediaQuery.sizeOf(context).width / 2.5 - 10),
padding: EdgeInsets.only(top: 92.h + 32.h, left: MediaQuery.sizeOf(context).width / 2.5 - 10, right: MediaQuery.sizeOf(context).width / 2.5 - 10),
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: LandingPageData().getLoggedInServiceCardsList.length,
@ -751,10 +778,7 @@ class _LandingPageState extends State<LandingPage> {
trackColor: Color(0xffD9D9D9),
trackBorderColor: Colors.transparent,
trackRadius: Radius.circular(10.0),
padding: EdgeInsets.only(
top: 92.h + 32.h,
left: MediaQuery.sizeOf(context).width / 2.5 - 10,
right: MediaQuery.sizeOf(context).width / 2.5 - 10),
padding: EdgeInsets.only(top: 92.h + 32.h, left: MediaQuery.sizeOf(context).width / 2.5 - 10, right: MediaQuery.sizeOf(context).width / 2.5 - 10),
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: LandingPageData.getNotLoggedInServiceCardsList.length,
@ -877,10 +901,7 @@ class _LandingPageState extends State<LandingPage> {
),
),
),
(appState.isAuthenticated &&
!insuranceVM.isInsuranceLoading &&
insuranceVM.isInsuranceExpired &&
insuranceVM.isInsuranceExpiryBannerShown)
(appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown)
? Container(
height: MediaQuery.paddingOf(context).top + 50.h,
decoration: ShapeDecoration(
@ -897,24 +918,17 @@ class _LandingPageState extends State<LandingPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.insuranceExpiredOrInactive
.tr(context: context)
.toText14(color: AppColors.primaryRedColor, isBold: true)
.paddingSymmetrical(0.h, 0.h),
LocaleKeys.insuranceExpiredOrInactive.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true).paddingSymmetrical(0.h, 0.h),
Row(
children: [
CustomButton(
text: LocaleKeys.updateInsurance.tr(context: context),
onPressed: () {
insuranceVM.setIsInsuranceUpdateDetailsLoading(true);
insuranceVM.getPatientInsuranceDetailsForUpdate(appState.getAuthenticatedUser()!.patientId.toString(),
appState.getAuthenticatedUser()!.patientIdentificationNo.toString());
insuranceVM.getPatientInsuranceDetailsForUpdate(
appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString());
showCommonBottomSheetWithoutHeight(context,
child: PatientInsuranceCardUpdateCard(),
callBackFunc: () {},
title: "",
isCloseButtonVisible: false,
isFullScreen: false);
child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false);
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.secondaryLightRedBorderColor,
@ -1064,8 +1078,7 @@ class _LandingPageState extends State<LandingPage> {
SizedBox(height: 6.h),
_buildServingNowSection(),
SizedBox(height: 5.h),
_buildQueueActionButton(currentStatus, currentQueue.roomNo ?? "")
.toShimmer2(isShow: myAppointmentsViewModel.patientQueueDetailsList.isEmpty),
_buildQueueActionButton(currentStatus, currentQueue.roomNo ?? "").toShimmer2(isShow: myAppointmentsViewModel.patientQueueDetailsList.isEmpty),
],
),
),
@ -1094,8 +1107,7 @@ class _LandingPageState extends State<LandingPage> {
hasShadow: false,
),
padding: EdgeInsets.all(6.h),
child: Lottie.asset(AppAnimations.hourGlass,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 40.h, height: 40.h, fit: BoxFit.fill)),
child: Lottie.asset(AppAnimations.hourGlass, repeat: true, reverse: false, frameRate: FrameRate(60), width: 40.h, height: 40.h, fit: BoxFit.fill)),
],
);
}
@ -1274,8 +1286,7 @@ class _LandingPageState extends State<LandingPage> {
hasShadow: false,
),
padding: EdgeInsets.all(6.h),
child: Lottie.asset(AppAnimations.hourGlass,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 40.h, height: 40.h, fit: BoxFit.fill)),
child: Lottie.asset(AppAnimations.hourGlass, repeat: true, reverse: false, frameRate: FrameRate(60), width: 40.h, height: 40.h, fit: BoxFit.fill)),
// Utils.buildSvgWithAssets(
// icon: AppAssets.waiting_icon,
// width: 24.h,
@ -1323,8 +1334,7 @@ class _LandingPageState extends State<LandingPage> {
// Appointment Card Wrapper (reusable)
Widget _buildAppointmentCardWrapper(appointment) {
return Container(
decoration:
RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true, hasDenseShadow: true),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true, hasDenseShadow: true),
child: AppointmentCard(
patientAppointmentHistoryResponseModel: appointment,
myAppointmentsViewModel: myAppointmentsViewModel,

@ -30,6 +30,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart';
import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
@ -218,11 +219,19 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UserAvatarWidget(
width: 56.h,
height: 56.h,
fit: BoxFit.cover,
isCircular: true,
Selector<ProfileSettingsViewModel, String?>(
selector: (_, profileVM) => profileVM.profileImageData ?? appState.getProfileImageData,
shouldRebuild: (previous, next) => previous != next,
builder: (context, profileImageData, child) {
return UserAvatarWidget(
key: ValueKey('medical_avatar_${profileImageData?.hashCode ?? 0}'),
width: 56.h,
height: 56.h,
fit: BoxFit.cover,
isCircular: true,
customProfileImageData: profileImageData,
);
},
),
SizedBox(width: 8.w),
Column(

@ -92,7 +92,10 @@ class _FamilyMedicalScreenState extends State<FamilyMedicalScreen> {
Selector<MedicalFileViewModel, ({int selectedIndex, List<FamilyFileResponseModelLists> patientFiles, List<FamilyFileResponseModelLists> pendingFiles})>(
selector: (_, model) => (selectedIndex: model.getSelectedFamilyFileTabIndex, patientFiles: model.patientFamilyFiles, pendingFiles: model.pendingFamilyFiles),
shouldRebuild: (previous, next) {
return previous.selectedIndex != next.selectedIndex || previous.patientFiles.length != next.patientFiles.length || previous.pendingFiles.length != next.pendingFiles.length;
// Only rebuild if something actually changed
return previous.selectedIndex != next.selectedIndex ||
!identical(previous.patientFiles, next.patientFiles) ||
!identical(previous.pendingFiles, next.pendingFiles);
},
builder: (context, data, child) => getFamilyTabs(index: data.selectedIndex, patientFiles: data.patientFiles, pendingFiles: data.pendingFiles),
),

@ -1,6 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -16,11 +14,13 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart';
import 'package:hmg_patient_app_new/features/profile_picture/profile_picture_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/services/permission_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
@ -29,7 +29,6 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/expandable_list_widget.dart';
import 'package:hmg_patient_app_new/widgets/user_avatar_widget.dart';
import 'package:hmg_patient_app_new/widgets/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
class FamilyCards extends StatefulWidget {
@ -63,10 +62,9 @@ class FamilyCards extends StatefulWidget {
class _FamilyCardsState extends State<FamilyCards> {
AppState appState = getIt<AppState>();
final PermissionService _permissionService = getIt<PermissionService>();
late InsuranceViewModel insuranceViewModel;
late ProfileSettingsViewModel profileSettingsViewModel;
File? _selectedImage;
bool _isUploadingImage = false;
@override
void initState() {
@ -77,318 +75,27 @@ class _FamilyCardsState extends State<FamilyCards> {
}
void _pickImage() {
// Show image picker options without checking permissions first
ImageOptions.showImageOptionsNew(
context,
false, // Don't show files option, only camera and gallery
(base64String, file) async {
try {
print('=== Starting image processing ===');
print('File path: ${file.path}');
print('File exists: ${await file.exists()}');
print('Original file size: ${await file.length() / 1024} KB');
// Compress and resize the image
print('Calling compressAndResizeImage...');
final compressedFile = await ImageCompressionHelper.compressAndResizeImage(file);
File finalFile;
String finalBase64;
if (compressedFile == null) {
print('⚠️ Compression failed - using original file as fallback');
// Fallback: use original image if compression fails
final originalSize = await file.length();
final maxSize = 1048576; // 1MB
if (originalSize > maxSize) {
print('❌ Original file is too large: ${originalSize / 1024} KB');
if (mounted) {
Utils.showToast(
LocaleKeys.imageSizeTooLarge.tr(context: context),
);
}
return;
}
print('✅ Using original file (${originalSize / 1024} KB)');
finalFile = file;
var bytes = await file.readAsBytes();
finalBase64 = base64.encode(bytes);
} else {
// Check compressed file size
final fileSize = await compressedFile.length();
final maxSize = 1048576; // 1MB
print('✅ Compression successful: ${fileSize / 1024} KB');
if (fileSize > maxSize) {
print('❌ Compressed file still too large');
if (mounted) {
Utils.showToast(
LocaleKeys.imageSizeTooLarge.tr(context: context),
);
}
return;
}
finalFile = compressedFile;
var bytes = await compressedFile.readAsBytes();
finalBase64 = base64.encode(bytes);
}
print('Converting to base64... Length: ${finalBase64.length}');
if (mounted) {
setState(() {
_selectedImage = finalFile;
});
print('📤 Starting upload...');
// Upload the image
_uploadImage(finalBase64);
}
print('=== Image processing complete ===');
} catch (e, stackTrace) {
print('❌ Error in _pickImage: $e');
print('Stack trace: $stackTrace');
if (mounted) {
Utils.showToast(
LocaleKeys.failedToProcessImage.tr(context: context),
);
}
}
},
checkCameraPermission: _checkCameraPermission,
checkGalleryPermission: _checkGalleryPermission,
);
}
Future<bool> _checkCameraPermission() async {
try {
print('=== Checking camera permission ===');
final profilePictureViewModel = context.read<ProfilePictureViewModel>();
// First check current status
PermissionStatus currentStatus = await Permission.camera.status;
print('Current camera permission status: $currentStatus');
// If already granted, return true
if (currentStatus.isGranted) {
print('✅ Camera permission already granted');
return true;
}
// If denied or permanently denied, show settings dialog
if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) {
// Request permission first
PermissionStatus newStatus = await Permission.camera.request();
print('Camera permission after request: $newStatus');
if (newStatus.isGranted) {
print('✅ Camera permission granted');
return true;
}
// Still denied - show settings dialog
print('⚠️ Camera permission denied - showing settings dialog');
profilePictureViewModel.pickImage(
context,
showImagePicker: ImageOptions.showImageOptionsNew,
compressImage: ImageCompressionHelper.compressAndResizeImage,
onSuccess: (data) {
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
}
// Request permission for the first time
PermissionStatus newStatus = await Permission.camera.request();
print('Camera permission after request: $newStatus');
if (newStatus.isGranted) {
print('✅ Camera permission granted');
return true;
}
// Denied - show settings dialog
print('❌ Camera permission denied - showing settings dialog');
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
} catch (e) {
print('❌ Error checking camera permission: $e');
if (mounted) {
Utils.showToast(
LocaleKeys.failedToCheckPermissions.tr(context: context),
);
}
return false;
}
}
Future<bool> _checkGalleryPermission() async {
try {
print('=== Checking gallery permission ===');
// Determine which permission to check based on platform and Android version
Permission galleryPermission;
if (Platform.isIOS) {
galleryPermission = Permission.photos;
} else {
// Android: use photos permission which handles API level differences automatically
galleryPermission = Permission.photos;
}
// First check current status
PermissionStatus currentStatus = await galleryPermission.status;
print('Current gallery permission status: $currentStatus');
// If already granted, return true
if (currentStatus.isGranted || currentStatus.isLimited) {
print('✅ Gallery permission already granted');
return true;
}
// If denied or permanently denied, request permission first
if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) {
// Request permission first
PermissionStatus newStatus = await galleryPermission.request();
print('Gallery permission after request: $newStatus');
if (newStatus.isGranted || newStatus.isLimited) {
print('✅ Gallery permission granted');
return true;
Utils.showToast(LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context));
}
// Still denied - show settings dialog
print('⚠️ Gallery permission denied - showing settings dialog');
},
onError: (error) {
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
}
// Request permission for the first time
PermissionStatus newStatus = await galleryPermission.request();
print('Gallery permission after request: $newStatus');
if (newStatus.isGranted || newStatus.isLimited) {
print('✅ Gallery permission granted');
return true;
}
// Denied - show settings dialog
print('❌ Gallery permission denied - showing settings dialog');
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
} catch (e) {
print('❌ Error checking gallery permission: $e');
if (mounted) {
Utils.showToast(
LocaleKeys.failedToCheckPermissions.tr(context: context),
);
}
return false;
}
}
void _uploadImage(String base64String) {
final patientID = appState.getAuthenticatedUser()?.patientId;
if (patientID != null) {
setState(() {
_isUploadingImage = true;
});
profileSettingsViewModel.uploadProfileImage(
patientID: patientID,
imageData: base64String,
onSuccess: (data) {
if (mounted) {
setState(() {
_selectedImage = null; // Clear selected image after successful upload
_isUploadingImage = false;
});
Utils.showToast(
LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context),
);
}
},
onError: (error) {
if (mounted) {
setState(() {
_isUploadingImage = false;
});
}
Utils.showToast(error);
},
);
}
}
},
imageSizeTooLargeMessage: LocaleKeys.imageSizeTooLarge.tr(context: context),
failedToProcessImageMessage: LocaleKeys.failedToProcessImage.tr(context: context),
checkCameraPermission: (ctx) => _permissionService.checkCameraPermission(ctx),
checkGalleryPermission: (ctx) => _permissionService.checkGalleryPermission(ctx),
);
}
double _calculateAspectRatio(BuildContext context) {
@ -553,40 +260,44 @@ class _FamilyCardsState extends State<FamilyCards> {
Positioned(
right: 0,
bottom: 0,
child: GestureDetector(
onTap: () {
if (!_isUploadingImage) {
_pickImage();
}
},
child: Container(
width: 20.w,
height: 20.h,
decoration: BoxDecoration(
color: AppColors.primaryRedColor,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.whiteColor,
width: 1.5.w,
),
),
child: _isUploadingImage
? SizedBox(
width: 10.w,
height: 10.h,
child: CircularProgressIndicator(
strokeWidth: 1.5.w,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.whiteColor,
),
).paddingAll(4.w),
)
: Icon(
Icons.camera_alt,
child: Consumer<ProfileSettingsViewModel>(
builder: (context, profileVm, _) {
return GestureDetector(
onTap: () {
if (!profileVm.isUploadingProfileImage) {
_pickImage();
}
},
child: Container(
width: 20.w,
height: 20.h,
decoration: BoxDecoration(
color: AppColors.primaryRedColor,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.whiteColor,
size: 10.w,
width: 1.5.w,
),
),
),
child: profileVm.isUploadingProfileImage
? SizedBox(
width: 10.w,
height: 10.h,
child: CircularProgressIndicator(
strokeWidth: 1.5.w,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.whiteColor,
),
).paddingAll(4.w),
)
: Icon(
Icons.camera_alt,
color: AppColors.whiteColor,
size: 10.w,
),
),
);
},
),
),
],

@ -0,0 +1,262 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:share_plus/share_plus.dart';
class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget {
final List<String>? images;
final int initialIndex;
const OfferAndDiscountsFullScreenSwiperPage({
super.key,
this.images,
this.initialIndex = 0,
});
@override
State<OfferAndDiscountsFullScreenSwiperPage> createState() => _OfferAndDiscountsFullScreenSwiperPageState();
}
class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscountsFullScreenSwiperPage> {
late PageController _pageController;
late int _currentPage;
late List<String> _promoImages;
@override
void initState() {
super.initState();
// Use provided images or default demo images
_promoImages = widget.images ??
[
'assets/images/offersanddiscounts/promo.jpg',
'assets/images/offersanddiscounts/promo.jpg',
'assets/images/offersanddiscounts/promo.jpg',
'assets/images/offersanddiscounts/promo.jpg',
];
_currentPage = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFADA6D0),
body: SafeArea(
child: Stack(
children: [
// Main content area
Column(
children: [
SizedBox(height: 26.h),
// Page indicators
_buildPageIndicators(),
SizedBox(height: 70.h),
// Swipeable image section
Expanded(
child: PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
itemCount: _promoImages.length,
itemBuilder: (context, index) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 0.h),
child: Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: Image.asset(
_promoImages[index],
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16.r),
),
child: Center(
child: Icon(
Icons.image_outlined,
size: 100.h,
color: Colors.white.withValues(alpha: 0.5),
),
),
);
},
),
),
),
),
);
},
),
),
SizedBox(height: 70.h),
// Bottom action buttons
_buildBottomActions(),
SizedBox(height: 12.h),
],
),
// Close button (top right)
Positioned(
top: 26.h + 14.h, // After indicator spacing
right: 24.w,
child: _buildCloseButton()),
],
),
),
);
}
Widget _buildPageIndicators() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Row(
children: List.generate(
_promoImages.length,
(index) => Expanded(
child: Container(
margin: EdgeInsets.only(
right: index < _promoImages.length - 1 ? 4.w : 0,
),
height: 4.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2.r),
// Fill bar if it's current page or any previous page
color: index <= _currentPage ? Colors.white : const Color(0x33000000), // 20% opacity
),
),
),
),
),
);
}
Widget _buildCloseButton() {
return GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
width: 32.w,
height: 32.h,
decoration: BoxDecoration(
color: const Color(0x33FFFFFF), // 20% opacity
borderRadius: BorderRadius.circular(8.r),
),
child: Center(
child: Icon(
Icons.close,
color: AppColors.textColor,
size: 20.h,
),
),
),
);
}
Widget _buildBottomActions() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Row(
children: [
// Buy Now button (takes remaining space)
Expanded(
child: CustomButton(
text: 'Buy Now',
icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor,
onPressed: () {
// Handle buy now action
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
isBold: true,
borderRadius: 12.r,
height: 56.h,
),
),
SizedBox(width: 12.w),
// Next button
_buildIconButton(
svgIcon: AppAssets.nextSwiper,
onTap: () {
if (_currentPage < _promoImages.length - 1) {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
),
SizedBox(width: 12.w),
_buildIconButton(
svgIcon: AppAssets.share,
onTap: () async {
await Share.share('Check out this amazing offer!');
}),
],
),
);
}
Widget _buildIconButton({
IconData? icon,
String? svgIcon,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 56.w,
height: 56.h,
decoration: BoxDecoration(
color: const Color(0x57303957), // 34% opacity approximation
borderRadius: BorderRadius.circular(12.r),
),
child: Center(
child: svgIcon != null
? Utils.buildSvgWithAssets(
icon: svgIcon,
iconColor: AppColors.whiteColor,
width: 24.h,
height: 24.h,
)
: Icon(
icon!,
color: AppColors.whiteColor,
size: 24.h,
),
),
),
);
}
}

@ -0,0 +1,223 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
class OffersAndDiscountsDetailedPage extends StatelessWidget {
final OffersAndDiscountsResponseModel offer;
const OffersAndDiscountsDetailedPage({
super.key,
required this.offer,
});
String _formatDateString(String? dateString) {
if (dateString == null || dateString.isEmpty) return '';
try {
DateTime date = DateTime.parse(dateString);
return DateFormat('d MMM, yyyy').format(date);
} catch (e) {
return dateString;
}
}
// Determine the status of the offer based on dates and isActive flag
String _getOfferStatus(String? endDate, bool? isActive) {
if (endDate == null || endDate.isEmpty) return 'Expired';
try {
DateTime end = DateTime.parse(endDate);
DateTime now = DateTime.now();
if (now.isAfter(end)) {
return 'Expired';
} else {
if (isActive == true) {
return 'Active';
} else {
return 'Availed';
}
}
} catch (e) {
return 'Expired';
}
}
Color _getStatusBgColor(String status) {
switch (status) {
case 'Active':
return AppColors.successColor.withValues(alpha: 0.1);
case 'Availed':
return AppColors.infoColor.withValues(alpha: 0.1);
case 'Expired':
return AppColors.errorColor.withValues(alpha: 0.1);
default:
return AppColors.greyColor;
}
}
Color _getStatusTextColor(String status) {
switch (status) {
case 'Active':
return AppColors.successColor;
case 'Availed':
return AppColors.infoColor;
case 'Expired':
return AppColors.errorColor;
default:
return AppColors.textColor;
}
}
@override
Widget build(BuildContext context) {
final status = _getOfferStatus(offer.endDate, offer.isActive);
return CollapsingListView(
title: "${LocaleKeys.offers.tr(context: context)} ${LocaleKeys.details.tr(context: context)}",
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Offer Image
if (offer.imageUrl != null)
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(24.h),
child: Image.network(
offer.imageUrl!,
width: double.infinity,
height: 250.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 250.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.greyColor,
borderRadius: 24.h,
),
child: Center(
child: Icon(Icons.image_not_supported, size: 64.h),
),
);
},
),
),
// Discount badge at top right on image
// if (offer.discount != null)
// Positioned(
// top: 16.h,
// right: 16.w,
// child: Container(
// padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
// decoration: BoxDecoration(
// color: AppColors.primaryRedColor,
// borderRadius: BorderRadius.circular(8.r),
// ),
// child: Text(
// offer.discount!,
// style: TextStyle(
// fontSize: 14.f,
// fontWeight: FontWeight.w700,
// color: AppColors.whiteColor,
// ),
// ),
// ),
// ),
],
),
SizedBox(height: 24.h),
// Title
if (offer.title != null)
offer.title!.toText24(
isBold: true,
fontWeight: FontWeight.w700,
),
SizedBox(height: 16.h),
// Chips Row (Valid Till and Status)
Wrap(
spacing: 12.w,
runSpacing: 12.h,
children: [
// Valid till chip
if (offer.endDate != null)
AppCustomChipWidget(
labelText: "Valid till ${_formatDateString(offer.endDate)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.calendar,
iconSize: 14.h,
iconColor: AppColors.textColor,
isEnglishOnly: true,
),
// Status chip
AppCustomChipWidget(
labelText: status,
backgroundColor: _getStatusBgColor(status),
textColor: _getStatusTextColor(status),
),
// Facility type chip
if (offer.facilityType != null)
AppCustomChipWidget(
labelText: offer.facilityType!,
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
),
],
),
SizedBox(height: 32.h),
// Description Heading
LocaleKeys.description.tr(context: context).toText18(
isBold: true,
weight: FontWeight.w700,
),
SizedBox(height: 12.h),
// Description Content
if (offer.description != null)
offer.description!.toText16(
color: AppColors.greyTextColor,
height: 1.5,
),
SizedBox(height: 32.h),
// Buy Now Button
CustomButton(
text: "Buy Now",
icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor,
onPressed: () {
// Handle buy now action
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
isBold: true,
borderRadius: 12.r,
height: 56.h,
),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.h, 0.h),
),
);
}
}

@ -0,0 +1,292 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
class OffersAndDiscountsHistoryPage extends StatefulWidget {
const OffersAndDiscountsHistoryPage({super.key});
@override
State<OffersAndDiscountsHistoryPage> createState() => _OffersAndDiscountsHistoryPageState();
}
class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistoryPage> {
late OffersAndDiscountsViewModel offersAndDiscountsViewModel;
late AppState appState;
@override
void initState() {
scheduleMicrotask(() {
offersAndDiscountsViewModel.getOffersAndDiscountsHistory();
});
super.initState();
}
String _formatDateString(String? dateString) {
if (dateString == null || dateString.isEmpty) return '';
try {
DateTime date = DateTime.parse(dateString);
return DateFormat('d MMM, yyyy').format(date);
} catch (e) {
return dateString;
}
}
// Determine the status of the offer based on dates and isActive flag
String _getOfferStatus(String? endDate, bool? isActive) {
if (endDate == null || endDate.isEmpty) return 'Expired';
try {
DateTime end = DateTime.parse(endDate);
DateTime now = DateTime.now();
if (now.isAfter(end)) {
return 'Expired';
} else {
// If end date is in future, check isActive flag
if (isActive == true) {
return 'Active';
} else {
return 'Availed';
}
}
} catch (e) {
return 'Expired';
}
}
Color _getStatusBgColor(String status) {
switch (status) {
case 'Active':
return AppColors.successColor.withValues(alpha: 0.1);
case 'Availed':
return AppColors.infoColor.withValues(alpha: 0.1);
case 'Expired':
return AppColors.errorColor.withValues(alpha: 0.1);
default:
return AppColors.greyColor;
}
}
Color _getStatusTextColor(String status) {
switch (status) {
case 'Active':
return AppColors.successColor;
case 'Availed':
return AppColors.infoColor;
case 'Expired':
return AppColors.errorColor;
default:
return AppColors.textColor;
}
}
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
return CollapsingListView(
title: '${LocaleKeys.order.tr(context: context)} ${LocaleKeys.history.tr(context: context)}',
child: SingleChildScrollView(
child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
offersAndDiscountVM.isHistoryLoading
? Container(
height: 200.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
child: Center(child: Utils.getLoadingWidget()),
).paddingSymmetrical(24.h, 0.h)
: ListView.separated(
padding: EdgeInsets.only(top: 12.h),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: offersAndDiscountVM.filteredHistory.isNotEmpty ? offersAndDiscountVM.filteredHistory.length : 1,
itemBuilder: (context, index) {
if (offersAndDiscountVM.filteredHistory.isEmpty) {
return Utils.getNoDataWidget(context, noDataText: LocaleKeys.noDataAvailable.tr(context: context));
}
final offer = offersAndDiscountVM.filteredHistory[index];
final status = _getOfferStatus(offer.endDate, offer.isActive);
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
child: Stack(
children: [
Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title with space for status chip
Padding(
padding: EdgeInsets.only(right: 80.w),
child: offer.title?.toText18(
isBold: true,
weight: FontWeight.w700,
) ??
SizedBox(),
),
SizedBox(height: 12.h),
// Info chips
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: [
// Start Date chip
if (offer.startDate != null)
AppCustomChipWidget(
labelText: _formatDateString(offer.startDate),
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.calendar,
iconSize: 12.h,
iconColor: AppColors.textColor,
isEnglishOnly: true,
),
// Hospital/Facility chip
if (offer.facilityType != null)
AppCustomChipWidget(
labelText: offer.facilityType!,
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
),
// Purchase date chip (using startDate as purchased date)
if (offer.startDate != null)
AppCustomChipWidget(
labelText: "Purchased ${_formatDateString(offer.startDate)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
isEnglishOnly: true,
),
// Valid until chip
if (offer.endDate != null)
AppCustomChipWidget(
labelText: "Valid till ${_formatDateString(offer.endDate)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.calendar,
iconSize: 12.h,
iconColor: AppColors.textColor,
isEnglishOnly: true,
),
// Order ID chip (using offer.id)
if (offer.id != null)
AppCustomChipWidget(
labelText: "ID: #${offer.id}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
isEnglishOnly: true,
),
],
),
SizedBox(height: 16.h),
// Action buttons
Row(
children: [
Expanded(
child: CustomButton(
text: 'View Offer Details',
onPressed: () {
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountsDetailedPage(
offer: offersAndDiscountVM.filteredOffers[index],
),
),
);
},
backgroundColor: AppColors.errorColor.withValues(alpha: 0.1),
borderColor: Colors.transparent,
textColor: AppColors.errorColor,
fontSize: 12.f,
isBold: true,
borderRadius: 12.r,
height: 40.h,
),
),
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
text: 'View Order Details',
onPressed: () {
// Handle view order details
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.textColor,
textColor: AppColors.textColor,
fontSize: 12.f,
isBold: true,
borderRadius: 12.r,
height: 40.h,
),
),
],
),
],
),
),
// Status chip at top right
Positioned(
top: 16.h,
right: 16.w,
child: AppCustomChipWidget(
labelText: status,
backgroundColor: _getStatusBgColor(status),
textColor: _getStatusTextColor(status),
),
),
],
),
).paddingSymmetrical(24.h, 0.h),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
),
SizedBox(height: 24.h),
],
);
}),
),
);
}
}

@ -0,0 +1,279 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_history_page.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.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/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import '../../widgets/input_widget.dart';
class OffersAndDiscountsPage extends StatefulWidget {
const OffersAndDiscountsPage({super.key});
@override
State<OffersAndDiscountsPage> createState() => _OffersAndDiscountsPageState();
}
class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
late OffersAndDiscountsViewModel offersAndDiscountsViewModel;
late AppState appState;
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
scheduleMicrotask(() {
offersAndDiscountsViewModel.initOffersAndDiscounts();
});
super.initState();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
return CollapsingListView(
title: "${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}",
history: () {
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountsHistoryPage(),
),
);
},
child: SingleChildScrollView(
child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Search Input
TextInputWidget(
labelText: LocaleKeys.search.tr(context: context),
hintText: LocaleKeys.search.tr(context: context),
controller: _searchController,
onChange: (value) {
offersAndDiscountVM.setSearchQuery(value!);
},
isEnable: true,
prefix: null,
autoFocus: false,
isBorderAllowed: false,
keyboardType: TextInputType.text,
isAllowLeadingIcon: true,
selectionType: SelectionTypeEnum.search,
padding: EdgeInsets.symmetric(
vertical: ResponsiveExtension(10).h,
horizontal: ResponsiveExtension(15).h,
),
),
SizedBox(height: 16.h),
OffersAndDiscountTypeSelectionWidget(
selectedOffers: offersAndDiscountVM.selectedFacilities,
onOfferClicked: (selectedValues) {
offersAndDiscountVM.setSelectedFacility(selectedValues);
},
),
SizedBox(height: 16.h),
// Offers Grid
offersAndDiscountVM.isOffersLoading
? GridView.builder(
padding: EdgeInsets.symmetric(horizontal: 24.w),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.w,
mainAxisSpacing: 16.h,
childAspectRatio: 0.55, // Adjust this to control card height
),
itemCount: 6,
// Show 6 loading placeholders
itemBuilder: (context, index) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
child: Center(child: Utils.getLoadingWidget()),
);
},
)
: offersAndDiscountVM.filteredOffers.isNotEmpty
? GridView.builder(
padding: EdgeInsets.symmetric(horizontal: 0.w),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, crossAxisSpacing: 16.w, mainAxisSpacing: 16.h, childAspectRatio: 0.55),
itemCount: offersAndDiscountVM.filteredOffers.length,
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredGrid(
position: index,
duration: const Duration(milliseconds: 500),
columnCount: 2,
child: ScaleAnimation(
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (offersAndDiscountVM.filteredOffers[index].imageUrl != null)
Expanded(
flex: 3,
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(24.h)),
child: SizedBox.expand(
child: Image.network(
offersAndDiscountVM.filteredOffers[index].imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: AppColors.greyColor,
child: Center(
child: Icon(Icons.image_not_supported, size: 32.h),
),
);
},
),
),
).onPress(() {
Navigator.of(context).push(
CustomPageRoute(
page: OffersAndDiscountsDetailedPage(
offer: offersAndDiscountVM.filteredOffers[index],
),
),
);
}),
// Discount badge at top right
// if (offersAndDiscountVM.filteredOffers[index].discount != null)
// Positioned(
// top: 8.h,
// right: 8.w,
// child: Container(
// padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
// decoration: BoxDecoration(
// color: AppColors.primaryRedColor,
// borderRadius: BorderRadius.circular(6.r),
// ),
// child: Text(
// offersAndDiscountVM.filteredOffers[index].discount!,
// style: TextStyle(
// fontSize: 10.f,
// fontWeight: FontWeight.w700,
// color: AppColors.whiteColor,
// ),
// ),
// ),
// ),
],
),
),
Expanded(
flex: 3,
child: Padding(
padding: EdgeInsets.all(12.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (offersAndDiscountVM.filteredOffers[index].title != null)
Text(
offersAndDiscountVM.filteredOffers[index].title!,
style: TextStyle(
fontSize: 14.f,
fontWeight: FontWeight.w700,
color: AppColors.textColor,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8.h),
// Valid till chip
if (offersAndDiscountVM.filteredOffers[index].endDate != null)
AppCustomChipWidget(
labelText: "Valid till ${Utils.formatDateToDisplay(offersAndDiscountVM.filteredOffers[index].endDate ?? "")}",
// labelText: "Valid till ${_formatDateString(offersAndDiscountVM.filteredOffers[index].endDate)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.textColor,
icon: AppAssets.calendar,
iconSize: 12.h,
iconColor: AppColors.textColor,
isEnglishOnly: true,
labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 8.w),
),
SizedBox(height: 20.h),
CustomButton(
text: 'Buy Now',
icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor,
onPressed: () {
// Handle buy now action
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 14.f,
isBold: true,
borderRadius: 12.r,
height: 40.h,
),
],
),
),
),
],
),
),
),
),
);
},
)
: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Utils.getNoDataWidget(context, noDataText: LocaleKeys.noDataAvailable.tr(context: context)),
),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.h, 0.h);
}),
),
);
}
}

@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
class OffersAndDiscountTypeSelectionWidget extends StatelessWidget {
final List<String> selectedOffers;
final Function(List<String>) onOfferClicked;
const OffersAndDiscountTypeSelectionWidget({
super.key,
required this.selectedOffers,
required this.onOfferClicked,
});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildOfferTypeCard(
context: context,
title: "All Offers",
facilityType: 'All Offers',
isSelected: selectedOffers.contains('All Offers'),
),
SizedBox(width: 12.w),
_buildOfferTypeCard(
context: context,
title: 'Female',
facilityType: 'Female',
isSelected: selectedOffers.contains('Female'),
),
SizedBox(width: 12.w),
_buildOfferTypeCard(
context: context,
title: 'OB-Gyne',
facilityType: 'OB-Gyne',
isSelected: selectedOffers.contains('OB-Gyne'),
),
SizedBox(width: 12.w),
_buildOfferTypeCard(
context: context,
title: 'Dermatology',
facilityType: 'Dermatology',
isSelected: selectedOffers.contains('Dermatology'),
),
SizedBox(width: 12.w),
_buildOfferTypeCard(
context: context,
title: 'Radiology',
facilityType: 'Radiology',
isSelected: selectedOffers.contains('Radiology'),
),
],
),
);
}
Widget _buildOfferTypeCard({
required BuildContext context,
required String title,
required String facilityType,
required bool isSelected,
}) {
return AnimatedContainer(
duration: Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: isSelected ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderRadius: 12.r,
hasShadow: true,
side: isSelected ? BorderSide(color: AppColors.primaryRedColor, width: 2) : BorderSide(color: AppColors.borderGrayColor, width: 1),
),
child: Center(
child: title.toText14(
color: isSelected ? AppColors.primaryRedColor : AppColors.textColor,
weight: isSelected ? FontWeight.w700 : FontWeight.w500,
isBold: isSelected,
),
),
).onPress(() {
List<String> updatedSelection = List.from(selectedOffers);
if (facilityType == 'All Offers') {
if (updatedSelection.contains('All Offers')) {
updatedSelection.clear();
} else {
updatedSelection.clear();
updatedSelection.add('All Offers');
}
} else {
// Remove "All Offers" if any specific type is selected
updatedSelection.remove('All Offers');
// Toggle the clicked facility type
if (updatedSelection.contains(facilityType)) {
updatedSelection.remove(facilityType);
// If no selection left, default to "All Offers"
if (updatedSelection.isEmpty) {
updatedSelection.add('All Offers');
}
} else {
updatedSelection.add(facilityType);
}
}
onOfferClicked(updatedSelection);
});
}
}

@ -0,0 +1,135 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:smooth_corner/smooth_corner.dart';
class OffersAndDiscountsCarousel extends StatefulWidget {
const OffersAndDiscountsCarousel({super.key});
@override
State<OffersAndDiscountsCarousel> createState() => _OffersAndDiscountsCarouselState();
}
class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel> {
final ScrollController _scrollController = ScrollController();
Timer? _autoScrollTimer;
// List of offer images - you can add more images here
final List<String> offerImages = [
'assets/images/offersanddiscounts/img1.png',
'assets/images/offersanddiscounts/img2.png',
'assets/images/offersanddiscounts/img3.png',
'assets/images/offersanddiscounts/img1.png',
'assets/images/offersanddiscounts/img2.png',
'assets/images/offersanddiscounts/img3.png',
'assets/images/offersanddiscounts/img1.png',
];
@override
void initState() {
super.initState();
// _startAutoScroll();
}
void _startAutoScroll() {
_autoScrollTimer = Timer.periodic(const Duration(milliseconds: 50), (timer) {
if (_scrollController.hasClients) {
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
final delta = 1.0; // Scroll speed
if (currentScroll >= maxScroll) {
// Reset to beginning for infinite scroll effect
_scrollController.jumpTo(0);
} else {
_scrollController.animateTo(
currentScroll + delta,
duration: const Duration(milliseconds: 50),
curve: Curves.linear,
);
}
}
});
}
@override
void dispose() {
_autoScrollTimer?.cancel();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 79.h,
child: ListView.separated(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 0.w),
itemCount: offerImages.length * 100,
itemBuilder: (context, index) {
final imageIndex = index % offerImages.length;
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const OfferAndDiscountsFullScreenSwiperPage()),
);
},
child: _buildOfferItem(offerImages[imageIndex]),
);
},
separatorBuilder: (context, index) => SizedBox(width: 12.w),
),
);
}
Widget _buildOfferItem(String imagePath) {
return Container(
width: 79.w,
height: 79.h,
decoration: ShapeDecoration(
shape: SmoothRectangleBorder(
borderRadius: BorderRadius.circular(13.r),
smoothness: 0.6,
side: BorderSide(
color: Color(0xff2E3039),
width: 2.w,
),
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(13.r),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: AppColors.textColor, width: 2.w),
borderRadius: BorderRadius.circular(11.r),
),
child: Padding(
padding: const EdgeInsets.all(2.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(11.r),
child: Image.asset(
imagePath,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: AppColors.greyColor,
child: Icon(
Icons.image,
color: AppColors.textColorLight,
size: 32.h,
),
);
},
),
),
),
),
),
);
}
}

@ -1,10 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
@ -12,12 +8,12 @@ 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/core/utils/image_compression_helper.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/profile_picture/profile_picture_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/permission_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/image_picker.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
class ProfilePictureWidget extends StatefulWidget {
@ -29,467 +25,63 @@ class ProfilePictureWidget extends StatefulWidget {
class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
final AppState _appState = getIt.get<AppState>();
File? _selectedImage;
int? _currentPatientId;
bool _isInitialLoadTriggered = false;
/// Cache decoded image bytes to avoid decoding base64 on every rebuild
Uint8List? _cachedImageBytes;
String? _cachedImageDataHash;
final PermissionService _permissionService = getIt.get<PermissionService>();
@override
void initState() {
super.initState();
_currentPatientId = _appState.getAuthenticatedUser()?.patientId;
// Pre-cache existing image data if available (prevents blink from default loaded)
_tryCacheExistingImage();
// Use addPostFrameCallback to ensure widget is built before loading
// Initialize view model
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _isInitialLoadTriggered) return;
_isInitialLoadTriggered = true;
final patientID = _appState.getAuthenticatedUser()?.patientId;
if (patientID == null) {
print('⚠️ No authenticated user found');
return;
}
// Load fresh data from API
print('📥 Loading fresh profile image from API for patient: $patientID');
_loadProfileImage(forceRefresh: false);
if (!mounted) return;
final profilePictureViewModel = context.read<ProfilePictureViewModel>();
profilePictureViewModel.initialize();
profilePictureViewModel.triggerInitialLoad();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Only check for user switch, NOT on initial load (initState handles that)
if (_isInitialLoadTriggered) {
_checkAndUpdateUserImage();
// Check for user switch
final profilePictureViewModel = context.read<ProfilePictureViewModel>();
if (profilePictureViewModel.isInitialLoadTriggered) {
profilePictureViewModel.checkForUserSwitch();
}
}
@override
void didUpdateWidget(ProfilePictureWidget oldWidget) {
super.didUpdateWidget(oldWidget);
_checkAndUpdateUserImage();
}
/// Pre-cache already-loaded image bytes so we don't flash default avatar
void _tryCacheExistingImage() {
final imageData = _appState.getProfileImageData;
if (imageData != null && imageData.isNotEmpty) {
try {
_cachedImageBytes = base64Decode(imageData);
_cachedImageDataHash = '${imageData.length}_${imageData.hashCode}';
} catch (_) {
_cachedImageBytes = null;
_cachedImageDataHash = null;
}
}
}
void _checkAndUpdateUserImage() {
// Check if the authenticated user has changed (family member switch)
final currentPatientId = _appState.getAuthenticatedUser()?.patientId;
if (currentPatientId != null && currentPatientId != _currentPatientId) {
print('🔄 User switched detected: $_currentPatientId -> $currentPatientId');
// Update patient ID IMMEDIATELY before any other operations
final oldPatientId = _currentPatientId;
_currentPatientId = currentPatientId;
// Clear the old profile image data from BOTH AppState and ViewModel
try {
final profileVm = context.read<ProfileSettingsViewModel>();
print('🧹 Clearing cache for old user: $oldPatientId');
// Clear AppState cache first
_appState.clearProfileImageCache();
// Then clear ViewModel cache
profileVm.clearProfileImageCache();
// Clear local decoded bytes cache
_cachedImageBytes = null;
_cachedImageDataHash = null;
// Force rebuild to show default avatar immediately
if (mounted) {
setState(() {
_selectedImage = null; // Clear any selected image
});
}
print('📥 Loading profile image for new user: $currentPatientId');
// Load the new user's profile image immediately
profileVm.getProfileImage(
patientID: currentPatientId,
forceRefresh: true,
onSuccess: (data) {
print('✅ Profile image loaded successfully for user: $currentPatientId');
if (mounted) {
_tryCacheExistingImage();
setState(() {}); // Force rebuild to show new data
}
},
onError: (error) {
print('❌ Error loading profile image: $error');
if (mounted) {
setState(() {}); // Force rebuild to show default avatar
}
},
);
} catch (e) {
print('❌ Error in _checkAndUpdateUserImage: $e');
}
}
}
void _loadProfileImage({bool forceRefresh = false}) {
// Check if profile image is already loaded in AppState (skip if forcing refresh)
if (!forceRefresh && _appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) {
// Image already loaded, no need to call API
return;
}
final profileVm = context.read<ProfileSettingsViewModel>();
final patientID = _appState.getAuthenticatedUser()?.patientId;
if (patientID != null) {
print('📥 Loading profile image for patient: $patientID (forceRefresh: $forceRefresh)');
profileVm.getProfileImage(
patientID: patientID,
forceRefresh: forceRefresh,
onSuccess: (data) {
print('✅ Profile image loaded successfully');
if (mounted) {
_tryCacheExistingImage();
setState(() {}); // Rebuild with new cached bytes
}
},
onError: (error) {
print('❌ Error loading profile image: $error');
// Error loading image
},
);
}
// Check for user switch
context.read<ProfilePictureViewModel>().checkForUserSwitch();
}
void _pickImage() {
// Show image picker options without checking permissions first
ImageOptions.showImageOptionsNew(
context,
false, // Don't show files option, only camera and gallery
(base64String, file) async {
try {
print('=== Starting image processing ===');
print('File path: ${file.path}');
print('File exists: ${await file.exists()}');
print('Original file size: ${await file.length() / 1024} KB');
// Compress and resize the image
print('Calling compressAndResizeImage...');
final compressedFile = await ImageCompressionHelper.compressAndResizeImage(file);
File finalFile;
String finalBase64;
if (compressedFile == null) {
print('⚠️ Compression failed - using original file as fallback');
final profilePictureViewModel = context.read<ProfilePictureViewModel>();
// Fallback: use original image if compression fails
final originalSize = await file.length();
final maxSize = 1048576; // 1MB
if (originalSize > maxSize) {
print('❌ Original file is too large: ${originalSize / 1024} KB');
if (mounted) {
Utils.showToast(
LocaleKeys.imageSizeTooLarge.tr(context: context),
);
}
return;
}
print('✅ Using original file (${originalSize / 1024} KB)');
finalFile = file;
var bytes = await file.readAsBytes();
finalBase64 = base64.encode(bytes);
} else {
// Check compressed file size
final fileSize = await compressedFile.length();
final maxSize = 1048576; // 1MB
print('✅ Compression successful: ${fileSize / 1024} KB');
if (fileSize > maxSize) {
print('❌ Compressed file still too large');
if (mounted) {
Utils.showToast(
LocaleKeys.imageSizeTooLarge.tr(context: context),
);
}
return;
}
finalFile = compressedFile;
var bytes = await compressedFile.readAsBytes();
finalBase64 = base64.encode(bytes);
}
print('Converting to base64... Length: ${finalBase64.length}');
if (mounted) {
setState(() {
_selectedImage = finalFile;
});
print('📤 Starting upload...');
// Upload the image
_uploadImage(finalBase64);
}
print('=== Image processing complete ===');
} catch (e, stackTrace) {
print('❌ Error in _pickImage: $e');
print('Stack trace: $stackTrace');
if (mounted) {
Utils.showToast(
LocaleKeys.failedToProcessImage.tr(context: context),
);
}
}
},
checkCameraPermission: _checkCameraPermission,
checkGalleryPermission: _checkGalleryPermission,
);
}
Future<bool> _checkCameraPermission() async {
try {
print('=== Checking camera permission ===');
// First check current status
PermissionStatus currentStatus = await Permission.camera.status;
print('Current camera permission status: $currentStatus');
// If already granted, return true
if (currentStatus.isGranted) {
print('✅ Camera permission already granted');
return true;
}
// If denied or permanently denied, show settings dialog
if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) {
// Request permission first
PermissionStatus newStatus = await Permission.camera.request();
print('Camera permission after request: $newStatus');
if (newStatus.isGranted) {
print('✅ Camera permission granted');
return true;
}
// Still denied - show settings dialog
print('⚠️ Camera permission denied - showing settings dialog');
profilePictureViewModel.pickImage(
context,
showImagePicker: ImageOptions.showImageOptionsNew,
compressImage: ImageCompressionHelper.compressAndResizeImage,
onSuccess: (data) {
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
}
// Request permission for the first time
PermissionStatus newStatus = await Permission.camera.request();
print('Camera permission after request: $newStatus');
if (newStatus.isGranted) {
print('✅ Camera permission granted');
return true;
}
// Denied - show settings dialog
print('❌ Camera permission denied - showing settings dialog');
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
} catch (e) {
print('❌ Error checking camera permission: $e');
if (mounted) {
Utils.showToast(
LocaleKeys.failedToCheckPermissions.tr(context: context),
);
}
return false;
}
}
Future<bool> _checkGalleryPermission() async {
try {
print('=== Checking gallery permission ===');
// For Android 13+ (API 33+), the Android Photo Picker handles permissions internally
// No need to request READ_MEDIA_IMAGES or READ_EXTERNAL_STORAGE permissions
if (Platform.isAndroid) {
print('✅ Android Photo Picker will handle permissions internally');
return true;
}
// iOS: Check photos permission
Permission galleryPermission = Permission.photos;
// First check current status
PermissionStatus currentStatus = await galleryPermission.status;
print('Current gallery permission status: $currentStatus');
// If already granted, return true
if (currentStatus.isGranted || currentStatus.isLimited) {
print('✅ Gallery permission already granted');
return true;
}
// If denied or permanently denied, request permission first
if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) {
// Request permission first
PermissionStatus newStatus = await galleryPermission.request();
print('Gallery permission after request: $newStatus');
if (newStatus.isGranted || newStatus.isLimited) {
print('✅ Gallery permission granted');
return true;
Utils.showToast(LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context));
}
// Still denied - show settings dialog
print('⚠️ Gallery permission denied - showing settings dialog');
},
onError: (error) {
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
}
// Request permission for the first time
PermissionStatus newStatus = await galleryPermission.request();
print('Gallery permission after request: $newStatus');
if (newStatus.isGranted || newStatus.isLimited) {
print('✅ Gallery permission granted');
return true;
}
// Denied - show settings dialog
print('❌ Gallery permission denied - showing settings dialog');
if (mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
} catch (e) {
print('❌ Error checking gallery permission: $e');
if (mounted) {
Utils.showToast(
LocaleKeys.failedToCheckPermissions.tr(context: context),
);
}
return false;
}
}
void _uploadImage(String base64String) {
final profileVm = context.read<ProfileSettingsViewModel>();
final patientID = _appState.getAuthenticatedUser()?.patientId;
if (patientID != null) {
profileVm.uploadProfileImage(
patientID: patientID,
imageData: base64String,
onSuccess: (data) {
if (mounted) {
// Update cached bytes immediately from the uploaded data
_tryCacheExistingImage();
setState(() {
_selectedImage = null; // Clear selected image after successful upload
});
print(
LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context),
);
}
},
onError: (error) {
Utils.showToast(error);
},
);
}
}
},
imageSizeTooLargeMessage: LocaleKeys.imageSizeTooLarge.tr(context: context),
failedToProcessImageMessage: LocaleKeys.failedToProcessImage.tr(context: context),
checkCameraPermission: (ctx) => _permissionService.checkCameraPermission(ctx),
checkGalleryPermission: (ctx) => _permissionService.checkGalleryPermission(ctx),
);
}
Widget _buildProfileImage(ProfileSettingsViewModel profileVm) {
Widget _buildProfileImage(ProfilePictureViewModel profilePictureViewModel, ProfileSettingsViewModel profileVm) {
// Always get fresh user data
final currentUser = _appState.getAuthenticatedUser();
final currentPatientId = currentUser?.patientId;
@ -507,10 +99,10 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
}
// Show selected image if available (only during upload)
if (_selectedImage != null) {
if (profilePictureViewModel.selectedImage != null) {
return ClipOval(
child: Image.file(
_selectedImage!,
profilePictureViewModel.selectedImage!,
width: 136.w,
height: 136.h,
fit: BoxFit.cover,
@ -518,30 +110,14 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
);
}
// Use cached decoded bytes update cache if source data changed
final String? imageData = GetIt.instance<AppState>().getProfileImageData;
final String? currentHash = (imageData != null && imageData.isNotEmpty) ? '${imageData.length}_${imageData.hashCode}' : null;
// Re-decode only if the underlying data actually changed
if (currentHash != null && currentHash != _cachedImageDataHash) {
try {
_cachedImageBytes = base64Decode(imageData!);
_cachedImageDataHash = currentHash;
} catch (e) {
print('❌ Error decoding profile image: $e');
_cachedImageBytes = null;
_cachedImageDataHash = null;
}
} else if (currentHash == null) {
_cachedImageBytes = null;
_cachedImageDataHash = null;
}
// Update cache if needed
profilePictureViewModel.updateCacheIfNeeded();
// Show cached decoded image if available
if (_cachedImageBytes != null) {
if (profilePictureViewModel.cachedImageBytes != null) {
return ClipOval(
child: Image.memory(
_cachedImageBytes!,
profilePictureViewModel.cachedImageBytes!,
key: ValueKey('profile_$currentPatientId'),
width: 136.w,
height: 136.h,
@ -564,33 +140,34 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
@override
Widget build(BuildContext context) {
// Removed addPostFrameCallback from build() it was causing redundant
// _checkAndUpdateUserImage calls on every single rebuild.
// didChangeDependencies + didUpdateWidget already handle user switches.
return Consumer<ProfileSettingsViewModel>(
builder: (context, profileVm, _) {
return Consumer2<ProfilePictureViewModel, ProfileSettingsViewModel>(
builder: (context, profilePictureViewModel, profileVm, _) {
// If we already have cached bytes, show the image even while "loading"
// to prevent the shimmerimage blink on page open
final bool showShimmer = profileVm.isProfileImageLoading && _cachedImageBytes == null && _selectedImage == null;
final bool showShimmer = profilePictureViewModel.shouldShowShimmer();
return Center(
child: Stack(
children: [
// Profile Image use AnimatedSwitcher to smooth transition
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: showShimmer
? Container(
key: const ValueKey('shimmer'),
width: 136.w,
height: 136.h,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.greyTextColor.withValues(alpha: 0.2),
),
).toShimmer2(isShow: true)
: _buildProfileImage(profileVm),
// Profile Image use ValueListenableBuilder for targeted updates
ValueListenableBuilder<int>(
valueListenable: profilePictureViewModel.profileImageVersion,
builder: (context, version, child) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: showShimmer
? Container(
key: const ValueKey('shimmer'),
width: 136.w,
height: 136.h,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.greyTextColor.withValues(alpha: 0.2),
),
).toShimmer2(isShow: true)
: _buildProfileImage(profilePictureViewModel, profileVm),
);
},
),
// Edit button

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
// import 'package:vibration/vibration.dart';
import 'package:geolocator/geolocator.dart' as geo;
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
class PermissionService {
// final LocalStorage storage = new LocalStorage("permission");
@ -73,6 +76,202 @@ class PermissionService {
openAppSettings();
}
/// Check and request camera permission with proper dialog handling
/// Returns true if permission is granted, false otherwise
Future<bool> checkCameraPermission(BuildContext context) async {
try {
print('=== Checking camera permission ===');
// First check current status
PermissionStatus currentStatus = await Permission.camera.status;
print('Current camera permission status: $currentStatus');
// If already granted, return true
if (currentStatus.isGranted) {
print('✅ Camera permission already granted');
return true;
}
// If denied or permanently denied, show settings dialog
if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) {
// Request permission first
PermissionStatus newStatus = await Permission.camera.request();
print('Camera permission after request: $newStatus');
if (newStatus.isGranted) {
print('✅ Camera permission granted');
return true;
}
// Still denied - show settings dialog
print('⚠️ Camera permission denied - showing settings dialog');
if (context.mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
}
// Request permission for the first time
PermissionStatus newStatus = await Permission.camera.request();
print('Camera permission after request: $newStatus');
if (newStatus.isGranted) {
print('✅ Camera permission granted');
return true;
}
// Denied - show settings dialog
print('❌ Camera permission denied - showing settings dialog');
if (context.mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
} catch (e) {
print('❌ Error checking camera permission: $e');
if (context.mounted) {
Utils.showToast(
LocaleKeys.failedToCheckPermissions.tr(context: context),
);
}
return false;
}
}
/// Check and request gallery/photos permission with proper dialog handling
/// Returns true if permission is granted, false otherwise
Future<bool> checkGalleryPermission(BuildContext context) async {
try {
print('=== Checking gallery permission ===');
// For Android 13+ (API 33+), the Android Photo Picker handles permissions internally
// No need to request READ_MEDIA_IMAGES or READ_EXTERNAL_STORAGE permissions
if (Platform.isAndroid) {
print('✅ Android Photo Picker will handle permissions internally');
return true;
}
// iOS: Check photos permission
Permission galleryPermission = Permission.photos;
// First check current status
PermissionStatus currentStatus = await galleryPermission.status;
print('Current gallery permission status: $currentStatus');
// If already granted, return true
if (currentStatus.isGranted || currentStatus.isLimited) {
print('✅ Gallery permission already granted');
return true;
}
// If denied or permanently denied, request permission first
if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) {
// Request permission first
PermissionStatus newStatus = await galleryPermission.request();
print('Gallery permission after request: $newStatus');
if (newStatus.isGranted || newStatus.isLimited) {
print('✅ Gallery permission granted');
return true;
}
// Still denied - show settings dialog
print('⚠️ Gallery permission denied - showing settings dialog');
if (context.mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
}
// Request permission for the first time
PermissionStatus newStatus = await galleryPermission.request();
print('Gallery permission after request: $newStatus');
if (newStatus.isGranted || newStatus.isLimited) {
print('✅ Gallery permission granted');
return true;
}
// Denied - show settings dialog
print('❌ Gallery permission denied - showing settings dialog');
if (context.mounted) {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: context),
context,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
return false;
} catch (e) {
print('❌ Error checking gallery permission: $e');
if (context.mounted) {
Utils.showToast(
LocaleKeys.failedToCheckPermissions.tr(context: context),
);
}
return false;
}
}
static isLocationEnabled() async {
var permission = await geo.Geolocator.checkPermission();
if (permission == geo.LocationPermission.denied) {

@ -78,7 +78,7 @@ class ImageOptions {
}
},
onFilesTap: () async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
FilePickerResult? result = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: [
'jpg',

@ -64,11 +64,16 @@ class UserAvatarWidget extends StatelessWidget {
if (profileImageData != null && profileImageData.isNotEmpty) {
try {
final bytes = base64Decode(profileImageData);
// Use a key based on data hash to help Flutter detect changes
final imageKey = ValueKey('avatar_${profileImageData.hashCode}');
final imageWidget = Image.memory(
bytes,
key: imageKey,
width: width,
height: height,
fit: fit ?? BoxFit.cover,
gaplessPlayback: true, // Smooth transition without flicker
);
return ClipRRect(

Loading…
Cancel
Save