marco.pms.mobileapp/lib/view/auth/request_demo_bottom_sheet.dart

459 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_text.dart';
class OrganizationFormBottomSheet {
static void show(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => DraggableScrollableSheet(
expand: false,
initialChildSize: 0.85,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) =>
_OrganizationForm(scrollController: scrollController),
),
);
}
}
class _OrganizationForm extends StatefulWidget {
final ScrollController scrollController;
const _OrganizationForm({required this.scrollController});
@override
State<_OrganizationForm> createState() => _OrganizationFormState();
}
class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
final MyFormValidator validator = MyFormValidator();
bool _loading = false;
bool _agreed = false;
List<Map<String, dynamic>> _industries = [];
String? _selectedIndustryId;
String? _selectedSize;
final List<String> _sizes = ['1-10', '11-50', '51-200', '201-1000', '1000+'];
final Map<String, String> _sizeApiMap = {
'1-10': 'less than 10',
'11-50': '11 to 50',
'51-200': '51 to 200',
'201-1000': 'more than 200',
'1000+': 'more than 1000',
};
@override
void initState() {
super.initState();
_loadIndustries();
_registerFields();
}
void _registerFields() {
validator.addField('organizationName',
required: true, controller: TextEditingController());
validator.addField('email',
required: true, controller: TextEditingController());
validator.addField('contactPerson',
required: true, controller: TextEditingController());
validator.addField('contactNumber',
required: true, controller: TextEditingController());
validator.addField('about', controller: TextEditingController());
validator.addField('address',
required: true, controller: TextEditingController());
}
Future<void> _loadIndustries() async {
final industries = await AuthService.getIndustries();
if (industries != null) {
setState(() {
_industries = industries;
});
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
padding: const EdgeInsets.fromLTRB(20, 20, 20, 40),
child: SingleChildScrollView(
controller: widget.scrollController,
child: Form(
key: validator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 5,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10),
),
),
),
Center(
child: Column(
children: [
MyText.titleLarge(
'Adventure starts here 🚀',
fontWeight: 600,
color: Colors.black87,
),
const SizedBox(height: 4),
MyText.bodySmall(
"Make your app management easy and fun!",
color: Colors.grey,
),
],
),
),
const SizedBox(height: 20),
_sectionHeader('Organization Info'),
_buildTextField('organizationName', 'Organization Name'),
_buildTextField('email', 'Email',
keyboardType: TextInputType.emailAddress),
_buildTextField('about', 'About Organization'),
_sectionHeader('Contact Details'),
_buildTextField('contactPerson', 'Contact Person'),
_buildTextField('contactNumber', 'Contact Number',
keyboardType: TextInputType.phone),
_buildTextField('address', 'Current Address'),
_sectionHeader('Additional Details'),
_buildPopupMenuField(
'Organization Size',
_sizes,
_selectedSize,
(val) => setState(() => _selectedSize = val),
'Please select organization size',
),
_buildPopupMenuField(
'Industry',
_industries.map((e) => e['name'] as String).toList(),
_selectedIndustryId != null
? _industries.firstWhere(
(e) => e['id'] == _selectedIndustryId)['name']
: null,
(val) {
setState(() {
final selectedIndustry = _industries.firstWhere(
(element) => element['name'] == val,
orElse: () => {},
);
_selectedIndustryId = selectedIndustry['id'];
});
},
'Please select industry',
),
const SizedBox(height: 12),
Row(
children: [
Checkbox(
value: _agreed,
onChanged: (val) => setState(() => _agreed = val ?? false),
fillColor: MaterialStateProperty.resolveWith((states) =>
states.contains(MaterialState.selected)
? contentTheme.brandRed
: Colors.white),
checkColor: Colors.white,
side: const BorderSide(color: Colors.red, width: 2),
),
Row(
children: [
MyText(
'I agree to the ',
color: Colors.black87,
),
MyText(
'privacy policy & terms',
color: contentTheme.brandRed,
fontWeight: 600,
),
],
)
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back, color: Colors.red),
label: MyText.bodyMedium("Back", color: Colors.red),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.red),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 14),
),
),
ElevatedButton.icon(
onPressed: _loading ? null : _submitForm,
icon: _loading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.check_circle_outline,
color: Colors.white),
label: _loading
? const SizedBox.shrink()
: MyText.bodyMedium("Submit", color: Colors.white),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 28, vertical: 14),
),
),
],
),
const SizedBox(height: 8),
Center(
child: TextButton.icon(
onPressed: () => Navigator.pop(context),
icon:
const Icon(Icons.arrow_back, size: 18, color: Colors.red),
label: MyText.bodySmall(
'Back to log in',
fontWeight: 600,
color: contentTheme.brandRed,
),
),
),
],
),
),
),
);
}
Widget _sectionHeader(String title) {
return Padding(
padding: const EdgeInsets.only(top: 20, bottom: 8),
child: MyText.titleSmall(
title,
fontWeight: 600,
color: Colors.grey[800],
),
);
}
Widget _buildTextField(String fieldName, String label,
{TextInputType keyboardType = TextInputType.text}) {
final controller = validator.getController(fieldName);
final defaultValidator = validator.getValidation<String>(fieldName);
String? Function(String?)? validatorFunc = defaultValidator;
if (fieldName == 'contactNumber') {
validatorFunc = (value) {
if (value == null || value.isEmpty) return 'Contact number is required';
if (!RegExp(r'^\d{10}$').hasMatch(value))
return 'Enter a valid 10-digit contact number';
return null;
};
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
validator: validatorFunc,
style: const TextStyle(fontSize: 15, color: Colors.black87),
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(color: Colors.grey[700], fontSize: 14),
filled: true,
fillColor: Colors.grey[100],
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[400]!),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: contentTheme.brandRed, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.red),
),
),
),
);
}
Widget _buildPopupMenuField(
String label,
List<String> items,
String? selectedValue,
ValueChanged<String?> onSelected,
String errorText,
) {
final bool hasError = selectedValue == null;
final GlobalKey _key = GlobalKey();
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: FormField<String>(
validator: (value) => hasError ? errorText : null,
builder: (fieldState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodySmall(label, color: Colors.grey[700]),
const SizedBox(height: 6),
GestureDetector(
key: _key,
onTap: () async {
final renderBox =
_key.currentContext!.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
final selected = await showMenu<String>(
context: fieldState.context,
position: RelativeRect.fromLTRB(
offset.dx,
offset.dy + size.height,
offset.dx + size.width,
offset.dy,
),
items: items
.map((item) => PopupMenuItem<String>(
value: item,
child: MyText.bodyMedium(item),
))
.toList(),
);
if (selected != null) {
onSelected(selected);
fieldState.didChange(selected);
}
},
child: InputDecorator(
decoration: InputDecoration(
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[400]!),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
BorderSide(color: contentTheme.brandRed, width: 1.5),
),
errorText: fieldState.errorText,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyMedium(
selectedValue ?? 'Select $label',
color:
selectedValue == null ? Colors.grey : Colors.black,
),
const Icon(Icons.arrow_drop_down, color: Colors.grey),
],
),
),
),
],
);
},
),
);
}
void _submitForm() async {
bool isValid = validator.validateForm();
if (_selectedSize == null || _selectedIndustryId == null) {
isValid = false;
setState(() {});
}
if (!_agreed) {
isValid = false;
showAppSnackbar(
title: "Agreement Required",
message: "Please agree to the privacy policy & terms",
type: SnackbarType.warning,
);
}
if (!isValid) return;
setState(() => _loading = true);
final formData = validator.getData();
final requestBody = {
'organizatioinName': formData['organizationName'],
'email': formData['email'],
'about': formData['about'],
'contactNumber': formData['contactNumber'],
'contactPerson': formData['contactPerson'],
'industryId': _selectedIndustryId ?? '',
'oragnizationSize': _sizeApiMap[_selectedSize] ?? '',
'terms': _agreed,
'address': formData['address'],
};
final error = await AuthService.requestDemo(requestBody);
setState(() => _loading = false);
if (error == null) {
showAppSnackbar(
title: "Success",
message: "Demo request submitted successfully!",
type: SnackbarType.success,
);
Navigator.pop(context);
} else {
showAppSnackbar(
title: "Error",
message: error['error'] ?? 'Unknown error occurred',
type: SnackbarType.error,
);
}
}
}