206 lines
7.1 KiB
TypeScript
206 lines
7.1 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { View, Text, Animated, Easing } from 'react-native';
|
|
import { useAppTheme } from '@theme';
|
|
import { PrimaryButton, LocationRow } from '@components';
|
|
import { useAppDispatch, useAppSelector } from '@store';
|
|
import {
|
|
respondToOfferThunk,
|
|
clearDeliveryOffer,
|
|
} from '@store/commonReducers/delivery';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
import { AppStackParamList } from '@navigation/navigationTypes';
|
|
import { RouteNames } from '@utils/constants';
|
|
import { formatCurrency } from '@utils/formatters';
|
|
import { getStyles } from './newJobRequestScreen.styles';
|
|
|
|
const TOTAL_SECONDS = 15;
|
|
|
|
export const NewJobRequestScreen: React.FC = () => {
|
|
const { colors } = useAppTheme();
|
|
const styles = getStyles(colors);
|
|
const dispatch = useAppDispatch();
|
|
const navigation =
|
|
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
|
|
|
|
// Read from the socket-driven delivery slice
|
|
const currentOffer = useAppSelector(state => state.delivery.currentOffer);
|
|
const respondLoading = useAppSelector(
|
|
state => state.delivery.respondLoading,
|
|
);
|
|
|
|
const [secondsLeft, setSecondsLeft] = useState(TOTAL_SECONDS);
|
|
|
|
// Animated values
|
|
const pulseAnim = useRef(new Animated.Value(1)).current;
|
|
|
|
// ── Countdown timer ──────────────────────────────────────────────────────
|
|
useEffect(() => {
|
|
if (secondsLeft <= 0) {
|
|
handleReject();
|
|
return;
|
|
}
|
|
const timer = setInterval(() => {
|
|
setSecondsLeft(prev => prev - 1);
|
|
}, 1000);
|
|
return () => clearInterval(timer);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [secondsLeft]);
|
|
|
|
// ── Pulse animation on Accept button when < 5 s remain ──────────────────
|
|
useEffect(() => {
|
|
if (secondsLeft <= 5 && secondsLeft > 0) {
|
|
Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1.06,
|
|
duration: 400,
|
|
easing: Easing.inOut(Easing.ease),
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1,
|
|
duration: 400,
|
|
easing: Easing.inOut(Easing.ease),
|
|
useNativeDriver: true,
|
|
}),
|
|
]),
|
|
).start();
|
|
} else {
|
|
pulseAnim.setValue(1);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [secondsLeft <= 5]);
|
|
|
|
// ── Handlers ─────────────────────────────────────────────────────────────
|
|
const handleAccept = () => {
|
|
if (!currentOffer) return;
|
|
dispatch(
|
|
respondToOfferThunk({
|
|
deliveryId: currentOffer.deliveryId,
|
|
action: 'ACCEPT',
|
|
}),
|
|
)
|
|
.unwrap()
|
|
.then(() => {
|
|
navigation.navigate(RouteNames.OrderAccepted);
|
|
})
|
|
.catch(() => {
|
|
// stay on screen — respondError will be set in redux
|
|
});
|
|
};
|
|
|
|
const handleReject = () => {
|
|
if (currentOffer) {
|
|
dispatch(
|
|
respondToOfferThunk({
|
|
deliveryId: currentOffer.deliveryId,
|
|
action: 'REJECT',
|
|
}),
|
|
);
|
|
}
|
|
dispatch(clearDeliveryOffer());
|
|
navigation.navigate(RouteNames.BottomTabs, {
|
|
screen: RouteNames.Dashboard,
|
|
});
|
|
};
|
|
|
|
if (!currentOffer) return null;
|
|
|
|
// Timer text color: warning when < 5s
|
|
const timerColor = secondsLeft <= 5 ? colors.error : colors.primary;
|
|
const timerBorderColor =
|
|
secondsLeft <= 5 ? colors.error : colors.primary;
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={styles.modalContent}>
|
|
{/* ── Header Row ─────────────────────────────────────────────────── */}
|
|
<View style={styles.headerRow}>
|
|
<Text style={styles.modalTitle}>New Delivery Request</Text>
|
|
|
|
{/* Circular countdown timer (pure RN — no SVG needed) */}
|
|
<View
|
|
style={[
|
|
styles.timerWrapper,
|
|
{ borderColor: timerBorderColor },
|
|
]}
|
|
>
|
|
<Text style={[styles.timerText, { color: timerColor }]}>
|
|
{secondsLeft}s
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* ── Earnings Badge ─────────────────────────────────────────────── */}
|
|
<View
|
|
style={[
|
|
styles.earningsBadge,
|
|
{ backgroundColor: colors.primary },
|
|
]}
|
|
>
|
|
<Text style={styles.earningsBadgeLabel}>Estimated Earnings</Text>
|
|
<Text style={styles.earningsBadgeValue}>
|
|
{formatCurrency(currentOffer.totalAmount)}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* ── Route Details ──────────────────────────────────────────────── */}
|
|
<View style={styles.routeContainer}>
|
|
<LocationRow
|
|
type="pickup"
|
|
name="Pickup Location"
|
|
address={currentOffer.pickupAddress.label}
|
|
/>
|
|
<View style={styles.spacer} />
|
|
<LocationRow
|
|
type="drop"
|
|
name="Drop Location"
|
|
address={currentOffer.dropAddress.label}
|
|
/>
|
|
</View>
|
|
|
|
{/* ── Distance & Order ID ────────────────────────────────────────── */}
|
|
<View style={styles.detailsRow}>
|
|
<View style={styles.detailBox}>
|
|
<Text style={styles.detailLabel}>Distance</Text>
|
|
<Text style={styles.detailValue}>
|
|
{currentOffer.distanceKm} km
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.divider} />
|
|
|
|
<View style={styles.detailBox}>
|
|
<Text style={styles.detailLabel}>Order</Text>
|
|
<Text style={styles.detailValue}>#{currentOffer.orderId}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* ── Action Buttons ─────────────────────────────────────────────── */}
|
|
<View style={styles.btnRow}>
|
|
<PrimaryButton
|
|
title="Reject"
|
|
onPress={handleReject}
|
|
style={styles.rejectBtn}
|
|
textStyle={{ color: colors.text }}
|
|
disabled={respondLoading}
|
|
/>
|
|
<Animated.View
|
|
style={[{ flex: 2, transform: [{ scale: pulseAnim }] }]}
|
|
>
|
|
<PrimaryButton
|
|
title={respondLoading ? 'Accepting...' : 'Accept'}
|
|
onPress={handleAccept}
|
|
style={[styles.acceptBtn, { flex: undefined }]}
|
|
disabled={respondLoading}
|
|
/>
|
|
</Animated.View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default NewJobRequestScreen;
|