356 lines
9.3 KiB
Dart
356 lines
9.3 KiB
Dart
/// Core validation utilities for form fields
|
|
library validators;
|
|
|
|
/// Validator function type
|
|
typedef FieldValidator = String? Function(String?)?;
|
|
|
|
/// Compose multiple validators
|
|
FieldValidator composeValidators(List<FieldValidator> validators) {
|
|
return (String? value) {
|
|
for (final validator in validators) {
|
|
final result = validator?.call(value);
|
|
if (result != null) {
|
|
return result;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Common validators
|
|
class Validators {
|
|
Validators._(); // Prevent instantiation
|
|
|
|
/// Required field validator
|
|
static FieldValidator required({String? message}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return message ?? 'Ce champ est requis';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Minimum length validator
|
|
static FieldValidator minLength(int length, {String? message}) {
|
|
return (String? value) {
|
|
if (value != null && value.trim().length < length) {
|
|
return message ?? 'Minimum $length caractères requis';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Maximum length validator
|
|
static FieldValidator maxLength(int length, {String? message}) {
|
|
return (String? value) {
|
|
if (value != null && value.length > length) {
|
|
return message ?? 'Maximum $length caractères autorisés';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Email validator
|
|
static FieldValidator email({String? message}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null; // Use required() separately if needed
|
|
}
|
|
|
|
final emailRegex = RegExp(
|
|
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
|
);
|
|
|
|
if (!emailRegex.hasMatch(value.trim())) {
|
|
return message ?? 'Adresse email invalide';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Numeric validator
|
|
static FieldValidator numeric({String? message}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null; // Use required() separately if needed
|
|
}
|
|
|
|
if (double.tryParse(value.trim()) == null) {
|
|
return message ?? 'Veuillez entrer un nombre valide';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Minimum value validator (for numeric fields)
|
|
static FieldValidator minValue(double min, {String? message}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
final numValue = double.tryParse(value.trim());
|
|
if (numValue == null) {
|
|
return 'Nombre invalide';
|
|
}
|
|
|
|
if (numValue < min) {
|
|
return message ?? 'La valeur doit être au moins $min';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Maximum value validator (for numeric fields)
|
|
static FieldValidator maxValue(double max, {String? message}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
final numValue = double.tryParse(value.trim());
|
|
if (numValue == null) {
|
|
return 'Nombre invalide';
|
|
}
|
|
|
|
if (numValue > max) {
|
|
return message ?? 'La valeur doit être au maximum $max';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Range validator (for numeric fields)
|
|
static FieldValidator range(double min, double max, {String? message}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
final numValue = double.tryParse(value.trim());
|
|
if (numValue == null) {
|
|
return 'Nombre invalide';
|
|
}
|
|
|
|
if (numValue < min || numValue > max) {
|
|
return message ?? 'La valeur doit être entre $min et $max';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Phone number validator (simple version)
|
|
static FieldValidator phone({String? message}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
// Allow digits, spaces, +, -, ()
|
|
final phoneRegex = RegExp(r'^[\d\s\+\-\(\)]+$');
|
|
if (!phoneRegex.hasMatch(value.trim())) {
|
|
return message ?? 'Numéro de téléphone invalide';
|
|
}
|
|
|
|
// Check minimum length (at least 8 digits)
|
|
final digitsOnly = value.replaceAll(RegExp(r'[^\d]'), '');
|
|
if (digitsOnly.length < 8) {
|
|
return message ?? 'Numéro de téléphone trop court';
|
|
}
|
|
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// URL validator
|
|
static FieldValidator url({String? message}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
final uri = Uri.parse(value.trim());
|
|
if (!uri.hasScheme || !uri.hasAuthority) {
|
|
return message ?? 'URL invalide';
|
|
}
|
|
} catch (e) {
|
|
return message ?? 'URL invalide';
|
|
}
|
|
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Pattern/Regex validator
|
|
static FieldValidator pattern(RegExp regex, {String? message}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
if (!regex.hasMatch(value.trim())) {
|
|
return message ?? 'Format invalide';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Match validator (confirm password, etc.)
|
|
static FieldValidator match(String otherValue, {String? message}) {
|
|
return (String? value) {
|
|
if (value != otherValue) {
|
|
return message ?? 'Les valeurs ne correspondent pas';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Custom validator
|
|
static FieldValidator custom(bool Function(String?) test, {String? message}) {
|
|
return (String? value) {
|
|
if (!test(value)) {
|
|
return message ?? 'Valeur invalide';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Alphanumeric validator
|
|
static FieldValidator alphanumeric({String? message}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
final alphanumericRegex = RegExp(r'^[a-zA-Z0-9]+$');
|
|
if (!alphanumericRegex.hasMatch(value.trim())) {
|
|
return message ?? 'Seuls les caractères alphanumériques sont autorisés';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// No whitespace validator
|
|
static FieldValidator noWhitespace({String? message}) {
|
|
return (String? value) {
|
|
if (value == null) return null;
|
|
|
|
if (value.contains(' ')) {
|
|
return message ?? 'Les espaces ne sont pas autorisés';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Finance-specific validators
|
|
class FinanceValidators {
|
|
FinanceValidators._();
|
|
|
|
/// Amount validator (positive number with max 2 decimals)
|
|
static FieldValidator amount({
|
|
double? min,
|
|
double? max,
|
|
String? message,
|
|
}) {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
// Check if numeric
|
|
final numValue = double.tryParse(value.trim());
|
|
if (numValue == null) {
|
|
return 'Montant invalide';
|
|
}
|
|
|
|
// Check if positive
|
|
if (numValue <= 0) {
|
|
return 'Le montant doit être positif';
|
|
}
|
|
|
|
// Check min/max
|
|
if (min != null && numValue < min) {
|
|
return message ?? 'Le montant minimum est $min';
|
|
}
|
|
if (max != null && numValue > max) {
|
|
return message ?? 'Le montant maximum est $max';
|
|
}
|
|
|
|
// Check max 2 decimals
|
|
final parts = value.trim().split('.');
|
|
if (parts.length > 1 && parts[1].length > 2) {
|
|
return 'Maximum 2 décimales autorisées';
|
|
}
|
|
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/// Budget line name validator
|
|
static FieldValidator budgetLineName() {
|
|
return composeValidators([
|
|
Validators.required(message: 'Le nom de la ligne budgétaire est requis'),
|
|
Validators.minLength(3, message: 'Minimum 3 caractères'),
|
|
Validators.maxLength(100, message: 'Maximum 100 caractères'),
|
|
]);
|
|
}
|
|
|
|
/// Budget description validator
|
|
static FieldValidator budgetDescription({bool required = false}) {
|
|
return composeValidators([
|
|
if (required)
|
|
Validators.required(message: 'La description est requise'),
|
|
Validators.maxLength(500, message: 'Maximum 500 caractères'),
|
|
]);
|
|
}
|
|
|
|
/// Rejection reason validator
|
|
static FieldValidator rejectionReason() {
|
|
return composeValidators([
|
|
Validators.required(message: 'La raison du rejet est requise'),
|
|
Validators.minLength(10, message: 'Veuillez fournir une raison plus détaillée (min 10 caractères)'),
|
|
Validators.maxLength(500, message: 'Maximum 500 caractères'),
|
|
]);
|
|
}
|
|
|
|
/// Approval comment validator (optional but with constraints if provided)
|
|
static FieldValidator approvalComment() {
|
|
return composeValidators([
|
|
Validators.maxLength(500, message: 'Maximum 500 caractères'),
|
|
]);
|
|
}
|
|
|
|
/// Budget name validator
|
|
static FieldValidator budgetName() {
|
|
return composeValidators([
|
|
Validators.required(message: 'Le nom du budget est requis'),
|
|
Validators.minLength(3, message: 'Minimum 3 caractères'),
|
|
Validators.maxLength(200, message: 'Maximum 200 caractères'),
|
|
]);
|
|
}
|
|
|
|
/// Fiscal year validator
|
|
static FieldValidator fiscalYear() {
|
|
return (String? value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'L\'année est requise';
|
|
}
|
|
|
|
final year = int.tryParse(value.trim());
|
|
if (year == null) {
|
|
return 'Année invalide';
|
|
}
|
|
|
|
final currentYear = DateTime.now().year;
|
|
if (year < currentYear - 5 || year > currentYear + 10) {
|
|
return 'L\'année doit être entre ${currentYear - 5} et ${currentYear + 10}';
|
|
}
|
|
|
|
return null;
|
|
};
|
|
}
|
|
}
|