freshyo/apps/admin-web/src/routes/dashboard.users.$id.tsx
2026-09-06 23:05:20 +05:30

250 lines
8.7 KiB
TypeScript

import React, { useCallback } from 'react'
import type { OrderCardActionProps } from '@packages/shared'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { ChevronLeft, ChevronRight, CircleAlert, User as UserIcon, Clock, ShoppingBag } from 'lucide-react'
import { AppContainer, p as MyText, Checkbox } from 'web-components'
import { trpc } from '@/lib/trpc-client'
import { formatDistanceToNow } from 'date-fns'
import dayjs from 'dayjs'
import { UserIncidentsView } from '../components/UserIncidentsView'
export const Route = createFileRoute('/dashboard/users/$id')({
component: UserDetails,
})
interface Order {
id: number
readableId: number
totalAmount: string
createdAt: string
status: string
isFlashDelivery: boolean
itemCount: number
}
interface OrderItemProps extends OrderCardActionProps<Order> {
}
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 (
<div
onClick={onPress}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter') onPress()
}}
className="mb-3 cursor-pointer rounded-xl border border-gray-100 bg-white p-4 text-left shadow-sm transition-colors hover:bg-gray-50"
>
<div className="mb-3 flex-row flex items-center justify-between">
<div className="flex-row flex items-center">
<MyText className="text-lg font-bold text-gray-900">
#{order.readableId}
</MyText>
{order.isFlashDelivery && (
<span className="ml-2 rounded-full border border-amber-200 bg-amber-100 px-2 py-0.5">
<MyText className="text-[10px] font-black uppercase text-amber-700"></MyText>
</span>
)}
</div>
<span className={`rounded-full border px-3 py-1 ${statusStyle}`}>
<MyText className={`text-xs font-bold uppercase tracking-wider ${statusStyle.split(' ')[0]}`}>
{order.status}
</MyText>
</span>
</div>
<div className="flex-row flex items-center justify-between">
<div>
<MyText className="mb-1 text-sm text-gray-500">
{dayjs(order.createdAt).format('MMM DD, YYYY • h:mm A')}
</MyText>
<MyText className="text-xs text-gray-400">
{order.itemCount} {order.itemCount === 1 ? 'item' : 'items'}
</MyText>
</div>
<MyText className="text-xl font-bold text-blue-600">
{order.totalAmount}
</MyText>
</div>
<div className="mt-3 flex-row flex items-center justify-center border-t border-gray-100 pt-3">
<MyText className="text-sm font-medium text-blue-600">
View Order Details
</MyText>
<ChevronRight size={18} color="#3b82f6" />
</div>
</div>
)
}
function UserDetails() {
const navigate = useNavigate()
const { id } = Route.useParams() as { 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) => {
navigate({ to: '/dashboard/orders/$id', params: { id: String(orderId) } })
}, [navigate])
const handleSuspensionToggle = useCallback((isSuspended: boolean) => {
updateSuspension.mutate({ userId, isSuspended })
}, [userId, updateSuspension])
if (isLoading) {
return (
<AppContainer>
<div className="flex flex-1 items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-blue-500" />
<MyText className="mt-4 text-gray-500">Loading user details...</MyText>
</div>
</AppContainer>
)
}
if (error || !data) {
return (
<AppContainer>
<div className="flex flex-1 items-center justify-center p-8">
<CircleAlert size={64} color="#ef4444" />
<MyText className="mt-4 text-lg font-bold text-gray-900">Error</MyText>
<MyText className="mt-2 text-center text-gray-500">
{error?.message || 'Failed to load user details'}
</MyText>
<button
onClick={() => refetch()}
className="mt-6 rounded-full bg-blue-600 px-6 py-3"
>
<MyText className="font-semibold text-white">Retry</MyText>
</button>
</div>
</AppContainer>
)
}
const { user, orders } = data
const displayName = user.name || 'Unnamed User'
return (
<AppContainer>
<div className="flex-1 bg-gray-50">
<div className="flex-row flex items-center border-b border-gray-200 bg-white px-4 py-4">
<button
onClick={() => window.history.back()}
className="-ml-4 p-2"
aria-label="Go back"
>
<ChevronLeft size={24} color="#374151" />
</button>
<MyText className="ml-2 text-xl font-bold text-gray-900">User Details</MyText>
</div>
<div>
<div className="m-4 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
<div className="mb-4 flex-row flex items-center">
<div className="mr-4 flex h-12 w-12 items-center justify-center rounded-full bg-blue-50">
<UserIcon size={24} color="#3B82F6" />
</div>
<div className="flex-1">
<div className="flex-row flex items-center">
<MyText className="mb-0.5 text-lg font-bold text-gray-900">
{user.mobile || 'No Mobile'}
</MyText>
{user.isSuspended && (
<span className="ml-2 rounded-full border border-red-200 bg-red-100 px-2 py-0.5">
<MyText className="text-[10px] font-bold uppercase text-red-700">Suspended</MyText>
</span>
)}
</div>
<MyText className="text-gray-500">
{displayName}
</MyText>
</div>
</div>
<div className="mb-4 rounded-xl bg-gray-50 p-3">
<div className="flex-row flex items-center">
<Clock size={18} color="#6B7280" />
<MyText className="ml-2 text-gray-600">
Registered {formatDistanceToNow(new Date(user.createdAt), { addSuffix: true })}
</MyText>
</div>
</div>
<div className="flex-row flex items-center justify-between border-t border-gray-100 pt-4">
<div className="flex-1">
<MyText className="mb-1 text-base font-bold text-gray-900">
Suspend User
</MyText>
<MyText className="text-sm text-gray-500">
Prevent user from placing orders
</MyText>
</div>
{updateSuspension.isPending ? (
<div className="h-5 w-5 animate-spin rounded-full border-2 border-gray-300 border-t-blue-500" />
) : (
<Checkbox
checked={user.isSuspended}
onPress={() => handleSuspensionToggle(!user.isSuspended)}
size={28}
/>
)}
</div>
</div>
<UserIncidentsView userId={userId} orderId={null} />
<div className="px-4 pb-8">
<div className="mb-4 flex-row flex items-center justify-between">
<MyText className="text-lg font-bold text-gray-900">Order History</MyText>
<MyText className="text-sm text-gray-500">
{orders.length} {orders.length === 1 ? 'order' : 'orders'}
</MyText>
</div>
{orders.length === 0 ? (
<div className="items-center rounded-xl border border-gray-100 bg-white p-8">
<ShoppingBag size={48} color="#e5e7eb" />
<MyText className="mt-4 text-center text-gray-500">
No orders yet
</MyText>
</div>
) : (
orders.map((order) => (
<OrderItem
key={order.id}
order={order}
onPress={() => handleOrderPress(order.id)}
/>
))
)}
</div>
</div>
</div>
</AppContainer>
)
}