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;

@ -27,7 +27,7 @@ class RequestUtils {
}) { }) {
bool fileNo = false; bool fileNo = false;
if (nationId.isNotEmpty) { if (nationId.isNotEmpty) {
final numericRegex = RegExp(r'^[0-9]+$'); final numericRegex = RegExp(r'^[0-9]+$');
fileNo = nationId.length < 10 && numericRegex.hasMatch(nationId); fileNo = nationId.length < 10 && numericRegex.hasMatch(nationId);
//fileNo = nationId.length < 10 && nationId.isNumericOnly() ; //fileNo = nationId.length < 10 && nationId.isNumericOnly() ;
if (fileNo) { if (fileNo) {
@ -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 {
@ -71,15 +80,15 @@ class RequestUtils {
static dynamic getCommonRequestWelcome( static dynamic getCommonRequestWelcome(
{required String phoneNumber, {required String phoneNumber,
required OTPTypeEnum otpTypeEnum, required OTPTypeEnum otpTypeEnum,
required String? deviceToken, required String? deviceToken,
required bool patientOutSA, required bool patientOutSA,
required String? loginTokenID, required String? loginTokenID,
RegistrationDataModelPayload? registeredData, RegistrationDataModelPayload? registeredData,
int? patientId, int? patientId,
required String nationIdText, required String nationIdText,
required String countryCode, required String countryCode,
required int loginType}) { required int loginType}) {
bool fileNo = false; bool fileNo = false;
if (nationIdText.isNotEmpty) { if (nationIdText.isNotEmpty) {
final numericRegex = RegExp(r'^[0-9]+$'); final numericRegex = RegExp(r'^[0-9]+$');
@ -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;
@ -106,8 +124,8 @@ class RequestUtils {
request.searchType = registeredData.searchType != null request.searchType = registeredData.searchType != null
? registeredData.searchType ? registeredData.searchType
: fileNo : fileNo
? 1 ? 1
: 2; : 2;
request.patientID = registeredData.patientId ?? 0; request.patientID = registeredData.patientId ?? 0;
request.patientIdentificationID = request.nationalID = (registeredData.patientIdentificationId ?? 0); request.patientIdentificationID = request.nationalID = (registeredData.patientIdentificationId ?? 0);
request.dob = registeredData.dob; request.dob = registeredData.dob;
@ -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;
@ -240,16 +268,16 @@ class RequestUtils {
"Patientobject": { "Patientobject": {
"TempValue": true, "TempValue": true,
"PatientIdentificationType": (isDubai "PatientIdentificationType": (isDubai
? appState.getUserRegistrationPayload.patientIdentificationId?.toString().substring(0, 1) ? appState.getUserRegistrationPayload.patientIdentificationId?.toString().substring(0, 1)
: appState.getNHICUserData.idNumber!.substring(0, 1)) == : appState.getNHICUserData.idNumber!.substring(0, 1)) ==
"1" "1"
? 1 ? 1
: 2, : 2,
"PatientIdentificationNo": "PatientIdentificationNo":
isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(), isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(),
"MobileNumber": appState.getUserRegistrationPayload.patientMobileNumber ?? 0, "MobileNumber": appState.getUserRegistrationPayload.patientMobileNumber ?? 0,
"PatientOutSA": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode || "PatientOutSA": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode ||
appState.getUserRegistrationPayload.zipCode == '+966') appState.getUserRegistrationPayload.zipCode == '+966')
? 0 ? 0
: 1, : 1,
"FirstNameN": isDubai ? "..." : appState.getNHICUserData.firstNameAr, "FirstNameN": isDubai ? "..." : appState.getNHICUserData.firstNameAr,
@ -266,31 +294,31 @@ class RequestUtils {
"DateofBirthN": date, "DateofBirthN": date,
"EmailAddress": emailAddress, "EmailAddress": emailAddress,
"SourceType": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode || "SourceType": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode ||
appState.getUserRegistrationPayload.zipCode == '+966') appState.getUserRegistrationPayload.zipCode == '+966')
? "1" ? "1"
: "2", : "2",
"PreferredLanguage": appState.getLanguageCode() == "ar" ? (isDubai ? "1" : 1) : (isDubai ? "2" : 2), "PreferredLanguage": appState.getLanguageCode() == "ar" ? (isDubai ? "1" : 1) : (isDubai ? "2" : 2),
"Marital": isDubai "Marital": isDubai
? (maritalStatus == MaritalStatusTypeEnum.single ? (maritalStatus == MaritalStatusTypeEnum.single
? '0' ? '0'
: maritalStatus == MaritalStatusTypeEnum.married : maritalStatus == MaritalStatusTypeEnum.married
? '1' ? '1'
: '2') : '2')
: (appState.getNHICUserData.maritalStatusCode == 'U' : (appState.getNHICUserData.maritalStatusCode == 'U'
? '0' ? '0'
: appState.getNHICUserData.maritalStatusCode == 'M' : appState.getNHICUserData.maritalStatusCode == 'M'
? '1' ? '1'
: '2'), : '2'),
}, },
"PatientIdentificationID": "PatientIdentificationID":
isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(), isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(),
"PatientMobileNumber": appState.getUserRegistrationPayload.patientMobileNumber.toString()[0] == '0' "PatientMobileNumber": appState.getUserRegistrationPayload.patientMobileNumber.toString()[0] == '0'
? appState.getUserRegistrationPayload.patientMobileNumber ? appState.getUserRegistrationPayload.patientMobileNumber
: '0${appState.getUserRegistrationPayload.patientMobileNumber}', : '0${appState.getUserRegistrationPayload.patientMobileNumber}',
"DOB": dob, "DOB": dob,
"IsHijri": appState.getUserRegistrationPayload.isHijri, "IsHijri": appState.getUserRegistrationPayload.isHijri,
"PatientOutSA": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode || "PatientOutSA": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode ||
appState.getUserRegistrationPayload.zipCode == '+966') appState.getUserRegistrationPayload.zipCode == '+966')
? 0 ? 0
: 1, : 1,
"isDentalAllowedBackend": appState.getUserRegistrationPayload.isDentalAllowedBackend, "isDentalAllowedBackend": appState.getUserRegistrationPayload.isDentalAllowedBackend,
@ -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;
// Update filtered list based on current tab // Auto-switch to Arrived tab if no upcoming appointments
updateListWRTTab(selectedTabIndex); if (patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) {
selectedTabIndex = 1;
updateListWRTTab(1);
} else {
// Update filtered list based on current tab
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) { // if (selectedTabIndex == 0) {
sourceList = patientAppointmentsHistoryList; // sourceList = patientAppointmentsHistoryList;
} else if (selectedTabIndex == 1) { // } else
if (selectedTabIndex == 0) {
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,12 +69,18 @@ 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!) {
setState(() { if (mounted) {
isShowTamara = true; setState(() {
}); isShowTamara = true;
});
}
} }
} }
}); });

@ -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,12 +120,23 @@ 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) {
authVM.checkLastLoginStatus(() { final user = appState.getAuthenticatedUser();
showQuickLogin(context); final hasUserQuickLoginData =
}); user != null &&
(user.mobileNumber?.isNotEmpty ?? false) &&
(user.patientIdentificationNo?.isNotEmpty ?? false);
if (hasUserQuickLoginData) {
authVM.checkLastLoginStatus(() {
if (mounted) {
showQuickLogin(context);
}
});
}
} }
_scrollController.addListener(() { _scrollController.addListener(() {
@ -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,45 +235,82 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) { Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) {
return GestureDetector( return GestureDetector(
onHorizontalDragEnd: (details) { onHorizontalDragEnd: !_enableSwipeToFlip
// Swipe left or right to toggle view ? null
if (details.primaryVelocity != null) { : (details) {
if (details.primaryVelocity! < -200 || details.primaryVelocity! > 200) { // Swipe left or right to toggle view
viewModel.toggleView(); if (details.primaryVelocity != null) {
} if (details.primaryVelocity! < -200 || details.primaryVelocity! > 200) {
} viewModel.toggleView();
}, }
child: Padding( }
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 600),
transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: Builder(
key: ValueKey<BodyView>(viewModel.currentView),
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 String bodyAsset = viewModel.currentView == BodyView.front
? (isFemale ? AppAssets.fullBodyFrontFemale : AppAssets.fullBodyFrontMale)
: (isFemale ? AppAssets.fullBodyBackFemale : AppAssets.fullBodyBackMale);
return InteractiveBodyWidget(
bodyImageAsset: bodyAsset,
organs: viewModel.currentOrgans,
selectedOrganIds: viewModel.selectedOrganIds,
onOrganTap: viewModel.toggleOrganSelection,
isBodyHidden: viewModel.isBodyHidden,
tooltipOrganId: viewModel.tooltipOrganId,
isArabic: _appState.isArabic(),
zoomScale: viewModel.currentZoomScale,
);
}, },
), child: LayoutBuilder(
), builder: (context, constraints) {
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),
transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: Builder(
key: ValueKey<BodyView>(viewModel.currentView),
builder: (context) {
final bool isFemale = (viewModel.selectedGender != null && (viewModel.selectedGender!.toLowerCase() == 'female' || viewModel.selectedGender == 'أنثى'));
final double effectiveZoomScale = viewModel.currentZoomScale * _introZoomHintAnimation.value;
final String bodyAsset = viewModel.currentView == BodyView.front
? (isFemale ? AppAssets.fullBodyFrontFemale : AppAssets.fullBodyFrontMale)
: (isFemale ? AppAssets.fullBodyBackFemale : AppAssets.fullBodyBackMale);
final body = InteractiveBodyWidget(
bodyImageAsset: bodyAsset,
organs: viewModel.currentOrgans,
selectedOrganIds: viewModel.selectedOrganIds,
onOrganTap: viewModel.toggleOrganSelection,
isBodyHidden: viewModel.isBodyHidden,
tooltipOrganId: viewModel.tooltipOrganId,
isArabic: _appState.isArabic(),
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),
),
),
),
);
},
), ),
); );
} }
@ -408,37 +505,73 @@ 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,
alignment: Alignment.center, child: Stack(
child: Container( clipBehavior: Clip.none,
width: 48.w, children: [
height: 48.h, Align(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( alignment: Alignment.center,
color: AppColors.whiteColor, child: Container(
borderRadius: 11.r, width: 48.w,
), height: 48.h,
child: Center( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
child: Transform.flip( color: AppColors.whiteColor,
flipX: _appState.isArabic(), borderRadius: 11.r,
child: AnimatedRotation( ),
duration: const Duration(milliseconds: 300), child: Center(
turns: viewModel.isBottomSheetExpanded ? 0.25 : -0.25, child: Transform.flip(
child: Utils.buildSvgWithAssets( flipX: _appState.isArabic(),
icon: AppAssets.arrowRight, child: AnimatedRotation(
width: 25.w, duration: const Duration(milliseconds: 300),
height: 25.h, turns: viewModel.isBottomSheetExpanded ? 0.25 : -0.25,
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrowRight,
width: 25.w,
height: 25.h,
),
),
),
), ),
), ),
), ),
), 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