import 'dart:async'; import 'dart:developer'; import 'package:wanisdriver/Provider/RideProvider.dart'; import 'package:wanisdriver/config/AppSnackbar.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:geolocator/geolocator.dart'; import 'package:provider/provider.dart'; import '../Provider/RideNotification.dart'; import '../Provider/RideStatusProvider.dart'; import '../config/constants/app_constant.dart'; import '../config/enum/ride_enum.dart'; import '../models/Ride.dart'; import '../routes/app_routes.dart'; import '../utils/DistanceCalculation.dart'; import '../utils/colors.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:beemwidget/text_styles/beem_text_styles.dart'; class RideCardView extends StatefulWidget { final Ride ride; final bool isSelected; final Duration animationDuration; final Position? driverCurrentPosition; const RideCardView({ Key? key, required this.ride, required this.isSelected, this.animationDuration = const Duration(seconds: 15), this.driverCurrentPosition, }) : super(key: key); @override State createState() => _RideCardViewState(); } class _RideCardViewState extends State with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { late final DateTime _startTime; late Timer _timer; bool isprocessing = false; bool _isTimerExpired = false; int _remainingSeconds = 0; double _animationValue = 0.0; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); // Record the start time for this specific card _startTime = DateTime.now(); _remainingSeconds = widget.animationDuration.inSeconds; _startTimer(); } void _startTimer() { // Update every 100ms for smooth animation _timer = Timer.periodic(const Duration(milliseconds: 100), (timer) { if (!mounted) { timer.cancel(); return; } final elapsed = DateTime.now().difference(_startTime); final remaining = widget.animationDuration - elapsed; if (remaining.isNegative) { // Timer expired timer.cancel(); if (!_isTimerExpired) { _isTimerExpired = true; _remainingSeconds = 0; _animationValue = 1.0; setState(() {}); _handleAutoDecline(); } return; } // Update values final newRemainingSeconds = remaining.inSeconds; final newAnimationValue = elapsed.inMilliseconds / widget.animationDuration.inMilliseconds; if (newRemainingSeconds != _remainingSeconds || (newAnimationValue - _animationValue).abs() > 0.01) { setState(() { _remainingSeconds = newRemainingSeconds; _animationValue = newAnimationValue.clamp(0.0, 1.0); }); } }); } void _handleAutoDecline() async { if (mounted && !isprocessing) { final rideNotification = context.read(); final rideProvider = context.read(); // Remove this specific ride from the queue rideNotification.removeRide(widget.ride.id); // Cancel the ride on the server await rideProvider.cancel_ride_driver(widget.ride.id); print("Ride #${widget.ride.id} auto-declined due to timeout."); } } @override void dispose() { log("❌ RideCard #${widget.ride.id}: dispose"); _timer.cancel(); super.dispose(); } Future decline() async { if (_isTimerExpired) return false; final rideprovider = context.read(); bool result = await rideprovider.cancel_ride_driver(widget.ride.id); if (!result && mounted) { AppSnackbar.showError("Error", "Error occurred please try again later"); } return result; } void stopTimerAndAnimation() { _timer.cancel(); } @override Widget build(BuildContext context) { super.build(context); final rideNotification = context.read(); final rideStatusProvider = context.read(); final rideprovider = context.read(); return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), decoration: BoxDecoration( border: widget.isSelected ? Border.all( color: Theme.of(context).brightness == Brightness.dark ? AppColors.darkPrimaryColor : AppColors.primaryColor, width: 2) : null, borderRadius: BorderRadius.circular(22), ), child: Material( elevation: 10, shadowColor: Colors.black.withOpacity(0.078), borderRadius: BorderRadius.circular(20), child: Container( decoration: BoxDecoration( color: Theme.of(context).brightness == Brightness.dark ? AppColors.blackBackgroundColor : AppColors.whiteBackgroundColor, borderRadius: BorderRadius.circular(20.0), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Rider Info Section Padding( padding: EdgeInsets.only(left: 20.sp, right: 20.sp, top: 23.sp), child: Row( children: [ ClipOval( child: CachedNetworkImage( imageUrl: '$mediaUrl${widget.ride.userimageurl}', width: 40.sp, height: 40.sp, fit: BoxFit.fill, placeholder: (context, url) => const Center(child: CircularProgressIndicator()), errorWidget: (context, url, error) => Image.asset( 'assets/images/user_avatar.png', fit: BoxFit.cover), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.ride.ridername, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), Row( children: [ const Icon(Icons.star, color: Colors.amber, size: 16), const SizedBox(width: 4), Text( widget.ride.averagedriverrating .toStringAsFixed(1), style: const TextStyle( fontSize: 14, color: Colors.grey), ), ], ), ], ), ), Column( children: [ Text( "${widget.ride.totalPrice?.toStringAsFixed(2) ?? 0} TND", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold,color: AppColors.primaryColor), ), Text( "${widget.ride.durationInMinutes?.toStringAsFixed(0) ?? 0} min", style: const TextStyle(fontSize: 14, color: Colors.grey), ), ], ), ], ), ), // Location Details Padding( padding: EdgeInsets.only(top: 7.sp), child: Container( height: 56.sp, color: Theme.of(context).brightness == Brightness.dark ? AppColors.darkCardColor : AppColors.whiteBackgroundColor, child: Row( children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 13.sp) .copyWith(left: 35.sp), child: SvgPicture.asset( 'assets/icons/currentlocation_map.svg'), ), Flexible( child: Text( widget.ride.startLocationName, maxLines: 1, overflow: TextOverflow.ellipsis, style: BeemTextStyles.poppinsBold12 .copyWith(fontSize: 10), ), ), ], ), ), ), Container( height: 56.sp, color: Theme.of(context).brightness == Brightness.dark ? AppColors.darkCardColor : AppColors.whiteBackgroundColor, child: Row( children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 13.sp) .copyWith(left: 35.sp), child: SvgPicture.asset('assets/icons/ic_pin.svg'), ), Flexible( child: Text( widget.ride.endLocationName, maxLines: 2, overflow: TextOverflow.ellipsis, style: BeemTextStyles.poppinsBold12.copyWith(fontSize: 10), ), ), ], ), ), // Distance Info Padding( padding: EdgeInsets.only(left: 35.sp, right: 30.sp, top: 17.sp), child: Row( children: [ const Icon(Icons.route, color: Colors.orange, size: 20), const SizedBox(width: 8), // Text( // "${widget.ride.distancekm.toStringAsFixed(0)} Km", // style: const TextStyle( // fontSize: 16, fontWeight: FontWeight.w500), // ), // Text( // "${calculateDistanceBetweenDriverAndUser( // "Here", // widget.ride.startLocation, // )?.toStringAsFixed(0)} Km", // style: const TextStyle( // fontSize: 16, fontWeight: FontWeight.w500), // ), Text( rideprovider.getCalculatedDistanceString("${widget.driverCurrentPosition?.latitude},${widget.driverCurrentPosition?.longitude}",widget.ride.startLocation), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500), ), ], ), ), // Action Buttons Padding( padding: EdgeInsets.only( left: 35.sp, right: 30.sp, top: 14.sp, bottom: 16.sp), child: Row( children: [ // Decline Button Expanded( child: ElevatedButton( onPressed: _isTimerExpired ? null : () async { stopTimerAndAnimation(); if (await decline()) { rideNotification.removeRide(widget.ride.id); } }, style: ElevatedButton.styleFrom( backgroundColor: _isTimerExpired ? Colors.grey : AppColors.secondBtnColor, foregroundColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25)), padding: const EdgeInsets.symmetric(vertical: 12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (context.watch().isdiclineLoading) const Padding( padding: EdgeInsets.only(right: 8.0), child: SizedBox( width: 15, height: 15, child: CircularProgressIndicator( color: Colors.black, strokeWidth: 2), ), ), Text( _isTimerExpired ? "Expired" : "Decline", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600), ), ], ), ), ), const SizedBox(width: 16), // Accept Button with Fixed Timer Expanded( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(25), gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: _isTimerExpired ? [ Colors.grey, Colors.grey.withAlpha(100), ] : [ Theme.of(context).brightness == Brightness.dark ? AppColors.darkPrimaryColor : AppColors.primaryColor, Theme.of(context).brightness == Brightness.dark ? AppColors.darkPrimaryColor : AppColors.primaryColor.withAlpha(50), ], stops: [ (1.0 - _animationValue - 0.3).clamp(0.0, 1.0), (1.0 - _animationValue).clamp(0.0, 1.0), ], ), ), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(25), child: InkWell( borderRadius: BorderRadius.circular(25), onTap: (_isTimerExpired || isprocessing) ? null : () async { setState(() => isprocessing = true); stopTimerAndAnimation(); log('*******--> 001'); // Check if ride is still available - but only show message for this specific ride Ride? newdata = await rideprovider.getRideDetails(widget.ride.id, forceRefresh: true); log('*******--> 002 $newdata'); if (newdata == null ||newdata.driverId != null ||["CANCELLED", "ONGOING", "COMPLETED"].contains(newdata.rideStatus)) { log('*******--> 003x'); // Only show "Too Late" message for this specific ride String message ="Another driver accepted this ride"; if (newdata?.rideStatus == "CANCELLED") { message ="The client canceled this ride"; } else if (newdata == null) { message = "This ride is no longer available"; } AppSnackbar.showInfo("Too Late", message); // Remove only this specific ride // await decline(); rideNotification.removeRide(widget.ride.id); if (mounted) { setState(() => isprocessing = false); } return; } // Try to accept the ride if (await rideprovider.accept_ride_driver(widget.ride.id)) { await rideprovider.findbyid(widget.ride.id); rideStatusProvider.updatestates(RideEnum.to_client); // Clear ALL pending rides when one is accepted rideNotification.clearAllPendingRides(); await rideNotification.locationconnectionservice(); } else { // await decline(); AppSnackbar.showInfo("Ride", "This ride is no longer available."); if (mounted) { setState(() => isprocessing = false); } } }, child: Container( padding: const EdgeInsets.symmetric(vertical: 12), alignment: Alignment.center, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (context .watch() .isAcceptLoading) const Padding( padding: EdgeInsets.only(right: 8.0), child: SizedBox( width: 15, height: 15, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2), ), ), Text( _isTimerExpired ? "Expired" : "Accept ($_remainingSeconds s)", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), ), ], ), ), ), ), ), ), ], ), ), ], ), ), ), ); } }