1339 lines
67 KiB
Dart
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,
|
|
);
|
|
}
|
|
}
|