diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 568f671..d4e252f 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -9,21 +9,29 @@ import { ServerStackIcon, ArrowRightIcon } from '@heroicons/react/24/outline' -import { useMemo } from 'react' +import { useMemo, useState, useEffect } from 'react' import { useRouter } from 'next/navigation' +import { useAdminUsers } from '../hooks/useAdminUsers' export default function AdminDashboardPage() { const router = useRouter() + const { userStats, isAdmin } = useAdminUsers() + const [isClient, setIsClient] = useState(false) - // Mocked aggregates (replace with real fetch) - const userStats = useMemo(() => ({ - total: 101, - admins: 1, - pending: 3, - active: 54, - personal: 94, - company: 7 - }), []) + // Handle client-side mounting + useEffect(() => { + setIsClient(true) + }, []) + + // Fallback for loading/no data + const displayStats = userStats || { + totalUsers: 0, + adminUsers: 0, + verificationPending: 0, + activeUsers: 0, + personalUsers: 0, + companyUsers: 0 + } const permissionStats = useMemo(() => ({ permissions: 1 // TODO: fetch permission definitions @@ -36,6 +44,38 @@ export default function AdminDashboardPage() { memory: '0.1 / 7.8', recentErrors: [] as { id: string; ts: string; msg: string }[] }), []) + + // Show loading during SSR/initial client render + if (!isClient) { + return ( + +
+
+
+
+

Loading...

+
+
+
+ + ) + } + + // Access check (only after client-side hydration) + if (!isAdmin) { + return ( + +
+
+
+

Access Denied

+

You need admin privileges to access this page.

+
+
+
+
+ ) + } return ( @@ -105,27 +145,51 @@ export default function AdminDashboardPage() {
Total Users
-
{userStats.total}
+
+ {userStats ? displayStats.totalUsers : ( + + )} +
Admin Users
-
{userStats.admins}
+
+ {userStats ? displayStats.adminUsers : ( + + )} +
Verification Pending
-
{userStats.pending}
+
+ {userStats ? displayStats.verificationPending : ( + + )} +
Active Users
-
{userStats.active}
+
+ {userStats ? displayStats.activeUsers : ( + + )} +
Personal Users
-
{userStats.personal}
+
+ {userStats ? displayStats.personalUsers : ( + + )} +
Company Users
-
{userStats.company}
+
+ {userStats ? displayStats.companyUsers : ( + + )} +
+
+ + )} + + {/* Filter Card */}
- {current.map(u => { - const initials = `${u.firstName[0]||''}${u.lastName[0]||''}`.toUpperCase() + {loading ? ( + + +
+
+ Loading users... +
+ + + ) : current.map(u => { + const displayName = u.user_type === 'company' + ? u.company_name || 'Unknown Company' + : `${u.first_name || 'Unknown'} ${u.last_name || 'User'}` + + const initials = u.user_type === 'company' + ? (u.company_name?.[0] || 'C').toUpperCase() + : `${u.first_name?.[0] || 'U'}${u.last_name?.[0] || 'U'}`.toUpperCase() + + // Map backend status to frontend status for display + const userStatus: UserStatus = u.is_admin_verified === 1 ? 'active' : + u.status === 'pending' ? 'pending' : + u.status === 'suspended' ? 'disabled' : + 'pending' // default fallback + + const createdDate = new Date(u.created_at).toLocaleDateString() + const lastLoginDate = u.last_login_at ? new Date(u.last_login_at).toLocaleDateString() : 'Never' + return ( @@ -260,7 +367,7 @@ export default function AdminUserManagementPage() {
- {u.firstName} {u.lastName} + {displayName}
{u.email} @@ -268,29 +375,29 @@ export default function AdminUserManagementPage() {
- {typeBadge(u.type)} - {statusBadge(u.status)} + {typeBadge(u.user_type)} + {statusBadge(userStatus)} {roleBadge(u.role)} - {u.created} + {createdDate} - {u.lastLogin ?? 'Never'} + {lastLoginDate}
+
+ + )} + {/* Filter Card */} User Type + Progress Status Role Created - Last Login Actions - {current.map(u => { - const initials = `${u.firstName[0]}${u.lastName[0]}`.toUpperCase() + {loading ? ( + + +
+
+ Loading users... +
+ + + ) : current.map(u => { + const displayName = u.user_type === 'company' + ? u.company_name || 'Unknown Company' + : `${u.first_name || 'Unknown'} ${u.last_name || 'User'}` + + const initials = u.user_type === 'company' + ? (u.company_name?.[0] || 'C').toUpperCase() + : `${u.first_name?.[0] || 'U'}${u.last_name?.[0] || 'U'}`.toUpperCase() + + const isVerifying = verifying.has(u.id.toString()) + const createdDate = new Date(u.created_at).toLocaleDateString() + + // Check if user has completed all verification steps + const isReadyToVerify = u.email_verified === 1 && u.profile_completed === 1 && + u.documents_uploaded === 1 && u.contract_signed === 1 + return ( @@ -246,31 +309,29 @@ export default function AdminUserVerifyPage() {
- {u.firstName} {u.lastName} + {displayName}
{u.email}
- {typeBadge(u.type)} + {typeBadge(u.user_type)} + {verificationStatusBadge(u)} {statusBadge(u.status)} {roleBadge(u.role)} - {u.created} - {u.lastLogin} + {createdDate} - {u.status === 'active' ? ( - Verified - ) : ( + {isReadyToVerify ? ( + ) : ( + Incomplete steps )} ) })} - {current.length === 0 && ( + {current.length === 0 && !loading && ( - No pending users match current filters. + No unverified users match current filters. )} diff --git a/src/app/hooks/useAdminUsers.ts b/src/app/hooks/useAdminUsers.ts new file mode 100644 index 0000000..4c303e7 --- /dev/null +++ b/src/app/hooks/useAdminUsers.ts @@ -0,0 +1,113 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { AdminAPI, PendingUser, AdminUserStats } from '../utils/api' +import useAuthStore from '../store/authStore' + +export const useAdminUsers = () => { + const [pendingUsers, setPendingUsers] = useState([]) + const [userStats, setUserStats] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [verifying, setVerifying] = useState>(new Set()) + + const user = useAuthStore(state => state.user) + const token = useAuthStore(state => state.accessToken) + + const fetchPendingUsers = useCallback(async () => { + if (!token || !user?.role || !['admin', 'super_admin'].includes(user.role)) { + setError('Admin access required') + return + } + + setLoading(true) + setError(null) + + try { + // Use the new unverified users endpoint instead of verification pending + const response = await AdminAPI.getUnverifiedUsers(token) + if (response.success) { + setPendingUsers(response.users || []) + } else { + throw new Error(response.message || 'Failed to fetch unverified users') + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch unverified users' + setError(errorMessage) + console.error('useAdminUsers.fetchPendingUsers error:', err) + } finally { + setLoading(false) + } + }, [token, user?.role]) + + const fetchUserStats = useCallback(async () => { + if (!token || !user?.role || !['admin', 'super_admin'].includes(user.role)) { + return + } + + try { + const response = await AdminAPI.getUserStats(token) + if (response.success) { + setUserStats(response.stats) + } + } catch (err) { + console.error('useAdminUsers.fetchUserStats error:', err) + } + }, [token, user?.role]) + + const verifyUser = useCallback(async (userId: string, permissions: string[] = []) => { + if (!token) { + setError('No authentication token') + return false + } + + setVerifying(prev => new Set(prev).add(userId)) + setError(null) + + try { + const response = await AdminAPI.verifyUser(token, userId, permissions) + if (response.success) { + // Remove user from pending list + setPendingUsers(prev => prev.filter(u => u.id.toString() !== userId)) + + // Refresh stats + await fetchUserStats() + + return true + } else { + throw new Error(response.message || 'Failed to verify user') + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to verify user' + setError(errorMessage) + console.error('useAdminUsers.verifyUser error:', err) + return false + } finally { + setVerifying(prev => { + const newSet = new Set(prev) + newSet.delete(userId) + return newSet + }) + } + }, [token, fetchUserStats]) + + // Auto-fetch on mount and when dependencies change + useEffect(() => { + if (token && user?.role && ['admin', 'super_admin'].includes(user.role)) { + fetchPendingUsers() + fetchUserStats() + } + }, [fetchPendingUsers, fetchUserStats]) + + return { + pendingUsers, + userStats, + loading, + error, + verifying, + fetchPendingUsers, + fetchUserStats, + verifyUser, + isAdmin: user?.role && ['admin', 'super_admin'].includes(user.role) + } +} \ No newline at end of file diff --git a/src/app/utils/api.ts b/src/app/utils/api.ts index c31d2c1..ff0bba8 100644 --- a/src/app/utils/api.ts +++ b/src/app/utils/api.ts @@ -35,6 +35,11 @@ export const API_ENDPOINTS = { // Admin ADMIN_USERS: '/api/admin/users/:id/full', + ADMIN_USER_STATS: '/api/admin/user-stats', + ADMIN_USER_LIST: '/api/admin/user-list', + ADMIN_VERIFICATION_PENDING: '/api/admin/verification-pending-users', + ADMIN_UNVERIFIED_USERS: '/api/admin/unverified-users', + ADMIN_VERIFY_USER: '/api/admin/verify-user/:id', } // API Helper Functions @@ -226,6 +231,51 @@ export interface ApiError { code?: string } +// Admin API Functions +export class AdminAPI { + static async getUserStats(token: string) { + const response = await ApiClient.get(API_ENDPOINTS.ADMIN_USER_STATS, token) + if (!response.ok) { + throw new Error('Failed to fetch user stats') + } + return response.json() + } + + static async getUserList(token: string) { + const response = await ApiClient.get(API_ENDPOINTS.ADMIN_USER_LIST, token) + if (!response.ok) { + throw new Error('Failed to fetch user list') + } + return response.json() + } + + static async getVerificationPendingUsers(token: string) { + const response = await ApiClient.get(API_ENDPOINTS.ADMIN_VERIFICATION_PENDING, token) + if (!response.ok) { + throw new Error('Failed to fetch pending users') + } + return response.json() + } + + static async getUnverifiedUsers(token: string) { + const response = await ApiClient.get(API_ENDPOINTS.ADMIN_UNVERIFIED_USERS, token) + if (!response.ok) { + throw new Error('Failed to fetch unverified users') + } + return response.json() + } + + static async verifyUser(token: string, userId: string, permissions: string[] = []) { + const endpoint = API_ENDPOINTS.ADMIN_VERIFY_USER.replace(':id', userId) + const response = await ApiClient.post(endpoint, { permissions }, token) + if (!response.ok) { + const error = await response.json() + throw new Error(error.message || 'Failed to verify user') + } + return response.json() + } +} + // Response Types export interface UserStatus { emailVerified: boolean @@ -234,6 +284,33 @@ export interface UserStatus { contractSigned: boolean } +export interface AdminUserStats { + totalUsers: number + adminUsers: number + verificationPending: number + activeUsers: number + personalUsers: number + companyUsers: number +} + +export interface PendingUser { + id: number + email: string + user_type: 'personal' | 'company' + role: 'user' | 'admin' + created_at: string + last_login_at: string | null + status: string + is_admin_verified: number + email_verified?: number + profile_completed?: number + documents_uploaded?: number + contract_signed?: number + first_name?: string + last_name?: string + company_name?: string +} + export interface ApiResponse { success: boolean message?: string