319 lines
9.7 KiB
Dart
319 lines
9.7 KiB
Dart
import 'package:flutter/services.dart';
|
||
|
||
class Validators {
|
||
Validators._();
|
||
|
||
static final RegExp _gstinPattern =
|
||
RegExp(r'^\d{2}[A-Z]{5}\d{4}[A-Z][A-Z\d]Z[A-Z\d]$');
|
||
static final RegExp _panPattern = RegExp(r'^[A-Z]{5}\d{4}[A-Z]$');
|
||
static final RegExp _emailPattern = RegExp(
|
||
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
||
);
|
||
static final RegExp _ifscPattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
|
||
static final RegExp _pincodePattern = RegExp(r'^[1-9]\d{5}$');
|
||
|
||
static String? required(String? value, {String fieldName = 'This field'}) {
|
||
if (value == null || value.trim().isEmpty) {
|
||
return '$fieldName is required';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static String? email(String? value) {
|
||
if (value == null || value.trim().isEmpty) return 'Email is required';
|
||
return _validateEmail(value.trim());
|
||
}
|
||
|
||
/// Validates email only when a value is entered.
|
||
static String? optionalEmail(String? value) {
|
||
if (value == null || value.trim().isEmpty) return null;
|
||
return _validateEmail(value.trim());
|
||
}
|
||
|
||
static String? _validateEmail(String raw) {
|
||
if (!_emailPattern.hasMatch(raw)) {
|
||
return 'Enter a valid email address';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// Required 9–18 digit bank account number.
|
||
static String? accountNumber(String? value) {
|
||
if (value == null || value.trim().isEmpty) {
|
||
return 'Account number is required';
|
||
}
|
||
return _validateAccountNumber(value.trim());
|
||
}
|
||
|
||
/// Validates account number only when a value is entered.
|
||
static String? optionalAccountNumber(String? value) {
|
||
if (value == null || value.trim().isEmpty) return null;
|
||
return _validateAccountNumber(value.trim());
|
||
}
|
||
|
||
static String? _validateAccountNumber(String digits) {
|
||
if (!RegExp(r'^\d+$').hasMatch(digits)) {
|
||
return 'Account number must contain digits only';
|
||
}
|
||
if (digits.length < 9 || digits.length > 18) {
|
||
return 'Account number must be 9 to 18 digits';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// Required 11-character IFSC code (e.g. SBIN0001234).
|
||
static String? ifsc(String? value) {
|
||
if (value == null || value.trim().isEmpty) {
|
||
return 'IFSC is required';
|
||
}
|
||
return _validateIfsc(value.trim());
|
||
}
|
||
|
||
/// Validates IFSC only when a value is entered.
|
||
static String? optionalIfsc(String? value) {
|
||
if (value == null || value.trim().isEmpty) return null;
|
||
return _validateIfsc(value.trim());
|
||
}
|
||
|
||
static String? _validateIfsc(String raw) {
|
||
final normalized = raw.toUpperCase();
|
||
if (normalized.length != 11) {
|
||
return 'IFSC must be exactly 11 characters';
|
||
}
|
||
if (!_ifscPattern.hasMatch(normalized)) {
|
||
return 'Enter a valid IFSC code (e.g. SBIN0001234)';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// Required 6-digit Indian pincode.
|
||
static String? pincode(String? value) {
|
||
if (value == null || value.trim().isEmpty) {
|
||
return 'Pincode is required';
|
||
}
|
||
return _validatePincode(value.trim());
|
||
}
|
||
|
||
/// Validates pincode only when a value is entered.
|
||
static String? optionalPincode(String? value) {
|
||
if (value == null || value.trim().isEmpty) return null;
|
||
return _validatePincode(value.trim());
|
||
}
|
||
|
||
static String? _validatePincode(String digits) {
|
||
if (!RegExp(r'^\d+$').hasMatch(digits)) {
|
||
return 'Pincode must contain digits only';
|
||
}
|
||
if (!_pincodePattern.hasMatch(digits)) {
|
||
return 'Enter a valid 6-digit pincode';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// Required 10-digit mobile — digits only.
|
||
static String? mobile(String? value) {
|
||
if (value == null || value.trim().isEmpty) {
|
||
return 'Mobile number is required';
|
||
}
|
||
return _validateMobileDigits(value.trim());
|
||
}
|
||
|
||
/// Alias for [mobile].
|
||
static String? phone(String? value) => mobile(value);
|
||
|
||
/// Validates mobile only when a value is entered.
|
||
static String? optionalMobile(String? value) {
|
||
if (value == null || value.trim().isEmpty) return null;
|
||
return _validateMobileDigits(value.trim());
|
||
}
|
||
|
||
static String? _validateMobileDigits(String digits) {
|
||
if (!RegExp(r'^\d+$').hasMatch(digits)) {
|
||
return 'Mobile number must contain digits only';
|
||
}
|
||
if (digits.length != 10) {
|
||
return 'Mobile number must be exactly 10 digits';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static String? password(String? value) {
|
||
if (value == null || value.isEmpty) return 'Password is required';
|
||
if (value.length < 8) return 'Password must be at least 8 characters';
|
||
if (!RegExp(r'[A-Z]').hasMatch(value)) return 'Must contain an uppercase letter';
|
||
if (!RegExp(r'[a-z]').hasMatch(value)) return 'Must contain a lowercase letter';
|
||
if (!RegExp(r'[0-9]').hasMatch(value)) return 'Must contain a number';
|
||
return null;
|
||
}
|
||
|
||
/// Required 15-character GSTIN pattern.
|
||
static String? gstin(String? value) {
|
||
if (value == null || value.trim().isEmpty) {
|
||
return 'GSTIN is required';
|
||
}
|
||
return _validateGstin(value.trim());
|
||
}
|
||
|
||
/// Validates GSTIN only when a value is entered.
|
||
static String? optionalGstin(String? value) {
|
||
if (value == null || value.trim().isEmpty) return null;
|
||
return _validateGstin(value.trim());
|
||
}
|
||
|
||
/// Alias for [optionalGstin] (legacy company forms).
|
||
static String? gstNumber(String? value) => optionalGstin(value);
|
||
|
||
static String? _validateGstin(String raw) {
|
||
final normalized = raw.toUpperCase();
|
||
if (normalized.length != 15) {
|
||
return 'GSTIN must be exactly 15 characters';
|
||
}
|
||
if (!_gstinPattern.hasMatch(normalized)) {
|
||
return 'Enter a valid GSTIN (e.g. 27ABCDE1234F1Z5)';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// Required 10-character PAN pattern (e.g. ABCDE1234F).
|
||
static String? pan(String? value) {
|
||
if (value == null || value.trim().isEmpty) {
|
||
return 'PAN is required';
|
||
}
|
||
return _validatePan(value.trim());
|
||
}
|
||
|
||
/// Validates PAN only when a value is entered.
|
||
static String? optionalPan(String? value) {
|
||
if (value == null || value.trim().isEmpty) return null;
|
||
return _validatePan(value.trim());
|
||
}
|
||
|
||
static String? _validatePan(String raw) {
|
||
final normalized = raw.toUpperCase();
|
||
if (normalized.length != 10) {
|
||
return 'PAN must be exactly 10 characters';
|
||
}
|
||
if (!_panPattern.hasMatch(normalized)) {
|
||
return 'Enter a valid PAN (e.g. ABCDE1234F)';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static String? minLength(String? value, int min, {String fieldName = 'Field'}) {
|
||
if (value == null || value.length < min) {
|
||
return '$fieldName must be at least $min characters';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// Resolves validators for master-data and dynamic form fields by key.
|
||
static String? forFieldKey(
|
||
String key,
|
||
String? value, {
|
||
required bool required,
|
||
String fieldName = 'Field',
|
||
}) {
|
||
final normalizedKey = key.trim().toLowerCase();
|
||
|
||
if (normalizedKey == 'phone' || normalizedKey == 'mobile') {
|
||
return required ? mobile(value) : optionalMobile(value);
|
||
}
|
||
if (normalizedKey == 'email') {
|
||
return required ? email(value) : optionalEmail(value);
|
||
}
|
||
if (normalizedKey == 'gstin' ||
|
||
normalizedKey == 'gst_number' ||
|
||
normalizedKey == 'gst') {
|
||
return required ? gstin(value) : optionalGstin(value);
|
||
}
|
||
if (normalizedKey == 'pan') {
|
||
return required ? pan(value) : optionalPan(value);
|
||
}
|
||
if (normalizedKey == 'pincode' ||
|
||
normalizedKey == 'postal_code' ||
|
||
normalizedKey == 'zip') {
|
||
return required ? pincode(value) : optionalPincode(value);
|
||
}
|
||
if (normalizedKey == 'ifsc') {
|
||
return required ? ifsc(value) : optionalIfsc(value);
|
||
}
|
||
if (normalizedKey == 'account_number' || normalizedKey == 'account_no') {
|
||
return required ? accountNumber(value) : optionalAccountNumber(value);
|
||
}
|
||
|
||
if (required) {
|
||
return Validators.required(value, fieldName: fieldName);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static List<TextInputFormatter> inputFormattersForFieldKey(String key) {
|
||
final normalizedKey = key.trim().toLowerCase();
|
||
|
||
if (normalizedKey == 'phone' || normalizedKey == 'mobile') {
|
||
return mobileInput;
|
||
}
|
||
if (normalizedKey == 'pincode' ||
|
||
normalizedKey == 'postal_code' ||
|
||
normalizedKey == 'zip') {
|
||
return pincodeInput;
|
||
}
|
||
if (normalizedKey == 'ifsc') {
|
||
return ifscInput;
|
||
}
|
||
if (normalizedKey == 'account_number' || normalizedKey == 'account_no') {
|
||
return accountNumberInput;
|
||
}
|
||
if (normalizedKey == 'gstin' ||
|
||
normalizedKey == 'gst_number' ||
|
||
normalizedKey == 'gst') {
|
||
return gstinInput;
|
||
}
|
||
if (normalizedKey == 'pan') {
|
||
return panInput;
|
||
}
|
||
return const [];
|
||
}
|
||
|
||
static List<TextInputFormatter> get mobileInput => [
|
||
FilteringTextInputFormatter.digitsOnly,
|
||
LengthLimitingTextInputFormatter(10),
|
||
];
|
||
|
||
static List<TextInputFormatter> get pincodeInput => [
|
||
FilteringTextInputFormatter.digitsOnly,
|
||
LengthLimitingTextInputFormatter(6),
|
||
];
|
||
|
||
static List<TextInputFormatter> get accountNumberInput => [
|
||
FilteringTextInputFormatter.digitsOnly,
|
||
LengthLimitingTextInputFormatter(18),
|
||
];
|
||
|
||
static List<TextInputFormatter> get ifscInput => [
|
||
FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')),
|
||
LengthLimitingTextInputFormatter(11),
|
||
_upperCaseFormatter,
|
||
];
|
||
|
||
static List<TextInputFormatter> get gstinInput => [
|
||
FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')),
|
||
LengthLimitingTextInputFormatter(15),
|
||
_upperCaseFormatter,
|
||
];
|
||
|
||
static List<TextInputFormatter> get panInput => [
|
||
FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')),
|
||
LengthLimitingTextInputFormatter(10),
|
||
_upperCaseFormatter,
|
||
];
|
||
|
||
static final TextInputFormatter _upperCaseFormatter =
|
||
TextInputFormatter.withFunction(
|
||
(oldValue, newValue) => TextEditingValue(
|
||
text: newValue.text.toUpperCase(),
|
||
selection: newValue.selection,
|
||
),
|
||
);
|
||
}
|