287 lines
11 KiB
TypeScript
287 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { usersService, User, uploadService } from '@/services';
|
|
import Link from 'next/link';
|
|
|
|
export default function DashboardPage() {
|
|
const [stats, setStats] = useState({
|
|
totalUsers: 0,
|
|
activeUsers: 0,
|
|
agents: 0,
|
|
pendingUsers: 0,
|
|
});
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [recentUsers, setRecentUsers] = useState<User[]>([]);
|
|
const [avatarUrls, setAvatarUrls] = useState<Record<string, string>>({});
|
|
|
|
useEffect(() => {
|
|
fetchDashboardData();
|
|
}, []);
|
|
|
|
const fetchDashboardData = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await usersService.getUsers({ page: 1, limit: 100 });
|
|
const users = response.users || [];
|
|
setStats({
|
|
totalUsers: response.total || 0,
|
|
activeUsers: users.filter((u) => u.status === 'ACTIVE').length,
|
|
agents: users.filter((u) => u.role === 'AGENT').length,
|
|
pendingUsers: users.filter((u) => u.status === 'PENDING_VERIFICATION').length,
|
|
});
|
|
const recent = users.slice(0, 5);
|
|
setRecentUsers(recent);
|
|
|
|
// Fetch presigned URLs for avatars
|
|
const urls: Record<string, string> = {};
|
|
await Promise.all(
|
|
recent.map(async (user) => {
|
|
if (user.profile?.avatar && uploadService.isS3Key(user.profile.avatar)) {
|
|
try {
|
|
urls[user.id] = await uploadService.getPresignedDownloadUrl(user.profile.avatar);
|
|
} catch {
|
|
// Ignore errors for individual avatar fetches
|
|
}
|
|
}
|
|
})
|
|
);
|
|
setAvatarUrls(urls);
|
|
} catch (err) {
|
|
console.error('Failed to fetch dashboard data:', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* Page Title */}
|
|
<div className="mb-6">
|
|
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
|
<p className="text-gray-500">Welcome to Re-Quest Admin Panel</p>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center">
|
|
<div className="p-3 bg-blue-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-blue-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-500">Total Users</p>
|
|
<p className="text-2xl font-bold text-gray-900">{stats.totalUsers}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center">
|
|
<div className="p-3 bg-green-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-green-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-500">Active</p>
|
|
<p className="text-2xl font-bold text-gray-900">{stats.activeUsers}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center">
|
|
<div className="p-3 bg-purple-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-purple-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-500">Agents</p>
|
|
<p className="text-2xl font-bold text-gray-900">{stats.agents}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center">
|
|
<div className="p-3 bg-yellow-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-yellow-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-500">Pending</p>
|
|
<p className="text-2xl font-bold text-gray-900">{stats.pendingUsers}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Users */}
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-lg font-semibold text-gray-900">Recent Users</h2>
|
|
<Link
|
|
href="/dashboard/users"
|
|
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
|
|
>
|
|
View All →
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
{recentUsers.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<svg
|
|
className="mx-auto h-12 w-12 text-gray-400"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
|
/>
|
|
</svg>
|
|
<p className="mt-2 text-gray-500">No users found</p>
|
|
</div>
|
|
) : (
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
User
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Role
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Joined
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{recentUsers.map((user) => (
|
|
<tr key={user.id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0 h-10 w-10">
|
|
{avatarUrls[user.id] ? (
|
|
<img
|
|
className="h-10 w-10 rounded-full object-cover"
|
|
src={avatarUrls[user.id]}
|
|
alt=""
|
|
/>
|
|
) : (
|
|
<div className="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
|
|
<span className="text-gray-500 font-medium text-sm">
|
|
{user.profile?.firstName?.[0] || user.email[0].toUpperCase()}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{user.profile?.firstName} {user.profile?.lastName}
|
|
</div>
|
|
<div className="text-sm text-gray-500">{user.email}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span
|
|
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
|
user.role === 'ADMIN'
|
|
? 'bg-red-100 text-red-800'
|
|
: user.role === 'AGENT'
|
|
? 'bg-purple-100 text-purple-800'
|
|
: 'bg-blue-100 text-blue-800'
|
|
}`}
|
|
>
|
|
{user.role}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span
|
|
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
|
user.status === 'ACTIVE'
|
|
? 'bg-green-100 text-green-800'
|
|
: user.status === 'PENDING_VERIFICATION'
|
|
? 'bg-yellow-100 text-yellow-800'
|
|
: user.status === 'SUSPENDED'
|
|
? 'bg-red-100 text-red-800'
|
|
: 'bg-gray-100 text-gray-800'
|
|
}`}
|
|
>
|
|
{user.status === 'PENDING_VERIFICATION' ? 'PENDING' : user.status}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{new Date(user.createdAt).toLocaleDateString()}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|