254 lines
8.6 KiB
TypeScript
254 lines
8.6 KiB
TypeScript
import React, { useCallback } from 'react';
|
|
import {
|
|
View,
|
|
TouchableOpacity,
|
|
ActivityIndicator,
|
|
ScrollView,
|
|
} from 'react-native';
|
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
import {
|
|
AppContainer,
|
|
MyText,
|
|
tw,
|
|
Checkbox,
|
|
} from 'common-ui';
|
|
import { trpc } from '@/src/trpc-client';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import dayjs from 'dayjs';
|
|
|
|
interface Order {
|
|
id: number;
|
|
readableId: number;
|
|
totalAmount: string;
|
|
createdAt: string;
|
|
status: string;
|
|
isFlashDelivery: boolean;
|
|
itemCount: number;
|
|
}
|
|
|
|
interface OrderItemProps {
|
|
order: Order;
|
|
onPress: () => void;
|
|
}
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'delivered':
|
|
return 'text-green-600 bg-green-50 border-green-100';
|
|
case 'cancelled':
|
|
return 'text-red-600 bg-red-50 border-red-100';
|
|
default:
|
|
return 'text-yellow-600 bg-yellow-50 border-yellow-100';
|
|
}
|
|
};
|
|
|
|
const OrderItem: React.FC<OrderItemProps> = ({ order, onPress }) => {
|
|
const statusStyle = getStatusColor(order.status);
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
onPress={onPress}
|
|
activeOpacity={0.7}
|
|
style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-3 shadow-sm`}
|
|
>
|
|
<View style={tw`flex-row items-center justify-between mb-3`}>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MyText style={tw`text-lg font-bold text-gray-900`}>
|
|
#{order.readableId}
|
|
</MyText>
|
|
{order.isFlashDelivery && (
|
|
<View style={tw`ml-2 px-2 py-0.5 bg-amber-100 rounded-full border border-amber-200`}>
|
|
<MyText style={tw`text-[10px] font-black text-amber-700 uppercase`}>⚡</MyText>
|
|
</View>
|
|
)}
|
|
</View>
|
|
<View style={tw`px-3 py-1 rounded-full border ${statusStyle}`}>
|
|
<MyText style={tw`text-xs font-bold uppercase tracking-wider ${statusStyle.split(' ')[0]}`}>
|
|
{order.status}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={tw`flex-row justify-between items-center`}>
|
|
<View>
|
|
<MyText style={tw`text-gray-500 text-sm mb-1`}>
|
|
{dayjs(order.createdAt).format('MMM DD, YYYY • h:mm A')}
|
|
</MyText>
|
|
<MyText style={tw`text-gray-400 text-xs`}>
|
|
{order.itemCount} {order.itemCount === 1 ? 'item' : 'items'}
|
|
</MyText>
|
|
</View>
|
|
<MyText style={tw`text-xl font-bold text-blue-600`}>
|
|
₹{order.totalAmount}
|
|
</MyText>
|
|
</View>
|
|
|
|
<View style={tw`mt-3 pt-3 border-t border-gray-100 flex-row items-center justify-center`}>
|
|
<MyText style={tw`text-blue-600 font-medium text-sm`}>
|
|
View Order Details
|
|
</MyText>
|
|
<MaterialIcons name="chevron-right" size={18} color="#3b82f6" />
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
export default function UserDetails() {
|
|
const router = useRouter();
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const userId = id ? parseInt(id) : 0;
|
|
|
|
const { data, isLoading, error, refetch } = trpc.admin.user.getUserDetails.useQuery(
|
|
{ userId },
|
|
{ enabled: !!userId }
|
|
);
|
|
|
|
const updateSuspension = trpc.admin.user.updateUserSuspension.useMutation({
|
|
onSuccess: () => {
|
|
refetch();
|
|
},
|
|
});
|
|
|
|
const handleOrderPress = useCallback((orderId: number) => {
|
|
router.push(`/(drawer)/order-details/${orderId}`);
|
|
}, [router]);
|
|
|
|
const handleSuspensionToggle = useCallback((isSuspended: boolean) => {
|
|
updateSuspension.mutate({ userId, isSuspended });
|
|
}, [userId, updateSuspension]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 justify-center items-center`}>
|
|
<ActivityIndicator size="large" color="#3b82f6" />
|
|
<MyText style={tw`text-gray-500 mt-4`}>Loading user details...</MyText>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|
|
|
|
if (error || !data) {
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 justify-center items-center p-8`}>
|
|
<MaterialIcons name="error-outline" size={64} color="#ef4444" />
|
|
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Error</MyText>
|
|
<MyText style={tw`text-gray-500 mt-2 text-center`}>
|
|
{error?.message || 'Failed to load user details'}
|
|
</MyText>
|
|
<TouchableOpacity
|
|
onPress={() => refetch()}
|
|
style={tw`mt-6 bg-blue-600 px-6 py-3 rounded-full`}
|
|
>
|
|
<MyText style={tw`text-white font-semibold`}>Retry</MyText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|
|
|
|
const { user, orders } = data;
|
|
const displayName = user.name || 'Unnamed User';
|
|
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 bg-gray-50`}>
|
|
{/* Header */}
|
|
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center`}>
|
|
<TouchableOpacity
|
|
onPress={() => router.back()}
|
|
style={tw`p-2 -ml-4`}
|
|
>
|
|
<MaterialIcons name="chevron-left" size={24} color="#374151" />
|
|
</TouchableOpacity>
|
|
<MyText style={tw`text-xl font-bold text-gray-900 ml-2`}>User Details</MyText>
|
|
</View>
|
|
|
|
<ScrollView showsVerticalScrollIndicator={false}>
|
|
{/* User Info Card */}
|
|
<View style={tw`bg-white p-5 m-4 rounded-2xl shadow-sm border border-gray-100`}>
|
|
<View style={tw`flex-row items-center mb-4`}>
|
|
<View style={tw`w-12 h-12 bg-blue-50 rounded-full items-center justify-center mr-4`}>
|
|
<MaterialIcons name="person" size={24} color="#3B82F6" />
|
|
</View>
|
|
<View style={tw`flex-1`}>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MyText style={tw`text-gray-900 font-bold text-lg mb-0.5`}>
|
|
{user.mobile || 'No Mobile'}
|
|
</MyText>
|
|
{user.isSuspended && (
|
|
<View style={tw`ml-2 px-2 py-0.5 bg-red-100 rounded-full border border-red-200`}>
|
|
<MyText style={tw`text-[10px] font-bold text-red-700 uppercase`}>Suspended</MyText>
|
|
</View>
|
|
)}
|
|
</View>
|
|
<MyText style={tw`text-gray-500`}>
|
|
{displayName}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={tw`bg-gray-50 p-3 rounded-xl mb-4`}>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MaterialIcons name="access-time" size={18} color="#6B7280" />
|
|
<MyText style={tw`ml-2 text-gray-600`}>
|
|
Registered {formatDistanceToNow(new Date(user.createdAt), { addSuffix: true })}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Suspension Toggle */}
|
|
<View style={tw`flex-row items-center justify-between pt-4 border-t border-gray-100`}>
|
|
<View style={tw`flex-1`}>
|
|
<MyText style={tw`text-gray-900 font-bold text-base mb-1`}>
|
|
Suspend User
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 text-sm`}>
|
|
Prevent user from placing orders
|
|
</MyText>
|
|
</View>
|
|
{updateSuspension.isPending ? (
|
|
<ActivityIndicator size="small" color="#3b82f6" />
|
|
) : (
|
|
<Checkbox
|
|
checked={user.isSuspended}
|
|
onPress={() => handleSuspensionToggle(!user.isSuspended)}
|
|
size={28}
|
|
/>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Orders Section */}
|
|
<View style={tw`px-4 pb-8`}>
|
|
<View style={tw`flex-row items-center justify-between mb-4`}>
|
|
<MyText style={tw`text-lg font-bold text-gray-900`}>Order History</MyText>
|
|
<MyText style={tw`text-gray-500 text-sm`}>
|
|
{orders.length} {orders.length === 1 ? 'order' : 'orders'}
|
|
</MyText>
|
|
</View>
|
|
|
|
{orders.length === 0 ? (
|
|
<View style={tw`bg-white rounded-xl border border-gray-100 p-8 items-center`}>
|
|
<MaterialIcons name="shopping-bag" size={48} color="#e5e7eb" />
|
|
<MyText style={tw`text-gray-500 mt-4 text-center`}>
|
|
No orders yet
|
|
</MyText>
|
|
</View>
|
|
) : (
|
|
orders.map((order) => (
|
|
<OrderItem
|
|
key={order.id}
|
|
order={order}
|
|
onPress={() => handleOrderPress(order.id)}
|
|
/>
|
|
))
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|