- Replaced the custom delete confirmation dialog with a reusable ConfirmDialog widget for better code organization and reusability. - Improved the add expense bottom sheet by implementing form validation using a GlobalKey and TextFormField. - Enhanced user experience by adding validation for required fields and specific formats (e.g., GST, transaction ID). - Updated the expense list to reflect changes in the confirmation dialog and improved the handling of attachments. - Cleaned up code by removing unnecessary comments and ensuring consistent formatting.
		
			
				
	
	
		
			272 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			272 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| // lib/utils/validators.dart
 | ||
| import 'package:flutter/services.dart';
 | ||
| 
 | ||
| /// Common validators for Indian IDs, payments, and typical form fields.
 | ||
| class Validators {
 | ||
|   // -----------------------------
 | ||
|   // Regexes (compiled once)
 | ||
|   // -----------------------------
 | ||
|   static final RegExp _panRegex = RegExp(r'^[A-Z]{5}[0-9]{4}[A-Z]$');
 | ||
|   // GSTIN: 2-digit/valid state code, PAN, entity code (1-9A-Z), 'Z', checksum (0-9A-Z)
 | ||
|   static final RegExp _gstRegex = RegExp(
 | ||
|     r'^(0[1-9]|1[0-9]|2[0-9]|3[0-7])[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$',
 | ||
|   );
 | ||
|   // Aadhaar digits only
 | ||
|   static final RegExp _aadhaarRegex = RegExp(r'^[2-9]\d{11}$');
 | ||
|   // Name (letters + spaces + dots + hyphen/apostrophe)
 | ||
|   static final RegExp _nameRegex = RegExp(r"^[A-Za-z][A-Za-z .'\-]{1,49}$");
 | ||
|   // Email (generic)
 | ||
|   static final RegExp _emailRegex =
 | ||
|       RegExp(r"^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$");
 | ||
|   // Indian mobile
 | ||
|   static final RegExp _mobileRegex = RegExp(r'^[6-9]\d{9}$');
 | ||
|   // Pincode (India: 6 digits starting 1–9)
 | ||
|   static final RegExp _pincodeRegex = RegExp(r'^[1-9][0-9]{5}$');
 | ||
|   // IFSC (4 letters + 0 + 6 alphanumeric)
 | ||
|   static final RegExp _ifscRegex = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
 | ||
|   // Bank account number (9–18 digits)
 | ||
|   static final RegExp _bankAccountRegex = RegExp(r'^\d{9,18}$');
 | ||
|   // UPI ID (name@bank, simple check)
 | ||
|   static final RegExp _upiRegex =
 | ||
|       RegExp(r'^[\w.\-]{2,}@[\w]{2,}$', caseSensitive: false);
 | ||
|   // Strong password (8+ chars, upper, lower, digit, special)
 | ||
|   static final RegExp _passwordRegex =
 | ||
|       RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$');
 | ||
|   // Date dd/mm/yyyy (basic validation)
 | ||
|   static final RegExp _dateRegex =
 | ||
|       RegExp(r'^([0-2][0-9]|3[0-1])/(0[1-9]|1[0-2])/[0-9]{4}$');
 | ||
|   // URL
 | ||
|   static final RegExp _urlRegex = RegExp(
 | ||
|       r'^(https?:\/\/)?([a-zA-Z0-9.-]+)\.[a-zA-Z]{2,}(:\d+)?(\/\S*)?$');
 | ||
|   // Transaction ID (alphanumeric, dashes/underscores, 8–36 chars)
 | ||
|   static final RegExp _transactionIdRegex =
 | ||
|       RegExp(r'^[A-Za-z0-9\-_]{8,36}$');
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // PAN
 | ||
|   // -----------------------------
 | ||
|   static bool isValidPAN(String? input) {
 | ||
|     if (input == null) return false;
 | ||
|     return _panRegex.hasMatch(input.trim().toUpperCase());
 | ||
|   }
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // GSTIN
 | ||
|   // -----------------------------
 | ||
|   static bool isValidGSTIN(String? input) {
 | ||
|     if (input == null) return false;
 | ||
|     return _gstRegex.hasMatch(_compact(input).toUpperCase());
 | ||
|   }
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // Aadhaar
 | ||
|   // -----------------------------
 | ||
|   static bool isValidAadhaar(String? input, {bool enforceChecksum = true}) {
 | ||
|     if (input == null) return false;
 | ||
|     final a = _digitsOnly(input);
 | ||
|     if (!_aadhaarRegex.hasMatch(a)) return false;
 | ||
|     return enforceChecksum ? _verhoeffValidate(a) : true;
 | ||
|   }
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // Mobile
 | ||
|   // -----------------------------
 | ||
|   static bool isValidIndianMobile(String? input) {
 | ||
|     if (input == null) return false;
 | ||
|     final s = _digitsOnly(input.replaceFirst(RegExp(r'^(?:\+?91|0)'), ''));
 | ||
|     return _mobileRegex.hasMatch(s);
 | ||
|   }
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // Email
 | ||
|   // -----------------------------
 | ||
|   static bool isValidEmail(String? input, {bool gmailOnly = false}) {
 | ||
|     if (input == null) return false;
 | ||
|     final e = input.trim();
 | ||
|     if (!_emailRegex.hasMatch(e)) return false;
 | ||
|     if (!gmailOnly) return true;
 | ||
|     final domain = e.split('@').last.toLowerCase();
 | ||
|     return domain == 'gmail.com' || domain == 'googlemail.com';
 | ||
|   }
 | ||
| 
 | ||
|   static bool isValidGmail(String? input) =>
 | ||
|       isValidEmail(input, gmailOnly: true);
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // Name
 | ||
|   // -----------------------------
 | ||
|   static bool isValidName(String? input, {int minLen = 2, int maxLen = 50}) {
 | ||
|     if (input == null) return false;
 | ||
|     final s = input.trim();
 | ||
|     if (s.length < minLen || s.length > maxLen) return false;
 | ||
|     return _nameRegex.hasMatch(s);
 | ||
|   }
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // Transaction ID
 | ||
|   // -----------------------------
 | ||
|   static bool isValidTransactionId(String? input) {
 | ||
|     if (input == null) return false;
 | ||
|     return _transactionIdRegex.hasMatch(input.trim());
 | ||
|   }
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // Other fields
 | ||
|   // -----------------------------
 | ||
|   static bool isValidPincode(String? input) =>
 | ||
|       input != null && _pincodeRegex.hasMatch(input.trim());
 | ||
| 
 | ||
|   static bool isValidIFSC(String? input) =>
 | ||
|       input != null && _ifscRegex.hasMatch(input.trim().toUpperCase());
 | ||
| 
 | ||
|   static bool isValidBankAccount(String? input) =>
 | ||
|       input != null && _bankAccountRegex.hasMatch(_digitsOnly(input));
 | ||
| 
 | ||
|   static bool isValidUPI(String? input) =>
 | ||
|       input != null && _upiRegex.hasMatch(input.trim());
 | ||
| 
 | ||
|   static bool isValidPassword(String? input) =>
 | ||
|       input != null && _passwordRegex.hasMatch(input.trim());
 | ||
| 
 | ||
|   static bool isValidDate(String? input) =>
 | ||
|       input != null && _dateRegex.hasMatch(input.trim());
 | ||
| 
 | ||
|   static bool isValidURL(String? input) =>
 | ||
|       input != null && _urlRegex.hasMatch(input.trim());
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // Numbers
 | ||
|   // -----------------------------
 | ||
|   static bool isInt(String? input) =>
 | ||
|       input != null && int.tryParse(input.trim()) != null;
 | ||
| 
 | ||
|   static bool isDouble(String? input) =>
 | ||
|       input != null && double.tryParse(input.trim()) != null;
 | ||
| 
 | ||
|   static bool isNumeric(String? input) => isInt(input) || isDouble(input);
 | ||
| 
 | ||
|   static bool isInRange(num? value,
 | ||
|       {num? min, num? max, bool inclusive = true}) {
 | ||
|     if (value == null) return false;
 | ||
|     if (min != null && (inclusive ? value < min : value <= min)) return false;
 | ||
|     if (max != null && (inclusive ? value > max : value >= max)) return false;
 | ||
|     return true;
 | ||
|   }
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // Flutter-friendly validator lambdas (return null when valid)
 | ||
|   // -----------------------------
 | ||
|   static String? requiredField(String? v, {String fieldName = 'This field'}) =>
 | ||
|       (v == null || v.trim().isEmpty) ? '$fieldName is required' : null;
 | ||
| 
 | ||
|   static String? panValidator(String? v) =>
 | ||
|       isValidPAN(v) ? null : 'Enter a valid PAN (e.g., ABCDE1234F)';
 | ||
| 
 | ||
|   static String? gstValidator(String? v, {bool optional = false}) {
 | ||
|     if (optional && (v == null || v.trim().isEmpty)) return null;
 | ||
|     return isValidGSTIN(v) ? null : 'Enter a valid GSTIN';
 | ||
|   }
 | ||
| 
 | ||
|   static String? aadhaarValidator(String? v) =>
 | ||
|       isValidAadhaar(v) ? null : 'Enter a valid Aadhaar (12 digits)';
 | ||
| 
 | ||
|   static String? mobileValidator(String? v) =>
 | ||
|       isValidIndianMobile(v) ? null : 'Enter a valid 10-digit mobile';
 | ||
| 
 | ||
|   static String? emailValidator(String? v, {bool gmailOnly = false}) =>
 | ||
|       isValidEmail(v, gmailOnly: gmailOnly)
 | ||
|           ? null
 | ||
|           : gmailOnly
 | ||
|               ? 'Enter a valid Gmail address'
 | ||
|               : 'Enter a valid email address';
 | ||
| 
 | ||
|   static String? nameValidator(String? v, {int minLen = 2, int maxLen = 50}) =>
 | ||
|       isValidName(v, minLen: minLen, maxLen: maxLen)
 | ||
|           ? null
 | ||
|           : 'Enter a valid name ($minLen–$maxLen chars)';
 | ||
| 
 | ||
|   static String? transactionIdValidator(String? v) =>
 | ||
|       isValidTransactionId(v)
 | ||
|           ? null
 | ||
|           : 'Enter a valid Transaction ID (8–36 chars, letters/numbers)';
 | ||
| 
 | ||
|   static String? pincodeValidator(String? v) =>
 | ||
|       isValidPincode(v) ? null : 'Enter a valid 6-digit pincode';
 | ||
| 
 | ||
|   static String? ifscValidator(String? v) =>
 | ||
|       isValidIFSC(v) ? null : 'Enter a valid IFSC code';
 | ||
| 
 | ||
|   static String? bankAccountValidator(String? v) =>
 | ||
|       isValidBankAccount(v) ? null : 'Enter a valid bank account (9–18 digits)';
 | ||
| 
 | ||
|   static String? upiValidator(String? v) =>
 | ||
|       isValidUPI(v) ? null : 'Enter a valid UPI ID';
 | ||
| 
 | ||
|   static String? passwordValidator(String? v) =>
 | ||
|       isValidPassword(v)
 | ||
|           ? null
 | ||
|           : 'Password must be 8+ chars with upper, lower, digit, special';
 | ||
| 
 | ||
|   static String? dateValidator(String? v) =>
 | ||
|       isValidDate(v) ? null : 'Enter date in dd/mm/yyyy format';
 | ||
| 
 | ||
|   static String? urlValidator(String? v) =>
 | ||
|       isValidURL(v) ? null : 'Enter a valid URL';
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // Helpers
 | ||
|   // -----------------------------
 | ||
|   static String _digitsOnly(String s) => s.replaceAll(RegExp(r'\D'), '');
 | ||
|   static String _compact(String s) => s.replaceAll(RegExp(r'\s'), '');
 | ||
| 
 | ||
|   // -----------------------------
 | ||
|   // Verhoeff checksum (for Aadhaar)
 | ||
|   // -----------------------------
 | ||
|   static const List<List<int>> _verhoeffD = [
 | ||
|     [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
 | ||
|     [1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
 | ||
|     [2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
 | ||
|     [3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
 | ||
|     [4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
 | ||
|     [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
 | ||
|     [6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
 | ||
|     [7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
 | ||
|     [8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
 | ||
|     [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
 | ||
|   ];
 | ||
|   static const List<List<int>> _verhoeffP = [
 | ||
|     [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
 | ||
|     [1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
 | ||
|     [5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
 | ||
|     [8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
 | ||
|     [9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
 | ||
|     [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
 | ||
|     [2, 7, 9, 3, 8, 0, 5, 4, 1, 6],
 | ||
|     [7, 0, 4, 6, 9, 1, 2, 3, 5, 8],
 | ||
|   ];
 | ||
| 
 | ||
|   static bool _verhoeffValidate(String numStr) {
 | ||
|     int c = 0;
 | ||
|     final rev = numStr.split('').reversed.map(int.parse).toList();
 | ||
|     for (int i = 0; i < rev.length; i++) {
 | ||
|       c = _verhoeffD[c][_verhoeffP[(i % 8)][rev[i]]];
 | ||
|     }
 | ||
|     return c == 0;
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /// Common input formatters/masks useful in TextFields.
 | ||
| class InputFormatters {
 | ||
|   static final digitsOnly = FilteringTextInputFormatter.digitsOnly;
 | ||
|   static final upperAlnum =
 | ||
|       FilteringTextInputFormatter.allow(RegExp(r'[A-Z0-9]'));
 | ||
|   static final upperLetters =
 | ||
|       FilteringTextInputFormatter.allow(RegExp(r'[A-Z]'));
 | ||
|   static final name =
 | ||
|       FilteringTextInputFormatter.allow(RegExp(r"[A-Za-z .'\-]"));
 | ||
|   static final alnumWithSpace =
 | ||
|       FilteringTextInputFormatter.allow(RegExp(r"[A-Za-z0-9 ]"));
 | ||
|   static LengthLimitingTextInputFormatter maxLen(int n) =>
 | ||
|       LengthLimitingTextInputFormatter(n);
 | ||
| }
 |