430 lines
19 KiB
TypeScript
430 lines
19 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
||
import { View, ScrollView, Alert, TextInput, ActivityIndicator, KeyboardAvoidingView, Platform } from "react-native";
|
||
import { Image } from 'expo-image';
|
||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||
import { AppContainer, MyText, tw, MyTouchableOpacity, theme, BottomDialog } from "common-ui";
|
||
import { trpc } from "@/src/trpc-client";
|
||
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||
import dayjs from "dayjs";
|
||
import ComplaintForm from "@/components/ComplaintForm";
|
||
|
||
export default function OrderDetails() {
|
||
const { id } = useLocalSearchParams<{ id: string }>();
|
||
const router = useRouter();
|
||
|
||
const {
|
||
data: orderData,
|
||
isLoading,
|
||
error,
|
||
refetch,
|
||
} = trpc.user.order.getOrderById.useQuery(
|
||
{ orderId: id },
|
||
{ enabled: !!id }
|
||
);
|
||
|
||
const [isEditingNotes, setIsEditingNotes] = useState(false);
|
||
const [notesText, setNotesText] = useState("");
|
||
const [notesInput, setNotesInput] = useState("");
|
||
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
|
||
const [cancelReason, setCancelReason] = useState("");
|
||
const [complaintDialogOpen, setComplaintDialogOpen] = useState(false);
|
||
|
||
const updateNotesMutation = trpc.user.order.updateUserNotes.useMutation({
|
||
onSuccess: () => {
|
||
setNotesText(notesInput);
|
||
setIsEditingNotes(false);
|
||
refetch();
|
||
Alert.alert('Success', 'Notes updated successfully');
|
||
},
|
||
onError: (error: any) => {
|
||
Alert.alert('Error', error.message || 'Failed to update notes');
|
||
},
|
||
});
|
||
|
||
const cancelOrderMutation = trpc.user.order.cancelOrder.useMutation({
|
||
onSuccess: () => {
|
||
setCancelDialogOpen(false);
|
||
setCancelReason("");
|
||
refetch();
|
||
Alert.alert('Success', 'Order cancelled successfully');
|
||
},
|
||
onError: (error: any) => {
|
||
Alert.alert('Error', error.message || 'Failed to cancel order');
|
||
},
|
||
});
|
||
|
||
const handleCancelOrder = () => {
|
||
if (!cancelReason.trim()) {
|
||
Alert.alert('Error', 'Please enter a reason for cancellation');
|
||
return;
|
||
}
|
||
cancelOrderMutation.mutate({ id: order.orderId, reason: cancelReason });
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (orderData?.userNotes) {
|
||
setNotesText(orderData.userNotes);
|
||
setNotesInput(orderData.userNotes);
|
||
}
|
||
}, [orderData]);
|
||
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<AppContainer>
|
||
<View style={tw`flex-1 justify-center items-center bg-white`}>
|
||
<MyText style={tw`text-slate-400 font-medium`}>Loading details...</MyText>
|
||
</View>
|
||
</AppContainer>
|
||
);
|
||
}
|
||
|
||
if (error || !orderData) {
|
||
return (
|
||
<AppContainer>
|
||
<View style={tw`flex-1 justify-center items-center p-8 bg-white`}>
|
||
<MaterialIcons name="error-outline" size={48} color={theme.colors.red1} />
|
||
<MyText style={tw`text-slate-900 text-lg font-bold mt-4`}>Failed to load</MyText>
|
||
<MyTouchableOpacity
|
||
onPress={() => router.back()}
|
||
style={tw`mt-6 bg-slate-900 px-6 py-2 rounded-xl`}
|
||
>
|
||
<MyText style={tw`text-white font-bold`}>Go Back</MyText>
|
||
</MyTouchableOpacity>
|
||
</View>
|
||
</AppContainer>
|
||
);
|
||
}
|
||
|
||
const order = orderData;
|
||
const getStatusConfig = (status: string) => {
|
||
const s = status.toLowerCase();
|
||
switch (s) {
|
||
case "delivered":
|
||
case "success":
|
||
return { label: "Delivered", color: "#10B981" };
|
||
case "cancelled":
|
||
case "failed":
|
||
return { label: "Cancelled", color: "#EF4444" };
|
||
case "pending":
|
||
case "processing":
|
||
return { label: "Pending", color: "#F59E0B" };
|
||
default:
|
||
return { label: status, color: theme.colors.brand500 };
|
||
}
|
||
};
|
||
|
||
const subtotal = order.items.reduce((sum, item) => sum + item.amount, 0);
|
||
const discountAmount = order.discountAmount || 0;
|
||
const totalAmount = order.orderAmount;
|
||
const statusConfig = getStatusConfig(order.deliveryStatus || "pending");
|
||
|
||
return (
|
||
<AppContainer>
|
||
<View style={tw`flex-1 bg-slate-50`}>
|
||
{/* Simple Header */}
|
||
<View style={tw`bg-white px-6 pt-4 pb-4 border-b border-slate-100 flex-row items-center justify-between`}>
|
||
<View style={tw`flex-row items-center`}>
|
||
<View>
|
||
<MyText style={tw`text-slate-900 font-bold text-lg`}>Order #{order.orderId}</MyText>
|
||
<MyText style={tw`text-slate-400 text-xs`}>{dayjs(order.orderDate).format("DD MMM, h:mm A")}</MyText>
|
||
{order.isFlashDelivery && (
|
||
<MyText style={tw`text-amber-600 text-xs font-bold mt-1`}>⚡ 30-Minute Flash Delivery</MyText>
|
||
)}
|
||
</View>
|
||
</View>
|
||
<View style={tw`flex-row items-center gap-2`}>
|
||
<View style={[tw`px-3 py-1 rounded-full`, { backgroundColor: statusConfig.color + '10' }]}>
|
||
<MyText style={[tw`text-[10px] font-bold uppercase`, { color: statusConfig.color }]}>
|
||
{statusConfig.label}
|
||
</MyText>
|
||
</View>
|
||
{order.isFlashDelivery && (
|
||
<View style={tw`px-2 py-1 bg-amber-100 rounded-full border border-amber-200`}>
|
||
<MyText style={tw`text-[10px] font-black text-amber-700 uppercase`}>⚡</MyText>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
|
||
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={tw`p-4 pb-12`}>
|
||
{/* Flash Delivery Banner */}
|
||
{order.isFlashDelivery && (
|
||
<View style={tw`bg-gradient-to-r from-amber-50 to-yellow-50 border border-amber-200 rounded-2xl p-4 mb-4`}>
|
||
<View style={tw`flex-row items-center`}>
|
||
<MaterialIcons name="bolt" size={24} color="#D97706" />
|
||
<View style={tw`ml-3 flex-1`}>
|
||
<MyText style={tw`text-amber-900 font-bold text-sm`}>Flash Delivery Order</MyText>
|
||
<MyText style={tw`text-amber-700 text-xs mt-1`}>
|
||
Your order will be delivered within 30 minutes of placement
|
||
</MyText>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* Main Info Card */}
|
||
<View style={tw`bg-white rounded-2xl p-5 mb-4 border border-slate-100`}>
|
||
<View style={tw`flex-row items-center justify-between mb-4`}>
|
||
<View style={tw`flex-row items-center`}>
|
||
<View style={tw`w-10 h-10 rounded-full bg-slate-50 items-center justify-center mr-3`}>
|
||
<MaterialIcons name="payments" size={20} color="#64748B" />
|
||
</View>
|
||
<View>
|
||
<MyText style={tw`text-slate-400 text-[10px] font-bold uppercase`}>Payment Method</MyText>
|
||
<MyText style={tw`text-slate-900 font-bold`}>
|
||
{order.paymentMode?.toUpperCase() === "COD" ? "Cash on Delivery" : order.paymentMode}
|
||
</MyText>
|
||
</View>
|
||
</View>
|
||
<View style={tw`items-end`}>
|
||
<MyText style={tw`text-slate-400 text-[10px] font-bold uppercase`}>Status</MyText>
|
||
<MyText style={tw`text-slate-900 font-bold capitalize`}>{order.paymentStatus}</MyText>
|
||
</View>
|
||
</View>
|
||
|
||
{(order.deliveryDate || order.isFlashDelivery) && ["delivered", "success"].includes(order.deliveryStatus?.toLowerCase() || "") && (
|
||
<View style={tw`flex-row items-center pt-4 border-t border-slate-50`}>
|
||
{order.isFlashDelivery ? (
|
||
<MaterialIcons name="bolt" size={18} color="#D97706" />
|
||
) : (
|
||
<MaterialIcons name="event-available" size={18} color="#10B981" />
|
||
)}
|
||
<MyText style={[tw`text-xs ml-2 font-medium`, order.isFlashDelivery ? tw`text-amber-700` : tw`text-slate-600`]}>
|
||
{order.isFlashDelivery
|
||
? `Flash Delivered on ${dayjs(order.createdAt || order.orderDate).add(30, 'minutes').format("DD MMM YYYY, h:mm A")}`
|
||
: `Delivered on ${dayjs(order.deliveryDate).format("DD MMM YYYY, h:mm A")}`
|
||
}
|
||
</MyText>
|
||
</View>
|
||
)}
|
||
|
||
{/* Flash Delivery Info */}
|
||
{order.isFlashDelivery && !["delivered", "success"].includes(order.deliveryStatus?.toLowerCase() || "") && (
|
||
<View style={tw`flex-row items-center pt-4 border-t border-slate-50`}>
|
||
<MaterialIcons name="bolt" size={18} color="#D97706" />
|
||
<MyText style={tw`text-amber-700 text-xs ml-2 font-medium`}>
|
||
Flash Delivery: {dayjs(order.createdAt || order.orderDate).add(30, 'minutes').format("DD MMM YYYY, h:mm A")}
|
||
</MyText>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* Special Instructions */}
|
||
<View style={tw`bg-white rounded-2xl p-5 mb-4 border border-slate-100`}>
|
||
<View style={tw`flex-row items-center justify-between mb-3`}>
|
||
<MyText style={tw`text-slate-400 text-[10px] font-bold uppercase`}>Special Instructions</MyText>
|
||
{isEditingNotes ? (
|
||
<View style={tw`flex-row gap-2`}>
|
||
<MyTouchableOpacity
|
||
onPress={() => {
|
||
setIsEditingNotes(false);
|
||
setNotesInput(notesText);
|
||
}}
|
||
style={tw`px-3 py-1 bg-slate-100 rounded-lg`}
|
||
>
|
||
<MyText style={tw`text-slate-600 text-xs font-bold`}>Cancel</MyText>
|
||
</MyTouchableOpacity>
|
||
<MyTouchableOpacity
|
||
onPress={() => {
|
||
updateNotesMutation.mutate({ id: order.orderId, userNotes: notesInput });
|
||
}}
|
||
disabled={updateNotesMutation.isPending}
|
||
style={tw`px-3 py-1 bg-brand500 rounded-lg`}
|
||
>
|
||
<MyText style={tw`text-white text-xs font-bold`}>
|
||
{updateNotesMutation.isPending ? 'Saving...' : 'Save'}
|
||
</MyText>
|
||
</MyTouchableOpacity>
|
||
</View>
|
||
) : (
|
||
<MyTouchableOpacity
|
||
onPress={() => {
|
||
setNotesInput(notesText || "");
|
||
setIsEditingNotes(true);
|
||
}}
|
||
style={tw`flex-row items-center px-2 py-1 bg-slate-50 rounded-lg`}
|
||
>
|
||
<MaterialIcons name="edit" size={12} color="#64748B" />
|
||
<MyText style={tw`text-slate-500 text-xs ml-1 font-medium`}>
|
||
{notesText ? 'Edit' : 'Add'}
|
||
</MyText>
|
||
</MyTouchableOpacity>
|
||
)}
|
||
</View>
|
||
{isEditingNotes ? (
|
||
<TextInput
|
||
style={tw`bg-slate-50 border border-slate-200 rounded-xl p-3 min-h-20 text-sm text-slate-700`}
|
||
value={notesInput}
|
||
onChangeText={setNotesInput}
|
||
placeholder="Add special delivery instructions..."
|
||
placeholderTextColor="#94A3B8"
|
||
multiline
|
||
numberOfLines={4}
|
||
textAlignVertical="top"
|
||
/>
|
||
) : (
|
||
<MyText style={tw`text-slate-700 text-sm leading-5`}>
|
||
{notesText || "No instructions added"}
|
||
</MyText>
|
||
)}
|
||
</View>
|
||
|
||
{/* Cancellation Detail */}
|
||
{order.cancelReason && (
|
||
<View style={tw`bg-rose-50 border border-rose-100 rounded-2xl p-5 mb-4`}>
|
||
<MyText style={tw`text-rose-700 text-[10px] font-bold uppercase mb-2`}>Cancellation Reason</MyText>
|
||
<MyText style={tw`text-rose-900 text-sm font-medium`}>{order.cancelReason}</MyText>
|
||
{order.refundAmount && (
|
||
<View style={tw`mt-3 pt-3 border-t border-rose-200/50 flex-row justify-between items-center`}>
|
||
<MyText style={tw`text-rose-700 text-xs`}>Refund Amount</MyText>
|
||
<MyText style={tw`text-rose-900 font-bold text-base`}>₹{order.refundAmount}</MyText>
|
||
</View>
|
||
)}
|
||
</View>
|
||
)}
|
||
|
||
{/* Items Section */}
|
||
<View style={tw`mb-2 px-1`}>
|
||
<MyText style={tw`text-slate-900 font-bold text-base mb-3`}>Order Items</MyText>
|
||
</View>
|
||
|
||
{order.items.map((item: any, index: number) => (
|
||
<View key={index} style={tw`bg-white rounded-2xl p-3 mb-3 border border-slate-100 flex-row items-center shadow-sm`}>
|
||
<View style={tw`w-14 h-14 rounded-xl bg-slate-50 border border-slate-100 p-1 mr-4`}>
|
||
<Image
|
||
source={{ uri: item.image || undefined }}
|
||
style={tw`w-full h-full rounded-lg`}
|
||
contentFit="cover"
|
||
/>
|
||
</View>
|
||
<View style={tw`flex-1`}>
|
||
<MyText style={tw`text-slate-900 font-bold text-sm`} numberOfLines={1}>{item.productName}</MyText>
|
||
<MyText style={tw`text-slate-400 text-xs mt-1`}>{item.quantity} × ₹{item.price}</MyText>
|
||
</View>
|
||
<MyText style={tw`text-slate-900 font-bold text-base ml-2`}>₹{item.amount}</MyText>
|
||
</View>
|
||
))}
|
||
|
||
{/* Coupon */}
|
||
{order.couponCode && (
|
||
<View style={tw`bg-emerald-50 border border-emerald-100 rounded-2xl p-4 mb-4 mt-2 flex-row items-center justify-between`}>
|
||
<View style={tw`flex-row items-center`}>
|
||
<MaterialIcons name="local-offer" size={20} color="#059669" />
|
||
<View style={tw`ml-3`}>
|
||
<MyText style={tw`text-emerald-900 font-bold text-sm`}>{order.couponCode}</MyText>
|
||
<MyText style={tw`text-emerald-600 text-[10px]`}>Coupon Applied</MyText>
|
||
</View>
|
||
</View>
|
||
<MyText style={tw`text-emerald-700 font-bold`}>-₹{order.discountAmount}</MyText>
|
||
</View>
|
||
)}
|
||
|
||
{/* Summary Section */}
|
||
<View style={tw`bg-white rounded-2xl p-5 mb-6 border border-slate-100`}>
|
||
<View style={tw`flex-row justify-between mb-3`}>
|
||
<MyText style={tw`text-slate-500 text-sm`}>Subtotal</MyText>
|
||
<MyText style={tw`text-slate-900 font-medium`}>₹{subtotal}</MyText>
|
||
</View>
|
||
{discountAmount > 0 && (
|
||
<View style={tw`flex-row justify-between mb-3`}>
|
||
<MyText style={tw`text-emerald-600 text-sm`}>Discount</MyText>
|
||
<MyText style={tw`text-emerald-600 font-medium`}>-₹{discountAmount}</MyText>
|
||
</View>
|
||
)}
|
||
<View style={tw`pt-4 border-t border-slate-50 flex-row justify-between items-center`}>
|
||
<MyText style={tw`text-slate-900 font-bold text-base`}>Total Amount</MyText>
|
||
<MyText style={tw`text-slate-900 font-bold text-xl`}>₹{totalAmount}</MyText>
|
||
</View>
|
||
</View>
|
||
|
||
{/* Footer Actions */}
|
||
<View style={tw`flex-row gap-3`}>
|
||
<MyTouchableOpacity
|
||
onPress={() => router.back()}
|
||
style={tw`flex-1 bg-slate-100 py-3.5 rounded-xl items-center`}
|
||
>
|
||
<MyText style={tw`text-slate-600 font-bold`}>Dismiss</MyText>
|
||
</MyTouchableOpacity>
|
||
<MyTouchableOpacity
|
||
onPress={() => setComplaintDialogOpen(true)}
|
||
style={[tw`flex-1 py-3.5 rounded-xl items-center`, { backgroundColor: theme.colors.brand600 }]}
|
||
>
|
||
<MyText style={tw`text-white font-bold`}>Raise Complaint</MyText>
|
||
</MyTouchableOpacity>
|
||
</View>
|
||
|
||
{/* Additional Actions */}
|
||
<View style={tw`mt-4`}>
|
||
{order.deliveryStatus !== 'success' && order.deliveryStatus !== 'cancelled' && (
|
||
<MyTouchableOpacity
|
||
onPress={() => setCancelDialogOpen(true)}
|
||
style={tw`bg-red-50 border border-red-100 py-3.5 rounded-xl items-center flex-row justify-center`}
|
||
>
|
||
<MaterialIcons name="cancel" size={18} color="#DC2626" />
|
||
<MyText style={tw`text-red-600 font-bold ml-2`}>Cancel Order</MyText>
|
||
</MyTouchableOpacity>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
|
||
{/* Cancel Order Dialog */}
|
||
<BottomDialog open={cancelDialogOpen} onClose={() => setCancelDialogOpen(false)}>
|
||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||
<View style={tw`p-6`}>
|
||
<View style={tw`flex-row justify-between items-center mb-4`}>
|
||
<MyText style={tw`text-xl font-bold text-gray-900`}>Cancel Order</MyText>
|
||
<MyTouchableOpacity onPress={() => setCancelDialogOpen(false)}>
|
||
<MaterialIcons name="close" size={24} color="#9CA3AF" />
|
||
</MyTouchableOpacity>
|
||
</View>
|
||
|
||
<View style={tw`bg-red-50 p-4 rounded-xl mb-4 border border-red-100`}>
|
||
<MyText style={tw`text-red-800 text-sm`}>
|
||
Are you sure you want to cancel this order? This action cannot be undone.
|
||
</MyText>
|
||
</View>
|
||
|
||
<MyText style={tw`text-gray-700 font-medium mb-2`}>Reason for cancellation</MyText>
|
||
<TextInput
|
||
style={tw`bg-gray-50 border border-gray-200 rounded-xl p-4 min-h-24 text-base text-gray-800 mb-6`}
|
||
value={cancelReason}
|
||
onChangeText={setCancelReason}
|
||
placeholder="Please tell us why you are cancelling..."
|
||
placeholderTextColor="#9CA3AF"
|
||
multiline
|
||
numberOfLines={3}
|
||
textAlignVertical="top"
|
||
/>
|
||
|
||
<MyTouchableOpacity
|
||
style={tw`bg-red-600 py-4 rounded-xl shadow-sm items-center ${cancelOrderMutation.isPending ? 'opacity-70' : ''}`}
|
||
onPress={handleCancelOrder}
|
||
disabled={cancelOrderMutation.isPending}
|
||
>
|
||
{cancelOrderMutation.isPending ? (
|
||
<ActivityIndicator color="white" />
|
||
) : (
|
||
<MyText style={tw`text-white font-bold text-lg`}>Confirm Cancellation</MyText>
|
||
)}
|
||
</MyTouchableOpacity>
|
||
</View>
|
||
</KeyboardAvoidingView>
|
||
</BottomDialog>
|
||
|
||
{/* Raise Complaint Dialog */}
|
||
<BottomDialog open={complaintDialogOpen} onClose={() => setComplaintDialogOpen(false)}>
|
||
<ComplaintForm
|
||
open={complaintDialogOpen}
|
||
onClose={() => {
|
||
setComplaintDialogOpen(false);
|
||
refetch();
|
||
}}
|
||
orderId={order.orderId}
|
||
/>
|
||
</BottomDialog>
|
||
</View>
|
||
</AppContainer>
|
||
);
|
||
}
|