feat: implement delivery workflow screens, state management, and API services for order tracking and fulfillment

This commit is contained in:
Tamojit Biswas 2026-07-14 16:34:02 +05:30
parent 98e28742de
commit 55bab67d91
25 changed files with 641 additions and 266 deletions

10
app/api/accountApi.ts Normal file
View File

@ -0,0 +1,10 @@
import { apiClient } from '@services';
import { DeliveryPartnerProfileResponse } from '@interfaces';
export const getAccountInfo =
async (): Promise<DeliveryPartnerProfileResponse> => {
const response = await apiClient.get<DeliveryPartnerProfileResponse>(
'/account-info',
);
return response;
};

View File

@ -2,7 +2,9 @@ import {
DeliveryAction, DeliveryAction,
RespondToOfferResponse, RespondToOfferResponse,
PendingOfferResponse, PendingOfferResponse,
ActiveOrdersResponse, PickupOrderResponse,
DeliveryDetailsResponse,
OrderHistoryResponse,
} from '@interfaces'; } from '@interfaces';
import { apiClient } from '@services'; import { apiClient } from '@services';
@ -32,9 +34,29 @@ export const getPendingOffer = async (): Promise<PendingOfferResponse> => {
return response; return response;
}; };
export const getActiveDeliveries = async (): Promise<ActiveOrdersResponse> => { export const getActiveDeliveries =
const response = await apiClient.get<ActiveOrdersResponse>( async (): Promise<DeliveryDetailsResponse> => {
'/delivery-partners/deliveries/active', const response = await apiClient.get<DeliveryDetailsResponse>(
'/delivery-partners/deliveries/active',
);
return response;
};
export const changeOrderStatus = async (
deliveryId: string,
action: string,
proofKey?: string,
): Promise<PickupOrderResponse> => {
const response = await apiClient.post<PickupOrderResponse>(
`/delivery-partners/deliveries/${deliveryId}/status`,
{ action, proofKey },
);
return response;
};
export const completedDeliveries = async (): Promise<OrderHistoryResponse> => {
const response = await apiClient.get<OrderHistoryResponse>(
'/delivery-partners/deliveries/history',
); );
return response; return response;
}; };

View File

@ -3,3 +3,4 @@ export * from './onBoardApi';
export * from './kycApi'; export * from './kycApi';
export * from './dashboardApi'; export * from './dashboardApi';
export * from './deliveryApi'; export * from './deliveryApi';
export * from './accountApi';

View File

@ -12,15 +12,16 @@ import { getStyles } from './arrivedAtCustomerScreen.styles';
export const ArrivedAtCustomerScreen: React.FC = () => { export const ArrivedAtCustomerScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const navigation = useNavigation<NativeStackNavigationProp<AppStackParamList>>(); const navigation =
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const activeJob = useAppSelector((state) => state.job.activeJob); const activeJob = useAppSelector(state => state.job.activeJob);
const handleConfirmArrival = () => { const handleConfirmArrival = () => {
navigation.navigate(RouteNames.DeliverOrder); // navigation.navigate(RouteNames.DeliverOrder);
}; };
if (!activeJob) return null; // if (!activeJob) return null;
return ( return (
<View style={styles.container}> <View style={styles.container}>
@ -32,8 +33,8 @@ export const ArrivedAtCustomerScreen: React.FC = () => {
{/* Arrived Card */} {/* Arrived Card */}
<View style={styles.bottomCard}> <View style={styles.bottomCard}>
<Text style={styles.cardTitle}>You've arrived</Text> <Text style={styles.cardTitle}>You've arrived</Text>
<Text style={styles.customerName}>{activeJob.dropName}</Text> <Text style={styles.customerName}>{activeJob?.dropName}</Text>
<Text style={styles.customerAddress}>{activeJob.dropAddress}</Text> <Text style={styles.customerAddress}>{activeJob?.dropAddress}</Text>
<PrimaryButton <PrimaryButton
title="Confirm Arrival" title="Confirm Arrival"

View File

@ -2,35 +2,46 @@ import React, { useState } from 'react';
import { View, Text, ScrollView, Alert } from 'react-native'; import { View, Text, ScrollView, Alert } from 'react-native';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { ScreenHeader, PrimaryButton } from '@components'; import { ScreenHeader, PrimaryButton } from '@components';
import { useAppDispatch, useAppSelector } from '@store'; import { changeOrderStatusThunk, useAppDispatch, useAppSelector } from '@store';
import { completeJob } from '@store/commonReducers/job'; import { completeJob } from '@store/commonReducers/job';
import { useNavigation } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } 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 { JobStatus, RouteNames } from '@utils/constants';
import { getStyles } from './deliverOrderScreen.styles'; import { getStyles } from './deliverOrderScreen.styles';
export const DeliverOrderScreen: React.FC = () => { export const DeliverOrderScreen: 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 route =
useRoute<RouteProp<AppStackParamList, RouteNames.OrderPickedUp>>();
const { jobId } = route.params;
const { activeDeliveries } = useAppSelector(state => state.deliveries);
const activeJob = activeDeliveries?.delivery;
const activeJob = useAppSelector((state) => state.job.activeJob);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const items = [ // const items = [
{ id: 0, name: 'Cappuccino (Medium)', qty: 1 }, // { id: 0, name: 'Cappuccino (Medium)', qty: 1 },
{ id: 1, name: 'Chocolate Croissant', qty: 1 }, // { id: 1, name: 'Chocolate Croissant', qty: 1 },
]; // ];
const handleCompleteDelivery = () => { const handleCompleteDelivery = () => {
setIsLoading(true); dispatch(
setTimeout(() => { changeOrderStatusThunk({
setIsLoading(false); deliveryId: jobId,
dispatch(completeJob()); action: JobStatus.Delivered,
navigation.navigate(RouteNames.DeliveryCompleted); }),
}, 1200); )
.unwrap()
.then(() => {
// dispatch(completeJob());
navigation.navigate(RouteNames.DeliveryCompleted);
});
}; };
if (!activeJob) return null; if (!activeJob) return null;
@ -43,34 +54,46 @@ export const DeliverOrderScreen: React.FC = () => {
onBack={() => navigation.goBack()} onBack={() => navigation.goBack()}
/> />
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}> <ScrollView
contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}
>
{/* Customer Detail Card */} {/* Customer Detail Card */}
<View style={styles.card}> <View style={styles.card}>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>Customer Name</Text> <Text style={styles.label}>Customer Name</Text>
<Text style={styles.value}>{activeJob.dropName}</Text> <Text style={styles.value}>
{activeJob?.order?.customer?.user?.name}
</Text>
</View> </View>
<View style={[styles.row, { marginTop: 10 }]}> <View style={[styles.row, { marginTop: 10 }]}>
<Text style={styles.label}>Order ID</Text> <Text style={styles.label}>Order ID</Text>
<Text style={styles.value}>{activeJob.orderId}</Text> <Text style={styles.value}>{activeJob?.orderId}</Text>
</View> </View>
</View> </View>
{/* Items Summary list */} {/* Items Summary list */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Items Details</Text> <Text style={styles.sectionTitle}>Items Details</Text>
{items.map((item) => ( {activeJob?.order?.orderItems?.map(item => (
<View key={item.id} style={styles.itemRow}> <View key={item.id} style={styles.itemRow}>
<Text style={styles.itemName}>{item.name}</Text> <Text style={styles.itemName}>{item.name}</Text>
<Text style={styles.itemQty}>x{item.qty}</Text> <Text style={styles.itemQty}>x{item.quantity}</Text>
</View> </View>
))} ))}
</View> </View>
{/* Payment Collect Details */} {/* Payment Collect Details */}
<View style={styles.paymentCard}> <View style={styles.paymentCard}>
<Text style={styles.paymentLabel}>Cash to Collect</Text> <Text style={styles.paymentLabel}>
<Text style={styles.paymentVal}>0.00 (Online Paid)</Text> {activeJob?.order?.paymentMethod === 'WALLET' ||
activeJob?.order?.paymentMethod === 'UPI'
? 'Already Paid'
: 'Collect from Customer'}
</Text>
<Text style={styles.paymentVal}>
{activeJob?.order?.totalAmount}
</Text>
</View> </View>
<PrimaryButton <PrimaryButton

View File

@ -6,7 +6,6 @@ export * from './dashboardScreen/dashboardScreen';
export * from './earningsScreen/earningsScreen'; export * from './earningsScreen/earningsScreen';
export * from './jobsScreen/jobsScreen'; export * from './jobsScreen/jobsScreen';
export * from './inboxScreen/inboxScreen'; export * from './inboxScreen/inboxScreen';
export * from './profileScreen/profileScreen';
export * from './documentsScreen/documentsScreen'; export * from './documentsScreen/documentsScreen';
export * from './newJobRequestScreen/newJobRequestScreen'; export * from './newJobRequestScreen/newJobRequestScreen';
export * from './orderAcceptedScreen/orderAcceptedScreen'; export * from './orderAcceptedScreen/orderAcceptedScreen';
@ -20,3 +19,4 @@ export * from './deliveryCompletedScreen/deliveryCompletedScreen';
export * from './setLocationScreen'; export * from './setLocationScreen';
export * from './completeProfileScreen'; export * from './completeProfileScreen';
export * from './kycScreen'; export * from './kycScreen';
export * from './profileScreen';

View File

@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity } from 'react-native'; import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { OrderCard, EmptyState } from '@components'; import { OrderCard, EmptyState } from '@components';
import { useAppDispatch, useAppSelector } from '@store'; import { useAppDispatch, useAppSelector } from '@store';
import { ClipboardIcon } from '@icons'; import { ClipboardIcon } from '@icons';
import { getStyles } from './jobsScreen.styles'; import { getStyles } from './jobsScreen.styles';
import { getAllActiveDeliveries } from './thunk'; import { getAllActiveDeliveries, getCompletedDeliveries } from './thunk';
import { useNavigation } from '@react-navigation/native'; 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';
@ -20,45 +20,52 @@ export const JobsScreen: React.FC = () => {
const [activeTab, setActiveTab] = useState<'active' | 'history'>('active'); const [activeTab, setActiveTab] = useState<'active' | 'history'>('active');
const { activeJob, jobHistory } = useAppSelector(state => state.job); const { activeJob, jobHistory } = useAppSelector(state => state.job);
const { activeDeliveries } = useAppSelector(state => state.deliveries); const { activeDeliveries, completedDeliveries } = useAppSelector(
state => state.deliveries,
);
useEffect(() => { // console.log(activeDeliveries, 'activeDeliveries');
dispatch(getAllActiveDeliveries());
}, [dispatch]); useFocusEffect(
useCallback(() => {
dispatch(getAllActiveDeliveries());
dispatch(getCompletedDeliveries());
}, [dispatch]),
);
// Default history mock jobs // Default history mock jobs
const defaultHistory = [ // const defaultHistory = [
{ // {
pickupName: 'Cafe Coffee Day', // pickupName: 'Cafe Coffee Day',
pickupAddress: 'MG Road, Bengaluru', // pickupAddress: 'MG Road, Bengaluru',
dropName: 'Rohit Sharma', // dropName: 'Rohit Sharma',
dropAddress: 'Koramangala 4th Block, Bengaluru', // dropAddress: 'Koramangala 4th Block, Bengaluru',
amount: '78.0', // amount: '78.0',
distance: '1.2', // distance: '1.2',
itemCount: 2, // itemCount: 2,
paymentType: 'online', // paymentType: 'online',
}, // },
{ // {
pickupName: 'Pizza Hut', // pickupName: 'Pizza Hut',
pickupAddress: 'Indiranagar, Bengaluru', // pickupAddress: 'Indiranagar, Bengaluru',
dropName: 'Aishwarya Sen', // dropName: 'Aishwarya Sen',
dropAddress: 'Domlur Stage 2, Bengaluru', // dropAddress: 'Domlur Stage 2, Bengaluru',
amount: '95.0', // amount: '95.0',
distance: '3.8', // distance: '3.8',
itemCount: 3, // itemCount: 3,
paymentType: 'cod', // paymentType: 'cod',
}, // },
{ // {
pickupName: 'Burgers & Co', // pickupName: 'Burgers & Co',
pickupAddress: 'Koramangala 5th Block', // pickupAddress: 'Koramangala 5th Block',
dropName: 'Sameer Khan', // dropName: 'Sameer Khan',
dropAddress: 'HSR Layout Sector 3, Bengaluru', // dropAddress: 'HSR Layout Sector 3, Bengaluru',
amount: '110.0', // amount: '110.0',
distance: '4.5', // distance: '4.5',
itemCount: 1, // itemCount: 1,
paymentType: 'online', // paymentType: 'online',
}, // },
]; // ];
return ( return (
<View style={styles.container}> <View style={styles.container}>
@ -105,22 +112,30 @@ export const JobsScreen: React.FC = () => {
> >
{activeTab === 'active' ? ( {activeTab === 'active' ? (
// Active job tab // Active job tab
activeDeliveries ? ( activeDeliveries?.delivery ? (
<View style={styles.cardWrapper}> <View style={styles.cardWrapper}>
<OrderCard <OrderCard
pickupName={activeDeliveries?.order?.pickupAddress?.city} pickupName={
pickupAddress={ activeDeliveries?.delivery?.order?.pickupAddress?.city
activeDeliveries?.order?.pickupAddress?.addressLine1
} }
dropName={activeDeliveries?.order?.dropAddress?.city} pickupAddress={
dropAddress={activeDeliveries?.order?.dropAddress?.addressLine1} activeDeliveries?.delivery?.order?.pickupAddress?.addressLine1
amount={activeDeliveries?.order?.totalAmount} }
distance={activeDeliveries?.estimatedDistanceKm} dropName={activeDeliveries?.delivery?.order?.dropAddress?.city}
itemCount={activeDeliveries?.order?.orderItems?.length} dropAddress={
paymentType={activeDeliveries?.order?.paymentMethod} activeDeliveries?.delivery?.order?.dropAddress?.addressLine1
}
amount={activeDeliveries?.delivery?.order?.totalAmount}
distance={
activeDeliveries?.delivery?.estimatedDistanceKm || '0'
}
itemCount={
activeDeliveries?.delivery?.order?.orderItems?.length
}
paymentType={activeDeliveries?.delivery?.order?.paymentMethod}
onPress={() => onPress={() =>
navigation.navigate(RouteNames.ArrivedAtStore, { navigation.navigate(RouteNames.OrderPickedUp, {
jobId: activeDeliveries?.orderId, jobId: activeDeliveries?.delivery?.id,
}) })
} }
/> />
@ -136,35 +151,29 @@ export const JobsScreen: React.FC = () => {
) : ( ) : (
// History tab // History tab
<View> <View>
{jobHistory.length > 0 {completedDeliveries && completedDeliveries?.length > 0 ? (
? jobHistory.map((job, idx) => ( completedDeliveries?.map((job, idx) => (
<View key={idx} style={styles.cardWrapper}> <View key={idx} style={styles.cardWrapper}>
<OrderCard <OrderCard
pickupName={job.pickupName} pickupName={
pickupAddress={job.pickupAddress} job.order.merchant.name || job.order.pickupAddress.city
dropName={job.dropName} }
dropAddress={job.dropAddress} pickupAddress={job.order.pickupAddress.addressLine1}
amount={job.amount} dropName={job.order.dropAddress.city}
distance={job.distance} dropAddress={job.order.dropAddress.addressLine1}
itemCount={job.itemCount} amount={job.order.totalAmount}
paymentType={job.paymentType} distance={job.estimatedDistanceKm?.toString() || '0'}
/> // itemCount={job.order?.}
</View> // paymentType={job.order.paymentMethod}
)) />
: defaultHistory.map((job, idx) => ( </View>
<View key={idx} style={styles.cardWrapper}> ))
<OrderCard ) : (
pickupName={job.pickupName} <View style={styles.emptyStateContainer}>
pickupAddress={job.pickupAddress} <ClipboardIcon size={48} color={colors.placeholder} />
dropName={job.dropName} <Text style={styles.emptyStateText}>No history found</Text>
dropAddress={job.dropAddress} </View>
amount={job.amount} )}
distance={job.distance}
itemCount={job.itemCount}
paymentType={job.paymentType}
/>
</View>
))}
</View> </View>
)} )}
</ScrollView> </ScrollView>

View File

@ -1,17 +1,19 @@
import { ActiveOrdersResponse } from '@interfaces'; import { DeliveryDetailsResponse, OrderHistoryResponse } from '@interfaces';
import { createReducer } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit';
import { getAllActiveDeliveries } from './thunk'; import { getAllActiveDeliveries, getCompletedDeliveries } from './thunk';
export interface ActiveDeliveriesState { export interface ActiveDeliveriesState {
activeDeliveries: ActiveOrdersResponse | null; activeDeliveries: DeliveryDetailsResponse | null;
loading: boolean; loading: boolean;
error: string | null; error: string | null;
completedDeliveries: OrderHistoryResponse | null;
} }
const initialState: ActiveDeliveriesState = { const initialState: ActiveDeliveriesState = {
activeDeliveries: null, activeDeliveries: null,
loading: false, loading: false,
error: null, error: null,
completedDeliveries: null,
}; };
export const activeDeliveriesReducer = createReducer(initialState, builder => { export const activeDeliveriesReducer = createReducer(initialState, builder => {
@ -27,5 +29,16 @@ export const activeDeliveriesReducer = createReducer(initialState, builder => {
.addCase(getAllActiveDeliveries.rejected, (state, action) => { .addCase(getAllActiveDeliveries.rejected, (state, action) => {
state.loading = false; state.loading = false;
state.error = action.payload as string; state.error = action.payload as string;
})
.addCase(getCompletedDeliveries.pending, state => {
state.loading = true;
})
.addCase(getCompletedDeliveries.fulfilled, (state, action) => {
state.loading = false;
state.completedDeliveries = action.payload;
})
.addCase(getCompletedDeliveries.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
}); });
}); });

View File

@ -1,4 +1,4 @@
import { getActiveDeliveries } from '@api'; import { completedDeliveries, getActiveDeliveries } from '@api';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
export const getAllActiveDeliveries = createAsyncThunk( export const getAllActiveDeliveries = createAsyncThunk(
@ -12,3 +12,15 @@ export const getAllActiveDeliveries = createAsyncThunk(
} }
}, },
); );
export const getCompletedDeliveries = createAsyncThunk(
'jobs/getCompletedDeliveries',
async (_, { rejectWithValue }) => {
try {
const response = await completedDeliveries();
return response;
} catch (error) {
return rejectWithValue(error);
}
},
);

View File

@ -4,7 +4,7 @@ import { useAppTheme } from '@theme';
import { PrimaryButton } from '@components'; import { PrimaryButton } 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 { useNavigation } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } 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, JobStatus } from '@utils/constants'; import { RouteNames, JobStatus } from '@utils/constants';
@ -14,27 +14,41 @@ export const LiveTrackingScreen: 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 route =
useRoute<RouteProp<AppStackParamList, RouteNames.OrderPickedUp>>();
const { jobId } = route.params;
const activeJob = useAppSelector((state) => state.job.activeJob); const { activeDeliveries } = useAppSelector(state => state.deliveries);
const activeJob = activeDeliveries?.delivery;
const handleArrived = () => { const handleArrived = () => {
dispatch(advanceJobStatus(JobStatus.ArrivedAtCustomer)); dispatch(advanceJobStatus(JobStatus.ArrivedAtCustomer));
navigation.navigate(RouteNames.ArrivedAtCustomer); navigation.navigate(RouteNames.DeliverOrder, {
jobId: jobId,
});
}; };
const handleContact = () => { const handleContact = () => {
Alert.alert('Contact Customer', `Calling ${activeJob?.dropName || 'Customer'} (+91 99887 76655)...`); Alert.alert(
'Contact Customer',
`Calling ${activeJob?.order?.customer?.user?.name || 'Customer'} at ${
activeJob?.order?.customer?.user?.phone
} `,
);
}; };
if (!activeJob) return null; // if (!activeJob) return 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}>Arriving in 8 mins</Text>
<Text style={styles.etaSub}>Distance: {activeJob.distance} km Speed: 24 km/h</Text> <Text style={styles.etaSub}>
Distance: {activeJob?.estimatedDistanceKm} km Speed: 24 km/h
</Text>
</View> </View>
{/* Map Background Placeholder */} {/* Map Background Placeholder */}
@ -45,8 +59,14 @@ export const LiveTrackingScreen: React.FC = () => {
{/* Customer Location Bottom Card */} {/* Customer Location Bottom Card */}
<View style={styles.bottomCard}> <View style={styles.bottomCard}>
<Text style={styles.cardTitle}>Deliver to</Text> <Text style={styles.cardTitle}>Deliver to</Text>
<Text style={styles.customerName}>{activeJob.dropName}</Text> <Text style={styles.customerName}>
<Text style={styles.customerAddress}>{activeJob.dropAddress}</Text> {activeJob?.order?.customer?.user?.name}
</Text>
<Text style={styles.customerAddress}>
{activeJob?.order?.dropAddress?.houseNumber +
', ' +
activeJob?.order?.dropAddress?.addressLine1}
</Text>
<View style={styles.btnRow}> <View style={styles.btnRow}>
<PrimaryButton <PrimaryButton

View File

@ -2,9 +2,9 @@ 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 } from '@components';
import { useAppDispatch, useAppSelector } from '@store'; import { changeOrderStatusThunk, useAppDispatch, useAppSelector } from '@store';
import { advanceJobStatus } from '@store/commonReducers/job'; import { advanceJobStatus } from '@store/commonReducers/job';
import { useNavigation } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } 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, JobStatus } from '@utils/constants'; import { RouteNames, JobStatus } from '@utils/constants';
@ -14,13 +14,24 @@ export const OrderPickedUpScreen: 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 route =
useRoute<RouteProp<AppStackParamList, RouteNames.OrderPickedUp>>();
const { jobId } = route.params;
// console.log(jobId, 'jobId');
const navigation =
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
const activeJob = useAppSelector((state) => state.job.activeJob); // const activeJob = useAppSelector(state => state.job.activeJob);
const { activeDeliveries } = useAppSelector(state => state.deliveries);
const activeJob = activeDeliveries?.delivery;
const handleStartDelivery = () => { const handleStartDelivery = () => {
dispatch(advanceJobStatus(JobStatus.EnRoute)); dispatch(
navigation.navigate(RouteNames.LiveTracking); changeOrderStatusThunk({ deliveryId: jobId, action: JobStatus.PickedUp }),
);
navigation.navigate(RouteNames.LiveTracking, {
jobId: jobId,
});
}; };
if (!activeJob) return null; if (!activeJob) return null;
@ -35,8 +46,18 @@ export const OrderPickedUpScreen: React.FC = () => {
{/* Deliver Bottom Card */} {/* Deliver Bottom Card */}
<View style={styles.bottomCard}> <View style={styles.bottomCard}>
<Text style={styles.cardTitle}>Deliver to Customer</Text> <Text style={styles.cardTitle}>Deliver to Customer</Text>
<Text style={styles.customerName}>{activeJob.dropName}</Text> <Text style={styles.customerName}>
<Text style={styles.customerAddress}>{activeJob.dropAddress}</Text> {activeJob?.order?.customer?.user?.name}
</Text>
<Text style={styles.customerAddress}>
{activeJob?.order?.dropAddress?.houseNumber},{' '}
{activeJob?.order?.dropAddress?.addressLine1}
{activeJob?.order?.dropAddress?.city},{' '}
{activeJob?.order?.dropAddress?.state},
{activeJob?.order?.dropAddress?.postalCode}
{'\n'}
{activeJob?.order?.customer?.user?.phone}
</Text>
<PrimaryButton <PrimaryButton
title="Start Delivery" title="Start Delivery"

View File

@ -0,0 +1,4 @@
export * from './profileScreen';
export * from './profileScreen.styles';
export * from './thunk';
export * from './reducer';

View File

@ -0,0 +1,31 @@
import { createReducer, createSlice } from '@reduxjs/toolkit';
import { getAccountInfoThunk } from './thunk';
import { DeliveryPartnerProfileResponse } from '@interfaces';
interface ProfileState {
profile: DeliveryPartnerProfileResponse | null;
loading: boolean;
error: string | null;
}
const initialState: ProfileState = {
profile: null,
loading: false,
error: null,
};
export const profileReducer = createReducer(initialState, builder => {
builder
.addCase(getAccountInfoThunk.pending, state => {
state.loading = true;
state.error = null;
})
.addCase(getAccountInfoThunk.fulfilled, (state, action) => {
state.loading = false;
state.profile = action.payload;
})
.addCase(getAccountInfoThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
});

View File

@ -0,0 +1,14 @@
import { getAccountInfo } from '@api';
import { createAsyncThunk } from '@reduxjs/toolkit';
export const getAccountInfoThunk = createAsyncThunk(
'account/getAccountInfo',
async (_, { rejectWithValue }) => {
try {
const response = await getAccountInfo();
return response;
} catch (error) {
return rejectWithValue(error);
}
},
);

View File

@ -0,0 +1,34 @@
export interface DeliveryPartnerProfileResponse {
id: string;
userId: string;
partnerCode: string;
kycStatus: string;
status: string;
availabilityStatus: string;
rating: string;
currentLatitude: string;
currentLongitude: string;
lastLocationAt: string;
user: DeliveryPartnerUser;
vehicles: DeliveryPartnerVehicle[];
completedDeliveries: number;
}
export interface DeliveryPartnerUser {
id: string;
name: string;
email: string;
phone: string;
profileImage: string | null;
roleEnum: string;
}
export interface DeliveryPartnerVehicle {
id: string;
deliveryPartnerId: string;
vehicleNumber: string;
vehicleType: string;
status: string;
createdAt: string;
updatedAt: string;
}

View File

@ -4,3 +4,4 @@ export * from './kyc';
export * from './dashboard'; export * from './dashboard';
export * from './delivery'; export * from './delivery';
export * from './order'; export * from './order';
export * from './accountInfo';

View File

@ -1,112 +1,224 @@
export interface ActiveOrdersResponse { export interface DeliveryDetailsResponse {
delivery: Delivery;
}
export interface Delivery {
id: string; id: string;
orderId: string; orderId: string;
deliveryPartnerId: string; deliveryPartnerId: string;
deliveryNumber: string; deliveryNumber: string;
status: string; status: DeliveryStatus;
estimatedDistanceKm: string; estimatedDistanceKm: string | null;
actualDistanceKm: string; actualDistanceKm: string | null;
proofOfPickupKey: string; proofOfPickupKey: string | null;
proofOfDeliveryKey: string; proofOfDeliveryKey: string | null;
pickedUpAt: string; pickedUpAt: string | null;
deliveredAt: string; deliveredAt: string | null;
order: { order: DeliveryOrder;
id: string;
orderNumber: string;
customerId: string;
merchantId: string;
couponCode: string;
appliedPromotionId: string;
status: string;
orderType: string;
paymentMethodId: string;
paymentMethod: string;
currency: string;
subtotal: string;
deliveryFee: string;
platformFee: string;
serviceFee: string;
taxAmount: string;
discountAmount: string;
totalAmount: string;
pickupAddress: {
city: string;
label: string;
phone: string;
state: string;
landmark: string;
latitude: number;
longitude: number;
postalCode: string;
houseNumber: string;
addressLine1: string;
};
dropAddress: {
city: string;
label: string;
phone: string;
state: string;
landmark: string;
latitude: number;
longitude: number;
postalCode: string;
houseNumber: string;
addressLine1: string;
};
// "metadata": {
// "idempotencyKey": "idemp-key-12345",
// "trackingHistory": {
// "CONFIRMED": "2026-07-13T10:21:35.863Z"
// },
// "rejectedDriverIds": [
// "e5f639a8-f0cf-4357-ab45-919ebc3012bd"
// ]
// },
version: 1;
createdAt: string;
updatedAt: string;
deletedAt: string;
customer: {
id: string;
userId: string;
customerCode: string;
createdAt: string;
deletedAt: string;
user: {
id: string;
name: string;
phone: string;
profileImage: string;
};
};
// "merchant": {
// "id": "f53b10bf-1d72-42e6-9d4e-b78032ad051b",
// "name": "eererer",
// "imageUrl": null,
// "addresses": [
// {
// "id": "5476dd01-4b9c-4597-ad0f-1898ad333af6",
// "mapAddress": "Bantra, Jaynagar - I, South 24 Parganas, West Bengal, India",
// "latitude": "22.204311",
// "longitude": "88.509444",
// "city": "New York",
// "landmark": "Near Central Park",
// "phone": "+19999999999"
// }
// ]
// },
orderItems: OrderItem[];
};
} }
export interface OrderItem { export type DeliveryStatus =
| 'PENDING'
| 'ACCEPTED'
| 'ARRIVED_AT_PICKUP'
| 'OUT_FOR_DELIVERY'
| 'PICKED_UP'
| 'IN_TRANSIT'
| 'ARRIVED_AT_DROPOFF'
| 'DELIVERED'
| 'CANCELLED';
export interface DeliveryOrder {
id: string;
orderNumber: string;
customerId: string;
merchantId: string;
couponCode: string | null;
appliedPromotionId: string | null;
status: OrderStatus;
orderType: OrderType;
paymentMethodId: string;
paymentMethod: string;
currency: string;
subtotal: string;
deliveryFee: string;
platformFee: string;
serviceFee: string;
taxAmount: string;
discountAmount: string;
totalAmount: string;
pickupAddress: DeliveryAddress;
dropAddress: DeliveryAddress;
metadata: DeliveryMetadata;
version: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
customer: DeliveryCustomer;
merchant: DeliveryMerchant;
orderItems: DeliveryOrderItem[];
}
export type OrderStatus =
| 'PLACED'
| 'CONFIRMED'
| 'PREPARING'
| 'READY_FOR_PICKUP'
| 'OUT_FOR_DELIVERY'
| 'DELIVERED'
| 'CANCELLED';
export type OrderType = 'DELIVERY' | 'PICKUP';
export interface DeliveryAddress {
city: string;
label: string | null;
phone: string;
state: string;
landmark: string;
latitude: number;
longitude: number;
postalCode: string;
houseNumber: string;
addressLine1: string;
}
export interface DeliveryMetadata {
idempotencyKey: string;
trackingHistory: TrackingHistory;
}
export interface TrackingHistory {
PLACED?: string;
CONFIRMED?: string;
PREPARING?: string;
READY_FOR_PICKUP?: string;
PICKED_UP?: string;
OUT_FOR_DELIVERY?: string;
DELIVERED?: string;
CANCELLED?: string;
}
export interface DeliveryCustomer {
id: string;
userId: string;
customerCode: string;
createdAt: string;
deletedAt: string | null;
user: DeliveryUser;
}
export interface DeliveryUser {
id: string;
name: string;
phone: string;
profileImage: string | null;
}
export interface DeliveryMerchant {
id: string;
name: string;
imageUrl: string | null;
addresses: MerchantAddress[];
}
export interface MerchantAddress {
id: string;
mapAddress: string;
latitude: string;
longitude: string;
city: string;
landmark: string;
phone: string;
}
export interface DeliveryOrderItem {
id: string; id: string;
orderId: string; orderId: string;
productId: string; productId: string;
itemType: string; itemType: 'PRODUCT' | 'SERVICE';
name: string; name: string;
quantity: string; quantity: string;
unitPrice: string; unitPrice: string;
totalAmount: string; totalAmount: string;
attributes: object; attributes: Record<string, any>;
}
//pickup
export interface PickupOrderResponse {
message: string;
delivery: PickupDelivery;
}
export interface PickupDelivery {
id: string;
orderId: string;
deliveryPartnerId: string;
deliveryNumber: string;
status: DeliveryStatus;
estimatedDistanceKm: number | null;
actualDistanceKm: number | null;
proofOfPickupKey: string | null;
proofOfDeliveryKey: string | null;
pickedUpAt: string | null;
deliveredAt: string | null;
order: PickupOrder;
}
export interface PickupOrder {
id: string;
orderNumber: string;
dropAddress: DeliveryAddress;
customer: PickupCustomer;
}
export interface PickupCustomer {
id: string;
userId: string;
customerCode: string;
createdAt: string;
deletedAt: string | null;
user: PickupUser;
}
export interface PickupUser {
name: string;
phone: string;
}
//order History
export type OrderHistoryResponse = OrderHistoryItem[];
export interface OrderHistoryItem {
id: string;
orderId: string;
deliveryPartnerId: string;
deliveryNumber: string;
status: DeliveryStatus;
estimatedDistanceKm: number | null;
actualDistanceKm: string | null;
proofOfPickupKey: string | null;
proofOfDeliveryKey: string | null;
pickedUpAt: string | null;
deliveredAt: string | null;
order: OrderHistoryOrder;
}
export interface OrderHistoryOrder {
id: string;
orderNumber: string;
subtotal: string;
deliveryFee: string;
totalAmount: string;
currency: string;
pickupAddress: DeliveryAddress;
dropAddress: DeliveryAddress;
createdAt: string;
merchant: OrderHistoryMerchant;
}
export interface OrderHistoryMerchant {
id: string;
name: string;
imageUrl: string | null;
} }

View File

@ -30,10 +30,10 @@ export type AppStackParamList = {
[RouteNames.OrderAccepted]: undefined; [RouteNames.OrderAccepted]: undefined;
[RouteNames.ArrivedAtStore]: { jobId: string }; [RouteNames.ArrivedAtStore]: { jobId: string };
[RouteNames.ConfirmPickup]: undefined; [RouteNames.ConfirmPickup]: undefined;
[RouteNames.OrderPickedUp]: undefined; [RouteNames.OrderPickedUp]: { jobId: string };
[RouteNames.LiveTracking]: undefined; [RouteNames.LiveTracking]: { jobId: string };
[RouteNames.ArrivedAtCustomer]: { jobId: string }; [RouteNames.ArrivedAtCustomer]: { jobId: string };
[RouteNames.DeliverOrder]: undefined; [RouteNames.DeliverOrder]: { jobId: string };
[RouteNames.DeliveryCompleted]: undefined; [RouteNames.DeliveryCompleted]: undefined;
[RouteNames.Documents]: undefined; [RouteNames.Documents]: undefined;
// [RouteNames.JobDetails]: { jobId: string }; // [RouteNames.JobDetails]: { jobId: string };

View File

@ -15,7 +15,7 @@ const STORAGE_KEYS = {
} as const; } as const;
// ─── Config ────────────────────────────────────────────────────────────────── // ─── Config ──────────────────────────────────────────────────────────────────
const BASE_URL = 'https://8415-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL const BASE_URL = 'https://157a-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

@ -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://8415-202-8-116-13.ngrok-free.app'; const BASE_URL = 'https://157a-202-8-116-13.ngrok-free.app';
type Callback = (data: any) => void; type Callback = (data: any) => void;
@ -30,8 +30,12 @@ class SocketService {
* the store and navigationRef to be injected independently. * the store and navigationRef to be injected independently.
*/ */
bootstrap(store: Store | null, navigationRef: any) { bootstrap(store: Store | null, navigationRef: any) {
if (store) { this.store = store; } if (store) {
if (navigationRef) { this.navigationRef = navigationRef; } this.store = store;
}
if (navigationRef) {
this.navigationRef = navigationRef;
}
} }
/** /**
@ -64,21 +68,18 @@ class SocketService {
}); });
// ── Listen for delivery offers ───────────────────────────────────── // ── Listen for delivery offers ─────────────────────────────────────
this.socket.on( this.socket.on(SocketEvents.DeliveryOffer, (data: DeliveryOffer) => {
SocketEvents.DeliveryOffer, console.log('[Socket] delivery_offer received:', data);
(data: DeliveryOffer) => {
console.log('[Socket] delivery_offer received:', data);
if (this.store) { if (this.store) {
this.store.dispatch(setDeliveryOffer(data)); this.store.dispatch(setDeliveryOffer(data));
} }
// Navigate to NewJobRequest screen regardless of current screen // Navigate to NewJobRequest screen regardless of current screen
if (this.navigationRef?.isReady()) { if (this.navigationRef?.isReady()) {
this.navigationRef.navigate(RouteNames.NewJobRequest); this.navigationRef.navigate(RouteNames.NewJobRequest);
} }
}, });
);
} }
/** /**

View File

@ -1,12 +1,19 @@
import { createReducer, createAction } from '@reduxjs/toolkit'; import { createReducer, createAction } from '@reduxjs/toolkit';
import { DeliveryOffer } from '@interfaces'; import { DeliveryOffer, PickupOrderResponse } from '@interfaces';
import { respondToOfferThunk, checkPendingOfferThunk } from './thunk'; import {
respondToOfferThunk,
checkPendingOfferThunk,
changeOrderStatusThunk,
} from './thunk';
export interface DeliveryState { export interface DeliveryState {
currentOffer: DeliveryOffer | null; currentOffer: DeliveryOffer | null;
respondLoading: boolean; respondLoading: boolean;
respondError: string | null; respondError: string | null;
pendingOfferLoading: boolean; pendingOfferLoading: boolean;
pickupLoading: boolean;
pickupError: string | null;
pickupOrderResponse: PickupOrderResponse | null;
} }
const initialState: DeliveryState = { const initialState: DeliveryState = {
@ -14,6 +21,9 @@ const initialState: DeliveryState = {
respondLoading: false, respondLoading: false,
respondError: null, respondError: null,
pendingOfferLoading: false, pendingOfferLoading: false,
pickupLoading: false,
pickupError: null,
pickupOrderResponse: null,
}; };
/** Dispatched by socketService when a `delivery_offer` event is received. */ /** Dispatched by socketService when a `delivery_offer` event is received. */
@ -57,5 +67,17 @@ export const deliveryReducer = createReducer(initialState, builder => {
}) })
.addCase(checkPendingOfferThunk.rejected, state => { .addCase(checkPendingOfferThunk.rejected, state => {
state.pendingOfferLoading = false; state.pendingOfferLoading = false;
})
.addCase(changeOrderStatusThunk.pending, state => {
state.pickupLoading = true;
state.pickupError = null;
})
.addCase(changeOrderStatusThunk.fulfilled, (state, action) => {
state.pickupLoading = false;
state.pickupOrderResponse = action.payload;
})
.addCase(changeOrderStatusThunk.rejected, (state, action) => {
state.pickupLoading = false;
state.pickupError = action.payload as string;
}); });
}); });

View File

@ -4,7 +4,7 @@ import {
RespondToOfferResponse, RespondToOfferResponse,
PendingOfferResponse, PendingOfferResponse,
} from '@interfaces'; } from '@interfaces';
import * as deliveryApi from '../../../api/deliveryApi'; import { changeOrderStatus, getPendingOffer, respondToOffer } from '@api';
/** /**
* Respond to a delivery offer (ACCEPT / REJECT). * Respond to a delivery offer (ACCEPT / REJECT).
@ -16,7 +16,7 @@ export const respondToOfferThunk = createAsyncThunk<
'delivery/respondToOffer', 'delivery/respondToOffer',
async ({ deliveryId, action }, { rejectWithValue }) => { async ({ deliveryId, action }, { rejectWithValue }) => {
try { try {
return await deliveryApi.respondToOffer(deliveryId, action); return await respondToOffer(deliveryId, action);
} catch (error: any) { } catch (error: any) {
return rejectWithValue( return rejectWithValue(
error.message || 'Failed to respond to delivery offer', error.message || 'Failed to respond to delivery offer',
@ -32,11 +32,27 @@ export const checkPendingOfferThunk = createAsyncThunk<PendingOfferResponse>(
'delivery/checkPendingOffer', 'delivery/checkPendingOffer',
async (_, { rejectWithValue }) => { async (_, { rejectWithValue }) => {
try { try {
return await deliveryApi.getPendingOffer(); return await getPendingOffer();
} catch (error: any) { } catch (error: any) {
return rejectWithValue( return rejectWithValue(error.message || 'Failed to check pending offers');
error.message || 'Failed to check pending offers', }
); },
);
export const changeOrderStatusThunk = createAsyncThunk(
'delivery/changeOrderStatus',
async (
{
deliveryId,
action,
proofKey,
}: { deliveryId: string; action: string; proofKey?: string },
{ rejectWithValue },
) => {
try {
return await changeOrderStatus(deliveryId, action, proofKey);
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to change order status');
} }
}, },
); );

View File

@ -4,7 +4,11 @@ import { earningsReducer } from './commonReducers/earnings';
import { jobReducer } from './commonReducers/job'; import { jobReducer } from './commonReducers/job';
import { deliveryReducer } from './commonReducers/delivery'; import { deliveryReducer } from './commonReducers/delivery';
import setLocationReducer from '@features/screens/setLocationScreen/reducer'; import setLocationReducer from '@features/screens/setLocationScreen/reducer';
import { completeProfileReducer, kycReducer } from '@features/screens'; import {
completeProfileReducer,
kycReducer,
profileReducer,
} from '@features/screens';
import { onBoardingCompleteReducer } from '@features/screens/onboardingCompleteScreen'; import { onBoardingCompleteReducer } from '@features/screens/onboardingCompleteScreen';
import { dashBoardReducer } from '@features/screens/dashboardScreen'; import { dashBoardReducer } from '@features/screens/dashboardScreen';
import { activeDeliveriesReducer } from '@features/screens/jobsScreen'; import { activeDeliveriesReducer } from '@features/screens/jobsScreen';
@ -20,6 +24,7 @@ const rootReducer = combineReducers({
onboardingComplete: onBoardingCompleteReducer, onboardingComplete: onBoardingCompleteReducer,
dashboard: dashBoardReducer, dashboard: dashBoardReducer,
deliveries: activeDeliveriesReducer, deliveries: activeDeliveriesReducer,
accountInfo: profileReducer,
}); });
export type RootState = ReturnType<typeof rootReducer>; export type RootState = ReturnType<typeof rootReducer>;

View File

@ -37,10 +37,10 @@ export enum JobStatus {
NewRequest = 'newRequest', NewRequest = 'newRequest',
Accepted = 'accepted', Accepted = 'accepted',
ArrivedAtStore = 'arrivedAtStore', ArrivedAtStore = 'arrivedAtStore',
PickedUp = 'pickedUp', PickedUp = 'PICKUP',
EnRoute = 'enRoute', EnRoute = 'enRoute',
ArrivedAtCustomer = 'arrivedAtCustomer', ArrivedAtCustomer = 'arrivedAtCustomer',
Delivered = 'delivered', Delivered = 'DELIVER',
} }
export enum SocketEvents { export enum SocketEvents {

View File

@ -1,5 +1,8 @@
export const formatCurrency = (amount: number, currency: string = '₹'): string => { export const formatCurrency = (
return `${currency}${amount.toLocaleString('en-IN', { amount: number | undefined,
currency: string = '₹',
): string => {
return `${currency}${amount?.toLocaleString('en-IN', {
minimumFractionDigits: 0, minimumFractionDigits: 0,
maximumFractionDigits: 2, maximumFractionDigits: 2,
})}`; })}`;
@ -7,7 +10,7 @@ export const formatCurrency = (amount: number, currency: string = '₹'): string
export const formatDistance = (km: number): string => { export const formatDistance = (km: number): string => {
if (km < 1) return `${Math.round(km * 1000)} m`; if (km < 1) return `${Math.round(km * 1000)} m`;
return `${km.toFixed(1)} km`; return `${km?.toFixed(1)} km`;
}; };
export const formatDuration = (seconds: number): string => { export const formatDuration = (seconds: number): string => {