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,
RespondToOfferResponse,
PendingOfferResponse,
ActiveOrdersResponse,
PickupOrderResponse,
DeliveryDetailsResponse,
OrderHistoryResponse,
} from '@interfaces';
import { apiClient } from '@services';
@ -32,9 +34,29 @@ export const getPendingOffer = async (): Promise<PendingOfferResponse> => {
return response;
};
export const getActiveDeliveries = async (): Promise<ActiveOrdersResponse> => {
const response = await apiClient.get<ActiveOrdersResponse>(
'/delivery-partners/deliveries/active',
export const getActiveDeliveries =
async (): Promise<DeliveryDetailsResponse> => {
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;
};

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,19 @@
import { ActiveOrdersResponse } from '@interfaces';
import { DeliveryDetailsResponse, OrderHistoryResponse } from '@interfaces';
import { createReducer } from '@reduxjs/toolkit';
import { getAllActiveDeliveries } from './thunk';
import { getAllActiveDeliveries, getCompletedDeliveries } from './thunk';
export interface ActiveDeliveriesState {
activeDeliveries: ActiveOrdersResponse | null;
activeDeliveries: DeliveryDetailsResponse | null;
loading: boolean;
error: string | null;
completedDeliveries: OrderHistoryResponse | null;
}
const initialState: ActiveDeliveriesState = {
activeDeliveries: null,
loading: false,
error: null,
completedDeliveries: null,
};
export const activeDeliveriesReducer = createReducer(initialState, builder => {
@ -27,5 +29,16 @@ export const activeDeliveriesReducer = createReducer(initialState, builder => {
.addCase(getAllActiveDeliveries.rejected, (state, action) => {
state.loading = false;
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';
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 { useAppDispatch, useAppSelector } from '@store';
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 { AppStackParamList } from '@navigation/navigationTypes';
import { RouteNames, JobStatus } from '@utils/constants';
@ -14,27 +14,41 @@ export const LiveTrackingScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
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 = () => {
dispatch(advanceJobStatus(JobStatus.ArrivedAtCustomer));
navigation.navigate(RouteNames.ArrivedAtCustomer);
navigation.navigate(RouteNames.DeliverOrder, {
jobId: jobId,
});
};
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 (
<View style={styles.container}>
{/* Top Floating ETA Card */}
<View style={styles.etaCard}>
<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>
{/* Map Background Placeholder */}
@ -45,8 +59,14 @@ export const LiveTrackingScreen: React.FC = () => {
{/* Customer Location Bottom Card */}
<View style={styles.bottomCard}>
<Text style={styles.cardTitle}>Deliver to</Text>
<Text style={styles.customerName}>{activeJob.dropName}</Text>
<Text style={styles.customerAddress}>{activeJob.dropAddress}</Text>
<Text style={styles.customerName}>
{activeJob?.order?.customer?.user?.name}
</Text>
<Text style={styles.customerAddress}>
{activeJob?.order?.dropAddress?.houseNumber +
', ' +
activeJob?.order?.dropAddress?.addressLine1}
</Text>
<View style={styles.btnRow}>
<PrimaryButton

View File

@ -2,9 +2,9 @@ import React from 'react';
import { View, Text } from 'react-native';
import { useAppTheme } from '@theme';
import { PrimaryButton } from '@components';
import { useAppDispatch, useAppSelector } from '@store';
import { changeOrderStatusThunk, useAppDispatch, useAppSelector } from '@store';
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 { AppStackParamList } from '@navigation/navigationTypes';
import { RouteNames, JobStatus } from '@utils/constants';
@ -14,13 +14,24 @@ export const OrderPickedUpScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
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 = () => {
dispatch(advanceJobStatus(JobStatus.EnRoute));
navigation.navigate(RouteNames.LiveTracking);
dispatch(
changeOrderStatusThunk({ deliveryId: jobId, action: JobStatus.PickedUp }),
);
navigation.navigate(RouteNames.LiveTracking, {
jobId: jobId,
});
};
if (!activeJob) return null;
@ -35,8 +46,18 @@ export const OrderPickedUpScreen: React.FC = () => {
{/* Deliver Bottom Card */}
<View style={styles.bottomCard}>
<Text style={styles.cardTitle}>Deliver to Customer</Text>
<Text style={styles.customerName}>{activeJob.dropName}</Text>
<Text style={styles.customerAddress}>{activeJob.dropAddress}</Text>
<Text style={styles.customerName}>
{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
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 './delivery';
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;
orderId: string;
deliveryPartnerId: string;
deliveryNumber: string;
status: string;
estimatedDistanceKm: string;
actualDistanceKm: string;
proofOfPickupKey: string;
proofOfDeliveryKey: string;
pickedUpAt: string;
deliveredAt: string;
order: {
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[];
};
status: DeliveryStatus;
estimatedDistanceKm: string | null;
actualDistanceKm: string | null;
proofOfPickupKey: string | null;
proofOfDeliveryKey: string | null;
pickedUpAt: string | null;
deliveredAt: string | null;
order: DeliveryOrder;
}
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;
orderId: string;
productId: string;
itemType: string;
itemType: 'PRODUCT' | 'SERVICE';
name: string;
quantity: string;
unitPrice: 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.ArrivedAtStore]: { jobId: string };
[RouteNames.ConfirmPickup]: undefined;
[RouteNames.OrderPickedUp]: undefined;
[RouteNames.LiveTracking]: undefined;
[RouteNames.OrderPickedUp]: { jobId: string };
[RouteNames.LiveTracking]: { jobId: string };
[RouteNames.ArrivedAtCustomer]: { jobId: string };
[RouteNames.DeliverOrder]: undefined;
[RouteNames.DeliverOrder]: { jobId: string };
[RouteNames.DeliveryCompleted]: undefined;
[RouteNames.Documents]: undefined;
// [RouteNames.JobDetails]: { jobId: string };

View File

@ -15,7 +15,7 @@ const STORAGE_KEYS = {
} as const;
// ─── 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 ───────────────────────────────────────────────────────────
export const tokenManager = {

View File

@ -6,7 +6,7 @@ import { setDeliveryOffer } from '@store/commonReducers/delivery';
import { RouteNames } from '@utils/constants';
// 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;
@ -30,8 +30,12 @@ class SocketService {
* the store and navigationRef to be injected independently.
*/
bootstrap(store: Store | null, navigationRef: any) {
if (store) { this.store = store; }
if (navigationRef) { this.navigationRef = navigationRef; }
if (store) {
this.store = store;
}
if (navigationRef) {
this.navigationRef = navigationRef;
}
}
/**
@ -64,21 +68,18 @@ class SocketService {
});
// ── Listen for delivery offers ─────────────────────────────────────
this.socket.on(
SocketEvents.DeliveryOffer,
(data: DeliveryOffer) => {
console.log('[Socket] delivery_offer received:', data);
this.socket.on(SocketEvents.DeliveryOffer, (data: DeliveryOffer) => {
console.log('[Socket] delivery_offer received:', data);
if (this.store) {
this.store.dispatch(setDeliveryOffer(data));
}
if (this.store) {
this.store.dispatch(setDeliveryOffer(data));
}
// Navigate to NewJobRequest screen regardless of current screen
if (this.navigationRef?.isReady()) {
this.navigationRef.navigate(RouteNames.NewJobRequest);
}
},
);
// Navigate to NewJobRequest screen regardless of current screen
if (this.navigationRef?.isReady()) {
this.navigationRef.navigate(RouteNames.NewJobRequest);
}
});
}
/**

View File

@ -1,12 +1,19 @@
import { createReducer, createAction } from '@reduxjs/toolkit';
import { DeliveryOffer } from '@interfaces';
import { respondToOfferThunk, checkPendingOfferThunk } from './thunk';
import { DeliveryOffer, PickupOrderResponse } from '@interfaces';
import {
respondToOfferThunk,
checkPendingOfferThunk,
changeOrderStatusThunk,
} from './thunk';
export interface DeliveryState {
currentOffer: DeliveryOffer | null;
respondLoading: boolean;
respondError: string | null;
pendingOfferLoading: boolean;
pickupLoading: boolean;
pickupError: string | null;
pickupOrderResponse: PickupOrderResponse | null;
}
const initialState: DeliveryState = {
@ -14,6 +21,9 @@ const initialState: DeliveryState = {
respondLoading: false,
respondError: null,
pendingOfferLoading: false,
pickupLoading: false,
pickupError: null,
pickupOrderResponse: null,
};
/** Dispatched by socketService when a `delivery_offer` event is received. */
@ -57,5 +67,17 @@ export const deliveryReducer = createReducer(initialState, builder => {
})
.addCase(checkPendingOfferThunk.rejected, state => {
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,
PendingOfferResponse,
} from '@interfaces';
import * as deliveryApi from '../../../api/deliveryApi';
import { changeOrderStatus, getPendingOffer, respondToOffer } from '@api';
/**
* Respond to a delivery offer (ACCEPT / REJECT).
@ -16,7 +16,7 @@ export const respondToOfferThunk = createAsyncThunk<
'delivery/respondToOffer',
async ({ deliveryId, action }, { rejectWithValue }) => {
try {
return await deliveryApi.respondToOffer(deliveryId, action);
return await respondToOffer(deliveryId, action);
} catch (error: any) {
return rejectWithValue(
error.message || 'Failed to respond to delivery offer',
@ -32,11 +32,27 @@ export const checkPendingOfferThunk = createAsyncThunk<PendingOfferResponse>(
'delivery/checkPendingOffer',
async (_, { rejectWithValue }) => {
try {
return await deliveryApi.getPendingOffer();
return await getPendingOffer();
} catch (error: any) {
return rejectWithValue(
error.message || 'Failed to check pending offers',
);
return rejectWithValue(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 { deliveryReducer } from './commonReducers/delivery';
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 { dashBoardReducer } from '@features/screens/dashboardScreen';
import { activeDeliveriesReducer } from '@features/screens/jobsScreen';
@ -20,6 +24,7 @@ const rootReducer = combineReducers({
onboardingComplete: onBoardingCompleteReducer,
dashboard: dashBoardReducer,
deliveries: activeDeliveriesReducer,
accountInfo: profileReducer,
});
export type RootState = ReturnType<typeof rootReducer>;

View File

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

View File

@ -1,5 +1,8 @@
export const formatCurrency = (amount: number, currency: string = '₹'): string => {
return `${currency}${amount.toLocaleString('en-IN', {
export const formatCurrency = (
amount: number | undefined,
currency: string = '₹',
): string => {
return `${currency}${amount?.toLocaleString('en-IN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
})}`;
@ -7,7 +10,7 @@ export const formatCurrency = (amount: number, currency: string = '₹'): string
export const formatDistance = (km: number): string => {
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 => {