206 lines
6.8 KiB
TypeScript
206 lines
6.8 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { View, Text, Alert, BackHandler } from 'react-native';
|
|
import { useAppTheme } from '@theme';
|
|
import { PrimaryButton, MapViewComponent } from '@components';
|
|
import { useAppDispatch, useAppSelector } from '@store';
|
|
import { advanceJobStatus } from '@store/commonReducers/job';
|
|
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
import { AppStackParamList } from '@navigation/navigationTypes';
|
|
import { RouteNames, JobStatus } from '@utils/constants';
|
|
import { getStyles } from './liveTrackingScreen.styles';
|
|
import { Coordinates } from '@app-types/index';
|
|
import {
|
|
requestLocationPermission,
|
|
showPermissionDeniedAlert,
|
|
getCurrentPosition,
|
|
getHaversineDistance,
|
|
} from '@services/locationService';
|
|
import { socketService } from '@services/socketService';
|
|
import Geolocation from 'react-native-geolocation-service';
|
|
|
|
export const LiveTrackingScreen: React.FC = () => {
|
|
const { colors } = useAppTheme();
|
|
const styles = getStyles(colors);
|
|
const dispatch = useAppDispatch();
|
|
const navigation =
|
|
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
|
|
const route =
|
|
useRoute<RouteProp<AppStackParamList, RouteNames.LiveTracking>>();
|
|
const { jobId } = route.params;
|
|
|
|
const { activeDeliveries } = useAppSelector(state => state.deliveries);
|
|
const activeJob = activeDeliveries?.delivery;
|
|
|
|
const [currentLocation, setCurrentLocation] = useState<Coordinates | null>(
|
|
null,
|
|
);
|
|
const [distance, setDistance] = useState<number | null>(null);
|
|
const [speed, setSpeed] = useState<number>(24); // default speed in km/h
|
|
|
|
// Block the hardware back button — once delivery starts the partner
|
|
// must not navigate back to the OrderPickedUp screen.
|
|
// useEffect(() => {
|
|
// const onBackPress = () => true; // return true = event consumed, back suppressed
|
|
// const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
|
// return () => subscription.remove();
|
|
// }, []);
|
|
|
|
useEffect(() => {
|
|
let watchId: number | null = null;
|
|
|
|
const startTracking = async () => {
|
|
try {
|
|
const hasPermission = await requestLocationPermission();
|
|
if (!hasPermission) {
|
|
showPermissionDeniedAlert();
|
|
return;
|
|
}
|
|
|
|
// Get initial current position
|
|
const initialPos = await getCurrentPosition();
|
|
setCurrentLocation(initialPos);
|
|
|
|
// Send initial location update to socket
|
|
socketService.updateLocation(initialPos);
|
|
|
|
// Start watching position
|
|
watchId = Geolocation.watchPosition(
|
|
position => {
|
|
const coords: Coordinates = {
|
|
latitude: position.coords.latitude,
|
|
longitude: position.coords.longitude,
|
|
};
|
|
setCurrentLocation(coords);
|
|
|
|
// Emit location update via socket safely
|
|
try {
|
|
socketService.updateLocation(coords);
|
|
} catch (err) {
|
|
console.warn('Socket location update error:', err);
|
|
}
|
|
|
|
// Update speed if available (speed in m/s from GPS)
|
|
if (position.coords.speed !== null && position.coords.speed > 0) {
|
|
const speedKmH = position.coords.speed * 3.6;
|
|
setSpeed(Math.max(speedKmH, 10)); // keep speed at least 10 km/h for a realistic ETA
|
|
}
|
|
},
|
|
error => {
|
|
console.error('watchPosition error:', error);
|
|
},
|
|
{
|
|
enableHighAccuracy: true,
|
|
distanceFilter: 10, // every 10 meters
|
|
interval: 5000,
|
|
fastestInterval: 2000,
|
|
},
|
|
);
|
|
} catch (error) {
|
|
console.error('Error starting location tracking:', error);
|
|
}
|
|
};
|
|
|
|
startTracking();
|
|
|
|
return () => {
|
|
if (watchId !== null) {
|
|
Geolocation.clearWatch(watchId);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// Compute live distance to drop-off address
|
|
useEffect(() => {
|
|
if (currentLocation && activeJob?.order?.dropAddress) {
|
|
const dropCoords = {
|
|
latitude: activeJob.order.dropAddress.latitude,
|
|
longitude: activeJob.order.dropAddress.longitude,
|
|
};
|
|
const km = getHaversineDistance(currentLocation, dropCoords);
|
|
setDistance(km);
|
|
}
|
|
}, [currentLocation, activeJob?.order?.dropAddress]);
|
|
|
|
const handleArrived = () => {
|
|
dispatch(advanceJobStatus(JobStatus.ArrivedAtCustomer));
|
|
navigation.navigate(RouteNames.DeliverOrder, {
|
|
jobId: jobId,
|
|
});
|
|
};
|
|
|
|
const handleContact = () => {
|
|
Alert.alert(
|
|
'Contact Customer',
|
|
`Calling ${activeJob?.order?.customer?.user?.name || 'Customer'} at ${
|
|
activeJob?.order?.customer?.user?.phone
|
|
} `,
|
|
);
|
|
};
|
|
|
|
// Calculate ETA in minutes based on distance and speed (distance / speed * 60)
|
|
const etaMinutes =
|
|
distance !== null ? Math.max(Math.round((distance / speed) * 60), 1) : null;
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
{/* Top Floating ETA Card */}
|
|
<View style={styles.etaCard}>
|
|
<Text style={styles.etaTitle}>
|
|
{etaMinutes !== null
|
|
? `Arriving in ${etaMinutes} mins`
|
|
: 'Calculating ETA…'}
|
|
</Text>
|
|
<Text style={styles.etaSub}>
|
|
Distance: {distance !== null ? `${distance.toFixed(2)} km` : '...'} •
|
|
Speed: {Math.round(speed)} km/h
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Map Content */}
|
|
{activeJob?.order?.dropAddress ? (
|
|
<MapViewComponent
|
|
currentLocation={currentLocation || undefined}
|
|
origin={currentLocation || undefined}
|
|
destination={activeJob.order.dropAddress}
|
|
showRoute={!!currentLocation}
|
|
style={styles.mapPlaceholder}
|
|
/>
|
|
) : (
|
|
<View style={styles.mapPlaceholder}>
|
|
<Text style={styles.placeholderText}>Loading Map...</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Customer Location Bottom Card */}
|
|
<View style={styles.bottomCard}>
|
|
<Text style={styles.cardTitle}>Deliver to</Text>
|
|
<Text style={styles.customerName}>
|
|
{activeJob?.order?.customer?.user?.name}
|
|
</Text>
|
|
<Text style={styles.customerAddress}>
|
|
{activeJob?.order?.dropAddress
|
|
? `${activeJob.order.dropAddress.houseNumber}, ${activeJob.order.dropAddress.addressLine1}`
|
|
: 'Loading address...'}
|
|
</Text>
|
|
|
|
<View style={styles.btnRow}>
|
|
<PrimaryButton
|
|
title="Call"
|
|
onPress={handleContact}
|
|
style={styles.contactBtn}
|
|
textStyle={{ color: colors.text }}
|
|
/>
|
|
<PrimaryButton
|
|
title="Arrived at Location"
|
|
onPress={handleArrived}
|
|
style={styles.arrivedBtn}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default LiveTrackingScreen;
|