454 lines
15 KiB
TypeScript
454 lines
15 KiB
TypeScript
import React, { useCallback, useEffect, useRef } from 'react';
|
||
import {
|
||
View,
|
||
Text,
|
||
ScrollView,
|
||
TouchableOpacity,
|
||
Alert,
|
||
Switch,
|
||
InteractionManager,
|
||
} from 'react-native';
|
||
import { useAppTheme, borderRadius, spacing, shadow, typography } from '@theme';
|
||
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 { 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 {
|
||
getCurrentLocationWithAddress,
|
||
showPermissionDeniedAlert,
|
||
socketService,
|
||
} from '@services';
|
||
|
||
export const DashboardScreen: React.FC = () => {
|
||
const { colors } = useAppTheme();
|
||
const styles = getStyles(colors);
|
||
const dispatch = useAppDispatch();
|
||
const navigation =
|
||
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
|
||
|
||
// Derive online status from API response stored in dashboard slice.
|
||
// Falls back to auth.isOnline during the initial load before any toggle API call.
|
||
const { availabiltyStatus, loading: toggleLoading } = useAppSelector(
|
||
state => state.dashboard,
|
||
);
|
||
const { user } = useAppSelector(state => state.auth);
|
||
const authIsOnline = useAppSelector(state => state.auth.isOnline);
|
||
const isOnline = availabiltyStatus
|
||
? availabiltyStatus.availabilityStatus === 'ONLINE'
|
||
: authIsOnline;
|
||
const { todayEarnings, completedCount, onlineMinutes, weeklyData } =
|
||
useAppSelector(state => state.earnings);
|
||
const { jobStatus, activeJob } = useAppSelector(state => state.job);
|
||
|
||
const locationIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
|
||
null,
|
||
);
|
||
|
||
const sendLocationUpdate = useCallback(async () => {
|
||
try {
|
||
const result = await getCurrentLocationWithAddress();
|
||
const coords = result.coords;
|
||
|
||
dispatch(
|
||
updateDeliveryPartnerLocation({
|
||
latitude: coords.latitude,
|
||
longitude: coords.longitude,
|
||
accuracyMeters: coords.accuracy ?? 0,
|
||
speedKph: coords.speed ? coords.speed * 3.6 : 0, // convert m/s -> km/h if needed
|
||
heading: coords.heading ?? 0,
|
||
}),
|
||
);
|
||
} catch (err) {
|
||
console.log('--------location update error------', err);
|
||
}
|
||
}, [dispatch]);
|
||
|
||
useEffect(() => {
|
||
if (isOnline) {
|
||
// Fire one immediately on going online, then every 15s
|
||
sendLocationUpdate();
|
||
locationIntervalRef.current = setInterval(sendLocationUpdate, 15000);
|
||
} else {
|
||
if (locationIntervalRef.current) {
|
||
clearInterval(locationIntervalRef.current);
|
||
locationIntervalRef.current = null;
|
||
}
|
||
}
|
||
|
||
return () => {
|
||
if (locationIntervalRef.current) {
|
||
clearInterval(locationIntervalRef.current);
|
||
locationIntervalRef.current = null;
|
||
}
|
||
};
|
||
}, [isOnline, sendLocationUpdate]);
|
||
|
||
// Notify the socket server when online/offline status changes
|
||
useEffect(() => {
|
||
if (isOnline) {
|
||
socketService.goOnline();
|
||
} else {
|
||
socketService.goOffline();
|
||
}
|
||
}, [isOnline]);
|
||
|
||
const handleToggleOnline = () => {
|
||
const newStatus = isOnline ? 'OFFLINE' : 'ONLINE';
|
||
|
||
// Optimistically update the auth flag so the timer and UI react immediately
|
||
dispatch(setOnlineStatus(!isOnline));
|
||
|
||
dispatch(toggleStatus({ availabilityStatus: newStatus }))
|
||
.unwrap()
|
||
.then(response => {
|
||
// Confirm state from server — setOnlineStatus stays in sync
|
||
const isNowOnline = response.availabilityStatus === 'ONLINE';
|
||
dispatch(setOnlineStatus(isNowOnline));
|
||
})
|
||
.catch(() => {
|
||
// Revert the optimistic update on API failure
|
||
dispatch(setOnlineStatus(isOnline));
|
||
Alert.alert(
|
||
'Status Update Failed',
|
||
'Could not update your availability. Please try again.',
|
||
);
|
||
});
|
||
};
|
||
|
||
const handleActiveJobBannerPress = () => {
|
||
// Navigate back to the screen corresponding to the current job step
|
||
switch (jobStatus) {
|
||
case JobStatus.NewRequest:
|
||
navigation.navigate(RouteNames.NewJobRequest);
|
||
break;
|
||
case JobStatus.Accepted:
|
||
navigation.navigate(RouteNames.OrderAccepted);
|
||
break;
|
||
case JobStatus.ArrivedAtStore:
|
||
navigation.navigate(RouteNames.ConfirmPickup);
|
||
break;
|
||
case JobStatus.PickedUp:
|
||
navigation.navigate(RouteNames.OrderPickedUp);
|
||
break;
|
||
case JobStatus.EnRoute:
|
||
navigation.navigate(RouteNames.LiveTracking);
|
||
break;
|
||
case JobStatus.ArrivedAtCustomer:
|
||
navigation.navigate(RouteNames.DeliverOrder);
|
||
break;
|
||
default:
|
||
Alert.alert('Status Error', 'Unknown active delivery state.');
|
||
}
|
||
};
|
||
|
||
const formatOnlineTime = (mins: number) => {
|
||
const hours = Math.floor(mins / 60);
|
||
const remainingMins = mins % 60;
|
||
return `${hours}h ${
|
||
remainingMins < 10 ? `0${remainingMins}` : remainingMins
|
||
}m`;
|
||
};
|
||
|
||
// Helper to render chart bar heights dynamically based on amounts
|
||
const maxWeeklyAmount = Math.max(...weeklyData.map(d => d.amount), 1);
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<ScrollView
|
||
contentContainerStyle={styles.scrollContainer}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
{/* Header Section */}
|
||
<View style={styles.header}>
|
||
<View>
|
||
<Text style={styles.greetingText}>Good Morning ☀️</Text>
|
||
<Text style={styles.nameText}>{user?.name}</Text>
|
||
</View>
|
||
|
||
<View style={styles.toggleWrapper}>
|
||
<View
|
||
style={[
|
||
styles.statusDot,
|
||
{
|
||
backgroundColor: isOnline
|
||
? colors.statusOnline
|
||
: colors.statusOffline,
|
||
},
|
||
]}
|
||
/>
|
||
<Text
|
||
style={[
|
||
styles.statusText,
|
||
{
|
||
color: isOnline ? colors.statusOnline : colors.statusOffline,
|
||
},
|
||
]}
|
||
>
|
||
{isOnline ? 'Online' : 'Offline'}
|
||
</Text>
|
||
<Switch
|
||
value={isOnline}
|
||
onValueChange={handleToggleOnline}
|
||
trackColor={{ false: colors.border, true: colors.primaryLight }}
|
||
thumbColor={isOnline ? colors.primary : colors.placeholder}
|
||
disabled={toggleLoading}
|
||
style={{ marginLeft: 8, opacity: toggleLoading ? 0.5 : 1 }}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
{/* Offline Warning Banner */}
|
||
{!isOnline && (
|
||
<View style={styles.offlineBanner}>
|
||
<Text style={styles.offlineBannerText}>
|
||
You are offline. Go online to start receiving orders.
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
{/* Today's Summary Card */}
|
||
<View style={styles.summaryCard}>
|
||
<Text style={styles.summaryTitle}>Today's Summary</Text>
|
||
<View style={styles.statsRow}>
|
||
<View style={styles.statBox}>
|
||
<Text style={styles.statVal}>
|
||
{formatCurrency(isOnline ? todayEarnings : 0)}
|
||
</Text>
|
||
<Text style={styles.statLabel}>Earnings</Text>
|
||
</View>
|
||
|
||
<View style={styles.statDivider} />
|
||
|
||
<View style={styles.statBox}>
|
||
<Text style={styles.statVal}>
|
||
{isOnline ? completedCount : 0}
|
||
</Text>
|
||
<Text style={styles.statLabel}>Completed</Text>
|
||
</View>
|
||
|
||
<View style={styles.statDivider} />
|
||
|
||
<View style={styles.statBox}>
|
||
<Text style={styles.statVal}>
|
||
{formatOnlineTime(isOnline ? onlineMinutes : 0)}
|
||
</Text>
|
||
<Text style={styles.statLabel}>Online Time</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* Conditional Layout for Online vs Offline */}
|
||
{isOnline ? (
|
||
<>
|
||
{/* Performance Stats */}
|
||
<View style={styles.performanceCard}>
|
||
<Text style={styles.perfTitle}>Your Performance</Text>
|
||
<View style={styles.perfRow}>
|
||
<View style={styles.perfBox}>
|
||
<Text style={[styles.perfVal, { color: colors.warning }]}>
|
||
4.8 ★
|
||
</Text>
|
||
<Text style={styles.perfLabel}>Rating</Text>
|
||
</View>
|
||
<View style={styles.perfBox}>
|
||
<Text style={styles.perfVal}>95%</Text>
|
||
<Text style={styles.perfLabel}>Acceptance</Text>
|
||
</View>
|
||
<View style={styles.perfBox}>
|
||
<Text style={styles.perfVal}>98%</Text>
|
||
<Text style={styles.perfLabel}>Completion</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* Weekly Earnings Chart (Styled SVG/Flex columns) */}
|
||
<View style={styles.chartCard}>
|
||
<Text style={styles.chartTitle}>Weekly Overview</Text>
|
||
<View style={styles.chartRow}>
|
||
{weeklyData.map((data, index) => {
|
||
// Bar height percent
|
||
const heightPercent = `${
|
||
(data.amount / maxWeeklyAmount) * 85
|
||
}%`;
|
||
return (
|
||
<View key={index} style={styles.barContainer}>
|
||
<Text style={styles.barVal}>₹{data.amount}</Text>
|
||
<View
|
||
style={[
|
||
styles.bar,
|
||
{
|
||
height: heightPercent as any,
|
||
backgroundColor:
|
||
index === weeklyData.length - 1
|
||
? colors.primary
|
||
: colors.placeholder,
|
||
},
|
||
]}
|
||
/>
|
||
<Text style={styles.barLabel}>{data.day}</Text>
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
</View>
|
||
|
||
<PrimaryButton
|
||
title="Go Offline"
|
||
onPress={handleToggleOnline}
|
||
style={[styles.offlineBtn, { backgroundColor: colors.error }]}
|
||
/>
|
||
</>
|
||
) : (
|
||
<View style={styles.promptCard}>
|
||
<Text style={styles.promptTitle}>
|
||
Go Online to start receiving delivery requests
|
||
</Text>
|
||
<Text style={styles.promptDesc}>
|
||
Make sure you are in a high-demand delivery zone to maximize your
|
||
earnings.
|
||
</Text>
|
||
<PrimaryButton
|
||
title="Go Online"
|
||
onPress={handleToggleOnline}
|
||
style={styles.onlineBtn}
|
||
/>
|
||
</View>
|
||
)}
|
||
|
||
{/* Help & Support Cards at bottom */}
|
||
<View style={styles.helpSection}>
|
||
<Text style={styles.helpTitle}>Help & Support</Text>
|
||
<View style={styles.helpRow}>
|
||
<TouchableOpacity
|
||
style={styles.helpItem}
|
||
onPress={() =>
|
||
Alert.alert(
|
||
'Emergency SOS',
|
||
'Calling nearest security/emergency dispatch...',
|
||
)
|
||
}
|
||
>
|
||
<BellIcon size={24} color={colors.error} />
|
||
<Text style={styles.helpLabel}>Emergency SOS</Text>
|
||
<Text style={styles.helpSub}>Immediate Assistance</Text>
|
||
</TouchableOpacity>
|
||
|
||
<TouchableOpacity
|
||
style={styles.helpItem}
|
||
onPress={() =>
|
||
navigation.navigate(RouteNames.BottomTabs, {
|
||
screen: RouteNames.Profile,
|
||
})
|
||
}
|
||
>
|
||
<ClipboardIcon size={24} color={colors.primary} />
|
||
<Text style={styles.helpLabel}>FAQ Center</Text>
|
||
<Text style={styles.helpSub}>Help & Guides</Text>
|
||
</TouchableOpacity>
|
||
|
||
<TouchableOpacity
|
||
style={styles.helpItem}
|
||
onPress={() =>
|
||
Alert.alert(
|
||
'Support Chat',
|
||
'Initiating live helpdesk agent chat...',
|
||
)
|
||
}
|
||
>
|
||
<PersonIcon size={24} color={colors.statusOnDelivery} />
|
||
<Text style={styles.helpLabel}>Live Support</Text>
|
||
<Text style={styles.helpSub}>24/7 Chat Help</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</ScrollView>
|
||
|
||
{/* Active Job Floating Bottom Banner */}
|
||
{jobStatus !== JobStatus.Idle &&
|
||
jobStatus !== JobStatus.NewRequest &&
|
||
activeJob && (
|
||
<TouchableOpacity
|
||
style={{
|
||
position: 'absolute',
|
||
bottom: spacing.xs,
|
||
left: spacing.md,
|
||
right: spacing.md,
|
||
backgroundColor: colors.primary,
|
||
borderRadius: borderRadius.md,
|
||
paddingVertical: spacing.md,
|
||
paddingHorizontal: spacing.lg,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
...shadow.lg,
|
||
}}
|
||
activeOpacity={0.9}
|
||
onPress={handleActiveJobBannerPress}
|
||
>
|
||
<View
|
||
style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}
|
||
>
|
||
<ClipboardIcon size={20} color={colors.white} />
|
||
<View style={{ marginLeft: spacing.sm, flex: 1 }}>
|
||
<Text
|
||
style={{
|
||
color: colors.white,
|
||
fontWeight: 'bold',
|
||
fontSize: typography.fontSize.sm,
|
||
}}
|
||
>
|
||
Order {activeJob.orderId}
|
||
</Text>
|
||
<Text
|
||
style={{
|
||
color: colors.primaryLight,
|
||
fontSize: typography.fontSize.xs,
|
||
}}
|
||
numberOfLines={1}
|
||
>
|
||
{jobStatus === JobStatus.Accepted &&
|
||
'Heading to pickup store...'}
|
||
{jobStatus === JobStatus.ArrivedAtStore &&
|
||
'At pickup store, check items...'}
|
||
{jobStatus === JobStatus.PickedUp &&
|
||
'Order picked up, routing to customer...'}
|
||
{jobStatus === JobStatus.EnRoute &&
|
||
'On the way to customer...'}
|
||
{jobStatus === JobStatus.ArrivedAtCustomer &&
|
||
'Arrived at customer location...'}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
<View
|
||
style={{
|
||
backgroundColor: colors.white,
|
||
paddingHorizontal: spacing.md,
|
||
paddingVertical: 6,
|
||
borderRadius: borderRadius.sm,
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
color: colors.primary,
|
||
fontWeight: 'bold',
|
||
fontSize: typography.fontSize.xs,
|
||
}}
|
||
>
|
||
View
|
||
</Text>
|
||
</View>
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
// export default DashboardScreen;
|