Compare commits

..

10 Commits

@ -21,7 +21,7 @@
"mySchedule": "My Schedule", "mySchedule": "My Schedule",
"logout": "Logout", "logout": "Logout",
"respirationRate": "Respiration Rate", "respirationRate": "Respiration Rate",
"bookAppo": "New Appointment", "bookAppo": "Book Appointment",
"searchBy": "Search By:", "searchBy": "Search By:",
"clinic": "Clinic", "clinic": "Clinic",
"byClinic": "By Clinic", "byClinic": "By Clinic",

@ -86,7 +86,7 @@
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>This app requires microphone access to enable virtual consultation between patient &amp; doctor</string> <string>This app requires microphone access to enable virtual consultation between patient &amp; doctor</string>
<key>NSMotionUsageDescription</key> <key>NSMotionUsageDescription</key>
<string>This app requires motion detection access to function properly.</string> <string>This app requires access to motion detection to count your daily steps.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>This app requires photo library access to select image as document &amp; upload it.</string> <string>This app requires photo library access to select image as document &amp; upload it.</string>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>

@ -210,8 +210,8 @@ class ApiClientImp implements ApiClient {
body['TokenID'] = "@dm!n"; body['TokenID'] = "@dm!n";
} }
body['TokenID'] = "@dm!n"; // body['TokenID'] = "@dm!n";
// body['PatientID'] = 1307867; // body['PatientID'] = 3310954;
// body['PatientID'] = 53320; // body['PatientID'] = 53320;
// body['PatientTypeID'] = 1; // body['PatientTypeID'] = 1;
// body['PatientOutSA'] = 0; // body['PatientOutSA'] = 0;

@ -40,14 +40,23 @@ class RequestUtils {
if (zipCode == "0") { if (zipCode == "0") {
request.patientMobileNumberOthers = phoneNumber; request.patientMobileNumberOthers = phoneNumber;
} else { } else {
request.patientMobileNumber = int.parse(phoneNumber); // Remove any non-numeric characters before parsing
final numericPhone = phoneNumber.replaceAll(RegExp(r'[^0-9]'), '');
final parsedPhone = int.tryParse(numericPhone);
if (parsedPhone != null) {
request.patientMobileNumber = parsedPhone;
} else {
// If parsing fails, use as string in Others field
request.patientMobileNumberOthers = phoneNumber;
}
} }
} }
request.oTPSendType = otpTypeEnum.toInt(); // could map OTPTypeEnum if needed request.oTPSendType = otpTypeEnum.toInt(); // could map OTPTypeEnum if needed
request.zipCode = zipCode; // or countryCode if defined elsewhere request.zipCode = zipCode; // or countryCode if defined elsewhere
if (isForRegister) { if (isForRegister) {
request.patientIdentificationID = int.parse(nationId); final parsedNationId = int.tryParse(nationId.replaceAll(RegExp(r'[^0-9]'), ''));
request.patientIdentificationID = parsedNationId ?? 0;
request.searchType = 1; request.searchType = 1;
request.isHijri = calenderType.toInt; request.isHijri = calenderType.toInt;
request.patientID = patientId; request.patientID = patientId;
@ -56,7 +65,7 @@ class RequestUtils {
request.isDentalAllowedBackend = false; request.isDentalAllowedBackend = false;
} else { } else {
if (fileNo) { if (fileNo) {
request.patientID = patientId ?? int.parse(nationId); request.patientID = patientId ?? (int.tryParse(nationId.replaceAll(RegExp(r'[^0-9]'), '')) ?? 0);
request.patientIdentificationID = request.nationalID; request.patientIdentificationID = request.nationalID;
request.searchType = 2; request.searchType = 2;
} else { } else {
@ -91,8 +100,17 @@ class RequestUtils {
request.patientMobileNumberOthers = phoneNumber; request.patientMobileNumberOthers = phoneNumber;
request.mobileNo = phoneNumber; request.mobileNo = phoneNumber;
} else { } else {
request.patientMobileNumber = int.parse(phoneNumber); // Remove any non-numeric characters before parsing
request.mobileNo = '0$phoneNumber'; final numericPhone = phoneNumber.replaceAll(RegExp(r'[^0-9]'), '');
final parsedPhone = int.tryParse(numericPhone);
if (parsedPhone != null) {
request.patientMobileNumber = parsedPhone;
request.mobileNo = '0$numericPhone';
} else {
// If parsing fails, use as string in Others field
request.patientMobileNumberOthers = phoneNumber;
request.mobileNo = phoneNumber;
}
} }
request.deviceToken = deviceToken; request.deviceToken = deviceToken;
request.projectOutSA = patientOutSA; request.projectOutSA = patientOutSA;
@ -115,7 +133,8 @@ class RequestUtils {
log("nationIdText: ${nationIdText}"); log("nationIdText: ${nationIdText}");
} else { } else {
if (fileNo) { if (fileNo) {
request.patientID = patientId ?? int.parse(nationIdText); final numericNationId = nationIdText.replaceAll(RegExp(r'[^0-9]'), '');
request.patientID = patientId ?? (int.tryParse(numericNationId) ?? 0);
request.patientIdentificationID = request.nationalID = '0'; request.patientIdentificationID = request.nationalID = '0';
request.searchType = 2; request.searchType = 2;
//TODO: Issue HEre is Not Login //TODO: Issue HEre is Not Login
@ -155,8 +174,17 @@ class RequestUtils {
request.patientMobileNumberOthers = mobileNumber; request.patientMobileNumberOthers = mobileNumber;
request.mobileNo = mobileNumber; request.mobileNo = mobileNumber;
} else { } else {
request.patientMobileNumber = int.parse(mobileNumber); // Remove any non-numeric characters before parsing
request.mobileNo = '0$mobileNumber'; final numericMobile = mobileNumber.replaceAll(RegExp(r'[^0-9]'), '');
final parsedMobile = int.tryParse(numericMobile);
if (parsedMobile != null) {
request.patientMobileNumber = parsedMobile;
request.mobileNo = '0$numericMobile';
} else {
// If parsing fails, use as string in Others field
request.patientMobileNumberOthers = mobileNumber;
request.mobileNo = mobileNumber;
}
} }
} }
request.projectOutSA = patientOutSA; request.projectOutSA = patientOutSA;
@ -314,7 +342,9 @@ class RequestUtils {
request.sharedPatientId = 0; request.sharedPatientId = 0;
request.sharedPatientIdentificationId = nationalIDorFile; request.sharedPatientIdentificationId = nationalIDorFile;
} else if (loginType == 2) { } else if (loginType == 2) {
request.sharedPatientId = int.parse(nationalIDorFile); // Remove any non-numeric characters before parsing
final numericId = nationalIDorFile.replaceAll(RegExp(r'[^0-9]'), '');
request.sharedPatientId = int.tryParse(numericId) ?? 0;
request.sharedPatientIdentificationId = ''; request.sharedPatientIdentificationId = '';
} }
request.searchType = loginType; request.searchType = loginType;

@ -676,7 +676,6 @@ class BookAppointmentsViewModel extends ChangeNotifier {
(failure) async { (failure) async {
isDoctorsListLoading = false; isDoctorsListLoading = false;
if (onError != null) onError(LocaleKeys.noDoctorFound.tr()); if (onError != null) onError(LocaleKeys.noDoctorFound.tr());
notifyListeners(); notifyListeners();
}, },
(apiResponse) { (apiResponse) {
@ -691,7 +690,7 @@ class BookAppointmentsViewModel extends ChangeNotifier {
clearSearchFilters(); clearSearchFilters();
getFiltersFromDoctorList(); getFiltersFromDoctorList();
_groupDoctorsList(); _groupDoctorsList();
setIsNearestAppointmentSelected(isNearest); // setIsNearestAppointmentSelected(isNearest);
notifyListeners(); notifyListeners();
if (onSuccess != null) { if (onSuccess != null) {
onSuccess(apiResponse); onSuccess(apiResponse);

@ -78,7 +78,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
List<PatientAppointmentHistoryResponseModel> patientUpcomingAppointmentsHistoryList = []; List<PatientAppointmentHistoryResponseModel> patientUpcomingAppointmentsHistoryList = [];
List<PatientAppointmentHistoryResponseModel> patientArrivedAppointmentsHistoryList = []; List<PatientAppointmentHistoryResponseModel> patientArrivedAppointmentsHistoryList = [];
List<PatientAppointmentHistoryResponseModel> patientAllArrivedAppointmentsHistoryList = []; // List<PatientAppointmentHistoryResponseModel> patientAllArrivedAppointmentsHistoryList = [];
List<PatientAppointmentHistoryResponseModel> patientMyDoctorsList = []; List<PatientAppointmentHistoryResponseModel> patientMyDoctorsList = [];
@ -174,7 +174,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
patientAppointmentsHistoryList.clear(); patientAppointmentsHistoryList.clear();
patientUpcomingAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear();
patientArrivedAppointmentsHistoryList.clear(); patientArrivedAppointmentsHistoryList.clear();
patientAllArrivedAppointmentsHistoryList.clear(); // patientAllArrivedAppointmentsHistoryList.clear();
patientEyeMeasurementsAppointmentsHistoryList.clear(); patientEyeMeasurementsAppointmentsHistoryList.clear();
isMyAppointmentsLoading = true; isMyAppointmentsLoading = true;
isTimeLineAppointmentsLoading = true; isTimeLineAppointmentsLoading = true;
@ -290,7 +290,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
patientAppointmentsByHospital.clear(); patientAppointmentsByHospital.clear();
patientAppointmentsViewList.clear(); patientAppointmentsViewList.clear();
patientAllArrivedAppointmentsHistoryList.clear(); // patientAllArrivedAppointmentsHistoryList.clear();
filteredAppointmentList.clear(); filteredAppointmentList.clear();
patientAppointmentsHistoryList.clear(); patientAppointmentsHistoryList.clear();
patientUpcomingAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear();
@ -329,7 +329,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
isMyAppointmentsLoading = false; isMyAppointmentsLoading = false;
isAppointmentDataToBeLoaded = false; isAppointmentDataToBeLoaded = false;
if (!isForTimeLine) { if (!isForTimeLine) {
patientAllArrivedAppointmentsHistoryList = apiResponse.data!; // patientAllArrivedAppointmentsHistoryList = apiResponse.data!;
isArrivedAppointmentDataLoaded = true; isArrivedAppointmentDataLoaded = true;
isFullArrivedAppointmentsLoaded = true; isFullArrivedAppointmentsLoaded = true;
} }
@ -363,11 +363,25 @@ class MyAppointmentsViewModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
changeTabToArrived() {
if (patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) {
selectedTabIndex = 1;
updateListWRTTab(1);
notifyListeners();
}
}
/// Loads full arrived appointments list if not already loaded /// Loads full arrived appointments list if not already loaded
/// This is called when navigating to My Appointments page from landing page /// This is called when navigating to My Appointments page from landing page
Future<void> loadFullArrivedAppointmentsIfNeeded({Function(dynamic)? onSuccess, Function(String)? onError}) async { Future<void> loadFullArrivedAppointmentsIfNeeded({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// Skip if already loaded // Skip if already loaded
if (isFullArrivedAppointmentsLoaded) { if (isFullArrivedAppointmentsLoaded) {
// Auto-switch to Arrived tab if no upcoming appointments (even when data is already loaded)
if (patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) {
selectedTabIndex = 1;
updateListWRTTab(1);
notifyListeners();
}
return; return;
} }
@ -383,7 +397,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
// Handle error // Handle error
} else if (apiResponse.messageStatus == 1) { } else if (apiResponse.messageStatus == 1) {
patientArrivedAppointmentsHistoryList = apiResponse.data!; patientArrivedAppointmentsHistoryList = apiResponse.data!;
patientAllArrivedAppointmentsHistoryList = apiResponse.data!; // patientAllArrivedAppointmentsHistoryList = apiResponse.data!;
isArrivedAppointmentDataLoaded = true; isArrivedAppointmentDataLoaded = true;
isFullArrivedAppointmentsLoaded = true; isFullArrivedAppointmentsLoaded = true;
@ -394,8 +408,14 @@ class MyAppointmentsViewModel extends ChangeNotifier {
isMyAppointmentsLoading = false; isMyAppointmentsLoading = false;
// Auto-switch to Arrived tab if no upcoming appointments
if (patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) {
selectedTabIndex = 1;
updateListWRTTab(1);
} else {
// Update filtered list based on current tab // Update filtered list based on current tab
updateListWRTTab(selectedTabIndex); updateListWRTTab(selectedTabIndex);
}
notifyListeners(); notifyListeners();
if (onSuccess != null) { if (onSuccess != null) {
@ -414,9 +434,9 @@ class MyAppointmentsViewModel extends ChangeNotifier {
availableFilters.add(AppointmentListingFilters.LIVECARE); availableFilters.add(AppointmentListingFilters.LIVECARE);
} }
if (filteredAppointmentList.any((element) => element.isLiveCareAppointment == false)) { // if (filteredAppointmentList.any((element) => element.isLiveCareAppointment == false)) {
availableFilters.add(AppointmentListingFilters.WALKIN); // availableFilters.add(AppointmentListingFilters.WALKIN);
} // }
if (filteredAppointmentList.any((element) => AppointmentType.isArrived(element) == true)) { if (filteredAppointmentList.any((element) => AppointmentType.isArrived(element) == true)) {
availableFilters.add(AppointmentListingFilters.ARRIVED); availableFilters.add(AppointmentListingFilters.ARRIVED);
@ -853,15 +873,15 @@ class MyAppointmentsViewModel extends ChangeNotifier {
selectedFilter = []; selectedFilter = [];
// if(previouslySelectedTab == selectedTabIndex ) return; // if(previouslySelectedTab == selectedTabIndex ) return;
switch (index) { switch (index) {
// case 0:
// filteredAppointmentList.clear();
// filteredAppointmentList.addAll(patientAppointmentsHistoryList);
// break;
case 0: case 0:
filteredAppointmentList.clear();
filteredAppointmentList.addAll(patientAppointmentsHistoryList);
break;
case 1:
filteredAppointmentList.clear(); filteredAppointmentList.clear();
filteredAppointmentList.addAll(patientUpcomingAppointmentsHistoryList); filteredAppointmentList.addAll(patientUpcomingAppointmentsHistoryList);
break; break;
case 2: case 1:
filteredAppointmentList.clear(); filteredAppointmentList.clear();
filteredAppointmentList.addAll(patientArrivedAppointmentsHistoryList); filteredAppointmentList.addAll(patientArrivedAppointmentsHistoryList);
break; break;
@ -887,11 +907,12 @@ class MyAppointmentsViewModel extends ChangeNotifier {
this.end = end; this.end = end;
isDateFilterSelected = true; isDateFilterSelected = true;
List<PatientAppointmentHistoryResponseModel> sourceList = []; List<PatientAppointmentHistoryResponseModel> sourceList = [];
// if (selectedTabIndex == 0) {
// sourceList = patientAppointmentsHistoryList;
// } else
if (selectedTabIndex == 0) { if (selectedTabIndex == 0) {
sourceList = patientAppointmentsHistoryList;
} else if (selectedTabIndex == 1) {
sourceList = patientUpcomingAppointmentsHistoryList; sourceList = patientUpcomingAppointmentsHistoryList;
} else if (selectedTabIndex == 2) { } else if (selectedTabIndex == 1) {
sourceList = patientArrivedAppointmentsHistoryList; sourceList = patientArrivedAppointmentsHistoryList;
} }
// if (isDateFilterSelected) sourceList = filteredAppointmentList; // if (isDateFilterSelected) sourceList = filteredAppointmentList;

@ -191,7 +191,9 @@ class PrescriptionsViewModel extends ChangeNotifier {
result.fold( result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure), // (failure) async => await errorHandlerService.handleError(failure: failure),
(failure) async { (failure) async {
onError!(failure.message); if (onError != null) {
onError(failure.message);
}
}, },
(apiResponse) async { (apiResponse) async {
if (apiResponse.messageStatus == 2) { if (apiResponse.messageStatus == 2) {

@ -70,9 +70,7 @@ class ProfilePictureViewModel extends ChangeNotifier {
try { try {
_cachedImageBytes = base64Decode(imageData); _cachedImageBytes = base64Decode(imageData);
_cachedImageDataHash = '${imageData.length}_${imageData.hashCode}'; _cachedImageDataHash = '${imageData.length}_${imageData.hashCode}';
print('✅ Cached existing profile image');
} catch (e) { } catch (e) {
print('❌ Error caching existing image: $e');
_cachedImageBytes = null; _cachedImageBytes = null;
_cachedImageDataHash = null; _cachedImageDataHash = null;
} }
@ -85,7 +83,6 @@ class ProfilePictureViewModel extends ChangeNotifier {
final currentPatientId = _appState.getAuthenticatedUser()?.patientId; final currentPatientId = _appState.getAuthenticatedUser()?.patientId;
if (currentPatientId != null && currentPatientId != _currentPatientId) { if (currentPatientId != null && currentPatientId != _currentPatientId) {
print('🔄 User switched detected: $_currentPatientId -> $currentPatientId');
_handleUserSwitch(currentPatientId); _handleUserSwitch(currentPatientId);
return true; return true;
} }
@ -97,8 +94,6 @@ class ProfilePictureViewModel extends ChangeNotifier {
final oldPatientId = _currentPatientId; final oldPatientId = _currentPatientId;
_currentPatientId = newPatientId; _currentPatientId = newPatientId;
print('🧹 Clearing cache for old user: $oldPatientId');
// Clear AppState cache // Clear AppState cache
_appState.clearProfileImageCache(); _appState.clearProfileImageCache();
@ -113,17 +108,14 @@ class ProfilePictureViewModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
// Load new user's profile image // Load new user's profile image
print('📥 Loading profile image for new user: $newPatientId');
_profileSettingsViewModel.getProfileImage( _profileSettingsViewModel.getProfileImage(
patientID: newPatientId, patientID: newPatientId,
forceRefresh: true, forceRefresh: true,
onSuccess: (data) { onSuccess: (data) {
print('✅ Profile image loaded successfully for user: $newPatientId');
_tryCacheExistingImage(); _tryCacheExistingImage();
notifyListeners(); notifyListeners();
}, },
onError: (error) { onError: (error) {
print('❌ Error loading profile image: $error');
notifyListeners(); notifyListeners();
}, },
); );
@ -133,22 +125,18 @@ class ProfilePictureViewModel extends ChangeNotifier {
void loadProfileImage({bool forceRefresh = false}) { void loadProfileImage({bool forceRefresh = false}) {
// Check if profile image is already loaded in AppState (skip if forcing refresh) // Check if profile image is already loaded in AppState (skip if forcing refresh)
if (!forceRefresh && _appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) { if (!forceRefresh && _appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) {
print('✅ Profile image already cached in AppState');
return; return;
} }
final patientID = _appState.getAuthenticatedUser()?.patientId; final patientID = _appState.getAuthenticatedUser()?.patientId;
if (patientID == null) { if (patientID == null) {
print('⚠️ Cannot load profile image - no authenticated user');
return; return;
} }
print('📥 Loading profile image for patient: $patientID (forceRefresh: $forceRefresh)');
_profileSettingsViewModel.getProfileImage( _profileSettingsViewModel.getProfileImage(
patientID: patientID, patientID: patientID,
forceRefresh: forceRefresh, forceRefresh: forceRefresh,
onSuccess: (data) { onSuccess: (data) {
print('✅ Profile image loaded successfully');
_tryCacheExistingImage(); _tryCacheExistingImage();
notifyListeners(); notifyListeners();
}, },
@ -188,10 +176,6 @@ class ProfilePictureViewModel extends ChangeNotifier {
false, // Don't show files option, only camera and gallery false, // Don't show files option, only camera and gallery
(base64String, file) async { (base64String, file) async {
try { 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 // Compress and resize the image
print('Calling compressAndResizeImage...'); print('Calling compressAndResizeImage...');
@ -266,14 +250,10 @@ class ProfilePictureViewModel extends ChangeNotifier {
onError('No authenticated user found'); onError('No authenticated user found');
return; return;
} }
print('📤 Uploading profile image for patient: $patientID');
_profileSettingsViewModel.uploadProfileImage( _profileSettingsViewModel.uploadProfileImage(
patientID: patientID, patientID: patientID,
imageData: base64String, imageData: base64String,
onSuccess: (data) async { onSuccess: (data) async {
print('✅ Profile image uploaded successfully');
// Clear old cache first to ensure fresh data // Clear old cache first to ensure fresh data
_cachedImageBytes = null; _cachedImageBytes = null;
_cachedImageDataHash = null; _cachedImageDataHash = null;
@ -287,9 +267,13 @@ class ProfilePictureViewModel extends ChangeNotifier {
// Increment version to trigger targeted rebuild (no full screen refresh) // Increment version to trigger targeted rebuild (no full screen refresh)
_profileImageVersion.value++; _profileImageVersion.value++;
print('🔄 Profile image version updated to ${_profileImageVersion.value} (targeted rebuild)');
onSuccess(data); final String successMessage = data is String
? data
: (data is Map && data['message'] != null
? data['message'].toString()
: data?.toString() ?? 'Success');
onSuccess(successMessage);
}, },
onError: (error) { onError: (error) {
print('❌ Error uploading profile image: $error'); print('❌ Error uploading profile image: $error');
@ -308,7 +292,6 @@ class ProfilePictureViewModel extends ChangeNotifier {
try { try {
_cachedImageBytes = base64Decode(imageData!); _cachedImageBytes = base64Decode(imageData!);
_cachedImageDataHash = currentHash; _cachedImageDataHash = currentHash;
print('🔄 Updated cached image bytes');
} catch (e) { } catch (e) {
print('❌ Error decoding profile image: $e'); print('❌ Error decoding profile image: $e');
_cachedImageBytes = null; _cachedImageBytes = null;
@ -326,7 +309,6 @@ class ProfilePictureViewModel extends ChangeNotifier {
_cachedImageDataHash = null; _cachedImageDataHash = null;
_selectedImage = null; _selectedImage = null;
notifyListeners(); notifyListeners();
print('🧹 Cleared all profile picture cache');
} }
/// Check if we should show shimmer loading /// Check if we should show shimmer loading

@ -69,14 +69,20 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
widget.patientAppointmentHistoryResponseModel.clinicID, widget.patientAppointmentHistoryResponseModel.clinicID,
widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, onSuccess: (val) { widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, onSuccess: (val) {
myAppointmentsViewModel.getTamaraInstallmentsDetails().then((val) { myAppointmentsViewModel.getTamaraInstallmentsDetails().then((val) {
if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null) { // if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null && myAppointmentsViewModel.patientAppointmentShareResponseModel != null) {
if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null &&
myAppointmentsViewModel.patientAppointmentShareResponseModel?.patientShareWithTax != null &&
myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel?.minLimit?.amount != null &&
myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel?.maxLimit?.amount != null) {
if (myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! >= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && if (myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! >= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! &&
myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) { myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) {
if (mounted) {
setState(() { setState(() {
isShowTamara = true; isShowTamara = true;
}); });
} }
} }
}
}); });
}, onError: (err) { }, onError: (err) {
Navigator.of(context).pop(); Navigator.of(context).pop();

@ -47,8 +47,7 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
myAppointmentsViewModel.initAppointmentsViewModel(); myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false); myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false);
} }
// Load full arrived appointments if not already loaded myAppointmentsViewModel.changeTabToArrived();
myAppointmentsViewModel.loadFullArrivedAppointmentsIfNeeded();
}); });
super.initState(); super.initState();
} }
@ -71,9 +70,9 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
activeTextColor: Color(0xffED1C2B), activeTextColor: Color(0xffED1C2B),
activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1),
tabs: [ tabs: [
CustomTabBarModel(null, LocaleKeys.allAppt.tr(context: context)), // CustomTabBarModel(null, LocaleKeys.allAppt.tr(context: context)),
CustomTabBarModel(null, LocaleKeys.upcoming.tr(context: context)), CustomTabBarModel(null, LocaleKeys.upcoming.tr(context: context)),
CustomTabBarModel(null, LocaleKeys.completed.tr(context: context)), CustomTabBarModel(null, LocaleKeys.arrived.tr(context: context)),
], ],
onTabChange: (index) { onTabChange: (index) {
setState(() { setState(() {
@ -82,6 +81,10 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
myAppointmentsViewModel.onTabChange(index); myAppointmentsViewModel.onTabChange(index);
myAppointmentsViewModel.updateListWRTTab(index); myAppointmentsViewModel.updateListWRTTab(index);
context.read<DateRangeSelectorRangeViewModel>().flush(); context.read<DateRangeSelectorRangeViewModel>().flush();
if(index == 1) {
myAppointmentsViewModel.loadFullArrivedAppointmentsIfNeeded();
}
}, },
).paddingSymmetrical(24.h, 0.h), ).paddingSymmetrical(24.h, 0.h),
// Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) { // Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
@ -152,7 +155,7 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
? myAppointmentsVM.patientAppointmentsViewList.length ? myAppointmentsVM.patientAppointmentsViewList.length
: 1, : 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isExpanded = myAppointmentsVM.selectedTabIndex == 1 ? true : expandedIndex == index; final isExpanded = myAppointmentsVM.selectedTabIndex == 0 ? true : expandedIndex == index;
return myAppointmentsVM.isMyAppointmentsLoading return myAppointmentsVM.isMyAppointmentsLoading
? Container( ? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),

@ -1021,6 +1021,7 @@ class _SelectClinicPageState extends State<SelectClinicPage> {
void onClinicSelected(GetClinicsListResponseModel clinic) { void onClinicSelected(GetClinicsListResponseModel clinic) {
bookAppointmentsViewModel.setSelectedClinic(clinic); bookAppointmentsViewModel.setSelectedClinic(clinic);
bookAppointmentsViewModel.setIsDoctorsListLoading(true); bookAppointmentsViewModel.setIsDoctorsListLoading(true);
searchEditingController.text = "";
if (clinic.isLiveCareClinicAndOnline ?? false) { if (clinic.isLiveCareClinicAndOnline ?? false) {
Navigator.of(context).push( Navigator.of(context).push(
CustomPageRoute( CustomPageRoute(

@ -48,7 +48,7 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
Clarity.setCurrentScreenName('Select Doctor Page'); Clarity.setCurrentScreenName('Select Doctor Page');
_scrollController = ScrollController(); _scrollController = ScrollController();
scheduleMicrotask(() { scheduleMicrotask(() {
bookAppointmentsViewModel.setIsNearestAppointmentSelected(true); bookAppointmentsViewModel.setIsNearestAppointmentSelected(false);
if (bookAppointmentsViewModel.isLiveCareSchedule) { if (bookAppointmentsViewModel.isLiveCareSchedule) {
bookAppointmentsViewModel.getLiveCareDoctorsList(); bookAppointmentsViewModel.getLiveCareDoctorsList();
} else { } else {
@ -57,8 +57,7 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
} else if (bookAppointmentsViewModel.isGetDocForHealthCal) { } else if (bookAppointmentsViewModel.isGetDocForHealthCal) {
bookAppointmentsViewModel.getDoctorsListByHealthCal(); bookAppointmentsViewModel.getDoctorsListByHealthCal();
} else { } else {
bookAppointmentsViewModel.setIsNearestAppointmentSelected(true); bookAppointmentsViewModel.getDoctorsList(isNearest: false);
bookAppointmentsViewModel.getDoctorsList(isNearest: true);
} }
} }
}); });

@ -144,8 +144,9 @@ class DoctorCard extends StatelessWidget {
spacing: 3.h, spacing: 3.h,
runSpacing: 4.h, runSpacing: 4.h,
children: [ children: [
bookAppointmentsViewModel.isNearestAppointmentSelected // bookAppointmentsViewModel.isNearestAppointmentSelected
? doctorsListResponseModel.nearestFreeSlot != null // ?
doctorsListResponseModel.nearestFreeSlot != null
? AppCustomChipWidget( ? AppCustomChipWidget(
labelText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)), labelText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)),
// richText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)) // richText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot))
@ -155,7 +156,8 @@ class DoctorCard extends StatelessWidget {
textColor: AppColors.successColor, textColor: AppColors.successColor,
).toShimmer2(isShow: isLoading) ).toShimmer2(isShow: isLoading)
: SizedBox.shrink() : SizedBox.shrink()
: SizedBox.shrink(), // : SizedBox.shrink()
,
AppCustomChipWidget( AppCustomChipWidget(
labelText: "${isLoading ? "Cardiologist" : doctorsListResponseModel.clinicName}", labelText: "${isLoading ? "Cardiologist" : doctorsListResponseModel.clinicName}",
).toShimmer2(isShow: isLoading), ).toShimmer2(isShow: isLoading),

@ -120,13 +120,24 @@ class _LandingPageState extends State<LandingPage> {
authVM = context.read<AuthenticationViewModel>(); authVM = context.read<AuthenticationViewModel>();
habibWalletVM = context.read<HabibWalletViewModel>(); habibWalletVM = context.read<HabibWalletViewModel>();
appointmentRatingViewModel = context.read<AppointmentRatingViewModel>(); appointmentRatingViewModel = context.read<AppointmentRatingViewModel>();
appState = getIt.get<AppState>();
authVM.savePushTokenToAppState(); authVM.savePushTokenToAppState();
if (mounted) { if (mounted) {
final user = appState.getAuthenticatedUser();
final hasUserQuickLoginData =
user != null &&
(user.mobileNumber?.isNotEmpty ?? false) &&
(user.patientIdentificationNo?.isNotEmpty ?? false);
if (hasUserQuickLoginData) {
authVM.checkLastLoginStatus(() { authVM.checkLastLoginStatus(() {
if (mounted) {
showQuickLogin(context); showQuickLogin(context);
}
}); });
} }
}
_scrollController.addListener(() { _scrollController.addListener(() {
final scrollOffset = _scrollController.offset; final scrollOffset = _scrollController.offset;
@ -172,12 +183,19 @@ class _LandingPageState extends State<LandingPage> {
appointmentNo, appointmentNo,
projectID, projectID,
onSuccess: ((response) { onSuccess: ((response) {
// Only open dialog if details are loaded AND widget is mounted
if (!mounted || appointmentRatingViewModel.appointmentDetails == null) return;
appointmentRatingViewModel.setClinicOrDoctor(false); appointmentRatingViewModel.setClinicOrDoctor(false);
appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr(context: context)); appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr());
appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context)); appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr());
openLastRating(); openLastRating();
appState.setRatedVisible(true); appState.setRatedVisible(true);
}), }),
onError: (String errorMessage) {
// Silently mark as rated to prevent retry loops
if (mounted) appState.setRatedVisible(true);
},
); );
} }
}, },
@ -1458,6 +1476,7 @@ class _LandingPageState extends State<LandingPage> {
} }
openLastRating() { openLastRating() {
if (!mounted) return;
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
titleWidget: Selector<AppointmentRatingViewModel, String?>( titleWidget: Selector<AppointmentRatingViewModel, String?>(

@ -74,7 +74,7 @@ class _PreferredLanguageWidgetState extends State<PreferredLanguageWidget> {
callBackFunc: () async { callBackFunc: () async {
Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!).pop(); Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!).pop();
profileSettingsViewModel.getProfileSettings(); profileSettingsViewModel.getProfileSettings();
}, isFullScreen: false, isAutoDismiss: true); }, isFullScreen: false, isAutoDismiss: true, isCloseButtonVisible: false);
}, },
onError: (error) { onError: (error) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();

@ -134,7 +134,7 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
callBackFunc: () async { callBackFunc: () async {
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).pop(); Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).pop();
profileSettingsViewModel!.getProfileSettings(); profileSettingsViewModel!.getProfileSettings();
}, isFullScreen: false, isAutoDismiss: true); }, isFullScreen: false, isAutoDismiss: true, isCloseButtonVisible: false);
}, },
onError: (error) { onError: (error) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();

@ -29,11 +29,19 @@ class OrganSelectorPage extends StatefulWidget {
State<OrganSelectorPage> createState() => _OrganSelectorPageState(); State<OrganSelectorPage> createState() => _OrganSelectorPageState();
} }
class _OrganSelectorPageState extends State<OrganSelectorPage> { class _OrganSelectorPageState extends State<OrganSelectorPage> with SingleTickerProviderStateMixin {
static const double _expandedBodyZoomFactor = 1.08;
static const double _expandedSheetHeightFactor = 0.3;
static const bool _enableTutorialOverlay = false;
static const bool _enableSwipeToFlip = false;
late final AppState _appState; late final AppState _appState;
late final DialogService dialogService; late final DialogService dialogService;
late final CacheService cacheService; late final CacheService cacheService;
late final AnimationController _introZoomHintController;
late final Animation<double> _introZoomHintAnimation;
bool _showTutorial = false; bool _showTutorial = false;
bool _hasPlayedIntroZoomHint = false;
@override @override
void initState() { void initState() {
@ -41,10 +49,52 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
_appState = getIt.get<AppState>(); _appState = getIt.get<AppState>();
dialogService = getIt<DialogService>(); dialogService = getIt<DialogService>();
cacheService = getIt<CacheService>(); cacheService = getIt<CacheService>();
_introZoomHintController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
)..addListener(() {
if (mounted) setState(() {});
});
_introZoomHintAnimation = TweenSequence<double>([
TweenSequenceItem(
tween: Tween<double>(begin: 1.0, end: 1.25),
weight: 45,
),
TweenSequenceItem(
tween: Tween<double>(begin: 1.25, end: 1.0),
weight: 55,
),
]).animate(CurvedAnimation(
parent: _introZoomHintController,
curve: Curves.easeInOut,
));
_checkAndShowTutorial(); _checkAndShowTutorial();
_playIntroZoomHint();
}
@override
void dispose() {
_introZoomHintController.dispose();
super.dispose();
}
Future<void> _playIntroZoomHint() async {
if (_hasPlayedIntroZoomHint) return;
_hasPlayedIntroZoomHint = true;
await Future.delayed(const Duration(milliseconds: 700));
if (!mounted) return;
_introZoomHintController.forward(from: 0);
} }
Future<void> _checkAndShowTutorial() async { Future<void> _checkAndShowTutorial() async {
if (!_enableTutorialOverlay) {
_showTutorial = false;
return;
}
// final hasSeenTutorial = cacheService.getBool(key: CacheConst.organSelectorTutorialShown) ?? false; // final hasSeenTutorial = cacheService.getBool(key: CacheConst.organSelectorTutorialShown) ?? false;
// if (!hasSeenTutorial) { // if (!hasSeenTutorial) {
// Show tutorial after a short delay to ensure the screen is fully built // Show tutorial after a short delay to ensure the screen is fully built
@ -118,7 +168,11 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
Expanded( Expanded(
child: Stack( child: Stack(
children: [ children: [
_buildBodyViewer(viewModel), SafeArea(
top: false,
bottom: false,
child: _buildBodyViewer(viewModel),
),
_buildViewToggleButtons(viewModel), _buildViewToggleButtons(viewModel),
// _buildViewZoomButtons(viewModel), // _buildViewZoomButtons(viewModel),
_buildBottomSheet(viewModel), _buildBottomSheet(viewModel),
@ -131,7 +185,7 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
), ),
), ),
// Tutorial overlay // Tutorial overlay
if (_showTutorial) if (_enableTutorialOverlay && _showTutorial)
PinchZoomTutorialOverlay( PinchZoomTutorialOverlay(
onComplete: _onTutorialComplete, onComplete: _onTutorialComplete,
), ),
@ -152,7 +206,13 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
height: 24.h, height: 24.h,
), ),
padding: EdgeInsetsDirectional.only(start: 12, end: 12), padding: EdgeInsetsDirectional.only(start: 12, end: 12),
onPressed: () => Navigator.pop(context), onPressed: () {
// Clear selected organs and sheet status when going back
final viewModel = context.read<SymptomsCheckerViewModel>();
viewModel.clearAllSelections();
viewModel.setBottomSheetExpanded(false);
Navigator.pop(context);
},
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
), ),
), ),
@ -175,7 +235,9 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) { Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) {
return GestureDetector( return GestureDetector(
onHorizontalDragEnd: (details) { onHorizontalDragEnd: !_enableSwipeToFlip
? null
: (details) {
// Swipe left or right to toggle view // Swipe left or right to toggle view
if (details.primaryVelocity != null) { if (details.primaryVelocity != null) {
if (details.primaryVelocity! < -200 || details.primaryVelocity! > 200) { if (details.primaryVelocity! < -200 || details.primaryVelocity! > 200) {
@ -183,9 +245,15 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
} }
} }
}, },
child: Padding( child: LayoutBuilder(
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h), builder: (context, constraints) {
child: AnimatedSwitcher( final bool isExpanded = viewModel.isBottomSheetExpanded;
final double screenHeight = MediaQuery.of(context).size.height;
final double extraBottomInset = isExpanded ? screenHeight * _expandedSheetHeightFactor : 0;
final bool isUserZoomedIn = viewModel.currentZoomScale > 1.01;
Widget buildInteractiveBody({required bool useExpandedZoom}) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 600), duration: const Duration(milliseconds: 600),
transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation), transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation),
switchInCurve: Curves.easeInOut, switchInCurve: Curves.easeInOut,
@ -193,15 +261,14 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
child: Builder( child: Builder(
key: ValueKey<BodyView>(viewModel.currentView), key: ValueKey<BodyView>(viewModel.currentView),
builder: (context) { builder: (context) {
// Detect female gender from viewModel; allow Arabic value as fallback final bool isFemale = (viewModel.selectedGender != null && (viewModel.selectedGender!.toLowerCase() == 'female' || viewModel.selectedGender == 'أنثى'));
final bool isFemale = final double effectiveZoomScale = viewModel.currentZoomScale * _introZoomHintAnimation.value;
(viewModel.selectedGender != null && (viewModel.selectedGender!.toLowerCase() == 'female' || viewModel.selectedGender == 'أنثى'));
final String bodyAsset = viewModel.currentView == BodyView.front final String bodyAsset = viewModel.currentView == BodyView.front
? (isFemale ? AppAssets.fullBodyFrontFemale : AppAssets.fullBodyFrontMale) ? (isFemale ? AppAssets.fullBodyFrontFemale : AppAssets.fullBodyFrontMale)
: (isFemale ? AppAssets.fullBodyBackFemale : AppAssets.fullBodyBackMale); : (isFemale ? AppAssets.fullBodyBackFemale : AppAssets.fullBodyBackMale);
return InteractiveBodyWidget( final body = InteractiveBodyWidget(
bodyImageAsset: bodyAsset, bodyImageAsset: bodyAsset,
organs: viewModel.currentOrgans, organs: viewModel.currentOrgans,
selectedOrganIds: viewModel.selectedOrganIds, selectedOrganIds: viewModel.selectedOrganIds,
@ -209,13 +276,43 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
isBodyHidden: viewModel.isBodyHidden, isBodyHidden: viewModel.isBodyHidden,
tooltipOrganId: viewModel.tooltipOrganId, tooltipOrganId: viewModel.tooltipOrganId,
isArabic: _appState.isArabic(), isArabic: _appState.isArabic(),
zoomScale: viewModel.currentZoomScale, zoomScale: effectiveZoomScale,
); );
return body;
}, },
), ),
);
}
if (!isExpanded) {
return Padding(
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h),
child: buildInteractiveBody(useExpandedZoom: false),
);
}
return SingleChildScrollView(
physics: isExpanded && !isUserZoomedIn ? const ClampingScrollPhysics() : const NeverScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight + extraBottomInset,
),
child: Padding(
padding: EdgeInsets.only(
bottom: extraBottomInset,
top: 24.h,
),
child: Padding(
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h),
child: buildInteractiveBody(useExpandedZoom: true),
),
), ),
), ),
); );
},
),
);
} }
Widget _build3DFlipTransition(Widget child, Animation<double> animation) { Widget _build3DFlipTransition(Widget child, Animation<double> animation) {
@ -408,15 +505,22 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
} }
Widget _buildExpandCollapseButton(SymptomsCheckerViewModel viewModel) { Widget _buildExpandCollapseButton(SymptomsCheckerViewModel viewModel) {
final organCount = viewModel.selectedOrgans.length;
final showBadge = !viewModel.isBottomSheetExpanded && organCount > 0;
return PositionedDirectional( return PositionedDirectional(
end: 24.w, end: 24.w,
top: -24.h, top: -24.h,
child: GestureDetector( child: GestureDetector(
onTap: viewModel.toggleBottomSheet, onTap: viewModel.toggleBottomSheet,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: Container( child: SizedBox(
width: 70.w, width: 70.w,
height: 70.h, height: 70.h,
child: Stack(
clipBehavior: Clip.none,
children: [
Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Container( child: Container(
width: 48.w, width: 48.w,
@ -441,6 +545,35 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
), ),
), ),
), ),
if (showBadge)
PositionedDirectional(
top: 8.h,
end: 8.w,
child: Container(
width: 22.w,
height: 22.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.primaryRedColor,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.whiteColor,
width: 1.5,
),
),
child: Text(
'$organCount',
style: TextStyle(
color: AppColors.whiteColor,
fontSize: 10.f,
fontWeight: FontWeight.w700,
),
),
),
),
],
),
),
), ),
); );
} }

@ -1,3 +1,4 @@
import 'dart:developer';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -7,7 +8,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_dot_widget.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_dot_widget.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
class InteractiveBodyWidget extends StatefulWidget { class InteractiveBodyWidget extends StatefulWidget {
final String bodyImageAsset; final String bodyImageAsset;
@ -63,9 +63,12 @@ class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
@override @override
void dispose() { void dispose() {
_transformationController.dispose(); _transformationController.dispose();
currentZoom = 0.0;
super.dispose(); super.dispose();
} }
double currentZoom = 0.0;
Future<void> _loadImageAspectRatio() async { Future<void> _loadImageAspectRatio() async {
final ByteData data = await rootBundle.load(widget.bodyImageAsset); final ByteData data = await rootBundle.load(widget.bodyImageAsset);
final ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); final ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
@ -77,17 +80,16 @@ class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
_imageAspectRatio = image.width / image.height; _imageAspectRatio = image.width / image.height;
}); });
} }
_transformationController.addListener(() {
currentZoom = _transformationController.value.getMaxScaleOnAxis();
setState(() {});
});
} }
void _updateZoom(double scale) { void _updateZoom(double scale) {
// Get current translation // Keep programmatic zoom centered to avoid drifting the body off-screen.
final currentTransform = _transformationController.value; final newTransform = Matrix4.identity()..scaleByDouble(scale, scale, 1.0, 1.0);
final currentTranslation = currentTransform.getTranslation();
// Create new transformation with updated scale while preserving translation
final newTransform = Matrix4.identity()
..setTranslation(currentTranslation)
..scaleByVector3(Vector3(scale, scale, 1.0));
_transformationController.value = newTransform; _transformationController.value = newTransform;
} }
@ -103,8 +105,12 @@ class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
return Center( return Center(
child: InteractiveViewer( child: InteractiveViewer(
transformationController: _transformationController, transformationController: _transformationController,
minScale: 0.5, alignment: Alignment.center,
maxScale: 4.0, panEnabled: true,
scaleEnabled: true,
boundaryMargin: currentZoom > 1.2 ? EdgeInsets.all(200.h) : EdgeInsets.zero,
minScale: 1.0,
maxScale: 9.0,
clipBehavior: Clip.none, clipBehavior: Clip.none,
child: AspectRatio( child: AspectRatio(
aspectRatio: _imageAspectRatio!, aspectRatio: _imageAspectRatio!,
@ -135,7 +141,7 @@ class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
// Organ dots // Organ dots
...widget.organs.map((organ) { ...widget.organs.map((organ) {
final isSelected = widget.selectedOrganIds.contains(organ.id); final isSelected = widget.selectedOrganIds.contains(organ.id);
final dotSize = 16.0; final dotSize = 18.0;
final leftPos = (organ.position.x * imageConstraints.maxWidth) - (dotSize / 2); final leftPos = (organ.position.x * imageConstraints.maxWidth) - (dotSize / 2);
final topPos = (organ.position.y * imageConstraints.maxHeight) - (dotSize / 2); final topPos = (organ.position.y * imageConstraints.maxHeight) - (dotSize / 2);

@ -23,7 +23,8 @@ abstract class DialogService {
Future<void> showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}); Future<void> showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed});
Future<void> showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}); Future<void> showCommonBottomSheetWithoutH(
{String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed});
Future<void> showSuccessBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}); Future<void> showSuccessBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed});
@ -119,21 +120,15 @@ class DialogServiceImp implements DialogService {
} }
@override @override
Future<void> showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) async { Future<void> showCommonBottomSheetWithoutH(
{String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) async {
final context = navigationService.navigatorKey.currentContext; final context = navigationService.navigatorKey.currentContext;
if (context == null) return; if (context == null) return;
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
title: label ?? "", title: label ?? "",
child: exceptionBottomSheetWidget( child: exceptionBottomSheetWidget(
context: context, context: context, message: message, okLabel: okLabel, cancelLabel: cancelLabel, onOkPressed: onOkPressed, onCancelPressed: onCancelPressed, isConfirmButton: isConfirmButton),
message: message,
okLabel: okLabel,
cancelLabel: cancelLabel,
onOkPressed: onOkPressed,
onCancelPressed: onCancelPressed,
isConfirmButton: isConfirmButton
),
callBackFunc: () {}, callBackFunc: () {},
); );
} }
@ -240,7 +235,8 @@ class DialogServiceImp implements DialogService {
} }
} }
Widget exceptionBottomSheetWidget({required BuildContext context, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) { Widget exceptionBottomSheetWidget(
{required BuildContext context, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) {
return Column( return Column(
children: [ children: [
(message).toText16(isBold: false, color: AppColors.textColor), (message).toText16(isBold: false, color: AppColors.textColor),

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
@ -15,10 +16,7 @@ class AppTheme {
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,
brightness: Brightness.light, brightness: Brightness.light,
pageTransitionsTheme: const PageTransitionsTheme( pageTransitionsTheme: const PageTransitionsTheme(
builders: { builders: {TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder()},
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder()
},
), ),
hintColor: Colors.grey[400], hintColor: Colors.grey[400],
disabledColor: Colors.grey[300], disabledColor: Colors.grey[300],
@ -51,10 +49,7 @@ class AppTheme {
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,
brightness: Brightness.dark, brightness: Brightness.dark,
pageTransitionsTheme: const PageTransitionsTheme( pageTransitionsTheme: const PageTransitionsTheme(
builders: { builders: {TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder()},
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder()
},
), ),
hintColor: Colors.grey[600], hintColor: Colors.grey[600],
disabledColor: Colors.grey[700], disabledColor: Colors.grey[700],
@ -63,8 +58,7 @@ class AppTheme {
scaffoldBackgroundColor: AppColors.dark.scaffoldBgColor, scaffoldBackgroundColor: AppColors.dark.scaffoldBgColor,
highlightColor: Colors.grey[800]!.withOpacity(0.4), highlightColor: Colors.grey[800]!.withOpacity(0.4),
splashColor: Colors.transparent, splashColor: Colors.transparent,
bottomSheetTheme: BottomSheetThemeData( bottomSheetTheme: BottomSheetThemeData(backgroundColor: Colors.black.withOpacity(0)),
backgroundColor: Colors.black.withOpacity(0)),
floatingActionButtonTheme: const FloatingActionButtonThemeData(highlightElevation: 2, disabledElevation: 0, elevation: 2), floatingActionButtonTheme: const FloatingActionButtonThemeData(highlightElevation: 2, disabledElevation: 0, elevation: 2),
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
color: AppColors.dark.scaffoldBgColor, color: AppColors.dark.scaffoldBgColor,

@ -257,8 +257,7 @@ void showCommonBottomSheetWithoutHeight(
duration: Duration(milliseconds: 500), duration: Duration(milliseconds: 500),
reverseDuration: Duration(milliseconds: 300), reverseDuration: Duration(milliseconds: 300),
), ),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width //MediaQuery.of(context).size.width, // Full width constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width),
),
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
showDragHandle: false, showDragHandle: false,

Loading…
Cancel
Save