import 'package:bookmywages/consts_widgets/app_assets.dart'; import 'package:bookmywages/consts_widgets/app_colors.dart'; import 'package:bookmywages/routers/consts_router.dart'; import 'package:bookmywages/view/user_main_screens/image_page.dart'; import 'package:bookmywages/viewmodel/api_controller.dart'; import 'package:bookmywages/viewmodel/consts_api.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dotted_line/dotted_line.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; import 'package:youtube_player_flutter/youtube_player_flutter.dart'; import 'package:video_player/video_player.dart'; class DetailServicePage extends ConsumerStatefulWidget { final int id; const DetailServicePage({super.key, required this.id}); @override ConsumerState createState() => _DetailServicePageState(); } class _DetailServicePageState extends ConsumerState with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { int selectedButton = 1; final ScrollController _scrollController = ScrollController(); final ScrollController localScrollController = ScrollController(); int _currentServiceId = 0; int selectedIndex = 1; // Store current detail for button access dynamic currentDetail; // Video related variables final List _controllers = []; final List _youtubeControllers = []; bool _isDisposed = false; bool _controllersInitialized = false; // Form related variables final _formKey = GlobalKey(); AutovalidateMode _autoValidateMode = AutovalidateMode.disabled; final nameController = TextEditingController(); final phoneController = TextEditingController(); final emailController = TextEditingController(); final messageController = TextEditingController(); @override bool get wantKeepAlive => true; @override void initState() { super.initState(); _currentServiceId = widget.id; WidgetsBinding.instance.addObserver(this); } @override void dispose() { _isDisposed = true; WidgetsBinding.instance.removeObserver(this); // Dispose controllers safely _disposeControllers(); // Dispose scroll controllers _scrollController.dispose(); localScrollController.dispose(); // Dispose text controllers nameController.dispose(); phoneController.dispose(); emailController.dispose(); messageController.dispose(); super.dispose(); } // Required method for AutomaticKeepAliveClientMixin @override void didChangeMetrics() { // Handle device metrics changes (screen rotation, keyboard, etc.) if (mounted) { debugPrint('Device metrics changed'); } } // Handle app lifecycle changes @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); switch (state) { case AppLifecycleState.paused: _pauseAllVideos(); break; case AppLifecycleState.resumed: break; case AppLifecycleState.inactive: break; case AppLifecycleState.detached: break; case AppLifecycleState.hidden: break; } } void _pauseAllVideos() { try { for (var controller in _controllers) { if (controller.value.isInitialized && controller.value.isPlaying) { controller.pause(); } } } catch (e) { debugPrint('Error pausing videos: $e'); } } void _disposeControllers() { // Dispose video controllers for (var controller in _controllers) { try { if (!controller.value.isInitialized) continue; controller.pause(); controller.dispose(); } catch (e) { debugPrint('Error disposing video controller: $e'); } } _controllers.clear(); // Dispose YouTube controllers for (var controller in _youtubeControllers) { try { controller.dispose(); } catch (e) { debugPrint('Error disposing YouTube controller: $e'); } } _youtubeControllers.clear(); _controllersInitialized = false; } // Method to check if URL is YouTube link bool isYoutubeLink(String url) { return url.contains('youtube.com') || url.contains('youtu.be') || url.contains('m.youtube.com'); } // Method to check if URL is direct video bool isDirectVideo(String url) { return url.contains('.mp4') || url.contains('.mov') || url.contains('.avi') || url.contains('.mkv') || url.contains('.webm') || url.contains('.3gp') || url.contains('.flv'); } // Initialize video controllers with better error handling void _initializeVideoControllers(List videoUrls) { if (_isDisposed || _controllersInitialized) return; // Dispose existing controllers first _disposeControllers(); try { for (String url in videoUrls) { if (_isDisposed) break; if (isDirectVideo(url)) { final controller = VideoPlayerController.network(url); controller .initialize() .then((_) { if (!_isDisposed && mounted) { setState(() {}); } }) .catchError((error) { debugPrint('Error initializing video controller: $error'); }); _controllers.add(controller); } else if (isYoutubeLink(url)) { final videoId = YoutubePlayer.convertUrlToId(url); if (videoId != null && videoId.isNotEmpty) { try { final youtubeController = YoutubePlayerController( initialVideoId: videoId, flags: const YoutubePlayerFlags( autoPlay: false, mute: false, controlsVisibleAtStart: true, ), ); _youtubeControllers.add(youtubeController); } catch (e) { debugPrint('Error creating YouTube controller: $e'); } } } } _controllersInitialized = true; } catch (e) { debugPrint('Error in _initializeVideoControllers: $e'); } } // Method to update service and smooth scroll to top void _updateService(dynamic item) { if (_isDisposed) return; setState(() { _currentServiceId = item.id ?? widget.id; }); // Dispose old controllers before loading new service _disposeControllers(); // Smooth scroll to top to show the new service details if (_scrollController.hasClients) { _scrollController.animateTo( 0.0, duration: const Duration(milliseconds: 800), curve: Curves.easeInOut, ); } } void _onFieldChanged() { if (_autoValidateMode == AutovalidateMode.onUserInteraction && mounted) { setState(() {}); // Triggers error revalidation and hiding } } @override Widget build(BuildContext context) { super.build(context); return Scaffold( backgroundColor: AppColors.secondprimary, body: SafeArea( child: Column( children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 12.0, vertical: 5.0, ), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back_ios), onPressed: () => Navigator.of(context).pop(), ), const SizedBox(width: 8), const Text( "Service Details", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), ), ], ), ), Expanded( child: Consumer( builder: (context, ref, _) { final detailAsyncValue = ref.watch( detailpageProvider(_currentServiceId.toString()), ); return detailAsyncValue.when( data: (detailList) { if (detailList.isEmpty) { return const Center(child: Text("No details found.")); } final detail = detailList.first; // Store current detail for button access WidgetsBinding.instance.addPostFrameCallback((_) { if (!_isDisposed) { currentDetail = detail; } }); final imageUrls = detail.images1 ?? []; final videoUrls = detail.videos ?? []; final categoryId = detail.category.toString() ?? '0'; // Initialize video controllers only once per service if (!_controllersInitialized && videoUrls.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!_isDisposed) { _initializeVideoControllers(videoUrls); } }); } final mostPopularAsyncValue = ref.watch( mostPopularProvider(categoryId), ); return SingleChildScrollView( controller: _scrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Animated container for smooth transition AnimatedSwitcher( duration: const Duration(milliseconds: 500), child: Stack( key: ValueKey(_currentServiceId), clipBehavior: Clip.none, children: [ Padding( padding: const EdgeInsets.all(16.0), child: ClipRRect( borderRadius: BorderRadius.circular(30), child: imageUrls.isNotEmpty ? CachedNetworkImage( imageUrl: imageUrls[0], width: double.infinity, height: 250, fit: BoxFit.cover, placeholder: (context, url) => Container( width: double.infinity, height: 250, color: Colors.grey[300], child: const Center( child: Icon( Icons.image, size: 50, color: Colors.grey, ), ), ), errorWidget: ( context, url, error, ) => Container( width: double.infinity, height: 250, color: Colors.grey[300], child: const Icon( Icons.image_not_supported, size: 50, color: Colors.grey, ), ), ) : Container( width: double.infinity, height: 250, color: Colors.grey[300], child: const Icon( Icons.image_not_supported, size: 50, color: Colors.grey, ), ), ), ), Positioned( bottom: -100, left: 25, right: 25, child: AnimatedContainer( duration: const Duration( milliseconds: 300, ), 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( detail.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( detail.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 ${detail.amount ?? 0}', 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( detail.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, ), Text( (((double.tryParse( detail.averageReview ?? '0', ) ?? 0.0) * 2) .floor() / 2) .toStringAsFixed( 1, ), style: const TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight .w500, fontSize: 14, color: Color( 0xFF636363, ), ), ), ], ), ], ), ), ], ), ), ], ), ), ), ], ), ), const SizedBox(height: 120), Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, ), child: Container( width: double.infinity, height: 93, decoration: BoxDecoration( color: const Color(0xFFFAFCFF), borderRadius: BorderRadius.circular(22), border: Border.all( color: const Color(0xFFF1F1F1), width: 1, ), ), child: Row( children: [ const SizedBox(width: 12), ClipOval( child: CachedNetworkImage( imageUrl: detail.profilePic?.toString() ?? '', placeholder: (context, url) => Container( width: 67, height: 67, decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.grey, ), child: const Icon( Icons.person, color: Colors.white, ), ), errorWidget: (context, url, error) => Container( width: 67, height: 67, decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.grey, ), child: const Icon( Icons.person, color: Colors.white, ), ), width: 55, height: 55, fit: BoxFit.cover, ), ), const SizedBox(width: 12), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( detail.vendorname ?? '', style: const TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight.w500, fontSize: 20, color: Color(0xFF353434), ), ), const SizedBox(height: 4), Text( detail.categoryName ?? '', style: const TextStyle( fontFamily: 'Gilroy-Regular', fontWeight: FontWeight.w400, fontSize: 15, color: Color(0xFF717171), ), ), ], ), ), Row( children: [ const Icon( Icons.star, color: Colors.orange, size: 18, ), const SizedBox(width: 4), Text( (((double.tryParse( detail.averageReview ?? '0', ) ?? 0.0) * 2) .floor() / 2) .toStringAsFixed(1), style: const TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight.w500, fontSize: 14, color: Color(0xFF636363), ), ), const SizedBox(width: 12), ], ), ], ), ), ), const SizedBox(height: 10), const SizedBox(height: 10), // Two Buttons (Description / Review Switch) Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, ), child: Container( width: double.infinity, height: 75, decoration: BoxDecoration( color: const Color(0xFFFAFCFF), borderRadius: BorderRadius.circular(22), border: Border.all( color: const Color(0xFFF1F1F1), width: 1, ), ), child: Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Description Button GestureDetector( onTap: () { if (mounted) { setState(() { selectedButton = 1; }); } }, child: Container( width: 130, height: 48, decoration: BoxDecoration( color: selectedButton == 1 ? AppColors.primary : Colors.white, borderRadius: BorderRadius.circular( 10, ), border: Border.all( color: selectedButton == 1 ? AppColors.primary : const Color(0xFFCFCFCF), width: 1, ), ), child: Center( child: Text( 'Description', style: TextStyle( color: selectedButton == 1 ? Colors.white : Colors.black, fontFamily: 'Gilroy-Bold', fontWeight: FontWeight.w400, fontSize: 15, height: 13.17 / 18, letterSpacing: 0.01 * 18, ), ), ), ), ), const SizedBox(width: 16), // Review Button GestureDetector( onTap: () { if (mounted) { setState(() { selectedButton = 2; }); ref.refresh( getreviewuserProvider( detail.id.toString(), ), ); } }, child: Container( width: 130, height: 48, decoration: BoxDecoration( color: selectedButton == 2 ? AppColors.primary : Colors.white, borderRadius: BorderRadius.circular( 10, ), border: Border.all( color: selectedButton == 2 ? AppColors.primary : const Color(0xFFCFCFCF), width: 1, ), ), child: Center( child: Text( 'Review', style: TextStyle( color: selectedButton == 2 ? Colors.white : Colors.black, fontFamily: 'Gilroy-Bold', fontWeight: FontWeight.w400, fontSize: 15, height: 13.17 / 18, letterSpacing: 0.01 * 18, ), ), ), ), ), ], ), ), ), ), const SizedBox(height: 20), // Switch between Description and Review Padding( padding: const EdgeInsets.symmetric( horizontal: 24.0, ), child: selectedButton == 1 ? Row( children: [ Expanded( child: Text.rich( TextSpan( children: [ const TextSpan( text: 'Description: ', style: TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight.w600, fontSize: 16, height: 28 / 16, letterSpacing: 0, ), ), TextSpan( text: detail.description ?? '', style: const TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight.w400, fontSize: 16, height: 28 / 16, letterSpacing: 0, ), ), ], ), ), ), ], ) : Consumer( builder: (context, ref, _) { final reviewsAsync = ref.watch( getreviewuserProvider( detail.id.toString(), ), ); return reviewsAsync.when( data: (reviews) { if (reviews.isEmpty) { return Center( child: Container( padding: const EdgeInsets.all( 16, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular( 22, ), border: Border.all( color: Colors.grey.shade300, width: 1, ), boxShadow: [ BoxShadow( color: Colors.black .withOpacity(0.25), offset: const Offset( 0, 1, ), blurRadius: 4.3, ), ], ), child: const Text( 'No reviews available for this service yet.', style: TextStyle( fontFamily: 'Gilroy-Medium', fontSize: 16, ), ), ), ); } return Container( width: double.infinity, height: 150, margin: const EdgeInsets.only( bottom: 16, ), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(22), border: Border.all( color: Colors.grey.shade300, width: 1, ), boxShadow: [ BoxShadow( color: Colors.black .withOpacity(0.25), offset: const Offset(0, 1), blurRadius: 4.3, ), ], ), child: Scrollbar( controller: localScrollController, thumbVisibility: true, thickness: 6.0, radius: const Radius.circular( 10, ), child: SingleChildScrollView( controller: localScrollController, child: Column( crossAxisAlignment: CrossAxisAlignment .start, children: reviews.map(( review, ) { return Padding( padding: const EdgeInsets.only( bottom: 8.0, ), child: Row( children: [ ClipOval( child: CachedNetworkImage( imageUrl: review .profilePic1 ?.toString() ?? '', placeholder: (context, url) => Container( width: 39, height: 39, decoration: const BoxDecoration( shape: BoxShape .circle, color: Colors .grey, ), child: const Icon( Icons .person, size: 20, color: Colors .white, ), ), errorWidget: ( context, url, error, ) => Container( width: 39, height: 39, decoration: const BoxDecoration( shape: BoxShape .circle, color: Colors .grey, ), child: const Icon( Icons .person, size: 20, color: Colors .white, ), ), width: 39, height: 39, fit: BoxFit .cover, ), ), const SizedBox( width: 8, ), Expanded( child: Text( review.userName ?? '', style: const TextStyle( fontFamily: 'Gilroy-Bold', fontWeight: FontWeight .w700, fontSize: 20, height: 13.17 / 20, letterSpacing: 0.2, ), ), ), Row( children: List.generate( int.tryParse( review.review ?? '0', ) ?? 0, ( index, ) => const Icon( Icons.star, color: Colors .amber, size: 20, ), ), ), ], ), ); }).toList(), ), ), ), ); }, loading: () => SizedBox( height: 150, child: const Center( child: Text('Loading reviews...'), ), ), error: (error, stack) => SizedBox( height: 150, child: Center( child: Text( 'Unable to load reviews', style: const TextStyle( color: Colors.red, ), ), ), ), ); }, ), ), Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.center, children: [ FloatingActionButton( onPressed: () { // TODO: Add phone call logic }, backgroundColor: Colors.green, // Optional shape: const CircleBorder(), // Ensures it's a perfect circle child: const Icon( Icons.call, color: Colors.white, ), ), Image.asset(AppAssets.map), Image.asset(AppAssets.share), ], ), ), // Images Section Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( "Image :", style: TextStyle( fontFamily: 'Gilroy-Bold', fontWeight: FontWeight.w700, fontSize: 20, height: 1.0, letterSpacing: 1.0, ), ), GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ImagePage(mediaUrls: imageUrls), ), ); }, child: const Text( "View all", style: TextStyle( fontFamily: 'Gilroy-Bold', fontWeight: FontWeight.w700, fontSize: 17, height: 1.0, letterSpacing: 1.0, ), ), ), ], ), ), // Images ListView SizedBox( height: 120, child: imageUrls.isEmpty ? const Center( child: Text("No images available"), ) : ListView.builder( scrollDirection: Axis.horizontal, itemCount: imageUrls.length, padding: const EdgeInsets.symmetric( horizontal: 16.0, ), itemBuilder: (context, index) { return Container( width: 120, margin: const EdgeInsets.only( right: 10, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular( 15, ), border: Border.all( color: Colors.grey.shade300, ), ), child: ClipRRect( borderRadius: BorderRadius.circular( 15, ), child: CachedNetworkImage( imageUrl: imageUrls[index], fit: BoxFit.cover, placeholder: (context, url) => Container( color: Colors.grey[200], child: const Center( child: Icon( Icons.image, color: Colors.grey, size: 30, ), ), ), errorWidget: (context, url, error) => Container( color: Colors.grey[200], child: const Center( child: Icon( Icons.error, color: Colors.red, size: 30, ), ), ), ), ), ); }, ), ), // Videos Section if (videoUrls.isNotEmpty) ...[ const Padding( padding: EdgeInsets.all(16.0), child: Text( "Videos :", style: TextStyle( fontFamily: 'Gilroy-Bold', fontWeight: FontWeight.w700, fontSize: 20, height: 1.0, letterSpacing: 1.0, ), ), ), SizedBox( height: 150, child: ListView.builder( key: ValueKey( 'video-list-$_currentServiceId', ), scrollDirection: Axis.horizontal, itemCount: videoUrls.length, padding: const EdgeInsets.symmetric( horizontal: 16, ), itemBuilder: (context, index) { final url = videoUrls[index]; if (isYoutubeLink(url)) { final videoId = YoutubePlayer.convertUrlToId(url) ?? ''; if (videoId.isEmpty) { return Container( width: 113.14, height: 101.83, margin: const EdgeInsets.only( right: 10, ), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular( 14.71, ), border: Border.all( color: Colors.grey.shade300, ), ), child: const Text( "Invalid YouTube URL", ), ); } // Use pre-created controller if available YoutubePlayerController? youtubeController; if (index < _youtubeControllers.length) { youtubeController = _youtubeControllers[index]; } else { youtubeController = YoutubePlayerController( initialVideoId: videoId, flags: const YoutubePlayerFlags( autoPlay: false, mute: false, ), ); } return Container( width: 113.14, height: 101.83, margin: const EdgeInsets.only( right: 10, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular( 14.71, ), border: Border.all( color: Colors.grey.shade300, ), ), child: ClipRRect( borderRadius: BorderRadius.circular( 14.71, ), child: YoutubePlayer( controller: youtubeController, showVideoProgressIndicator: true, ), ), ); } else if (isDirectVideo(url)) { // Find the matching controller for this URL VideoPlayerController? controller; if (index < _controllers.length) { controller = _controllers[index]; } return GestureDetector( onTap: () { if (controller != null && controller.value.isInitialized) { if (mounted) { setState(() { if (controller! .value .isPlaying) { controller.pause(); } else { for (var c in _controllers) { if (c != controller && c.value.isPlaying) { c.pause(); } } controller.play(); } }); } } }, child: Container( width: 113.14, height: 101.83, margin: const EdgeInsets.only( right: 10, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular( 14.71, ), border: Border.all( color: Colors.grey.shade300, ), ), child: ClipRRect( borderRadius: BorderRadius.circular( 14.71, ), child: Stack( alignment: Alignment.center, children: [ if (controller != null && controller .value .isInitialized) AspectRatio( aspectRatio: controller .value .aspectRatio, child: VideoPlayer( controller, ), ) else Container( color: Colors.grey[300], child: const Center( child: Text('Loading...'), ), ), Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.black .withOpacity(0.5), shape: BoxShape.circle, ), child: Icon( controller != null && controller .value .isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.white, ), ), ], ), ), ), ); } else { return Container( width: 113.14, height: 101.83, margin: const EdgeInsets.only( right: 10, ), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular( 14.71, ), border: Border.all( color: Colors.grey.shade300, ), ), child: const Text("Invalid video"), ); } }, ), ), ], // Relevant Services Section const Padding( padding: EdgeInsets.all(16.0), child: Text( "Relevant Service :", style: TextStyle( fontFamily: 'Gilroy-Bold', fontWeight: FontWeight.w700, fontSize: 20, height: 1.0, letterSpacing: 1.0, ), ), ), const SizedBox(height: 20), // Dynamic most popular services from API SizedBox( height: 210, child: mostPopularAsyncValue.when( data: (services) { // Filter out the current service from relevant services final filteredServices = services .where( (service) => service.id != _currentServiceId, ) .toList(); return filteredServices.isEmpty ? const Center( child: Text( 'No relevant services found', ), ) : ListView.builder( scrollDirection: Axis.horizontal, itemCount: filteredServices.length, padding: const EdgeInsets.symmetric( horizontal: 16.0, ), itemBuilder: (context, index) { final item = filteredServices[index]; bool isSelected = item.id == _currentServiceId; return AnimatedContainer( duration: const Duration( milliseconds: 300, ), width: 180, margin: const EdgeInsets.only( right: 16, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: isSelected ? AppColors.primary : AppColors.lightGrey, width: isSelected ? 2 : 1, ), boxShadow: isSelected ? [ BoxShadow( color: AppColors .primary .withOpacity(0.3), blurRadius: 8, offset: const Offset( 0, 4, ), ), ] : [], ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular( 12, ), ), child: (item.images1 != null && item .images1! .isNotEmpty) ? CachedNetworkImage( imageUrl: item .images1! .first, width: double.infinity, height: 100, fit: BoxFit.cover, placeholder: ( context, url, ) => Container( width: double .infinity, height: 100, color: Colors .grey[300], child: const Icon( Icons.image, color: Colors .grey, ), ), errorWidget: ( context, url, error, ) => Container( width: double .infinity, height: 100, color: Colors .grey[300], child: const Icon( Icons .image_not_supported, color: Colors .grey, ), ), ) : Container( width: double.infinity, height: 100, color: Colors .grey[300], child: const Icon( Icons .image_not_supported, color: Colors.grey, ), ), ), Align( alignment: Alignment.centerRight, child: Container( height: 30, width: 80, transform: Matrix4.translationValues( -10, -15, 0, ), decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular( 20, ), border: Border.all( color: Colors.white, width: 1.5, ), ), child: Center( child: Text( 'Rs.${item.amount ?? "N/A"}', style: const TextStyle( fontFamily: 'Gilroy-Bold', fontWeight: FontWeight.w400, fontSize: 16.86, height: 15.17 / 16.86, letterSpacing: 0.02, color: Color( 0xFFFCFAFA, ), ), ), ), ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: 8, ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Expanded( child: Text( item.serviceName ?? 'Service', style: const TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight .w700, fontSize: 15.02, height: 16.22 / 18.02, letterSpacing: 0.0, ), maxLines: 1, overflow: TextOverflow .ellipsis, ), ), Row( children: [ const Icon( Icons.star, color: Colors .orange, size: 18, ), const SizedBox( width: 4, ), Text( (((double.tryParse( item.averageReview?.toString() ?? '0', ) ?? 0.0) * 2) .floor() / 2) .toStringAsFixed( 1, ), textAlign: TextAlign .center, style: const TextStyle( fontFamily: 'SF UI Display', fontWeight: FontWeight .w600, fontSize: 14.17, height: 20.09 / 14.17, letterSpacing: -0.5, ), ), ], ), ], ), const SizedBox( height: 10, ), Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Container( width: 125, padding: const EdgeInsets.all( 4, ), decoration: BoxDecoration( border: Border.all( color: AppColors .lightGrey, ), color: Colors .white, borderRadius: BorderRadius.circular( 12, ), ), child: Row( children: [ item.profilePic1 != null ? CircleAvatar( radius: 15, backgroundImage: NetworkImage( item.profilePic1!, ), ) : const CircleAvatar( radius: 15, backgroundImage: AssetImage( AppAssets.login, ), ), const SizedBox( width: 10, ), Column( crossAxisAlignment: CrossAxisAlignment .start, children: [ Text( item.vendorName ?? 'Vendor', style: const TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight.w400, fontSize: 11.75, height: 10.57 / 11.75, letterSpacing: 0.0, color: Color( 0xFF353434, ), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox( height: 5, ), ConstrainedBox( constraints: const BoxConstraints( maxWidth: 75, ), child: Text( item.serviceName ?? 'Service', style: const TextStyle( fontFamily: 'Gilroy-Regular', fontWeight: FontWeight.w400, fontSize: 11.75, height: 10.57 / 11.75, letterSpacing: 0.0, color: Color( 0xFF717171, ), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ], ), ), GestureDetector( onTap: () => _updateService( item, ), child: AnimatedContainer( duration: const Duration( milliseconds: 200, ), padding: const EdgeInsets.all( 8, ), decoration: BoxDecoration( color: AppColors .primary, borderRadius: BorderRadius.circular( 12, ), ), child: Icon( isSelected ? Icons .check_rounded : Icons .arrow_forward_ios_rounded, size: 16, color: Colors .white, ), ), ), ], ), ], ), ), ], ), ); }, ); }, loading: () => const SizedBox( height: 210, child: Center( child: Text('Loading services...'), ), ), error: (error, stack) => SizedBox( height: 210, child: Center( child: Text( 'Error loading services', style: TextStyle(color: Colors.red), ), ), ), ), ), // Two Buttons Section Center( child: Padding( padding: const EdgeInsets.only( left: 16.0, right: 16, top: 40, ), child: Row( children: [ Expanded(child: buildButton("Enquiry", 0)), const SizedBox(width: 16), Expanded(child: buildButton("Book now", 1)), ], ), ), ), const SizedBox(height: 50), // Bottom padding ], ), ); }, loading: () => const Center( child: Padding( padding: EdgeInsets.all(50.0), child: Text('Loading service details...'), ), ), error: (err, stack) => Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline, size: 48, color: Colors.red, ), const SizedBox(height: 16), Text( "Failed to load service details", style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Text( "Please check your connection and try again", style: TextStyle(color: Colors.grey[600]), textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton( onPressed: () { ref.refresh( detailpageProvider( _currentServiceId.toString(), ), ); }, child: const Text('Retry'), ), ], ), ), ), ); }, ), ), ], ), ), ); } Widget buildButton(String text, int index) { final bool isSelected = selectedIndex == index; return Container( height: 66, decoration: BoxDecoration( color: isSelected ? Color(0xFF0066FF) : Colors.white, borderRadius: BorderRadius.circular(48), border: Border.all(color: Color(0xFFCFCFCF)), ), child: TextButton( onPressed: () { if (mounted) { setState(() { selectedIndex = index; }); if (index == 0) { // Show enquiry bottom sheet _showEnquiryBottomSheet(); } else { // Navigate to booking page with current detail if (currentDetail != null) { Get.toNamed( RouterConts.bookingserivce, arguments: currentDetail, ); } else { // Fallback: Get detail from provider final detailAsyncValue = ref.read( detailpageProvider(_currentServiceId.toString()), ); detailAsyncValue.whenData((detailList) { if (detailList.isNotEmpty && mounted) { Get.toNamed( RouterConts.bookingserivce, arguments: detailList.first, ); } }); } } } }, child: Text( text, style: TextStyle( fontFamily: 'Gilroy-Bold', fontWeight: FontWeight.w700, fontSize: 18, height: 21.5 / 23.89, letterSpacing: 0.0, color: isSelected ? Colors.white : const Color(0xFF292929), ), ), ), ); } void _showEnquiryBottomSheet() { if (!mounted) return; showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(40)), ), backgroundColor: Colors.white, builder: (BuildContext context) { return Padding( padding: EdgeInsets.only( left: 16.0, right: 16.0, top: 24.0, bottom: MediaQuery.of(context).viewInsets.bottom + 24.0, ), child: SizedBox( width: double.infinity, height: 600, // Increased height for 5 fields child: Column( mainAxisSize: MainAxisSize.min, children: [ // Custom app bar-like header Row( children: [ GestureDetector( onTap: () => Navigator.pop(context), child: const Icon(Icons.arrow_back_ios_new, size: 20), ), const Expanded( child: Center( child: Text( "Enquiry", style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), ), ), // Invisible icon to balance the row for perfect center alignment const Opacity( opacity: 0, child: Icon(Icons.arrow_back_ios_new, size: 20), ), ], ), const SizedBox(height: 24), Expanded( child: SingleChildScrollView( child: Form( key: _formKey, autovalidateMode: _autoValidateMode, child: Column( children: [ _buildField( label: "Name", controller: nameController, validator: (value) => value == null || value.trim().isEmpty ? "Name is required" : null, ), const SizedBox(height: 16), _buildField( label: "Mobile Number", controller: phoneController, keyboardType: TextInputType.phone, validator: (value) => value == null || value.length != 10 ? "Enter valid mobile number" : null, ), const SizedBox(height: 16), _buildField( label: "Email", controller: emailController, keyboardType: TextInputType.emailAddress, validator: (value) { if (value == null || value.isEmpty) { return "Email is required"; } final regex = RegExp(r'^[^@]+@[^@]+\.[^@]+'); return regex.hasMatch(value) ? null : "Enter a valid email"; }, ), const SizedBox(height: 16), _buildField( label: "Message", controller: messageController, maxLines: 4, height: 120, validator: (value) => value == null || value.isEmpty ? "Message is required" : null, ), const SizedBox(height: 24), SizedBox( width: double.infinity, height: 55.38, child: ElevatedButton( onPressed: () async { if (mounted) { setState(() { _autoValidateMode = AutovalidateMode.onUserInteraction; }); if (_formKey.currentState!.validate()) { try { final repo = ref.read( enquriyupdateRepositoryProvider, ); await repo.updateEnquriy( context: context, url: ConstsApi.enquery, name: nameController.text.trim(), number: phoneController.text.trim(), email: emailController.text.trim(), message: messageController.text.trim(), serviceid: _currentServiceId.toString(), ); // Clear form fields nameController.clear(); phoneController.clear(); emailController.clear(); messageController.clear(); if (mounted) { Navigator.pop(context); } } catch (e) { if (mounted) { Fluttertoast.showToast( msg: 'Submission failed: $e', toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.BOTTOM, backgroundColor: Colors.red, textColor: Colors.white, ); } } } } }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24.52), ), ), child: const Text( "Submit", style: TextStyle( fontFamily: 'Gilroy-Bold', fontWeight: FontWeight.w700, fontSize: 20.17, height: 1.25, letterSpacing: 1, color: AppColors.secondprimary, ), ), ), ), const SizedBox(height: 16), // Extra bottom padding ], ), ), ), ), ], ), ), ); }, ); } Widget _buildField({ required String label, required TextEditingController controller, required String? Function(String?) validator, TextInputType keyboardType = TextInputType.text, int maxLines = 1, double height = 58, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 380, height: height, decoration: BoxDecoration( color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE1E1E1)), ), padding: const EdgeInsets.symmetric(horizontal: 16), alignment: Alignment.center, child: TextFormField( controller: controller, keyboardType: keyboardType, maxLines: maxLines, decoration: InputDecoration( hintText: label, border: InputBorder.none, ), validator: validator, onChanged: (_) => _onFieldChanged(), ), ), const SizedBox(height: 4), ], ); } }