Input Field Error Handling

dev_aamir
Aamir CSol 8 hours ago
parent f47809777d
commit 1911f7ce64

@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts {
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

@ -95,6 +95,79 @@ class AuthenticationViewModel extends ChangeNotifier {
final ValueNotifier<bool> otpScreenNotifier = ValueNotifier<bool>(false);
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 = '';
@ -138,8 +211,342 @@ class AuthenticationViewModel extends ChangeNotifier {
_appState.setNHICUserData = CheckUserStatusResponseNHIC();
getIt.get<SymptomsCheckerViewModel>().setSelectedHeight(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) {
selectedCountrySignup = country;
notifyListeners();
@ -173,11 +580,13 @@ class AuthenticationViewModel extends ChangeNotifier {
void onMaritalStatusChange(String? status) {
maritalStatus = MaritalStatusTypeExtension.fromType(status)!;
clearMaritalStatusError();
notifyListeners();
}
void onGenderChange(String? status) {
genderType = GenderTypeExtension.fromType(status)!;
clearGenderError();
notifyListeners();
}
@ -186,7 +595,8 @@ class AuthenticationViewModel extends ChangeNotifier {
}
void onUAEUserCountrySelection(String? value) {
pickedCountryByUAEUser = countriesList!.firstWhere((element) => element.name == value);
pickedCountryByUAEUser = countriesList!.firstWhere((element) => element.name == value || element.nameN == value);
clearCountryError();
notifyListeners();
}

@ -52,6 +52,62 @@ class ContactUsViewModel extends ChangeNotifier {
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});
initContactUsViewModel() async {
@ -95,7 +151,12 @@ class ContactUsViewModel extends ChangeNotifier {
setIsSendFeedbackTabSelected(bool 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!)}");
}
notifyListeners();

@ -22,6 +22,61 @@ class DoctorFilterViewModel extends ChangeNotifier{
String? selectedClinicForFilters;
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() {
searchedRegionList.clear();
searchedHospitalList.clear();
@ -36,6 +91,7 @@ class DoctorFilterViewModel extends ChangeNotifier{
selectedHospitalForFilters = null;
selectedRegionForFilters = [];
applyFilters = false;
clearAllFilterErrors();
notifyListeners();
}
@ -76,6 +132,7 @@ class DoctorFilterViewModel extends ChangeNotifier{
void setSelectedHospital(PatientDoctorAppointmentList? hospital) {
selectedHospitalForFilters = hospital;
clearHospitalError();
notifyListeners();
}
@ -91,6 +148,7 @@ class DoctorFilterViewModel extends ChangeNotifier{
void setSelectedClinicForFilter(String? clinic) {
selectedClinicForFilters = clinic;
clearClinicError();
notifyListeners();
}

@ -33,6 +33,69 @@ class HabibWalletViewModel extends ChangeNotifier {
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});
initHabibWalletProvider() {
@ -57,6 +120,7 @@ class HabibWalletViewModel extends ChangeNotifier {
setSelectedHospital(HospitalsModel hospital) {
selectedHospital = hospital;
clearHospitalError();
notifyListeners();
}

@ -34,6 +34,102 @@ class HealthTrackersViewModel extends ChangeNotifier {
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> 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) {
final isArabic = getIt.get<AppState>().isArabic();
@ -1111,7 +1262,44 @@ class HealthTrackersViewModel extends ChangeNotifier {
// ==================== 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) {
final isArabic = getIt.get<AppState>().isArabic();
@ -1190,7 +1378,67 @@ class HealthTrackersViewModel extends ChangeNotifier {
// ==================== 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) {
final isArabic = getIt.get<AppState>().isArabic();

@ -49,6 +49,96 @@ class ProfileSettingsViewModel extends ChangeNotifier {
String? profileImageData;
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({
required CacheService cacheService,
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/utils/size_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/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
@ -35,17 +34,33 @@ class LoginScreenState extends State<LoginScreen> {
void initState() {
super.initState();
_nationalIdFocusNode = FocusNode();
// Clear errors when entering login screen
WidgetsBinding.instance.addPostFrameCallback((_) {
final authVm = context.read<AuthenticationViewModel>();
authVm.clearNationalIdError();
authVm.clearPhoneNumberError();
});
}
@override
void 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();
}
@override
Widget build(BuildContext context) {
AuthenticationViewModel authVm = context.read<AuthenticationViewModel>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
appBar: CustomAppBar(
@ -56,94 +71,103 @@ class LoginScreenState extends State<LoginScreen> {
context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA'));
},
),
body: GestureDetector(
onTap: () {
// Dismiss the keyboard and unfocus any focused widget when tapping outside
_nationalIdFocusNode.unfocus();
FocusScope.of(context).unfocus();
},
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.h),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.showLottie(context: context, assetPath: AppAnimations.login, width: 200.h, height: 200.h, repeat: true, fit: BoxFit.cover),
// SizedBox(height: 130.h),
SizedBox(height: MediaQuery.of(context).size.height * 0.12), // based on the screen height Adjusted to sizer unit
LocaleKeys.welcomeToDrSulaiman.tr(context: context).toText32(isBold: true, color: AppColors.textColor ),
SizedBox(height: 32.h),
Localizations.override(context: context, locale: Locale('en', 'US'), child: Container()), // Force English locale for this widget
TextInputWidget(
labelText: "${LocaleKeys.nationalIdFileNumber.tr(context: context)}",
hintText: "xxxxxxxxx",
controller: authVm.nationalIdController,
focusNode: _nationalIdFocusNode,
keyboardType: TextInputType.text,
isEnable: true,
prefix: null,
autoFocus: true,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.h),
leadingIcon: AppAssets.student_card,
errorMessage: LocaleKeys.enterValidIDorIqama.tr(context: context),
hasError: false,
fontFamily: "Poppins",
),
SizedBox(height: 16.h),
CustomButton(
height: 50.h,
text: LocaleKeys.login.tr(context: context),
icon: AppAssets.login1,
iconColor: Colors.white,
onPressed: () {
_nationalIdFocusNode.unfocus();
FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatedId(
nationalId: authVm.nationalIdController.text,
onOkPress: () {
Navigator.of(context).pop();
})) {
showLoginModelSheet(context: context, phoneNumberController: authVm.phoneNumberController, authViewModel: authVm);
}
},
),
SizedBox(height: 10.h),
Center(
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: context.dynamicTextStyle(color: AppColors.textColor, fontSize: 14.f, height: 26 / 16, fontWeight: FontWeight.w600,),
children: <TextSpan>[
TextSpan(text: LocaleKeys.dontHaveAccount.tr(context: context), style: context.dynamicTextStyle()),
TextSpan(text: " "),
TextSpan(
text: LocaleKeys.registernow.tr(context: context),
style: context.dynamicTextStyle(
color: AppColors.primaryRedColor,
fontSize: 14.f, // Adjusted to sizer unit
height: 26 / 16, // Ratio
fontWeight: FontWeight.w600,),
recognizer: TapGestureRecognizer()
..onTap = () {
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context) => RegisterNew()),
);
},
body: Consumer<AuthenticationViewModel>(
builder: (context, authVm, child) {
return GestureDetector(
onTap: () {
// Dismiss the keyboard and unfocus any focused widget when tapping outside
_nationalIdFocusNode.unfocus();
FocusScope.of(context).unfocus();
},
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.h),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.showLottie(context: context, assetPath: AppAnimations.login, width: 200.h, height: 200.h, repeat: true, fit: BoxFit.cover),
// SizedBox(height: 130.h),
SizedBox(height: MediaQuery.of(context).size.height * 0.12), // based on the screen height Adjusted to sizer unit
LocaleKeys.welcomeToDrSulaiman.tr(context: context).toText32(isBold: true, color: AppColors.textColor ),
SizedBox(height: 32.h),
Localizations.override(context: context, locale: Locale('en', 'US'), child: Container()), // Force English locale for this widget
TextInputWidget(
labelText: LocaleKeys.nationalIdFileNumber.tr(context: context),
hintText: "xxxxxxxxx",
controller: authVm.nationalIdController,
focusNode: _nationalIdFocusNode,
keyboardType: TextInputType.text,
isEnable: true,
prefix: null,
autoFocus: true,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.h),
leadingIcon: AppAssets.student_card,
errorMessage: authVm.nationalIdError,
hasError: authVm.nationalIdError != null,
fontFamily: "Poppins",
onChange: (value) {
// Clear error when user starts typing
authVm.clearNationalIdError();
},
),
SizedBox(height: 16.h),
CustomButton(
height: 50.h,
text: LocaleKeys.login.tr(context: context),
icon: AppAssets.login1,
iconColor: Colors.white,
onPressed: () {
_nationalIdFocusNode.unfocus();
FocusScope.of(context).unfocus();
// Use ViewModel validation method
if (authVm.validateNationalId()) {
showLoginModelSheet(
context: context,
phoneNumberController: authVm.phoneNumberController,
authViewModel: authVm,
);
}
},
),
SizedBox(height: 10.h),
Center(
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: context.dynamicTextStyle(color: AppColors.textColor, fontSize: 14.f, height: 26 / 16, fontWeight: FontWeight.w600,),
children: <TextSpan>[
TextSpan(text: LocaleKeys.dontHaveAccount.tr(context: context), style: context.dynamicTextStyle()),
TextSpan(text: " "),
TextSpan(
text: LocaleKeys.registernow.tr(context: context),
style: context.dynamicTextStyle(
color: AppColors.primaryRedColor,
fontSize: 14.f, // Adjusted to sizer unit
height: 26 / 16, // Ratio
fontWeight: FontWeight.w600,),
recognizer: TapGestureRecognizer()
..onTap = () {
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context) => RegisterNew()),
);
},
),
],
),
],
).withVerticalPadding(2.h),
),
).withVerticalPadding(2.h),
SizedBox(height: 20.h),
],
),
SizedBox(height: 20.h),
],
),
),
),
),
);
},
),
);
}
@ -154,6 +178,7 @@ class LoginScreenState extends State<LoginScreen> {
required AuthenticationViewModel authViewModel,
}) async {
AppState appState = getIt<AppState>();
context.showBottomSheet(
isScrollControlled: true,
isDismissible: false,
@ -161,80 +186,85 @@ class LoginScreenState extends State<LoginScreen> {
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width),
backgroundColor: AppColors.transparent,
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(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: SingleChildScrollView(
child: GenericBottomSheet(
countryCode: authViewModel.selectedCountrySignup.countryCode,
initialPhoneNumber: "",
textController: phoneNumberController,
isEnableCountryDropdown: true,
onCountryChange: (country) {
authViewModel.onCountryChange(country);
setModalState(() {});
},
onChange: authViewModel.onPhoneNumberChange,
buttons: [
if (authViewModel.selectedCountrySignup != CountryEnum.others)
Padding(
padding: EdgeInsets.only(bottom: 10.h),
child: CustomButton(
text: LocaleKeys.sendOTPSMS.tr(context: context),
onPressed: () async {
if (ValidationUtils.isValidatePhone(
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,
borderColor: AppColors.primaryRedBorderColor,
textColor: Colors.white,
iconColor: Colors.white,
icon: AppAssets.message,
),
),
if (authViewModel.selectedCountrySignup != CountryEnum.others)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
child: Consumer<AuthenticationViewModel>(
builder: (context, authVm, child) {
return GenericBottomSheet(
countryCode: authVm.selectedCountrySignup.countryCode,
initialPhoneNumber: "",
textController: phoneNumberController,
isEnableCountryDropdown: true,
onCountryChange: (country) {
authVm.onCountryChange(country);
// Clear error when country changes
authVm.clearPhoneNumberError();
},
onChange: (value) {
authVm.onPhoneNumberChange(value);
// Clear error when user starts typing
authVm.clearPhoneNumberError();
},
phoneNumberError: authVm.phoneNumberError,
buttons: [
if (authVm.selectedCountrySignup != CountryEnum.others)
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.h),
child: LocaleKeys.oR.tr(context: context).toText16(color: AppColors.textColor),
padding: EdgeInsets.only(bottom: 10.h),
child: CustomButton(
text: LocaleKeys.sendOTPSMS.tr(context: context),
onPressed: () async {
validatePhoneAndProceed(OTPTypeEnum.sms);
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedBorderColor,
textColor: Colors.white,
iconColor: Colors.white,
icon: AppAssets.message,
),
),
],
),
Padding(
padding: EdgeInsets.only(bottom: 10.h, top: 10.h),
child: CustomButton(
text: LocaleKeys.sendOTPWHATSAPP.tr(context: context),
onPressed: () async {
if (ValidationUtils.isValidatePhone(
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,
borderColor: AppColors.textColor,
textColor: AppColors.textColor,
icon: AppAssets.whatsapp,
iconColor: null,
applyThemeColor: false,
),
),
],
if (authVm.selectedCountrySignup != CountryEnum.others)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.h),
child: LocaleKeys.oR.tr(context: context).toText16(color: AppColors.textColor),
),
],
),
Padding(
padding: EdgeInsets.only(bottom: 10.h, top: 10.h),
child: CustomButton(
text: LocaleKeys.sendOTPWHATSAPP.tr(context: context),
onPressed: () async {
validatePhoneAndProceed(OTPTypeEnum.whatsapp);
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.textColor,
textColor: AppColors.textColor,
icon: AppAssets.whatsapp,
iconColor: null,
applyThemeColor: false,
),
),
],
);
},
),
),
);

@ -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/utils/size_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/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
@ -34,19 +33,35 @@ class _RegisterNew extends State<RegisterNew> {
super.initState();
_nationalIdFocusNode = FocusNode();
_dobFocusNode = FocusNode();
// Clear errors when entering register screen
WidgetsBinding.instance.addPostFrameCallback((_) {
final authVm = context.read<AuthenticationViewModel>();
authVm.clearNationalIdError();
authVm.clearDobError();
authVm.clearPhoneNumberError();
});
}
@override
void dispose() {
_nationalIdFocusNode.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();
}
@override
Widget build(BuildContext context) {
AuthenticationViewModel authVm = context.read<AuthenticationViewModel>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
appBar: CustomAppBar(
@ -57,199 +72,216 @@ class _RegisterNew extends State<RegisterNew> {
context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA'));
},
),
body: GestureDetector(
onTap: () {
// Dismiss keyboard and unfocus all input fields
_nationalIdFocusNode.unfocus();
_dobFocusNode.unfocus();
FocusScope.of(context).unfocus();
},
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(overscroll: false, physics: const ClampingScrollPhysics()),
child: NotificationListener<OverscrollIndicatorNotification>(
onNotification: (notification) {
notification.disallowIndicator();
return true;
body: Consumer<AuthenticationViewModel>(
builder: (context, authVm, child) {
return GestureDetector(
onTap: () {
// Dismiss keyboard and unfocus all input fields
_nationalIdFocusNode.unfocus();
_dobFocusNode.unfocus();
FocusScope.of(context).unfocus();
},
child: SingleChildScrollView(
physics: ClampingScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 24.h),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.showLottie(context: context, assetPath: 'assets/animations/lottie/register.json', width: 200.w, height: 200.h, fit: BoxFit.cover, repeat: true),
SizedBox(height: 16.h),
LocaleKeys.prepareToElevate.tr(context: context).toText32(isBold: true),
SizedBox(height: 24.h),
Directionality(
textDirection: Directionality.of(context),
child: Container(
decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24)),
padding: EdgeInsets.symmetric(horizontal: 16.h),
child: Column(
children: [
CustomCountryDropdown(
countryList: CountryEnum.values.where((c) => c != CountryEnum.others).toList(),
onCountryChange: authVm.onCountryChange,
// isRtl: Directionality.of(context) == TextDirection.LTR,
).withVerticalPadding(8.h),
Divider(height: 1.h),
TextInputWidget(
labelText: LocaleKeys.nationalIdNumber.tr(context: context),
hintText: "xxxxxxxxx",
controller: authVm.nationalIdController,
focusNode: _nationalIdFocusNode,
keyboardType: TextInputType.number,
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
autoFocus: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.student_card,
fontFamily: "Poppins",
).withVerticalPadding(8),
Divider(height: 1),
TextInputWidget(
labelText: LocaleKeys.dob.tr(context: context),
hintText: "11 July, 1994",
controller: authVm.dobController,
focusNode: _dobFocusNode,
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.birthday_cake,
selectionType: SelectionTypeEnum.calendar,
onCalendarTypeChanged: authVm.onCalenderTypeChange,
onChange: authVm.onDobChange,
fontFamily: "Poppins",
).withVerticalPadding(8),
],
),
),
),
SizedBox(height: 25.h),
GestureDetector(
onTap: authVm.onTermAccepted,
child: Row(
children: [
Selector<AuthenticationViewModel, bool>(
selector: (_, viewModel) => viewModel.isTermsAccepted,
shouldRebuild: (previous, next) => previous != next,
builder: (context, isTermsAccepted, child) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 24.h,
width: 24.h,
decoration: BoxDecoration(
color: isTermsAccepted ? AppColors.primaryRedColor : Colors.transparent,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: isTermsAccepted ? AppColors.primaryRedBorderColor : AppColors.greyColor, width: 2.h),
),
child: isTermsAccepted ? Icon(Icons.check, size: 16.f, color: Colors.white) : null,
);
},
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(overscroll: false, physics: const ClampingScrollPhysics()),
child: NotificationListener<OverscrollIndicatorNotification>(
onNotification: (notification) {
notification.disallowIndicator();
return true;
},
child: SingleChildScrollView(
physics: ClampingScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 24.h),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.showLottie(context: context, assetPath: 'assets/animations/lottie/register.json', width: 200.w, height: 200.h, fit: BoxFit.cover, repeat: true),
SizedBox(height: 16.h),
LocaleKeys.prepareToElevate.tr(context: context).toText32(isBold: true),
SizedBox(height: 24.h),
Directionality(
textDirection: Directionality.of(context),
child: Container(
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),
child: Column(
children: [
CustomCountryDropdown(
countryList: CountryEnum.values.where((c) => c != CountryEnum.others).toList(),
onCountryChange: authVm.onCountryChange,
// isRtl: Directionality.of(context) == TextDirection.LTR,
).withVerticalPadding(8.h),
Divider(height: 1.h),
TextInputWidget(
labelText: LocaleKeys.nationalIdNumber.tr(context: context),
hintText: "xxxxxxxxx",
controller: authVm.nationalIdController,
focusNode: _nationalIdFocusNode,
keyboardType: TextInputType.number,
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
autoFocus: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.student_card,
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),
Divider(height: 1),
TextInputWidget(
labelText: LocaleKeys.dob.tr(context: context),
hintText: "11 July, 1994",
controller: authVm.dobController,
focusNode: _dobFocusNode,
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.birthday_cake,
selectionType: SelectionTypeEnum.calendar,
onCalendarTypeChanged: authVm.onCalenderTypeChange,
onChange: (value) {
authVm.onDobChange(value);
// Clear error when user selects a date
authVm.clearDobError();
},
fontFamily: "Poppins",
hasError: false, // Don't show individual field border
errorMessage: authVm.dobError, // Show error message if exists
).withVerticalPadding(8),
],
),
),
SizedBox(width: 12.h),
Row(
),
SizedBox(height: 25.h),
GestureDetector(
onTap: authVm.onTermAccepted,
child: Row(
children: [
Text(
LocaleKeys.iAcceptThe.tr(context: context),
style: context.dynamicTextStyle(fontSize: 14.f, fontWeight: FontWeight.w600, color: Color(0xFF2E3039)),
),
GestureDetector(
onTap: () {
// Navigate to terms and conditions page
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Terms.aspx',
Selector<AuthenticationViewModel, bool>(
selector: (_, viewModel) => viewModel.isTermsAccepted,
shouldRebuild: (previous, next) => previous != next,
builder: (context, isTermsAccepted, child) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 24.h,
width: 24.h,
decoration: BoxDecoration(
color: isTermsAccepted ? AppColors.primaryRedColor : Colors.transparent,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: isTermsAccepted ? AppColors.primaryRedBorderColor : AppColors.greyColor, width: 2.h),
),
child: isTermsAccepted ? Icon(Icons.check, size: 16.f, color: Colors.white) : null,
);
},
child: Text(
" ${LocaleKeys.termsConditoins.tr(context: context)}",
style: context.dynamicTextStyle(
fontSize: 14.f,
fontWeight: FontWeight.w600,
color: AppColors.primaryRedColor,
decoration: TextDecoration.underline,
decorationColor: AppColors.primaryRedBorderColor,
),
SizedBox(width: 12.h),
Row(
children: [
Text(
LocaleKeys.iAcceptThe.tr(context: context),
style: context.dynamicTextStyle(fontSize: 14.f, fontWeight: FontWeight.w600, color: Color(0xFF2E3039)),
),
),
GestureDetector(
onTap: () {
// Navigate to terms and conditions page
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Terms.aspx',
);
},
child: Text(
" ${LocaleKeys.termsConditoins.tr(context: context)}",
style: context.dynamicTextStyle(
fontSize: 14.f,
fontWeight: FontWeight.w600,
color: AppColors.primaryRedColor,
decoration: TextDecoration.underline,
decorationColor: AppColors.primaryRedBorderColor,
),
),
),
],
),
// Expanded(
// child: Text(
// LocaleKeys.iAcceptTermsConditions.tr(context: context).split("the").first,
// style: context.dynamicTextStyle(fontSize: 14.fSize, isBold: true, color: Color(0xFF2E3039)),
// ),
// ),
],
),
// Expanded(
// child: Text(
// LocaleKeys.iAcceptTermsConditions.tr(context: context).split("the").first,
// style: context.dynamicTextStyle(fontSize: 14.fSize, isBold: true, color: Color(0xFF2E3039)),
// ),
// ),
],
),
),
SizedBox(height: 25.h),
CustomButton(
text: LocaleKeys.registernow.tr(context: context),
icon: AppAssets.note_edit,
onPressed: () {
// Dismiss keyboard before proceeding
_nationalIdFocusNode.unfocus();
_dobFocusNode.unfocus();
FocusScope.of(context).unfocus();
),
SizedBox(height: 25.h),
CustomButton(
text: LocaleKeys.registernow.tr(context: context),
icon: AppAssets.note_edit,
onPressed: () {
// Dismiss keyboard before proceeding
_nationalIdFocusNode.unfocus();
_dobFocusNode.unfocus();
FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatedId(
nationalId: authVm.nationalIdController.text,
selectedCountry: authVm.selectedCountrySignup,
isTermsAccepted: authVm.isTermsAccepted,
dob: authVm.dobController.text,
onOkPress: () {
Navigator.of(context).pop();
})) {
showRegisterModel(context: context, authVM: authVm);
}
},
),
SizedBox(height: 14),
Center(
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: context.dynamicTextStyle(
color: Colors.black,
fontSize: 16.f,
height: 26 / 16,
fontWeight: FontWeight.w600,
),
children: <TextSpan>[
TextSpan(text: LocaleKeys.alreadyHaveAccount.tr(context: context), style: context.dynamicTextStyle()),
TextSpan(text: " "),
TextSpan(
text: LocaleKeys.loginNow.tr(context: context),
// Use ViewModel validation method
if (authVm.validateRegistrationForm()) {
showRegisterModel(context: context, authVM: authVm);
}
},
),
SizedBox(height: 14),
Center(
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: context.dynamicTextStyle(
color: AppColors.primaryRedColor,
color: Colors.black,
fontSize: 16.f,
height: 26 / 16,
fontWeight: FontWeight.w600,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Navigator.of(context).pop();
},
children: <TextSpan>[
TextSpan(text: LocaleKeys.alreadyHaveAccount.tr(context: context), style: context.dynamicTextStyle()),
TextSpan(text: " "),
TextSpan(
text: LocaleKeys.loginNow.tr(context: context),
style: context.dynamicTextStyle(
color: AppColors.primaryRedColor,
fontSize: 16.f,
height: 26 / 16,
fontWeight: FontWeight.w600,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Navigator.of(context).pop();
},
),
],
),
],
),
),
),
SizedBox(height: 30.h),
],
),
SizedBox(height: 30.h),
],
),
),
),
),
),
);
},
));
}
@ -264,73 +296,75 @@ class _RegisterNew extends State<RegisterNew> {
builder: (bottomSheetContext) => Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(bottomSheetContext).viewInsets.bottom),
child: SingleChildScrollView(
child: GenericBottomSheet(
countryCode: authVM.selectedCountrySignup.countryCode,
initialPhoneNumber: authVM.phoneNumberController.text,
textController: authVM.phoneNumberController,
isEnableCountryDropdown: false,
onCountryChange: authVM.onCountryChange,
onChange: authVM.onPhoneNumberChange,
autoFocus: true,
buttons: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: CustomButton(
text: LocaleKeys.sendOTPSMS.tr(context: context),
onPressed: () async {
// Dismiss keyboard before validation
FocusScope.of(context).unfocus();
child: Consumer<AuthenticationViewModel>(
builder: (context, authVm, child) {
return GenericBottomSheet(
countryCode: authVm.selectedCountrySignup.countryCode,
initialPhoneNumber: authVm.phoneNumberController.text,
textController: authVm.phoneNumberController,
isEnableCountryDropdown: false,
onCountryChange: authVm.onCountryChange,
onChange: (value) {
authVm.onPhoneNumberChange(value);
// Clear error when user starts typing
authVm.clearPhoneNumberError();
},
autoFocus: true,
phoneNumberError: authVm.phoneNumberError,
buttons: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: CustomButton(
text: LocaleKeys.sendOTPSMS.tr(context: context),
onPressed: () async {
// Dismiss keyboard before validation
FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatePhone(
phoneNumber: authVM.phoneNumberController.text,
onOkPress: () {
Navigator.of(context).pop();
// Use ViewModel validation method
if (authVm.validatePhoneNumber()) {
appState.setSelectDeviceByImeiRespModelElement(null);
await authVm.onRegistrationStart(otpTypeEnum: OTPTypeEnum.sms);
}
},
)) {
appState.setSelectDeviceByImeiRespModelElement(null);
await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.sms);
}
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedBorderColor,
textColor: AppColors.whiteColor,
icon: AppAssets.message,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedBorderColor,
textColor: AppColors.whiteColor,
icon: AppAssets.message,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.h),
child: LocaleKeys.oR.tr(context: context).toText16(color: AppColors.textColor),
),
],
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.h),
child: LocaleKeys.oR.tr(context: context).toText16(color: AppColors.textColor),
padding: EdgeInsets.only(bottom: 10.h, top: 10.h),
child: CustomButton(
text: LocaleKeys.sendOTPWHATSAPP.tr(context: context),
onPressed: () async {
FocusScope.of(context).unfocus();
// Use ViewModel validation method
if (authVm.validatePhoneNumber()) {
appState.setSelectDeviceByImeiRespModelElement(null);
await authVm.onRegistrationStart(otpTypeEnum: OTPTypeEnum.whatsapp);
}
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.borderOnlyColor,
textColor: AppColors.textColor,
icon: AppAssets.whatsapp,
iconColor: null,
),
),
],
),
Padding(
padding: EdgeInsets.only(bottom: 10.h, top: 10.h),
child: CustomButton(
text: LocaleKeys.sendOTPWHATSAPP.tr(context: context),
onPressed: () async {
FocusScope.of(context).unfocus();
if (ValidationUtils.isValidatePhone(
phoneNumber: authVM.phoneNumberController.text,
onOkPress: () {
Navigator.of(context).pop();
},
)) {
appState.setSelectDeviceByImeiRespModelElement(null);
await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.whatsapp);
}
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.borderOnlyColor,
textColor: AppColors.textColor,
icon: AppAssets.whatsapp,
iconColor: null,
),
),
],
);
},
),
),
),

@ -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/utils/date_util.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/widget_extensions.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>();
insuranceVM = context.read<InsuranceViewModel>();
// Clear errors when entering the page
WidgetsBinding.instance.addPostFrameCallback((_) {
authVM?.clearAllStep2FieldErrors();
});
// Call insurance API to fetch data
WidgetsBinding.instance.addPostFrameCallback((_) {
debugPrint("Registration Step 2: Calling insurance API");
@ -51,6 +55,10 @@ class _RegisterNew extends State<RegisterNewStep2> {
@override
void dispose() {
// Clear errors when leaving the page
WidgetsBinding.instance.addPostFrameCallback((_) {
authVM?.clearAllStep2FieldErrors();
});
super.dispose();
}
@ -107,18 +115,17 @@ class _RegisterNew extends State<RegisterNewStep2> {
icon: AppAssets.confirm,
iconColor: AppColors.whiteColor,
onPressed: () {
// Unfocus keyboard
FocusScope.of(context).unfocus();
// For UAE users, validate the form first
if (appState.getUserRegistrationPayload.zipCode != CountryEnum.saudiArabia.countryCode) {
if (ValidationUtils.validateUaeRegistration(
name: authVM!.nameController.text,
gender: authVM!.genderType,
country: authVM!.pickedCountryByUAEUser,
maritalStatus: authVM!.maritalStatus,
onOkPress: () {
Navigator.of(context).pop();
})) {
// Use ViewModel validation method
if (authVM!.validateRegistrationStep2Form()) {
showModel(context: context);
}
} else {
// For Saudi users, no validation needed, show email modal directly
showModel(context: context);
}
},
@ -166,10 +173,21 @@ class _RegisterNew extends State<RegisterNewStep2> {
},
),
Container(
decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24)),
padding: EdgeInsets.only(left: 16.h, right: 16.h),
child: Column(
// Form Container with Error Border
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),
child: Column(
children: [
TextInputWidget(
labelText: authVM!.isUserFromUAE() ? LocaleKeys.fullName.tr(context: context) : LocaleKeys.name.tr(context: context),
@ -184,11 +202,27 @@ class _RegisterNew extends State<RegisterNewStep2> {
onSubmitted: (value) {
FocusScope.of(context).unfocus();
},
onChange: (value) {
// Clear error when user starts typing
authVM!.clearNameError();
},
isAllowLeadingIcon: true,
isReadOnly: authVM!.isUserFromUAE() ? false : true,
leadingIcon: AppAssets.user_circle,
labelColor: AppColors.textColor,
).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),
TextInputWidget(
labelText: LocaleKeys.nationalIdNumber.tr(context: context),
@ -240,6 +274,18 @@ class _RegisterNew extends State<RegisterNewStep2> {
labelColor: AppColors.textColor,
onChange: (value) {})
.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),
authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel, MaritalStatusTypeEnum?>(
@ -279,6 +325,18 @@ class _RegisterNew extends State<RegisterNewStep2> {
leadingIcon: AppAssets.smart_phone,
onChange: (value) {})
.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),
authVM!.isUserFromUAE()
? Selector<AuthenticationViewModel, ({List<NationalityCountries>? countriesList, NationalityCountries? selectedCountry, bool isArabic})>(
@ -329,6 +387,18 @@ class _RegisterNew extends State<RegisterNewStep2> {
leadingIcon: AppAssets.globe,
onChange: (value) {})
.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(
height: 1,
color: AppColors.greyColor,
@ -365,6 +435,8 @@ class _RegisterNew extends State<RegisterNewStep2> {
).paddingSymmetrical(0.h, 8.h),
],
),
);
},
),
SizedBox(height: 50.h),
// Row(
@ -438,12 +510,17 @@ class _RegisterNew extends State<RegisterNewStep2> {
child: CustomButton(
text: LocaleKeys.submit.tr(context: context),
onPressed: () {
if (ValidationUtils.isValidateEmail(
email: authVM!.emailController.text,
onOkPress: () {
Navigator.of(context).pop();
})) {
// Use ViewModel validation method
if (authVM!.validateEmail()) {
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,

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

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

@ -1,5 +1,3 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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 'dart:ui' as ui;
class RechargeWalletPage extends StatefulWidget {
const RechargeWalletPage({super.key});
@ -33,7 +29,7 @@ class RechargeWalletPage extends StatefulWidget {
}
class _RechargeWalletPageState extends State<RechargeWalletPage> {
FocusNode textFocusNode = FocusNode();
late FocusNode _amountFocusNode;
late HabibWalletViewModel habibWalletVM;
late AppState appState;
@ -42,88 +38,152 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
@override
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}",
appState.getAuthenticatedUser()!.mobileNumber!);
habibWalletVM.setSelectedRechargeType(0);
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
Widget build(BuildContext context) {
habibWalletVM = Provider.of<HabibWalletViewModel>(context, listen: false);
appState = getIt.get<AppState>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: CollapsingListView(
title: LocaleKeys.rechargePageKey.tr(context: context),
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(24.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 135.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
side: BorderSide(color: AppColors.textColor, width: 2.h),
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//TODO: Check with hussain to show AED or SAR
LocaleKeys.amount.tr(context: context).toText14(color: AppColors.greyTextColor, isBold: true),
Spacer(),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
return PopScope(
onPopInvokedWithResult: (bool didPop, dynamic result) {
if (didPop) {
// Clear errors when user navigates back
habibWalletVM.clearAllRechargeErrors();
}
},
child: Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
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,
children: [
Expanded(
child: CollapsingListView(
title: LocaleKeys.rechargePageKey.tr(context: context),
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(24.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Consumer<HabibWalletViewModel>(
builder: (context, viewModel, child) {
return Container(
height: 135.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
side: BorderSide(
color: viewModel.amountError != null ? AppColors.primaryRedBorderColor : AppColors.textColor,
width: 2.h,
),
),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 150.h,
child: TextInputWidget(
controller: amountTextController,
labelText: "",
hintText: "",
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
autoFocus: true,
fontSize: 25.f,
padding: EdgeInsets.symmetric(horizontal: 8.h, vertical: 0.h),
focusNode: textFocusNode,
isWalletAmountInput: true,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
fontFamily: "Poppins",
// leadingIcon: AppAssets.student_card,
),
//TODO: Check with hussain to show AED or SAR
LocaleKeys.amount.tr(context: context).toText14(color: AppColors.greyTextColor, isBold: true),
Spacer(),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(
width: 150.h,
child: TextInputWidget(
controller: amountTextController,
labelText: "",
hintText: "",
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
autoFocus: true,
fontSize: 25.f,
padding: EdgeInsets.symmetric(horizontal: 8.h, vertical: 0.h),
focusNode: _amountFocusNode,
isWalletAmountInput: true,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
fontFamily: "Poppins",
onChange: (value) {
// Clear error when user starts typing
viewModel.clearAmountError();
},
// leadingIcon: AppAssets.student_card,
),
),
const Spacer(),
LocaleKeys.sar.tr(context: context).toText20(color: AppColors.greyTextColor, isBold: true),
],
),
const Spacer(),
LocaleKeys.sar.tr(context: context).toText20(color: AppColors.greyTextColor, isBold: true),
],
),
],
),
),
),
);
},
),
// 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),
Consumer<HabibWalletViewModel>(builder: (context, habibWalletVM, child) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
borderRadius: BorderRadius.circular(24.h),
border: Border.all(
color: habibWalletVM.hospitalError != null ? AppColors.primaryRedBorderColor : Colors.transparent,
width: 2.h,
),
),
child: Padding(
padding: EdgeInsets.all(16.h),
@ -194,6 +254,18 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
showCommonBottomSheetWithoutHeight(context,
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),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h),
@ -249,27 +321,11 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
child: CustomButton(
text: LocaleKeys.next.tr(context: context),
onPressed: () {
if (amountTextController.text.isEmpty) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.enterAmount.tr(context: context)),
callBackFunc: () {
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 {
_amountFocusNode.unfocus();
FocusScope.of(context).unfocus();
// Use ViewModel validation method
if (habibWalletVM.validateRechargeForm(amountTextController.text)) {
habibWalletVM.setWalletRechargeAmount(num.parse(amountTextController.text.replaceAll(',', '')));
habibWalletVM.setNotesText(notesTextController.text);
// 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() {
super.initState();
dialogService = getIt.get<DialogService>();
// Clear errors when entering the page
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<HealthTrackersViewModel>();
viewModel.clearAllFieldErrors();
});
}
@override
void dispose() {
dateController.dispose();
timeController.dispose();
// Clear errors when leaving the page
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
final viewModel = context.read<HealthTrackersViewModel>();
viewModel.clearAllFieldErrors();
}
});
super.dispose();
}
@ -94,10 +109,17 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
// Save Blood Sugar entry
Future<void> _saveBloodSugarEntry(HealthTrackersViewModel viewModel) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Combine date and time
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
await viewModel.saveBloodSugarEntry(
dateTime: dateTime,
@ -115,10 +137,17 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
// Save Weight entry
Future<void> _saveWeightEntry(HealthTrackersViewModel viewModel) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Combine date and time
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
await viewModel.saveWeightEntry(
dateTime: dateTime,
@ -135,10 +164,17 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
// Save Blood Pressure entry
Future<void> _saveBloodPressureEntry(HealthTrackersViewModel viewModel) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Combine date and time
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
await viewModel.saveBloodPressureEntry(
dateTime: dateTime,
@ -286,13 +322,19 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
}
// 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(
controller: controller,
keyboardType: keyboardType,
maxLines: 1,
cursorHeight: 14.h,
textAlignVertical: TextAlignVertical.center,
onChanged: onChanged,
decoration: InputDecoration(
border: InputBorder.none,
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
Widget _buildSettingsRow({
required String icon,
@ -401,133 +460,230 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
/// Blood Sugar form fields
Widget _buildBloodSugarForm(HealthTrackersViewModel viewModel) {
return Column(
children: [
_buildSettingsRow(
icon: AppAssets.heightIcon,
label: LocaleKeys.enterBloodSugar.tr(context: context),
inputField: _buildTextField(viewModel.bloodSugarController, '', keyboardType: TextInputType.number),
unit: viewModel.selectedBloodSugarUnit,
onUnitTap: () => _showBloodSugarUnitSelectionBottomSheet(context, viewModel),
),
_buildDateTimeFields(),
Divider(height: 1, color: AppColors.dividerColor),
_buildSettingsRow(
icon: AppAssets.weight_tracker_icon,
label: LocaleKeys.selectMeasureTime.tr(context: context),
value: viewModel.selectedBloodSugarMeasureTime,
onRowTap: () => _showBloodSugarEntryTimeBottomSheet(context, viewModel),
),
],
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: [
_buildSettingsRow(
icon: AppAssets.heightIcon,
label: LocaleKeys.enterBloodSugar.tr(context: context),
inputField: _buildTextField(
viewModel.bloodSugarController,
'',
keyboardType: TextInputType.number,
onChanged: (value) => viewModel.clearBloodSugarError(),
),
unit: viewModel.selectedBloodSugarUnit,
onUnitTap: () => _showBloodSugarUnitSelectionBottomSheet(context, viewModel),
),
if (viewModel.bloodSugarError != null)
_buildErrorText(viewModel.bloodSugarError!),
_buildDateTimeFields(),
Divider(height: 1, color: AppColors.dividerColor),
_buildSettingsRow(
icon: AppAssets.weight_tracker_icon,
label: LocaleKeys.selectMeasureTime.tr(context: context),
value: viewModel.selectedBloodSugarMeasureTime,
onRowTap: () => _showBloodSugarEntryTimeBottomSheet(context, viewModel),
),
if (viewModel.bloodSugarMeasureTimeError != null)
_buildErrorText(viewModel.bloodSugarMeasureTimeError!),
],
),
);
},
);
}
/// Blood Pressure form fields
Widget _buildBloodPressureForm(HealthTrackersViewModel viewModel) {
return Column(
children: [
_buildSettingsRow(
icon: AppAssets.bloodPressureIcon,
iconColor: AppColors.greyTextColor,
label: LocaleKeys.enterSystolicValue.tr(context: context),
inputField: _buildTextField(viewModel.systolicController, '', keyboardType: TextInputType.number),
),
_buildSettingsRow(
icon: AppAssets.bloodPressureIcon,
iconColor: AppColors.greyTextColor,
label: LocaleKeys.enterDiastolicValue.tr(context: context),
inputField: _buildTextField(viewModel.diastolicController, '', keyboardType: TextInputType.number),
),
_buildSettingsRow(
icon: AppAssets.bodyIcon,
iconColor: AppColors.greyTextColor,
label: LocaleKeys.selectArm.tr(context: context),
value: viewModel.selectedMeasuredArmDisplay,
onRowTap: () => _showMeasuredArmSelectionBottomSheet(context, viewModel),
),
_buildDateTimeFields(),
],
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: [
_buildSettingsRow(
icon: AppAssets.bloodPressureIcon,
iconColor: AppColors.greyTextColor,
label: LocaleKeys.enterSystolicValue.tr(context: context),
inputField: _buildTextField(
viewModel.systolicController,
'',
keyboardType: TextInputType.number,
onChanged: (value) => viewModel.clearSystolicError(),
),
),
if (viewModel.systolicError != null)
_buildErrorText(viewModel.systolicError!),
_buildSettingsRow(
icon: AppAssets.bloodPressureIcon,
iconColor: AppColors.greyTextColor,
label: LocaleKeys.enterDiastolicValue.tr(context: context),
inputField: _buildTextField(
viewModel.diastolicController,
'',
keyboardType: TextInputType.number,
onChanged: (value) => viewModel.clearDiastolicError(),
),
),
if (viewModel.diastolicError != null)
_buildErrorText(viewModel.diastolicError!),
_buildSettingsRow(
icon: AppAssets.bodyIcon,
iconColor: AppColors.greyTextColor,
label: LocaleKeys.selectArm.tr(context: context),
value: viewModel.selectedMeasuredArmDisplay,
onRowTap: () {
_showMeasuredArmSelectionBottomSheet(context, viewModel);
viewModel.clearMeasuredArmError();
},
),
if (viewModel.measuredArmError != null)
_buildErrorText(viewModel.measuredArmError!),
_buildDateTimeFields(),
],
),
);
},
);
}
/// Weight form fields
Widget _buildWeightForm(HealthTrackersViewModel viewModel) {
return Column(
children: [
_buildSettingsRow(
icon: AppAssets.weightScale,
label: LocaleKeys.enterWeight.tr(context: context),
inputField: _buildTextField(viewModel.weightController, '', keyboardType: TextInputType.number),
unit: viewModel.selectedWeightUnitDisplay,
onUnitTap: () => _showWeightUnitSelectionBottomSheet(context, viewModel),
),
_buildDateTimeFields(),
],
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: [
_buildSettingsRow(
icon: AppAssets.weightScale,
label: LocaleKeys.enterWeight.tr(context: context),
inputField: _buildTextField(
viewModel.weightController,
'',
keyboardType: TextInputType.number,
onChanged: (value) => viewModel.clearWeightError(),
),
unit: viewModel.selectedWeightUnitDisplay,
onUnitTap: () => _showWeightUnitSelectionBottomSheet(context, viewModel),
),
if (viewModel.weightError != null)
_buildErrorText(viewModel.weightError!),
_buildDateTimeFields(),
],
),
);
},
);
}
/// Common date and time fields
Widget _buildDateTimeFields() {
return Column(
children: [
SizedBox(width: 8.w),
TextInputWidget(
controller: dateController,
isReadOnly: true,
isArrowTrailing: true,
labelText: LocaleKeys.date.tr(context: context),
hintText: LocaleKeys.pickADate.tr(context: context),
focusNode: FocusNode(),
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.calendarGrey,
selectionType: SelectionTypeEnum.calendar,
isHideSwitcher: true,
btnTitle: LocaleKeys.add.tr(context: context),
onCalendarTypeChanged: (val) {},
onChange: (val) {
if (val == null) return;
try {
final parsedDate = DateTime.parse(val);
final formattedDate = DateFormat('dd MMM yyyy').format(parsedDate);
dateController.text = formattedDate;
log("date: $formattedDate");
} catch (e) {
dateController.text = val;
log("date: $val");
}
},
),
Divider(height: 1, color: AppColors.dividerColor),
SizedBox(width: 8.w),
TextInputWidget(
controller: timeController,
isReadOnly: true,
isArrowTrailing: true,
labelText: LocaleKeys.time.tr(context: context),
hintText: LocaleKeys.selectMeasureTime.tr(context: context),
focusNode: FocusNode(),
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.calendarGrey,
selectionType: SelectionTypeEnum.time,
isHideSwitcher: true,
onCalendarTypeChanged: (val) {},
onChange: (val) {
if (val == null) return;
timeController.text = val;
log("time: $val");
},
),
],
return Consumer<HealthTrackersViewModel>(
builder: (context, viewModel, child) {
return Column(
children: [
SizedBox(width: 8.w),
TextInputWidget(
controller: dateController,
isReadOnly: true,
isArrowTrailing: true,
labelText: LocaleKeys.date.tr(context: context),
hintText: LocaleKeys.pickADate.tr(context: context),
focusNode: FocusNode(),
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.calendarGrey,
selectionType: SelectionTypeEnum.calendar,
isHideSwitcher: true,
btnTitle: LocaleKeys.add.tr(context: context),
onCalendarTypeChanged: (val) {},
onChange: (val) {
if (val == null) return;
try {
final parsedDate = DateTime.parse(val);
final formattedDate = DateFormat('dd MMM yyyy').format(parsedDate);
dateController.text = formattedDate;
viewModel.clearDateError();
log("date: $formattedDate");
} catch (e) {
dateController.text = val;
viewModel.clearDateError();
log("date: $val");
}
},
),
if (viewModel.dateError != null)
_buildErrorText(viewModel.dateError!),
Divider(height: 1, color: AppColors.dividerColor),
SizedBox(width: 8.w),
TextInputWidget(
controller: timeController,
isReadOnly: true,
isArrowTrailing: true,
labelText: LocaleKeys.time.tr(context: context),
hintText: LocaleKeys.selectMeasureTime.tr(context: context),
focusNode: FocusNode(),
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.calendarGrey,
selectionType: SelectionTypeEnum.time,
isHideSwitcher: true,
onCalendarTypeChanged: (val) {},
onChange: (val) {
if (val == null) return;
timeController.text = val;
viewModel.clearTimeError();
log("time: $val");
},
),
if (viewModel.timeError != null)
_buildErrorText(viewModel.timeError!),
],
);
},
);
}
@ -555,12 +711,7 @@ 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/custom_chip_widget.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/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
@ -390,7 +391,6 @@ class _FamilyCardsState extends State<FamilyCards> {
}
}
double _calculateAspectRatio(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final itemWidth = (screenWidth - 32.w - 10.w) / 2;
@ -424,35 +424,49 @@ class _FamilyCardsState extends State<FamilyCards> {
if (widget.isRequestDesign) {
return Column(
children: [
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,
)
// 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: [
SizedBox(height: 10.h),
manageFamily()
],
),
],
),
SizedBox(height: 24.h),
widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).isEmpty
? Utils.getNoDataWidget(context)
: ListView.builder(
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
? Utils.getNoDataWidget(context)
: ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
@ -477,10 +491,10 @@ class _FamilyCardsState extends State<FamilyCards> {
backgroundColor: profile.status == FamilyFileEnum.pending.toInt
? AppColors.alertLightColor.withValues(alpha: 0.20)
: profile.status == FamilyFileEnum.rejected.toInt
? AppColors.primaryRedColor.withValues(alpha: 0.20)
: profile.status == FamilyFileEnum.active.toInt
? AppColors.lightGreenColor
: AppColors.lightGrayBGColor,
? AppColors.primaryRedColor.withValues(alpha: 0.20)
: profile.status == FamilyFileEnum.active.toInt
? AppColors.lightGreenColor
: AppColors.lightGrayBGColor,
chipText: profile.statusDescription ?? " N/A",
iconAsset: null,
isShowBorder: false,
@ -488,10 +502,10 @@ class _FamilyCardsState extends State<FamilyCards> {
textColor: profile.status == FamilyFileEnum.pending.toInt
? AppColors.alertLightColor
: profile.status == FamilyFileEnum.rejected.toInt
? AppColors.primaryRedColor
: profile.status == FamilyFileEnum.active.toInt
? AppColors.textGreenColor
: AppColors.alertColor),
? AppColors.primaryRedColor
: profile.status == FamilyFileEnum.active.toInt
? AppColors.textGreenColor
: AppColors.alertColor),
SizedBox(height: 8.h),
Wrap(alignment: WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.start, runAlignment: WrapAlignment.start, spacing: 0.h, children: [
(profile.patientName ?? "").toText14(isBold: true, isCenter: false, maxlines: 1),
@ -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),
],
);
@ -525,7 +545,7 @@ class _FamilyCardsState extends State<FamilyCards> {
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(

@ -1,8 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -31,17 +29,34 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
void initState() {
_textFieldFocusNode = FocusNode();
textController = TextEditingController();
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(() {
textController!.text = profileSettingsViewModel!.getPatientInfoForUpdate.emailAddress ?? "";
textController!.text = viewModel.getPatientInfoForUpdate.emailAddress ?? "";
});
});
super.initState();
}
@override
void dispose() {
_textFieldFocusNode.dispose();
// Clear email error when closing
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
profileSettingsViewModel?.clearEmailError();
}
});
super.dispose();
}
@ -58,28 +73,48 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
children: [
LocaleKeys.updateEmailAddressTitle.tr().toText16(textAlign: TextAlign.start, isBold: true),
SizedBox(height: 12.h),
TextInputWidget(
labelText: LocaleKeys.email.tr(),
hintText: "demo@gmail.com",
controller: textController,
focusNode: _textFieldFocusNode,
autoFocus: true,
padding: EdgeInsets.all(8.h),
keyboardType: TextInputType.emailAddress,
isEnable: true,
isReadOnly: false,
prefix: null,
isBorderAllowed: false,
isAllowLeadingIcon: true,
fontSize: 14.f,
isCountryDropDown: false,
leadingIcon: AppAssets.email,
fontFamily: "Poppins",
Consumer<ProfileSettingsViewModel>(
builder: (context, viewModel, child) {
return TextInputWidget(
labelText: LocaleKeys.email.tr(),
hintText: "demo@gmail.com",
controller: textController,
focusNode: _textFieldFocusNode,
autoFocus: true,
padding: EdgeInsets.all(8.h),
keyboardType: TextInputType.emailAddress,
isEnable: true,
isReadOnly: false,
prefix: null,
isBorderAllowed: true,
isAllowLeadingIcon: true,
fontSize: 14.f,
isCountryDropDown: false,
leadingIcon: AppAssets.email,
fontFamily: "Poppins",
hasError: viewModel.emailError != null,
errorMessage: viewModel.emailError,
onChange: (value) {
// Clear error when user starts typing
viewModel.clearEmailError();
},
);
},
),
SizedBox(height: 12.h),
CustomButton(
text: LocaleKeys.submit.tr(context: context),
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));
profileSettingsViewModel!.updatePatientInfo(
patientInfo: {
@ -92,6 +127,7 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
},
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
profileSettingsViewModel!.clearEmailError();
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.success.tr(context: context), child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()),
callBackFunc: () async {
Navigator.of(context).pop();

@ -1,8 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -31,17 +29,34 @@ class _UpdateEmergencyContactDialogState extends State<UpdateEmergencyContactDia
void initState() {
_textFieldFocusNode = FocusNode();
textController = TextEditingController();
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(() {
textController!.text = profileSettingsViewModel!.getPatientInfoForUpdate.emergencyContactNo!;
textController!.text = viewModel.getPatientInfoForUpdate.emergencyContactNo ?? "";
});
});
super.initState();
}
@override
void dispose() {
_textFieldFocusNode.dispose();
// Clear emergency contact error when closing
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
profileSettingsViewModel?.clearEmergencyContactError();
}
});
super.dispose();
}
@ -58,28 +73,48 @@ class _UpdateEmergencyContactDialogState extends State<UpdateEmergencyContactDia
children: [
LocaleKeys.enterNewNewContactNumber.tr().toText16(textAlign: TextAlign.start, isBold: true),
SizedBox(height: 12.h),
TextInputWidget(
labelText: LocaleKeys.emrgNo.tr(),
hintText: "05xxxxxxxx",
controller: textController,
focusNode: _textFieldFocusNode,
autoFocus: true,
padding: EdgeInsets.all(8.h),
keyboardType: TextInputType.number,
isEnable: true,
isReadOnly: false,
prefix: null,
isBorderAllowed: false,
isAllowLeadingIcon: true,
fontSize: 14.f,
isCountryDropDown: false,
leadingIcon: AppAssets.call_fill,
fontFamily: "Poppins",
Consumer<ProfileSettingsViewModel>(
builder: (context, viewModel, child) {
return TextInputWidget(
labelText: LocaleKeys.emrgNo.tr(),
hintText: "05xxxxxxxx",
controller: textController,
focusNode: _textFieldFocusNode,
autoFocus: true,
padding: EdgeInsets.all(8.h),
keyboardType: TextInputType.number,
isEnable: true,
isReadOnly: false,
prefix: null,
isBorderAllowed: true,
isAllowLeadingIcon: true,
fontSize: 14.f,
isCountryDropDown: false,
leadingIcon: AppAssets.call_fill,
fontFamily: "Poppins",
hasError: viewModel.emergencyContactError != null,
errorMessage: viewModel.emergencyContactError,
onChange: (value) {
// Clear error when user starts typing
viewModel.clearEmergencyContactError();
},
);
},
),
SizedBox(height: 12.h),
CustomButton(
text: LocaleKeys.submit.tr(context: context),
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));
profileSettingsViewModel!.updatePatientInfo(
patientInfo: {
@ -92,6 +127,7 @@ class _UpdateEmergencyContactDialogState extends State<UpdateEmergencyContactDia
},
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
profileSettingsViewModel!.clearEmergencyContactError();
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.success.tr(context: context), child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()),
callBackFunc: () async {
Navigator.of(context).pop();

@ -24,6 +24,8 @@ class GenericBottomSheet extends StatefulWidget {
final bool isFromSavedLogin;
final Function(String?)? onChange;
final bool autoFocus;
final String? phoneNumberError;
final String? emailError;
// FocusNode myFocusNode;
@ -39,6 +41,8 @@ class GenericBottomSheet extends StatefulWidget {
this.isFromSavedLogin = false,
this.onChange,
this.autoFocus = false,
this.phoneNumberError,
this.emailError,
// required this.myFocusNode
});
@ -163,6 +167,8 @@ class GenericBottomSheetState extends State<GenericBottomSheet> {
isCountryDropDown: widget.isEnableCountryDropdown,
leadingIcon: widget.isForEmail ? AppAssets.email : AppAssets.smart_phone,
fontFamily: "Poppins",
hasError: widget.isForEmail ? (widget.emailError != null) : (widget.phoneNumberError != null),
errorMessage: widget.isForEmail ? widget.emailError : widget.phoneNumberError,
)
: 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/enums.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/widget_extensions.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/theme/colors.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> {
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
Widget build(BuildContext context) {
AuthenticationViewModel authVm = getIt.get<AuthenticationViewModel>();
// TODO: implement build
// Use the stored reference instead of getting it here
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
@ -38,20 +63,32 @@ class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
children: [
widget.message.toText16(color: AppColors.textColor, isBold: true),
SizedBox(height: 20.h),
Container(
decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24)),
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h),
child: Column(
Selector<AuthenticationViewModel, bool>(
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),
child: Column(
children: [
CustomCountryDropdown(
countryList: CountryEnum.values.where((c) => c != CountryEnum.others).toList(),
onCountryChange: authVm.onCountryChange,
onCountryChange: _authVm.onCountryChange,
).paddingOnly(top: 8.h, bottom: 16.h),
Divider(height: 1.h, color: AppColors.spacerLineColor),
TextInputWidget(
labelText: LocaleKeys.nationalIdNumber.tr(),
hintText: "xxxxxxxxx",
controller: authVm.nationalIdController,
controller: _authVm.nationalIdController,
isEnable: true,
prefix: null,
isAllowRadius: true,
@ -62,6 +99,12 @@ class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
fontFamily: "Poppins",
padding: EdgeInsets.symmetric(vertical: 8.h),
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),
Divider(height: 1.h, color: AppColors.spacerLineColor),
Selector<AuthenticationViewModel, String>(
@ -70,7 +113,7 @@ class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
return TextInputWidget(
labelText: LocaleKeys.phoneNumber.tr(),
hintText: "",
controller: authVm.phoneNumberController,
controller: _authVm.phoneNumberController,
isEnable: true,
prefix: countryCode,
isAllowRadius: true,
@ -81,11 +124,19 @@ class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
fontFamily: "Poppins",
padding: EdgeInsets.symmetric(vertical: 8.h),
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);
},
),
],
),
);
},
),
SizedBox(height: 20.h),
CustomButton(
@ -94,15 +145,9 @@ class _FamilyFileAddWidgetState extends State<FamilyFileAddWidget> {
// Unfocus all text fields and dismiss keyboard
FocusManager.instance.primaryFocus?.unfocus();
if (ValidationUtils.isValidatedIdAndPhoneWithCountryValidation(
nationalId: authVm.nationalIdController.text,
selectedCountry: authVm.selectedCountrySignup,
phoneNumber: authVm.phoneNumberController.text,
onOkPress: () {
Navigator.of(context).pop();
},
)) {
// authVm.addFamilyMember(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true);
// Use ViewModel validation method
if (_authVm.validateIdAndPhone()) {
// _authVm.addFamilyMember(otpTypeEnum: OTPTypeEnum.sms, isExcludedUser: true);
if (widget.onVerificationPress != null) {
widget.onVerificationPress!();
}

@ -129,6 +129,20 @@ class TextInputWidget extends StatelessWidget {
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
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(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@ -140,7 +154,7 @@ class TextInputWidget extends StatelessWidget {
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: isAllowRadius ? (12.r) : null,
side: isBorderAllowed ? BorderSide(color: hasError ? errorColor : const Color(0xffefefef), width: 1) : null,
side: borderSide,
),
child: Row(
textDirection: Directionality.of(context),
@ -190,7 +204,7 @@ class TextInputWidget extends StatelessWidget {
],
),
),
if (hasError && errorMessage != null)
if (errorMessage != null && errorMessage!.isNotEmpty)
Padding(
padding: EdgeInsets.only(top: 4.h, left: 12.h), // Adjust padding as needed
child: Text(
@ -297,10 +311,87 @@ class TextInputWidget extends StatelessWidget {
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) {
double fontS = fontSize ?? 14.f;
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(
builder: (context) {
return Directionality(
@ -362,6 +453,8 @@ class TextInputWidget extends StatelessWidget {
border: InputBorder.none,
focusedBorder: 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