diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 762c964d..0fb36b85 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -41,10 +41,6 @@
android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"
tools:node="remove" />
-
-
-
-
diff --git a/assets/images/offersanddiscounts/img1.png b/assets/images/offersanddiscounts/img1.png
new file mode 100644
index 00000000..e2ac1909
Binary files /dev/null and b/assets/images/offersanddiscounts/img1.png differ
diff --git a/assets/images/offersanddiscounts/img2.png b/assets/images/offersanddiscounts/img2.png
new file mode 100644
index 00000000..a85f4b8a
Binary files /dev/null and b/assets/images/offersanddiscounts/img2.png differ
diff --git a/assets/images/offersanddiscounts/img3.png b/assets/images/offersanddiscounts/img3.png
new file mode 100644
index 00000000..21d03820
Binary files /dev/null and b/assets/images/offersanddiscounts/img3.png differ
diff --git a/assets/images/offersanddiscounts/promo.jpg b/assets/images/offersanddiscounts/promo.jpg
new file mode 100644
index 00000000..9275c79c
Binary files /dev/null and b/assets/images/offersanddiscounts/promo.jpg differ
diff --git a/assets/images/svg/arrow-right-02.svg b/assets/images/svg/arrow-right-02.svg
new file mode 100644
index 00000000..1c5ca62a
--- /dev/null
+++ b/assets/images/svg/arrow-right-02.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/svg/share.svg b/assets/images/svg/share.svg
new file mode 100644
index 00000000..c30b9e0f
--- /dev/null
+++ b/assets/images/svg/share.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/svg/shoppingcart.svg b/assets/images/svg/shoppingcart.svg
new file mode 100644
index 00000000..d1c36860
--- /dev/null
+++ b/assets/images/svg/shoppingcart.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json
index 62cdd752..e8409260 100644
--- a/assets/langs/en-US.json
+++ b/assets/langs/en-US.json
@@ -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",
diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart
index e8e53a9a..8dbc9ce3 100644
--- a/lib/core/api_consts.dart
+++ b/lib/core/api_consts.dart
@@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts {
static const maxSmallScreen = 660;
- static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat;
+ static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.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';
diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart
index 57f91017..d6e041da 100644
--- a/lib/core/app_assets.dart
+++ b/lib/core/app_assets.dart
@@ -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';
diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart
index 5eb7a05d..9a4826b0 100644
--- a/lib/core/app_state.dart
+++ b/lib/core/app_state.dart
@@ -25,6 +25,8 @@ class AppState {
_loadProfileImageFromCache();
}
+ bool isEnabledOffersAndDiscountsCarousel = false;
+
double userLat = 0.0;
set setUserLat(v) => userLat = v;
diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart
index 0c014232..a45e9e6d 100644
--- a/lib/core/dependencies.dart
+++ b/lib/core/dependencies.dart
@@ -46,11 +46,14 @@ 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/paytabs/paytabs_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';
@@ -78,6 +81,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';
@@ -150,6 +154,8 @@ class AppDependencies {
() => LocalAuthService(loggerService: getIt(), localAuth: getIt()),
);
+ getIt.registerLazySingleton(() => PermissionService());
+
// Repositories
getIt.registerLazySingleton(() => CommonRepoImp(loggerService: getIt()));
getIt.registerLazySingleton(() => AuthenticationRepoImp(loggerService: getIt(), apiClient: getIt()));
@@ -189,6 +195,7 @@ class AppDependencies {
getIt.registerLazySingleton(() => AskDoctorRepoImp(loggerService: getIt(), apiClient: getIt()));
getIt.registerLazySingleton(() => ServicesPriceListRepoImp(loggerService: getIt(), apiClient: getIt()));
getIt.registerLazySingleton(() => ProfileSettingsRepoImp(loggerService: getIt(), apiClient: getIt()));
+ getIt.registerLazySingleton(() => OffersAndDiscountsRepoImp(loggerService: getIt(), apiClient: getIt()));
// ViewModels
// Global/shared VMs β LazySingleton
@@ -247,6 +254,11 @@ class AppDependencies {
errorHandlerService: getIt(),
));
+ getIt.registerLazySingleton(() => ProfilePictureViewModel(
+ appState: getIt(),
+ profileSettingsViewModel: getIt(),
+ ));
+
getIt.registerLazySingleton(() => DateRangeSelectorRangeViewModel());
getIt.registerLazySingleton(() => DoctorFilterViewModel());
@@ -327,6 +339,9 @@ class AppDependencies {
getIt.registerLazySingleton(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt()));
+
+ getIt.registerLazySingleton(() => OffersAndDiscountsViewModel(offersAndDiscountsRepo: getIt(), errorHandlerService: getIt()));
+
getIt.registerLazySingleton(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()));
getIt.registerLazySingleton(() => PayTabsViewModel(appState: getIt()));
diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart
index 8b7ce7c3..41a726d9 100644
--- a/lib/features/medical_file/medical_file_view_model.dart
+++ b/lib/features/medical_file/medical_file_view_model.dart
@@ -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");
}
diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart
index e2c456fb..0ad21586 100644
--- a/lib/features/my_appointments/my_appointments_view_model.dart
+++ b/lib/features/my_appointments/my_appointments_view_model.dart
@@ -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();
@@ -948,7 +948,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;
@@ -1203,7 +1204,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:
@@ -1436,7 +1438,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:
@@ -1507,7 +1510,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 {
diff --git a/lib/features/offers_and_discounts/models/offers_and_discounts_response_model.dart b/lib/features/offers_and_discounts/models/offers_and_discounts_response_model.dart
new file mode 100644
index 00000000..4a1a250f
--- /dev/null
+++ b/lib/features/offers_and_discounts/models/offers_and_discounts_response_model.dart
@@ -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 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 toJson() {
+ return {
+ 'ID': id,
+ 'Title': title,
+ 'Description': description,
+ 'ImageURL': imageUrl,
+ 'FacilityType': facilityType,
+ 'StartDate': startDate,
+ 'EndDate': endDate,
+ 'Discount': discount,
+ 'IsActive': isActive,
+ };
+ }
+}
+
diff --git a/lib/features/offers_and_discounts/offers_and_discounts_repo.dart b/lib/features/offers_and_discounts/offers_and_discounts_repo.dart
new file mode 100644
index 00000000..53f26a6f
--- /dev/null
+++ b/lib/features/offers_and_discounts/offers_and_discounts_repo.dart
@@ -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>>> getOffersAndDiscounts();
+ Future>>> getOffersAndDiscountsHistory();
+}
+
+class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
+ final ApiClient apiClient;
+ final LoggerService loggerService;
+
+ OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient});
+
+ @override
+ Future>>> getOffersAndDiscounts() async {
+ Map mapDevice = {};
+
+ try {
+ GenericApiModel>? 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)).toList().cast();
+
+ apiResponse = GenericApiModel>(
+ 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>>> getOffersAndDiscountsHistory() async {
+ Map mapDevice = {};
+
+ try {
+ GenericApiModel>? 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)).toList().cast();
+
+ apiResponse = GenericApiModel>(
+ 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()));
+ }
+ }
+}
+
diff --git a/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart b/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart
new file mode 100644
index 00000000..88e15ce6
--- /dev/null
+++ b/lib/features/offers_and_discounts/offers_and_discounts_view_model.dart
@@ -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 selectedFacilities = ['All Offers']; // Default to 'All Offers'
+ String searchQuery = '';
+
+ OffersAndDiscountsRepo offersAndDiscountsRepo;
+ ErrorHandlerService errorHandlerService;
+
+ List offersList = [];
+ List 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 facilities) {
+ selectedFacilities = facilities;
+ notifyListeners();
+ }
+
+ setSearchQuery(String query) {
+ searchQuery = query;
+ notifyListeners();
+ }
+
+ Future 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 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 get filteredOffers {
+ List 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 get filteredHistory {
+ List 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;
+ }
+}
+
diff --git a/lib/features/profile_settings/profile_settings_view_model.dart b/lib/features/profile_settings/profile_settings_view_model.dart
index 64db2481..02cbc10f 100644
--- a/lib/features/profile_settings/profile_settings_view_model.dart
+++ b/lib/features/profile_settings/profile_settings_view_model.dart
@@ -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().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.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");
}
diff --git a/lib/main.dart b/lib/main.dart
index 1ebcf0d6..e2643438 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -35,8 +35,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';
@@ -169,6 +171,9 @@ void main() async {
ChangeNotifierProvider(
create: (_) => getIt.get(),
),
+ ChangeNotifierProvider(
+ create: (_) => getIt.get(),
+ ),
ChangeNotifierProvider(
create: (_) => getIt.get(),
),
@@ -258,7 +263,10 @@ void main() async {
),
ChangeNotifierProvider(
create: (_) => getIt.get(),
- ) ,
+ ),
+ ChangeNotifierProvider(
+ create: (_) => getIt.get(),
+ ),
ChangeNotifierProvider(
create: (_) => getIt.get(),
), ChangeNotifierProvider(
diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart
index f31a04cc..15bce8b0 100644
--- a/lib/presentation/home/landing_page.dart
+++ b/lib/presentation/home/landing_page.dart
@@ -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/paytabs/paytabs_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';
@@ -50,6 +51,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';
@@ -240,11 +243,20 @@ class _LandingPageState extends State {
);
},
name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'),
- imageWidget: UserAvatarWidget(
- width: 42.w,
- height: 42.h,
- fit: BoxFit.cover,
- isCircular: true,
+ imageWidget: Selector(
+ 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(
@@ -336,9 +348,7 @@ class _LandingPageState extends State {
// }),
!appState.isAuthenticated
? Row(children: [
- SizedBox(
- width: 24.w,
- ),
+ 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'));
})
@@ -394,6 +404,34 @@ class _LandingPageState extends State {
),
).paddingSymmetrical(24.w, 0.h)
: SizedBox.shrink(),
+
+ // Offers And Discounts Carousel - Auto-scrolling from right to left
+ appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel
+ ? 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: [
diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart
index c22e261d..e281743a 100644
--- a/lib/presentation/medical_file/medical_file_page.dart
+++ b/lib/presentation/medical_file/medical_file_page.dart
@@ -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 {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- UserAvatarWidget(
- width: 56.h,
- height: 56.h,
- fit: BoxFit.cover,
- isCircular: true,
+ Selector(
+ 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(
@@ -265,9 +274,7 @@ class _MedicalFilePageState extends State {
runSpacing: 4.h,
children: [
AppCustomChipWidget(
- labelText: LocaleKeys.ageYearsOld.tr(
- namedArgs: {'age': '${appState.getAuthenticatedUser()!.age}', 'yearsOld': LocaleKeys.yearsOld.tr(context: context)},
- context: context),
+ labelText: LocaleKeys.ageYearsOld.tr(namedArgs: {'age': '${appState.getAuthenticatedUser()!.age}', 'yearsOld': LocaleKeys.yearsOld.tr(context: context)}, context: context),
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
),
AppCustomChipWidget(
@@ -310,14 +317,9 @@ class _MedicalFilePageState extends State {
onChipTap: () {
if (!insuranceVM.isInsuranceActive) {
insuranceVM.setIsInsuranceUpdateDetailsLoading(true);
- insuranceVM.getPatientInsuranceDetailsForUpdate(appState.getAuthenticatedUser()!.patientId.toString(),
- appState.getAuthenticatedUser()!.patientIdentificationNo.toString());
- showCommonBottomSheetWithoutHeight(context,
- child: PatientInsuranceCardUpdateCard(),
- callBackFunc: () {},
- title: "",
- isCloseButtonVisible: false,
- isFullScreen: false);
+ insuranceVM.getPatientInsuranceDetailsForUpdate(
+ appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString());
+ showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false);
// showCommonBottomSheetWithoutHeight(
// title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!),
// navigationService.navigatorKey.currentContext!,
@@ -491,13 +493,10 @@ class _MedicalFilePageState extends State {
getSelectedTabData(0),
],
),
- ExpandableListItem(
- title: LocaleKeys.medicalReports.tr(context: context).toText18(isBold: true),
- expandedBackgroundColor: Colors.transparent,
- children: [
- SizedBox(height: 10.h),
- getSelectedTabData(2),
- ]),
+ ExpandableListItem(title: LocaleKeys.medicalReports.tr(context: context).toText18(isBold: true), expandedBackgroundColor: Colors.transparent, children: [
+ SizedBox(height: 10.h),
+ getSelectedTabData(2),
+ ]),
ExpandableListItem(
title: LocaleKeys.insuranceAndPayments.tr(context: context).toText18(isBold: true),
expandedBackgroundColor: Colors.transparent,
@@ -601,14 +600,9 @@ class _MedicalFilePageState extends State {
text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}",
onPressed: () {
insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true);
- insuranceViewModel.getPatientInsuranceDetailsForUpdate(appState.getAuthenticatedUser()!.patientId.toString(),
- appState.getAuthenticatedUser()!.patientIdentificationNo.toString());
- showCommonBottomSheetWithoutHeight(context,
- child: PatientInsuranceCardUpdateCard(),
- callBackFunc: () {},
- title: "",
- isCloseButtonVisible: false,
- isFullScreen: false);
+ insuranceViewModel.getPatientInsuranceDetailsForUpdate(
+ appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString());
+ showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false);
},
backgroundColor: AppColors.bgGreenColor.withOpacity(0.20),
borderColor: AppColors.bgGreenColor.withOpacity(0.0),
@@ -733,8 +727,7 @@ class _MedicalFilePageState extends State {
? Container(
padding: EdgeInsets.all(12.w),
width: MediaQuery.of(context).size.width,
- decoration:
- RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: false),
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: false),
child: Column(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h),
@@ -893,8 +886,7 @@ class _MedicalFilePageState extends State {
? const CommonShimmerWidget().paddingSymmetrical(0.w, 0.h)
: prescriptionVM.patientPrescriptionOrders.isNotEmpty
? Container(
- decoration:
- RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r, hasShadow: false),
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r, hasShadow: false),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
@@ -931,15 +923,13 @@ class _MedicalFilePageState extends State {
spacing: 3.w,
runSpacing: 4.w,
children: [
- AppCustomChipWidget(
- labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!),
+ AppCustomChipWidget(labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!),
Directionality(
textDirection: ui.TextDirection.ltr,
child: AppCustomChipWidget(
icon: AppAssets.doctor_calendar_icon,
labelText: DateUtil.formatDateToDate(
- DateUtil.convertStringToDate(
- prescriptionVM.patientPrescriptionOrders[index].appointmentDate),
+ DateUtil.convertStringToDate(prescriptionVM.patientPrescriptionOrders[index].appointmentDate),
false,
),
isEnglishOnly: true,
@@ -953,20 +943,14 @@ class _MedicalFilePageState extends State {
// SizedBox(width: 40.h),
Transform.flip(
flipX: appState.isArabic(),
- child: Utils.buildSvgWithAssets(
- icon: AppAssets.forward_arrow_icon_small,
- width: 15.w,
- height: 15.h,
- fit: BoxFit.contain,
- iconColor: AppColors.textColor)),
+ child:
+ Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 15.w, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)),
],
).onPress(() {
prescriptionVM.setPrescriptionsDetailsLoading();
Navigator.of(context).push(
CustomPageRoute(
- page: PrescriptionDetailPage(
- isFromAppointments: false,
- prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]),
+ page: PrescriptionDetailPage(isFromAppointments: false, prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]),
),
);
}),
@@ -1128,10 +1112,7 @@ class _MedicalFilePageState extends State {
SizedBox(height: 8.h),
SizedBox(
width: 80.w,
- child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName)
- .toString()
- .toText12(isBold: true, isCenter: true, maxLine: 2)
- .toShimmer2(isShow: false),
+ child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName).toString().toText12(isBold: true, isCenter: true, maxLine: 2).toShimmer2(isShow: false),
),
],
),
@@ -1146,8 +1127,7 @@ class _MedicalFilePageState extends State {
LoaderBottomSheet.hideLoader();
Navigator.of(context).push(
CustomPageRoute(
- page: DoctorProfilePage(
- isDoctorAllowedToBook: !(myAppointmentsVM.patientMyDoctorsList[index].isLiveCareClinic ?? false)),
+ page: DoctorProfilePage(isDoctorAllowedToBook: !(myAppointmentsVM.patientMyDoctorsList[index].isLiveCareClinic ?? false)),
),
);
}, onError: (err) {
@@ -1284,13 +1264,8 @@ class _MedicalFilePageState extends State {
);
}),
SizedBox(height: 16.h),
- Selector listRequest, List listReady})>(
- selector: (context, vm) => (
- isLoading: vm.isPatientMedicalReportsListLoading,
- listRequest: vm.patientMedicalReportRequestedList,
- listReady: vm.patientMedicalReportReadyList
- ),
+ Selector listRequest, List listReady})>(
+ selector: (context, vm) => (isLoading: vm.isPatientMedicalReportsListLoading, listRequest: vm.patientMedicalReportRequestedList, listReady: vm.patientMedicalReportReadyList),
builder: (context, data, _) {
return MedicalReportCard(isLoading: data.isLoading, listRequest: data.listRequest, listReady: data.listReady);
},
@@ -1546,17 +1521,11 @@ class _MedicalFilePageState extends State {
child: _buildVitalSignCard(
icon: AppAssets.bloodPressure,
label: LocaleKeys.bloodPressure.tr(context: context),
- value: (vitalSign.bloodPressureLower != null &&
- vitalSign.bloodPressureHigher != null &&
- vitalSign.bloodPressureLower != 0 &&
- vitalSign.bloodPressureHigher != 0)
+ value: (vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null && vitalSign.bloodPressureLower != 0 && vitalSign.bloodPressureHigher != 0)
? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}"
: '--',
unit: '',
- status: (vitalSign.bloodPressureLower != null &&
- vitalSign.bloodPressureHigher != null &&
- vitalSign.bloodPressureLower != 0 &&
- vitalSign.bloodPressureHigher != 0)
+ status: (vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null && vitalSign.bloodPressureLower != 0 && vitalSign.bloodPressureHigher != 0)
? _getBloodPressureStatus(
systolic: vitalSign.bloodPressureHigher,
diastolic: vitalSign.bloodPressureLower,
@@ -1640,8 +1609,7 @@ class _MedicalFilePageState extends State {
weight: FontWeight.w600,
),
),
- Utils.buildSvgWithAssets(
- icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward, width: 18.w, height: 18.h),
+ Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward, width: 18.w, height: 18.h),
],
),
Spacer(),
diff --git a/lib/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart
index d25154c7..4eebc75c 100644
--- a/lib/presentation/my_family/my_family.dart
+++ b/lib/presentation/my_family/my_family.dart
@@ -92,7 +92,10 @@ class _FamilyMedicalScreenState extends State {
Selector patientFiles, List 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),
),
diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart
index bfae773f..2f48bcbe 100644
--- a/lib/presentation/my_family/widget/family_cards.dart
+++ b/lib/presentation/my_family/widget/family_cards.dart
@@ -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 {
AppState appState = getIt();
+ final PermissionService _permissionService = getIt();
late InsuranceViewModel insuranceViewModel;
late ProfileSettingsViewModel profileSettingsViewModel;
- File? _selectedImage;
- bool _isUploadingImage = false;
@override
void initState() {
@@ -77,301 +75,27 @@ class _FamilyCardsState extends State {
}
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 {
- // Compress and resize the image
- final compressedFile = await ImageCompressionHelper.compressAndResizeImage(file);
-
- File finalFile;
- String finalBase64;
-
- if (compressedFile == null) {
-
- // Fallback: use original image if compression fails
- final originalSize = await file.length();
- final maxSize = 1048576; // 1MB
-
- if (originalSize > maxSize) {
- if (mounted) {
- Utils.showToast(
- LocaleKeys.imageSizeTooLarge.tr(context: context),
- );
- }
- return;
- }
- 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
-
- if (fileSize > maxSize) {
- if (mounted) {
- Utils.showToast(
- LocaleKeys.imageSizeTooLarge.tr(context: context),
- );
- }
- return;
- }
-
- finalFile = compressedFile;
- var bytes = await compressedFile.readAsBytes();
- finalBase64 = base64.encode(bytes);
- }
-
- if (mounted) {
- setState(() {
- _selectedImage = finalFile;
- });
-
- // Upload the image
- _uploadImage(finalBase64);
- }
-
- } catch (e, stackTrace) {
- if (mounted) {
- Utils.showToast(
- LocaleKeys.failedToProcessImage.tr(context: context),
- );
- }
- }
- },
- checkCameraPermission: _checkCameraPermission,
- checkGalleryPermission: _checkGalleryPermission,
- );
- }
-
- Future _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');
+ final profilePictureViewModel = context.read();
- 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,
- );
+ Utils.showToast(LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context));
}
- 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 _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
- print('β
Android Photo Picker will handle permissions internally');
- return true;
- }
-
- // 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');
+ },
+ 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) {
@@ -537,40 +261,44 @@ class _FamilyCardsState extends State {
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(
- AppColors.whiteColor,
- ),
- ).paddingAll(4.w),
- )
- : Icon(
- Icons.camera_alt,
+ child: Consumer(
+ 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(
+ AppColors.whiteColor,
+ ),
+ ).paddingAll(4.w),
+ )
+ : Icon(
+ Icons.camera_alt,
+ color: AppColors.whiteColor,
+ size: 10.w,
+ ),
+ ),
+ );
+ },
),
),
],
diff --git a/lib/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_page.dart b/lib/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_page.dart
new file mode 100644
index 00000000..8e7a53cd
--- /dev/null
+++ b/lib/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_page.dart
@@ -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? images;
+ final int initialIndex;
+
+ const OfferAndDiscountsFullScreenSwiperPage({
+ super.key,
+ this.images,
+ this.initialIndex = 0,
+ });
+
+ @override
+ State createState() => _OfferAndDiscountsFullScreenSwiperPageState();
+}
+
+class _OfferAndDiscountsFullScreenSwiperPageState extends State {
+ late PageController _pageController;
+ late int _currentPage;
+ late List _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().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,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart b/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart
new file mode 100644
index 00000000..86dedbcd
--- /dev/null
+++ b/lib/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart
@@ -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),
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/offers_and_discounts/offers_and_discounts_history_page.dart b/lib/presentation/offers_and_discounts/offers_and_discounts_history_page.dart
new file mode 100644
index 00000000..74ef2426
--- /dev/null
+++ b/lib/presentation/offers_and_discounts/offers_and_discounts_history_page.dart
@@ -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 createState() => _OffersAndDiscountsHistoryPageState();
+}
+
+class _OffersAndDiscountsHistoryPageState extends State {
+ 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();
+ offersAndDiscountsViewModel = Provider.of(context, listen: false);
+
+ return CollapsingListView(
+ title: '${LocaleKeys.order.tr(context: context)} ${LocaleKeys.history.tr(context: context)}',
+ child: SingleChildScrollView(
+ child: Consumer(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),
+ ],
+ );
+ }),
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/offers_and_discounts/offers_and_discounts_page.dart b/lib/presentation/offers_and_discounts/offers_and_discounts_page.dart
new file mode 100644
index 00000000..a38c7935
--- /dev/null
+++ b/lib/presentation/offers_and_discounts/offers_and_discounts_page.dart
@@ -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 createState() => _OffersAndDiscountsPageState();
+}
+
+class _OffersAndDiscountsPageState extends State {
+ 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();
+ offersAndDiscountsViewModel = Provider.of(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(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);
+ }),
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.dart b/lib/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.dart
new file mode 100644
index 00000000..11713003
--- /dev/null
+++ b/lib/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.dart
@@ -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 selectedOffers;
+ final Function(List) 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 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);
+ });
+ }
+}
diff --git a/lib/presentation/offers_and_discounts/widgets/offers_and_discounts.dart b/lib/presentation/offers_and_discounts/widgets/offers_and_discounts.dart
new file mode 100644
index 00000000..cfd6a86e
--- /dev/null
+++ b/lib/presentation/offers_and_discounts/widgets/offers_and_discounts.dart
@@ -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 createState() => _OffersAndDiscountsCarouselState();
+}
+
+class _OffersAndDiscountsCarouselState extends State {
+ final ScrollController _scrollController = ScrollController();
+ Timer? _autoScrollTimer;
+
+ // List of offer images - you can add more images here
+ final List 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,
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart
index 6b30c0fc..06cf3a8f 100644
--- a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart
+++ b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart
@@ -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 {
final AppState _appState = getIt.get();
- 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();
@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.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();
+ 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();
-
- 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();
- 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().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();
- // 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 _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 _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();
- 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 {
}
// 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 {
);
}
- // Use cached decoded bytes β update cache if source data changed
- final String? imageData = GetIt.instance().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 {
@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(
- builder: (context, profileVm, _) {
+ return Consumer2(
+ builder: (context, profilePictureViewModel, profileVm, _) {
// If we already have cached bytes, show the image even while "loading"
// to prevent the shimmerβimage 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(
+ 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
diff --git a/lib/services/permission_service.dart b/lib/services/permission_service.dart
index 62872340..5f749af8 100644
--- a/lib/services/permission_service.dart
+++ b/lib/services/permission_service.dart
@@ -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,206 @@ class PermissionService {
openAppSettings();
}
+ /// Check and request camera permission with proper dialog handling
+ /// Returns true if permission is granted, false otherwise
+ Future 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 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
+ // 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
+ print('β
Android Photo Picker will handle permissions internally');
+ return true;
+ }
+
+ // 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 {
+ Navigator.pop(context);
+ 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) {
diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart
index 3fa7d061..1b6fd3ab 100644
--- a/lib/widgets/image_picker.dart
+++ b/lib/widgets/image_picker.dart
@@ -78,7 +78,7 @@ class ImageOptions {
}
},
onFilesTap: () async {
- FilePickerResult? result = await FilePicker.platform.pickFiles(
+ FilePickerResult? result = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: [
'jpg',
diff --git a/lib/widgets/user_avatar_widget.dart b/lib/widgets/user_avatar_widget.dart
index 3271adf4..84499efa 100644
--- a/lib/widgets/user_avatar_widget.dart
+++ b/lib/widgets/user_avatar_widget.dart
@@ -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(