bharat_erp/lib/core/utils/validators.dart
2026-07-14 16:02:04 +05:30

563 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 918 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;
}
/// Validates a positive decimal when a value is entered.
static String? optionalPositiveDouble(
String? value, {
String fieldName = 'Value',
}) {
if (value == null || value.trim().isEmpty) return null;
final parsed = double.tryParse(value.trim());
if (parsed == null) return 'Enter a valid number';
if (parsed <= 0) return '$fieldName must be greater than 0';
return null;
}
/// Validates a non-negative decimal when a value is entered.
static String? optionalNonNegativeDouble(
String? value, {
String fieldName = 'Value',
}) {
if (value == null || value.trim().isEmpty) return null;
final parsed = double.tryParse(value.trim());
if (parsed == null) return 'Enter a valid number';
if (parsed < 0) return '$fieldName cannot be negative';
return null;
}
/// Validates a positive whole number when a value is entered.
static String? optionalPositiveInt(
String? value, {
String fieldName = 'Value',
}) {
if (value == null || value.trim().isEmpty) return null;
final parsed = int.tryParse(value.trim());
if (parsed == null) return 'Enter a valid whole number';
if (parsed <= 0) return '$fieldName must be greater than 0';
return null;
}
/// Validates a percentage between 0 and 100 when a value is entered.
static String? optionalPercentage(
String? value, {
String fieldName = 'Percentage',
}) {
if (value == null || value.trim().isEmpty) return null;
final parsed = double.tryParse(value.trim());
if (parsed == null) return 'Enter a valid percentage';
if (parsed < 0 || parsed > 100) {
return '$fieldName must be between 0 and 100';
}
return null;
}
static final RegExp _roleNamePattern = RegExp(r'^[a-zA-Z0-9 ]+$');
static final RegExp _masterNamePattern =
RegExp(r'^[A-Za-z0-9 _/&.()\-]+$');
static final RegExp _masterNameCharPattern =
RegExp(r'[A-Za-z0-9 _/&.()\-]');
static bool isMasterNameFieldKey(String key) {
final normalizedKey = key.trim().toLowerCase();
return normalizedKey == 'name' || normalizedKey == 'item_name';
}
/// Required master name — allowed: A-Z a-z 0-9 space - _ / & . ( )
static String? masterName(String? value, {String fieldName = 'Name'}) {
final requiredError = required(value, fieldName: fieldName);
if (requiredError != null) return requiredError;
return _validateMasterNameChars(value!.trim(), fieldName: fieldName);
}
static String? _validateMasterNameChars(
String raw, {
String fieldName = 'Name',
}) {
if (!_masterNamePattern.hasMatch(raw)) {
return '$fieldName can only contain letters, numbers, spaces, and - _ / & . ( )';
}
return null;
}
/// Validates master name format and uniqueness against existing records.
static String? uniqueMasterName(
String? value, {
required String nameKey,
required Iterable<Map<String, dynamic>> existingRecords,
String? currentRecordId,
String fieldName = 'Name',
}) {
final formatError = masterName(value, fieldName: fieldName);
if (formatError != null) return formatError;
final normalized = value!.trim().toLowerCase();
for (final record in existingRecords) {
final recordId = record['id']?.toString();
if (currentRecordId != null && recordId == currentRecordId) continue;
final existingName = record[nameKey]?.toString().trim().toLowerCase();
if (existingName != null && existingName.isNotEmpty && existingName == normalized) {
return '$fieldName must be unique';
}
}
return null;
}
static List<TextInputFormatter> get masterNameInput => [
FilteringTextInputFormatter.allow(_masterNameCharPattern),
];
static final RegExp _masterCodePattern = RegExp(r'^[A-Za-z0-9\-_/]+$');
static final RegExp _masterCodeCharPattern = RegExp(r'[A-Za-z0-9\-_/]');
static bool isMasterCodeFieldKey(String key) {
final normalizedKey = key.trim().toLowerCase();
return normalizedKey == 'code' || normalizedKey == 'item_code';
}
/// HSN/SAC codes are 48 digits.
static final RegExp _hsnCodePattern = RegExp(r'^\d{4,8}$');
static String? hsnCode(String? value, {String fieldName = 'HSN/SAC Code'}) {
final requiredError = required(value, fieldName: fieldName);
if (requiredError != null) return requiredError;
final trimmed = value!.trim();
if (!_hsnCodePattern.hasMatch(trimmed)) {
return '$fieldName must be 48 digits';
}
return null;
}
static String? uniqueHsnCode(
String? value, {
required Iterable<Map<String, dynamic>> existingRecords,
String? currentRecordId,
String fieldName = 'HSN/SAC Code',
}) {
final formatError = hsnCode(value, fieldName: fieldName);
if (formatError != null) return formatError;
final normalized = value!.trim();
for (final record in existingRecords) {
final recordId = record['id']?.toString();
if (currentRecordId != null && recordId == currentRecordId) continue;
final existingCode = record['code']?.toString().trim();
if (existingCode != null &&
existingCode.isNotEmpty &&
existingCode == normalized) {
return '$fieldName must be unique';
}
}
return null;
}
static List<TextInputFormatter> get hsnCodeInput => [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(8),
];
/// Required master code — allowed: A-Z a-z 0-9 - _ /
static String? masterCode(String? value, {String fieldName = 'Code'}) {
final requiredError = required(value, fieldName: fieldName);
if (requiredError != null) return requiredError;
return _validateMasterCodeChars(value!.trim(), fieldName: fieldName);
}
static String? _validateMasterCodeChars(
String raw, {
String fieldName = 'Code',
}) {
if (!_masterCodePattern.hasMatch(raw)) {
return '$fieldName can only contain letters, numbers, and - _ /';
}
return null;
}
/// Validates master code format and uniqueness against existing records.
static String? uniqueMasterCode(
String? value, {
required String codeKey,
required Iterable<Map<String, dynamic>> existingRecords,
String? currentRecordId,
String fieldName = 'Code',
}) {
final formatError = masterCode(value, fieldName: fieldName);
if (formatError != null) return formatError;
final normalized = value!.trim().toLowerCase();
for (final record in existingRecords) {
final recordId = record['id']?.toString();
if (currentRecordId != null && recordId == currentRecordId) continue;
final existingCode = record[codeKey]?.toString().trim().toLowerCase();
if (existingCode != null && existingCode.isNotEmpty && existingCode == normalized) {
return '$fieldName must be unique';
}
}
return null;
}
static List<TextInputFormatter> get masterCodeInput => [
FilteringTextInputFormatter.allow(_masterCodeCharPattern),
];
/// Required role name — letters, numbers, and spaces only.
static String? roleName(String? value) {
final requiredError = required(value, fieldName: 'Role Name');
if (requiredError != null) return requiredError;
final trimmed = value!.trim();
if (!_roleNamePattern.hasMatch(trimmed)) {
return 'Role name must not contain special characters';
}
return null;
}
static List<TextInputFormatter> get roleNameInput => [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 ]')),
];
/// 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 (isMasterNameFieldKey(normalizedKey)) {
if (required) {
return masterName(value, fieldName: fieldName);
}
if (value != null && value.trim().isNotEmpty) {
return _validateMasterNameChars(value.trim(), fieldName: fieldName);
}
return null;
}
if (isMasterCodeFieldKey(normalizedKey)) {
if (required) {
return masterCode(value, fieldName: fieldName);
}
if (value != null && value.trim().isNotEmpty) {
return _validateMasterCodeChars(value.trim(), fieldName: fieldName);
}
return null;
}
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;
}
if (isMasterNameFieldKey(normalizedKey)) {
return masterNameInput;
}
if (isMasterCodeFieldKey(normalizedKey)) {
return masterCodeInput;
}
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,
),
);
}