531 lines
15 KiB
Dart
531 lines
15 KiB
Dart
/// Form Validators for Vietnamese Locale
|
|
///
|
|
/// Provides validation utilities for forms with Vietnamese-specific
|
|
/// validations for phone numbers, email, passwords, etc.
|
|
library;
|
|
|
|
/// Form field validators
|
|
class Validators {
|
|
Validators._();
|
|
|
|
// ========================================================================
|
|
// Required Field Validators
|
|
// ========================================================================
|
|
|
|
/// Validate required field
|
|
static String? required(String? value, {String? fieldName}) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return fieldName != null
|
|
? '$fieldName là bắt buộc'
|
|
: 'Trường này là bắt buộc';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Phone Number Validators
|
|
// ========================================================================
|
|
|
|
/// Validate Vietnamese phone number
|
|
///
|
|
/// Accepts formats:
|
|
/// - 0xxx xxx xxx (10 digits starting with 0)
|
|
/// - +84xxx xxx xxx (starts with +84)
|
|
/// - 84xxx xxx xxx (starts with 84)
|
|
static String? phone(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng nhập số điện thoại';
|
|
}
|
|
|
|
final cleaned = value.replaceAll(RegExp(r'\D'), '');
|
|
|
|
// Check if starts with valid Vietnamese mobile prefix
|
|
final vietnamesePattern = RegExp(r'^(0|\+?84)(3|5|7|8|9)[0-9]{8}$');
|
|
|
|
if (!vietnamesePattern.hasMatch(value.replaceAll(RegExp(r'[^\d+]'), ''))) {
|
|
return 'Số điện thoại không hợp lệ';
|
|
}
|
|
|
|
if (cleaned.length < 10 || cleaned.length > 11) {
|
|
return 'Số điện thoại phải có 10 chữ số';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Validate phone number (optional)
|
|
static String? phoneOptional(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null; // Optional, so null is valid
|
|
}
|
|
return phone(value);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Email Validators
|
|
// ========================================================================
|
|
|
|
/// Validate email address
|
|
static String? email(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng nhập email';
|
|
}
|
|
|
|
final emailRegex = RegExp(
|
|
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
|
);
|
|
|
|
if (!emailRegex.hasMatch(value)) {
|
|
return 'Email không hợp lệ';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Validate email (optional)
|
|
static String? emailOptional(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
return email(value);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Password Validators
|
|
// ========================================================================
|
|
|
|
/// Validate password strength
|
|
///
|
|
/// Requirements:
|
|
/// - At least 8 characters
|
|
/// - At least 1 uppercase letter
|
|
/// - At least 1 lowercase letter
|
|
/// - At least 1 number
|
|
/// - At least 1 special character
|
|
static String? password(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng nhập mật khẩu';
|
|
}
|
|
|
|
if (value.length < 8) {
|
|
return 'Mật khẩu phải có ít nhất 8 ký tự';
|
|
}
|
|
|
|
if (!RegExp(r'[A-Z]').hasMatch(value)) {
|
|
return 'Mật khẩu phải có ít nhất 1 chữ hoa';
|
|
}
|
|
|
|
if (!RegExp(r'[a-z]').hasMatch(value)) {
|
|
return 'Mật khẩu phải có ít nhất 1 chữ thường';
|
|
}
|
|
|
|
if (!RegExp(r'[0-9]').hasMatch(value)) {
|
|
return 'Mật khẩu phải có ít nhất 1 số';
|
|
}
|
|
|
|
if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(value)) {
|
|
return 'Mật khẩu phải có ít nhất 1 ký tự đặc biệt';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Validate password confirmation
|
|
static String? confirmPassword(String? value, String? password) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng xác nhận mật khẩu';
|
|
}
|
|
|
|
if (value != password) {
|
|
return 'Mật khẩu không khớp';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Simple password validator (minimum length only)
|
|
static String? passwordSimple(String? value, {int minLength = 6}) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng nhập mật khẩu';
|
|
}
|
|
|
|
if (value.length < minLength) {
|
|
return 'Mật khẩu phải có ít nhất $minLength ký tự';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ========================================================================
|
|
// OTP Validators
|
|
// ========================================================================
|
|
|
|
/// Validate OTP code
|
|
static String? otp(String? value, {int length = 6}) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng nhập mã OTP';
|
|
}
|
|
|
|
if (value.length != length) {
|
|
return 'Mã OTP phải có $length chữ số';
|
|
}
|
|
|
|
if (!RegExp(r'^[0-9]+$').hasMatch(value)) {
|
|
return 'Mã OTP chỉ được chứa số';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Text Length Validators
|
|
// ========================================================================
|
|
|
|
/// Validate minimum length
|
|
static String? minLength(String? value, int min, {String? fieldName}) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return fieldName != null
|
|
? '$fieldName là bắt buộc'
|
|
: 'Trường này là bắt buộc';
|
|
}
|
|
|
|
if (value.length < min) {
|
|
return fieldName != null
|
|
? '$fieldName phải có ít nhất $min ký tự'
|
|
: 'Phải có ít nhất $min ký tự';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Validate maximum length
|
|
static String? maxLength(String? value, int max, {String? fieldName}) {
|
|
if (value != null && value.length > max) {
|
|
return fieldName != null
|
|
? '$fieldName không được vượt quá $max ký tự'
|
|
: 'Không được vượt quá $max ký tự';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Validate length range
|
|
static String? lengthRange(
|
|
String? value,
|
|
int min,
|
|
int max, {
|
|
String? fieldName,
|
|
}) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return fieldName != null
|
|
? '$fieldName là bắt buộc'
|
|
: 'Trường này là bắt buộc';
|
|
}
|
|
|
|
if (value.length < min || value.length > max) {
|
|
return fieldName != null
|
|
? '$fieldName phải có từ $min đến $max ký tự'
|
|
: 'Phải có từ $min đến $max ký tự';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Number Validators
|
|
// ========================================================================
|
|
|
|
/// Validate number
|
|
static String? number(String? value, {String? fieldName}) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return fieldName != null
|
|
? '$fieldName là bắt buộc'
|
|
: 'Trường này là bắt buộc';
|
|
}
|
|
|
|
if (double.tryParse(value) == null) {
|
|
return fieldName != null ? '$fieldName phải là số' : 'Giá trị phải là số';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Validate integer
|
|
static String? integer(String? value, {String? fieldName}) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return fieldName != null
|
|
? '$fieldName là bắt buộc'
|
|
: 'Trường này là bắt buộc';
|
|
}
|
|
|
|
if (int.tryParse(value) == null) {
|
|
return fieldName != null
|
|
? '$fieldName phải là số nguyên'
|
|
: 'Giá trị phải là số nguyên';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Validate positive number
|
|
static String? positiveNumber(String? value, {String? fieldName}) {
|
|
final numberError = number(value, fieldName: fieldName);
|
|
if (numberError != null) return numberError;
|
|
|
|
final num = double.parse(value!);
|
|
if (num <= 0) {
|
|
return fieldName != null
|
|
? '$fieldName phải lớn hơn 0'
|
|
: 'Giá trị phải lớn hơn 0';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Validate number range
|
|
static String? numberRange(
|
|
String? value,
|
|
double min,
|
|
double max, {
|
|
String? fieldName,
|
|
}) {
|
|
final numberError = number(value, fieldName: fieldName);
|
|
if (numberError != null) return numberError;
|
|
|
|
final num = double.parse(value!);
|
|
if (num < min || num > max) {
|
|
return fieldName != null
|
|
? '$fieldName phải từ $min đến $max'
|
|
: 'Giá trị phải từ $min đến $max';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Date Validators
|
|
// ========================================================================
|
|
|
|
/// Validate date format (dd/MM/yyyy)
|
|
static String? date(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng nhập ngày';
|
|
}
|
|
|
|
final dateRegex = RegExp(r'^\d{2}/\d{2}/\d{4}$');
|
|
if (!dateRegex.hasMatch(value)) {
|
|
return 'Định dạng ngày không hợp lệ (dd/MM/yyyy)';
|
|
}
|
|
|
|
try {
|
|
final parts = value.split('/');
|
|
final day = int.parse(parts[0]);
|
|
final month = int.parse(parts[1]);
|
|
final year = int.parse(parts[2]);
|
|
|
|
final date = DateTime(year, month, day);
|
|
|
|
if (date.day != day || date.month != month || date.year != year) {
|
|
return 'Ngày không hợp lệ';
|
|
}
|
|
} catch (e) {
|
|
return 'Ngày không hợp lệ';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Validate age (must be at least 18 years old)
|
|
static String? age(String? value, {int minAge = 18}) {
|
|
final dateError = date(value);
|
|
if (dateError != null) return dateError;
|
|
|
|
try {
|
|
final parts = value!.split('/');
|
|
final birthDate = DateTime(
|
|
int.parse(parts[2]),
|
|
int.parse(parts[1]),
|
|
int.parse(parts[0]),
|
|
);
|
|
|
|
final today = DateTime.now();
|
|
final age =
|
|
today.year -
|
|
birthDate.year -
|
|
(today.month > birthDate.month ||
|
|
(today.month == birthDate.month && today.day >= birthDate.day)
|
|
? 0
|
|
: 1);
|
|
|
|
if (age < minAge) {
|
|
return 'Bạn phải từ $minAge tuổi trở lên';
|
|
}
|
|
|
|
return null;
|
|
} catch (e) {
|
|
return 'Ngày sinh không hợp lệ';
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// Address Validators
|
|
// ========================================================================
|
|
|
|
/// Validate Vietnamese address
|
|
static String? address(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng nhập địa chỉ';
|
|
}
|
|
|
|
if (value.length < 10) {
|
|
return 'Địa chỉ quá ngắn';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Tax ID Validators
|
|
// ========================================================================
|
|
|
|
/// Validate Vietnamese Tax ID (Mã số thuế)
|
|
/// Format: 10 or 13 digits
|
|
static String? taxId(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng nhập mã số thuế';
|
|
}
|
|
|
|
final cleaned = value.replaceAll(RegExp(r'\D'), '');
|
|
|
|
if (cleaned.length != 10 && cleaned.length != 13) {
|
|
return 'Mã số thuế phải có 10 hoặc 13 chữ số';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Validate tax ID (optional)
|
|
static String? taxIdOptional(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
return taxId(value);
|
|
}
|
|
|
|
// ========================================================================
|
|
// URL Validators
|
|
// ========================================================================
|
|
|
|
/// Validate URL
|
|
static String? url(String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng nhập URL';
|
|
}
|
|
|
|
final urlRegex = RegExp(
|
|
r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$',
|
|
);
|
|
|
|
if (!urlRegex.hasMatch(value)) {
|
|
return 'URL không hợp lệ';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Combination Validators
|
|
// ========================================================================
|
|
|
|
/// Combine multiple validators
|
|
static String? Function(String?) combine(
|
|
List<String? Function(String?)> validators,
|
|
) {
|
|
return (String? value) {
|
|
for (final validator in validators) {
|
|
final error = validator(value);
|
|
if (error != null) return error;
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
// ========================================================================
|
|
// Custom Pattern Validators
|
|
// ========================================================================
|
|
|
|
/// Validate against custom regex pattern
|
|
static String? pattern(String? value, RegExp pattern, String errorMessage) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Trường này là bắt buộc';
|
|
}
|
|
|
|
if (!pattern.hasMatch(value)) {
|
|
return errorMessage;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Match Validators
|
|
// ========================================================================
|
|
|
|
/// Validate that value matches another value
|
|
static String? match(String? value, String? matchValue, String fieldName) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Vui lòng nhập $fieldName';
|
|
}
|
|
|
|
if (value != matchValue) {
|
|
return '$fieldName không khớp';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Password strength enum
|
|
enum PasswordStrength { weak, medium, strong, veryStrong }
|
|
|
|
/// Password strength calculator
|
|
class PasswordStrengthCalculator {
|
|
/// Calculate password strength
|
|
static PasswordStrength calculate(String password) {
|
|
if (password.isEmpty) return PasswordStrength.weak;
|
|
|
|
var score = 0;
|
|
|
|
// Length check
|
|
if (password.length >= 8) score++;
|
|
if (password.length >= 12) score++;
|
|
if (password.length >= 16) score++;
|
|
|
|
// Character variety check
|
|
if (RegExp(r'[a-z]').hasMatch(password)) score++;
|
|
if (RegExp(r'[A-Z]').hasMatch(password)) score++;
|
|
if (RegExp(r'[0-9]').hasMatch(password)) score++;
|
|
if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) score++;
|
|
|
|
// Return strength based on score
|
|
if (score <= 2) return PasswordStrength.weak;
|
|
if (score <= 4) return PasswordStrength.medium;
|
|
if (score <= 6) return PasswordStrength.strong;
|
|
return PasswordStrength.veryStrong;
|
|
}
|
|
|
|
/// Get strength label in Vietnamese
|
|
static String getLabel(PasswordStrength strength) {
|
|
switch (strength) {
|
|
case PasswordStrength.weak:
|
|
return 'Yếu';
|
|
case PasswordStrength.medium:
|
|
return 'Trung bình';
|
|
case PasswordStrength.strong:
|
|
return 'Mạnh';
|
|
case PasswordStrength.veryStrong:
|
|
return 'Rất mạnh';
|
|
}
|
|
}
|
|
}
|