297 lines
11 KiB
TypeScript
297 lines
11 KiB
TypeScript
import React, { useEffect, useState, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
ScrollView,
|
|
ActivityIndicator,
|
|
TouchableOpacity,
|
|
} from 'react-native';
|
|
import { useRoute, useNavigation } from '@react-navigation/native';
|
|
import { StackNavigationProp } from '@react-navigation/stack';
|
|
import { RouteProp } from '@react-navigation/native';
|
|
import { Header, OrderStatusTimeline, ProductRatingModal } from '@components';
|
|
import { useAppTheme } from '@theme';
|
|
import { getStyles } from './orderDetailsScreen.styles';
|
|
import { RootState, useAppDispatch, useAppSelector } from '@store';
|
|
import { getOrderByIdThunk } from '../checkoutPaymentScreen';
|
|
import { formatDate, formatTime } from '@utils/helper';
|
|
import { submitReview } from './thunk';
|
|
import { giveRatingPayload, OrderItem } from '@interfaces';
|
|
import { AppStackParamList } from '@navigation';
|
|
|
|
type OrderDetailsRouteProp = RouteProp<AppStackParamList, 'OrderDetailsScreen'>;
|
|
type OrderDetailsNavProp = StackNavigationProp<AppStackParamList>;
|
|
|
|
export const OrderDetailsScreen: React.FC = () => {
|
|
const { colors } = useAppTheme();
|
|
const styles = getStyles(colors);
|
|
const route = useRoute<OrderDetailsRouteProp>();
|
|
const navigation = useNavigation<OrderDetailsNavProp>();
|
|
const dispatch = useAppDispatch();
|
|
|
|
const orderId = route.params?.orderId;
|
|
|
|
const { orderDetails, orderDetailsLoading, orderDetailsError } =
|
|
useAppSelector((state: RootState) => state.paymentMethods);
|
|
|
|
// ── Rating modal state ────────────────────────────────────────────────────
|
|
const [ratingModalItem, setRatingModalItem] = useState<OrderItem | null>(
|
|
null,
|
|
);
|
|
/** Tracks which productIds have already been rated this session */
|
|
const [ratedProductIds, setRatedProductIds] = useState<Set<string>>(
|
|
new Set(),
|
|
);
|
|
|
|
const isDelivered = orderDetails?.status === 'DELIVERED';
|
|
|
|
useEffect(() => {
|
|
if (orderId) {
|
|
dispatch(getOrderByIdThunk(orderId));
|
|
}
|
|
}, [dispatch, orderId]);
|
|
|
|
const handleRatingSubmit = useCallback(
|
|
(payload: giveRatingPayload) => {
|
|
if (!ratingModalItem) return;
|
|
// Dispatch the API call: POST /products/:productId/ratings
|
|
dispatch(
|
|
submitReview({
|
|
id: ratingModalItem.productId,
|
|
payload: {
|
|
rating: payload.rating,
|
|
comment: payload.comment,
|
|
images: payload.images,
|
|
orderId: orderDetails?.id ?? '',
|
|
},
|
|
}),
|
|
)
|
|
.unwrap()
|
|
.then(() => {
|
|
setRatedProductIds(prev =>
|
|
new Set(prev).add(ratingModalItem.productId),
|
|
);
|
|
});
|
|
},
|
|
[dispatch, ratingModalItem, orderDetails?.id],
|
|
);
|
|
|
|
const handleModalClose = useCallback(() => {
|
|
setRatingModalItem(null);
|
|
}, []);
|
|
|
|
// ── Loading ───────────────────────────────────────────────────────────────
|
|
if (orderDetailsLoading) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Header title="Order Details" onBack={() => navigation.goBack()} />
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={colors.primary} />
|
|
<Text style={styles.loadingText}>Fetching order details...</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (orderDetailsError || !orderDetails) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Header title="Order Details" onBack={() => navigation.goBack()} />
|
|
<View style={styles.errorContainer}>
|
|
<Text style={styles.errorText}>
|
|
{orderDetailsError || 'Could not load order details.'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const orderDate = orderDetails.createdAt
|
|
? `${formatDate(orderDetails.createdAt)} at ${formatTime(
|
|
orderDetails.createdAt,
|
|
)}`
|
|
: '';
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Header title="Order Details" onBack={() => navigation.goBack()} />
|
|
<ScrollView contentContainerStyle={styles.content}>
|
|
{/* Top Info Section */}
|
|
<View style={styles.section}>
|
|
<View style={styles.headerRow}>
|
|
<Text style={styles.orderNumber}>
|
|
Order {orderDetails.orderNumber?.split('-').pop() || ''}
|
|
</Text>
|
|
<View style={styles.statusBadge}>
|
|
<Text style={styles.statusText}>{orderDetails.status}</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={styles.dateText}>Placed on {orderDate}</Text>
|
|
<Text style={styles.dateText}>
|
|
Payment: {orderDetails.paymentMethod}
|
|
</Text>
|
|
|
|
{/* Merchant Info */}
|
|
<View style={styles.merchantRow}>
|
|
<View style={styles.merchantIcon}>
|
|
<Text>🏪</Text>
|
|
</View>
|
|
<Text style={styles.merchantName}>
|
|
{orderDetails.merchant?.name || 'Store'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Order Status Timeline Section */}
|
|
<View style={styles.timelineSection}>
|
|
<Text style={styles.sectionTitle}>Order Status</Text>
|
|
<OrderStatusTimeline
|
|
tracking={orderDetails.tracking}
|
|
currentStatus={orderDetails.status}
|
|
deliveryPartner={orderDetails?.delivery?.deliveryPartner?.user}
|
|
/>
|
|
</View>
|
|
|
|
{/* Order Items Section */}
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Order Items</Text>
|
|
|
|
{/* Rate all hint — shown only when delivered */}
|
|
{isDelivered && (
|
|
<View style={styles.rateHintBanner}>
|
|
<Text style={styles.rateHintEmoji}>⭐</Text>
|
|
<Text style={styles.rateHintText}>
|
|
Tap any item below to rate it
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{orderDetails.orderItems?.map((item, index) => {
|
|
const isRated = ratedProductIds.has(item.productId);
|
|
return (
|
|
<TouchableOpacity
|
|
key={item.id || index}
|
|
style={[styles.itemRow, isDelivered && styles.itemRowTappable]}
|
|
activeOpacity={isDelivered ? 0.65 : 1}
|
|
onPress={() => {
|
|
if (isDelivered) {
|
|
setRatingModalItem(item);
|
|
}
|
|
}}
|
|
disabled={!isDelivered}
|
|
>
|
|
<View style={styles.itemInfo}>
|
|
<View style={styles.itemQuantityBadge}>
|
|
<Text style={styles.itemQuantityText}>
|
|
{item.quantity}x
|
|
</Text>
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={styles.itemName}>{item.name}</Text>
|
|
{/* Rating status pill — only shown when DELIVERED */}
|
|
{isDelivered && (
|
|
<View style={styles.ratingPillRow}>
|
|
{isRated ? (
|
|
<View style={styles.ratedPill}>
|
|
<Text style={styles.ratedPillText}>✓ Rated</Text>
|
|
</View>
|
|
) : (
|
|
<View style={styles.ratePill}>
|
|
<Text style={styles.ratePillText}>
|
|
⭐ Rate this
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
<Text style={styles.itemPrice}>₹{item.totalAmount}</Text>
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</View>
|
|
|
|
{/* Bill Summary Section */}
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Bill Summary</Text>
|
|
<View style={styles.billRow}>
|
|
<Text style={styles.billLabel}>Item Total</Text>
|
|
<Text style={styles.billValue}>₹{orderDetails.subtotal}</Text>
|
|
</View>
|
|
<View style={styles.billRow}>
|
|
<Text style={styles.billLabel}>Delivery Fee</Text>
|
|
<Text style={styles.billValue}>₹{orderDetails.deliveryFee}</Text>
|
|
</View>
|
|
<View style={styles.billRow}>
|
|
<Text style={styles.billLabel}>Platform Fee</Text>
|
|
<Text style={styles.billValue}>₹{orderDetails.platformFee}</Text>
|
|
</View>
|
|
<View style={styles.billRow}>
|
|
<Text style={styles.billLabel}>Taxes</Text>
|
|
<Text style={styles.billValue}>₹{orderDetails.taxAmount}</Text>
|
|
</View>
|
|
|
|
<View style={styles.billDivider} />
|
|
|
|
<View style={styles.billRow}>
|
|
<Text style={styles.billTotalLabel}>Grand Total</Text>
|
|
<Text style={styles.billTotalValue}>
|
|
₹{orderDetails.totalAmount}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Delivery Address Section */}
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Delivery Details</Text>
|
|
<Text style={styles.addressType}>
|
|
{orderDetails.dropAddress?.label || 'Home'}
|
|
</Text>
|
|
<Text style={styles.addressText}>
|
|
{orderDetails.dropAddress?.houseNumber
|
|
? `${orderDetails.dropAddress.houseNumber}, `
|
|
: ''}
|
|
{orderDetails.dropAddress?.addressLine1}
|
|
</Text>
|
|
<Text style={styles.addressText}>
|
|
{orderDetails.dropAddress?.landmark
|
|
? `Landmark: ${orderDetails.dropAddress.landmark}`
|
|
: ''}
|
|
</Text>
|
|
<Text style={styles.addressText}>
|
|
Phone: {orderDetails.dropAddress?.phone}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Payment Info */}
|
|
{orderDetails.payments && orderDetails.payments.length > 0 && (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Payment Details</Text>
|
|
<View style={styles.paymentRow}>
|
|
<Text style={styles.paymentIcon}>💳</Text>
|
|
<Text style={styles.paymentText}>
|
|
Paid via {orderDetails.payments[0].method} (
|
|
{orderDetails.payments[0].status})
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
|
|
{/* Product Rating Modal */}
|
|
<ProductRatingModal
|
|
visible={ratingModalItem !== null}
|
|
productId={ratingModalItem?.productId ?? ''}
|
|
productName={ratingModalItem?.name ?? ''}
|
|
orderId={orderDetails?.id}
|
|
quantity={ratingModalItem?.quantity}
|
|
onSubmit={handleRatingSubmit}
|
|
onClose={handleModalClose}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default OrderDetailsScreen;
|