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
}
}
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")

View File

@ -1,6 +1,7 @@
import {
DeliveryPartnerLocationRequest,
DeliveryPartnerLocationResponse,
EarningsSummaryResponse,
ToggleStatusPayload,
ToggleStatusResponse,
} from '@interfaces';
@ -25,3 +26,10 @@ export const deliveryPartnerLocation = async (
);
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 { useAppDispatch, useAppSelector } from '@store';
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 { AppStackParamList } from '@navigation/navigationTypes';
import { RouteNames, JobStatus } from '@utils/constants';
import { formatCurrency } from '@utils/formatters';
import { getStyles } from './dashboardScreen.styles';
import { toggleStatus, updateDeliveryPartnerLocation } from './thunk';
import { getDashBoardDetails, toggleStatus, updateDeliveryPartnerLocation } from './thunk';
import {
getCurrentLocationWithAddress,
showPermissionDeniedAlert,
@ -47,11 +47,13 @@ export const DashboardScreen: React.FC = () => {
const { todayEarnings, completedCount, onlineMinutes, weeklyData } =
useAppSelector(state => state.earnings);
// const { jobStatus, activeJob } = useAppSelector(state => state.job);
const { earningsSummaryResponse } = useAppSelector(state => state.dashboard);
const locationIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
null,
);
const sendLocationUpdate = useCallback(async () => {
try {
const result = await getCurrentLocationWithAddress();
@ -74,6 +76,9 @@ export const DashboardScreen: React.FC = () => {
useEffect(() => {
dispatch(getAccountInfoThunk());
}, [dispatch]);
useFocusEffect(useCallback(() => {
dispatch(getDashBoardDetails());
}, [dispatch]));
useEffect(() => {
if (isOnline) {
@ -156,9 +161,8 @@ export const DashboardScreen: React.FC = () => {
const formatOnlineTime = (mins: number) => {
const hours = Math.floor(mins / 60);
const remainingMins = mins % 60;
return `${hours}h ${
remainingMins < 10 ? `0${remainingMins}` : remainingMins
}m`;
return `${hours}h ${remainingMins < 10 ? `0${remainingMins}` : remainingMins
}m`;
};
// 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.statBox}>
<Text style={styles.statVal}>
{formatCurrency(isOnline ? todayEarnings : 0)}
{formatCurrency(Number(earningsSummaryResponse?.todayEarnings?.total))}
</Text>
<Text style={styles.statLabel}>Earnings</Text>
</View>
@ -233,7 +237,7 @@ export const DashboardScreen: React.FC = () => {
<View style={styles.statBox}>
<Text style={styles.statVal}>
{isOnline ? completedCount : 0}
{earningsSummaryResponse?.todayEarnings?.completedCount}
</Text>
<Text style={styles.statLabel}>Completed</Text>
</View>
@ -242,7 +246,7 @@ export const DashboardScreen: React.FC = () => {
<View style={styles.statBox}>
<Text style={styles.statVal}>
{formatOnlineTime(isOnline ? onlineMinutes : 0)}
{formatOnlineTime(Number(earningsSummaryResponse?.todayEarnings?.onlineMinutes))}
</Text>
<Text style={styles.statLabel}>Online Time</Text>
</View>
@ -277,11 +281,10 @@ export const DashboardScreen: React.FC = () => {
<View style={styles.chartCard}>
<Text style={styles.chartTitle}>Weekly Overview</Text>
<View style={styles.chartRow}>
{weeklyData.map((data, index) => {
{earningsSummaryResponse?.weeklyEarnings?.map((data, index) => {
// Bar height percent
const heightPercent = `${
(data.amount / maxWeeklyAmount) * 85
}%`;
const heightPercent = `${(data.amount / maxWeeklyAmount) * 85
}%`;
return (
<View key={index} style={styles.barContainer}>
<Text style={styles.barVal}>{data.amount}</Text>

View File

@ -1,9 +1,10 @@
import {
DeliveryPartnerLocationResponse,
EarningsSummaryResponse,
ToggleStatusResponse,
} from '@interfaces';
import { createReducer } from '@reduxjs/toolkit';
import { toggleStatus, updateDeliveryPartnerLocation } from './thunk';
import { getDashBoardDetails, toggleStatus, updateDeliveryPartnerLocation } from './thunk';
export interface ToggleState {
loading: boolean;
@ -11,6 +12,9 @@ export interface ToggleState {
availabiltyStatus: ToggleStatusResponse | null;
deliveryPartnerLocation: DeliveryPartnerLocationResponse | null;
deliveryLocError: string | null;
earningsSummaryResponse: EarningsSummaryResponse | null;
earningsSummaryLoading: boolean;
earningsSummaryError: string | null;
}
export const initialState: ToggleState = {
@ -19,6 +23,9 @@ export const initialState: ToggleState = {
availabiltyStatus: null,
deliveryPartnerLocation: null,
deliveryLocError: null,
earningsSummaryResponse: null,
earningsSummaryLoading: false,
earningsSummaryError: null,
};
export const dashBoardReducer = createReducer(initialState, builder => {
@ -40,5 +47,17 @@ export const dashBoardReducer = createReducer(initialState, builder => {
})
.addCase(updateDeliveryPartnerLocation.rejected, (state, action) => {
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 { deliveryPartnerLocation, toggleStatus as toggleStatusApi } from '@api';
import { dashBoardApi, deliveryPartnerLocation, toggleStatus as toggleStatusApi } from '@api';
import {
DeliveryPartnerLocationRequest,
EarningsSummaryResponse,
ToggleStatusPayload,
} 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;
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 { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { BottomTabParamList } from '@navigation/navigationTypes';
import {
EarningsScreen,
@ -18,33 +19,75 @@ const BottomTabNavigator: React.FC = () => {
return (
<Tab.Navigator
screenOptions={{
screenOptions={({ route }) => ({
headerShown: false,
tabBarActiveTintColor: colors.primary,
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
name={RouteNames.Dashboard}
component={DashboardScreen}
options={{ tabBarLabel: 'Home' }}
/>
<Tab.Screen
name={RouteNames.Earnings}
component={EarningsScreen}
options={{ tabBarLabel: 'Earning' }}
/>
<Tab.Screen
name={RouteNames.Jobs}
component={JobsScreen}
options={{ tabBarLabel: 'Jobs' }}
/>
<Tab.Screen
name={RouteNames.Inbox}
component={InboxScreen}
options={{ tabBarLabel: 'Inbox' }}
/>
<Tab.Screen
name={RouteNames.Profile}
component={ProfileScreen}

View File

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

View File

@ -2706,6 +2706,15 @@ cliui@^6.0.0:
strip-ansi "^6.0.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:
version "8.0.1"
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"
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"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
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"
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:
version "0.10.2"
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"
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:
version "21.1.1"
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"
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:
version "17.7.3"
resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz"