378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
TouchableOpacity,
|
|
Linking,
|
|
Platform,
|
|
LayoutAnimation,
|
|
UIManager,
|
|
} from 'react-native';
|
|
import { OrderStatus, TrackingEntry } from '@interfaces';
|
|
import { formatDate, formatTime } from '@utils/helper';
|
|
import { OrderStatusTimelineProps } from './OrderStatusTimeline.props';
|
|
import { useAppTheme } from '@theme';
|
|
import { getStyles } from './OrderStatusTimeline.styles';
|
|
|
|
if (
|
|
Platform.OS === 'android' &&
|
|
UIManager.setLayoutAnimationEnabledExperimental
|
|
) {
|
|
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
}
|
|
|
|
export const STATUS_FLOW: OrderStatus[] = [
|
|
'PENDING',
|
|
'CONFIRMED',
|
|
'PREPARING',
|
|
'READY_FOR_PICKUP',
|
|
'OUT_FOR_DELIVERY',
|
|
'DELIVERED',
|
|
];
|
|
|
|
export const STATUS_LABEL: Record<OrderStatus, string> = {
|
|
PENDING: 'Order Placed',
|
|
CONFIRMED: 'Confirmed',
|
|
PREPARING: 'Preparing',
|
|
READY_FOR_PICKUP: 'Ready for Pickup',
|
|
OUT_FOR_DELIVERY: 'Out for Delivery',
|
|
DELIVERED: 'Delivered',
|
|
CANCELLED: 'Cancelled',
|
|
};
|
|
|
|
const STATUS_DETAILS: Record<
|
|
OrderStatus,
|
|
{ title: string; desc: string; icon: string }
|
|
> = {
|
|
PENDING: {
|
|
title: 'Waiting for Confirmation',
|
|
desc: 'The merchant is reviewing your order. We will start preparing it soon.',
|
|
icon: '⏳',
|
|
},
|
|
CONFIRMED: {
|
|
title: 'Order Confirmed',
|
|
desc: 'Your order has been accepted. The chef will start preparing it shortly.',
|
|
icon: '✅',
|
|
},
|
|
PREPARING: {
|
|
title: 'Preparing Your Food',
|
|
desc: 'The kitchen is preparing your delicious order with fresh ingredients.',
|
|
icon: '🍳',
|
|
},
|
|
READY_FOR_PICKUP: {
|
|
title: 'Ready for Pickup',
|
|
desc: 'Your order is ready. The delivery partner is picking it up now.',
|
|
icon: '📦',
|
|
},
|
|
OUT_FOR_DELIVERY: {
|
|
title: 'Out for Delivery',
|
|
desc: 'Our delivery partner is on the way with your food. Keep your phone handy!',
|
|
icon: '🛵',
|
|
},
|
|
DELIVERED: {
|
|
title: 'Order Delivered',
|
|
desc: 'Your order has been successfully delivered. Enjoy your meal!',
|
|
icon: '🎉',
|
|
},
|
|
CANCELLED: {
|
|
title: 'Order Cancelled',
|
|
desc: 'This order was cancelled. Any refund will be processed shortly.',
|
|
icon: '❌',
|
|
},
|
|
};
|
|
|
|
const getStepIndex = (status: OrderStatus): number => {
|
|
switch (status) {
|
|
case 'PENDING':
|
|
case 'CONFIRMED':
|
|
return 0;
|
|
case 'PREPARING':
|
|
return 1;
|
|
case 'READY_FOR_PICKUP':
|
|
case 'OUT_FOR_DELIVERY':
|
|
return 2;
|
|
case 'DELIVERED':
|
|
return 3;
|
|
default:
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
export const OrderStatusTimeline: React.FC<OrderStatusTimelineProps> = ({
|
|
tracking = [],
|
|
currentStatus,
|
|
styles: propStyles,
|
|
}) => {
|
|
const { colors } = useAppTheme();
|
|
const localStyles = getStyles(colors);
|
|
|
|
// If the caller passes the screen styles, they won't have timelineRow etc, so we fallback to localStyles
|
|
const styles =
|
|
propStyles && propStyles.timelineRow ? propStyles : localStyles;
|
|
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
const toggleExpand = () => {
|
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
setExpanded(!expanded);
|
|
};
|
|
|
|
const handleCall = () => {
|
|
Linking.openURL('tel:+919876543210');
|
|
};
|
|
|
|
const handleMessage = () => {
|
|
Linking.openURL('sms:+919876543210');
|
|
};
|
|
|
|
const trackingMap = tracking.reduce<Record<string, string>>((acc, t) => {
|
|
acc[t.status] = t.time;
|
|
return acc;
|
|
}, {});
|
|
|
|
const details = STATUS_DETAILS[currentStatus] || STATUS_DETAILS.PENDING;
|
|
|
|
if (currentStatus === 'CANCELLED') {
|
|
return (
|
|
<View style={localStyles.cardContainer}>
|
|
{/* Cancelled Status Banner */}
|
|
<View style={localStyles.headerSection}>
|
|
<View
|
|
style={[
|
|
localStyles.headerIconContainer,
|
|
{ backgroundColor: '#FFEBEE' },
|
|
]}
|
|
>
|
|
<Text style={localStyles.headerIconText}>❌</Text>
|
|
</View>
|
|
<View style={localStyles.headerTextContainer}>
|
|
<Text
|
|
style={[localStyles.statusHeaderTitle, { color: colors.error }]}
|
|
>
|
|
{details.title}
|
|
</Text>
|
|
<Text style={localStyles.statusHeaderDesc}>{details.desc}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Collapsible detailed view */}
|
|
<View style={localStyles.toggleSection}>
|
|
<TouchableOpacity
|
|
onPress={toggleExpand}
|
|
style={localStyles.toggleButton}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Text style={localStyles.toggleButtonText}>
|
|
{expanded ? 'Hide Details' : 'View Detailed History'}
|
|
</Text>
|
|
<Text style={localStyles.toggleChevron}>
|
|
{expanded ? '▲' : '▼'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{expanded && (
|
|
<View style={localStyles.detailedTimelineWrapper}>
|
|
<View style={styles.timelineRow}>
|
|
<View style={styles.timelineDotColumn}>
|
|
<View
|
|
style={[styles.timelineDot, styles.timelineDotCancelled]}
|
|
/>
|
|
</View>
|
|
<View style={styles.timelineContent}>
|
|
<Text style={styles.timelineLabelCancelled}>
|
|
Order Cancelled
|
|
</Text>
|
|
{trackingMap['CANCELLED'] && (
|
|
<Text style={styles.timelineTime}>
|
|
{formatDate(trackingMap['CANCELLED'])} at{' '}
|
|
{formatTime(trackingMap['CANCELLED'])}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const activeStep = getStepIndex(currentStatus);
|
|
const steps = [
|
|
{ label: 'Placed', status: 'PENDING' },
|
|
{ label: 'Preparing', status: 'PREPARING' },
|
|
{ label: 'On the Way', status: 'OUT_FOR_DELIVERY' },
|
|
{ label: 'Delivered', status: 'DELIVERED' },
|
|
];
|
|
|
|
const showDriver = [
|
|
'PREPARING',
|
|
'READY_FOR_PICKUP',
|
|
'OUT_FOR_DELIVERY',
|
|
'DELIVERED',
|
|
].includes(currentStatus);
|
|
const currentIndex = STATUS_FLOW.indexOf(currentStatus);
|
|
|
|
return (
|
|
<View style={localStyles.cardContainer}>
|
|
{/* Current Status Header Banner */}
|
|
<View style={localStyles.headerSection}>
|
|
<View style={localStyles.headerIconContainer}>
|
|
<Text style={localStyles.headerIconText}>{details.icon}</Text>
|
|
</View>
|
|
<View style={localStyles.headerTextContainer}>
|
|
<Text style={localStyles.statusHeaderTitle}>{details.title}</Text>
|
|
<Text style={localStyles.statusHeaderDesc}>{details.desc}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Horizontal Progress Stepper */}
|
|
<View style={localStyles.stepperWrapper}>
|
|
<View style={localStyles.stepperRow}>
|
|
<View style={localStyles.stepLineBackground} />
|
|
<View
|
|
style={[
|
|
localStyles.stepLineActive,
|
|
{ width: `${(activeStep / (steps.length - 1)) * 76}%` },
|
|
]}
|
|
/>
|
|
|
|
{steps.map((step, idx) => {
|
|
const isCompleted = idx < activeStep;
|
|
const isActive = idx === activeStep;
|
|
|
|
return (
|
|
<View key={step.label} style={localStyles.stepItem}>
|
|
<View
|
|
style={[
|
|
localStyles.stepDot,
|
|
isCompleted && localStyles.stepDotCompleted,
|
|
isActive && localStyles.stepDotActive,
|
|
]}
|
|
>
|
|
{isCompleted ? (
|
|
<Text style={localStyles.stepCheckmark}>✓</Text>
|
|
) : isActive ? (
|
|
<View style={localStyles.stepInnerDotActive} />
|
|
) : null}
|
|
</View>
|
|
<Text
|
|
style={[
|
|
localStyles.stepLabel,
|
|
(isCompleted || isActive) && localStyles.stepLabelActive,
|
|
]}
|
|
>
|
|
{step.label}
|
|
</Text>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Delivery Partner Details (if driver assigned) */}
|
|
{showDriver && (
|
|
<View style={localStyles.driverSection}>
|
|
<View style={localStyles.driverInfoRow}>
|
|
<View style={localStyles.driverAvatarContainer}>
|
|
<Text style={localStyles.driverAvatarText}>RS</Text>
|
|
</View>
|
|
<View style={localStyles.driverMeta}>
|
|
<Text style={localStyles.driverRoleText}>Your Delivery Hero</Text>
|
|
<Text style={localStyles.driverNameText}>Rahul Sharma</Text>
|
|
<Text style={localStyles.driverVehicleText}>
|
|
KA-01-AB-1234 • Honda Activa
|
|
</Text>
|
|
</View>
|
|
<View style={localStyles.ratingBadge}>
|
|
<Text style={localStyles.ratingText}>⭐ 4.8</Text>
|
|
</View>
|
|
</View>
|
|
<View style={localStyles.driverActionsRow}>
|
|
<TouchableOpacity
|
|
style={[localStyles.actionButton, localStyles.callButton]}
|
|
onPress={handleCall}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Text style={localStyles.actionIcon}>📞</Text>
|
|
<Text style={localStyles.callButtonText}>Call Partner</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[localStyles.actionButton, localStyles.messageButton]}
|
|
onPress={handleMessage}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Text style={localStyles.actionIcon}>💬</Text>
|
|
<Text style={localStyles.messageButtonText}>Chat</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* View Detailed History Toggle */}
|
|
<View style={localStyles.toggleSection}>
|
|
<TouchableOpacity
|
|
onPress={toggleExpand}
|
|
style={localStyles.toggleButton}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Text style={localStyles.toggleButtonText}>
|
|
{expanded ? 'Hide Details' : 'View Detailed History'}
|
|
</Text>
|
|
<Text style={localStyles.toggleChevron}>{expanded ? '▲' : '▼'}</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Detailed Vertical Logs */}
|
|
{expanded && (
|
|
<View style={localStyles.detailedTimelineWrapper}>
|
|
{STATUS_FLOW.map((status, index) => {
|
|
const isCompleted = index <= currentIndex;
|
|
const isLast = index === STATUS_FLOW.length - 1;
|
|
const time = trackingMap[status];
|
|
|
|
return (
|
|
<View key={status} style={styles.timelineRow}>
|
|
<View style={styles.timelineDotColumn}>
|
|
<View
|
|
style={[
|
|
styles.timelineDot,
|
|
isCompleted && styles.timelineDotCompleted,
|
|
]}
|
|
>
|
|
{isCompleted && <Text style={styles.timelineCheck}>✓</Text>}
|
|
</View>
|
|
{!isLast && (
|
|
<View
|
|
style={[
|
|
styles.timelineLine,
|
|
index < currentIndex && styles.timelineLineCompleted,
|
|
]}
|
|
/>
|
|
)}
|
|
</View>
|
|
<View style={styles.timelineContent}>
|
|
<Text
|
|
style={
|
|
isCompleted
|
|
? styles.timelineLabelCompleted
|
|
: styles.timelineLabelPending
|
|
}
|
|
>
|
|
{STATUS_LABEL[status]}
|
|
</Text>
|
|
{time && (
|
|
<Text style={styles.timelineTime}>
|
|
{formatDate(time)} at {formatTime(time)}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|