Compare commits

..

No commits in common. 'dc6b71a4ca84a00e40e6b0a3accd79c78931718e' and '8fad8999d7dfd77a910213d281ccf01de3f5cc15' have entirely different histories.

@ -21,7 +21,7 @@
"mySchedule": "My Schedule", "mySchedule": "My Schedule",
"logout": "Logout", "logout": "Logout",
"respirationRate": "Respiration Rate", "respirationRate": "Respiration Rate",
"bookAppo": "Book Appointment", "bookAppo": "New 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 access to motion detection to count your daily steps.</string> <string>This app requires motion detection access to function properly.</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'] = 3310954; // body['PatientID'] = 1307867;
// 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,23 +40,14 @@ class RequestUtils {
if (zipCode == "0") { if (zipCode == "0") {
request.patientMobileNumberOthers = phoneNumber; request.patientMobileNumberOthers = phoneNumber;
} else { } else {
// Remove any non-numeric characters before parsing request.patientMobileNumber = int.parse(phoneNumber);
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) {
final parsedNationId = int.tryParse(nationId.replaceAll(RegExp(r'[^0-9]'), '')); request.patientIdentificationID = int.parse(nationId);
request.patientIdentificationID = parsedNationId ?? 0;
request.searchType = 1; request.searchType = 1;
request.isHijri = calenderType.toInt; request.isHijri = calenderType.toInt;
request.patientID = patientId; request.patientID = patientId;
@ -65,7 +56,7 @@ class RequestUtils {
request.isDentalAllowedBackend = false; request.isDentalAllowedBackend = false;
} else { } else {
if (fileNo) { if (fileNo) {
request.patientID = patientId ?? (int.tryParse(nationId.replaceAll(RegExp(r'[^0-9]'), '')) ?? 0); request.patientID = patientId ?? int.parse(nationId);
request.patientIdentificationID = request.nationalID; request.patientIdentificationID = request.nationalID;
request.searchType = 2; request.searchType = 2;
} else { } else {
@ -80,15 +71,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]+$');
@ -100,17 +91,8 @@ class RequestUtils {
request.patientMobileNumberOthers = phoneNumber; request.patientMobileNumberOthers = phoneNumber;
request.mobileNo = phoneNumber; request.mobileNo = phoneNumber;
} else { } else {
// Remove any non-numeric characters before parsing request.patientMobileNumber = int.parse(phoneNumber);
final numericPhone = phoneNumber.replaceAll(RegExp(r'[^0-9]'), ''); request.mobileNo = '0$phoneNumber';
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;
@ -124,8 +106,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;
@ -133,8 +115,7 @@ class RequestUtils {
log("nationIdText: ${nationIdText}"); log("nationIdText: ${nationIdText}");
} else { } else {
if (fileNo) { if (fileNo) {
final numericNationId = nationIdText.replaceAll(RegExp(r'[^0-9]'), ''); request.patientID = patientId ?? int.parse(nationIdText);
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
@ -174,17 +155,8 @@ class RequestUtils {
request.patientMobileNumberOthers = mobileNumber; request.patientMobileNumberOthers = mobileNumber;
request.mobileNo = mobileNumber; request.mobileNo = mobileNumber;
} else { } else {
// Remove any non-numeric characters before parsing request.patientMobileNumber = int.parse(mobileNumber);
final numericMobile = mobileNumber.replaceAll(RegExp(r'[^0-9]'), ''); request.mobileNo = '0$mobileNumber';
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;
@ -268,16 +240,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,
@ -294,31 +266,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,
@ -342,9 +314,7 @@ class RequestUtils {
request.sharedPatientId = 0; request.sharedPatientId = 0;
request.sharedPatientIdentificationId = nationalIDorFile; request.sharedPatientIdentificationId = nationalIDorFile;
} else if (loginType == 2) { } else if (loginType == 2) {
// Remove any non-numeric characters before parsing request.sharedPatientId = int.parse(nationalIDorFile);
final numericId = nationalIDorFile.replaceAll(RegExp(r'[^0-9]'), '');
request.sharedPatientId = int.tryParse(numericId) ?? 0;
request.sharedPatientIdentificationId = ''; request.sharedPatientIdentificationId = '';
} }
request.searchType = loginType; request.searchType = loginType;
@ -355,4 +325,4 @@ class RequestUtils {
request.isDentalAllowedBackend = false; request.isDentalAllowedBackend = false;
return request; return request;
} }
} }

@ -676,6 +676,7 @@ 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) {
@ -690,7 +691,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,25 +363,11 @@ 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;
} }
@ -397,7 +383,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;
@ -408,14 +394,8 @@ class MyAppointmentsViewModel extends ChangeNotifier {
isMyAppointmentsLoading = false; isMyAppointmentsLoading = false;
// Auto-switch to Arrived tab if no upcoming appointments // Update filtered list based on current tab
if (patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) { updateListWRTTab(selectedTabIndex);
selectedTabIndex = 1;
updateListWRTTab(1);
} else {
// Update filtered list based on current tab
updateListWRTTab(selectedTabIndex);
}
notifyListeners(); notifyListeners();
if (onSuccess != null) { if (onSuccess != null) {
@ -434,9 +414,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);
@ -873,15 +853,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.clear();
filteredAppointmentList.addAll(patientUpcomingAppointmentsHistoryList); filteredAppointmentList.addAll(patientAppointmentsHistoryList);
break; break;
case 1: case 1:
filteredAppointmentList.clear();
filteredAppointmentList.addAll(patientUpcomingAppointmentsHistoryList);
break;
case 2:
filteredAppointmentList.clear(); filteredAppointmentList.clear();
filteredAppointmentList.addAll(patientArrivedAppointmentsHistoryList); filteredAppointmentList.addAll(patientArrivedAppointmentsHistoryList);
break; break;
@ -907,12 +887,11 @@ 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 == 0) {
sourceList = patientUpcomingAppointmentsHistoryList;
} else if (selectedTabIndex == 1) { } else if (selectedTabIndex == 1) {
sourceList = patientUpcomingAppointmentsHistoryList;
} else if (selectedTabIndex == 2) {
sourceList = patientArrivedAppointmentsHistoryList; sourceList = patientArrivedAppointmentsHistoryList;
} }
// if (isDateFilterSelected) sourceList = filteredAppointmentList; // if (isDateFilterSelected) sourceList = filteredAppointmentList;

@ -191,9 +191,7 @@ 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 {
if (onError != null) { onError!(failure.message);
onError(failure.message);
}
}, },
(apiResponse) async { (apiResponse) async {
if (apiResponse.messageStatus == 2) { if (apiResponse.messageStatus == 2) {

@ -70,7 +70,9 @@ 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;
} }
@ -83,6 +85,7 @@ 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;
} }
@ -94,6 +97,8 @@ 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();
@ -108,14 +113,17 @@ 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();
}, },
); );
@ -125,18 +133,22 @@ 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();
}, },
@ -176,6 +188,10 @@ 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...');
@ -250,10 +266,14 @@ 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;
@ -267,13 +287,9 @@ 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)');
final String successMessage = data is String onSuccess(data);
? 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');
@ -292,6 +308,7 @@ 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;
@ -309,6 +326,7 @@ 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,18 +69,12 @@ 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 && myAppointmentsViewModel.patientAppointmentShareResponseModel != null) { if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != 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; });
});
}
} }
} }
}); });

@ -47,7 +47,8 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
myAppointmentsViewModel.initAppointmentsViewModel(); myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false); myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false);
} }
myAppointmentsViewModel.changeTabToArrived(); // Load full arrived appointments if not already loaded
myAppointmentsViewModel.loadFullArrivedAppointmentsIfNeeded();
}); });
super.initState(); super.initState();
} }
@ -70,9 +71,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.arrived.tr(context: context)), CustomTabBarModel(null, LocaleKeys.completed.tr(context: context)),
], ],
onTabChange: (index) { onTabChange: (index) {
setState(() { setState(() {
@ -81,10 +82,6 @@ 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) {
@ -155,7 +152,7 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
? myAppointmentsVM.patientAppointmentsViewList.length ? myAppointmentsVM.patientAppointmentsViewList.length
: 1, : 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isExpanded = myAppointmentsVM.selectedTabIndex == 0 ? true : expandedIndex == index; final isExpanded = myAppointmentsVM.selectedTabIndex == 1 ? 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,7 +1021,6 @@ 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(false); bookAppointmentsViewModel.setIsNearestAppointmentSelected(true);
if (bookAppointmentsViewModel.isLiveCareSchedule) { if (bookAppointmentsViewModel.isLiveCareSchedule) {
bookAppointmentsViewModel.getLiveCareDoctorsList(); bookAppointmentsViewModel.getLiveCareDoctorsList();
} else { } else {
@ -57,7 +57,8 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
} else if (bookAppointmentsViewModel.isGetDocForHealthCal) { } else if (bookAppointmentsViewModel.isGetDocForHealthCal) {
bookAppointmentsViewModel.getDoctorsListByHealthCal(); bookAppointmentsViewModel.getDoctorsListByHealthCal();
} else { } else {
bookAppointmentsViewModel.getDoctorsList(isNearest: false); bookAppointmentsViewModel.setIsNearestAppointmentSelected(true);
bookAppointmentsViewModel.getDoctorsList(isNearest: true);
} }
} }
}); });

@ -144,9 +144,8 @@ 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))
@ -156,8 +155,7 @@ 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,23 +120,12 @@ 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(); authVM.checkLastLoginStatus(() {
final hasUserQuickLoginData = showQuickLogin(context);
user != null && });
(user.mobileNumber?.isNotEmpty ?? false) &&
(user.patientIdentificationNo?.isNotEmpty ?? false);
if (hasUserQuickLoginData) {
authVM.checkLastLoginStatus(() {
if (mounted) {
showQuickLogin(context);
}
});
}
} }
_scrollController.addListener(() { _scrollController.addListener(() {
@ -183,19 +172,12 @@ 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()); appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr(context: context));
appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr()); appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context));
openLastRating(); openLastRating();
appState.setRatedVisible(true); appState.setRatedVisible(true);
}), }),
onError: (String errorMessage) {
// Silently mark as rated to prevent retry loops
if (mounted) appState.setRatedVisible(true);
},
); );
} }
}, },
@ -1476,7 +1458,6 @@ 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, isCloseButtonVisible: false); }, isFullScreen: false, isAutoDismiss: true);
}, },
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, isCloseButtonVisible: false); }, isFullScreen: false, isAutoDismiss: true);
}, },
onError: (error) { onError: (error) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();

@ -29,19 +29,11 @@ class OrganSelectorPage extends StatefulWidget {
State<OrganSelectorPage> createState() => _OrganSelectorPageState(); State<OrganSelectorPage> createState() => _OrganSelectorPageState();
} }
class _OrganSelectorPageState extends State<OrganSelectorPage> with SingleTickerProviderStateMixin { class _OrganSelectorPageState extends State<OrganSelectorPage> {
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() {
@ -49,52 +41,10 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> with SingleTicker
_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
@ -168,11 +118,7 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> with SingleTicker
Expanded( Expanded(
child: Stack( child: Stack(
children: [ children: [
SafeArea( _buildBodyViewer(viewModel),
top: false,
bottom: false,
child: _buildBodyViewer(viewModel),
),
_buildViewToggleButtons(viewModel), _buildViewToggleButtons(viewModel),
// _buildViewZoomButtons(viewModel), // _buildViewZoomButtons(viewModel),
_buildBottomSheet(viewModel), _buildBottomSheet(viewModel),
@ -185,7 +131,7 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> with SingleTicker
), ),
), ),
// Tutorial overlay // Tutorial overlay
if (_enableTutorialOverlay && _showTutorial) if (_showTutorial)
PinchZoomTutorialOverlay( PinchZoomTutorialOverlay(
onComplete: _onTutorialComplete, onComplete: _onTutorialComplete,
), ),
@ -206,13 +152,7 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> with SingleTicker
height: 24.h, height: 24.h,
), ),
padding: EdgeInsetsDirectional.only(start: 12, end: 12), padding: EdgeInsetsDirectional.only(start: 12, end: 12),
onPressed: () { onPressed: () => Navigator.pop(context),
// 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,
), ),
), ),
@ -235,82 +175,45 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> with SingleTicker
Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) { Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) {
return GestureDetector( return GestureDetector(
onHorizontalDragEnd: !_enableSwipeToFlip onHorizontalDragEnd: (details) {
? null // Swipe left or right to toggle view
: (details) { if (details.primaryVelocity != null) {
// Swipe left or right to toggle view if (details.primaryVelocity! < -200 || details.primaryVelocity! > 200) {
if (details.primaryVelocity != null) { viewModel.toggleView();
if (details.primaryVelocity! < -200 || details.primaryVelocity! > 200) {
viewModel.toggleView();
}
}
},
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( child: Padding(
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h), padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h),
child: buildInteractiveBody(useExpandedZoom: false), child: AnimatedSwitcher(
); duration: const Duration(milliseconds: 600),
} transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation),
switchInCurve: Curves.easeInOut,
return SingleChildScrollView( switchOutCurve: Curves.easeInOut,
physics: isExpanded && !isUserZoomedIn ? const ClampingScrollPhysics() : const NeverScrollableScrollPhysics(), child: Builder(
child: ConstrainedBox( key: ValueKey<BodyView>(viewModel.currentView),
constraints: BoxConstraints( builder: (context) {
minHeight: constraints.maxHeight + extraBottomInset, // Detect female gender from viewModel; allow Arabic value as fallback
), final bool isFemale =
child: Padding( (viewModel.selectedGender != null && (viewModel.selectedGender!.toLowerCase() == 'female' || viewModel.selectedGender == 'أنثى'));
padding: EdgeInsets.only(
bottom: extraBottomInset, final String bodyAsset = viewModel.currentView == BodyView.front
top: 24.h, ? (isFemale ? AppAssets.fullBodyFrontFemale : AppAssets.fullBodyFrontMale)
), : (isFemale ? AppAssets.fullBodyBackFemale : AppAssets.fullBodyBackMale);
child: Padding(
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h), return InteractiveBodyWidget(
child: buildInteractiveBody(useExpandedZoom: true), bodyImageAsset: bodyAsset,
), organs: viewModel.currentOrgans,
), selectedOrganIds: viewModel.selectedOrganIds,
), onOrganTap: viewModel.toggleOrganSelection,
); isBodyHidden: viewModel.isBodyHidden,
}, tooltipOrganId: viewModel.tooltipOrganId,
isArabic: _appState.isArabic(),
zoomScale: viewModel.currentZoomScale,
);
},
),
),
), ),
); );
} }
@ -505,73 +408,37 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> with SingleTicker
} }
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: SizedBox( child: Container(
width: 70.w, width: 70.w,
height: 70.h, height: 70.h,
child: Stack( alignment: Alignment.center,
clipBehavior: Clip.none, child: Container(
children: [ width: 48.w,
Align( height: 48.h,
alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
child: Container( color: AppColors.whiteColor,
width: 48.w, borderRadius: 11.r,
height: 48.h, ),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( child: Center(
color: AppColors.whiteColor, child: Transform.flip(
borderRadius: 11.r, flipX: _appState.isArabic(),
), child: AnimatedRotation(
child: Center( duration: const Duration(milliseconds: 300),
child: Transform.flip( turns: viewModel.isBottomSheetExpanded ? 0.25 : -0.25,
flipX: _appState.isArabic(), child: Utils.buildSvgWithAssets(
child: AnimatedRotation( icon: AppAssets.arrowRight,
duration: const Duration(milliseconds: 300), width: 25.w,
turns: viewModel.isBottomSheetExpanded ? 0.25 : -0.25, height: 25.h,
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,4 +1,3 @@
import 'dart:developer';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -8,6 +7,7 @@ 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,12 +63,9 @@ 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());
@ -80,16 +77,17 @@ 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) {
// Keep programmatic zoom centered to avoid drifting the body off-screen. // Get current translation
final newTransform = Matrix4.identity()..scaleByDouble(scale, scale, 1.0, 1.0); final currentTransform = _transformationController.value;
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;
} }
@ -105,12 +103,8 @@ class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
return Center( return Center(
child: InteractiveViewer( child: InteractiveViewer(
transformationController: _transformationController, transformationController: _transformationController,
alignment: Alignment.center, minScale: 0.5,
panEnabled: true, maxScale: 4.0,
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!,
@ -141,7 +135,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 = 18.0; final dotSize = 16.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,8 +23,7 @@ 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( Future<void> showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed});
{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});
@ -120,15 +119,21 @@ class DialogServiceImp implements DialogService {
} }
@override @override
Future<void> showCommonBottomSheetWithoutH( Future<void> showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) async {
{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, message: message, okLabel: okLabel, cancelLabel: cancelLabel, onOkPressed: onOkPressed, onCancelPressed: onCancelPressed, isConfirmButton: isConfirmButton), context: context,
message: message,
okLabel: okLabel,
cancelLabel: cancelLabel,
onOkPressed: onOkPressed,
onCancelPressed: onCancelPressed,
isConfirmButton: isConfirmButton
),
callBackFunc: () {}, callBackFunc: () {},
); );
} }
@ -235,8 +240,7 @@ class DialogServiceImp implements DialogService {
} }
} }
Widget exceptionBottomSheetWidget( Widget exceptionBottomSheetWidget({required BuildContext context, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) {
{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,4 +1,3 @@
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';
@ -16,7 +15,10 @@ class AppTheme {
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,
brightness: Brightness.light, brightness: Brightness.light,
pageTransitionsTheme: const PageTransitionsTheme( pageTransitionsTheme: const PageTransitionsTheme(
builders: {TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder()}, builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder()
},
), ),
hintColor: Colors.grey[400], hintColor: Colors.grey[400],
disabledColor: Colors.grey[300], disabledColor: Colors.grey[300],
@ -49,7 +51,10 @@ class AppTheme {
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,
brightness: Brightness.dark, brightness: Brightness.dark,
pageTransitionsTheme: const PageTransitionsTheme( pageTransitionsTheme: const PageTransitionsTheme(
builders: {TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder()}, builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder()
},
), ),
hintColor: Colors.grey[600], hintColor: Colors.grey[600],
disabledColor: Colors.grey[700], disabledColor: Colors.grey[700],
@ -58,7 +63,8 @@ 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(backgroundColor: Colors.black.withOpacity(0)), bottomSheetTheme: BottomSheetThemeData(
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,7 +257,8 @@ 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), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width //MediaQuery.of(context).size.width, // Full width
),
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
showDragHandle: false, showDragHandle: false,

Loading…
Cancel
Save