Files
Xpress-rider/lib/views/Dashboard/deliveries/map.dart

832 lines
38 KiB
Dart

part of 'deliveries.dart';
// -------------------------------------------------------------------------
// SCREEN 1: DELIVERY MAP PREVIEW (Shows route, has "Start" button)
// -------------------------------------------------------------------------
class _DeliveryMapScreen extends StatefulWidget {
final Map<String, dynamic> delivery;
final _MyDeliveriesState? parentState;
const _DeliveryMapScreen({
required this.delivery,
this.parentState,
});
@override
State<_DeliveryMapScreen> createState() => _DeliveryMapScreenState();
}
class _DeliveryMapScreenState extends State<_DeliveryMapScreen> {
GoogleMapController? _mapController;
late final LatLng _pickupLocation;
late final LatLng _dropLocation;
final Set<Marker> _markers = {};
final Set<Polyline> _polylines = {};
late final PolylinePoints _polylinePoints;
bool _isLoadingRoute = true;
bool _isNavigating = false; // Prevent multiple clicks
static const String _googleApiKey = 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q';
@override
void initState() {
super.initState();
_polylinePoints = PolylinePoints(apiKey: _googleApiKey);
_resolveLocationsFromDelivery();
_setMarkers();
_isLoadingRoute = false;
_createRealRoute();
}
double _parseD(dynamic v) {
if (v == null) return 0.0;
if (v is num) return v.toDouble();
return double.tryParse(v.toString()) ?? 0.0;
}
void _resolveLocationsFromDelivery() {
final d = widget.delivery;
final double pickLat = _parseD(d['pickuplat'] ?? d['PickupLat']);
final double pickLon = _parseD(d['pickuplon'] ?? d['PickupLon']);
final double dropLat = _parseD(
d['droplat'] ?? d['DropLat'] ?? d['deliverylat'],
);
final double dropLon = _parseD(
d['droplon'] ?? d['DropLon'] ?? d['deliverylong'],
);
final double riderLat = _parseD(d['riderslat']);
final double riderLon = _parseD(d['riderslon']);
final bool hasPickup = pickLat != 0 && pickLon != 0;
final bool hasDrop = dropLat != 0 && dropLon != 0;
final LatLng pickup = hasPickup
? LatLng(pickLat, pickLon)
: (riderLat != 0 && riderLon != 0
? LatLng(riderLat, riderLon)
: const LatLng(10.998356, 76.977596));
final LatLng drop = hasDrop
? LatLng(dropLat, dropLon)
: const LatLng(11.004556, 76.967696);
_pickupLocation = pickup;
_dropLocation = drop;
}
void _setMarkers() {
_markers.addAll([
Marker(
markerId: const MarkerId('pickup'),
position: _pickupLocation,
infoWindow: const InfoWindow(title: 'Pickup Location'),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
),
Marker(
markerId: const MarkerId('drop'),
position: _dropLocation,
infoWindow: const InfoWindow(title: 'Drop Location'),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
),
]);
}
Future<void> _createRealRoute() async {
try {
final request = PolylineRequest(
origin: PointLatLng(
_pickupLocation.latitude,
_pickupLocation.longitude,
),
destination: PointLatLng(
_dropLocation.latitude,
_dropLocation.longitude,
),
mode: TravelMode.driving,
);
final result = await _polylinePoints.getRouteBetweenCoordinates(
request: request,
);
if (result.points.isNotEmpty) {
final routePoints = result.points
.map((e) => LatLng(e.latitude, e.longitude))
.toList();
if (mounted) {
setState(() {
_polylines.add(
Polyline(
polylineId: const PolylineId('real_route'),
color: ColorConstants.primaryColor,
width: 6,
points: routePoints,
),
);
_isLoadingRoute = false;
});
_fitMapToRoute();
}
} else {
debugPrint('[MAP_PREVIEW] No route found');
if (mounted) {
setState(() => _isLoadingRoute = false);
}
}
} catch (e) {
debugPrint('[MAP_PREVIEW] Error creating route: $e');
if (mounted) {
setState(() => _isLoadingRoute = false);
}
}
}
Future<void> _fitMapToRoute() async {
if (!mounted) return;
if (_mapController == null) return;
// Check if controller is still alive (important!)
try {
await _mapController!.getVisibleRegion();
} catch (e) {
debugPrint("❌ Map controller is dead. Skip animateCamera.");
return;
}
final bounds = LatLngBounds(
southwest: LatLng(
math.min(_pickupLocation.latitude, _dropLocation.latitude),
math.min(_pickupLocation.longitude, _dropLocation.longitude),
),
northeast: LatLng(
math.max(_pickupLocation.latitude, _dropLocation.latitude),
math.max(_pickupLocation.longitude, _dropLocation.longitude),
),
);
// Try animate safely
for (int i = 0; i < 10; i++) {
if (!mounted) return;
try {
await _mapController!.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 80),
);
return;
} catch (e) {
await Future.delayed(const Duration(milliseconds: 150));
}
}
debugPrint("❌ animateCamera failed after retries (map probably disposed)");
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
SizedBox.expand(
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: _pickupLocation,
zoom: 14.5,
),
onMapCreated: (controller) {
_mapController = controller;
Future.delayed(const Duration(milliseconds: 500), () {
if (!_isLoadingRoute && _polylines.isNotEmpty) {
_fitMapToRoute();
}
});
},
markers: _markers,
polylines: _polylines,
zoomControlsEnabled: false,
myLocationButtonEnabled: false,
),
),
Positioned(
top: 50,
left: 16,
child: CircleAvatar(
backgroundColor: Colors.white,
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.pop(context),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: LayoutBuilder(
builder: (ctx, constraints) {
final double maxSheetHeight =
MediaQuery.of(ctx).size.height * 0.35;
// ignore: unused_local_variable
final double allowedHeight = math.min(
constraints.maxHeight,
maxSheetHeight,
);
return ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Customer Details',
style: TextStyle(
fontSize: FontConstants.xxLarge(context),
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
InkWell(
onTap: () async {
// Just launch dialer; PiP is handled only from navigation screen
final phone =
(widget.delivery['deliverycontactno'] ?? '')
.toString();
final bool success = await launchPhoneDialer(
phone.isNotEmpty ? phone : '9876543210',
);
if (!success && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Could not launch dialer'),
),
);
}
},
child: Image.asset(
'assets/images/phone-call .png',
height: 27,
width: 27,
errorBuilder: (c, e, s) => const Icon(
Icons.phone,
size: 27,
color: Colors.green,
),
),
),
],
),
const SizedBox(height: 8),
const Divider(thickness: 1),
const SizedBox(height: 8),
_buildCustomerInfo(),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: _isNavigating ? null : () async {
// Prevent multiple clicks
if (_isNavigating || !mounted || !context.mounted) return;
setState(() {
_isNavigating = true;
});
try {
// Capture screen size before navigation
final screenSize = MediaQuery.of(context).size *
MediaQuery.of(context).devicePixelRatio;
final dc = Get.put(
DeliveriesController(),
permanent: true,
);
final d = widget.delivery;
final int deliveryId =
int.tryParse(
'${d['deliveryid'] ?? d['DeliveryId'] ?? 0}',
) ??
0;
final String orderId = (d['orderid'] ??
d['OrderId'] ??
'')
.toString();
final int orderHeaderId =
int.tryParse(
'${d['orderheaderid'] ?? d['OrderHeaderId'] ?? 0}',
) ??
0;
// Save ridertime start at the moment navigation is started
try {
if (deliveryId > 0) {
final prefs =
await SharedPreferences.getInstance();
// 1) Save rider time start (existing behaviour)
await prefs.setString(
'ridertime_start_$deliveryId',
DateTime.now().toIso8601String(),
);
// 2) Save ETA end time for this order so PiP timer can resume correctly
final rawEta = d['eta'];
int etaMinutes = 0;
if (rawEta != null) {
etaMinutes =
int.tryParse(rawEta.toString()) ?? 0;
}
if (etaMinutes > 0) {
final now = DateTime.now();
final endTime = now
.add(Duration(minutes: etaMinutes))
.millisecondsSinceEpoch ~/
1000; // store seconds
await prefs.setInt(
'eta_endtime_$orderId',
endTime,
);
debugPrint(
'[ACTIVE][ETA] Saved eta_endtime_$orderId -> $endTime (eta=$etaMinutes min)',
);
}
debugPrint(
'[ACTIVE] Saved ridertime_start for deliveryId=$deliveryId',
);
}
} catch (e) {
debugPrint(
'[ACTIVE] Error saving ridertime_start: $e',
);
}
final parentState =
widget.parentState ??
context
.findAncestorStateOfType<
_MyDeliveriesState
>();
// ✅ CRITICAL: Check active delivery BEFORE navigation
if (parentState != null) {
final currentOrderId = (d['orderid'] ??
d['OrderId'] ??
'')
.toString();
final activeOrderIds = parentState
._activeDeliveries
.map(
(del) => (del['orderid'] ??
del['OrderId'] ??
'')
.toString(),
)
.where((id) => id.isNotEmpty)
.toSet();
// Block if there's a different active delivery
if (parentState._activeDeliveries.isNotEmpty &&
!activeOrderIds.contains(currentOrderId)) {
if (mounted) {
setState(() {
_isNavigating = false;
});
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext dialogContext) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.orange.shade400,
Colors.red.shade500,
],
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Icon
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.warning_rounded,
color: Colors.white,
size: 48,
),
),
const SizedBox(height: 20),
// Title
Text(
'Active Delivery in Progress',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: FontConstants.xxLarge(context),
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(height: 12),
// Message
Text(
'Please complete your active delivery first before starting another delivery.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withOpacity(0.95),
fontSize: FontConstants.medium(context),
height: 1.4,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(height: 24),
// Action Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.of(dialogContext).pop();
// Navigate to active delivery
if (parentState._activeDeliveries.isNotEmpty) {
parentState.startDelivery(parentState._activeDeliveries.first);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.red.shade600,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.two_wheeler,
size: 22,
),
const SizedBox(width: 8),
Text(
'View Active Delivery',
style: TextStyle(
fontSize: FontConstants.regular(context),
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
),
const SizedBox(height: 12),
// Close Button
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
},
child: Text(
'Close',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 15,
fontWeight: FontWeight.w500,
fontFamily: FontConstants.fontFamily,
),
),
),
],
),
),
);
},
);
}
return; // Return early to prevent starting another delivery
}
}
if (deliveryId > 0 && orderId.isNotEmpty) {
String riderLatStr = '0';
String riderLngStr = '0';
try {
final position =
await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.medium,
timeLimit: Duration(seconds: 3),
),
).timeout(const Duration(seconds: 3));
riderLatStr = position.latitude
.toStringAsFixed(6);
riderLngStr = position.longitude
.toStringAsFixed(6);
} catch (e) {
debugPrint(
'[ACTIVE] Error getting rider location: $e',
);
try {
final lastPos =
await Geolocator.getLastKnownPosition();
if (lastPos != null) {
riderLatStr = lastPos.latitude
.toStringAsFixed(6);
riderLngStr = lastPos.longitude
.toStringAsFixed(6);
}
} catch (_) {}
}
// ✅ CRITICAL: Navigate FIRST, then handle status updates
if (!mounted || !context.mounted) {
setState(() {
_isNavigating = false;
});
return;
}
// Navigate immediately - this must happen
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _RiderNavigationScreen(
delivery: widget.delivery,
parentState: widget.parentState,
),
),
);
// Reset navigation state after navigation completes
setState(() {
_isNavigating = false;
});
// Continue with status updates and PiP in background
debugPrint(
'[ACTIVE] Updating status for deliveryId=$deliveryId orderId=$orderId lat=$riderLatStr lng=$riderLngStr',
);
final ok = await dc.updateActiveStatus(
deliveryId: deliveryId,
orderHeaderId: orderHeaderId,
ridersLat: riderLatStr,
ridersLng: riderLngStr,
orderId: orderId,
);
debugPrint(
'[ACTIVE] Status update result: $ok',
);
// ✅ CRITICAL: ENFORCE PiP when delivery becomes active (compulsory)
if (ok && !dc.isPipEnabled.value) {
try {
debugPrint('[ACTIVE] Delivery is now active - Enforcing PiP mode');
final floating = Floating();
const rational = Rational.landscape();
final height = (screenSize.height * 0.5).toInt();
final width = (screenSize.width * 0.9).toInt();
final arguments = ImmediatePiP(
aspectRatio: rational,
sourceRectHint: math.Rectangle<int>(
((screenSize.width - width) ~/ 2).toInt(),
((screenSize.height - height) ~/ 2).toInt(),
width,
height,
),
);
await floating.enable(arguments);
dc.isPipEnabled.value = true;
debugPrint('[ACTIVE] PiP enabled successfully');
// Also try method channel as backup
try {
const channel = MethodChannel('nearle/pip');
await channel.invokeMethod<bool>('enterPip');
} catch (_) {}
} catch (e) {
debugPrint('[ACTIVE] Error enabling PiP: $e');
}
}
if (parentState != null) {
final previousActive =
parentState._activeDeliveryOrderId;
if (previousActive != null &&
previousActive != orderId) {
parentState._stopDeliveryPosting(
previousActive,
);
}
parentState._activeDeliveryOrderId = orderId;
d['orderstatus'] = 'active';
final orderKey = parentState._getOrderKey(d);
if (parentState._skippedOrdersCache
.containsKey(orderKey)) {
parentState._skippedOrdersCache.remove(
orderKey,
);
parentState._skippedOrderTimestamps.remove(
orderKey,
);
await parentState._saveSkippedOrdersCache();
debugPrint(
'[ACTIVE] Removed from skipped cache (resumed): $orderKey',
);
}
await parentState._startDeliveryPosting(d);
try {
final prefs =
await SharedPreferences.getInstance();
await prefs.setString(
'active_delivery_order_id',
orderId,
);
} catch (e) {
debugPrint(
'[ACTIVE] Error saving active delivery ID: $e',
);
}
// ignore: invalid_use_of_protected_member
parentState.setState(() {});
}
} else {
debugPrint(
'[ACTIVE] Invalid deliveryId: $deliveryId or orderId: $orderId',
);
setState(() {
_isNavigating = false;
});
}
} catch (e) {
debugPrint(
'[ACTIVE] Error in navigation flow: $e',
);
if (mounted) {
setState(() {
_isNavigating = false;
});
}
}
},
child: _isNavigating
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
const SizedBox(width: 12),
Text(
'Starting...',
style: TextStyle(
fontSize: FontConstants.xLarge(context),
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
color: Colors.white,
),
),
],
)
: Text(
'Start Navigation',
style: TextStyle(
fontSize: FontConstants.xLarge(context),
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
color: Colors.white,
),
),
),
],
);
},
),
),
),
),
],
),
);
}
Widget _buildCustomerInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
'Name:',
(widget.delivery['deliverycustomer'] ?? 'Customer').toString(),
),
const SizedBox(height: 10),
_buildInfoRow(
'Address:',
(widget.delivery['deliveryaddress'] ?? 'Address not available')
.toString(),
isExpanded: true,
),
const SizedBox(height: 10),
_buildInfoRow(
'Order ID:',
'#${(widget.delivery['orderid'] ?? '').toString()}',
),
const SizedBox(height: 10),
_buildInfoRow(
'Distance:',
'${(widget.delivery['kms'] ?? '0').toString()} km',
valueColor: Colors.red,
),
],
);
}
Widget _buildInfoRow(
String label,
String value, {
bool isExpanded = false,
Color? valueColor,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: FontConstants.xLarge(context),
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(width: 15),
isExpanded
? Expanded(
child: Text(
value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: FontConstants.large(context),
fontFamily: FontConstants.fontFamily,
color: valueColor ?? Colors.black87,
),
),
)
: Text(
value,
style: TextStyle(
fontSize: FontConstants.large(context),
fontFamily: FontConstants.fontFamily,
color: valueColor ?? Colors.black87,
),
),
],
);
}
}