Input Field Error Handling

dev_aamir
Aamir CSol 18 hours ago
parent f47809777d
commit 1911f7ce64

@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts { class ApiConsts {
static const maxSmallScreen = 660; static const maxSmallScreen = 660;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat;
// static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT

@ -95,6 +95,79 @@ class AuthenticationViewModel extends ChangeNotifier {
final ValueNotifier<bool> otpScreenNotifier = ValueNotifier<bool>(false); final ValueNotifier<bool> otpScreenNotifier = ValueNotifier<bool>(false);
int patientShareRequestID = 0; int patientShareRequestID = 0;
//================== Validation Error States ==================
// Login screen errors
String? _nationalIdError;
String? get nationalIdError => _nationalIdError;
// Phone number errors (used in multiple screens)
String? _phoneNumberError;
String? get phoneNumberError => _phoneNumberError;
// Registration screen errors
String? _nameError;
String? get nameError => _nameError;
String? _emailError;
String? get emailError => _emailError;
String? _dobError;
String? get dobError => _dobError;
// Check if registration form has any errors (for container border)
bool get hasRegistrationFormError =>
_nationalIdError != null || _dobError != null;
// Check if ID and phone have errors (for family file container)
bool get hasIdAndPhoneError =>
_nationalIdError != null || _phoneNumberError != null;
// Additional field errors for UAE registration step 2
String? _genderError;
String? _maritalStatusError;
String? _countryError;
// Getters for step 2 field errors (nameError and emailError already exist above)
String? get genderError => _genderError;
String? get maritalStatusError => _maritalStatusError;
String? get countryError => _countryError;
// Check if registration step 2 form has any errors (for container border)
bool get hasRegistrationStep2FormError =>
_nameError != null ||
_genderError != null ||
_maritalStatusError != null ||
_countryError != null;
// Clear all step 2 field errors
void clearAllStep2FieldErrors() {
_nameError = null;
_genderError = null;
_maritalStatusError = null;
_countryError = null;
_emailError = null;
notifyListeners();
}
// Clear gender error
void clearGenderError() {
_genderError = null;
notifyListeners();
}
// Clear marital status error
void clearMaritalStatusError() {
_maritalStatusError = null;
notifyListeners();
}
// Clear country error
void clearCountryError() {
_countryError = null;
notifyListeners();
}
//================== //==================
String errorMsg = ''; String errorMsg = '';
@ -138,8 +211,342 @@ class AuthenticationViewModel extends ChangeNotifier {
_appState.setNHICUserData = CheckUserStatusResponseNHIC(); _appState.setNHICUserData = CheckUserStatusResponseNHIC();
getIt.get<SymptomsCheckerViewModel>().setSelectedHeight(0); getIt.get<SymptomsCheckerViewModel>().setSelectedHeight(0);
getIt.get<SymptomsCheckerViewModel>().setSelectedWeight(0); getIt.get<SymptomsCheckerViewModel>().setSelectedWeight(0);
// Clear all validation errors
clearAllErrors();
}
//==================== Validation Methods ====================
/// Clear all validation errors
void clearAllErrors() {
_nationalIdError = null;
_phoneNumberError = null;
_nameError = null;
_emailError = null;
_dobError = null;
notifyListeners();
}
/// Clear national ID error
void clearNationalIdError() {
_nationalIdError = null;
notifyListeners();
}
/// Clear phone number error
void clearPhoneNumberError() {
_phoneNumberError = null;
notifyListeners();
}
/// Clear name error
void clearNameError() {
_nameError = null;
notifyListeners();
}
/// Clear email error
void clearEmailError() {
_emailError = null;
notifyListeners();
}
/// Clear DOB error
void clearDobError() {
_dobError = null;
notifyListeners();
}
/// Validate National ID for login/registration
/// Returns true if valid, false otherwise
/// Only shows red border error, no dialog popups
bool validateNationalId() {
// Clear previous error
_nationalIdError = null;
// Check if field is empty
if (nationalIdController.text.isEmpty) {
_nationalIdError = LocaleKeys.pleaseEnterAnationalID.tr();
notifyListeners();
return false;
}
// Check if it's 10 digits and valid Saudi ID
String cleanedId = nationalIdController.text.replaceAll(RegExp(r'[^0-9]'), '');
if (cleanedId.length == 10) {
if (!Utils.isSAUDIIDValid(cleanedId)) {
_nationalIdError = LocaleKeys.enterValidNationalId.tr();
notifyListeners();
return false;
}
}
// Validate based on selected country
if (selectedCountrySignup == CountryEnum.saudiArabia) {
if (!ValidationUtils.validateIqama(nationalIdController.text)) {
_nationalIdError = LocaleKeys.pleaseEnterAValidIqamaID.tr();
notifyListeners();
return false;
}
} else if (selectedCountrySignup == CountryEnum.unitedArabEmirates) {
if (!ValidationUtils.validateUaeNationalId(nationalIdController.text)) {
_nationalIdError = LocaleKeys.pleaseEnterAValidNationalID.tr();
notifyListeners();
return false;
}
}
// Clear error on success
_nationalIdError = null;
notifyListeners();
return true;
}
/// Validate phone number
/// Returns true if valid, false otherwise
/// Only shows red border error, no dialog popups
bool validatePhoneNumber() {
// Clear previous error
_phoneNumberError = null;
// Check if phone is empty
if (phoneNumberController.text.isEmpty) {
_phoneNumberError = LocaleKeys.enterValidPhoneNumber.tr();
notifyListeners();
return false;
}
// Check if "Others" country is selected and phone number starts with restricted codes
if (selectedCountrySignup == CountryEnum.others) {
if (phoneNumberController.text.startsWith('00966') || phoneNumberController.text.startsWith('00971')) {
_phoneNumberError = LocaleKeys.cannotEnterSaudiOrUAENumber.tr();
notifyListeners();
return false;
}
}
// Clear error on success
_phoneNumberError = null;
notifyListeners();
return true;
}
/// Validate National ID and Phone Number together
/// Returns true if both are valid, false otherwise
/// Only shows red border errors, no dialog popups
/// Uses PROGRESSIVE VALIDATION - shows errors step by step
bool validateIdAndPhone() {
// Clear all previous errors first
_nationalIdError = null;
_phoneNumberError = null;
// Step 1: Validate National ID empty first (highest priority)
if (nationalIdController.text.isEmpty) {
_nationalIdError = LocaleKeys.pleaseEnterAnationalID.tr();
notifyListeners();
return false; // Stop here, don't check phone yet
}
// Step 2: Validate National ID format
String cleanedId = nationalIdController.text.replaceAll(RegExp(r'[^0-9]'), '');
if (cleanedId.length == 10) {
if (!Utils.isSAUDIIDValid(cleanedId)) {
_nationalIdError = LocaleKeys.enterValidNationalId.tr();
notifyListeners();
return false; // Stop here
}
}
// Step 3: Validate National ID based on country
if (selectedCountrySignup == CountryEnum.saudiArabia) {
if (!ValidationUtils.validateIqama(nationalIdController.text)) {
_nationalIdError = LocaleKeys.pleaseEnterAValidIqamaID.tr();
notifyListeners();
return false; // Stop here
}
} else if (selectedCountrySignup == CountryEnum.unitedArabEmirates) {
if (!ValidationUtils.validateUaeNationalId(nationalIdController.text)) {
_nationalIdError = LocaleKeys.pleaseEnterAValidNationalID.tr();
notifyListeners();
return false; // Stop here
}
} }
// Step 4: ONLY validate phone if National ID is fully valid
if (phoneNumberController.text.isEmpty) {
_phoneNumberError = LocaleKeys.enterValidPhoneNumber.tr();
notifyListeners();
return false; // Stop here
}
// Step 5: Validate phone number format based on country
if (selectedCountrySignup == CountryEnum.others) {
if (phoneNumberController.text.startsWith('00966') || phoneNumberController.text.startsWith('00971')) {
_phoneNumberError = LocaleKeys.cannotEnterSaudiOrUAENumber.tr();
notifyListeners();
return false;
}
}
// All validations passed
notifyListeners();
return true;
}
/// Validate registration form (National ID, DOB, Terms)
/// Returns true if all valid, false otherwise
/// Only shows red border errors, no dialog popups
/// Uses PROGRESSIVE VALIDATION - shows errors step by step
bool validateRegistrationForm() {
// Clear all previous errors first
_nationalIdError = null;
_dobError = null;
// Step 1: Validate National ID first (highest priority)
if (nationalIdController.text.isEmpty) {
_nationalIdError = LocaleKeys.pleaseEnterAnationalID.tr();
notifyListeners();
return false; // Stop here, don't check other fields
}
// Step 2: Validate National ID format
String cleanedId = nationalIdController.text.replaceAll(RegExp(r'[^0-9]'), '');
if (cleanedId.length == 10) {
if (!Utils.isSAUDIIDValid(cleanedId)) {
_nationalIdError = LocaleKeys.enterValidNationalId.tr();
notifyListeners();
return false; // Stop here
}
}
// Step 3: Validate based on selected country
if (selectedCountrySignup == CountryEnum.saudiArabia) {
if (!ValidationUtils.validateIqama(nationalIdController.text)) {
_nationalIdError = LocaleKeys.pleaseEnterAValidIqamaID.tr();
notifyListeners();
return false; // Stop here
}
} else if (selectedCountrySignup == CountryEnum.unitedArabEmirates) {
if (!ValidationUtils.validateUaeNationalId(nationalIdController.text)) {
_nationalIdError = LocaleKeys.pleaseEnterAValidNationalID.tr();
notifyListeners();
return false; // Stop here
}
}
// Step 4: Only validate DOB if National ID is valid
if (dobController.text.isEmpty || dob == null || dob!.isEmpty) {
_dobError = LocaleKeys.pleaseEnterAValidDateOfBirth.tr();
notifyListeners();
return false; // Stop here
}
// Step 5: Only validate Terms if both National ID and DOB are valid
// Show dialog popup for Terms (old behavior)
if (!isTermsAccepted) {
_dialogService.showExceptionBottomSheet(
message: LocaleKeys.pleaseAcceptTermsConditions.tr(),
onOkPressed: () {
_navigationService.pop();
},
);
notifyListeners();
return false; // Stop here
}
// All validations passed
notifyListeners();
return true;
}
//==================== End Validation Methods ====================
/// Validate registration step 2 form (UAE users: name, gender, marital status, country)
/// Returns true if all valid, false otherwise
/// Shows only the FIRST error encountered - progressive validation
bool validateRegistrationStep2Form() {
final isArabic = _appState.isArabic();
// Clear all previous errors first
_nameError = null;
_genderError = null;
_maritalStatusError = null;
_countryError = null;
// Step 1: Validate name (only for UAE users)
if (isUserFromUAE()) {
if (nameController.text.trim().isEmpty) {
_nameError = isArabic ? "الرجاء إدخال الاسم الكامل" : "Please enter full name";
notifyListeners();
return false; // Stop here
}
}
// Step 2: Validate gender (only for UAE users)
if (isUserFromUAE()) {
if (genderType == null) {
_genderError = isArabic ? "الرجاء اختيار الجنس" : "Please select gender";
notifyListeners();
return false; // Stop here
}
}
// Step 3: Validate marital status (only for UAE users)
if (isUserFromUAE()) {
if (maritalStatus == null) {
_maritalStatusError = isArabic ? "الرجاء اختيار الحالة الاجتماعية" : "Please select marital status";
notifyListeners();
return false; // Stop here
}
}
// Step 4: Validate country (only for UAE users)
if (isUserFromUAE()) {
if (pickedCountryByUAEUser == null) {
_countryError = isArabic ? "الرجاء اختيار البلد" : "Please select country";
notifyListeners();
return false; // Stop here
}
}
// All validations passed
notifyListeners();
return true;
}
/// Validate email in the bottom sheet
/// Returns true if valid, false otherwise
bool validateEmail() {
final isArabic = _appState.isArabic();
// Clear previous error
_emailError = null;
// Step 1: Check if email is empty
if (emailController.text.trim().isEmpty) {
_emailError = isArabic ? "الرجاء إدخال البريد الإلكتروني" : "Please enter email";
notifyListeners();
return false;
}
// Step 2: Validate email format
final bool emailIsValid = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}$").hasMatch(emailController.text.trim());
if (!emailIsValid) {
_emailError = isArabic ? "الرجاء إدخال تنسيق بريد إلكتروني صالح" : "Please enter a valid email format";
notifyListeners();
return false;
}
// Email is valid
notifyListeners();
return true;
}
//==================== End Validation Methods ====================
void onCountryChange(CountryEnum country) { void onCountryChange(CountryEnum country) {
selectedCountrySignup = country; selectedCountrySignup = country;
notifyListeners(); notifyListeners();
@ -173,11 +580,13 @@ class AuthenticationViewModel extends ChangeNotifier {
void onMaritalStatusChange(String? status) { void onMaritalStatusChange(String? status) {
maritalStatus = MaritalStatusTypeExtension.fromType(status)!; maritalStatus = MaritalStatusTypeExtension.fromType(status)!;
clearMaritalStatusError();
notifyListeners(); notifyListeners();
} }
void onGenderChange(String? status) { void onGenderChange(String? status) {
genderType = GenderTypeExtension.fromType(status)!; genderType = GenderTypeExtension.fromType(status)!;
clearGenderError();
notifyListeners(); notifyListeners();
} }
@ -186,7 +595,8 @@ class AuthenticationViewModel extends ChangeNotifier {
} }
void onUAEUserCountrySelection(String? value) { void onUAEUserCountrySelection(String? value) {
pickedCountryByUAEUser = countriesList!.firstWhere((element) => element.name == value); pickedCountryByUAEUser = countriesList!.firstWhere((element) => element.name == value || element.nameN == value);
clearCountryError();
notifyListeners(); notifyListeners();
} }

@ -52,6 +52,62 @@ class ContactUsViewModel extends ChangeNotifier {
FeedbackType selectedFeedbackType = FeedbackType(id: 5, nameEN: "Not classified", nameAR: 'غير محدد'); FeedbackType selectedFeedbackType = FeedbackType(id: 5, nameEN: "Not classified", nameAR: 'غير محدد');
// ==================== FEEDBACK ERROR TRACKING ====================
// Individual field errors
String? _subjectError;
String? _messageError;
// Getters for field errors
String? get subjectError => _subjectError;
String? get messageError => _messageError;
// Clear subject error
void clearSubjectError() {
_subjectError = null;
notifyListeners();
}
// Clear message error
void clearMessageError() {
_messageError = null;
notifyListeners();
}
// Clear all feedback errors
void clearAllFeedbackErrors() {
_subjectError = null;
_messageError = null;
notifyListeners();
}
// Validate feedback form - shows only first error
bool validateFeedbackForm(String subject, String message) {
final isArabic = appState.isArabic();
// Clear previous errors
_subjectError = null;
_messageError = null;
// Check if subject is empty
if (subject.trim().isEmpty) {
_subjectError = isArabic ? "الرجاء إدخال الموضوع" : "Please enter subject";
notifyListeners();
return false;
}
// Check if message is empty
if (message.trim().isEmpty) {
_messageError = isArabic ? "الرجاء إدخال الرسالة" : "Please enter message";
notifyListeners();
return false;
}
// All validations passed
notifyListeners();
return true;
}
ContactUsViewModel({required this.contactUsRepo, required this.errorHandlerService, required this.appState}); ContactUsViewModel({required this.contactUsRepo, required this.errorHandlerService, required this.appState});
initContactUsViewModel() async { initContactUsViewModel() async {
@ -95,7 +151,12 @@ class ContactUsViewModel extends ChangeNotifier {
setIsSendFeedbackTabSelected(bool isSelected) { setIsSendFeedbackTabSelected(bool isSelected) {
isSendFeedbackTabSelected = isSelected; isSendFeedbackTabSelected = isSelected;
if (!isSelected) { if (isSelected) {
// Clear errors when switching to send tab
clearAllFeedbackErrors();
} else {
// Clear errors when switching away from send tab
clearAllFeedbackErrors();
getStatusForCOC(identificationNo: appState.getAuthenticatedUser()!.patientIdentificationNo!, mobileNo: "966${Utils.getPhoneNumberWithoutZero(appState.getAuthenticatedUser()!.mobileNumber!)}"); getStatusForCOC(identificationNo: appState.getAuthenticatedUser()!.patientIdentificationNo!, mobileNo: "966${Utils.getPhoneNumberWithoutZero(appState.getAuthenticatedUser()!.mobileNumber!)}");
} }
notifyListeners(); notifyListeners();

@ -22,6 +22,61 @@ class DoctorFilterViewModel extends ChangeNotifier{
String? selectedClinicForFilters; String? selectedClinicForFilters;
bool applyFilters = false; bool applyFilters = false;
// ==================== FILTER ERROR TRACKING ====================
// Individual field errors
String? _hospitalError;
String? _clinicError;
// Getters for field errors
String? get hospitalError => _hospitalError;
String? get clinicError => _clinicError;
// Clear hospital error
void clearHospitalError() {
_hospitalError = null;
notifyListeners();
}
// Clear clinic error
void clearClinicError() {
_clinicError = null;
notifyListeners();
}
// Clear all filter errors
void clearAllFilterErrors() {
_hospitalError = null;
_clinicError = null;
notifyListeners();
}
// Validate filters - shows only first error
bool validateFilters() {
final isArabic = appState.isArabic();
// Clear previous errors
_hospitalError = null;
_clinicError = null;
// Check if at least one hospital is selected
if (selectedHospitalForFilters == null) {
_hospitalError = isArabic ? "الرجاء اختيار المستشفى" : "Please select a hospital";
notifyListeners();
return false;
}
// Check if at least one clinic is selected
if (selectedClinicForFilters == null || selectedClinicForFilters!.isEmpty) {
_clinicError = isArabic ? "الرجاء اختيار العيادة" : "Please select a clinic";
notifyListeners();
return false;
}
// All validations passed
notifyListeners();
return true;
}
void clearSearchFilters() { void clearSearchFilters() {
searchedRegionList.clear(); searchedRegionList.clear();
searchedHospitalList.clear(); searchedHospitalList.clear();
@ -36,6 +91,7 @@ class DoctorFilterViewModel extends ChangeNotifier{
selectedHospitalForFilters = null; selectedHospitalForFilters = null;
selectedRegionForFilters = []; selectedRegionForFilters = [];
applyFilters = false; applyFilters = false;
clearAllFilterErrors();
notifyListeners(); notifyListeners();
} }
@ -76,6 +132,7 @@ class DoctorFilterViewModel extends ChangeNotifier{
void setSelectedHospital(PatientDoctorAppointmentList? hospital) { void setSelectedHospital(PatientDoctorAppointmentList? hospital) {
selectedHospitalForFilters = hospital; selectedHospitalForFilters = hospital;
clearHospitalError();
notifyListeners(); notifyListeners();
} }
@ -91,6 +148,7 @@ class DoctorFilterViewModel extends ChangeNotifier{
void setSelectedClinicForFilter(String? clinic) { void setSelectedClinicForFilter(String? clinic) {
selectedClinicForFilters = clinic; selectedClinicForFilters = clinic;
clearClinicError();
notifyListeners(); notifyListeners();
} }

@ -33,6 +33,69 @@ class HabibWalletViewModel extends ChangeNotifier {
List<PatientAdvanceBalanceResponseModel> habibWalletBalanceList = []; List<PatientAdvanceBalanceResponseModel> habibWalletBalanceList = [];
// ==================== RECHARGE VALIDATION ERROR TRACKING ====================
// Individual field errors
String? _amountError;
String? _hospitalError;
// Getters for field errors
String? get amountError => _amountError;
String? get hospitalError => _hospitalError;
// Clear amount error
void clearAmountError() {
_amountError = null;
notifyListeners();
}
// Clear hospital error
void clearHospitalError() {
_hospitalError = null;
notifyListeners();
}
// Clear all recharge errors
void clearAllRechargeErrors() {
_amountError = null;
_hospitalError = null;
notifyListeners();
}
// Validate recharge form - shows only first error
bool validateRechargeForm(String amount) {
// Clear previous errors
_amountError = null;
_hospitalError = null;
// Check if amount is empty
if (amount.trim().isEmpty) {
_amountError = "Please enter amount";
notifyListeners();
return false;
}
// Remove commas and validate amount
String cleanAmount = amount.replaceAll(',', '');
final parsedAmount = num.tryParse(cleanAmount);
if (parsedAmount == null || parsedAmount <= 0) {
_amountError = "Please enter a valid amount";
notifyListeners();
return false;
}
// Check if hospital is selected
if (selectedHospital == null) {
_hospitalError = "Please select a hospital";
notifyListeners();
return false;
}
// All validations passed
notifyListeners();
return true;
}
HabibWalletViewModel({required this.habibWalletRepo, required this.errorHandlerService}); HabibWalletViewModel({required this.habibWalletRepo, required this.errorHandlerService});
initHabibWalletProvider() { initHabibWalletProvider() {
@ -57,6 +120,7 @@ class HabibWalletViewModel extends ChangeNotifier {
setSelectedHospital(HospitalsModel hospital) { setSelectedHospital(HospitalsModel hospital) {
selectedHospital = hospital; selectedHospital = hospital;
clearHospitalError();
notifyListeners(); notifyListeners();
} }

@ -34,6 +34,102 @@ class HealthTrackersViewModel extends ChangeNotifier {
String? get errorMessage => _errorMessage; String? get errorMessage => _errorMessage;
// ==================== FORM ERROR TRACKING ====================
// Individual field errors
String? _bloodSugarError;
String? _bloodSugarMeasureTimeError;
String? _dateError;
String? _timeError;
String? _weightError;
String? _systolicError;
String? _diastolicError;
String? _measuredArmError;
// Getters for field errors
String? get bloodSugarError => _bloodSugarError;
String? get bloodSugarMeasureTimeError => _bloodSugarMeasureTimeError;
String? get dateError => _dateError;
String? get timeError => _timeError;
String? get weightError => _weightError;
String? get systolicError => _systolicError;
String? get diastolicError => _diastolicError;
String? get measuredArmError => _measuredArmError;
// Check if blood sugar form has any errors (for container border)
bool get hasBloodSugarFormError =>
_bloodSugarError != null ||
_bloodSugarMeasureTimeError != null ||
_dateError != null ||
_timeError != null;
// Check if weight form has any errors (for container border)
bool get hasWeightFormError =>
_weightError != null ||
_dateError != null ||
_timeError != null;
// Check if blood pressure form has any errors (for container border)
bool get hasBloodPressureFormError =>
_systolicError != null ||
_diastolicError != null ||
_measuredArmError != null ||
_dateError != null ||
_timeError != null;
// Clear all field errors
void clearAllFieldErrors() {
_bloodSugarError = null;
_bloodSugarMeasureTimeError = null;
_dateError = null;
_timeError = null;
_weightError = null;
_systolicError = null;
_diastolicError = null;
_measuredArmError = null;
notifyListeners();
}
// Clear individual field errors
void clearBloodSugarError() {
_bloodSugarError = null;
notifyListeners();
}
void clearBloodSugarMeasureTimeError() {
_bloodSugarMeasureTimeError = null;
notifyListeners();
}
void clearDateError() {
_dateError = null;
notifyListeners();
}
void clearTimeError() {
_timeError = null;
notifyListeners();
}
void clearWeightError() {
_weightError = null;
notifyListeners();
}
void clearSystolicError() {
_systolicError = null;
notifyListeners();
}
void clearDiastolicError() {
_diastolicError = null;
notifyListeners();
}
void clearMeasuredArmError() {
_measuredArmError = null;
notifyListeners();
}
final List<String> durationFiltersEn = ["Week", "Month", "Year"]; final List<String> durationFiltersEn = ["Week", "Month", "Year"];
final List<String> durationFiltersAr = ["أسبوع", "شهر", "سنة"]; final List<String> durationFiltersAr = ["أسبوع", "شهر", "سنة"];
@ -1017,7 +1113,62 @@ class HealthTrackersViewModel extends ChangeNotifier {
} }
} }
// Validation method // Validation method - returns true if valid, false if errors exist
// Shows only the first error encountered
bool validateBloodSugarEntry(String dateTime) {
final isArabic = getIt.get<AppState>().isArabic();
// Clear all previous errors
_bloodSugarError = null;
_bloodSugarMeasureTimeError = null;
_dateError = null;
_timeError = null;
// Validate blood sugar value - stop at first error
if (bloodSugarController.text.trim().isEmpty) {
_bloodSugarError = isArabic ? "الرجاء إدخال مستوى سكر الدم" : "Please enter blood sugar value";
notifyListeners();
return false;
}
final bloodSugarValue = double.tryParse(bloodSugarController.text.trim());
if (bloodSugarValue == null) {
_bloodSugarError = isArabic ? "الرجاء إدخال رقم صحيح" : "Please enter a valid number";
notifyListeners();
return false;
}
if (bloodSugarValue <= 0) {
_bloodSugarError = isArabic ? "يجب أن يكون مستوى سكر الدم أكبر من 0" : "Blood sugar value must be greater than 0";
notifyListeners();
return false;
}
if (bloodSugarValue > 1000) {
_bloodSugarError = isArabic ? "مستوى سكر الدم يبدو مرتفع جداً. يرجى التحقق وإدخالها مرة أخرى" : "Blood sugar value seems too high. Please check and enter again";
notifyListeners();
return false;
}
// Validate date time
if (dateTime.trim().isEmpty) {
_dateError = isArabic ? "الرجاء اختيار التاريخ والوقت" : "Please select date and time";
notifyListeners();
return false;
}
// Validate measure time
if (_selectedBloodSugarMeasureTime.isEmpty) {
_bloodSugarMeasureTimeError = isArabic ? "الرجاء اختيار وقت القياس" : "Please select when the measurement was taken";
notifyListeners();
return false;
}
notifyListeners();
return true;
}
// Old validation method kept for reference
String? _validateBloodSugarEntry(String dateTime) { String? _validateBloodSugarEntry(String dateTime) {
final isArabic = getIt.get<AppState>().isArabic(); final isArabic = getIt.get<AppState>().isArabic();
@ -1111,7 +1262,44 @@ class HealthTrackersViewModel extends ChangeNotifier {
// ==================== WEIGHT ENTRY METHODS ==================== // ==================== WEIGHT ENTRY METHODS ====================
// Validate weight entry before saving // Validate weight entry before saving - returns true if valid, false if errors exist
// Shows only the first error encountered
bool validateWeightEntry(String dateTime) {
final isArabic = getIt.get<AppState>().isArabic();
// Clear all previous errors
_weightError = null;
_dateError = null;
_timeError = null;
// Validate weight value - stop at first error
final weightValue = weightController.text.trim();
if (weightValue.isEmpty) {
_weightError = isArabic ? "الرجاء إدخال قيمة الوزن" : "Please enter weight value";
notifyListeners();
return false;
}
// Check if it's a valid number
final parsedValue = double.tryParse(weightValue);
if (parsedValue == null || parsedValue <= 0) {
_weightError = isArabic ? "الرجاء إدخال قيمة وزن صحيحة" : "Please enter a valid weight value";
notifyListeners();
return false;
}
// Validate date time
if (dateTime.trim().isEmpty) {
_dateError = isArabic ? "الرجاء تحديد التاريخ والوقت" : "Please select date and time";
notifyListeners();
return false;
}
notifyListeners();
return true;
}
// Old validation method kept for reference
String? _validateWeightEntry(String dateTime) { String? _validateWeightEntry(String dateTime) {
final isArabic = getIt.get<AppState>().isArabic(); final isArabic = getIt.get<AppState>().isArabic();
@ -1190,7 +1378,67 @@ class HealthTrackersViewModel extends ChangeNotifier {
// ==================== BLOOD PRESSURE ENTRY METHODS ==================== // ==================== BLOOD PRESSURE ENTRY METHODS ====================
// Validate blood pressure entry before saving // Validate blood pressure entry before saving - returns true if valid, false if errors exist
// Shows only the first error encountered
bool validateBloodPressureEntry(String dateTime) {
final isArabic = getIt.get<AppState>().isArabic();
// Clear all previous errors
_systolicError = null;
_diastolicError = null;
_measuredArmError = null;
_dateError = null;
_timeError = null;
// Validate systolic value - stop at first error
final systolicValue = systolicController.text.trim();
if (systolicValue.isEmpty) {
_systolicError = isArabic ? "الرجاء إدخال القيمة الانقباضية" : "Please enter systolic value";
notifyListeners();
return false;
}
final parsedSystolic = int.tryParse(systolicValue);
if (parsedSystolic == null || parsedSystolic <= 0) {
_systolicError = isArabic ? "الرجاء إدخال قيمة صحيحة للضغط الانقباضي" : "Please enter a valid systolic value";
notifyListeners();
return false;
}
// Validate diastolic value
final diastolicValue = diastolicController.text.trim();
if (diastolicValue.isEmpty) {
_diastolicError = isArabic ? "الرجاء إدخال قيمة الضغط الانبساطي" : "Please enter diastolic value";
notifyListeners();
return false;
}
final parsedDiastolic = int.tryParse(diastolicValue);
if (parsedDiastolic == null || parsedDiastolic <= 0) {
_diastolicError = isArabic ? "الرجاء إدخال قيمة صحيحة للضغط الانبساطي" : "Please enter a valid diastolic value";
notifyListeners();
return false;
}
// Validate arm selection
if (_selectedMeasuredArm.isEmpty) {
_measuredArmError = isArabic ? "الرجاء اختيار الذراع المقاسة" : "Please select measured arm";
notifyListeners();
return false;
}
// Validate date time
if (dateTime.trim().isEmpty) {
_dateError = isArabic ? "الرجاء تحديد التاريخ والوقت" : "Please select date and time";
notifyListeners();
return false;
}
notifyListeners();
return true;
}
// Old validation method kept for reference
String? _validateBloodPressureEntry(String dateTime) { String? _validateBloodPressureEntry(String dateTime) {
final isArabic = getIt.get<AppState>().isArabic(); final isArabic = getIt.get<AppState>().isArabic();

@ -49,6 +49,96 @@ class ProfileSettingsViewModel extends ChangeNotifier {
String? profileImageData; String? profileImageData;
String? profileImageError; String? profileImageError;
// ==================== EMAIL UPDATE ERROR TRACKING ====================
// Individual field error for email
String? _emailError;
// Getter for email error
String? get emailError => _emailError;
// Clear email error
void clearEmailError() {
_emailError = null;
notifyListeners();
}
// Validate email before updating
bool validateEmail(String email) {
final appState = GetIt.instance.get<AppState>();
final isArabic = appState.isArabic();
// Clear previous error
_emailError = null;
// Check if email is empty
if (email.trim().isEmpty) {
_emailError = isArabic ? "الرجاء إدخال البريد الإلكتروني" : "Please enter email address";
notifyListeners();
return false;
}
// Validate email format
final bool emailIsValid = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9\-]+\.[a-zA-Z]{2,}$").hasMatch(email.trim());
if (!emailIsValid) {
_emailError = isArabic ? "الرجاء إدخال تنسيق بريد إلكتروني صالح" : "Please enter a valid email format";
notifyListeners();
return false;
}
// Email is valid
notifyListeners();
return true;
}
// ==================== EMERGENCY CONTACT ERROR TRACKING ====================
// Individual field error for emergency contact
String? _emergencyContactError;
// Getter for emergency contact error
String? get emergencyContactError => _emergencyContactError;
// Clear emergency contact error
void clearEmergencyContactError() {
_emergencyContactError = null;
notifyListeners();
}
// Validate emergency contact number before updating
bool validateEmergencyContact(String contactNumber) {
final appState = GetIt.instance.get<AppState>();
final isArabic = appState.isArabic();
// Clear previous error
_emergencyContactError = null;
// Check if contact number is empty
if (contactNumber.trim().isEmpty) {
_emergencyContactError = isArabic ? "الرجاء إدخال رقم الاتصال في حالات الطوارئ" : "Please enter emergency contact number";
notifyListeners();
return false;
}
// Remove any non-digit characters for validation
String cleanedNumber = contactNumber.replaceAll(RegExp(r'[^0-9]'), '');
// Check if it's a valid Saudi phone number (starts with 05 and has 10 digits)
if (cleanedNumber.length != 10) {
_emergencyContactError = isArabic ? "يجب أن يكون رقم الهاتف 10 أرقام" : "Phone number must be 10 digits";
notifyListeners();
return false;
}
if (!cleanedNumber.startsWith('05')) {
_emergencyContactError = isArabic ? "يجب أن يبدأ رقم الهاتف بـ 05" : "Phone number must start with 05";
notifyListeners();
return false;
}
// Contact number is valid
notifyListeners();
return true;
}
ProfileSettingsViewModel({ ProfileSettingsViewModel({
required CacheService cacheService, required CacheService cacheService,
required this.profileSettingsRepo, required this.profileSettingsRepo,

@ -7,7 +7,6 @@ import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/extensions/context_extensions.dart'; import 'package:hmg_patient_app_new/extensions/context_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
@ -35,17 +34,33 @@ class LoginScreenState extends State<LoginScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_nationalIdFocusNode = FocusNode(); _nationalIdFocusNode = FocusNode();
// Clear errors when entering login screen
WidgetsBinding.instance.addPostFrameCallback((_) {
final authVm = context.read<AuthenticationViewModel>();
authVm.clearNationalIdError();
authVm.clearPhoneNumberError();
});
} }
@override @override
void dispose() { void dispose() {
_nationalIdFocusNode.dispose(); _nationalIdFocusNode.dispose();
// Clear errors when leaving login screen
// Use post frame callback to avoid calling notifyListeners during dispose
WidgetsBinding.instance.addPostFrameCallback((_) {
final authVm = context.read<AuthenticationViewModel>();
authVm.clearNationalIdError();
authVm.clearPhoneNumberError();
});
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AuthenticationViewModel authVm = context.read<AuthenticationViewModel>();
return Scaffold( return Scaffold(
backgroundColor: AppColors.bgScaffoldColor, backgroundColor: AppColors.bgScaffoldColor,
appBar: CustomAppBar( appBar: CustomAppBar(
@ -56,7 +71,9 @@ class LoginScreenState extends State<LoginScreen> {
context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA')); context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA'));
}, },
), ),
body: GestureDetector( body: Consumer<AuthenticationViewModel>(
builder: (context, authVm, child) {
return GestureDetector(
onTap: () { onTap: () {
// Dismiss the keyboard and unfocus any focused widget when tapping outside // Dismiss the keyboard and unfocus any focused widget when tapping outside
_nationalIdFocusNode.unfocus(); _nationalIdFocusNode.unfocus();
@ -76,7 +93,7 @@ class LoginScreenState extends State<LoginScreen> {
SizedBox(height: 32.h), SizedBox(height: 32.h),
Localizations.override(context: context, locale: Locale('en', 'US'), child: Container()), // Force English locale for this widget Localizations.override(context: context, locale: Locale('en', 'US'), child: Container()), // Force English locale for this widget
TextInputWidget( TextInputWidget(
labelText: "${LocaleKeys.nationalIdFileNumber.tr(context: context)}", labelText: LocaleKeys.nationalIdFileNumber.tr(context: context),
hintText: "xxxxxxxxx", hintText: "xxxxxxxxx",
controller: authVm.nationalIdController, controller: authVm.nationalIdController,
focusNode: _nationalIdFocusNode, focusNode: _nationalIdFocusNode,
@ -89,9 +106,13 @@ class LoginScreenState extends State<LoginScreen> {
isAllowLeadingIcon: true, isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.h), padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.h),
leadingIcon: AppAssets.student_card, leadingIcon: AppAssets.student_card,
errorMessage: LocaleKeys.enterValidIDorIqama.tr(context: context), errorMessage: authVm.nationalIdError,
hasError: false, hasError: authVm.nationalIdError != null,
fontFamily: "Poppins", fontFamily: "Poppins",
onChange: (value) {
// Clear error when user starts typing
authVm.clearNationalIdError();
},
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
CustomButton( CustomButton(
@ -103,12 +124,13 @@ class LoginScreenState extends State<LoginScreen> {
_nationalIdFocusNode.unfocus(); _nationalIdFocusNode.unfocus();
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatedId( // Use ViewModel validation method
nationalId: authVm.nationalIdController.text, if (authVm.validateNationalId()) {
onOkPress: () { showLoginModelSheet(
Navigator.of(context).pop(); context: context,
})) { phoneNumberController: authVm.phoneNumberController,
showLoginModelSheet(context: context, phoneNumberController: authVm.phoneNumberController, authViewModel: authVm); authViewModel: authVm,
);
} }
}, },
), ),
@ -144,6 +166,8 @@ class LoginScreenState extends State<LoginScreen> {
), ),
), ),
), ),
);
},
), ),
); );
} }
@ -154,6 +178,7 @@ class LoginScreenState extends State<LoginScreen> {
required AuthenticationViewModel authViewModel, required AuthenticationViewModel authViewModel,
}) async { }) async {
AppState appState = getIt<AppState>(); AppState appState = getIt<AppState>();
context.showBottomSheet( context.showBottomSheet(
isScrollControlled: true, isScrollControlled: true,
isDismissible: false, isDismissible: false,
@ -161,36 +186,48 @@ class LoginScreenState extends State<LoginScreen> {
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width),
backgroundColor: AppColors.transparent, backgroundColor: AppColors.transparent,
child: StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { child: StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) {
// Helper function to validate phone number with error handling
void validatePhoneAndProceed(OTPTypeEnum otpType) {
// Use ViewModel validation method
bool isValid = authViewModel.validatePhoneNumber();
if (isValid) {
Navigator.of(context).pop();
appState.setSelectDeviceByImeiRespModelElement(null);
authViewModel.checkUserAuthentication(otpTypeEnum: otpType);
}
}
return Padding( return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: SingleChildScrollView( child: SingleChildScrollView(
child: GenericBottomSheet( child: Consumer<AuthenticationViewModel>(
countryCode: authViewModel.selectedCountrySignup.countryCode, builder: (context, authVm, child) {
return GenericBottomSheet(
countryCode: authVm.selectedCountrySignup.countryCode,
initialPhoneNumber: "", initialPhoneNumber: "",
textController: phoneNumberController, textController: phoneNumberController,
isEnableCountryDropdown: true, isEnableCountryDropdown: true,
onCountryChange: (country) { onCountryChange: (country) {
authViewModel.onCountryChange(country); authVm.onCountryChange(country);
setModalState(() {}); // Clear error when country changes
authVm.clearPhoneNumberError();
},
onChange: (value) {
authVm.onPhoneNumberChange(value);
// Clear error when user starts typing
authVm.clearPhoneNumberError();
}, },
onChange: authViewModel.onPhoneNumberChange, phoneNumberError: authVm.phoneNumberError,
buttons: [ buttons: [
if (authViewModel.selectedCountrySignup != CountryEnum.others) if (authVm.selectedCountrySignup != CountryEnum.others)
Padding( Padding(
padding: EdgeInsets.only(bottom: 10.h), padding: EdgeInsets.only(bottom: 10.h),
child: CustomButton( child: CustomButton(
text: LocaleKeys.sendOTPSMS.tr(context: context), text: LocaleKeys.sendOTPSMS.tr(context: context),
onPressed: () async { onPressed: () async {
if (ValidationUtils.isValidatePhone( validatePhoneAndProceed(OTPTypeEnum.sms);
phoneNumber: phoneNumberController!.text,
selectedCountry: authViewModel.selectedCountrySignup,
onOkPress: () {
Navigator.of(context).pop();
})) {
Navigator.of(context).pop();
appState.setSelectDeviceByImeiRespModelElement(null);
await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms);
}
}, },
backgroundColor: AppColors.primaryRedColor, backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedBorderColor, borderColor: AppColors.primaryRedBorderColor,
@ -199,7 +236,7 @@ class LoginScreenState extends State<LoginScreen> {
icon: AppAssets.message, icon: AppAssets.message,
), ),
), ),
if (authViewModel.selectedCountrySignup != CountryEnum.others) if (authVm.selectedCountrySignup != CountryEnum.others)
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -215,16 +252,7 @@ class LoginScreenState extends State<LoginScreen> {
child: CustomButton( child: CustomButton(
text: LocaleKeys.sendOTPWHATSAPP.tr(context: context), text: LocaleKeys.sendOTPWHATSAPP.tr(context: context),
onPressed: () async { onPressed: () async {
if (ValidationUtils.isValidatePhone( validatePhoneAndProceed(OTPTypeEnum.whatsapp);
phoneNumber: phoneNumberController!.text,
selectedCountry: authViewModel.selectedCountrySignup,
onOkPress: () {
Navigator.of(context).pop();
})) {
Navigator.of(context).pop();
appState.setSelectDeviceByImeiRespModelElement(null);
await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp);
}
}, },
backgroundColor: AppColors.whiteColor, backgroundColor: AppColors.whiteColor,
borderColor: AppColors.textColor, borderColor: AppColors.textColor,
@ -235,6 +263,8 @@ class LoginScreenState extends State<LoginScreen> {
), ),
), ),
], ],
);
},
), ),
), ),
); );

@ -7,7 +7,6 @@ import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
@ -34,19 +33,35 @@ class _RegisterNew extends State<RegisterNew> {
super.initState(); super.initState();
_nationalIdFocusNode = FocusNode(); _nationalIdFocusNode = FocusNode();
_dobFocusNode = FocusNode(); _dobFocusNode = FocusNode();
// Clear errors when entering register screen
WidgetsBinding.instance.addPostFrameCallback((_) {
final authVm = context.read<AuthenticationViewModel>();
authVm.clearNationalIdError();
authVm.clearDobError();
authVm.clearPhoneNumberError();
});
} }
@override @override
void dispose() { void dispose() {
_nationalIdFocusNode.dispose(); _nationalIdFocusNode.dispose();
_dobFocusNode.dispose(); _dobFocusNode.dispose();
// Clear errors when leaving register screen
// Use post frame callback to avoid calling notifyListeners during dispose
WidgetsBinding.instance.addPostFrameCallback((_) {
final authVm = context.read<AuthenticationViewModel>();
authVm.clearNationalIdError();
authVm.clearDobError();
authVm.clearPhoneNumberError();
});
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AuthenticationViewModel authVm = context.read<AuthenticationViewModel>();
return Scaffold( return Scaffold(
backgroundColor: AppColors.bgScaffoldColor, backgroundColor: AppColors.bgScaffoldColor,
appBar: CustomAppBar( appBar: CustomAppBar(
@ -57,7 +72,9 @@ class _RegisterNew extends State<RegisterNew> {
context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA')); context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA'));
}, },
), ),
body: GestureDetector( body: Consumer<AuthenticationViewModel>(
builder: (context, authVm, child) {
return GestureDetector(
onTap: () { onTap: () {
// Dismiss keyboard and unfocus all input fields // Dismiss keyboard and unfocus all input fields
_nationalIdFocusNode.unfocus(); _nationalIdFocusNode.unfocus();
@ -85,7 +102,14 @@ class _RegisterNew extends State<RegisterNew> {
Directionality( Directionality(
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
child: Container( child: Container(
decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24)), decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: authVm.hasRegistrationFormError ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 1,
),
),
padding: EdgeInsets.symmetric(horizontal: 16.h), padding: EdgeInsets.symmetric(horizontal: 16.h),
child: Column( child: Column(
children: [ children: [
@ -110,6 +134,12 @@ class _RegisterNew extends State<RegisterNew> {
padding: EdgeInsets.symmetric(vertical: 8.h), padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.student_card, leadingIcon: AppAssets.student_card,
fontFamily: "Poppins", fontFamily: "Poppins",
hasError: false, // Don't show individual field border
errorMessage: authVm.nationalIdError, // Show error message if exists
onChange: (value) {
// Clear error when user starts typing
authVm.clearNationalIdError();
},
).withVerticalPadding(8), ).withVerticalPadding(8),
Divider(height: 1), Divider(height: 1),
TextInputWidget( TextInputWidget(
@ -126,8 +156,14 @@ class _RegisterNew extends State<RegisterNew> {
leadingIcon: AppAssets.birthday_cake, leadingIcon: AppAssets.birthday_cake,
selectionType: SelectionTypeEnum.calendar, selectionType: SelectionTypeEnum.calendar,
onCalendarTypeChanged: authVm.onCalenderTypeChange, onCalendarTypeChanged: authVm.onCalenderTypeChange,
onChange: authVm.onDobChange, onChange: (value) {
authVm.onDobChange(value);
// Clear error when user selects a date
authVm.clearDobError();
},
fontFamily: "Poppins", fontFamily: "Poppins",
hasError: false, // Don't show individual field border
errorMessage: authVm.dobError, // Show error message if exists
).withVerticalPadding(8), ).withVerticalPadding(8),
], ],
), ),
@ -201,14 +237,8 @@ class _RegisterNew extends State<RegisterNew> {
_dobFocusNode.unfocus(); _dobFocusNode.unfocus();
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatedId( // Use ViewModel validation method
nationalId: authVm.nationalIdController.text, if (authVm.validateRegistrationForm()) {
selectedCountry: authVm.selectedCountrySignup,
isTermsAccepted: authVm.isTermsAccepted,
dob: authVm.dobController.text,
onOkPress: () {
Navigator.of(context).pop();
})) {
showRegisterModel(context: context, authVM: authVm); showRegisterModel(context: context, authVM: authVm);
} }
}, },
@ -250,6 +280,8 @@ class _RegisterNew extends State<RegisterNew> {
), ),
), ),
), ),
);
},
)); ));
} }
@ -264,14 +296,21 @@ class _RegisterNew extends State<RegisterNew> {
builder: (bottomSheetContext) => Padding( builder: (bottomSheetContext) => Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(bottomSheetContext).viewInsets.bottom), padding: EdgeInsets.only(bottom: MediaQuery.of(bottomSheetContext).viewInsets.bottom),
child: SingleChildScrollView( child: SingleChildScrollView(
child: GenericBottomSheet( child: Consumer<AuthenticationViewModel>(
countryCode: authVM.selectedCountrySignup.countryCode, builder: (context, authVm, child) {
initialPhoneNumber: authVM.phoneNumberController.text, return GenericBottomSheet(
textController: authVM.phoneNumberController, countryCode: authVm.selectedCountrySignup.countryCode,
initialPhoneNumber: authVm.phoneNumberController.text,
textController: authVm.phoneNumberController,
isEnableCountryDropdown: false, isEnableCountryDropdown: false,
onCountryChange: authVM.onCountryChange, onCountryChange: authVm.onCountryChange,
onChange: authVM.onPhoneNumberChange, onChange: (value) {
authVm.onPhoneNumberChange(value);
// Clear error when user starts typing
authVm.clearPhoneNumberError();
},
autoFocus: true, autoFocus: true,
phoneNumberError: authVm.phoneNumberError,
buttons: [ buttons: [
Padding( Padding(
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
@ -281,14 +320,10 @@ class _RegisterNew extends State<RegisterNew> {
// Dismiss keyboard before validation // Dismiss keyboard before validation
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatePhone( // Use ViewModel validation method
phoneNumber: authVM.phoneNumberController.text, if (authVm.validatePhoneNumber()) {
onOkPress: () {
Navigator.of(context).pop();
},
)) {
appState.setSelectDeviceByImeiRespModelElement(null); appState.setSelectDeviceByImeiRespModelElement(null);
await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.sms); await authVm.onRegistrationStart(otpTypeEnum: OTPTypeEnum.sms);
} }
}, },
backgroundColor: AppColors.primaryRedColor, backgroundColor: AppColors.primaryRedColor,
@ -313,14 +348,11 @@ class _RegisterNew extends State<RegisterNew> {
text: LocaleKeys.sendOTPWHATSAPP.tr(context: context), text: LocaleKeys.sendOTPWHATSAPP.tr(context: context),
onPressed: () async { onPressed: () async {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatePhone(
phoneNumber: authVM.phoneNumberController.text, // Use ViewModel validation method
onOkPress: () { if (authVm.validatePhoneNumber()) {
Navigator.of(context).pop();
},
)) {
appState.setSelectDeviceByImeiRespModelElement(null); appState.setSelectDeviceByImeiRespModelElement(null);
await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.whatsapp); await authVm.onRegistrationStart(otpTypeEnum: OTPTypeEnum.whatsapp);
} }
}, },
backgroundColor: AppColors.whiteColor, backgroundColor: AppColors.whiteColor,
@ -331,6 +363,8 @@ class _RegisterNew extends State<RegisterNew> {
), ),
), ),
], ],
);
},
), ),
), ),
), ),

@ -7,7 +7,6 @@ import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
@ -39,6 +38,11 @@ class _RegisterNew extends State<RegisterNewStep2> {
authVM = context.read<AuthenticationViewModel>(); authVM = context.read<AuthenticationViewModel>();
insuranceVM = context.read<InsuranceViewModel>(); insuranceVM = context.read<InsuranceViewModel>();
// Clear errors when entering the page
WidgetsBinding.instance.addPostFrameCallback((_) {
authVM?.clearAllStep2FieldErrors();
});
// Call insurance API to fetch data // Call insurance API to fetch data
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
debugPrint("Registration Step 2: Calling insurance API"); debugPrint("Registration Step 2: Calling insurance API");
@ -51,6 +55,10 @@ class _RegisterNew extends State<RegisterNewStep2> {
@override @override
void dispose() { void dispose() {
// Clear errors when leaving the page
WidgetsBinding.instance.addPostFrameCallback((_) {
authVM?.clearAllStep2FieldErrors();
});
super.dispose(); super.dispose();
} }
@ -107,18 +115,17 @@ class _RegisterNew extends State<RegisterNewStep2> {
icon: AppAssets.confirm, icon: AppAssets.confirm,
iconColor: AppColors.whiteColor, iconColor: AppColors.whiteColor,
onPressed: () { onPressed: () {
// Unfocus keyboard
FocusScope.of(context).unfocus();
// For UAE users, validate the form first
if (appState.getUserRegistrationPayload.zipCode != CountryEnum.saudiArabia.countryCode) { if (appState.getUserRegistrationPayload.zipCode != CountryEnum.saudiArabia.countryCode) {
if (ValidationUtils.validateUaeRegistration( // Use ViewModel validation method
name: authVM!.nameController.text, if (authVM!.validateRegistrationStep2Form()) {
gender: authVM!.genderType,
country: authVM!.pickedCountryByUAEUser,
maritalStatus: authVM!.maritalStatus,
onOkPress: () {
Navigator.of(context).pop();
})) {
showModel(context: context); showModel(context: context);
} }
} else { } else {
// For Saudi users, no validation needed, show email modal directly
showModel(context: context); showModel(context: context);
} }
}, },
@ -166,8 +173,19 @@ class _RegisterNew extends State<RegisterNewStep2> {
}, },
), ),
Container( // Form Container with Error Border
decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24)), Selector<AuthenticationViewModel, bool>(
selector: (_, model) => model.hasRegistrationStep2FormError,
builder: (context, hasError, child) {
return Container(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: hasError ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 1,
),
),
padding: EdgeInsets.only(left: 16.h, right: 16.h), padding: EdgeInsets.only(left: 16.h, right: 16.h),
child: Column( child: Column(
children: [ children: [
@ -184,11 +202,27 @@ class _RegisterNew extends State<RegisterNewStep2> {
onSubmitted: (value) { onSubmitted: (value) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
}, },
onChange: (value) {
// Clear error when user starts typing
authVM!.clearNameError();
},
isAllowLeadingIcon: true, isAllowLeadingIcon: true,
isReadOnly: authVM!.isUserFromUAE() ? false : true, isReadOnly: authVM!.isUserFromUAE() ? false : true,
leadingIcon: AppAssets.user_circle, leadingIcon: AppAssets.user_circle,
labelColor: AppColors.textColor, labelColor: AppColors.textColor,
).paddingSymmetrical(0.h, 8.h), ).paddingSymmetrical(0.h, 8.h),
// Show error message if exists
if (authVM!.isUserFromUAE() && authVM!.nameError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.nameError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
Divider(height: 1.h, color: AppColors.greyColor), Divider(height: 1.h, color: AppColors.greyColor),
TextInputWidget( TextInputWidget(
labelText: LocaleKeys.nationalIdNumber.tr(context: context), labelText: LocaleKeys.nationalIdNumber.tr(context: context),
@ -240,6 +274,18 @@ class _RegisterNew extends State<RegisterNewStep2> {
labelColor: AppColors.textColor, labelColor: AppColors.textColor,
onChange: (value) {}) onChange: (value) {})
.paddingSymmetrical(0.h, 8.h), .paddingSymmetrical(0.h, 8.h),
// Show gender error message if exists (for UAE users)
if (authVM!.isUserFromUAE() && authVM!.genderError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.genderError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
Divider(height: 1, color: AppColors.greyColor), Divider(height: 1, color: AppColors.greyColor),
authVM!.isUserFromUAE() authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel, MaritalStatusTypeEnum?>( ? Selector<AuthenticationViewModel, MaritalStatusTypeEnum?>(
@ -279,6 +325,18 @@ class _RegisterNew extends State<RegisterNewStep2> {
leadingIcon: AppAssets.smart_phone, leadingIcon: AppAssets.smart_phone,
onChange: (value) {}) onChange: (value) {})
.paddingSymmetrical(0.h, 8.h), .paddingSymmetrical(0.h, 8.h),
// Show marital status error message if exists (for UAE users)
if (authVM!.isUserFromUAE() && authVM!.maritalStatusError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.maritalStatusError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
Divider(height: 1.h, color: AppColors.greyColor), Divider(height: 1.h, color: AppColors.greyColor),
authVM!.isUserFromUAE() authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel, ({List<NationalityCountries>? countriesList, NationalityCountries? selectedCountry, bool isArabic})>( ? Selector<AuthenticationViewModel, ({List<NationalityCountries>? countriesList, NationalityCountries? selectedCountry, bool isArabic})>(
@ -329,6 +387,18 @@ class _RegisterNew extends State<RegisterNewStep2> {
leadingIcon: AppAssets.globe, leadingIcon: AppAssets.globe,
onChange: (value) {}) onChange: (value) {})
.paddingSymmetrical(0.h, 8.h), .paddingSymmetrical(0.h, 8.h),
// Show country error message if exists (for UAE users)
if (authVM!.isUserFromUAE() && authVM!.countryError != null)
Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w),
child: Text(
authVM!.countryError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
Divider( Divider(
height: 1, height: 1,
color: AppColors.greyColor, color: AppColors.greyColor,
@ -365,6 +435,8 @@ class _RegisterNew extends State<RegisterNewStep2> {
).paddingSymmetrical(0.h, 8.h), ).paddingSymmetrical(0.h, 8.h),
], ],
), ),
);
},
), ),
SizedBox(height: 50.h), SizedBox(height: 50.h),
// Row( // Row(
@ -438,12 +510,17 @@ class _RegisterNew extends State<RegisterNewStep2> {
child: CustomButton( child: CustomButton(
text: LocaleKeys.submit.tr(context: context), text: LocaleKeys.submit.tr(context: context),
onPressed: () { onPressed: () {
if (ValidationUtils.isValidateEmail( // Use ViewModel validation method
email: authVM!.emailController.text, if (authVM!.validateEmail()) {
onOkPress: () {
Navigator.of(context).pop();
})) {
authVM!.onRegistrationComplete(); authVM!.onRegistrationComplete();
} else {
// Show error in a dialog
if (authVM!.emailError != null) {
// dialogService can be used here if needed
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(authVM!.emailError!)),
);
}
} }
}, },
backgroundColor: AppColors.bgGreenColor, backgroundColor: AppColors.bgGreenColor,

@ -98,62 +98,90 @@ class DoctorsFilters extends StatelessWidget{
height: 42.h, height: 42.h,
child: FacilityChip()), child: FacilityChip()),
titleWidget(LocaleKeys.hospital.tr()), titleWidget(LocaleKeys.hospital.tr()),
Consumer<DoctorFilterViewModel>(
builder: (context, viewModel, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextInputWidget( TextInputWidget(
controller: TextEditingController()..text =context.watch<DoctorFilterViewModel>().selectedHospitalForFilters?.filterName??'', controller: TextEditingController()..text = viewModel.selectedHospitalForFilters?.filterName ?? '',
labelText: LocaleKeys.hospital.tr(context: context), labelText: LocaleKeys.hospital.tr(context: context),
hintText: LocaleKeys.searchHospital.tr(context: context), hintText: LocaleKeys.searchHospital.tr(context: context),
isEnable: false, isEnable: false,
prefix: null, prefix: null,
autoFocus: false, autoFocus: false,
isBorderAllowed: false, isBorderAllowed: true,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
suffix:context.watch<DoctorFilterViewModel>().selectedHospitalForFilters != null hasError: viewModel.hospitalError != null,
errorMessage: viewModel.hospitalError,
suffix: viewModel.selectedHospitalForFilters != null
? GestureDetector( ? GestureDetector(
onTap: () { onTap: () {
context.read<DoctorFilterViewModel>().setSelectedHospital(null); viewModel.setSelectedHospital(null);
}, },
child: Utils.buildSvgWithAssets(icon: AppAssets.ic_cross_circle, width: 24.h, height: 24.h, fit: BoxFit.scaleDown), child: Utils.buildSvgWithAssets(icon: AppAssets.ic_cross_circle, width: 24.h, height: 24.h, fit: BoxFit.scaleDown),
) )
: null, : null,
onChange: (value) { onChange: (value) {
// DoctorFilterViewModel.filterClinics(value!); // Clear error when field changes
viewModel.clearHospitalError();
}, },
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: ResponsiveExtension(8).h, vertical: ResponsiveExtension(8).h,
horizontal: ResponsiveExtension(10).h, horizontal: ResponsiveExtension(10).h,
), ),
).onPress((){ ).onPress(() {
// Clear error when opening bottom sheet
context.read<DoctorFilterViewModel>().clearHospitalError();
openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION); openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION);
}), }),
],
);
},
),
titleWidget(LocaleKeys.clinic.tr()), titleWidget(LocaleKeys.clinic.tr()),
Consumer<DoctorFilterViewModel>(
builder: (context, viewModel, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextInputWidget( TextInputWidget(
controller: TextEditingController()..text =context.watch<DoctorFilterViewModel>().selectedClinicForFilters ??'', controller: TextEditingController()..text = viewModel.selectedClinicForFilters ?? '',
labelText: LocaleKeys.clinicName.tr(context: context), labelText: LocaleKeys.clinicName.tr(context: context),
hintText: LocaleKeys.searchClinic.tr(), hintText: LocaleKeys.searchClinic.tr(),
isEnable: false, isEnable: false,
prefix: null, prefix: null,
autoFocus: false, autoFocus: false,
isBorderAllowed: false, isBorderAllowed: true,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
suffix:context.read<DoctorFilterViewModel>().selectedClinicForFilters?.isNotEmpty == true hasError: viewModel.clinicError != null,
errorMessage: viewModel.clinicError,
suffix: viewModel.selectedClinicForFilters?.isNotEmpty == true
? GestureDetector( ? GestureDetector(
onTap: () { onTap: () {
context.read<DoctorFilterViewModel>().setSelectedClinicForFilter(null); viewModel.setSelectedClinicForFilter(null);
}, },
child: Utils.buildSvgWithAssets(icon: AppAssets.ic_cross_circle, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), child: Utils.buildSvgWithAssets(icon: AppAssets.ic_cross_circle, width: 20.h, height: 20.h, fit: BoxFit.scaleDown),
) )
: null, : null,
onChange: (value) { onChange: (value) {
// DoctorFilterViewModel.filterClinics(value!); // Clear error when field changes
viewModel.clearClinicError();
}, },
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: 8.h, vertical: 8.h,
horizontal: 10.h, horizontal: 10.h,
), ),
).onPress((){ ).onPress(() {
openClinicListBottomSheet(context,); // Clear error when opening bottom sheet
context.read<DoctorFilterViewModel>().clearClinicError();
openClinicListBottomSheet(context);
}), }),
],
);
},
),
], ],

@ -86,20 +86,12 @@ class FeedbackPage extends StatelessWidget {
child: CustomButton( child: CustomButton(
text: LocaleKeys.submit.tr(context: context), text: LocaleKeys.submit.tr(context: context),
onPressed: () async { onPressed: () async {
if (subjectTextController.text.isEmpty) { // Use ViewModel validation method
showCommonBottomSheetWithoutHeight( if (!contactUsViewModel.validateFeedbackForm(subjectTextController.text, messageTextController.text)) {
context, // Validation failed, errors are already set in viewModel and displayed below fields
child: Utils.getErrorWidget(loadingText: LocaleKeys.emptySubject.tr(context: context)),
);
return;
}
if (messageTextController.text.isEmpty) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.emptyMessage.tr(context: context)),
);
return; return;
} }
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.sendingFeedback.tr(context: context)); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.sendingFeedback.tr(context: context));
contactUsViewModel.insertCOCItem( contactUsViewModel.insertCOCItem(
subject: subjectTextController.text, subject: subjectTextController.text,
@ -109,6 +101,7 @@ class FeedbackPage extends StatelessWidget {
subjectTextController.clear(); subjectTextController.clear();
messageTextController.clear(); messageTextController.clear();
contactUsViewModel.setPatientFeedbackSelectedAppointment(null); contactUsViewModel.setPatientFeedbackSelectedAppointment(null);
contactUsViewModel.clearAllFeedbackErrors();
showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr(context: context)), callBackFunc: () { showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr(context: context)), callBackFunc: () {
Navigator.pop(context); Navigator.pop(context);
}); });
@ -117,7 +110,7 @@ class FeedbackPage extends StatelessWidget {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
context, context,
child: Utils.getSuccessWidget(loadingText: err), child: Utils.getErrorWidget(loadingText: err),
); );
}); });
}, },
@ -303,35 +296,55 @@ class FeedbackPage extends StatelessWidget {
), ),
], ],
SizedBox(height: 16.h), SizedBox(height: 16.h),
TextInputWidget( Consumer<ContactUsViewModel>(
builder: (context, viewModel, child) {
return TextInputWidget(
labelText: LocaleKeys.subject.tr(context: context), labelText: LocaleKeys.subject.tr(context: context),
hintText: LocaleKeys.enterSubjectHere.tr(context: context), hintText: LocaleKeys.enterSubjectHere.tr(context: context),
controller: subjectTextController, controller: subjectTextController,
isEnable: true, isEnable: true,
prefix: null, prefix: null,
autoFocus: false, autoFocus: false,
isBorderAllowed: false, isBorderAllowed: true,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
hasError: viewModel.subjectError != null,
errorMessage: viewModel.subjectError,
onChange: (value) {
// Clear error when user starts typing
viewModel.clearSubjectError();
},
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: ResponsiveExtension(10).h, vertical: ResponsiveExtension(10).h,
horizontal: ResponsiveExtension(15).h, horizontal: ResponsiveExtension(15).h,
), ),
);
},
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
TextInputWidget( Consumer<ContactUsViewModel>(
builder: (context, viewModel, child) {
return TextInputWidget(
labelText: LocaleKeys.message.tr(context: context), labelText: LocaleKeys.message.tr(context: context),
hintText: LocaleKeys.enterMessageHere.tr(context: context), hintText: LocaleKeys.enterMessageHere.tr(context: context),
controller: messageTextController, controller: messageTextController,
isEnable: true, isEnable: true,
prefix: null, prefix: null,
autoFocus: false, autoFocus: false,
isBorderAllowed: false, isBorderAllowed: true,
isMultiline: true, isMultiline: true,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
hasError: viewModel.messageError != null,
errorMessage: viewModel.messageError,
onChange: (value) {
// Clear error when user starts typing
viewModel.clearMessageError();
},
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: ResponsiveExtension(10).h, vertical: ResponsiveExtension(10).h,
horizontal: ResponsiveExtension(15).h, horizontal: ResponsiveExtension(15).h,
), ),
);
},
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
CustomButton( CustomButton(

@ -1,5 +1,3 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -23,8 +21,6 @@ import 'package:provider/provider.dart';
import 'widgets/select-medical_file.dart'; import 'widgets/select-medical_file.dart';
import 'dart:ui' as ui;
class RechargeWalletPage extends StatefulWidget { class RechargeWalletPage extends StatefulWidget {
const RechargeWalletPage({super.key}); const RechargeWalletPage({super.key});
@ -33,7 +29,7 @@ class RechargeWalletPage extends StatefulWidget {
} }
class _RechargeWalletPageState extends State<RechargeWalletPage> { class _RechargeWalletPageState extends State<RechargeWalletPage> {
FocusNode textFocusNode = FocusNode(); late FocusNode _amountFocusNode;
late HabibWalletViewModel habibWalletVM; late HabibWalletViewModel habibWalletVM;
late AppState appState; late AppState appState;
@ -42,22 +38,54 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
@override @override
void initState() { void initState() {
scheduleMicrotask(() { super.initState();
_amountFocusNode = FocusNode();
// Clear errors when entering recharge wallet page
WidgetsBinding.instance.addPostFrameCallback((_) {
habibWalletVM = context.read<HabibWalletViewModel>();
habibWalletVM.setDepositorDetails(appState.getAuthenticatedUser()!.patientId.toString(), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", habibWalletVM.setDepositorDetails(appState.getAuthenticatedUser()!.patientId.toString(), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
appState.getAuthenticatedUser()!.mobileNumber!); appState.getAuthenticatedUser()!.mobileNumber!);
habibWalletVM.setSelectedRechargeType(0); habibWalletVM.setSelectedRechargeType(0);
habibWalletVM.getProjectsList(); habibWalletVM.getProjectsList();
// Clear any previous recharge errors
habibWalletVM.clearAllRechargeErrors();
}); });
super.initState(); }
@override
void dispose() {
// Clear errors before disposing
habibWalletVM.clearAllRechargeErrors();
_amountFocusNode.dispose();
amountTextController.dispose();
notesTextController.dispose();
super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
habibWalletVM = Provider.of<HabibWalletViewModel>(context, listen: false); habibWalletVM = Provider.of<HabibWalletViewModel>(context, listen: false);
appState = getIt.get<AppState>(); appState = getIt.get<AppState>();
return Scaffold( return PopScope(
onPopInvokedWithResult: (bool didPop, dynamic result) {
if (didPop) {
// Clear errors when user navigates back
habibWalletVM.clearAllRechargeErrors();
}
},
child: Scaffold(
backgroundColor: AppColors.bgScaffoldColor, backgroundColor: AppColors.bgScaffoldColor,
body: Column( body: GestureDetector(
onTap: () {
// Dismiss the keyboard and unfocus any focused widget when tapping outside
_amountFocusNode.unfocus();
FocusScope.of(context).unfocus();
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
@ -69,13 +97,18 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Consumer<HabibWalletViewModel>(
builder: (context, viewModel, child) {
return Container(
height: 135.h, height: 135.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
borderRadius: 24.h, borderRadius: 24.h,
hasShadow: false, hasShadow: false,
side: BorderSide(color: AppColors.textColor, width: 2.h), side: BorderSide(
color: viewModel.amountError != null ? AppColors.primaryRedBorderColor : AppColors.textColor,
width: 2.h,
),
), ),
child: Padding( child: Padding(
padding: EdgeInsets.all(16.h), padding: EdgeInsets.all(16.h),
@ -102,10 +135,14 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
autoFocus: true, autoFocus: true,
fontSize: 25.f, fontSize: 25.f,
padding: EdgeInsets.symmetric(horizontal: 8.h, vertical: 0.h), padding: EdgeInsets.symmetric(horizontal: 8.h, vertical: 0.h),
focusNode: textFocusNode, focusNode: _amountFocusNode,
isWalletAmountInput: true, isWalletAmountInput: true,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
fontFamily: "Poppins", fontFamily: "Poppins",
onChange: (value) {
// Clear error when user starts typing
viewModel.clearAmountError();
},
// leadingIcon: AppAssets.student_card, // leadingIcon: AppAssets.student_card,
), ),
), ),
@ -116,14 +153,37 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
], ],
), ),
), ),
);
},
),
// Show error message if exists
Consumer<HabibWalletViewModel>(
builder: (context, viewModel, child) {
if (viewModel.amountError != null) {
return Padding(
padding: EdgeInsets.only(left: 16.w, top: 8.h),
child: Text(
viewModel.amountError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
);
}
return SizedBox.shrink();
},
), ),
SizedBox(height: 24.h), SizedBox(height: 24.h),
Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) { Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Container( return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: BoxDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
borderRadius: 24.h, borderRadius: BorderRadius.circular(24.h),
hasShadow: false, border: Border.all(
color: habibWalletVM.hospitalError != null ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 2.h,
),
), ),
child: Padding( child: Padding(
padding: EdgeInsets.all(16.h), padding: EdgeInsets.all(16.h),
@ -194,6 +254,18 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
showCommonBottomSheetWithoutHeight(context, showCommonBottomSheetWithoutHeight(context,
title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false, child: SelectHospitalBottomSheet(), callBackFunc: () {}); title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false, child: SelectHospitalBottomSheet(), callBackFunc: () {});
}), }),
// Show hospital error message if exists
if (habibWalletVM.hospitalError != null)
Padding(
padding: EdgeInsets.only(top: 8.h),
child: Text(
habibWalletVM.hospitalError!,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
SizedBox(height: 16.h), SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h), SizedBox(height: 16.h),
@ -249,27 +321,11 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
child: CustomButton( child: CustomButton(
text: LocaleKeys.next.tr(context: context), text: LocaleKeys.next.tr(context: context),
onPressed: () { onPressed: () {
if (amountTextController.text.isEmpty) { _amountFocusNode.unfocus();
showCommonBottomSheetWithoutHeight( FocusScope.of(context).unfocus();
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.enterAmount.tr(context: context)), // Use ViewModel validation method
callBackFunc: () { if (habibWalletVM.validateRechargeForm(amountTextController.text)) {
textFocusNode.requestFocus();
},
isFullScreen: false,
isCloseButtonVisible: true,
);
} else if (habibWalletVM.selectedHospital == null) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.selectHospitalForAdvancePayment.tr(context: context)),
callBackFunc: () {
textFocusNode.requestFocus();
},
isFullScreen: false,
isCloseButtonVisible: true,
);
} else {
habibWalletVM.setWalletRechargeAmount(num.parse(amountTextController.text.replaceAll(',', ''))); habibWalletVM.setWalletRechargeAmount(num.parse(amountTextController.text.replaceAll(',', '')));
habibWalletVM.setNotesText(notesTextController.text); habibWalletVM.setNotesText(notesTextController.text);
// habibWalletVM.setDepositorDetails(appState.getAuthenticatedUser()!.patientId.toString(), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", // habibWalletVM.setDepositorDetails(appState.getAuthenticatedUser()!.patientId.toString(), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
@ -296,6 +352,8 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
), ),
], ],
), ),
),
),
); );
} }
} }

@ -44,12 +44,27 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
void initState() { void initState() {
super.initState(); super.initState();
dialogService = getIt.get<DialogService>(); dialogService = getIt.get<DialogService>();
// Clear errors when entering the page
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<HealthTrackersViewModel>();
viewModel.clearAllFieldErrors();
});
} }
@override @override
void dispose() { void dispose() {
dateController.dispose(); dateController.dispose();
timeController.dispose(); timeController.dispose();
// Clear errors when leaving the page
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
final viewModel = context.read<HealthTrackersViewModel>();
viewModel.clearAllFieldErrors();
}
});
super.dispose(); super.dispose();
} }
@ -94,10 +109,17 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
// Save Blood Sugar entry // Save Blood Sugar entry
Future<void> _saveBloodSugarEntry(HealthTrackersViewModel viewModel) async { Future<void> _saveBloodSugarEntry(HealthTrackersViewModel viewModel) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Combine date and time // Combine date and time
final dateTime = "${dateController.text} ${timeController.text}"; final dateTime = "${dateController.text} ${timeController.text}";
// Validate using ViewModel method that sets field errors
if (!viewModel.validateBloodSugarEntry(dateTime)) {
// Validation failed, errors are already set in viewModel
return;
}
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Call ViewModel method with callbacks // Call ViewModel method with callbacks
await viewModel.saveBloodSugarEntry( await viewModel.saveBloodSugarEntry(
dateTime: dateTime, dateTime: dateTime,
@ -115,10 +137,17 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
// Save Weight entry // Save Weight entry
Future<void> _saveWeightEntry(HealthTrackersViewModel viewModel) async { Future<void> _saveWeightEntry(HealthTrackersViewModel viewModel) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Combine date and time // Combine date and time
final dateTime = "${dateController.text} ${timeController.text}"; final dateTime = "${dateController.text} ${timeController.text}";
// Validate using ViewModel method that sets field errors
if (!viewModel.validateWeightEntry(dateTime)) {
// Validation failed, errors are already set in viewModel
return;
}
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Call ViewModel method with callbacks // Call ViewModel method with callbacks
await viewModel.saveWeightEntry( await viewModel.saveWeightEntry(
dateTime: dateTime, dateTime: dateTime,
@ -135,10 +164,17 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
// Save Blood Pressure entry // Save Blood Pressure entry
Future<void> _saveBloodPressureEntry(HealthTrackersViewModel viewModel) async { Future<void> _saveBloodPressureEntry(HealthTrackersViewModel viewModel) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Combine date and time // Combine date and time
final dateTime = "${dateController.text} ${timeController.text}"; final dateTime = "${dateController.text} ${timeController.text}";
// Validate using ViewModel method that sets field errors
if (!viewModel.validateBloodPressureEntry(dateTime)) {
// Validation failed, errors are already set in viewModel
return;
}
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Call ViewModel method with callbacks // Call ViewModel method with callbacks
await viewModel.saveBloodPressureEntry( await viewModel.saveBloodPressureEntry(
dateTime: dateTime, dateTime: dateTime,
@ -286,13 +322,19 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
} }
// Reusable method to build text field // Reusable method to build text field
Widget _buildTextField(TextEditingController controller, String hintText, {TextInputType keyboardType = TextInputType.name}) { Widget _buildTextField(
TextEditingController controller,
String hintText, {
TextInputType keyboardType = TextInputType.name,
Function(String)? onChanged,
}) {
return TextField( return TextField(
controller: controller, controller: controller,
keyboardType: keyboardType, keyboardType: keyboardType,
maxLines: 1, maxLines: 1,
cursorHeight: 14.h, cursorHeight: 14.h,
textAlignVertical: TextAlignVertical.center, textAlignVertical: TextAlignVertical.center,
onChanged: onChanged,
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@ -308,6 +350,23 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
); );
} }
// Build error text widget
Widget _buildErrorText(String errorMessage) {
return Padding(
padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
errorMessage,
style: TextStyle(
color: AppColors.primaryRedColor,
fontSize: 12.f,
),
),
),
);
}
// Reusable method to build settings row // Reusable method to build settings row
Widget _buildSettingsRow({ Widget _buildSettingsRow({
required String icon, required String icon,
@ -401,15 +460,36 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
/// Blood Sugar form fields /// Blood Sugar form fields
Widget _buildBloodSugarForm(HealthTrackersViewModel viewModel) { Widget _buildBloodSugarForm(HealthTrackersViewModel viewModel) {
return Column( return Selector<HealthTrackersViewModel, bool>(
selector: (_, model) => model.hasBloodSugarFormError,
builder: (context, hasError, child) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
side: hasError
? BorderSide(color: AppColors.primaryRedBorderColor, width: 1)
: null,
),
child: Column(
children: [ children: [
_buildSettingsRow( _buildSettingsRow(
icon: AppAssets.heightIcon, icon: AppAssets.heightIcon,
label: LocaleKeys.enterBloodSugar.tr(context: context), label: LocaleKeys.enterBloodSugar.tr(context: context),
inputField: _buildTextField(viewModel.bloodSugarController, '', keyboardType: TextInputType.number), inputField: _buildTextField(
viewModel.bloodSugarController,
'',
keyboardType: TextInputType.number,
onChanged: (value) => viewModel.clearBloodSugarError(),
),
unit: viewModel.selectedBloodSugarUnit, unit: viewModel.selectedBloodSugarUnit,
onUnitTap: () => _showBloodSugarUnitSelectionBottomSheet(context, viewModel), onUnitTap: () => _showBloodSugarUnitSelectionBottomSheet(context, viewModel),
), ),
if (viewModel.bloodSugarError != null)
_buildErrorText(viewModel.bloodSugarError!),
_buildDateTimeFields(), _buildDateTimeFields(),
Divider(height: 1, color: AppColors.dividerColor), Divider(height: 1, color: AppColors.dividerColor),
_buildSettingsRow( _buildSettingsRow(
@ -418,56 +498,123 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
value: viewModel.selectedBloodSugarMeasureTime, value: viewModel.selectedBloodSugarMeasureTime,
onRowTap: () => _showBloodSugarEntryTimeBottomSheet(context, viewModel), onRowTap: () => _showBloodSugarEntryTimeBottomSheet(context, viewModel),
), ),
if (viewModel.bloodSugarMeasureTimeError != null)
_buildErrorText(viewModel.bloodSugarMeasureTimeError!),
], ],
),
);
},
); );
} }
/// Blood Pressure form fields /// Blood Pressure form fields
Widget _buildBloodPressureForm(HealthTrackersViewModel viewModel) { Widget _buildBloodPressureForm(HealthTrackersViewModel viewModel) {
return Column( return Selector<HealthTrackersViewModel, bool>(
selector: (_, model) => model.hasBloodPressureFormError,
builder: (context, hasError, child) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
side: hasError
? BorderSide(color: AppColors.primaryRedBorderColor, width: 1)
: null,
),
child: Column(
children: [ children: [
_buildSettingsRow( _buildSettingsRow(
icon: AppAssets.bloodPressureIcon, icon: AppAssets.bloodPressureIcon,
iconColor: AppColors.greyTextColor, iconColor: AppColors.greyTextColor,
label: LocaleKeys.enterSystolicValue.tr(context: context), label: LocaleKeys.enterSystolicValue.tr(context: context),
inputField: _buildTextField(viewModel.systolicController, '', keyboardType: TextInputType.number), inputField: _buildTextField(
viewModel.systolicController,
'',
keyboardType: TextInputType.number,
onChanged: (value) => viewModel.clearSystolicError(),
), ),
),
if (viewModel.systolicError != null)
_buildErrorText(viewModel.systolicError!),
_buildSettingsRow( _buildSettingsRow(
icon: AppAssets.bloodPressureIcon, icon: AppAssets.bloodPressureIcon,
iconColor: AppColors.greyTextColor, iconColor: AppColors.greyTextColor,
label: LocaleKeys.enterDiastolicValue.tr(context: context), label: LocaleKeys.enterDiastolicValue.tr(context: context),
inputField: _buildTextField(viewModel.diastolicController, '', keyboardType: TextInputType.number), inputField: _buildTextField(
viewModel.diastolicController,
'',
keyboardType: TextInputType.number,
onChanged: (value) => viewModel.clearDiastolicError(),
),
), ),
if (viewModel.diastolicError != null)
_buildErrorText(viewModel.diastolicError!),
_buildSettingsRow( _buildSettingsRow(
icon: AppAssets.bodyIcon, icon: AppAssets.bodyIcon,
iconColor: AppColors.greyTextColor, iconColor: AppColors.greyTextColor,
label: LocaleKeys.selectArm.tr(context: context), label: LocaleKeys.selectArm.tr(context: context),
value: viewModel.selectedMeasuredArmDisplay, value: viewModel.selectedMeasuredArmDisplay,
onRowTap: () => _showMeasuredArmSelectionBottomSheet(context, viewModel), onRowTap: () {
_showMeasuredArmSelectionBottomSheet(context, viewModel);
viewModel.clearMeasuredArmError();
},
), ),
if (viewModel.measuredArmError != null)
_buildErrorText(viewModel.measuredArmError!),
_buildDateTimeFields(), _buildDateTimeFields(),
], ],
),
);
},
); );
} }
/// Weight form fields /// Weight form fields
Widget _buildWeightForm(HealthTrackersViewModel viewModel) { Widget _buildWeightForm(HealthTrackersViewModel viewModel) {
return Column( return Selector<HealthTrackersViewModel, bool>(
selector: (_, model) => model.hasWeightFormError,
builder: (context, hasError, child) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: true,
side: hasError
? BorderSide(color: AppColors.primaryRedBorderColor, width: 1)
: null,
),
child: Column(
children: [ children: [
_buildSettingsRow( _buildSettingsRow(
icon: AppAssets.weightScale, icon: AppAssets.weightScale,
label: LocaleKeys.enterWeight.tr(context: context), label: LocaleKeys.enterWeight.tr(context: context),
inputField: _buildTextField(viewModel.weightController, '', keyboardType: TextInputType.number), inputField: _buildTextField(
viewModel.weightController,
'',
keyboardType: TextInputType.number,
onChanged: (value) => viewModel.clearWeightError(),
),
unit: viewModel.selectedWeightUnitDisplay, unit: viewModel.selectedWeightUnitDisplay,
onUnitTap: () => _showWeightUnitSelectionBottomSheet(context, viewModel), onUnitTap: () => _showWeightUnitSelectionBottomSheet(context, viewModel),
), ),
if (viewModel.weightError != null)
_buildErrorText(viewModel.weightError!),
_buildDateTimeFields(), _buildDateTimeFields(),
], ],
),
);
},
); );
} }
/// Common date and time fields /// Common date and time fields
Widget _buildDateTimeFields() { Widget _buildDateTimeFields() {
return Consumer<HealthTrackersViewModel>(
builder: (context, viewModel, child) {
return Column( return Column(
children: [ children: [
SizedBox(width: 8.w), SizedBox(width: 8.w),
@ -495,13 +642,17 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
final parsedDate = DateTime.parse(val); final parsedDate = DateTime.parse(val);
final formattedDate = DateFormat('dd MMM yyyy').format(parsedDate); final formattedDate = DateFormat('dd MMM yyyy').format(parsedDate);
dateController.text = formattedDate; dateController.text = formattedDate;
viewModel.clearDateError();
log("date: $formattedDate"); log("date: $formattedDate");
} catch (e) { } catch (e) {
dateController.text = val; dateController.text = val;
viewModel.clearDateError();
log("date: $val"); log("date: $val");
} }
}, },
), ),
if (viewModel.dateError != null)
_buildErrorText(viewModel.dateError!),
Divider(height: 1, color: AppColors.dividerColor), Divider(height: 1, color: AppColors.dividerColor),
SizedBox(width: 8.w), SizedBox(width: 8.w),
TextInputWidget( TextInputWidget(
@ -524,11 +675,16 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
onChange: (val) { onChange: (val) {
if (val == null) return; if (val == null) return;
timeController.text = val; timeController.text = val;
viewModel.clearTimeError();
log("time: $val"); log("time: $val");
}, },
), ),
if (viewModel.timeError != null)
_buildErrorText(viewModel.timeError!),
], ],
); );
},
);
} }
@override @override
@ -555,14 +711,9 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
), ),
), ),
), ),
child: Container(
margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
child: _buildFormFields(viewModel), child: _buildFormFields(viewModel),
), ),
), ),
),
); );
} }
} }

@ -26,6 +26,7 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/expandable_list_widget.dart';
import 'package:hmg_patient_app_new/widgets/user_avatar_widget.dart'; import 'package:hmg_patient_app_new/widgets/user_avatar_widget.dart';
import 'package:hmg_patient_app_new/widgets/image_picker.dart'; import 'package:hmg_patient_app_new/widgets/image_picker.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -390,7 +391,6 @@ class _FamilyCardsState extends State<FamilyCards> {
} }
} }
double _calculateAspectRatio(BuildContext context) { double _calculateAspectRatio(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final itemWidth = (screenWidth - 32.w - 10.w) / 2; final itemWidth = (screenWidth - 32.w - 10.w) / 2;
@ -424,32 +424,46 @@ class _FamilyCardsState extends State<FamilyCards> {
if (widget.isRequestDesign) { if (widget.isRequestDesign) {
return Column( return Column(
children: [ children: [
Row( // Row(
// children: [
// Utils.buildSvgWithAssets(icon: AppAssets.alertSquare),
// SizedBox(width: 8.h),
// LocaleKeys.whoCanViewMyMedicalFile.tr(context: context).toText14(color: AppColors.textColor, isUnderLine: true, isBold: true).onPress(() {
// dialogService.showFamilyBottomSheetWithoutHWithChild(
// label: LocaleKeys.manageFiles.tr(context: context),
// message: "",
// child: manageFamily(),
// onOkPressed: () {},
// );
// }),
// SizedBox(width: 4.h),
// Transform.flip(
// flipX: getIt.get<AppState>().isArabic(),
// child: Utils.buildSvgWithAssets(
// icon: AppAssets.arrowRight,
// iconColor: AppColors.blackColor,
// width: 22.w,
// height: 22.h,
// fit: BoxFit.contain,
// )),
// ],
// ),
// SizedBox(height: 24.h),
CustomExpandableList(
expansionMode: ExpansionMode.exactlyOne,
dividerColor: AppColors.dividerColor,
itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h),
items: [
ExpandableListItem(
title: LocaleKeys.whoCanViewMyMedicalFile.tr(context: context).toText18(isBold: true),
expandedBackgroundColor: Colors.transparent,
children: [ children: [
Utils.buildSvgWithAssets(icon: AppAssets.alertSquare), SizedBox(height: 10.h),
SizedBox(width: 8.h), manageFamily()
LocaleKeys.whoCanViewMyMedicalFile.tr(context: context).toText14(color: AppColors.textColor, isUnderLine: true, isBold: true).onPress(() {
dialogService.showFamilyBottomSheetWithoutHWithChild(
label: LocaleKeys.manageFiles.tr(context: context),
message: "",
child: manageFamily(),
onOkPressed: () {},
);
}),
SizedBox(width: 4.h),
Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrowRight,
iconColor: AppColors.blackColor,
width: 22.w,
height: 22.h,
fit: BoxFit.contain,
)
),
], ],
), ),
SizedBox(height: 24.h), ExpandableListItem(title: LocaleKeys.notifications.tr(context: context).toText18(isBold: true), expandedBackgroundColor: Colors.transparent, initiallyExpanded: true, children: [
SizedBox(height: 10.h),
widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).isEmpty widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).isEmpty
? Utils.getNoDataWidget(context) ? Utils.getNoDataWidget(context)
: ListView.builder( : ListView.builder(
@ -514,6 +528,12 @@ class _FamilyCardsState extends State<FamilyCards> {
); );
}, },
), ),
])
],
theme: ExpandableListTheme.custom(
defaultTrailingIcon: Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 22.h, width: 22.w, iconColor: AppColors.textColor),
),
).paddingSymmetrical(0.w, 0.0),
SizedBox(height: 20.h), SizedBox(height: 20.h),
], ],
); );

@ -1,8 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -31,17 +29,34 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
void initState() { void initState() {
_textFieldFocusNode = FocusNode(); _textFieldFocusNode = FocusNode();
textController = TextEditingController(); textController = TextEditingController();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// Get the view model reference
final viewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false);
// Clear any previous email error
viewModel.clearEmailError();
// Set the text
setState(() { setState(() {
textController!.text = profileSettingsViewModel!.getPatientInfoForUpdate.emailAddress ?? ""; textController!.text = viewModel.getPatientInfoForUpdate.emailAddress ?? "";
}); });
}); });
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
_textFieldFocusNode.dispose(); _textFieldFocusNode.dispose();
// Clear email error when closing
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
profileSettingsViewModel?.clearEmailError();
}
});
super.dispose(); super.dispose();
} }
@ -58,7 +73,9 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
children: [ children: [
LocaleKeys.updateEmailAddressTitle.tr().toText16(textAlign: TextAlign.start, isBold: true), LocaleKeys.updateEmailAddressTitle.tr().toText16(textAlign: TextAlign.start, isBold: true),
SizedBox(height: 12.h), SizedBox(height: 12.h),
TextInputWidget( Consumer<ProfileSettingsViewModel>(
builder: (context, viewModel, child) {
return TextInputWidget(
labelText: LocaleKeys.email.tr(), labelText: LocaleKeys.email.tr(),
hintText: "demo@gmail.com", hintText: "demo@gmail.com",
controller: textController, controller: textController,
@ -69,17 +86,35 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
isEnable: true, isEnable: true,
isReadOnly: false, isReadOnly: false,
prefix: null, prefix: null,
isBorderAllowed: false, isBorderAllowed: true,
isAllowLeadingIcon: true, isAllowLeadingIcon: true,
fontSize: 14.f, fontSize: 14.f,
isCountryDropDown: false, isCountryDropDown: false,
leadingIcon: AppAssets.email, leadingIcon: AppAssets.email,
fontFamily: "Poppins", fontFamily: "Poppins",
hasError: viewModel.emailError != null,
errorMessage: viewModel.emailError,
onChange: (value) {
// Clear error when user starts typing
viewModel.clearEmailError();
},
);
},
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
CustomButton( CustomButton(
text: LocaleKeys.submit.tr(context: context), text: LocaleKeys.submit.tr(context: context),
onPressed: () { onPressed: () {
// Unfocus keyboard
_textFieldFocusNode.unfocus();
FocusScope.of(context).unfocus();
// Validate email using ViewModel method
if (!profileSettingsViewModel!.validateEmail(textController!.text)) {
// Validation failed, error is already set in viewModel and displayed below field
return;
}
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.updatingEmailAddress.tr(context: context)); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.updatingEmailAddress.tr(context: context));
profileSettingsViewModel!.updatePatientInfo( profileSettingsViewModel!.updatePatientInfo(
patientInfo: { patientInfo: {
@ -92,6 +127,7 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
}, },
onSuccess: (response) { onSuccess: (response) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
profileSettingsViewModel!.clearEmailError();
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.success.tr(context: context), child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.success.tr(context: context), child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()),
callBackFunc: () async { callBackFunc: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();

@ -1,8 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -31,17 +29,34 @@ class _UpdateEmergencyContactDialogState extends State<UpdateEmergencyContactDia
void initState() { void initState() {
_textFieldFocusNode = FocusNode(); _textFieldFocusNode = FocusNode();
textController = TextEditingController(); textController = TextEditingController();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// Get the view model reference
final viewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false);
// Clear any previous emergency contact error
viewModel.clearEmergencyContactError();
// Set the text
setState(() { setState(() {
textController!.text = profileSettingsViewModel!.getPatientInfoForUpdate.emergencyContactNo!; textController!.text = viewModel.getPatientInfoForUpdate.emergencyContactNo ?? "";
}); });
}); });
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
_textFieldFocusNode.dispose(); _textFieldFocusNode.dispose();
// Clear emergency contact error when closing
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
profileSettingsViewModel?.clearEmergencyContactError();
}
});
super.dispose(); super.dispose();
} }
@ -58,7 +73,9 @@ class _UpdateEmergencyContactDialogState extends State<UpdateEmergencyContactDia
children: [ children: [
LocaleKeys.enterNewNewContactNumber.tr().toText16(textAlign: TextAlign.start, isBold: true), LocaleKeys.enterNewNewContactNumber.tr().toText16(textAlign: TextAlign.start, isBold: true),
SizedBox(height: 12.h), SizedBox(height: 12.h),
TextInputWidget( Consumer<ProfileSettingsViewModel>(
builder: (context, viewModel, child) {
return TextInputWidget(
labelText: LocaleKeys.emrgNo.tr(), labelText: LocaleKeys.emrgNo.tr(),
hintText: "05xxxxxxxx", hintText: "05xxxxxxxx",
controller: textController, controller: textController,
@ -69,17 +86,35 @@ class _UpdateEmergencyContactDialogState extends State<UpdateEmergencyContactDia
isEnable: true, isEnable: true,
isReadOnly: false, isReadOnly: false,
prefix: null, prefix: null,
isBorderAllowed: false, isBorderAllowed: true,
isAllowLeadingIcon: true, isAllowLeadingIcon: true,
fontSize: 14.f, fontSize: 14.f,
isCountryDropDown: false, isCountryDropDown: false,
leadingIcon: AppAssets.call_fill, leadingIcon: AppAssets.call_fill,
fontFamily: "Poppins", fontFamily: "Poppins",
hasError: viewModel.emergencyContactError != null,
errorMessage: viewModel.emergencyContactError,
onChange: (value) {
// Clear error when user starts typing
viewModel.clearEmergencyContactError();
},
);
},
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
CustomButton( CustomButton(
text: LocaleKeys.submit.tr(context: context), text: LocaleKeys.submit.tr(context: context),
onPressed: () { onPressed: () {
// Unfocus keyboard
_textFieldFocusNode.unfocus();
FocusScope.of(context).unfocus();
// Validate emergency contact using ViewModel method
if (!profileSettingsViewModel!.validateEmergencyContact(textController!.text)) {
// Validation failed, error is already set in viewModel and displayed below field
return;
}
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
profileSettingsViewModel!.updatePatientInfo( profileSettingsViewModel!.updatePatientInfo(
patientInfo: { patientInfo: {
@ -92,6 +127,7 @@ class _UpdateEmergencyContactDialogState extends State<UpdateEmergencyContactDia
}, },
onSuccess: (response) { onSuccess: (response) {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
profileSettingsViewModel!.clearEmergencyContactError();
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.success.tr(context: context), child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.success.tr(context: context), child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()),
callBackFunc: () async { callBackFunc: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();

@ -24,6 +24,8 @@ class GenericBottomSheet extends StatefulWidget {
final bool isFromSavedLogin; final bool isFromSavedLogin;
final Function(String?)? onChange; final Function(String?)? onChange;
final bool autoFocus; final bool autoFocus;
final String? phoneNumberError;
final String? emailError;
// FocusNode myFocusNode; // FocusNode myFocusNode;
@ -39,6 +41,8 @@ class GenericBottomSheet extends StatefulWidget {
this.isFromSavedLogin = false, this.isFromSavedLogin = false,
this.onChange, this.onChange,
this.autoFocus = false, this.autoFocus = false,
this.phoneNumberError,
this.emailError,
// required this.myFocusNode // required this.myFocusNode
}); });
@ -163,6 +167,8 @@ class GenericBottomSheetState extends State<GenericBottomSheet> {
isCountryDropDown: widget.isEnableCountryDropdown, isCountryDropDown: widget.isEnableCountryDropdown,
leadingIcon: widget.isForEmail ? AppAssets.email : AppAssets.smart_phone, leadingIcon: widget.isForEmail ? AppAssets.email : AppAssets.smart_phone,
fontFamily: "Poppins", fontFamily: "Poppins",
hasError: widget.isForEmail ? (widget.emailError != null) : (widget.phoneNumberError != null),
errorMessage: widget.isForEmail ? widget.emailError : widget.phoneNumberError,
) )
: SizedBox(), : SizedBox(),
], ],

@ -4,11 +4,9 @@ import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -27,10 +25,37 @@ class FamilyFileAddWidget extends StatefulWidget {
} }
class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> { class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
late AuthenticationViewModel _authVm;
@override
void initState() {
super.initState();
// Get ViewModel reference early
_authVm = getIt.get<AuthenticationViewModel>();
// Clear errors when family file add widget is opened
WidgetsBinding.instance.addPostFrameCallback((_) {
_authVm.clearNationalIdError();
_authVm.clearPhoneNumberError();
});
}
@override
void dispose() {
// Clear errors when family file add widget is closed
// Use post frame callback to avoid calling notifyListeners during dispose
WidgetsBinding.instance.addPostFrameCallback((_) {
_authVm.clearNationalIdError();
_authVm.clearPhoneNumberError();
});
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AuthenticationViewModel authVm = getIt.get<AuthenticationViewModel>(); // Use the stored reference instead of getting it here
// TODO: implement build
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@ -38,20 +63,32 @@ class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
children: [ children: [
widget.message.toText16(color: AppColors.textColor, isBold: true), widget.message.toText16(color: AppColors.textColor, isBold: true),
SizedBox(height: 20.h), SizedBox(height: 20.h),
Container( Selector<AuthenticationViewModel, bool>(
decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24)), selector: (_, model) => model.hasIdAndPhoneError,
builder: (context, hasError, child) {
return Container(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: hasError
? AppColors.primaryRedBorderColor
: Colors.transparent,
width: 1,
),
),
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h),
child: Column( child: Column(
children: [ children: [
CustomCountryDropdown( CustomCountryDropdown(
countryList: CountryEnum.values.where((c) => c != CountryEnum.others).toList(), countryList: CountryEnum.values.where((c) => c != CountryEnum.others).toList(),
onCountryChange: authVm.onCountryChange, onCountryChange: _authVm.onCountryChange,
).paddingOnly(top: 8.h, bottom: 16.h), ).paddingOnly(top: 8.h, bottom: 16.h),
Divider(height: 1.h, color: AppColors.spacerLineColor), Divider(height: 1.h, color: AppColors.spacerLineColor),
TextInputWidget( TextInputWidget(
labelText: LocaleKeys.nationalIdNumber.tr(), labelText: LocaleKeys.nationalIdNumber.tr(),
hintText: "xxxxxxxxx", hintText: "xxxxxxxxx",
controller: authVm.nationalIdController, controller: _authVm.nationalIdController,
isEnable: true, isEnable: true,
prefix: null, prefix: null,
isAllowRadius: true, isAllowRadius: true,
@ -62,6 +99,12 @@ class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
fontFamily: "Poppins", fontFamily: "Poppins",
padding: EdgeInsets.symmetric(vertical: 8.h), padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.student_card, leadingIcon: AppAssets.student_card,
hasError: false, // Don't show individual field border
errorMessage: _authVm.nationalIdError, // Show error message if exists
onChange: (value) {
// Clear error when user starts typing
_authVm.clearNationalIdError();
},
).paddingOnly(top: 8.h, bottom: 8.h), ).paddingOnly(top: 8.h, bottom: 8.h),
Divider(height: 1.h, color: AppColors.spacerLineColor), Divider(height: 1.h, color: AppColors.spacerLineColor),
Selector<AuthenticationViewModel, String>( Selector<AuthenticationViewModel, String>(
@ -70,7 +113,7 @@ class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
return TextInputWidget( return TextInputWidget(
labelText: LocaleKeys.phoneNumber.tr(), labelText: LocaleKeys.phoneNumber.tr(),
hintText: "", hintText: "",
controller: authVm.phoneNumberController, controller: _authVm.phoneNumberController,
isEnable: true, isEnable: true,
prefix: countryCode, prefix: countryCode,
isAllowRadius: true, isAllowRadius: true,
@ -81,11 +124,19 @@ class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
fontFamily: "Poppins", fontFamily: "Poppins",
padding: EdgeInsets.symmetric(vertical: 8.h), padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.smart_phone, leadingIcon: AppAssets.smart_phone,
hasError: false, // Don't show individual field border
errorMessage: _authVm.phoneNumberError, // Show error message if exists
onChange: (value) {
// Clear error when user starts typing
_authVm.clearPhoneNumberError();
},
).paddingOnly(top: 8.h, bottom: 4.h); ).paddingOnly(top: 8.h, bottom: 4.h);
}, },
), ),
], ],
), ),
);
},
), ),
SizedBox(height: 20.h), SizedBox(height: 20.h),
CustomButton( CustomButton(
@ -94,15 +145,9 @@ class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
// Unfocus all text fields and dismiss keyboard // Unfocus all text fields and dismiss keyboard
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
if (ValidationUtils.isValidatedIdAndPhoneWithCountryValidation( // Use ViewModel validation method
nationalId: authVm.nationalIdController.text, if (_authVm.validateIdAndPhone()) {
selectedCountry: authVm.selectedCountrySignup, // _authVm.addFamilyMember(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true);
phoneNumber: authVm.phoneNumberController.text,
onOkPress: () {
Navigator.of(context).pop();
},
)) {
// authVm.addFamilyMember(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true);
if (widget.onVerificationPress != null) { if (widget.onVerificationPress != null) {
widget.onVerificationPress!(); widget.onVerificationPress!();
} }

@ -129,6 +129,20 @@ class TextInputWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>(); AppState appState = getIt.get<AppState>();
final errorColor = AppColors.primaryRedColor; final errorColor = AppColors.primaryRedColor;
// Determine border: always show red border on error, otherwise respect isBorderAllowed
BorderSide? borderSide;
if (hasError) {
// Always show red border when there's an error
borderSide = BorderSide(color: errorColor, width: 1);
} else if (isBorderAllowed) {
// Show normal border when allowed and no error
borderSide = BorderSide(color: const Color(0xffefefef), width: 1);
} else {
// No border
borderSide = null;
}
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -140,7 +154,7 @@ class TextInputWidget extends StatelessWidget {
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
borderRadius: isAllowRadius ? (12.r) : null, borderRadius: isAllowRadius ? (12.r) : null,
side: isBorderAllowed ? BorderSide(color: hasError ? errorColor : const Color(0xffefefef), width: 1) : null, side: borderSide,
), ),
child: Row( child: Row(
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
@ -190,7 +204,7 @@ class TextInputWidget extends StatelessWidget {
], ],
), ),
), ),
if (hasError && errorMessage != null) if (errorMessage != null && errorMessage!.isNotEmpty)
Padding( Padding(
padding: EdgeInsets.only(top: 4.h, left: 12.h), // Adjust padding as needed padding: EdgeInsets.only(top: 4.h, left: 12.h), // Adjust padding as needed
child: Text( child: Text(
@ -297,10 +311,87 @@ class TextInputWidget extends StatelessWidget {
return labelText.toText12(isBold: true, color: labelColor ?? AppColors.inputLabelTextColor); return labelText.toText12(isBold: true, color: labelColor ?? AppColors.inputLabelTextColor);
} }
// Original _buildTextField - kept as backup reference
// Widget _buildTextFieldOriginal(BuildContext context) {
// double fontS = fontSize ?? 14.f;
// final isArabic = getIt.get<AppState>().isArabic();
//
// return Builder(
// builder: (context) {
// return Directionality(
// textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr,
// child: Localizations.override(
// context: context,
// locale: const Locale('en', 'US'), // Force English locale for TextField
// child: TextField(
// hintLocales: const [Locale('en', 'US')],
// enabled: isEnable,
// scrollPadding: EdgeInsets.zero,
// keyboardType: isMultiline ? TextInputType.multiline : (isWalletAmountInput! ? const TextInputType.numberWithOptions(decimal: true) : keyboardType),
// controller: controller,
// readOnly: isReadOnly,
// textAlignVertical: TextAlignVertical.top,
// textAlign: isArabic ? TextAlign.right : TextAlign.left,
// textDirection: ui.TextDirection.ltr,
// onChanged: onChange,
// focusNode: focusNode ?? _focusNode,
// autofocus: autoFocus,
// textInputAction: TextInputAction.done,
// cursorHeight: isWalletAmountInput! ? 40.h : 20.h,
// maxLength: isWalletAmountInput! ? 7 : 100,
// inputFormatters: isWalletAmountInput!
// ? [
// _ThousandSeparatorInputFormatter(),
// ]
// : null,
// onTapOutside: (event) {
// FocusManager.instance.primaryFocus?.unfocus();
// },
// onSubmitted: onSubmitted,
// minLines: isMultiline ? minLines : 1,
// maxLines: isMultiline ? maxLines : 1,
// style: TextStyle(
// fontSize: fontS,
// height: isMultiline ? 1.2 : (isWalletAmountInput! ? 1 / 4 : 0),
// fontWeight: FontWeight.w600,
// color: AppColors.textColor,
// letterSpacing: -1,
// fontFamily: fontFamily,
// locale: const Locale('en', 'US'), // Force English locale for text style
// ),
// decoration: InputDecoration(
// counterText: "",
// isDense: true,
// hintText: hintText,
// hintStyle: TextStyle(
// fontFamily: isArabic ? 'CairoArabic' : 'Poppins',
// fontSize: 14.f,
// height: 21 / 16,
// fontWeight: FontWeight.w600,
// color: hintColor != null ? AppColors.textColor : Color(0xff898A8D),
// letterSpacing: -0.75,
// ),
// prefixIconConstraints: BoxConstraints(minWidth: 30.h),
// prefixIcon: prefix == null ? null : "+${prefix!}".toText14(letterSpacing: -1, color: AppColors.textColor, isBold: true),
// contentPadding: EdgeInsets.only(right: isArabic ? 10.w : 0),
// border: InputBorder.none,
// focusedBorder: InputBorder.none,
// enabledBorder: InputBorder.none,
// ),
// ),
// ),
// );
// },
// );
// }
Widget _buildTextField(BuildContext context) { Widget _buildTextField(BuildContext context) {
double fontS = fontSize ?? 14.f; double fontS = fontSize ?? 14.f;
final isArabic = getIt.get<AppState>().isArabic(); final isArabic = getIt.get<AppState>().isArabic();
// Note: Error border is already handled by the parent Container in build() method
// The Container shows red border when hasError is true
return Builder( return Builder(
builder: (context) { builder: (context) {
return Directionality( return Directionality(
@ -362,6 +453,8 @@ class TextInputWidget extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
focusedBorder: InputBorder.none, focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none, enabledBorder: InputBorder.none,
// Show red border when field has error - but we handle this in the parent Container
// The parent Container in build() already shows red border based on hasError flag
), ),
), ),
), ),

Loading…
Cancel
Save