taxgilde/lib/view/screens/profile/kyc_detail_screen.dart
2026-04-11 10:21:31 +05:30

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();
}
}