feat: implement real-time delivery dispatch flow with socket integration, status tracking, and UI screens

This commit is contained in:
Tamojit Biswas 2026-07-15 17:33:18 +05:30
parent 55bab67d91
commit 3e18e618d1
13 changed files with 540 additions and 116 deletions

View File

@ -4,7 +4,7 @@ import { DeliveryPartnerProfileResponse } from '@interfaces';
export const getAccountInfo = export const getAccountInfo =
async (): Promise<DeliveryPartnerProfileResponse> => { async (): Promise<DeliveryPartnerProfileResponse> => {
const response = await apiClient.get<DeliveryPartnerProfileResponse>( const response = await apiClient.get<DeliveryPartnerProfileResponse>(
'/account-info', '/delivery-partners/profile',
); );
return response; return response;
}; };

View File

@ -1,9 +1,13 @@
import React from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import MapView, { Marker, Polyline, PROVIDER_DEFAULT } from 'react-native-maps'; import MapView, { Marker, Polyline, PROVIDER_DEFAULT } from 'react-native-maps';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { Coordinates } from '@app-types/index'; import { Coordinates } from '@app-types/index';
import { getStyles } from './mapView.styles'; import { getStyles } from './mapView.styles';
import {
getRouteDirections,
getHaversineDistance,
} from '@services/locationService';
interface MapViewComponentProps { interface MapViewComponentProps {
origin?: Coordinates; origin?: Coordinates;
@ -22,6 +26,9 @@ export const MapViewComponent: React.FC<MapViewComponentProps> = ({
}) => { }) => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const mapRef = useRef<MapView>(null);
const [routeCoords, setRouteCoords] = useState<Coordinates[]>([]);
const lastFetchedOrigin = useRef<Coordinates | null>(null);
const defaultRegion = { const defaultRegion = {
latitude: currentLocation?.latitude || origin?.latitude || 12.9716, latitude: currentLocation?.latitude || origin?.latitude || 12.9716,
@ -30,9 +37,83 @@ export const MapViewComponent: React.FC<MapViewComponentProps> = ({
longitudeDelta: 0.05, longitudeDelta: 0.05,
}; };
// Fetch routing coordinates
useEffect(() => {
let active = true;
if (showRoute && origin && destination) {
// Calculate distance from last fetched origin
if (lastFetchedOrigin.current) {
const dist = getHaversineDistance(origin, lastFetchedOrigin.current);
// If driver moved less than 50 meters (0.05 km), skip re-fetching to prevent spamming
if (dist < 0.05 && routeCoords.length > 0) {
return;
}
}
getRouteDirections(origin, destination)
.then(points => {
if (active) {
setRouteCoords(points);
lastFetchedOrigin.current = origin;
}
})
.catch(err => {
console.error('Error in MapViewComponent getRouteDirections:', err);
if (active) {
setRouteCoords([origin, destination]);
}
});
} else {
setRouteCoords([]);
lastFetchedOrigin.current = null;
}
return () => {
active = false;
};
}, [
showRoute,
origin?.latitude,
origin?.longitude,
destination?.latitude,
destination?.longitude,
routeCoords.length,
]);
// Adjust camera to fit all coordinates
useEffect(() => {
const coordsToFit: Coordinates[] = [];
if (currentLocation) {
coordsToFit.push(currentLocation);
}
if (origin) {
coordsToFit.push(origin);
}
if (destination) {
coordsToFit.push(destination);
}
if (coordsToFit.length > 0 && mapRef.current) {
const timer = setTimeout(() => {
mapRef.current?.fitToCoordinates(coordsToFit, {
edgePadding: { top: 60, right: 60, bottom: 60, left: 60 },
animated: true,
});
}, 500);
return () => clearTimeout(timer);
}
}, [
currentLocation?.latitude,
currentLocation?.longitude,
origin?.latitude,
origin?.longitude,
destination?.latitude,
destination?.longitude,
]);
return ( return (
<View style={[styles.container, style]}> <View style={[styles.container, style]}>
<MapView <MapView
ref={mapRef}
provider={PROVIDER_DEFAULT} provider={PROVIDER_DEFAULT}
style={styles.map} style={styles.map}
initialRegion={defaultRegion} initialRegion={defaultRegion}
@ -60,9 +141,9 @@ export const MapViewComponent: React.FC<MapViewComponentProps> = ({
pinColor={colors.statusOnline} pinColor={colors.statusOnline}
/> />
)} )}
{showRoute && origin && destination && ( {showRoute && routeCoords.length > 0 && (
<Polyline <Polyline
coordinates={[origin, destination]} coordinates={routeCoords}
strokeColor={colors.primary} strokeColor={colors.primary}
strokeWidth={4} strokeWidth={4}
/> />
@ -74,4 +155,6 @@ export const MapViewComponent: React.FC<MapViewComponentProps> = ({
</View> </View>
); );
}; };
export default MapViewComponent; export default MapViewComponent;

View File

@ -25,6 +25,7 @@ import {
showPermissionDeniedAlert, showPermissionDeniedAlert,
socketService, socketService,
} from '@services'; } from '@services';
import { getAccountInfoThunk } from '../profileScreen';
export const DashboardScreen: React.FC = () => { export const DashboardScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
@ -38,14 +39,14 @@ export const DashboardScreen: React.FC = () => {
const { availabiltyStatus, loading: toggleLoading } = useAppSelector( const { availabiltyStatus, loading: toggleLoading } = useAppSelector(
state => state.dashboard, state => state.dashboard,
); );
const { user } = useAppSelector(state => state.auth); const { profile } = useAppSelector(state => state.accountInfo);
const authIsOnline = useAppSelector(state => state.auth.isOnline); const authIsOnline = useAppSelector(state => state.auth.isOnline);
const isOnline = availabiltyStatus const isOnline = availabiltyStatus
? availabiltyStatus.availabilityStatus === 'ONLINE' ? availabiltyStatus.availabilityStatus === 'ONLINE'
: authIsOnline; : authIsOnline;
const { todayEarnings, completedCount, onlineMinutes, weeklyData } = const { todayEarnings, completedCount, onlineMinutes, weeklyData } =
useAppSelector(state => state.earnings); useAppSelector(state => state.earnings);
const { jobStatus, activeJob } = useAppSelector(state => state.job); // const { jobStatus, activeJob } = useAppSelector(state => state.job);
const locationIntervalRef = useRef<ReturnType<typeof setInterval> | null>( const locationIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
null, null,
@ -70,6 +71,10 @@ export const DashboardScreen: React.FC = () => {
} }
}, [dispatch]); }, [dispatch]);
useEffect(() => {
dispatch(getAccountInfoThunk());
}, [dispatch]);
useEffect(() => { useEffect(() => {
if (isOnline) { if (isOnline) {
// Fire one immediately on going online, then every 15s // Fire one immediately on going online, then every 15s
@ -122,31 +127,31 @@ export const DashboardScreen: React.FC = () => {
}); });
}; };
const handleActiveJobBannerPress = () => { // const handleActiveJobBannerPress = () => {
// Navigate back to the screen corresponding to the current job step // // Navigate back to the screen corresponding to the current job step
switch (jobStatus) { // switch (jobStatus) {
case JobStatus.NewRequest: // case JobStatus.NewRequest:
navigation.navigate(RouteNames.NewJobRequest); // navigation.navigate(RouteNames.NewJobRequest);
break; // break;
case JobStatus.Accepted: // case JobStatus.Accepted:
navigation.navigate(RouteNames.OrderAccepted); // navigation.navigate(RouteNames.OrderAccepted);
break; // break;
case JobStatus.ArrivedAtStore: // case JobStatus.ArrivedAtStore:
navigation.navigate(RouteNames.ConfirmPickup); // navigation.navigate(RouteNames.ConfirmPickup);
break; // break;
case JobStatus.PickedUp: // case JobStatus.PickedUp:
navigation.navigate(RouteNames.OrderPickedUp); // navigation.navigate(RouteNames.OrderPickedUp);
break; // break;
case JobStatus.EnRoute: // case JobStatus.EnRoute:
navigation.navigate(RouteNames.LiveTracking); // navigation.navigate(RouteNames.LiveTracking);
break; // break;
case JobStatus.ArrivedAtCustomer: // case JobStatus.ArrivedAtCustomer:
navigation.navigate(RouteNames.DeliverOrder); // navigation.navigate(RouteNames.DeliverOrder);
break; // break;
default: // default:
Alert.alert('Status Error', 'Unknown active delivery state.'); // Alert.alert('Status Error', 'Unknown active delivery state.');
} // }
}; // };
const formatOnlineTime = (mins: number) => { const formatOnlineTime = (mins: number) => {
const hours = Math.floor(mins / 60); const hours = Math.floor(mins / 60);
@ -169,7 +174,7 @@ export const DashboardScreen: React.FC = () => {
<View style={styles.header}> <View style={styles.header}>
<View> <View>
<Text style={styles.greetingText}>Good Morning </Text> <Text style={styles.greetingText}>Good Morning </Text>
<Text style={styles.nameText}>{user?.name}</Text> <Text style={styles.nameText}>{profile?.user?.name}</Text>
</View> </View>
<View style={styles.toggleWrapper}> <View style={styles.toggleWrapper}>
@ -371,7 +376,7 @@ export const DashboardScreen: React.FC = () => {
</ScrollView> </ScrollView>
{/* Active Job Floating Bottom Banner */} {/* Active Job Floating Bottom Banner */}
{jobStatus !== JobStatus.Idle && {/* {jobStatus !== JobStatus.Idle &&
jobStatus !== JobStatus.NewRequest && jobStatus !== JobStatus.NewRequest &&
activeJob && ( activeJob && (
<TouchableOpacity <TouchableOpacity
@ -445,7 +450,7 @@ export const DashboardScreen: React.FC = () => {
</Text> </Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
)} )} */}
</View> </View>
); );
}; };

View File

@ -24,6 +24,7 @@ export const updateDeliveryPartnerLocation = createAsyncThunk(
const response = await deliveryPartnerLocation(payload); const response = await deliveryPartnerLocation(payload);
return response; return response;
} catch (error: unknown) { } catch (error: unknown) {
console.log(error);
return rejectWithValue(error); return rejectWithValue(error);
} }
}, },

View File

@ -10,6 +10,7 @@ import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes'; import { AppStackParamList } from '@navigation/navigationTypes';
import { RouteNames } from '@utils/constants'; import { RouteNames } from '@utils/constants';
import { DeliveryStatus } from '@interfaces';
export const JobsScreen: React.FC = () => { export const JobsScreen: React.FC = () => {
const navigation = const navigation =
@ -133,11 +134,23 @@ export const JobsScreen: React.FC = () => {
activeDeliveries?.delivery?.order?.orderItems?.length activeDeliveries?.delivery?.order?.orderItems?.length
} }
paymentType={activeDeliveries?.delivery?.order?.paymentMethod} paymentType={activeDeliveries?.delivery?.order?.paymentMethod}
onPress={() => onPress={() => {
navigation.navigate(RouteNames.OrderPickedUp, { const deliveryId = activeDeliveries?.delivery?.id;
jobId: activeDeliveries?.delivery?.id, const status = activeDeliveries?.delivery?.status as DeliveryStatus | undefined;
})
// Statuses that mean delivery has already started — skip OrderPickedUp
const deliveryStartedStatuses: DeliveryStatus[] = [
'OUT_FOR_DELIVERY',
'IN_TRANSIT',
'ARRIVED_AT_DROPOFF',
];
if (status && deliveryStartedStatuses.includes(status)) {
navigation.navigate(RouteNames.LiveTracking, { jobId: deliveryId });
} else {
navigation.navigate(RouteNames.OrderPickedUp, { jobId: deliveryId });
} }
}}
/> />
</View> </View>
) : ( ) : (

View File

@ -1,7 +1,7 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { View, Text, Alert } from 'react-native'; import { View, Text, Alert, BackHandler } from 'react-native';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { PrimaryButton } from '@components'; import { PrimaryButton, MapViewComponent } from '@components';
import { useAppDispatch, useAppSelector } from '@store'; import { useAppDispatch, useAppSelector } from '@store';
import { advanceJobStatus } from '@store/commonReducers/job'; import { advanceJobStatus } from '@store/commonReducers/job';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
@ -9,6 +9,15 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppStackParamList } from '@navigation/navigationTypes'; import { AppStackParamList } from '@navigation/navigationTypes';
import { RouteNames, JobStatus } from '@utils/constants'; import { RouteNames, JobStatus } from '@utils/constants';
import { getStyles } from './liveTrackingScreen.styles'; 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 = () => { export const LiveTrackingScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
@ -17,12 +26,102 @@ export const LiveTrackingScreen: React.FC = () => {
const navigation = const navigation =
useNavigation<NativeStackNavigationProp<AppStackParamList>>(); useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const route = const route =
useRoute<RouteProp<AppStackParamList, RouteNames.OrderPickedUp>>(); useRoute<RouteProp<AppStackParamList, RouteNames.LiveTracking>>();
const { jobId } = route.params; const { jobId } = route.params;
const { activeDeliveries } = useAppSelector(state => state.deliveries); const { activeDeliveries } = useAppSelector(state => state.deliveries);
const activeJob = activeDeliveries?.delivery; 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 = () => { const handleArrived = () => {
dispatch(advanceJobStatus(JobStatus.ArrivedAtCustomer)); dispatch(advanceJobStatus(JobStatus.ArrivedAtCustomer));
navigation.navigate(RouteNames.DeliverOrder, { navigation.navigate(RouteNames.DeliverOrder, {
@ -39,22 +138,39 @@ export const LiveTrackingScreen: React.FC = () => {
); );
}; };
// if (!activeJob) return null; // 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 ( return (
<View style={styles.container}> <View style={styles.container}>
{/* Top Floating ETA Card */} {/* Top Floating ETA Card */}
<View style={styles.etaCard}> <View style={styles.etaCard}>
<Text style={styles.etaTitle}>Arriving in 8 mins</Text> <Text style={styles.etaTitle}>
{etaMinutes !== null
? `Arriving in ${etaMinutes} mins`
: 'Calculating ETA…'}
</Text>
<Text style={styles.etaSub}> <Text style={styles.etaSub}>
Distance: {activeJob?.estimatedDistanceKm} km Speed: 24 km/h Distance: {distance !== null ? `${distance.toFixed(2)} km` : '...'}
Speed: {Math.round(speed)} km/h
</Text> </Text>
</View> </View>
{/* Map Background Placeholder */} {/* 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}> <View style={styles.mapPlaceholder}>
<Text style={styles.placeholderText}>[ Map Placeholder ]</Text> <Text style={styles.placeholderText}>Loading Map...</Text>
</View> </View>
)}
{/* Customer Location Bottom Card */} {/* Customer Location Bottom Card */}
<View style={styles.bottomCard}> <View style={styles.bottomCard}>
@ -63,9 +179,9 @@ export const LiveTrackingScreen: React.FC = () => {
{activeJob?.order?.customer?.user?.name} {activeJob?.order?.customer?.user?.name}
</Text> </Text>
<Text style={styles.customerAddress}> <Text style={styles.customerAddress}>
{activeJob?.order?.dropAddress?.houseNumber + {activeJob?.order?.dropAddress
', ' + ? `${activeJob.order.dropAddress.houseNumber}, ${activeJob.order.dropAddress.addressLine1}`
activeJob?.order?.dropAddress?.addressLine1} : 'Loading address...'}
</Text> </Text>
<View style={styles.btnRow}> <View style={styles.btnRow}>

View File

@ -25,9 +25,7 @@ export const NewJobRequestScreen: React.FC = () => {
// Read from the socket-driven delivery slice // Read from the socket-driven delivery slice
const currentOffer = useAppSelector(state => state.delivery.currentOffer); const currentOffer = useAppSelector(state => state.delivery.currentOffer);
const respondLoading = useAppSelector( const respondLoading = useAppSelector(state => state.delivery.respondLoading);
state => state.delivery.respondLoading,
);
const [secondsLeft, setSecondsLeft] = useState(TOTAL_SECONDS); const [secondsLeft, setSecondsLeft] = useState(TOTAL_SECONDS);
@ -83,7 +81,9 @@ export const NewJobRequestScreen: React.FC = () => {
) )
.unwrap() .unwrap()
.then(() => { .then(() => {
navigation.navigate(RouteNames.OrderAccepted); navigation.navigate(RouteNames.BottomTabs, {
screen: RouteNames.Jobs,
});
}) })
.catch(() => { .catch(() => {
// stay on screen — respondError will be set in redux // stay on screen — respondError will be set in redux
@ -109,8 +109,7 @@ export const NewJobRequestScreen: React.FC = () => {
// Timer text color: warning when < 5s // Timer text color: warning when < 5s
const timerColor = secondsLeft <= 5 ? colors.error : colors.primary; const timerColor = secondsLeft <= 5 ? colors.error : colors.primary;
const timerBorderColor = const timerBorderColor = secondsLeft <= 5 ? colors.error : colors.primary;
secondsLeft <= 5 ? colors.error : colors.primary;
return ( return (
<View style={styles.container}> <View style={styles.container}>
@ -121,10 +120,7 @@ export const NewJobRequestScreen: React.FC = () => {
{/* Circular countdown timer (pure RN — no SVG needed) */} {/* Circular countdown timer (pure RN — no SVG needed) */}
<View <View
style={[ style={[styles.timerWrapper, { borderColor: timerBorderColor }]}
styles.timerWrapper,
{ borderColor: timerBorderColor },
]}
> >
<Text style={[styles.timerText, { color: timerColor }]}> <Text style={[styles.timerText, { color: timerColor }]}>
{secondsLeft}s {secondsLeft}s
@ -134,10 +130,7 @@ export const NewJobRequestScreen: React.FC = () => {
{/* ── Earnings Badge ─────────────────────────────────────────────── */} {/* ── Earnings Badge ─────────────────────────────────────────────── */}
<View <View
style={[ style={[styles.earningsBadge, { backgroundColor: colors.primary }]}
styles.earningsBadge,
{ backgroundColor: colors.primary },
]}
> >
<Text style={styles.earningsBadgeLabel}>Estimated Earnings</Text> <Text style={styles.earningsBadgeLabel}>Estimated Earnings</Text>
<Text style={styles.earningsBadgeValue}> <Text style={styles.earningsBadgeValue}>
@ -164,9 +157,7 @@ export const NewJobRequestScreen: React.FC = () => {
<View style={styles.detailsRow}> <View style={styles.detailsRow}>
<View style={styles.detailBox}> <View style={styles.detailBox}>
<Text style={styles.detailLabel}>Distance</Text> <Text style={styles.detailLabel}>Distance</Text>
<Text style={styles.detailValue}> <Text style={styles.detailValue}>{currentOffer.distanceKm} km</Text>
{currentOffer.distanceKm} km
</Text>
</View> </View>
<View style={styles.divider} /> <View style={styles.divider} />

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { PrimaryButton } from '@components'; import { PrimaryButton, MapViewComponent } from '@components';
import { changeOrderStatusThunk, useAppDispatch, useAppSelector } from '@store'; import { changeOrderStatusThunk, useAppDispatch, useAppSelector } from '@store';
import { advanceJobStatus } from '@store/commonReducers/job'; import { advanceJobStatus } from '@store/commonReducers/job';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
@ -25,23 +25,40 @@ export const OrderPickedUpScreen: React.FC = () => {
const { activeDeliveries } = useAppSelector(state => state.deliveries); const { activeDeliveries } = useAppSelector(state => state.deliveries);
const activeJob = activeDeliveries?.delivery; const activeJob = activeDeliveries?.delivery;
const handleStartDelivery = () => { const handleStartDelivery = async () => {
dispatch( await dispatch(
changeOrderStatusThunk({ deliveryId: jobId, action: JobStatus.PickedUp }), changeOrderStatusThunk({ deliveryId: jobId, action: JobStatus.PickedUp }),
); )
navigation.navigate(RouteNames.LiveTracking, { .unwrap()
.then(() => {
// Use replace so OrderPickedUp is removed from the stack —
// the user cannot come back to it via the back button.
navigation.replace(RouteNames.LiveTracking, {
jobId: jobId, jobId: jobId,
}); });
})
.catch(err => {
console.log(err, 'err');
});
}; };
if (!activeJob) return null; if (!activeJob) return null;
return ( return (
<View style={styles.container}> <View style={styles.container}>
{/* Map Background Placeholder */} {/* Map Routing Path */}
{activeJob?.order?.pickupAddress && activeJob?.order?.dropAddress ? (
<MapViewComponent
origin={activeJob.order.pickupAddress}
destination={activeJob.order.dropAddress}
showRoute={true}
style={styles.mapPlaceholder}
/>
) : (
<View style={styles.mapPlaceholder}> <View style={styles.mapPlaceholder}>
<Text style={styles.placeholderText}>[ Map Placeholder ]</Text> <Text style={styles.placeholderText}>Loading Map...</Text>
</View> </View>
)}
{/* Deliver Bottom Card */} {/* Deliver Bottom Card */}
<View style={styles.bottomCard}> <View style={styles.bottomCard}>

View File

@ -2,7 +2,7 @@ import React from 'react';
import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native'; import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { PersonIcon, ChevronRightIcon, ClipboardIcon, StarIcon } from '@icons'; import { PersonIcon, ChevronRightIcon, ClipboardIcon, StarIcon } from '@icons';
import { useAppDispatch } from '@store'; import { useAppDispatch, useAppSelector } from '@store';
import { logout } from '@store/commonReducers/auth'; import { logout } from '@store/commonReducers/auth';
import { useNavigation, CommonActions } from '@react-navigation/native'; import { useNavigation, CommonActions } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
@ -14,15 +14,42 @@ export const ProfileScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigation = useNavigation<NativeStackNavigationProp<AppStackParamList>>(); const navigation =
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const { profile } = useAppSelector(state => state.accountInfo);
const menuItems = [ const menuItems = [
{ id: 'personal', title: 'Personal Info', icon: <PersonIcon size={20} color={colors.primary} /> }, {
{ id: 'vehicle', title: 'Vehicle Info', icon: <ClipboardIcon size={20} color={colors.primary} /> }, id: 'personal',
{ id: 'documents', title: 'Documents', icon: <ClipboardIcon size={20} color={colors.primary} />, route: RouteNames.Documents }, title: 'Personal Info',
{ id: 'bank', title: 'Bank Details', icon: <ClipboardIcon size={20} color={colors.primary} /> }, icon: <PersonIcon size={20} color={colors.primary} />,
{ id: 'emergency', title: 'Emergency Contact', icon: <PersonIcon size={20} color={colors.primary} /> }, },
{ id: 'settings', title: 'App Settings', icon: <PersonIcon size={20} color={colors.primary} /> }, {
id: 'vehicle',
title: 'Vehicle Info',
icon: <ClipboardIcon size={20} color={colors.primary} />,
},
{
id: 'documents',
title: 'Documents',
icon: <ClipboardIcon size={20} color={colors.primary} />,
route: RouteNames.Documents,
},
{
id: 'bank',
title: 'Bank Details',
icon: <ClipboardIcon size={20} color={colors.primary} />,
},
{
id: 'emergency',
title: 'Emergency Contact',
icon: <PersonIcon size={20} color={colors.primary} />,
},
{
id: 'settings',
title: 'App Settings',
icon: <PersonIcon size={20} color={colors.primary} />,
},
]; ];
const handleMenuPress = (item: any) => { const handleMenuPress = (item: any) => {
@ -49,32 +76,35 @@ export const ProfileScreen: React.FC = () => {
CommonActions.reset({ CommonActions.reset({
index: 0, index: 0,
routes: [{ name: 'Auth' }], routes: [{ name: 'Auth' }],
}) }),
); );
}, },
}, },
] ],
); );
}; };
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}> <ScrollView
contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}
>
{/* Header User Card */} {/* Header User Card */}
<View style={styles.headerCard}> <View style={styles.headerCard}>
<View style={styles.avatar}> <View style={styles.avatar}>
<PersonIcon size={44} color={colors.primary} /> <PersonIcon size={44} color={colors.primary} />
</View> </View>
<Text style={styles.name}>Rahul Kumar</Text> <Text style={styles.name}>{profile?.user?.name}</Text>
<View style={styles.ratingRow}> <View style={styles.ratingRow}>
<StarIcon size={16} color={colors.warning} /> <StarIcon size={16} color={colors.warning} />
<Text style={styles.ratingText}>4.8</Text> <Text style={styles.ratingText}>{profile?.rating}</Text>
<Text style={styles.tripsText}>(128 Trips)</Text> <Text style={styles.tripsText}>{profile?.user?.email}</Text>
</View> </View>
<Text style={styles.idText}>ID: DP126478</Text> <Text style={styles.idText}>ID: {profile?.user?.id}</Text>
</View> </View>
{/* Profile Menu options */} {/* Profile Menu options */}
@ -82,7 +112,10 @@ export const ProfileScreen: React.FC = () => {
{menuItems.map((item, idx) => ( {menuItems.map((item, idx) => (
<TouchableOpacity <TouchableOpacity
key={item.id} key={item.id}
style={[styles.menuItem, idx === menuItems.length - 1 && { borderBottomWidth: 0 }]} style={[
styles.menuItem,
idx === menuItems.length - 1 && { borderBottomWidth: 0 },
]}
activeOpacity={0.7} activeOpacity={0.7}
onPress={() => handleMenuPress(item)} onPress={() => handleMenuPress(item)}
> >
@ -96,7 +129,11 @@ export const ProfileScreen: React.FC = () => {
</View> </View>
{/* Logout Button */} {/* Logout Button */}
<TouchableOpacity style={styles.logoutBtn} activeOpacity={0.8} onPress={handleLogout}> <TouchableOpacity
style={styles.logoutBtn}
activeOpacity={0.8}
onPress={handleLogout}
>
<Text style={styles.logoutText}>Log Out</Text> <Text style={styles.logoutText}>Log Out</Text>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>

View File

@ -21,16 +21,46 @@ const Stack = createNativeStackNavigator<AppStackParamList>();
const AppStack: React.FC = () => { const AppStack: React.FC = () => {
return ( return (
<Stack.Navigator screenOptions={{ headerShown: false }}> <Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name={RouteNames.BottomTabs} component={BottomTabNavigator} /> <Stack.Screen
<Stack.Screen name={RouteNames.NewJobRequest} component={NewJobRequestScreen} /> name={RouteNames.BottomTabs}
<Stack.Screen name={RouteNames.OrderAccepted} component={OrderAcceptedScreen} /> component={BottomTabNavigator}
<Stack.Screen name={RouteNames.ArrivedAtStore} component={ArrivedAtStoreScreen} /> />
<Stack.Screen name={RouteNames.ConfirmPickup} component={ConfirmPickupScreen} /> <Stack.Screen
<Stack.Screen name={RouteNames.OrderPickedUp} component={OrderPickedUpScreen} /> name={RouteNames.NewJobRequest}
<Stack.Screen name={RouteNames.LiveTracking} component={LiveTrackingScreen} /> component={NewJobRequestScreen}
<Stack.Screen name={RouteNames.ArrivedAtCustomer} component={ArrivedAtCustomerScreen} /> />
<Stack.Screen name={RouteNames.DeliverOrder} component={DeliverOrderScreen} /> <Stack.Screen
<Stack.Screen name={RouteNames.DeliveryCompleted} component={DeliveryCompletedScreen} /> name={RouteNames.OrderAccepted}
component={OrderAcceptedScreen}
/>
<Stack.Screen
name={RouteNames.ArrivedAtStore}
component={ArrivedAtStoreScreen}
/>
<Stack.Screen
name={RouteNames.ConfirmPickup}
component={ConfirmPickupScreen}
/>
<Stack.Screen
name={RouteNames.OrderPickedUp}
component={OrderPickedUpScreen}
/>
<Stack.Screen
name={RouteNames.LiveTracking}
component={LiveTrackingScreen}
/>
<Stack.Screen
name={RouteNames.ArrivedAtCustomer}
component={ArrivedAtCustomerScreen}
/>
<Stack.Screen
name={RouteNames.DeliverOrder}
component={DeliverOrderScreen}
/>
<Stack.Screen
name={RouteNames.DeliveryCompleted}
component={DeliveryCompletedScreen}
/>
<Stack.Screen name={RouteNames.Documents} component={DocumentsScreen} /> <Stack.Screen name={RouteNames.Documents} component={DocumentsScreen} />
</Stack.Navigator> </Stack.Navigator>
); );

View File

@ -15,7 +15,7 @@ const STORAGE_KEYS = {
} as const; } as const;
// ─── Config ────────────────────────────────────────────────────────────────── // ─── Config ──────────────────────────────────────────────────────────────────
const BASE_URL = 'https://157a-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL const BASE_URL = 'https://53a9-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
// ─── Token Helpers ─────────────────────────────────────────────────────────── // ─── Token Helpers ───────────────────────────────────────────────────────────
export const tokenManager = { export const tokenManager = {

View File

@ -247,3 +247,134 @@ export const getCurrentLocationWithAddress =
return { coords, address }; return { coords, address };
}; };
// ---------------------------------------------------------------------------
// Route & Distance helpers
// ---------------------------------------------------------------------------
/**
* Decodes a Google Maps encoded polyline string.
* @param encoded - The encoded polyline string.
*/
export const decodePolyline = (
encoded: string,
): { latitude: number; longitude: number }[] => {
const points: { latitude: number; longitude: number }[] = [];
let index = 0;
const len = encoded.length;
let lat = 0;
let lng = 0;
while (index < len) {
let b;
let shift = 0;
let result = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
const dlat = result & 1 ? ~(result >> 1) : result >> 1;
lat += dlat;
shift = 0;
result = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
const dlng = result & 1 ? ~(result >> 1) : result >> 1;
lng += dlng;
points.push({
latitude: lat / 1e5,
longitude: lng / 1e5,
});
}
return points;
};
/**
* Fetch directions coordinates between origin and destination using the Google Maps Directions API.
* Falls back to a straight line [origin, destination] if the request fails.
*/
export const getRouteDirections = async (
origin: { latitude: number; longitude: number },
destination: { latitude: number; longitude: number },
): Promise<{ latitude: number; longitude: number }[]> => {
// 1. Try Google Maps Directions API first
try {
const url =
`https://maps.googleapis.com/maps/api/directions/json` +
`?origin=${origin.latitude},${origin.longitude}` +
`&destination=${destination.latitude},${destination.longitude}` +
`&key=${GOOGLE_MAPS_API_KEY}`;
const response = await fetch(url);
const data = await response.json();
if (data.status === 'OK' && data.routes && data.routes.length > 0) {
console.log('Successfully fetched Google Directions');
return decodePolyline(data.routes[0].overview_polyline.points);
} else {
console.warn(
'Google Maps Directions status not OK:',
data.status,
data.error_message || '',
);
throw new Error(`Google Directions status: ${data.status}`);
}
} catch (error) {
console.error(
'Error fetching Google Directions, trying OSRM fallback:',
error,
);
}
// 2. Fallback to OpenStreetMap OSRM API (completely free and no key required)
try {
const osmUrl = `https://router.project-osrm.org/route/v1/driving/${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}?overview=full&geometries=geojson`;
const response = await fetch(osmUrl);
const data = await response.json();
if (data.code === 'Ok' && data.routes && data.routes.length > 0) {
console.log('Successfully fetched OSRM Directions');
return data.routes[0].geometry.coordinates.map(
(point: [number, number]) => ({
latitude: point[1],
longitude: point[0],
}),
);
} else {
console.warn('OSM OSRM Routing status not Ok:', data.code);
}
} catch (error) {
console.error('Error fetching OSRM directions:', error);
}
// 3. Ultimate Fallback to straight line
return [origin, destination];
};
/**
* Calculate the distance between two coordinates in kilometers using the Haversine formula.
*/
export const getHaversineDistance = (
coords1: { latitude: number; longitude: number },
coords2: { latitude: number; longitude: number },
): number => {
const toRad = (value: number) => (value * Math.PI) / 180;
const R = 6371; // Earth's radius in km
const dLat = toRad(coords2.latitude - coords1.latitude);
const dLon = toRad(coords2.longitude - coords1.longitude);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(coords1.latitude)) *
Math.cos(toRad(coords2.latitude)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in km
};

View File

@ -6,7 +6,7 @@ import { setDeliveryOffer } from '@store/commonReducers/delivery';
import { RouteNames } from '@utils/constants'; import { RouteNames } from '@utils/constants';
// Must match the REST API base URL (without trailing slash) // Must match the REST API base URL (without trailing slash)
const BASE_URL = 'https://157a-202-8-116-13.ngrok-free.app'; const BASE_URL = 'https://53a9-202-8-116-13.ngrok-free.app';
type Callback = (data: any) => void; type Callback = (data: any) => void;