OnboardingPozoAppv1/flutter_app/lib/screens/business_setup_screen.dart

888 lines
36 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:permission_handler/permission_handler.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:ui';
import '../theme.dart';
import '../route_transitions.dart';
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();
String _selectedCategory = 'supermarket';
bool _isNameValid = false;
final Set<String> _selectedDocs = {'GSTIN', 'FSSAI'};
final Map<String, String> _uploadedDocs = {};
@override
void initState() {
super.initState();
_nameController.addListener(() {
setState(() {
_isNameValid = _nameController.text.trim().isNotEmpty;
});
});
}
@override
void dispose() {
_nameController.dispose();
_gstinController.dispose();
super.dispose();
}
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},
];
Future<void> _handleImagePick(String docName, ImageSource source) async {
final permission = source == ImageSource.camera ? Permission.camera : Permission.photos;
var status = await permission.status;
if (!mounted) return;
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(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Continue'),
),
],
),
);
if (!mounted) return;
if (proceed != true) return;
status = await permission.request();
if (!mounted) return;
}
if (status.isGranted || kIsWeb) {
try {
final picker = ImagePicker();
final XFile? file = await picker.pickImage(source: source);
if (!mounted) return;
if (file != null) {
setState(() {
_uploadedDocs[docName] = file.name;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('✅ Successfully loaded $docName!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to pick image: $e'),
behavior: SnackBarBehavior.floating,
),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Permission denied. Cannot load document image.'),
behavior: SnackBarBehavior.floating,
),
);
}
}
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: () {
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: () {
Navigator.pop(context);
_handleImagePick('facade_photo', ImageSource.gallery);
},
),
],
),
);
},
);
}
Widget _buildDocChip(String label, IconData icon) {
final theme = Theme.of(context);
final isSelected = _selectedDocs.contains(label);
return InkWell(
onTap: () {
setState(() {
if (isSelected) {
_selectedDocs.remove(label);
} else {
_selectedDocs.add(label);
}
});
},
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,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[600],
),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[800],
),
),
],
),
),
);
}
Widget _buildUploadSection(String docName, IconData icon) {
final theme = Theme.of(context);
final isUploaded = _uploadedDocs.containsKey(docName);
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: () {
setState(() {
_selectedDocs.remove(docName);
});
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
)
],
),
const SizedBox(height: 12),
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),
),
child: const Center(
child: Icon(
Icons.insert_drive_file,
color: Color(0xFFAF101A),
size: 24,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_uploadedDocs[docName] ?? 'Attached_document.pdf',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 2),
const Text(
'Attached • Ready',
style: TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () {
setState(() {
_uploadedDocs.remove(docName);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$docName attachment removed.'),
behavior: SnackBarBehavior.floating,
),
);
},
),
],
),
),
] 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: [
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(8)),
),
),
const SizedBox(width: 8),
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(8)),
),
),
],
),
],
),
),
),
],
],
),
),
);
}
@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)),
onPressed: () => Navigator.pop(context),
),
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(
child: AnimatedContainer(
duration: const Duration(milliseconds: 350),
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: [
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,
),
const SizedBox(height: 16),
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),
),
const SizedBox(height: 16),
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'];
return InkWell(
onTap: () {
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],
),
),
)
],
),
),
);
},
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'GSTIN',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
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,
decoration: const InputDecoration(
hintText: '15-DIGIT GST NUMBER',
),
style: const TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('GSTIN Verified Successfully!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
},
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),
),
),
child: const Text('Verify'),
),
],
),
const SizedBox(height: 6),
Text(
"Auto-fetches address & legal name on verification.",
style: TextStyle(fontSize: 11, color: Colors.grey[500]),
),
const SizedBox(height: 16),
Text(
'Shop Location *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('📍 Location detected: Sector 62, Noida, UP'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
},
borderRadius: BorderRadius.circular(16),
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,
),
),
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,
),
),
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],
),
],
),
),
),
const SizedBox(height: 16),
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),
],
),
const SizedBox(height: 8),
..._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);
}),
const SizedBox(height: 16),
Text(
'Shop Facade Photo *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
InkWell(
onTap: () {
_showFacadePhotoOptions();
},
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(
_uploadedDocs.containsKey('facade_photo') ? _uploadedDocs['facade_photo']! : 'Upload front view',
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],
),
),
],
),
),
),
const SizedBox(height: 12),
],
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: ElevatedButton(
onPressed: _isNameValid
? () {
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,
),
child: const Text('Continue to Verification'),
),
)
],
),
),
),
),
);
}
}
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;
}
}