2026-06-10 13:29:04 +05:30
|
|
|
import 'package:flutter/material.dart';
|
2026-06-10 13:48:06 +05:30
|
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
|
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
|
import 'package:image_picker/image_picker.dart';
|
2026-06-10 17:43:21 +05:30
|
|
|
import 'package:geolocator/geolocator.dart';
|
|
|
|
|
import 'package:geocoding/geocoding.dart';
|
2026-06-10 13:29:04 +05:30
|
|
|
import 'dart:ui';
|
2026-06-10 17:43:21 +05:30
|
|
|
import 'dart:io';
|
2026-06-10 13:29:04 +05:30
|
|
|
import '../theme.dart';
|
2026-06-10 13:48:06 +05:30
|
|
|
import '../route_transitions.dart';
|
2026-06-10 17:43:21 +05:30
|
|
|
import '../feedback_helper.dart';
|
2026-06-10 13:29:04 +05:30
|
|
|
import 'customize_setup_screen.dart';
|
|
|
|
|
|
|
|
|
|
class BusinessSetupScreen extends StatefulWidget {
|
|
|
|
|
const BusinessSetupScreen({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<BusinessSetupScreen> createState() => _BusinessSetupScreenState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
|
|
|
|
final TextEditingController _nameController = TextEditingController();
|
|
|
|
|
final TextEditingController _gstinController = TextEditingController();
|
2026-06-10 17:43:21 +05:30
|
|
|
final TextEditingController _addressController = TextEditingController();
|
2026-06-10 18:24:02 +05:30
|
|
|
final TextEditingController _fssaiController = TextEditingController();
|
|
|
|
|
final TextEditingController _udhyogController = TextEditingController();
|
|
|
|
|
final TextEditingController _panController = TextEditingController();
|
|
|
|
|
final TextEditingController _aadharController = TextEditingController();
|
2026-06-10 17:43:21 +05:30
|
|
|
|
2026-06-10 13:29:04 +05:30
|
|
|
String _selectedCategory = 'supermarket';
|
|
|
|
|
bool _isNameValid = false;
|
2026-06-10 17:43:21 +05:30
|
|
|
|
|
|
|
|
bool _isGstinValid = false;
|
|
|
|
|
bool _hasGstinInput = false;
|
|
|
|
|
|
2026-06-10 13:29:04 +05:30
|
|
|
final Set<String> _selectedDocs = {'GSTIN', 'FSSAI'};
|
2026-06-10 17:43:21 +05:30
|
|
|
final Map<String, XFile> _uploadedDocs = {};
|
2026-06-10 13:29:04 +05:30
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_nameController.addListener(() {
|
|
|
|
|
setState(() {
|
|
|
|
|
_isNameValid = _nameController.text.trim().isNotEmpty;
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-06-10 17:43:21 +05:30
|
|
|
_gstinController.addListener(_validateGstin);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _validateGstin() {
|
|
|
|
|
final text = _gstinController.text.trim();
|
|
|
|
|
if (text.isEmpty) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_hasGstinInput = false;
|
|
|
|
|
_isGstinValid = false;
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final RegExp gstinRegex = RegExp(r'^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[0-9]{1}Z[A-Z0-9]{1}$');
|
|
|
|
|
setState(() {
|
|
|
|
|
_hasGstinInput = true;
|
|
|
|
|
_isGstinValid = gstinRegex.hasMatch(text);
|
|
|
|
|
});
|
2026-06-10 13:29:04 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_nameController.dispose();
|
|
|
|
|
_gstinController.dispose();
|
2026-06-10 17:43:21 +05:30
|
|
|
_addressController.dispose();
|
2026-06-10 18:24:02 +05:30
|
|
|
_fssaiController.dispose();
|
|
|
|
|
_udhyogController.dispose();
|
|
|
|
|
_panController.dispose();
|
|
|
|
|
_aadharController.dispose();
|
2026-06-10 13:29:04 +05:30
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 18:24:02 +05:30
|
|
|
TextEditingController _getControllerForDoc(String docName) {
|
|
|
|
|
switch (docName) {
|
|
|
|
|
case 'GSTIN':
|
|
|
|
|
return _gstinController;
|
|
|
|
|
case 'FSSAI':
|
|
|
|
|
return _fssaiController;
|
|
|
|
|
case 'Udhyog Aadhar':
|
|
|
|
|
return _udhyogController;
|
|
|
|
|
case 'PAN':
|
|
|
|
|
return _panController;
|
|
|
|
|
case 'Aadhar':
|
|
|
|
|
return _aadharController;
|
|
|
|
|
default:
|
|
|
|
|
return TextEditingController();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 13:29:04 +05:30
|
|
|
final List<Map<String, dynamic>> _categories = [
|
|
|
|
|
{'id': 'supermarket', 'name': 'Supermarket', 'icon': Icons.shopping_cart},
|
|
|
|
|
{'id': 'restaurant', 'name': 'Restaurant', 'icon': Icons.restaurant},
|
|
|
|
|
{'id': 'clothing', 'name': 'Clothing', 'icon': Icons.checkroom},
|
|
|
|
|
{'id': 'kirana', 'name': 'Kirana', 'icon': Icons.local_mall},
|
|
|
|
|
{'id': 'grocers', 'name': 'Grocers', 'icon': Icons.storefront},
|
|
|
|
|
{'id': 'others', 'name': 'Others', 'icon': Icons.grid_view},
|
|
|
|
|
];
|
|
|
|
|
|
2026-06-10 13:48:06 +05:30
|
|
|
Future<void> _handleImagePick(String docName, ImageSource source) async {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.click();
|
2026-06-10 13:48:06 +05:30
|
|
|
final permission = source == ImageSource.camera ? Permission.camera : Permission.photos;
|
|
|
|
|
var status = await permission.status;
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
|
2026-06-10 17:43:21 +05:30
|
|
|
if (status.isPermanentlyDenied) {
|
|
|
|
|
_showSettingsDialog(source == ImageSource.camera ? 'Camera' : 'Photos');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 13:48:06 +05:30
|
|
|
if (status.isDenied && !kIsWeb) {
|
|
|
|
|
final String sourceName = source == ImageSource.camera ? 'Camera' : 'Photos/Gallery';
|
|
|
|
|
final bool? proceed = await showDialog<bool>(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) => AlertDialog(
|
|
|
|
|
title: Text('$sourceName Permission Required'),
|
|
|
|
|
content: Text(
|
|
|
|
|
'PozoApp requires $sourceName permission to upload document verification files and store facade images.',
|
|
|
|
|
),
|
|
|
|
|
actions: [
|
|
|
|
|
TextButton(
|
2026-06-10 17:43:21 +05:30
|
|
|
onPressed: () {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
Navigator.pop(context, false);
|
|
|
|
|
},
|
2026-06-10 13:48:06 +05:30
|
|
|
child: const Text('Cancel'),
|
|
|
|
|
),
|
|
|
|
|
TextButton(
|
2026-06-10 17:43:21 +05:30
|
|
|
onPressed: () {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
Navigator.pop(context, true);
|
|
|
|
|
},
|
2026-06-10 13:48:06 +05:30
|
|
|
child: const Text('Continue'),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
if (proceed != true) return;
|
|
|
|
|
status = await permission.request();
|
|
|
|
|
if (!mounted) return;
|
2026-06-10 17:43:21 +05:30
|
|
|
|
|
|
|
|
if (status.isPermanentlyDenied) {
|
|
|
|
|
_showSettingsDialog(sourceName);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-10 13:48:06 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status.isGranted || kIsWeb) {
|
|
|
|
|
try {
|
|
|
|
|
final picker = ImagePicker();
|
|
|
|
|
final XFile? file = await picker.pickImage(source: source);
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
if (file != null) {
|
|
|
|
|
setState(() {
|
2026-06-10 17:43:21 +05:30
|
|
|
_uploadedDocs[docName] = file;
|
2026-06-10 13:48:06 +05:30
|
|
|
});
|
2026-06-10 14:05:58 +05:30
|
|
|
showTopAlert(
|
|
|
|
|
context,
|
|
|
|
|
'Successfully loaded $docName!',
|
|
|
|
|
icon: Icons.check_circle_outline,
|
2026-06-10 13:48:06 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (!mounted) return;
|
2026-06-10 14:05:58 +05:30
|
|
|
showTopAlert(
|
|
|
|
|
context,
|
|
|
|
|
'Failed to pick image: $e',
|
|
|
|
|
isError: true,
|
2026-06-10 13:48:06 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2026-06-10 14:05:58 +05:30
|
|
|
showTopAlert(
|
|
|
|
|
context,
|
|
|
|
|
'Permission denied. Cannot load document image.',
|
|
|
|
|
isError: true,
|
2026-06-10 13:48:06 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 17:43:21 +05:30
|
|
|
void _showSettingsDialog(String permissionName) {
|
|
|
|
|
showDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) => AlertDialog(
|
|
|
|
|
title: Text('$permissionName Permission Disabled'),
|
|
|
|
|
content: Text('Please enable $permissionName access in system settings to proceed.'),
|
|
|
|
|
actions: [
|
|
|
|
|
TextButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
},
|
|
|
|
|
child: const Text('Cancel'),
|
|
|
|
|
),
|
|
|
|
|
TextButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
openAppSettings();
|
|
|
|
|
},
|
|
|
|
|
child: const Text('Open Settings'),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _fetchLocationAndAddress() async {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
bool serviceEnabled;
|
|
|
|
|
LocationPermission permission;
|
|
|
|
|
|
|
|
|
|
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
|
|
|
|
if (!serviceEnabled) {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
showTopAlert(context, 'Location services are disabled.', isError: true);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
permission = await Geolocator.checkPermission();
|
|
|
|
|
if (permission == LocationPermission.denied) {
|
|
|
|
|
permission = await Geolocator.requestPermission();
|
|
|
|
|
if (permission == LocationPermission.denied) {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
showTopAlert(context, 'Location permissions are denied', isError: true);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (permission == LocationPermission.deniedForever) {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
_showSettingsDialog('Location');
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mounted) {
|
|
|
|
|
showTopAlert(context, 'Fetching location...', icon: Icons.gps_fixed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
Position position = await Geolocator.getCurrentPosition(
|
|
|
|
|
desiredAccuracy: LocationAccuracy.high);
|
|
|
|
|
|
|
|
|
|
if (kIsWeb) {
|
|
|
|
|
// Reverse geocoding might have CORS issues on web without a backend proxy or proper keys depending on provider,
|
|
|
|
|
// Mocking for web demonstration unless specifically supported
|
|
|
|
|
setState(() {
|
|
|
|
|
_addressController.text = 'Web Mock Address: Lat ${position.latitude.toStringAsFixed(4)}, Lng ${position.longitude.toStringAsFixed(4)}\nCity, State, Country';
|
|
|
|
|
});
|
|
|
|
|
if (mounted) {
|
|
|
|
|
showTopAlert(context, 'Location detected (Web Mock)', icon: Icons.my_location_outlined);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
List<Placemark> placemarks = await placemarkFromCoordinates(
|
|
|
|
|
position.latitude, position.longitude);
|
|
|
|
|
|
|
|
|
|
if (placemarks.isNotEmpty) {
|
|
|
|
|
Placemark place = placemarks[0];
|
|
|
|
|
List<String> parts = [];
|
|
|
|
|
if (place.name != null && place.name!.isNotEmpty) parts.add(place.name!);
|
|
|
|
|
if (place.subLocality != null && place.subLocality!.isNotEmpty) parts.add(place.subLocality!);
|
|
|
|
|
if (place.locality != null && place.locality!.isNotEmpty) parts.add(place.locality!);
|
|
|
|
|
if (place.administrativeArea != null && place.administrativeArea!.isNotEmpty) parts.add(place.administrativeArea!);
|
|
|
|
|
if (place.postalCode != null && place.postalCode!.isNotEmpty) parts.add(place.postalCode!);
|
|
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
|
_addressController.text = parts.join(', ');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (mounted) {
|
|
|
|
|
showTopAlert(context, 'Address auto-filled successfully!', icon: Icons.my_location_outlined);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
showTopAlert(context, 'Failed to fetch address: $e', isError: true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 13:48:06 +05:30
|
|
|
void _showFacadePhotoOptions() {
|
|
|
|
|
showModalBottomSheet(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) {
|
|
|
|
|
return SafeArea(
|
|
|
|
|
child: Wrap(
|
|
|
|
|
children: [
|
|
|
|
|
ListTile(
|
|
|
|
|
leading: const Icon(Icons.camera_alt, color: Color(0xFFAF101A)),
|
|
|
|
|
title: const Text('Take Photo'),
|
|
|
|
|
onTap: () {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.click();
|
2026-06-10 13:48:06 +05:30
|
|
|
Navigator.pop(context);
|
|
|
|
|
_handleImagePick('facade_photo', ImageSource.camera);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
leading: const Icon(Icons.photo_library, color: Color(0xFFAF101A)),
|
|
|
|
|
title: const Text('Choose from Gallery'),
|
|
|
|
|
onTap: () {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.click();
|
2026-06-10 13:48:06 +05:30
|
|
|
Navigator.pop(context);
|
|
|
|
|
_handleImagePick('facade_photo', ImageSource.gallery);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 13:29:04 +05:30
|
|
|
Widget _buildDocChip(String label, IconData icon) {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
final isSelected = _selectedDocs.contains(label);
|
2026-06-10 14:47:50 +05:30
|
|
|
return InteractiveScale(
|
|
|
|
|
child: InkWell(
|
|
|
|
|
onTap: () {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.lightHaptic();
|
|
|
|
|
if (!isSelected) {
|
|
|
|
|
_selectedDocs.add(label);
|
|
|
|
|
_showFacadePhotoOptionsForDoc(label);
|
|
|
|
|
} else {
|
|
|
|
|
setState(() {
|
2026-06-10 14:47:50 +05:30
|
|
|
_selectedDocs.remove(label);
|
2026-06-10 17:43:21 +05:30
|
|
|
_uploadedDocs.remove(label);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-06-10 14:47:50 +05:30
|
|
|
},
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
child: Container(
|
|
|
|
|
constraints: const BoxConstraints(minHeight: 48),
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: isSelected ? const Color(0xFFFAF0F0) : Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
border: Border.all(
|
|
|
|
|
color: isSelected ? const Color(0xFFAF101A) : theme.colorScheme.outlineVariant,
|
|
|
|
|
width: isSelected ? 1.5 : 1,
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
icon,
|
|
|
|
|
size: 16,
|
|
|
|
|
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[600],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
Text(
|
|
|
|
|
label,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[800],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 17:43:21 +05:30
|
|
|
void _showFacadePhotoOptionsForDoc(String docName) {
|
|
|
|
|
showModalBottomSheet(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) {
|
|
|
|
|
return SafeArea(
|
|
|
|
|
child: Wrap(
|
|
|
|
|
children: [
|
|
|
|
|
ListTile(
|
|
|
|
|
leading: const Icon(Icons.camera_alt, color: Color(0xFFAF101A)),
|
|
|
|
|
title: const Text('Take Photo'),
|
|
|
|
|
onTap: () {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
_handleImagePick(docName, ImageSource.camera);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
leading: const Icon(Icons.photo_library, color: Color(0xFFAF101A)),
|
|
|
|
|
title: const Text('Choose from Gallery'),
|
|
|
|
|
onTap: () {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
_handleImagePick(docName, ImageSource.gallery);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 13:29:04 +05:30
|
|
|
Widget _buildUploadSection(String docName, IconData icon) {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
final isUploaded = _uploadedDocs.containsKey(docName);
|
2026-06-10 13:48:06 +05:30
|
|
|
|
2026-06-10 13:29:04 +05:30
|
|
|
return Container(
|
|
|
|
|
margin: const EdgeInsets.only(top: 12),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
border: Border.all(
|
|
|
|
|
color: theme.colorScheme.outlineVariant,
|
|
|
|
|
width: 1,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(icon, color: const Color(0xFFAF101A), size: 18),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
Text(
|
|
|
|
|
'$docName Document',
|
|
|
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: theme.colorScheme.onSurface,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.close, size: 18, color: Colors.grey),
|
|
|
|
|
onPressed: () {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.click();
|
2026-06-10 13:29:04 +05:30
|
|
|
setState(() {
|
|
|
|
|
_selectedDocs.remove(docName);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
constraints: const BoxConstraints(),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
2026-06-10 18:24:02 +05:30
|
|
|
Text(
|
|
|
|
|
'Enter $docName Number (Optional if photo uploaded)',
|
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Colors.grey[700],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
TextField(
|
|
|
|
|
controller: _getControllerForDoc(docName),
|
|
|
|
|
textCapitalization: TextCapitalization.characters,
|
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
hintText: 'e.g. Enter $docName number',
|
|
|
|
|
prefixIcon: Icon(icon, size: 16, color: Colors.grey[500]),
|
|
|
|
|
),
|
|
|
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(child: Divider(color: Colors.grey[300])),
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
|
|
|
child: Text(
|
|
|
|
|
'OR UPLOAD PHOTO',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Colors.grey[500],
|
|
|
|
|
letterSpacing: 0.5,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Expanded(child: Divider(color: Colors.grey[300])),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
2026-06-10 13:29:04 +05:30
|
|
|
if (isUploaded) ...[
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.grey[50],
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
border: Border.all(color: Colors.grey[200]!),
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
width: 50,
|
|
|
|
|
height: 50,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.red[50],
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
2026-06-10 17:43:21 +05:30
|
|
|
clipBehavior: Clip.antiAlias,
|
|
|
|
|
child: kIsWeb
|
|
|
|
|
? Image.network(
|
|
|
|
|
_uploadedDocs[docName]!.path,
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
|
|
|
const Icon(Icons.broken_image, color: Color(0xFFAF101A)),
|
|
|
|
|
)
|
|
|
|
|
: Image.file(
|
|
|
|
|
File(_uploadedDocs[docName]!.path),
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
|
|
|
const Icon(Icons.broken_image, color: Color(0xFFAF101A)),
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
2026-06-10 17:43:21 +05:30
|
|
|
_uploadedDocs[docName]?.name ?? 'Attached_document.jpg',
|
2026-06-10 13:29:04 +05:30
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
|
const Text(
|
2026-06-10 13:48:06 +05:30
|
|
|
'Attached • Ready',
|
2026-06-10 13:29:04 +05:30
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
color: Colors.grey,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
|
|
|
|
onPressed: () {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.click();
|
2026-06-10 13:29:04 +05:30
|
|
|
setState(() {
|
|
|
|
|
_uploadedDocs.remove(docName);
|
|
|
|
|
});
|
2026-06-10 14:05:58 +05:30
|
|
|
showTopAlert(
|
|
|
|
|
context,
|
|
|
|
|
'$docName attachment removed.',
|
|
|
|
|
icon: Icons.delete_outline,
|
2026-06-10 13:29:04 +05:30
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
] else ...[
|
|
|
|
|
CustomPaint(
|
|
|
|
|
painter: DashedBorderPainter(
|
|
|
|
|
color: const Color(0xFFAF101A),
|
|
|
|
|
radius: 16,
|
|
|
|
|
),
|
|
|
|
|
child: Container(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.transparent,
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
const Icon(
|
|
|
|
|
Icons.cloud_upload_outlined,
|
|
|
|
|
color: Color(0xFFAF101A),
|
|
|
|
|
size: 32,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Text(
|
|
|
|
|
'Upload your $docName Proof',
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Colors.grey,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
2026-06-10 14:47:50 +05:30
|
|
|
InteractiveScale(
|
|
|
|
|
child: ElevatedButton.icon(
|
|
|
|
|
onPressed: () => _handleImagePick(docName, ImageSource.camera),
|
|
|
|
|
icon: const Icon(Icons.camera_alt, size: 14),
|
|
|
|
|
label: const Text('Take Photo'),
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
minimumSize: const Size(0, 36),
|
|
|
|
|
backgroundColor: const Color(0xFFAF101A),
|
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
|
elevation: 1,
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(16)),
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
2026-06-10 14:47:50 +05:30
|
|
|
InteractiveScale(
|
|
|
|
|
child: OutlinedButton.icon(
|
|
|
|
|
onPressed: () => _handleImagePick(docName, ImageSource.gallery),
|
|
|
|
|
icon: const Icon(Icons.photo_library, size: 14),
|
|
|
|
|
label: const Text('From Gallery'),
|
|
|
|
|
style: OutlinedButton.styleFrom(
|
|
|
|
|
foregroundColor: const Color(0xFFAF101A),
|
|
|
|
|
side: const BorderSide(color: Color(0xFFAF101A)),
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(16)),
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
final padding = context.responsivePadding;
|
|
|
|
|
|
|
|
|
|
return Scaffold(
|
|
|
|
|
appBar: AppBar(
|
|
|
|
|
leading: IconButton(
|
|
|
|
|
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
|
2026-06-10 17:43:21 +05:30
|
|
|
onPressed: () {
|
|
|
|
|
AppFeedback.click();
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
},
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
title: Text(
|
|
|
|
|
'PozoApp',
|
|
|
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
|
|
|
color: theme.colorScheme.primary,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
actions: [
|
|
|
|
|
Padding(
|
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: padding),
|
|
|
|
|
child: Center(
|
|
|
|
|
child: Text(
|
|
|
|
|
'Step 3 of 5',
|
|
|
|
|
style: theme.textTheme.labelMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
bottom: PreferredSize(
|
|
|
|
|
preferredSize: const Size.fromHeight(6),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: List.generate(5, (index) {
|
|
|
|
|
return Expanded(
|
2026-06-10 13:48:06 +05:30
|
|
|
child: AnimatedContainer(
|
|
|
|
|
duration: const Duration(milliseconds: 350),
|
2026-06-10 13:29:04 +05:30
|
|
|
height: 6,
|
|
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 2.0),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: index <= 2 ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest,
|
|
|
|
|
borderRadius: BorderRadius.circular(3),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
body: SafeArea(
|
|
|
|
|
child: Center(
|
|
|
|
|
child: ConstrainedBox(
|
|
|
|
|
constraints: const BoxConstraints(maxWidth: 600),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
physics: const BouncingScrollPhysics(),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: Duration.zero,
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Complete verification',
|
|
|
|
|
style: theme.textTheme.headlineLarge?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
Text(
|
|
|
|
|
'Verify your business details to start selling.',
|
|
|
|
|
style: theme.textTheme.bodyMedium,
|
|
|
|
|
),
|
|
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 100),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Business/Shop Name *',
|
|
|
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
|
|
|
color: theme.colorScheme.onSurface,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
TextField(
|
|
|
|
|
controller: _nameController,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
hintText: 'e.g. Sharma General Store',
|
|
|
|
|
),
|
|
|
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
|
|
|
),
|
|
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 180),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Business Type *',
|
|
|
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
|
|
|
color: theme.colorScheme.onSurface,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
GridView.builder(
|
|
|
|
|
shrinkWrap: true,
|
|
|
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
|
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
|
|
|
crossAxisCount: 2,
|
|
|
|
|
crossAxisSpacing: 16,
|
|
|
|
|
mainAxisSpacing: 16,
|
|
|
|
|
childAspectRatio: 2.2,
|
|
|
|
|
),
|
|
|
|
|
itemCount: _categories.length,
|
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
|
final cat = _categories[index];
|
|
|
|
|
final isSelected = _selectedCategory == cat['id'];
|
2026-06-10 13:29:04 +05:30
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
return InteractiveScale(
|
|
|
|
|
child: InkWell(
|
|
|
|
|
onTap: () {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.select();
|
2026-06-10 14:47:50 +05:30
|
|
|
setState(() {
|
|
|
|
|
_selectedCategory = cat['id'];
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
child: Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: isSelected
|
|
|
|
|
? const Color(0xFFAF101A).withOpacity(0.08)
|
|
|
|
|
: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
border: Border.all(
|
|
|
|
|
color: isSelected ? const Color(0xFFAF101A) : const Color(0xFFE2E2E2),
|
|
|
|
|
width: isSelected ? 2 : 1,
|
|
|
|
|
),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: Colors.black.withOpacity(0.01),
|
|
|
|
|
blurRadius: 4,
|
|
|
|
|
offset: const Offset(0, 2),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Center(
|
|
|
|
|
child: Icon(
|
|
|
|
|
cat['icon'],
|
|
|
|
|
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[600],
|
|
|
|
|
size: 26,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
Center(
|
|
|
|
|
child: Text(
|
|
|
|
|
cat['name'],
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[800],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
2026-06-10 13:29:04 +05:30
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 260),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'GSTIN',
|
|
|
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
|
|
|
color: theme.colorScheme.onSurface,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
Text(
|
|
|
|
|
'(Optional)',
|
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
color: Colors.grey[500],
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: TextField(
|
|
|
|
|
controller: _gstinController,
|
|
|
|
|
textCapitalization: TextCapitalization.characters,
|
2026-06-10 17:43:21 +05:30
|
|
|
decoration: InputDecoration(
|
2026-06-10 14:47:50 +05:30
|
|
|
hintText: '15-DIGIT GST NUMBER',
|
2026-06-10 17:43:21 +05:30
|
|
|
suffixIcon: _hasGstinInput
|
|
|
|
|
? Icon(
|
|
|
|
|
_isGstinValid ? Icons.check_circle : Icons.error_outline,
|
|
|
|
|
color: _isGstinValid ? Colors.green : Colors.red,
|
|
|
|
|
)
|
|
|
|
|
: null,
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
style: const TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
InteractiveScale(
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
onPressed: () {
|
2026-06-10 17:43:21 +05:30
|
|
|
if (_isGstinValid) {
|
|
|
|
|
AppFeedback.success();
|
|
|
|
|
showTopAlert(
|
|
|
|
|
context,
|
|
|
|
|
'GSTIN Verified Successfully!',
|
|
|
|
|
icon: Icons.verified_outlined,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
AppFeedback.error();
|
|
|
|
|
showTopAlert(
|
|
|
|
|
context,
|
|
|
|
|
'Invalid GSTIN Format. Check 15-digit code.',
|
|
|
|
|
isError: true,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-06-10 14:47:50 +05:30
|
|
|
},
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
minimumSize: const Size(0, 48),
|
|
|
|
|
backgroundColor: theme.colorScheme.primary,
|
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
child: const Text('Verify'),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
Text(
|
|
|
|
|
"Auto-fetches address & legal name on verification.",
|
|
|
|
|
style: TextStyle(fontSize: 11, color: Colors.grey[500]),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 340),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Shop Location *',
|
|
|
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
|
|
|
color: theme.colorScheme.onSurface,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
InteractiveScale(
|
|
|
|
|
child: InkWell(
|
2026-06-10 17:43:21 +05:30
|
|
|
onTap: _fetchLocationAndAddress,
|
2026-06-10 13:29:04 +05:30
|
|
|
borderRadius: BorderRadius.circular(16),
|
2026-06-10 14:47:50 +05:30
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
border: Border.all(
|
|
|
|
|
color: theme.colorScheme.outlineVariant,
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(10),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.colorScheme.primary.withOpacity(0.1),
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
),
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.my_location,
|
|
|
|
|
color: theme.colorScheme.primary,
|
|
|
|
|
size: 20,
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const SizedBox(width: 14),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Detect current location',
|
|
|
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
fontSize: 15,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
|
Text(
|
|
|
|
|
'Pin shop address automatically',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
color: Colors.grey[600],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.chevron_right,
|
|
|
|
|
color: Colors.grey[400],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
),
|
2026-06-10 17:43:21 +05:30
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
Text(
|
|
|
|
|
'Shop Address *',
|
|
|
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
|
|
|
color: theme.colorScheme.onSurface,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
TextField(
|
|
|
|
|
controller: _addressController,
|
|
|
|
|
maxLines: 3,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
hintText: 'Enter complete shop address',
|
|
|
|
|
),
|
|
|
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 420),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Identity / License Documents (Select all that apply)',
|
|
|
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
|
|
|
color: theme.colorScheme.onSurface,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
Wrap(
|
|
|
|
|
spacing: 8,
|
|
|
|
|
runSpacing: 8,
|
|
|
|
|
children: [
|
|
|
|
|
_buildDocChip('GSTIN', Icons.description),
|
|
|
|
|
_buildDocChip('FSSAI', Icons.restaurant),
|
|
|
|
|
_buildDocChip('Aadhar', Icons.badge_outlined),
|
|
|
|
|
_buildDocChip('Udhyog Aadhar', Icons.domain),
|
|
|
|
|
_buildDocChip('PAN', Icons.credit_card),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
2026-06-10 13:48:06 +05:30
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 500),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: _selectedDocs.map((doc) {
|
|
|
|
|
IconData docIcon = Icons.description;
|
|
|
|
|
if (doc == 'GSTIN') docIcon = Icons.description;
|
|
|
|
|
if (doc == 'FSSAI') docIcon = Icons.restaurant;
|
|
|
|
|
if (doc == 'Aadhar') docIcon = Icons.badge_outlined;
|
|
|
|
|
if (doc == 'Udhyog Aadhar') docIcon = Icons.domain;
|
|
|
|
|
if (doc == 'PAN') docIcon = Icons.credit_card;
|
|
|
|
|
return _buildUploadSection(doc, docIcon);
|
|
|
|
|
}).toList(),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
|
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 580),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'Shop Facade Photo *',
|
|
|
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
|
|
|
color: theme.colorScheme.onSurface,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
InteractiveScale(
|
|
|
|
|
child: InkWell(
|
|
|
|
|
onTap: () {
|
|
|
|
|
_showFacadePhotoOptions();
|
|
|
|
|
},
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
child: Container(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
border: Border.all(
|
|
|
|
|
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green : theme.colorScheme.outlineVariant,
|
|
|
|
|
width: _uploadedDocs.containsKey('facade_photo') ? 2.0 : 1.0,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green.withOpacity(0.1) : theme.colorScheme.primary.withOpacity(0.1),
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
),
|
|
|
|
|
child: Icon(
|
|
|
|
|
_uploadedDocs.containsKey('facade_photo') ? Icons.check_circle : Icons.add_a_photo_outlined,
|
|
|
|
|
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green : theme.colorScheme.primary,
|
|
|
|
|
size: 24,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
Text(
|
2026-06-10 17:43:21 +05:30
|
|
|
_uploadedDocs.containsKey('facade_photo') ? _uploadedDocs['facade_photo']!.name : 'Upload front view',
|
2026-06-10 14:47:50 +05:30
|
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: theme.colorScheme.onSurface,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
Text(
|
|
|
|
|
_uploadedDocs.containsKey('facade_photo') ? 'Facade Photo Attached • Tap to change' : 'Clear photo showing shop board',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
color: Colors.grey[500],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
2026-06-10 14:47:50 +05:30
|
|
|
),
|
|
|
|
|
],
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
2026-06-10 14:47:50 +05:30
|
|
|
SlideFadeEntrance(
|
|
|
|
|
delay: const Duration(milliseconds: 660),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
|
|
|
|
child: InteractiveScale(
|
|
|
|
|
hoverScale: _isNameValid ? 1.03 : 1.0,
|
|
|
|
|
tapScale: _isNameValid ? 0.96 : 1.0,
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
onPressed: _isNameValid
|
|
|
|
|
? () {
|
2026-06-10 17:43:21 +05:30
|
|
|
AppFeedback.click();
|
2026-06-10 14:47:50 +05:30
|
|
|
Navigator.push(
|
|
|
|
|
context,
|
|
|
|
|
FadeSlidePageRoute(
|
|
|
|
|
child: CustomizeSetupScreen(
|
|
|
|
|
businessName: _nameController.text.trim(),
|
|
|
|
|
businessType: _selectedCategory,
|
|
|
|
|
gstin: _gstinController.text.trim(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
minimumSize: const Size.fromHeight(54),
|
|
|
|
|
backgroundColor: _isNameValid ? theme.colorScheme.primary : Colors.grey[200],
|
|
|
|
|
foregroundColor: _isNameValid ? Colors.white : Colors.grey[400],
|
|
|
|
|
elevation: _isNameValid ? 2 : 0,
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: const Text('Continue to Verification'),
|
|
|
|
|
),
|
2026-06-10 13:29:04 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class DashedBorderPainter extends CustomPainter {
|
|
|
|
|
final Color color;
|
|
|
|
|
final double strokeWidth;
|
|
|
|
|
final double gap;
|
|
|
|
|
final double radius;
|
|
|
|
|
|
|
|
|
|
DashedBorderPainter({
|
|
|
|
|
required this.color,
|
|
|
|
|
this.strokeWidth = 1.5,
|
|
|
|
|
this.gap = 4.0,
|
|
|
|
|
this.radius = 16.0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void paint(Canvas canvas, Size size) {
|
|
|
|
|
final paint = Paint()
|
|
|
|
|
..color = color
|
|
|
|
|
..strokeWidth = strokeWidth
|
|
|
|
|
..style = PaintingStyle.stroke;
|
|
|
|
|
|
|
|
|
|
final RRect rrect = RRect.fromRectAndRadius(
|
|
|
|
|
Rect.fromLTWH(0, 0, size.width, size.height),
|
|
|
|
|
Radius.circular(radius),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final Path path = Path()..addRRect(rrect);
|
|
|
|
|
final Path dashPath = Path();
|
|
|
|
|
|
|
|
|
|
double distance = 0.0;
|
|
|
|
|
bool draw = true;
|
|
|
|
|
|
|
|
|
|
for (final PathMetric metric in path.computeMetrics()) {
|
|
|
|
|
while (distance < metric.length) {
|
|
|
|
|
final double len = draw ? gap * 1.5 : gap;
|
|
|
|
|
if (draw) {
|
|
|
|
|
dashPath.addPath(
|
|
|
|
|
metric.extractPath(distance, (distance + len).clamp(0.0, metric.length)),
|
|
|
|
|
Offset.zero,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
distance += len;
|
|
|
|
|
draw = !draw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
canvas.drawPath(dashPath, paint);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
canvas.drawRRect(rrect, paint);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool shouldRepaint(covariant DashedBorderPainter oldDelegate) {
|
|
|
|
|
return oldDelegate.color != color ||
|
|
|
|
|
oldDelegate.strokeWidth != strokeWidth ||
|
|
|
|
|
oldDelegate.gap != gap ||
|
|
|
|
|
oldDelegate.radius != radius;
|
|
|
|
|
}
|
|
|
|
|
}
|