/// Core validation utilities for form fields library validators; /// Validator function type typedef FieldValidator = String? Function(String?)?; /// Compose multiple validators FieldValidator composeValidators(List 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; }; } }