bookmywages/lib/view/user_main_screens/booking_screen.dart
2025-10-16 11:21:52 +05:30

1339 lines
67 KiB
Dart

import 'dart:convert';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/model/detail_page_model.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/view/auth/auth_repository.dart';
import 'package:bookmywages/view/user_main_screens/main_contoller.dart';
import 'package:bookmywages/view/user_main_screens/sucessfull_screen.dart';
import 'package:bookmywages/viewmodel/consts_api.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dotted_line/dotted_line.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';
class BookingScreen extends StatefulWidget {
final DetailPageModel service;
const BookingScreen({super.key, required this.service});
@override
State<BookingScreen> createState() => _BookingScreenState();
}
class _BookingScreenState extends State<BookingScreen> {
final _formKey = GlobalKey<FormState>();
int rating = 0;
final TextEditingController _nameController = TextEditingController();
final TextEditingController _mobileController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _addressController = TextEditingController();
final TextEditingController _messageController = TextEditingController();
// Controllers for date and time
final TextEditingController _dateController = TextEditingController();
final TextEditingController _timeController = TextEditingController();
// Variables to store selected date and time
DateTime? _selectedDate;
TimeOfDay? _selectedTime;
// ScrollController to manage scrolling when keyboard appears
final ScrollController _scrollController = ScrollController();
// FocusNodes for each field - SOLUTION 1: Use FocusNodes
final FocusNode _nameFocusNode = FocusNode();
final FocusNode _mobileFocusNode = FocusNode();
final FocusNode _emailFocusNode = FocusNode();
final FocusNode _addressFocusNode = FocusNode();
final FocusNode _messageFocusNode = FocusNode();
// SOLUTION 2: Debounce validation to prevent frequent rebuilds
bool _isValidating = false;
@override
void initState() {
super.initState();
// SOLUTION 3: Remove listeners that cause frequent setState calls
// Only add listeners if absolutely necessary
_nameController.addListener(_onTextChangedDebounced);
_mobileController.addListener(_onTextChangedDebounced);
_emailController.addListener(_onTextChangedDebounced);
_addressController.addListener(_onTextChangedDebounced);
_messageController.addListener(_onTextChangedDebounced);
_dateController.addListener(_onTextChangedDebounced);
_timeController.addListener(_onTextChangedDebounced);
}
// SOLUTION 4: Debounced validation to prevent frequent rebuilds
void _onTextChangedDebounced() {
if (_isValidating) return;
_isValidating = true;
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted && _formKey.currentState != null) {
// Only validate if form has been submitted before
// This prevents validation during typing
_isValidating = false;
}
});
}
@override
void dispose() {
// Remove listeners before disposing controllers
_nameController.removeListener(_onTextChangedDebounced);
_mobileController.removeListener(_onTextChangedDebounced);
_emailController.removeListener(_onTextChangedDebounced);
_addressController.removeListener(_onTextChangedDebounced);
_messageController.removeListener(_onTextChangedDebounced);
_dateController.removeListener(_onTextChangedDebounced);
_timeController.removeListener(_onTextChangedDebounced);
// Dispose FocusNodes
_nameFocusNode.dispose();
_mobileFocusNode.dispose();
_emailFocusNode.dispose();
_addressFocusNode.dispose();
_messageFocusNode.dispose();
_nameController.dispose();
_mobileController.dispose();
_emailController.dispose();
_addressController.dispose();
_messageController.dispose();
_dateController.dispose();
_timeController.dispose();
_scrollController.dispose();
super.dispose();
}
// Function to open date picker
Future<void> _selectDate(BuildContext context) async {
final DateTime now = DateTime.now();
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedDate ?? now,
firstDate: now,
lastDate: DateTime(now.year + 1, now.month, now.day),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
primary: Colors.blue,
onPrimary: Colors.white,
onSurface: Colors.black,
),
),
child: child!,
);
},
);
if (picked != null && picked != _selectedDate) {
setState(() {
_selectedDate = picked;
_dateController.text = DateFormat('dd-MM-yyyy').format(picked);
});
}
}
// Function to open time picker
Future<void> _selectTime(BuildContext context) async {
final TimeOfDay now = TimeOfDay.now();
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: _selectedTime ?? now,
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
primary: Colors.blue,
onPrimary: Colors.white,
onSurface: Colors.black,
),
),
child: child!,
);
},
);
if (picked != null && picked != _selectedTime) {
setState(() {
_selectedTime = picked;
final hour = picked.hourOfPeriod == 0 ? 12 : picked.hourOfPeriod;
final period = picked.period == DayPeriod.am ? 'AM' : 'PM';
_timeController.text =
'${hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')} $period';
});
}
}
@override
Widget build(BuildContext context) {
final service = widget.service;
final screenWidth = MediaQuery.of(context).size.width;
final indexController = InheritedIndexController.of(context);
return Scaffold(
backgroundColor: AppColors.secondprimary,
resizeToAvoidBottomInset: false,
body: Stack(
children: [
// Top app bar
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 10.0, left: 16, right: 16),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () => Navigator.of(context).pop(),
),
Expanded(
child: Center(
child: Text(
'Booking',
style: Theme.of(context).textTheme.titleLarge,
),
),
),
const SizedBox(width: 48),
],
),
),
),
),
// Main scrollable content
Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 56,
),
child: ListView(
controller: _scrollController,
physics: const ClampingScrollPhysics(),
children: [
// Image + Overlay
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: const EdgeInsets.all(0.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(30),
child: service.images1.isNotEmpty
? CachedNetworkImage(
imageUrl: service.images1.first,
width: double.infinity,
height: 300,
fit: BoxFit.cover,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) =>
const Icon(Icons.error),
)
: Container(
width: double.infinity,
height: 300,
color: Colors.grey[300],
child: const Center(
child: Icon(
Icons.image_not_supported,
size: 50,
),
),
),
),
),
Positioned(
bottom: -130,
left: 16,
right: 16,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFCFCFC),
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: const Color(0xFFC9C9C9),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
service.vendorName,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
letterSpacing: 0.2,
color: Colors.black,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 5),
Text(
service.servicename,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w600,
fontSize: 16,
letterSpacing: 0.18,
color: Color(0xFF5A5A5A),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 15),
const DottedLine(
dashColor: Color(0xFFBABABA),
lineThickness: 1,
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"Payment:",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
),
),
const SizedBox(height: 15),
Text(
'Rs ${service.amount}',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 14,
color: Color(0xFF636363),
),
),
],
),
),
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"Duration:",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
),
),
const SizedBox(height: 15),
Text(
service.workingduration,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 14,
color: Color(0xFF636363),
),
),
],
),
),
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"Rating:",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
),
),
const SizedBox(height: 15),
Row(
children: [
const Icon(
Icons.star,
size: 16,
color: Colors.amber,
),
const SizedBox(width: 4),
],
),
],
),
),
],
),
),
],
),
),
),
],
),
),
const SizedBox(height: 140),
// Form
Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
// SOLUTION 5: Change autovalidate mode to only validate after user interaction
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFieldTitle("Name"),
const SizedBox(height: 8),
buildTextFormField(
controller: _nameController,
focusNode: _nameFocusNode, // Add FocusNode
hintText: "Enter your name",
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
return null;
},
onTap: () {
Future.delayed(
const Duration(milliseconds: 300),
() {
_scrollToField(0);
},
);
},
),
const SizedBox(height: 16),
buildFieldTitle("Mobile Number"),
const SizedBox(height: 8),
buildTextFormField(
controller: _mobileController,
focusNode: _mobileFocusNode, // Add FocusNode
hintText: "Enter your mobile number",
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your mobile number';
}
if (!RegExp(r'^[0-9]{10}$').hasMatch(value)) {
return 'Please enter a valid 10-digit mobile number';
}
return null;
},
onTap: () {
Future.delayed(
const Duration(milliseconds: 300),
() {
_scrollToField(1);
},
);
},
),
const SizedBox(height: 16),
buildFieldTitle("Email ID"),
const SizedBox(height: 8),
buildTextFormField(
controller: _emailController,
focusNode: _emailFocusNode, // Add FocusNode
hintText: "Enter your email address",
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email address';
}
if (!RegExp(
r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$',
).hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
},
onTap: () {
Future.delayed(
const Duration(milliseconds: 300),
() {
_scrollToField(2);
},
);
},
),
const SizedBox(height: 16),
// Date and Time Row
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFieldTitle("Date"),
const SizedBox(height: 8),
TextFormField(
controller: _dateController,
readOnly: true,
decoration: InputDecoration(
hintText: "Select Date",
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Color(0xFFB7B7B7),
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Color(0xFFB7B7B7),
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Color(0xFFB7B7B7),
width: 1,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Colors.red,
width: 1,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Colors.red,
width: 1,
),
),
suffixIcon: IconButton(
icon: const Icon(
Icons.calendar_today,
color: Colors.blue,
),
onPressed: () => _selectDate(context),
),
),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontSize: 14,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please select a date';
}
return null;
},
onTap: () => _selectDate(context),
),
],
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFieldTitle("Time"),
const SizedBox(height: 8),
TextFormField(
controller: _timeController,
readOnly: true,
decoration: InputDecoration(
hintText: "Select Time",
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Color(0xFFB7B7B7),
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Color(0xFFB7B7B7),
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Color(0xFFB7B7B7),
width: 1,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Colors.red,
width: 1,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Colors.red,
width: 1,
),
),
suffixIcon: IconButton(
icon: const Icon(
Icons.access_time,
color: Colors.blue,
),
onPressed: () => _selectTime(context),
),
),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontSize: 14,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please select a time';
}
return null;
},
onTap: () => _selectTime(context),
),
],
),
),
],
),
const SizedBox(height: 16),
buildFieldTitle("Address"),
const SizedBox(height: 8),
buildTextFormField(
controller: _addressController,
focusNode: _addressFocusNode, // Add FocusNode
hintText: "Enter your address",
maxLines: 1,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your address';
}
return null;
},
onTap: () {
Future.delayed(
const Duration(milliseconds: 300),
() {
_scrollToField(3);
},
);
},
),
const SizedBox(height: 16),
buildFieldTitle("Message (Optional)"),
const SizedBox(height: 8),
// SOLUTION 6: Special handling for message field
buildTextFormField(
controller: _messageController,
focusNode: _messageFocusNode, // Add FocusNode
hintText: "Any special instructions?",
maxLines: 3,
onTap: () {
// SOLUTION 7: Ensure focus is properly managed
_messageFocusNode.requestFocus();
Future.delayed(
const Duration(milliseconds: 300),
() {
_scrollToField(4);
},
);
},
),
const SizedBox(height: 24),
SizedBox(
width: screenWidth - 32,
height: 52,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
onPressed: () async {
// Hide keyboard
FocusScope.of(context).unfocus();
if (_formKey.currentState!.validate()) {
try {
final prefs =
await SharedPreferences.getInstance();
final userId =
prefs.getString('userId') ??
prefs.getString('user_id') ??
'';
if (userId.isEmpty) {
Fluttertoast.showToast(
msg:
'User session expired. Please login again.',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
return;
}
if (_selectedDate == null) {
Fluttertoast.showToast(
msg: 'Please select a date',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.orange,
textColor: Colors.white,
);
return;
}
if (_selectedTime == null) {
Fluttertoast.showToast(
msg: 'Please select a time',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.orange,
textColor: Colors.white,
);
return;
}
final formattedDate = DateFormat(
'yyyy-MM-dd',
).format(_selectedDate!);
final requestBody = {
'user_id': userId,
'name': _nameController.text.trim(),
'mobile_number': _mobileController.text
.trim(),
'email': _emailController.text.trim(),
'message': _messageController.text.trim(),
'address': _addressController.text.trim(),
'service_date': formattedDate,
'service_time': _timeController.text.trim(),
'service_id': service.id.toString(),
};
print('API URL: ${ConstsApi.bookservice}');
print(
'Request Body: ${jsonEncode(requestBody)}',
);
if (!ConstsApi.bookservice.startsWith(
'http',
)) {
print(
'ERROR: Invalid API URL - must start with http/https',
);
if (context.mounted) {
Fluttertoast.showToast(
msg:
'Invalid API configuration. Please contact support.',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
return;
}
print('Testing API connectivity...');
final isConnected =
await _testApiConnectivity();
if (!isConnected) {
if (context.mounted) {
Fluttertoast.showToast(
msg:
'Cannot reach server. Please check your internet connection.',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.orange,
textColor: Colors.white,
);
}
return;
}
final response = await http
.post(
Uri.parse(ConstsApi.bookservice),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode(requestBody),
)
.timeout(const Duration(seconds: 30));
print(
'Response Status: ${response.statusCode}',
);
print('Response Body: ${response.body}');
print(
'Response Headers: ${response.headers}',
);
final status = response.statusCode;
if (response.headers['content-type']
?.contains('text/html') ==
true ||
response.body.trim().startsWith(
'<!DOCTYPE html>',
) ||
response.body.trim().startsWith(
'<html',
)) {
print(
'API returned HTML instead of JSON. Possible issues:',
);
print(
'1. Wrong API endpoint URL: ${ConstsApi.bookservice}',
);
print('2. Server error (404, 500, etc.)');
print('3. Authentication required');
if (context.mounted) {
Fluttertoast.showToast(
msg:
'API endpoint error. Please check server configuration.',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
return;
}
Map<String, dynamic> jsonResponse;
try {
jsonResponse = jsonDecode(response.body);
} catch (e) {
print('JSON Parse Error: $e');
print('Raw Response: ${response.body}');
if (context.mounted) {
Fluttertoast.showToast(
msg:
'Invalid response from server. Please try again.',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
return;
}
if (status == 200) {
// Clear fields
_nameController.clear();
_mobileController.clear();
_emailController.clear();
_messageController.clear();
_addressController.clear();
_dateController.clear();
_timeController.clear();
setState(() {
_selectedDate = null;
_selectedTime = null;
});
if (context.mounted) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const SucessfullScreen(),
),
);
int rating = 0;
await showDialog(
context: context,
barrierColor: Colors.black.withOpacity(
0.5,
),
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(40),
),
child: StatefulBuilder(
builder: (context, setState) {
return Container(
width: double.infinity,
height: 280,
padding: const EdgeInsets.all(
16,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(
40,
),
),
child: Padding(
padding:
const EdgeInsets.symmetric(
vertical: 32,
),
child: Column(
mainAxisSize:
MainAxisSize.min,
children: [
const Text(
'Rate this service',
style: TextStyle(
fontSize: 20,
fontWeight:
FontWeight.bold,
),
),
const SizedBox(
height: 29,
),
RatingBar(
initialRating: 0,
minRating: 0.5,
direction:
Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
ratingWidget: RatingWidget(
full: Icon(
Icons.star,
color: AppColors
.primary,
),
half: Icon(
Icons.star_half,
color: AppColors
.primary,
),
empty: Icon(
Icons
.star_outline,
color:
Colors.black,
),
),
itemPadding:
const EdgeInsets.symmetric(
horizontal: 4.0,
),
onRatingUpdate:
(newRating) {
setState(() {
rating =
newRating
.round();
});
},
),
const SizedBox(
height: 20,
),
Align(
alignment:
Alignment.center,
child: SizedBox(
width: 207,
height: 47,
child: ElevatedButton(
onPressed: () async {
if (rating ==
0) {
Fluttertoast.showToast(
msg:
'Please provide a rating',
toastLength:
Toast
.LENGTH_SHORT,
gravity:
ToastGravity
.BOTTOM,
backgroundColor:
Colors
.orange,
textColor:
Colors
.white,
);
return;
}
try {
final repository =
updatereviewRepository();
await repository.updatereviews(
context:
context,
url: ConstsApi
.updatereview,
review: rating
.toString(),
serviceId:
service
.id
.toString(),
);
Navigator.of(
context,
).pop();
Fluttertoast.showToast(
msg:
'Thank you for your rating!',
toastLength:
Toast
.LENGTH_SHORT,
gravity:
ToastGravity
.BOTTOM,
backgroundColor:
Colors
.green,
textColor:
Colors
.white,
);
indexController
?.changeIndex(
3,
);
Get.offAllNamed(
RouterConts
.history,
arguments: {
'historyTab':
0,
},
);
} catch (e) {
Fluttertoast.showToast(
msg:
'Failed to submit rating: $e',
toastLength:
Toast
.LENGTH_LONG,
gravity:
ToastGravity
.BOTTOM,
backgroundColor:
Colors
.red,
textColor:
Colors
.white,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor:
Color(
0xFF0066FF,
),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(
20.83,
),
),
),
child: const Text(
"Submit",
textAlign:
TextAlign
.center,
style: TextStyle(
fontFamily:
'Gilroy-Bold',
fontWeight:
FontWeight
.w700,
fontSize: 20,
color: Colors
.white,
),
),
),
),
),
],
),
),
);
},
),
);
},
);
}
} else if (status == 404) {
if (context.mounted) {
String message =
'Maximum bookings reached';
if (jsonResponse['data'] != null) {
message = jsonResponse['data']
.toString();
}
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.orange,
textColor: Colors.white,
);
Get.offAllNamed(
RouterConts.packageList,
arguments: 1,
);
}
} else {
if (context.mounted) {
String message =
'Booking failed. Please try again.';
if (jsonResponse['message'] != null) {
message = jsonResponse['message']
.toString();
}
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
} catch (e) {
print('Network/API Error: $e');
print('Error Type: ${e.runtimeType}');
String errorMessage =
'Connection failed. Please check:';
if (e.toString().contains(
'SocketException',
) ||
e.toString().contains(
'NetworkException',
)) {
errorMessage =
'No internet connection. Please check your network.';
} else if (e.toString().contains(
'TimeoutException',
)) {
errorMessage =
'Request timeout. Please try again.';
} else if (e.toString().contains(
'FormatException',
)) {
errorMessage =
'Server returned invalid response. Please contact support.';
} else {
errorMessage =
'Booking failed: ${e.toString()}';
}
if (context.mounted) {
Fluttertoast.showToast(
msg: errorMessage,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
}
},
child: const Text(
"Book Now",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 18,
color: Colors.white,
),
),
),
),
const SizedBox(height: 30),
],
),
),
),
],
),
),
],
),
);
}
Future<bool> _testApiConnectivity() async {
try {
final uri = Uri.parse(ConstsApi.bookservice);
final baseUrl = '${uri.scheme}://${uri.host}';
print('Testing connectivity to: $baseUrl');
final response = await http
.get(
Uri.parse(baseUrl),
headers: {'Content-Type': 'application/json'},
)
.timeout(const Duration(seconds: 10));
print('Connectivity test - Status: ${response.statusCode}');
return response.statusCode < 500;
} catch (e) {
print('Connectivity test failed: $e');
return false;
}
}
void _scrollToField(int fieldIndex) {
final offset = 300.0 + (fieldIndex * 150.0);
if (_scrollController.hasClients) {
_scrollController.animateTo(
offset,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
Widget buildFieldTitle(String title) {
return Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16,
),
);
}
Widget buildTextFormField({
required TextEditingController controller,
FocusNode? focusNode, // Add FocusNode parameter
required String hintText,
TextInputType? keyboardType,
int maxLines = 1,
Function()? onTap,
String? Function(String?)? validator,
}) {
return TextFormField(
controller: controller,
focusNode: focusNode, // Use FocusNode
keyboardType: keyboardType,
maxLines: maxLines,
onTap: onTap,
// SOLUTION 8: Remove onChanged that causes frequent rebuilds
// Only validate on form submission, not on every character change
decoration: InputDecoration(
hintText: hintText,
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFFB7B7B7), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFFB7B7B7), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFFB7B7B7), width: 1),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.red, width: 1),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.red, width: 1),
),
),
style: const TextStyle(fontFamily: 'Gilroy-Medium', fontSize: 14),
validator: validator,
);
}
}