freshyo/apps/admin-ui/app/(drawer)/user-management/index.tsx
2026-02-08 16:02:57 +05:30

268 lines
7.9 KiB
TypeScript

import React, { useState, useCallback, useMemo, useEffect } from 'react';
import {
View,
TouchableOpacity,
Alert,
RefreshControl,
ActivityIndicator,
} from 'react-native';
import { useRouter } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import {
AppContainer,
MyText,
tw,
SearchBar,
MyFlatList,
} from 'common-ui';
import { trpc } from '@/src/trpc-client';
import { formatDistanceToNow } from 'date-fns';
interface User {
id: number;
name: string | null;
mobile: string | null;
createdAt: string;
totalOrders: number;
lastOrderDate: string | null;
isSuspended: boolean;
}
interface UserItemProps {
user: User;
index: number;
onPress: () => void;
}
const UserItem: React.FC<UserItemProps> = ({ user, index, onPress }) => {
const displayName = user.name || 'Unnamed User';
const hasOrders = user.totalOrders > 0;
const lastOrderText = user.lastOrderDate
? formatDistanceToNow(new Date(user.lastOrderDate), { addSuffix: true })
: 'Never';
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`}>
{/* Left: Index number */}
<View style={tw`w-8 h-8 rounded-full bg-gray-100 items-center justify-center mr-3`}>
<MyText style={tw`text-gray-600 text-xs font-bold`}>{index + 1}</MyText>
</View>
{/* Middle: User Info */}
<View style={tw`flex-1`}>
{/* Mobile number - primary identifier */}
<View style={tw`flex-row items-center mb-0.5`}>
<MyText style={tw`text-gray-900 font-bold text-base`}>
{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>
{/* Name */}
<MyText style={tw`text-gray-500 text-sm mb-1`}>
{displayName}
</MyText>
{/* Registration date */}
<MyText style={tw`text-gray-400 text-xs`}>
Registered: {formatDistanceToNow(new Date(user.createdAt), { addSuffix: true })}
</MyText>
</View>
{/* Right: Order Stats */}
<View style={tw`items-end`}>
{/* Total Orders */}
<View style={tw`flex-row items-center mb-1`}>
<MaterialIcons name="shopping-bag" size={14} color={hasOrders ? '#10B981' : '#9CA3AF'} />
<MyText style={tw`${hasOrders ? 'text-green-600' : 'text-gray-400'} font-bold text-sm ml-1`}>
{user.totalOrders} orders
</MyText>
</View>
{/* Last Order */}
<MyText style={tw`text-gray-400 text-xs`}>
Last: {lastOrderText}
</MyText>
</View>
</View>
</TouchableOpacity>
);
};
interface ListHeaderProps {
searchTerm: string;
onSearchChange: (text: string) => void;
userCount: number;
}
const ListHeader: React.FC<ListHeaderProps> = ({ searchTerm, onSearchChange, userCount }) => (
<View>
{/* Search Bar */}
<View style={tw`px-4 py-3 bg-white border-b border-gray-100`}>
<SearchBar
value={searchTerm}
onChangeText={onSearchChange}
placeholder="Search by mobile number..."
/>
</View>
{/* Stats Summary */}
<View style={tw`px-4 py-2 bg-gray-50`}>
<MyText style={tw`text-gray-500 text-sm`}>
Showing {userCount} users
</MyText>
</View>
</View>
);
const ListFooter: React.FC<{ isFetching: boolean }> = ({ isFetching }) => {
if (!isFetching) return null;
return (
<View style={tw`py-4 items-center`}>
<ActivityIndicator size="small" color="#3b82f6" />
</View>
);
};
export default function UserManagement() {
const router = useRouter();
const [searchTerm, setSearchTerm] = useState('');
const [refreshing, setRefreshing] = useState(false);
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
// Infinite scroll query
const {
data,
isLoading,
isError,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch,
} = trpc.admin.user.getAllUsers.useInfiniteQuery(
{
limit: 30,
search: searchTerm || undefined,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
useEffect(() => {
if (data?.pages?.length) {
setHasLoadedOnce(true);
}
}, [data]);
// Flatten all pages and remove duplicates
const users = useMemo(() => {
const allUsers = data?.pages.flatMap((page) => page.users) || [];
// Remove duplicates based on user id
const uniqueUsers = allUsers.filter((user, index, self) =>
index === self.findIndex((u) => u.id === user.id)
);
return uniqueUsers;
}, [data]);
const handleRefresh = useCallback(async () => {
setRefreshing(true);
await refetch();
setRefreshing(false);
}, [refetch]);
const handleLoadMore = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const handleUserPress = useCallback((userId: number) => {
router.push(`/(drawer)/user-management/${userId}`);
}, [router]);
const renderUserItem = useCallback(({ item, index }: { item: User; index: number }) => {
return <UserItem user={item} index={index} onPress={() => handleUserPress(item.id)} />;
}, [handleUserPress]);
if (isLoading && !hasLoadedOnce) {
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 users...</MyText>
</View>
</AppContainer>
);
}
if (isError) {
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 users'}
</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>
);
}
return (
<View style={tw`flex-1 bg-gray-50`}>
{/* Users List */}
<MyFlatList
data={users}
renderItem={renderUserItem}
keyExtractor={(item) => item.id.toString()}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.5}
ListHeaderComponent={
<ListHeader
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
userCount={users.length}
/>
}
ListFooterComponent={<ListFooter isFetching={isFetchingNextPage} />}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
contentContainerStyle={tw`pb-8`}
stickyHeaderIndices={[0]}
ListEmptyComponent={
<View style={tw`flex-1 justify-center items-center py-12`}>
<MaterialIcons name="people-outline" size={64} color="#e5e7eb" />
<MyText style={tw`text-gray-500 mt-4 text-center text-lg`}>
No users found
</MyText>
{searchTerm && (
<MyText style={tw`text-gray-400 mt-1 text-center`}>
Try adjusting your search
</MyText>
)}
</View>
}
/>
</View>
);
}