From 55bab67d91ba7b33cd2b9260d6f7c423d4be2845 Mon Sep 17 00:00:00 2001 From: Tamojit Biswas Date: Tue, 14 Jul 2026 16:34:02 +0530 Subject: [PATCH] feat: implement delivery workflow screens, state management, and API services for order tracking and fulfillment --- app/api/accountApi.ts | 10 + app/api/deliveryApi.ts | 30 +- app/api/index.ts | 1 + .../arrivedAtCustomerScreen.tsx | 13 +- .../deliverOrderScreen/deliverOrderScreen.tsx | 67 ++-- app/features/screens/index.ts | 2 +- .../screens/jobsScreen/jobsScreen.tsx | 169 +++++----- app/features/screens/jobsScreen/reducer.ts | 19 +- app/features/screens/jobsScreen/thunk.ts | 14 +- .../liveTrackingScreen/liveTrackingScreen.tsx | 38 ++- .../orderPickedUpScreen.tsx | 37 ++- app/features/screens/profileScreen/index.ts | 4 + app/features/screens/profileScreen/reducer.ts | 31 ++ app/features/screens/profileScreen/thunk.ts | 14 + app/interfaces/accountInfo.ts | 34 ++ app/interfaces/index.ts | 1 + app/interfaces/order.ts | 308 ++++++++++++------ app/navigation/navigationTypes.ts | 6 +- app/services/apiClient.ts | 2 +- app/services/socketService.ts | 33 +- app/store/commonReducers/delivery/reducer.ts | 26 +- app/store/commonReducers/delivery/thunk.ts | 28 +- app/store/rootReducer.ts | 7 +- app/utils/constants.ts | 4 +- app/utils/formatters.ts | 9 +- 25 files changed, 641 insertions(+), 266 deletions(-) create mode 100644 app/api/accountApi.ts create mode 100644 app/features/screens/profileScreen/index.ts create mode 100644 app/features/screens/profileScreen/reducer.ts create mode 100644 app/features/screens/profileScreen/thunk.ts create mode 100644 app/interfaces/accountInfo.ts diff --git a/app/api/accountApi.ts b/app/api/accountApi.ts new file mode 100644 index 0000000..e343b2a --- /dev/null +++ b/app/api/accountApi.ts @@ -0,0 +1,10 @@ +import { apiClient } from '@services'; +import { DeliveryPartnerProfileResponse } from '@interfaces'; + +export const getAccountInfo = + async (): Promise => { + const response = await apiClient.get( + '/account-info', + ); + return response; + }; diff --git a/app/api/deliveryApi.ts b/app/api/deliveryApi.ts index e42a373..5fa9149 100644 --- a/app/api/deliveryApi.ts +++ b/app/api/deliveryApi.ts @@ -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 => { return response; }; -export const getActiveDeliveries = async (): Promise => { - const response = await apiClient.get( - '/delivery-partners/deliveries/active', +export const getActiveDeliveries = + async (): Promise => { + const response = await apiClient.get( + '/delivery-partners/deliveries/active', + ); + return response; + }; + +export const changeOrderStatus = async ( + deliveryId: string, + action: string, + proofKey?: string, +): Promise => { + const response = await apiClient.post( + `/delivery-partners/deliveries/${deliveryId}/status`, + { action, proofKey }, + ); + return response; +}; + +export const completedDeliveries = async (): Promise => { + const response = await apiClient.get( + '/delivery-partners/deliveries/history', ); return response; }; diff --git a/app/api/index.ts b/app/api/index.ts index 6e98c14..cb16e59 100644 --- a/app/api/index.ts +++ b/app/api/index.ts @@ -3,3 +3,4 @@ export * from './onBoardApi'; export * from './kycApi'; export * from './dashboardApi'; export * from './deliveryApi'; +export * from './accountApi'; diff --git a/app/features/screens/arrivedAtCustomerScreen/arrivedAtCustomerScreen.tsx b/app/features/screens/arrivedAtCustomerScreen/arrivedAtCustomerScreen.tsx index 9a9f5b2..a2025ac 100644 --- a/app/features/screens/arrivedAtCustomerScreen/arrivedAtCustomerScreen.tsx +++ b/app/features/screens/arrivedAtCustomerScreen/arrivedAtCustomerScreen.tsx @@ -12,15 +12,16 @@ import { getStyles } from './arrivedAtCustomerScreen.styles'; export const ArrivedAtCustomerScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); - const navigation = useNavigation>(); + const navigation = + useNavigation>(); - 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 ( @@ -32,8 +33,8 @@ export const ArrivedAtCustomerScreen: React.FC = () => { {/* Arrived Card */} You've arrived - {activeJob.dropName} - {activeJob.dropAddress} + {activeJob?.dropName} + {activeJob?.dropAddress} { const { colors } = useAppTheme(); const styles = getStyles(colors); const dispatch = useAppDispatch(); - const navigation = useNavigation>(); + const navigation = + useNavigation>(); + const route = + useRoute>(); + 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()} /> - + {/* Customer Detail Card */} Customer Name - {activeJob.dropName} + + {activeJob?.order?.customer?.user?.name} + Order ID - {activeJob.orderId} + {activeJob?.orderId} {/* Items Summary list */} Items Details - {items.map((item) => ( + {activeJob?.order?.orderItems?.map(item => ( {item.name} - x{item.qty} + x{item.quantity} ))} {/* Payment Collect Details */} - Cash to Collect - ₹0.00 (Online Paid) + + {activeJob?.order?.paymentMethod === 'WALLET' || + activeJob?.order?.paymentMethod === 'UPI' + ? 'Already Paid' + : 'Collect from Customer'} + + + ₹{activeJob?.order?.totalAmount} + { 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 ( @@ -105,22 +112,30 @@ export const JobsScreen: React.FC = () => { > {activeTab === 'active' ? ( // Active job tab - activeDeliveries ? ( + activeDeliveries?.delivery ? ( - 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 - {jobHistory.length > 0 - ? jobHistory.map((job, idx) => ( - - - - )) - : defaultHistory.map((job, idx) => ( - - - - ))} + {completedDeliveries && completedDeliveries?.length > 0 ? ( + completedDeliveries?.map((job, idx) => ( + + + + )) + ) : ( + + + No history found + + )} )} diff --git a/app/features/screens/jobsScreen/reducer.ts b/app/features/screens/jobsScreen/reducer.ts index 657d2d2..de6fd76 100644 --- a/app/features/screens/jobsScreen/reducer.ts +++ b/app/features/screens/jobsScreen/reducer.ts @@ -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; }); }); diff --git a/app/features/screens/jobsScreen/thunk.ts b/app/features/screens/jobsScreen/thunk.ts index 787737c..03bc44b 100644 --- a/app/features/screens/jobsScreen/thunk.ts +++ b/app/features/screens/jobsScreen/thunk.ts @@ -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); + } + }, +); diff --git a/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx b/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx index c4298c1..6d68ffe 100644 --- a/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx +++ b/app/features/screens/liveTrackingScreen/liveTrackingScreen.tsx @@ -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>(); + const navigation = + useNavigation>(); + const route = + useRoute>(); + 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 ( {/* Top Floating ETA Card */} Arriving in 8 mins - Distance: {activeJob.distance} km • Speed: 24 km/h + + Distance: {activeJob?.estimatedDistanceKm} km • Speed: 24 km/h + {/* Map Background Placeholder */} @@ -45,8 +59,14 @@ export const LiveTrackingScreen: React.FC = () => { {/* Customer Location Bottom Card */} Deliver to - {activeJob.dropName} - {activeJob.dropAddress} + + {activeJob?.order?.customer?.user?.name} + + + {activeJob?.order?.dropAddress?.houseNumber + + ', ' + + activeJob?.order?.dropAddress?.addressLine1} + { const { colors } = useAppTheme(); const styles = getStyles(colors); const dispatch = useAppDispatch(); - const navigation = useNavigation>(); + const route = + useRoute>(); + const { jobId } = route.params; + // console.log(jobId, 'jobId'); + const navigation = + useNavigation>(); - 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 */} Deliver to Customer - {activeJob.dropName} - {activeJob.dropAddress} + + {activeJob?.order?.customer?.user?.name} + + + {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} + { + 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; + }); +}); diff --git a/app/features/screens/profileScreen/thunk.ts b/app/features/screens/profileScreen/thunk.ts new file mode 100644 index 0000000..d758766 --- /dev/null +++ b/app/features/screens/profileScreen/thunk.ts @@ -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); + } + }, +); diff --git a/app/interfaces/accountInfo.ts b/app/interfaces/accountInfo.ts new file mode 100644 index 0000000..db78998 --- /dev/null +++ b/app/interfaces/accountInfo.ts @@ -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; +} diff --git a/app/interfaces/index.ts b/app/interfaces/index.ts index 9d6c9e1..a2bd8fe 100644 --- a/app/interfaces/index.ts +++ b/app/interfaces/index.ts @@ -4,3 +4,4 @@ export * from './kyc'; export * from './dashboard'; export * from './delivery'; export * from './order'; +export * from './accountInfo'; diff --git a/app/interfaces/order.ts b/app/interfaces/order.ts index f20f925..85f4902 100644 --- a/app/interfaces/order.ts +++ b/app/interfaces/order.ts @@ -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; +} + +//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; } diff --git a/app/navigation/navigationTypes.ts b/app/navigation/navigationTypes.ts index 2e4dcf8..09022f6 100644 --- a/app/navigation/navigationTypes.ts +++ b/app/navigation/navigationTypes.ts @@ -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 }; diff --git a/app/services/apiClient.ts b/app/services/apiClient.ts index 76ed83d..19c9edc 100644 --- a/app/services/apiClient.ts +++ b/app/services/apiClient.ts @@ -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 = { diff --git a/app/services/socketService.ts b/app/services/socketService.ts index 1d0ea6f..b5f5ef4 100644 --- a/app/services/socketService.ts +++ b/app/services/socketService.ts @@ -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); + } + }); } /** diff --git a/app/store/commonReducers/delivery/reducer.ts b/app/store/commonReducers/delivery/reducer.ts index 92396e4..8e31ad3 100644 --- a/app/store/commonReducers/delivery/reducer.ts +++ b/app/store/commonReducers/delivery/reducer.ts @@ -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; }); }); diff --git a/app/store/commonReducers/delivery/thunk.ts b/app/store/commonReducers/delivery/thunk.ts index 073a2cf..8b4c0c6 100644 --- a/app/store/commonReducers/delivery/thunk.ts +++ b/app/store/commonReducers/delivery/thunk.ts @@ -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( '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'); } }, ); diff --git a/app/store/rootReducer.ts b/app/store/rootReducer.ts index ae657f0..e9cb52b 100644 --- a/app/store/rootReducer.ts +++ b/app/store/rootReducer.ts @@ -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; diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 6404b4a..0bedfc1 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -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 { diff --git a/app/utils/formatters.ts b/app/utils/formatters.ts index 6e10815..b0cf240 100644 --- a/app/utils/formatters.ts +++ b/app/utils/formatters.ts @@ -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 => {