feat: dashboard screen functionality for real-time delivery status management

This commit is contained in:
Atanu Das 2026-07-23 15:34:04 +05:30
parent 2148f4e0c3
commit 0da64513ad
9 changed files with 160 additions and 19 deletions

View File

@ -117,3 +117,5 @@ dependencies {
implementation jscFlavor implementation jscFlavor
} }
} }
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")

View File

@ -1,6 +1,7 @@
import { import {
DeliveryPartnerLocationRequest, DeliveryPartnerLocationRequest,
DeliveryPartnerLocationResponse, DeliveryPartnerLocationResponse,
EarningsSummaryResponse,
ToggleStatusPayload, ToggleStatusPayload,
ToggleStatusResponse, ToggleStatusResponse,
} from '@interfaces'; } from '@interfaces';
@ -25,3 +26,10 @@ export const deliveryPartnerLocation = async (
); );
return response; return response;
}; };
export const dashBoardApi = async (): Promise<EarningsSummaryResponse> => {
const response = await apiClient.get<EarningsSummaryResponse>(
'/delivery-partners/earnings/dashboard',
);
return response;
}

View File

@ -13,13 +13,13 @@ import { PrimaryButton } from '@components';
import { BellIcon, PersonIcon, WalletIcon, ClipboardIcon } from '@icons'; import { BellIcon, PersonIcon, WalletIcon, ClipboardIcon } from '@icons';
import { useAppDispatch, useAppSelector } from '@store'; import { useAppDispatch, useAppSelector } from '@store';
import { setOnlineStatus } from '@store/commonReducers/auth'; import { setOnlineStatus } from '@store/commonReducers/auth';
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, JobStatus } from '@utils/constants'; import { RouteNames, JobStatus } from '@utils/constants';
import { formatCurrency } from '@utils/formatters'; import { formatCurrency } from '@utils/formatters';
import { getStyles } from './dashboardScreen.styles'; import { getStyles } from './dashboardScreen.styles';
import { toggleStatus, updateDeliveryPartnerLocation } from './thunk'; import { getDashBoardDetails, toggleStatus, updateDeliveryPartnerLocation } from './thunk';
import { import {
getCurrentLocationWithAddress, getCurrentLocationWithAddress,
showPermissionDeniedAlert, showPermissionDeniedAlert,
@ -47,11 +47,13 @@ export const DashboardScreen: React.FC = () => {
const { todayEarnings, completedCount, onlineMinutes, weeklyData } = const { todayEarnings, completedCount, onlineMinutes, weeklyData } =
useAppSelector(state => state.earnings); useAppSelector(state => state.earnings);
// const { jobStatus, activeJob } = useAppSelector(state => state.job); // const { jobStatus, activeJob } = useAppSelector(state => state.job);
const { earningsSummaryResponse } = useAppSelector(state => state.dashboard);
const locationIntervalRef = useRef<ReturnType<typeof setInterval> | null>( const locationIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
null, null,
); );
const sendLocationUpdate = useCallback(async () => { const sendLocationUpdate = useCallback(async () => {
try { try {
const result = await getCurrentLocationWithAddress(); const result = await getCurrentLocationWithAddress();
@ -74,6 +76,9 @@ export const DashboardScreen: React.FC = () => {
useEffect(() => { useEffect(() => {
dispatch(getAccountInfoThunk()); dispatch(getAccountInfoThunk());
}, [dispatch]); }, [dispatch]);
useFocusEffect(useCallback(() => {
dispatch(getDashBoardDetails());
}, [dispatch]));
useEffect(() => { useEffect(() => {
if (isOnline) { if (isOnline) {
@ -156,9 +161,8 @@ export const DashboardScreen: React.FC = () => {
const formatOnlineTime = (mins: number) => { const formatOnlineTime = (mins: number) => {
const hours = Math.floor(mins / 60); const hours = Math.floor(mins / 60);
const remainingMins = mins % 60; const remainingMins = mins % 60;
return `${hours}h ${ return `${hours}h ${remainingMins < 10 ? `0${remainingMins}` : remainingMins
remainingMins < 10 ? `0${remainingMins}` : remainingMins }m`;
}m`;
}; };
// Helper to render chart bar heights dynamically based on amounts // Helper to render chart bar heights dynamically based on amounts
@ -224,7 +228,7 @@ export const DashboardScreen: React.FC = () => {
<View style={styles.statsRow}> <View style={styles.statsRow}>
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={styles.statVal}> <Text style={styles.statVal}>
{formatCurrency(isOnline ? todayEarnings : 0)} {formatCurrency(Number(earningsSummaryResponse?.todayEarnings?.total))}
</Text> </Text>
<Text style={styles.statLabel}>Earnings</Text> <Text style={styles.statLabel}>Earnings</Text>
</View> </View>
@ -233,7 +237,7 @@ export const DashboardScreen: React.FC = () => {
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={styles.statVal}> <Text style={styles.statVal}>
{isOnline ? completedCount : 0} {earningsSummaryResponse?.todayEarnings?.completedCount}
</Text> </Text>
<Text style={styles.statLabel}>Completed</Text> <Text style={styles.statLabel}>Completed</Text>
</View> </View>
@ -242,7 +246,7 @@ export const DashboardScreen: React.FC = () => {
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={styles.statVal}> <Text style={styles.statVal}>
{formatOnlineTime(isOnline ? onlineMinutes : 0)} {formatOnlineTime(Number(earningsSummaryResponse?.todayEarnings?.onlineMinutes))}
</Text> </Text>
<Text style={styles.statLabel}>Online Time</Text> <Text style={styles.statLabel}>Online Time</Text>
</View> </View>
@ -277,11 +281,10 @@ export const DashboardScreen: React.FC = () => {
<View style={styles.chartCard}> <View style={styles.chartCard}>
<Text style={styles.chartTitle}>Weekly Overview</Text> <Text style={styles.chartTitle}>Weekly Overview</Text>
<View style={styles.chartRow}> <View style={styles.chartRow}>
{weeklyData.map((data, index) => { {earningsSummaryResponse?.weeklyEarnings?.map((data, index) => {
// Bar height percent // Bar height percent
const heightPercent = `${ const heightPercent = `${(data.amount / maxWeeklyAmount) * 85
(data.amount / maxWeeklyAmount) * 85 }%`;
}%`;
return ( return (
<View key={index} style={styles.barContainer}> <View key={index} style={styles.barContainer}>
<Text style={styles.barVal}>{data.amount}</Text> <Text style={styles.barVal}>{data.amount}</Text>

View File

@ -1,9 +1,10 @@
import { import {
DeliveryPartnerLocationResponse, DeliveryPartnerLocationResponse,
EarningsSummaryResponse,
ToggleStatusResponse, ToggleStatusResponse,
} from '@interfaces'; } from '@interfaces';
import { createReducer } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit';
import { toggleStatus, updateDeliveryPartnerLocation } from './thunk'; import { getDashBoardDetails, toggleStatus, updateDeliveryPartnerLocation } from './thunk';
export interface ToggleState { export interface ToggleState {
loading: boolean; loading: boolean;
@ -11,6 +12,9 @@ export interface ToggleState {
availabiltyStatus: ToggleStatusResponse | null; availabiltyStatus: ToggleStatusResponse | null;
deliveryPartnerLocation: DeliveryPartnerLocationResponse | null; deliveryPartnerLocation: DeliveryPartnerLocationResponse | null;
deliveryLocError: string | null; deliveryLocError: string | null;
earningsSummaryResponse: EarningsSummaryResponse | null;
earningsSummaryLoading: boolean;
earningsSummaryError: string | null;
} }
export const initialState: ToggleState = { export const initialState: ToggleState = {
@ -19,6 +23,9 @@ export const initialState: ToggleState = {
availabiltyStatus: null, availabiltyStatus: null,
deliveryPartnerLocation: null, deliveryPartnerLocation: null,
deliveryLocError: null, deliveryLocError: null,
earningsSummaryResponse: null,
earningsSummaryLoading: false,
earningsSummaryError: null,
}; };
export const dashBoardReducer = createReducer(initialState, builder => { export const dashBoardReducer = createReducer(initialState, builder => {
@ -40,5 +47,17 @@ export const dashBoardReducer = createReducer(initialState, builder => {
}) })
.addCase(updateDeliveryPartnerLocation.rejected, (state, action) => { .addCase(updateDeliveryPartnerLocation.rejected, (state, action) => {
state.deliveryLocError = action.payload as string; state.deliveryLocError = action.payload as string;
})
.addCase(getDashBoardDetails.pending, state => {
state.earningsSummaryLoading = true;
state.earningsSummaryError = null;
})
.addCase(getDashBoardDetails.fulfilled, (state, action) => {
state.earningsSummaryLoading = false;
state.earningsSummaryResponse = action.payload;
})
.addCase(getDashBoardDetails.rejected, (state, action) => {
state.earningsSummaryLoading = false;
state.earningsSummaryError = action.payload as string;
}); });
}); });

View File

@ -1,7 +1,8 @@
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { deliveryPartnerLocation, toggleStatus as toggleStatusApi } from '@api'; import { dashBoardApi, deliveryPartnerLocation, toggleStatus as toggleStatusApi } from '@api';
import { import {
DeliveryPartnerLocationRequest, DeliveryPartnerLocationRequest,
EarningsSummaryResponse,
ToggleStatusPayload, ToggleStatusPayload,
} from '@interfaces'; } from '@interfaces';
@ -29,3 +30,16 @@ export const updateDeliveryPartnerLocation = createAsyncThunk(
} }
}, },
); );
export const getDashBoardDetails = createAsyncThunk(
'dashboard/getDashBoardDetails',
async (_, { rejectWithValue }) => {
try {
const response = await dashBoardApi();
return response;
} catch (error: unknown) {
console.log(error);
return rejectWithValue(error);
}
}
)

View File

@ -21,3 +21,19 @@ export interface DeliveryPartnerLocationResponse {
latitude: number; latitude: number;
longitude: number; longitude: number;
} }
export interface EarningsSummaryResponse {
todayEarnings: TodayEarnings;
weeklyEarnings: WeeklyEarning[];
}
export interface TodayEarnings {
total: number;
completedCount: number;
onlineMinutes: number;
}
export interface WeeklyEarning {
day: string;
amount: number;
}

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { BottomTabParamList } from '@navigation/navigationTypes'; import { BottomTabParamList } from '@navigation/navigationTypes';
import { import {
EarningsScreen, EarningsScreen,
@ -18,33 +19,75 @@ const BottomTabNavigator: React.FC = () => {
return ( return (
<Tab.Navigator <Tab.Navigator
screenOptions={{ screenOptions={({ route }) => ({
headerShown: false, headerShown: false,
tabBarActiveTintColor: colors.primary, tabBarActiveTintColor: colors.primary,
tabBarInactiveTintColor: colors.textSecondary, tabBarInactiveTintColor: colors.textSecondary,
tabBarStyle: { backgroundColor: colors.background }, tabBarStyle: {
}} backgroundColor: colors.background,
},
tabBarIcon: ({ color, size, focused }) => {
let iconName: string;
switch (route.name) {
case RouteNames.Dashboard:
iconName = focused ? 'home' : 'home-outline';
break;
case RouteNames.Earnings:
iconName = focused ? 'cash' : 'cash-multiple';
break;
case RouteNames.Jobs:
iconName = focused ? 'briefcase' : 'briefcase-outline';
break;
case RouteNames.Inbox:
iconName = focused ? 'message' : 'message-outline';
break;
case RouteNames.Profile:
iconName = focused ? 'account' : 'account-outline';
break;
default:
iconName = 'circle';
}
return (
<MaterialCommunityIcons
name={iconName}
size={size}
color={color}
/>
);
},
})}
> >
<Tab.Screen <Tab.Screen
name={RouteNames.Dashboard} name={RouteNames.Dashboard}
component={DashboardScreen} component={DashboardScreen}
options={{ tabBarLabel: 'Home' }} options={{ tabBarLabel: 'Home' }}
/> />
<Tab.Screen <Tab.Screen
name={RouteNames.Earnings} name={RouteNames.Earnings}
component={EarningsScreen} component={EarningsScreen}
options={{ tabBarLabel: 'Earning' }} options={{ tabBarLabel: 'Earning' }}
/> />
<Tab.Screen <Tab.Screen
name={RouteNames.Jobs} name={RouteNames.Jobs}
component={JobsScreen} component={JobsScreen}
options={{ tabBarLabel: 'Jobs' }} options={{ tabBarLabel: 'Jobs' }}
/> />
<Tab.Screen <Tab.Screen
name={RouteNames.Inbox} name={RouteNames.Inbox}
component={InboxScreen} component={InboxScreen}
options={{ tabBarLabel: 'Inbox' }} options={{ tabBarLabel: 'Inbox' }}
/> />
<Tab.Screen <Tab.Screen
name={RouteNames.Profile} name={RouteNames.Profile}
component={ProfileScreen} component={ProfileScreen}

View File

@ -32,6 +32,7 @@
"react-native-screens": "^4.25.2", "react-native-screens": "^4.25.2",
"react-native-svg": "^15.15.5", "react-native-svg": "^15.15.5",
"react-native-uuid": "^2.0.4", "react-native-uuid": "^2.0.4",
"react-native-vector-icons": "^10.3.0",
"react-native-worklets": "^0.10.0", "react-native-worklets": "^0.10.0",
"react-redux": "^9.3.0", "react-redux": "^9.3.0",
"reactotron-react-native": "^5.2.0", "reactotron-react-native": "^5.2.0",

View File

@ -2706,6 +2706,15 @@ cliui@^6.0.0:
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi "^6.2.0" wrap-ansi "^6.2.0"
cliui@^7.0.2:
version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
cliui@^8.0.1: cliui@^8.0.1:
version "8.0.1" version "8.0.1"
resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
@ -5787,7 +5796,7 @@ prompts@^2.0.1, prompts@^2.4.2:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types@*, prop-types@^15.8.1: prop-types@*, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -5961,6 +5970,14 @@ react-native-uuid@^2.0.4:
resolved "https://registry.npmjs.org/react-native-uuid/-/react-native-uuid-2.0.4.tgz" resolved "https://registry.npmjs.org/react-native-uuid/-/react-native-uuid-2.0.4.tgz"
integrity sha512-LSJNeh559qC17fgVPBsWuTSW/OygFp2dwTcf94IQBLYft5FzIQS9pCsuT36OPvyvDOMb6yiGr6TafaJDnz9PPQ== integrity sha512-LSJNeh559qC17fgVPBsWuTSW/OygFp2dwTcf94IQBLYft5FzIQS9pCsuT36OPvyvDOMb6yiGr6TafaJDnz9PPQ==
react-native-vector-icons@^10.3.0:
version "10.3.0"
resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz#de440f2627a2ed1079ce3b99d5b9d4f86894df28"
integrity sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==
dependencies:
prop-types "^15.7.2"
yargs "^16.1.1"
react-native-worklets@^0.10.0: react-native-worklets@^0.10.0:
version "0.10.2" version "0.10.2"
resolved "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.2.tgz" resolved "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.2.tgz"
@ -7131,6 +7148,11 @@ yargs-parser@^18.1.2:
camelcase "^5.0.0" camelcase "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"
yargs-parser@^20.2.2:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs-parser@^21.1.1: yargs-parser@^21.1.1:
version "21.1.1" version "21.1.1"
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
@ -7153,6 +7175,19 @@ yargs@^15.1.0:
y18n "^4.0.0" y18n "^4.0.0"
yargs-parser "^18.1.2" yargs-parser "^18.1.2"
yargs@^16.1.1:
version "16.2.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.2.tgz#c56731dca0d2788ae0866dd3c83907d6bab85f7d"
integrity sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==
dependencies:
cliui "^7.0.2"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.0"
y18n "^5.0.5"
yargs-parser "^20.2.2"
yargs@^17.3.1, yargs@^17.6.2: yargs@^17.3.1, yargs@^17.6.2:
version "17.7.3" version "17.7.3"
resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz" resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz"