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;