1086 lines
35 KiB
Dart
1086 lines
35 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:taxglide/consts/app_colors.dart';
|
|
import 'package:taxglide/consts/comman_dropdown.dart';
|
|
import 'package:taxglide/consts/image_permission.dart';
|
|
import 'package:taxglide/consts/validation_popup.dart';
|
|
import 'package:taxglide/controller/api_contoller.dart';
|
|
import 'package:taxglide/controller/api_repository.dart';
|
|
import 'package:taxglide/model/city_model.dart' as country;
|
|
import 'package:taxglide/model/country_model.dart' as country;
|
|
|
|
class KycDetailScreen extends ConsumerStatefulWidget {
|
|
const KycDetailScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<KycDetailScreen> createState() => _KycDetailScreenState();
|
|
}
|
|
|
|
class _KycDetailScreenState extends ConsumerState<KycDetailScreen> {
|
|
final ScrollController _scrollController = ScrollController();
|
|
|
|
// Controllers
|
|
late final TextEditingController companyCtrl;
|
|
late final TextEditingController panCtrl;
|
|
late final TextEditingController gstCtrl;
|
|
late final TextEditingController tanCtrl;
|
|
late final TextEditingController cinCtrl;
|
|
late final TextEditingController yearCtrl;
|
|
late final TextEditingController emailCtrl;
|
|
late final TextEditingController phoneCtrl;
|
|
late final TextEditingController addressCtrl;
|
|
late final TextEditingController pincodeCtrl;
|
|
|
|
// Focus Nodes
|
|
late final FocusNode companyNode;
|
|
late final FocusNode panNode;
|
|
late final FocusNode gstNode;
|
|
late final FocusNode tanNode;
|
|
late final FocusNode cinNode;
|
|
late final FocusNode yearNode;
|
|
late final FocusNode emailNode;
|
|
late final FocusNode phoneNode;
|
|
late final FocusNode addressNode;
|
|
late final FocusNode pincodeNode;
|
|
|
|
// Keys
|
|
final companyKey = GlobalKey();
|
|
final panKey = GlobalKey();
|
|
final gstKey = GlobalKey();
|
|
final tanKey = GlobalKey();
|
|
final cinKey = GlobalKey();
|
|
final yearKey = GlobalKey();
|
|
final emailKey = GlobalKey();
|
|
final phoneKey = GlobalKey();
|
|
final addressKey = GlobalKey();
|
|
final pincodeKey = GlobalKey();
|
|
final countryKey = GlobalKey();
|
|
final stateKey = GlobalKey();
|
|
final cityKey = GlobalKey();
|
|
final panImageKey = GlobalKey();
|
|
final gstImageKey = GlobalKey();
|
|
final incImageKey = GlobalKey();
|
|
final logoImageKey = GlobalKey();
|
|
|
|
// Selection state
|
|
String? selectedCountryName;
|
|
int? selectedCountryId;
|
|
String? selectedStateName;
|
|
int? selectedStateId;
|
|
String? selectedCityName;
|
|
int? selectedCityId;
|
|
|
|
String? countryError;
|
|
String? stateError;
|
|
String? cityError;
|
|
|
|
// File state (now supports both image and PDF)
|
|
File? panImageFile;
|
|
String? panImageUrl;
|
|
File? gstImageFile;
|
|
String? gstImageUrl;
|
|
File? incorporationImageFile;
|
|
String? incorporationImageUrl;
|
|
File? logoImageFile;
|
|
String? logoImageUrl;
|
|
|
|
String? panImageError;
|
|
String? gstImageError;
|
|
String? incImageError;
|
|
String? logoImageError;
|
|
|
|
bool isChecked = false;
|
|
Map<String, String?> errors = {};
|
|
bool _isInitialized = false;
|
|
bool _isSubmitting = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
companyCtrl = TextEditingController();
|
|
panCtrl = TextEditingController();
|
|
gstCtrl = TextEditingController();
|
|
tanCtrl = TextEditingController();
|
|
cinCtrl = TextEditingController();
|
|
yearCtrl = TextEditingController();
|
|
emailCtrl = TextEditingController();
|
|
phoneCtrl = TextEditingController();
|
|
addressCtrl = TextEditingController();
|
|
pincodeCtrl = TextEditingController();
|
|
|
|
companyNode = FocusNode();
|
|
panNode = FocusNode();
|
|
gstNode = FocusNode();
|
|
tanNode = FocusNode();
|
|
cinNode = FocusNode();
|
|
yearNode = FocusNode();
|
|
emailNode = FocusNode();
|
|
phoneNode = FocusNode();
|
|
addressNode = FocusNode();
|
|
pincodeNode = FocusNode();
|
|
|
|
_setupFocusListeners();
|
|
}
|
|
|
|
void _setupFocusListeners() {
|
|
companyNode.addListener(() => _scrollToFocused(companyKey, companyNode));
|
|
panNode.addListener(() => _scrollToFocused(panKey, panNode));
|
|
gstNode.addListener(() => _scrollToFocused(gstKey, gstNode));
|
|
tanNode.addListener(() => _scrollToFocused(tanKey, tanNode));
|
|
cinNode.addListener(() => _scrollToFocused(cinKey, cinNode));
|
|
yearNode.addListener(() => _scrollToFocused(yearKey, yearNode));
|
|
emailNode.addListener(() => _scrollToFocused(emailKey, emailNode));
|
|
phoneNode.addListener(() => _scrollToFocused(phoneKey, phoneNode));
|
|
addressNode.addListener(() => _scrollToFocused(addressKey, addressNode));
|
|
pincodeNode.addListener(() => _scrollToFocused(pincodeKey, pincodeNode));
|
|
}
|
|
|
|
void _scrollToFocused(GlobalKey key, FocusNode node) {
|
|
if (node.hasFocus && mounted) {
|
|
Future.delayed(const Duration(milliseconds: 250), () {
|
|
if (!mounted) return;
|
|
final context = key.currentContext;
|
|
if (context != null) {
|
|
Scrollable.ensureVisible(
|
|
context,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeInOut,
|
|
alignment: 0.2,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void _scrollToWidget(GlobalKey key) {
|
|
if (!mounted) return;
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
if (!mounted) return;
|
|
final context = key.currentContext;
|
|
if (context != null) {
|
|
Scrollable.ensureVisible(
|
|
context,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeInOut,
|
|
alignment: 0.2,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
bool _validate() {
|
|
final panRegex = RegExp(r'^[A-Z]{5}[0-9]{4}[A-Z]{1}$');
|
|
final gstRegex = RegExp(
|
|
r'^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$',
|
|
);
|
|
final tanRegex = RegExp(r'^[A-Z]{4}[0-9]{5}[A-Z]{1}$');
|
|
final cinRegex = RegExp(
|
|
r'^[A-Z]{1}[0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$',
|
|
);
|
|
final pincodeRegex = RegExp(r'^[0-9]{6}$');
|
|
|
|
setState(() {
|
|
errors.clear();
|
|
|
|
if (companyCtrl.text.isEmpty) {
|
|
errors['company'] = 'Company name required';
|
|
}
|
|
|
|
// PAN validation with exact digit check
|
|
if (panCtrl.text.isEmpty) {
|
|
errors['pan'] = 'PAN required';
|
|
} else {
|
|
final panUpper = panCtrl.text.toUpperCase();
|
|
if (!panRegex.hasMatch(panUpper)) {
|
|
errors['pan'] = 'Invalid PAN format (e.g. ABCDE1234F)';
|
|
} else {
|
|
// Extract the 4 digits (positions 5-8)
|
|
final panDigits = panUpper.substring(5, 9);
|
|
final panYear = int.tryParse(panDigits);
|
|
|
|
if (panYear == null) {
|
|
errors['pan'] = 'Invalid PAN number format';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (gstCtrl.text.isEmpty) {
|
|
errors['gst'] = 'GST required';
|
|
} else if (!gstRegex.hasMatch(gstCtrl.text.toUpperCase())) {
|
|
errors['gst'] = 'Invalid GST format (e.g. 22ABCDE1234F1Z5)';
|
|
}
|
|
|
|
// TAN validation with year check
|
|
if (tanCtrl.text.isEmpty) {
|
|
errors['tan'] = 'TAN required';
|
|
} else {
|
|
final tanUpper = tanCtrl.text.toUpperCase();
|
|
if (!tanRegex.hasMatch(tanUpper)) {
|
|
errors['tan'] = 'Invalid TAN format (e.g. ABCD12345E)';
|
|
} else {
|
|
// Extract the 5 digits (positions 4-8)
|
|
final tanDigits = tanUpper.substring(4, 9);
|
|
final tanYear = int.tryParse(tanDigits);
|
|
|
|
if (tanYear == null) {
|
|
errors['tan'] = 'Invalid TAN number format';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cinCtrl.text.isEmpty) {
|
|
errors['cin'] = 'CIN required';
|
|
} else if (!cinRegex.hasMatch(cinCtrl.text.toUpperCase())) {
|
|
errors['cin'] = 'Invalid CIN format (e.g. L12345TN2000PLC123456)';
|
|
}
|
|
|
|
// Year of Incorporation validation
|
|
if (yearCtrl.text.isEmpty) {
|
|
errors['year'] = 'Year required';
|
|
} else {
|
|
final year = int.tryParse(yearCtrl.text);
|
|
final currentYear = DateTime.now().year;
|
|
|
|
if (year == null) {
|
|
errors['year'] = 'Invalid year format';
|
|
} else if (year < 1800 || year > currentYear) {
|
|
errors['year'] = 'Year must be between 1800 and $currentYear';
|
|
}
|
|
}
|
|
|
|
if (emailCtrl.text.isEmpty) {
|
|
errors['email'] = 'Email required';
|
|
}
|
|
|
|
if (phoneCtrl.text.isEmpty) {
|
|
errors['phone'] = 'Phone required';
|
|
}
|
|
|
|
if (addressCtrl.text.isEmpty) {
|
|
errors['address'] = 'Address required';
|
|
}
|
|
|
|
// Pincode validation - must be exactly 6 digits
|
|
if (pincodeCtrl.text.isEmpty) {
|
|
errors['pincode'] = 'Pincode required';
|
|
} else if (!pincodeRegex.hasMatch(pincodeCtrl.text)) {
|
|
errors['pincode'] = 'Pincode must be exactly 6 digits';
|
|
}
|
|
|
|
countryError = selectedCountryId == null ? 'Select country' : null;
|
|
stateError = selectedStateId == null ? 'Select state' : null;
|
|
cityError = selectedCityId == null ? 'Select city' : null;
|
|
|
|
panImageError = (panImageFile == null && panImageUrl == null)
|
|
? 'Upload PAN card image/PDF'
|
|
: null;
|
|
gstImageError = (gstImageFile == null && gstImageUrl == null)
|
|
? 'Upload GST certificate image/PDF'
|
|
: null;
|
|
incImageError =
|
|
(incorporationImageFile == null && incorporationImageUrl == null)
|
|
? 'Upload incorporation certificate image/PDF'
|
|
: null;
|
|
logoImageError = (logoImageFile == null && logoImageUrl == null)
|
|
? 'Upload company logo'
|
|
: null;
|
|
});
|
|
|
|
if (errors.isNotEmpty) {
|
|
final firstError = errors.keys.first;
|
|
final keyMap = {
|
|
'company': companyKey,
|
|
'pan': panKey,
|
|
'gst': gstKey,
|
|
'tan': tanKey,
|
|
'cin': cinKey,
|
|
'year': yearKey,
|
|
'email': emailKey,
|
|
'phone': phoneKey,
|
|
'address': addressKey,
|
|
'pincode': pincodeKey,
|
|
};
|
|
_scrollToWidget(keyMap[firstError]!);
|
|
return false;
|
|
}
|
|
|
|
if (countryError != null) {
|
|
_scrollToWidget(countryKey);
|
|
return false;
|
|
}
|
|
if (stateError != null) {
|
|
_scrollToWidget(stateKey);
|
|
return false;
|
|
}
|
|
if (cityError != null) {
|
|
_scrollToWidget(cityKey);
|
|
return false;
|
|
}
|
|
if (panImageError != null) {
|
|
_scrollToWidget(panImageKey);
|
|
return false;
|
|
}
|
|
if (gstImageError != null) {
|
|
_scrollToWidget(gstImageKey);
|
|
return false;
|
|
}
|
|
if (incImageError != null) {
|
|
_scrollToWidget(incImageKey);
|
|
return false;
|
|
}
|
|
if (logoImageError != null) {
|
|
_scrollToWidget(logoImageKey);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Future<void> _handleSubmit() async {
|
|
if (_isSubmitting) return;
|
|
|
|
if (!_validate()) return;
|
|
|
|
if (!isChecked) {
|
|
Get.snackbar(
|
|
"Error",
|
|
"Please accept declaration",
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() => _isSubmitting = true);
|
|
|
|
try {
|
|
await ApiRepository().kycFormUpdate(
|
|
logo: logoImageFile,
|
|
panFile: panImageFile,
|
|
gstFile: gstImageFile,
|
|
incorporationFile: incorporationImageFile,
|
|
countryId: selectedCountryId!,
|
|
stateId: selectedStateId!,
|
|
cityId: selectedCityId!,
|
|
companyPincode: pincodeCtrl.text,
|
|
companyAddress: addressCtrl.text,
|
|
panNumber: panCtrl.text,
|
|
gstNumber: gstCtrl.text,
|
|
tanNumber: tanCtrl.text,
|
|
cinNumber: cinCtrl.text,
|
|
yearOfIncorporation: yearCtrl.text,
|
|
address: addressCtrl.text,
|
|
);
|
|
|
|
if (!mounted) return;
|
|
|
|
ValidationPopup().showSuccessMessage(
|
|
context,
|
|
"Form Submitted Successfully",
|
|
);
|
|
|
|
ref.invalidate(profileProvider);
|
|
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
if (mounted) {
|
|
Navigator.pop(context);
|
|
}
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
|
|
String errorMessage;
|
|
|
|
if (e is Map<String, dynamic>) {
|
|
final buffer = StringBuffer();
|
|
e.forEach((key, value) {
|
|
buffer.writeln(value);
|
|
});
|
|
errorMessage = buffer.toString().trim();
|
|
} else {
|
|
errorMessage = e.toString();
|
|
}
|
|
|
|
ValidationPopup().showErrorMessage(context, errorMessage);
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() => _isSubmitting = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final profileAsync = ref.watch(profileProvider);
|
|
final countryAsync = ref.watch(countryAndStatesProvider);
|
|
|
|
return Scaffold(
|
|
backgroundColor: AppColors.white,
|
|
body: SafeArea(
|
|
child: Stack(
|
|
children: [
|
|
profileAsync.when(
|
|
data: (profileData) {
|
|
if (!_isInitialized && profileData.data != null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
final p = profileData.data!;
|
|
companyCtrl.text = p.companyName ?? '';
|
|
panCtrl.text = p.panNumber ?? '';
|
|
gstCtrl.text = p.gstNumber ?? '';
|
|
tanCtrl.text = p.tanNumber ?? '';
|
|
cinCtrl.text = p.cin ?? '';
|
|
yearCtrl.text = p.yearOfIncorporation?.toString() ?? '';
|
|
emailCtrl.text = p.companyEmail ?? '';
|
|
phoneCtrl.text = p.companyMobile ?? '';
|
|
addressCtrl.text = p.companyAddress ?? '';
|
|
pincodeCtrl.text = p.companyPincode ?? '';
|
|
|
|
selectedCountryName = p.countryName;
|
|
selectedStateName = p.stateName;
|
|
selectedCityName = p.districtName;
|
|
|
|
panImageUrl = p.panFile;
|
|
gstImageUrl = p.gstFile;
|
|
incorporationImageUrl = p.incorporationFile;
|
|
logoImageUrl = p.logo;
|
|
|
|
_isInitialized = true;
|
|
});
|
|
});
|
|
}
|
|
|
|
return countryAsync.when(
|
|
data: (countryModel) {
|
|
final countryList =
|
|
countryModel.countriesData
|
|
?.map((e) => e.countryName ?? '')
|
|
.toList() ??
|
|
[];
|
|
|
|
final selectedCountryData = countryModel.countriesData
|
|
?.firstWhere(
|
|
(c) => c.countryName == selectedCountryName,
|
|
orElse: () => country.CountryData(),
|
|
);
|
|
|
|
if (selectedCountryData?.id != null) {
|
|
selectedCountryId = selectedCountryData!.id;
|
|
}
|
|
|
|
final selectedStateList =
|
|
countryModel.statesData
|
|
?.where((s) => s.countryId == selectedCountryId)
|
|
.map((s) => s.stateName ?? '')
|
|
.toList() ??
|
|
[];
|
|
|
|
final selectedStateData = countryModel.statesData
|
|
?.firstWhere(
|
|
(s) => s.stateName == selectedStateName,
|
|
orElse: () => country.StateData(),
|
|
);
|
|
|
|
if (selectedStateData?.id != null) {
|
|
selectedStateId = selectedStateData!.id;
|
|
}
|
|
|
|
final cityAsync = selectedStateId != null
|
|
? ref.watch(fetchCityProvider(selectedStateId!))
|
|
: const AsyncValue.data(null);
|
|
|
|
return cityAsync.when(
|
|
data: (cityModel) {
|
|
final cityList =
|
|
cityModel?.districtsData
|
|
?.map((c) => c.districtName ?? '')
|
|
.toList() ??
|
|
[];
|
|
|
|
if (cityModel?.districtsData != null) {
|
|
final selectedCityData = cityModel!.districtsData!
|
|
.firstWhere(
|
|
(c) => c.districtName == selectedCityName,
|
|
orElse: () => country.DistrictData(),
|
|
);
|
|
|
|
if (selectedCityData.id != null) {
|
|
selectedCityId = selectedCityData.id;
|
|
}
|
|
}
|
|
|
|
return _buildForm(
|
|
countryList,
|
|
selectedStateList,
|
|
cityList,
|
|
countryModel,
|
|
cityModel,
|
|
);
|
|
},
|
|
loading: () => _buildForm(
|
|
countryList,
|
|
selectedStateList,
|
|
[],
|
|
countryModel,
|
|
null,
|
|
),
|
|
error: (e, _) => _buildForm(
|
|
countryList,
|
|
selectedStateList,
|
|
[],
|
|
countryModel,
|
|
null,
|
|
),
|
|
);
|
|
},
|
|
loading: () => const Center(
|
|
child: CircularProgressIndicator(color: Colors.blue),
|
|
),
|
|
error: (e, _) => Center(child: Text('Error: $e')),
|
|
);
|
|
},
|
|
loading: () => const Center(
|
|
child: CircularProgressIndicator(color: Colors.blue),
|
|
),
|
|
error: (e, _) => Center(child: Text('Error: $e')),
|
|
),
|
|
|
|
if (_isSubmitting)
|
|
Container(
|
|
color: Colors.black54,
|
|
child: const Center(
|
|
child: Card(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'Submitting KYC Form...',
|
|
style: TextStyle(fontSize: 16),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildForm(
|
|
List<String> countries,
|
|
List<String> states,
|
|
List<String> cities,
|
|
country.CountryModel countryModel,
|
|
country.CityModel? cityModel,
|
|
) {
|
|
return SingleChildScrollView(
|
|
controller: _scrollController,
|
|
padding: const EdgeInsets.symmetric(horizontal: 23, vertical: 21),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
height: 50,
|
|
width: double.infinity,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
const Text(
|
|
"KYC Form",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontFamily: 'Gilroy-SemiBold',
|
|
fontWeight: FontWeight.w400,
|
|
fontStyle: FontStyle.normal,
|
|
fontSize: 24,
|
|
height: 1.3,
|
|
letterSpacing: 0.04 * 24,
|
|
color: Color(0xFF111827),
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 0,
|
|
child: GestureDetector(
|
|
onTap: () => Get.back(),
|
|
child: const Icon(Icons.arrow_back_ios_rounded),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
// Text fields
|
|
LabeledTextField(
|
|
fieldKey: companyKey,
|
|
label: 'Company Name',
|
|
keyName: 'company',
|
|
hint: 'Enter company name',
|
|
controller: companyCtrl,
|
|
focusNode: companyNode,
|
|
readOnly: true,
|
|
errors: errors,
|
|
onChanged: (val) {
|
|
if (errors['company'] != null && val.trim().isNotEmpty) {
|
|
setState(() => errors['company'] = null);
|
|
}
|
|
},
|
|
),
|
|
LabeledTextField(
|
|
fieldKey: panKey,
|
|
label: 'PAN Number',
|
|
keyName: 'pan',
|
|
hint: 'Enter PAN number',
|
|
controller: panCtrl,
|
|
focusNode: panNode,
|
|
textCapitalization: TextCapitalization.characters,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')),
|
|
LengthLimitingTextInputFormatter(10),
|
|
],
|
|
errors: errors,
|
|
onChanged: (val) {
|
|
if (errors['pan'] != null) {
|
|
final upper = val.toUpperCase();
|
|
if (RegExp(r'^[A-Z]{5}[0-9]{4}[A-Z]{1}$').hasMatch(upper)) {
|
|
if (int.tryParse(upper.substring(5, 9)) != null) {
|
|
setState(() => errors['pan'] = null);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
),
|
|
LabeledTextField(
|
|
fieldKey: gstKey,
|
|
label: 'GST Number',
|
|
keyName: 'gst',
|
|
hint: 'Enter GST number',
|
|
controller: gstCtrl,
|
|
focusNode: gstNode,
|
|
textCapitalization: TextCapitalization.characters,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')),
|
|
LengthLimitingTextInputFormatter(15),
|
|
],
|
|
errors: errors,
|
|
onChanged: (val) {
|
|
if (errors['gst'] != null) {
|
|
if (RegExp(
|
|
r'^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$',
|
|
).hasMatch(val.toUpperCase())) {
|
|
setState(() => errors['gst'] = null);
|
|
}
|
|
}
|
|
},
|
|
),
|
|
LabeledTextField(
|
|
fieldKey: tanKey,
|
|
label: 'TAN Number',
|
|
keyName: 'tan',
|
|
hint: 'Enter TAN number',
|
|
controller: tanCtrl,
|
|
focusNode: tanNode,
|
|
textCapitalization: TextCapitalization.characters,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')),
|
|
LengthLimitingTextInputFormatter(10),
|
|
],
|
|
errors: errors,
|
|
onChanged: (val) {
|
|
if (errors['tan'] != null) {
|
|
final upper = val.toUpperCase();
|
|
if (RegExp(r'^[A-Z]{4}[0-9]{5}[A-Z]{1}$').hasMatch(upper)) {
|
|
if (int.tryParse(upper.substring(4, 9)) != null) {
|
|
setState(() => errors['tan'] = null);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
),
|
|
LabeledTextField(
|
|
fieldKey: cinKey,
|
|
label: 'CIN Number',
|
|
keyName: 'cin',
|
|
hint: 'Enter CIN number',
|
|
controller: cinCtrl,
|
|
focusNode: cinNode,
|
|
textCapitalization: TextCapitalization.characters,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')),
|
|
LengthLimitingTextInputFormatter(21),
|
|
],
|
|
errors: errors,
|
|
onChanged: (val) {
|
|
if (errors['cin'] != null) {
|
|
if (RegExp(
|
|
r'^[A-Z]{1}[0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$',
|
|
).hasMatch(val.toUpperCase())) {
|
|
setState(() => errors['cin'] = null);
|
|
}
|
|
}
|
|
},
|
|
),
|
|
LabeledTextField(
|
|
fieldKey: yearKey,
|
|
label: 'Year of Incorporation',
|
|
keyName: 'year',
|
|
hint: 'Enter year (e.g. 2020)',
|
|
controller: yearCtrl,
|
|
focusNode: yearNode,
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
LengthLimitingTextInputFormatter(4),
|
|
],
|
|
errors: errors,
|
|
onChanged: (val) {
|
|
if (errors['year'] != null) {
|
|
final y = int.tryParse(val);
|
|
if (y != null && y >= 1800 && y <= DateTime.now().year) {
|
|
setState(() => errors['year'] = null);
|
|
}
|
|
}
|
|
},
|
|
),
|
|
LabeledTextField(
|
|
fieldKey: emailKey,
|
|
label: 'Official Email Address',
|
|
keyName: 'email',
|
|
hint: 'Enter email',
|
|
controller: emailCtrl,
|
|
focusNode: emailNode,
|
|
readOnly: true,
|
|
errors: errors,
|
|
onChanged: (val) {
|
|
if (errors['email'] != null && val.trim().isNotEmpty) {
|
|
setState(() => errors['email'] = null);
|
|
}
|
|
},
|
|
),
|
|
LabeledTextField(
|
|
fieldKey: phoneKey,
|
|
label: 'Phone Number',
|
|
keyName: 'phone',
|
|
hint: 'Enter phone number',
|
|
controller: phoneCtrl,
|
|
focusNode: phoneNode,
|
|
keyboardType: TextInputType.phone,
|
|
errors: errors,
|
|
readOnly: true,
|
|
onChanged: (val) {
|
|
if (errors['phone'] != null && val.trim().isNotEmpty) {
|
|
setState(() => errors['phone'] = null);
|
|
}
|
|
},
|
|
),
|
|
LabeledTextField(
|
|
fieldKey: addressKey,
|
|
label: 'Office Address',
|
|
keyName: 'address',
|
|
hint: 'Enter address',
|
|
controller: addressCtrl,
|
|
focusNode: addressNode,
|
|
errors: errors,
|
|
onChanged: (val) {
|
|
if (errors['address'] != null && val.trim().isNotEmpty) {
|
|
setState(() => errors['address'] = null);
|
|
}
|
|
},
|
|
),
|
|
|
|
// Dropdowns
|
|
_buildDropdown(
|
|
key: countryKey,
|
|
label: "Country",
|
|
hint: "Select Country",
|
|
value: selectedCountryName,
|
|
items: countries,
|
|
onChanged: (val) {
|
|
if (!mounted) return;
|
|
final countryData = countryModel.countriesData?.firstWhere(
|
|
(c) => c.countryName == val,
|
|
orElse: () => country.CountryData(),
|
|
);
|
|
|
|
setState(() {
|
|
selectedCountryName = val;
|
|
selectedCountryId = countryData?.id;
|
|
selectedStateName = null;
|
|
selectedStateId = null;
|
|
selectedCityName = null;
|
|
selectedCityId = null;
|
|
stateError = null;
|
|
cityError = null;
|
|
countryError = null;
|
|
});
|
|
},
|
|
error: countryError,
|
|
),
|
|
|
|
_buildDropdown(
|
|
key: stateKey,
|
|
label: "State",
|
|
hint: "Select State",
|
|
value: selectedStateName,
|
|
items: states,
|
|
onChanged: (val) {
|
|
if (!mounted) return;
|
|
final stateData = countryModel.statesData?.firstWhere(
|
|
(s) => s.stateName == val,
|
|
orElse: () => country.StateData(),
|
|
);
|
|
|
|
setState(() {
|
|
selectedStateName = val;
|
|
selectedStateId = stateData?.id;
|
|
selectedCityName = null;
|
|
selectedCityId = null;
|
|
stateError = null;
|
|
cityError = null;
|
|
});
|
|
},
|
|
error: stateError,
|
|
),
|
|
|
|
_buildDropdown(
|
|
key: cityKey,
|
|
label: "City",
|
|
hint: "Select City",
|
|
value: selectedCityName,
|
|
items: cities,
|
|
onChanged: (val) {
|
|
if (!mounted) return;
|
|
final cityData = cityModel?.districtsData?.firstWhere(
|
|
(c) => c.districtName == val,
|
|
orElse: () => country.DistrictData(),
|
|
);
|
|
|
|
setState(() {
|
|
selectedCityName = val;
|
|
selectedCityId = cityData?.id;
|
|
cityError = null;
|
|
});
|
|
},
|
|
error: cityError,
|
|
),
|
|
|
|
LabeledTextField(
|
|
fieldKey: pincodeKey,
|
|
label: 'Pincode',
|
|
keyName: 'pincode',
|
|
hint: 'Enter 6 digit pincode',
|
|
controller: pincodeCtrl,
|
|
focusNode: pincodeNode,
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
LengthLimitingTextInputFormatter(6),
|
|
],
|
|
errors: errors,
|
|
onChanged: (val) {
|
|
if (errors['pincode'] != null) {
|
|
if (RegExp(r'^[0-9]{6}$').hasMatch(val)) {
|
|
setState(() => errors['pincode'] = null);
|
|
}
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// 🔥 Updated File Upload Fields (Image/PDF support for PAN, GST, Incorporation)
|
|
FileUploadSectionField(
|
|
fileKey: panImageKey,
|
|
title: "Upload PAN Card (Image/PDF)",
|
|
fileData: panImageFile,
|
|
fileUrl: panImageUrl,
|
|
errorText: panImageError,
|
|
setError: (err) => setState(() => panImageError = err),
|
|
onFileSelected: (file) => setState(() {
|
|
panImageFile = file;
|
|
panImageUrl = null;
|
|
}),
|
|
onFileRemoved: () => setState(() {
|
|
panImageFile = null;
|
|
panImageUrl = null;
|
|
}),
|
|
),
|
|
FileUploadSectionField(
|
|
fileKey: gstImageKey,
|
|
title: "Upload GST Certificate (Image/PDF)",
|
|
fileData: gstImageFile,
|
|
fileUrl: gstImageUrl,
|
|
errorText: gstImageError,
|
|
setError: (err) => setState(() => gstImageError = err),
|
|
onFileSelected: (file) => setState(() {
|
|
gstImageFile = file;
|
|
gstImageUrl = null;
|
|
}),
|
|
onFileRemoved: () => setState(() {
|
|
gstImageFile = null;
|
|
gstImageUrl = null;
|
|
}),
|
|
),
|
|
FileUploadSectionField(
|
|
fileKey: incImageKey,
|
|
title: "Upload Incorporation Certificate (Image/PDF)",
|
|
fileData: incorporationImageFile,
|
|
fileUrl: incorporationImageUrl,
|
|
errorText: incImageError,
|
|
setError: (err) => setState(() => incImageError = err),
|
|
onFileSelected: (file) => setState(() {
|
|
incorporationImageFile = file;
|
|
incorporationImageUrl = null;
|
|
}),
|
|
onFileRemoved: () => setState(() {
|
|
incorporationImageFile = null;
|
|
incorporationImageUrl = null;
|
|
}),
|
|
),
|
|
|
|
// 🔥 Logo remains image-only
|
|
SingleImageSectionField(
|
|
imageKey: logoImageKey,
|
|
title: "Upload Company Logo",
|
|
imageFile: logoImageFile,
|
|
imageUrl: logoImageUrl,
|
|
errorText: logoImageError,
|
|
setError: (err) => setState(() => logoImageError = err),
|
|
onImageSelected: (file) => setState(() {
|
|
logoImageFile = file;
|
|
logoImageUrl = null;
|
|
}),
|
|
onImageRemoved: () => setState(() {
|
|
logoImageFile = null;
|
|
logoImageUrl = null;
|
|
}),
|
|
),
|
|
|
|
Row(
|
|
children: [
|
|
Checkbox(
|
|
value: isChecked,
|
|
onChanged: (v) => setState(() => isChecked = v ?? false),
|
|
),
|
|
const Expanded(
|
|
child: Text(
|
|
"I hereby declare that the above information is true.",
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: _isSubmitting ? null : _handleSubmit,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.commanbutton,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
disabledBackgroundColor: Colors.grey,
|
|
),
|
|
child: _isSubmitting
|
|
? const SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: const Text(
|
|
"Submit",
|
|
style: TextStyle(color: Colors.white, fontSize: 16),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDropdown({
|
|
required GlobalKey key,
|
|
required String label,
|
|
required String hint,
|
|
required String? value,
|
|
required List<String> items,
|
|
required void Function(String?) onChanged,
|
|
String? error,
|
|
}) {
|
|
return Padding(
|
|
key: key,
|
|
padding: const EdgeInsets.only(bottom: 15),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 16,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
CommanDropdown(
|
|
items: items,
|
|
value: value,
|
|
hint: hint,
|
|
onChanged: onChanged,
|
|
),
|
|
if (error != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4, left: 6),
|
|
child: Text(
|
|
error,
|
|
style: const TextStyle(color: Colors.red, fontSize: 12),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
companyCtrl.dispose();
|
|
panCtrl.dispose();
|
|
gstCtrl.dispose();
|
|
tanCtrl.dispose();
|
|
cinCtrl.dispose();
|
|
yearCtrl.dispose();
|
|
emailCtrl.dispose();
|
|
phoneCtrl.dispose();
|
|
addressCtrl.dispose();
|
|
pincodeCtrl.dispose();
|
|
companyNode.dispose();
|
|
panNode.dispose();
|
|
gstNode.dispose();
|
|
tanNode.dispose();
|
|
cinNode.dispose();
|
|
yearNode.dispose();
|
|
emailNode.dispose();
|
|
phoneNode.dispose();
|
|
addressNode.dispose();
|
|
pincodeNode.dispose();
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|