333 lines
13 KiB
TypeScript
333 lines
13 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
||
import { View, Text, ScrollView, TouchableOpacity, Alert, Switch } 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/slices/authSlice';
|
||
import { setNewJobOffer } from '@store/slices/jobSlice';
|
||
import { mockJobOffer } from '@store/mockData/mockJobs';
|
||
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';
|
||
|
||
const DashboardScreen: React.FC = () => {
|
||
const { colors } = useAppTheme();
|
||
const styles = getStyles(colors);
|
||
const dispatch = useAppDispatch();
|
||
const navigation = useNavigation<NativeStackNavigationProp<AppStackParamList>>();
|
||
|
||
const isOnline = useAppSelector((state) => state.auth.isOnline);
|
||
const { todayEarnings, completedCount, onlineMinutes, weeklyData } = useAppSelector(
|
||
(state) => state.earnings
|
||
);
|
||
const { jobStatus, activeJob } = useAppSelector((state) => state.job);
|
||
|
||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
|
||
// Background simulation: when online and idle, trigger new order offer after 12 seconds
|
||
useEffect(() => {
|
||
if (isOnline && jobStatus === JobStatus.Idle) {
|
||
timerRef.current = setTimeout(() => {
|
||
dispatch(setNewJobOffer(mockJobOffer));
|
||
navigation.navigate(RouteNames.NewJobRequest);
|
||
}, 12000);
|
||
} else {
|
||
if (timerRef.current) {
|
||
clearTimeout(timerRef.current);
|
||
timerRef.current = null;
|
||
}
|
||
}
|
||
|
||
return () => {
|
||
if (timerRef.current) {
|
||
clearTimeout(timerRef.current);
|
||
}
|
||
};
|
||
}, [isOnline, jobStatus, dispatch, navigation]);
|
||
|
||
const handleToggleOnline = () => {
|
||
dispatch(setOnlineStatus(!isOnline));
|
||
};
|
||
|
||
const handleSimulateOffer = () => {
|
||
if (!isOnline) {
|
||
Alert.alert('Go Online', 'Please go online to receive order offers.');
|
||
return;
|
||
}
|
||
if (jobStatus !== JobStatus.Idle) {
|
||
Alert.alert('Active Delivery', 'You already have an active job offer or order.');
|
||
return;
|
||
}
|
||
dispatch(setNewJobOffer(mockJobOffer));
|
||
navigation.navigate(RouteNames.NewJobRequest);
|
||
};
|
||
|
||
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}>Rahul Kumar</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}
|
||
style={{ marginLeft: 8 }}
|
||
/>
|
||
</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>
|
||
|
||
{/* Developer Simulation trigger */}
|
||
<TouchableOpacity
|
||
style={styles.simulateBtn}
|
||
activeOpacity={0.8}
|
||
onPress={handleSimulateOffer}
|
||
>
|
||
<Text style={styles.simulateBtnText}>⚡ Simulate Immediate Job Offer</Text>
|
||
</TouchableOpacity>
|
||
|
||
<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;
|