Compare commits
No commits in common. "92d96d0644a22b15e9baca79c22b13f737f21c70" and "c16ce3093cd805875e39e0b4caa5a66078e00b0c" have entirely different histories.
92d96d0644
...
c16ce3093c
@ -9,29 +9,21 @@ import {
|
|||||||
ServerStackIcon,
|
ServerStackIcon,
|
||||||
ArrowRightIcon
|
ArrowRightIcon
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
import { useMemo, useState, useEffect } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useAdminUsers } from '../hooks/useAdminUsers'
|
|
||||||
|
|
||||||
export default function AdminDashboardPage() {
|
export default function AdminDashboardPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { userStats, isAdmin } = useAdminUsers()
|
|
||||||
const [isClient, setIsClient] = useState(false)
|
|
||||||
|
|
||||||
// Handle client-side mounting
|
// Mocked aggregates (replace with real fetch)
|
||||||
useEffect(() => {
|
const userStats = useMemo(() => ({
|
||||||
setIsClient(true)
|
total: 101,
|
||||||
}, [])
|
admins: 1,
|
||||||
|
pending: 3,
|
||||||
// Fallback for loading/no data
|
active: 54,
|
||||||
const displayStats = userStats || {
|
personal: 94,
|
||||||
totalUsers: 0,
|
company: 7
|
||||||
adminUsers: 0,
|
}), [])
|
||||||
verificationPending: 0,
|
|
||||||
activeUsers: 0,
|
|
||||||
personalUsers: 0,
|
|
||||||
companyUsers: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const permissionStats = useMemo(() => ({
|
const permissionStats = useMemo(() => ({
|
||||||
permissions: 1 // TODO: fetch permission definitions
|
permissions: 1 // TODO: fetch permission definitions
|
||||||
@ -45,38 +37,6 @@ export default function AdminDashboardPage() {
|
|||||||
recentErrors: [] as { id: string; ts: string; msg: string }[]
|
recentErrors: [] as { id: string; ts: string; msg: string }[]
|
||||||
}), [])
|
}), [])
|
||||||
|
|
||||||
// Show loading during SSR/initial client render
|
|
||||||
if (!isClient) {
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
<div className="relative flex flex-col flex-1 px-4 sm:px-6 lg:px-10 py-10">
|
|
||||||
<div className="relative mx-auto w-full max-w-4xl space-y-8 bg-white/95 backdrop-blur rounded-2xl shadow-2xl ring-1 ring-black/10 p-6 sm:p-10">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="h-4 w-4 rounded-full border-2 border-blue-500 border-b-transparent animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">Loading...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access check (only after client-side hydration)
|
|
||||||
if (!isAdmin) {
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
<div className="relative flex flex-col flex-1 px-4 sm:px-6 lg:px-10 py-10">
|
|
||||||
<div className="relative mx-auto w-full max-w-4xl space-y-8 bg-white/95 backdrop-blur rounded-2xl shadow-2xl ring-1 ring-red-500/20 p-6 sm:p-10">
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="text-2xl font-bold text-red-600 mb-2">Access Denied</h1>
|
|
||||||
<p className="text-gray-600">You need admin privileges to access this page.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="relative flex flex-col flex-1 px-4 sm:px-6 lg:px-10 py-10">
|
<div className="relative flex flex-col flex-1 px-4 sm:px-6 lg:px-10 py-10">
|
||||||
@ -145,51 +105,27 @@ export default function AdminDashboardPage() {
|
|||||||
<dl className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-3 text-xs sm:text-sm mb-6">
|
<dl className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-3 text-xs sm:text-sm mb-6">
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-gray-500">Total Users</dt>
|
<dt className="text-gray-500">Total Users</dt>
|
||||||
<dd className="font-semibold text-gray-900">
|
<dd className="font-semibold text-gray-900">{userStats.total}</dd>
|
||||||
{userStats ? displayStats.totalUsers : (
|
|
||||||
<span className="inline-block w-8 h-4 bg-gray-200 rounded animate-pulse" />
|
|
||||||
)}
|
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-gray-500">Admin Users</dt>
|
<dt className="text-gray-500">Admin Users</dt>
|
||||||
<dd className="font-semibold text-gray-900">
|
<dd className="font-semibold text-gray-900">{userStats.admins}</dd>
|
||||||
{userStats ? displayStats.adminUsers : (
|
|
||||||
<span className="inline-block w-8 h-4 bg-gray-200 rounded animate-pulse" />
|
|
||||||
)}
|
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-gray-500">Verification Pending</dt>
|
<dt className="text-gray-500">Verification Pending</dt>
|
||||||
<dd className="font-semibold text-amber-600">
|
<dd className="font-semibold text-amber-600">{userStats.pending}</dd>
|
||||||
{userStats ? displayStats.verificationPending : (
|
|
||||||
<span className="inline-block w-8 h-4 bg-gray-200 rounded animate-pulse" />
|
|
||||||
)}
|
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-gray-500">Active Users</dt>
|
<dt className="text-gray-500">Active Users</dt>
|
||||||
<dd className="font-semibold text-emerald-600">
|
<dd className="font-semibold text-emerald-600">{userStats.active}</dd>
|
||||||
{userStats ? displayStats.activeUsers : (
|
|
||||||
<span className="inline-block w-8 h-4 bg-gray-200 rounded animate-pulse" />
|
|
||||||
)}
|
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-gray-500">Personal Users</dt>
|
<dt className="text-gray-500">Personal Users</dt>
|
||||||
<dd className="font-semibold text-gray-900">
|
<dd className="font-semibold text-gray-900">{userStats.personal}</dd>
|
||||||
{userStats ? displayStats.personalUsers : (
|
|
||||||
<span className="inline-block w-8 h-4 bg-gray-200 rounded animate-pulse" />
|
|
||||||
)}
|
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-gray-500">Company Users</dt>
|
<dt className="text-gray-500">Company Users</dt>
|
||||||
<dd className="font-semibold text-gray-900">
|
<dd className="font-semibold text-gray-900">{userStats.company}</dd>
|
||||||
{userStats ? displayStats.companyUsers : (
|
|
||||||
<span className="inline-block w-8 h-4 bg-gray-200 rounded animate-pulse" />
|
|
||||||
)}
|
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -1,86 +1,69 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useMemo, useState, useEffect, useCallback } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import PageLayout from '../../components/PageLayout'
|
import PageLayout from '../../components/PageLayout'
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
PencilSquareIcon,
|
PencilSquareIcon,
|
||||||
XMarkIcon,
|
XMarkIcon
|
||||||
ExclamationTriangleIcon
|
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
import { useAdminUsers } from '../../hooks/useAdminUsers'
|
|
||||||
import { AdminAPI } from '../../utils/api'
|
|
||||||
import useAuthStore from '../../store/authStore'
|
|
||||||
|
|
||||||
type UserType = 'personal' | 'company'
|
type UserType = 'personal' | 'company'
|
||||||
type UserStatus = 'active' | 'pending' | 'disabled'
|
type UserStatus = 'active' | 'pending' | 'disabled'
|
||||||
type UserRole = 'user' | 'admin'
|
type UserRole = 'user' | 'admin'
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number
|
id: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
email: string
|
email: string
|
||||||
user_type: UserType
|
type: UserType
|
||||||
|
status: UserStatus
|
||||||
role: UserRole
|
role: UserRole
|
||||||
created_at: string
|
created: string
|
||||||
last_login_at: string | null
|
lastLogin: string | null
|
||||||
status: string
|
|
||||||
is_admin_verified: number
|
|
||||||
first_name?: string
|
|
||||||
last_name?: string
|
|
||||||
company_name?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUSES: UserStatus[] = ['active','pending','disabled']
|
const STATUSES: UserStatus[] = ['active','pending','disabled']
|
||||||
const TYPES: UserType[] = ['personal','company']
|
const TYPES: UserType[] = ['personal','company']
|
||||||
const ROLES: UserRole[] = ['user','admin']
|
const ROLES: UserRole[] = ['user','admin']
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
const rand = (arr: any[]) => arr[Math.floor(Math.random() * arr.length)]
|
||||||
|
const daysAgo = (d: number) => {
|
||||||
|
const dt = new Date()
|
||||||
|
dt.setDate(dt.getDate() - d)
|
||||||
|
return dt.toISOString().slice(0,10)
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminUserManagementPage() {
|
export default function AdminUserManagementPage() {
|
||||||
const { isAdmin } = useAdminUsers()
|
// Mock users (stable memo)
|
||||||
const token = useAuthStore(state => state.accessToken)
|
const users = useMemo<User[]>(() => {
|
||||||
const [isClient, setIsClient] = useState(false)
|
const firstNames = ['Anka','Peter','Primoz','Bojana','Hiska','Augst','Katarina','Tamara','Darija','Luka','Sara','Jonas','Maja','Niko','Eva']
|
||||||
|
const lastNames = ['Eravec','Oblak','Sranic','Pilih','Kaja','Feviz','Kravar','Skusek','Abersek','Novak','Schmidt','Mueller','Keller','Hansen','Mayr']
|
||||||
// State for all users (not just pending)
|
const list: User[] = []
|
||||||
const [allUsers, setAllUsers] = useState<User[]>([])
|
for (let i=0;i<101;i++){
|
||||||
const [loading, setLoading] = useState(false)
|
const fn = rand(firstNames)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const ln = rand(lastNames)
|
||||||
|
const createdDays = Math.floor(Math.random()*15)
|
||||||
// Handle client-side mounting
|
const loginDays = Math.random() > 0.2 ? Math.floor(Math.random()*15) : null
|
||||||
useEffect(() => {
|
list.push({
|
||||||
setIsClient(true)
|
id: `U${i+1}`,
|
||||||
|
firstName: fn,
|
||||||
|
lastName: ln,
|
||||||
|
email: `${fn}.${ln}${i}@example.com`.toLowerCase(),
|
||||||
|
type: Math.random() > 0.92 ? 'company' : 'personal',
|
||||||
|
status: rand(STATUSES),
|
||||||
|
role: Math.random() > 0.96 ? 'admin' : 'user',
|
||||||
|
created: daysAgo(createdDays),
|
||||||
|
lastLogin: loginDays === null ? null : daysAgo(loginDays)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Fetch all users from backend
|
// Filters
|
||||||
const fetchAllUsers = useCallback(async () => {
|
|
||||||
if (!token || !isAdmin) return
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await AdminAPI.getUserList(token)
|
|
||||||
if (response.success) {
|
|
||||||
setAllUsers(response.users || [])
|
|
||||||
} else {
|
|
||||||
throw new Error(response.message || 'Failed to fetch users')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch users'
|
|
||||||
setError(errorMessage)
|
|
||||||
console.error('AdminUserManagement.fetchAllUsers error:', err)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [token, isAdmin])
|
|
||||||
|
|
||||||
// Load users on mount
|
|
||||||
useEffect(() => {
|
|
||||||
if (isClient && isAdmin && token) {
|
|
||||||
fetchAllUsers()
|
|
||||||
}
|
|
||||||
}, [fetchAllUsers, isClient])
|
|
||||||
|
|
||||||
// Filter hooks - must be declared before conditional returns
|
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [fType, setFType] = useState<'all'|UserType>('all')
|
const [fType, setFType] = useState<'all'|UserType>('all')
|
||||||
const [fStatus, setFStatus] = useState<'all'|UserStatus>('all')
|
const [fStatus, setFStatus] = useState<'all'|UserStatus>('all')
|
||||||
@ -89,69 +72,21 @@ export default function AdminUserManagementPage() {
|
|||||||
const PAGE_SIZE = 10
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
return allUsers.filter(u => {
|
return users.filter(u =>
|
||||||
const firstName = u.first_name || ''
|
(fType==='all'||u.type===fType) &&
|
||||||
const lastName = u.last_name || ''
|
(fStatus==='all'||u.status===fStatus) &&
|
||||||
const companyName = u.company_name || ''
|
|
||||||
const fullName = u.user_type === 'company' ? companyName : `${firstName} ${lastName}`
|
|
||||||
|
|
||||||
// Map backend status to frontend status
|
|
||||||
// Backend status can be: 'pending', 'active', 'suspended', etc.
|
|
||||||
// is_admin_verified: 1 = verified by admin, 0 = not verified
|
|
||||||
const userStatus: UserStatus = u.is_admin_verified === 1 ? 'active' :
|
|
||||||
u.status === 'pending' ? 'pending' :
|
|
||||||
u.status === 'suspended' ? 'disabled' :
|
|
||||||
'pending' // default fallback
|
|
||||||
|
|
||||||
return (
|
|
||||||
(fType === 'all' || u.user_type === fType) &&
|
|
||||||
(fStatus === 'all' || userStatus === fStatus) &&
|
|
||||||
(fRole==='all'||u.role===fRole) &&
|
(fRole==='all'||u.role===fRole) &&
|
||||||
(
|
(
|
||||||
!search.trim() ||
|
!search.trim() ||
|
||||||
u.email.toLowerCase().includes(search.toLowerCase()) ||
|
u.email.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
fullName.toLowerCase().includes(search.toLowerCase())
|
`${u.firstName} ${u.lastName}`.toLowerCase().includes(search.toLowerCase())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
}, [users, search, fType, fStatus, fRole])
|
||||||
}, [allUsers, search, fType, fStatus, fRole])
|
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
|
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
|
||||||
const current = filtered.slice((page-1)*PAGE_SIZE, page*PAGE_SIZE)
|
const current = filtered.slice((page-1)*PAGE_SIZE, page*PAGE_SIZE)
|
||||||
|
|
||||||
// Show loading during SSR/initial client render
|
|
||||||
if (!isClient) {
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
<div className="relative flex flex-col flex-1 px-4 sm:px-6 lg:px-10 py-10">
|
|
||||||
<div className="relative mx-auto w-full max-w-4xl space-y-8 bg-white/95 backdrop-blur rounded-2xl shadow-2xl ring-1 ring-black/10 p-6 sm:p-10">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="h-4 w-4 rounded-full border-2 border-blue-500 border-b-transparent animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">Loading...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access check (only after client-side hydration)
|
|
||||||
if (!isAdmin) {
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
<div className="relative flex flex-col flex-1 px-4 sm:px-6 lg:px-10 py-10">
|
|
||||||
<div className="relative mx-auto w-full max-w-4xl space-y-8 bg-white/95 backdrop-blur rounded-2xl shadow-2xl ring-1 ring-red-500/20 p-6 sm:p-10">
|
|
||||||
<div className="text-center">
|
|
||||||
<ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" />
|
|
||||||
<h1 className="text-2xl font-bold text-red-600 mb-2">Access Denied</h1>
|
|
||||||
<p className="text-gray-600">You need admin privileges to access this page.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const applyFilter = (e: React.FormEvent) => {
|
const applyFilter = (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setPage(1)
|
setPage(1)
|
||||||
@ -223,23 +158,6 @@ export default function AdminUserManagementPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-lg border border-red-300 bg-red-50 text-red-700 px-5 py-4 flex gap-3 items-start">
|
|
||||||
<ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold">Error loading users</p>
|
|
||||||
<p className="text-sm text-red-600">{error}</p>
|
|
||||||
<button
|
|
||||||
onClick={fetchAllUsers}
|
|
||||||
className="mt-2 text-sm underline hover:no-underline"
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Filter Card */}
|
{/* Filter Card */}
|
||||||
<form
|
<form
|
||||||
onSubmit={applyFilter}
|
onSubmit={applyFilter}
|
||||||
@ -331,33 +249,8 @@ export default function AdminUserManagementPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{loading ? (
|
{current.map(u => {
|
||||||
<tr>
|
const initials = `${u.firstName[0]||''}${u.lastName[0]||''}`.toUpperCase()
|
||||||
<td colSpan={7} className="px-4 py-10 text-center">
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<div className="h-4 w-4 rounded-full border-2 border-blue-500 border-b-transparent animate-spin" />
|
|
||||||
<span className="text-sm text-gray-500">Loading users...</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : 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 (
|
return (
|
||||||
<tr key={u.id} className="hover:bg-gray-50">
|
<tr key={u.id} className="hover:bg-gray-50">
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
@ -367,7 +260,7 @@ export default function AdminUserManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-900 leading-tight">
|
<div className="font-medium text-gray-900 leading-tight">
|
||||||
{displayName}
|
{u.firstName} {u.lastName}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[11px] text-gray-500">
|
<div className="text-[11px] text-gray-500">
|
||||||
{u.email}
|
{u.email}
|
||||||
@ -375,29 +268,29 @@ export default function AdminUserManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">{typeBadge(u.user_type)}</td>
|
<td className="px-4 py-3">{typeBadge(u.type)}</td>
|
||||||
<td className="px-4 py-3">{statusBadge(userStatus)}</td>
|
<td className="px-4 py-3">{statusBadge(u.status)}</td>
|
||||||
<td className="px-4 py-3">{roleBadge(u.role)}</td>
|
<td className="px-4 py-3">{roleBadge(u.role)}</td>
|
||||||
<td className="px-4 py-3 text-gray-700">{createdDate}</td>
|
<td className="px-4 py-3 text-gray-700">{u.created}</td>
|
||||||
<td className="px-4 py-3 text-gray-500 italic">
|
<td className="px-4 py-3 text-gray-500 italic">
|
||||||
{lastLoginDate}
|
{u.lastLogin ?? 'Never'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => onView(u.id.toString())}
|
onClick={() => onView(u.id)}
|
||||||
className="inline-flex items-center gap-1 rounded-md border border-blue-200 bg-blue-50 hover:bg-blue-100 text-blue-700 px-2.5 py-1 text-xs font-medium transition"
|
className="inline-flex items-center gap-1 rounded-md border border-blue-200 bg-blue-50 hover:bg-blue-100 text-blue-700 px-2.5 py-1 text-xs font-medium transition"
|
||||||
>
|
>
|
||||||
<EyeIcon className="h-4 w-4" /> View
|
<EyeIcon className="h-4 w-4" /> View
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onEdit(u.id.toString())}
|
onClick={() => onEdit(u.id)}
|
||||||
className="inline-flex items-center gap-1 rounded-md border border-amber-200 bg-amber-50 hover:bg-amber-100 text-amber-700 px-2.5 py-1 text-xs font-medium transition"
|
className="inline-flex items-center gap-1 rounded-md border border-amber-200 bg-amber-50 hover:bg-amber-100 text-amber-700 px-2.5 py-1 text-xs font-medium transition"
|
||||||
>
|
>
|
||||||
<PencilSquareIcon className="h-4 w-4" /> Edit
|
<PencilSquareIcon className="h-4 w-4" /> Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onDelete(u.id.toString())}
|
onClick={() => onDelete(u.id)}
|
||||||
className="inline-flex items-center gap-1 rounded-md border border-red-200 bg-red-50 hover:bg-red-100 text-red-600 px-2.5 py-1 text-xs font-medium transition"
|
className="inline-flex items-center gap-1 rounded-md border border-red-200 bg-red-50 hover:bg-red-100 text-red-600 px-2.5 py-1 text-xs font-medium transition"
|
||||||
>
|
>
|
||||||
<XMarkIcon className="h-4 w-4" /> Delete
|
<XMarkIcon className="h-4 w-4" /> Delete
|
||||||
|
|||||||
@ -1,59 +1,79 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useMemo, useState, useEffect } from 'react'
|
import { useMemo, useState, useCallback } from 'react'
|
||||||
import PageLayout from '../../components/PageLayout'
|
import PageLayout from '../../components/PageLayout'
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
CheckIcon,
|
CheckIcon
|
||||||
ExclamationTriangleIcon
|
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
import { useAdminUsers } from '../../hooks/useAdminUsers'
|
|
||||||
import { PendingUser } from '../../utils/api'
|
|
||||||
|
|
||||||
type UserType = 'personal' | 'company'
|
type UserType = 'personal' | 'company'
|
||||||
type UserRole = 'user' | 'admin'
|
type UserRole = 'user' | 'admin'
|
||||||
|
interface PendingUser {
|
||||||
|
id: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
email: string
|
||||||
|
type: UserType
|
||||||
|
role: UserRole
|
||||||
|
created: string
|
||||||
|
lastLogin: string
|
||||||
|
status: 'pending' | 'verifying' | 'active'
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPES: UserType[] = ['personal', 'company']
|
||||||
|
const ROLES: UserRole[] = ['user', 'admin']
|
||||||
|
|
||||||
|
const rand = <T,>(arr: T[]) => arr[Math.floor(Math.random() * arr.length)]
|
||||||
|
const daysAgo = (d: number) => {
|
||||||
|
const dt = new Date()
|
||||||
|
dt.setDate(dt.getDate() - d)
|
||||||
|
return dt.toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminUserVerifyPage() {
|
export default function AdminUserVerifyPage() {
|
||||||
const {
|
// Mock pending users (only a small subset)
|
||||||
pendingUsers,
|
const seedUsers = useMemo<PendingUser[]>(() => {
|
||||||
loading,
|
const first = ['Avgust', 'Katarina', 'Ana', 'Luka', 'Sara', 'Jonas', 'Niko', 'Eva']
|
||||||
error,
|
const last = ['Senica', 'Fedzer', 'Hochkraut', 'Novak', 'Schmidt', 'Keller', 'Mayr', 'Hansen']
|
||||||
verifying,
|
const list: PendingUser[] = []
|
||||||
verifyUser: handleVerifyUser,
|
for (let i = 0; i < 18; i++) {
|
||||||
isAdmin,
|
const fn = rand(first)
|
||||||
fetchPendingUsers
|
const ln = rand(last)
|
||||||
} = useAdminUsers()
|
list.push({
|
||||||
const [isClient, setIsClient] = useState(false)
|
id: `P${i + 1}`,
|
||||||
|
firstName: fn,
|
||||||
// Handle client-side mounting
|
lastName: ln,
|
||||||
useEffect(() => {
|
email: `${fn}.${ln}${i}@example.com`.toLowerCase(),
|
||||||
setIsClient(true)
|
type: Math.random() > 0.9 ? 'company' : 'personal',
|
||||||
|
role: Math.random() > 0.95 ? 'admin' : 'user',
|
||||||
|
created: daysAgo(Math.floor(Math.random() * 12)),
|
||||||
|
lastLogin: daysAgo(Math.floor(Math.random() * 5)),
|
||||||
|
status: 'pending'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const [users, setUsers] = useState<PendingUser[]>(seedUsers)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [fType, setFType] = useState<'all' | UserType>('all')
|
const [fType, setFType] = useState<'all' | UserType>('all')
|
||||||
const [fRole, setFRole] = useState<'all' | UserRole>('all')
|
const [fRole, setFRole] = useState<'all' | UserRole>('all')
|
||||||
const [perPage, setPerPage] = useState(10)
|
const [perPage, setPerPage] = useState(10)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
// All computations must be after hooks but before conditional returns
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
return pendingUsers.filter(u => {
|
return users.filter(u =>
|
||||||
const firstName = u.first_name || ''
|
u.status === 'pending' &&
|
||||||
const lastName = u.last_name || ''
|
(fType === 'all' || u.type === fType) &&
|
||||||
const companyName = u.company_name || ''
|
|
||||||
const fullName = u.user_type === 'company' ? companyName : `${firstName} ${lastName}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
(fType === 'all' || u.user_type === fType) &&
|
|
||||||
(fRole === 'all' || u.role === fRole) &&
|
(fRole === 'all' || u.role === fRole) &&
|
||||||
(
|
(
|
||||||
!search.trim() ||
|
!search.trim() ||
|
||||||
u.email.toLowerCase().includes(search.toLowerCase()) ||
|
u.email.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
fullName.toLowerCase().includes(search.toLowerCase())
|
`${u.firstName} ${u.lastName}`.toLowerCase().includes(search.toLowerCase())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
}, [users, search, fType, fRole])
|
||||||
}, [pendingUsers, search, fType, fRole])
|
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(filtered.length / perPage))
|
const totalPages = Math.max(1, Math.ceil(filtered.length / perPage))
|
||||||
const current = filtered.slice((page - 1) * perPage, page * perPage)
|
const current = filtered.slice((page - 1) * perPage, page * perPage)
|
||||||
@ -84,56 +104,13 @@ export default function AdminUserVerifyPage() {
|
|||||||
return badge('Active', 'bg-green-100 text-green-700')
|
return badge('Active', 'bg-green-100 text-green-700')
|
||||||
}
|
}
|
||||||
|
|
||||||
const verificationStatusBadge = (user: PendingUser) => {
|
const verifyUser = useCallback((id: string) => {
|
||||||
const steps = [
|
setUsers(prev => prev.map(u => u.id === id ? { ...u, status: 'verifying' } : u))
|
||||||
{ name: 'Email', completed: user.email_verified === 1 },
|
// simulate async approval
|
||||||
{ name: 'Profile', completed: user.profile_completed === 1 },
|
setTimeout(() => {
|
||||||
{ name: 'Documents', completed: user.documents_uploaded === 1 },
|
setUsers(prev => prev.map(u => u.id === id ? { ...u, status: 'active' } : u))
|
||||||
{ name: 'Contract', completed: user.contract_signed === 1 }
|
}, 900)
|
||||||
]
|
}, [])
|
||||||
|
|
||||||
const completedSteps = steps.filter(s => s.completed).length
|
|
||||||
const totalSteps = steps.length
|
|
||||||
|
|
||||||
if (completedSteps === totalSteps) {
|
|
||||||
return badge('Ready to Verify', 'bg-green-100 text-green-700')
|
|
||||||
} else {
|
|
||||||
return badge(`${completedSteps}/${totalSteps} Steps`, 'bg-gray-100 text-gray-700')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show loading during SSR/initial client render
|
|
||||||
if (!isClient) {
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
<div className="relative flex flex-col flex-1 px-4 sm:px-6 lg:px-10 py-10">
|
|
||||||
<div className="relative mx-auto w-full max-w-4xl space-y-8 bg-white/95 backdrop-blur rounded-2xl shadow-2xl ring-1 ring-black/10 p-6 sm:p-10">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="h-4 w-4 rounded-full border-2 border-blue-500 border-b-transparent animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">Loading...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access check (only after client-side hydration)
|
|
||||||
if (!isAdmin) {
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
<div className="relative flex flex-col flex-1 px-4 sm:px-6 lg:px-10 py-10">
|
|
||||||
<div className="relative mx-auto w-full max-w-4xl space-y-8 bg-white/95 backdrop-blur rounded-2xl shadow-2xl ring-1 ring-red-500/20 p-6 sm:p-10">
|
|
||||||
<div className="text-center">
|
|
||||||
<ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" />
|
|
||||||
<h1 className="text-2xl font-bold text-red-600 mb-2">Access Denied</h1>
|
|
||||||
<p className="text-gray-600">You need admin privileges to access this page.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
@ -164,30 +141,13 @@ export default function AdminUserVerifyPage() {
|
|||||||
<div className="relative mx-auto w-full max-w-7xl space-y-8 bg-white/95 backdrop-blur rounded-2xl shadow-2xl ring-1 ring-black/10 p-6 sm:p-10">
|
<div className="relative mx-auto w-full max-w-7xl space-y-8 bg-white/95 backdrop-blur rounded-2xl shadow-2xl ring-1 ring-black/10 p-6 sm:p-10">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight text-[#0e2f63]">
|
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight text-[#0e2f63]">
|
||||||
User Verification Center
|
Users Pending Verification
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-sm sm:text-base text-[#33507d] font-medium">
|
<p className="mt-2 text-sm sm:text-base text-[#33507d] font-medium">
|
||||||
Review and verify all users who need admin approval. Users must complete all steps before verification.
|
Review and verify users who have completed all registration steps.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-lg border border-red-300 bg-red-50 text-red-700 px-5 py-4 flex gap-3 items-start">
|
|
||||||
<ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold">Error loading data</p>
|
|
||||||
<p className="text-sm text-red-600">{error}</p>
|
|
||||||
<button
|
|
||||||
onClick={fetchPendingUsers}
|
|
||||||
className="mt-2 text-sm underline hover:no-underline"
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Filter Card */}
|
{/* Filter Card */}
|
||||||
<form
|
<form
|
||||||
onSubmit={applyFilters}
|
onSubmit={applyFilters}
|
||||||
@ -267,39 +227,16 @@ export default function AdminUserVerifyPage() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left">User</th>
|
<th className="px-4 py-2 text-left">User</th>
|
||||||
<th className="px-4 py-2 text-left">Type</th>
|
<th className="px-4 py-2 text-left">Type</th>
|
||||||
<th className="px-4 py-2 text-left">Progress</th>
|
|
||||||
<th className="px-4 py-2 text-left">Status</th>
|
<th className="px-4 py-2 text-left">Status</th>
|
||||||
<th className="px-4 py-2 text-left">Role</th>
|
<th className="px-4 py-2 text-left">Role</th>
|
||||||
<th className="px-4 py-2 text-left">Created</th>
|
<th className="px-4 py-2 text-left">Created</th>
|
||||||
|
<th className="px-4 py-2 text-left">Last Login</th>
|
||||||
<th className="px-4 py-2 text-left">Actions</th>
|
<th className="px-4 py-2 text-left">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{loading ? (
|
{current.map(u => {
|
||||||
<tr>
|
const initials = `${u.firstName[0]}${u.lastName[0]}`.toUpperCase()
|
||||||
<td colSpan={7} className="px-4 py-10 text-center">
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<div className="h-4 w-4 rounded-full border-2 border-blue-500 border-b-transparent animate-spin" />
|
|
||||||
<span className="text-sm text-gray-500">Loading users...</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : 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 (
|
return (
|
||||||
<tr key={u.id} className="hover:bg-gray-50">
|
<tr key={u.id} className="hover:bg-gray-50">
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
@ -309,29 +246,31 @@ export default function AdminUserVerifyPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-900 leading-tight">
|
<div className="font-medium text-gray-900 leading-tight">
|
||||||
{displayName}
|
{u.firstName} {u.lastName}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[11px] text-gray-500">{u.email}</div>
|
<div className="text-[11px] text-gray-500">{u.email}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">{typeBadge(u.user_type)}</td>
|
<td className="px-4 py-3">{typeBadge(u.type)}</td>
|
||||||
<td className="px-4 py-3">{verificationStatusBadge(u)}</td>
|
|
||||||
<td className="px-4 py-3">{statusBadge(u.status)}</td>
|
<td className="px-4 py-3">{statusBadge(u.status)}</td>
|
||||||
<td className="px-4 py-3">{roleBadge(u.role)}</td>
|
<td className="px-4 py-3">{roleBadge(u.role)}</td>
|
||||||
<td className="px-4 py-3 text-gray-700">{createdDate}</td>
|
<td className="px-4 py-3 text-gray-700">{u.created}</td>
|
||||||
|
<td className="px-4 py-3 text-gray-500 italic">{u.lastLogin}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
{isReadyToVerify ? (
|
{u.status === 'active' ? (
|
||||||
|
<span className="text-xs font-medium text-emerald-600">Verified</span>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleVerifyUser(u.id.toString())}
|
onClick={() => verifyUser(u.id)}
|
||||||
disabled={isVerifying}
|
disabled={u.status !== 'pending'}
|
||||||
className={`inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs font-medium transition
|
className={`inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs font-medium transition
|
||||||
${isVerifying
|
${u.status === 'pending'
|
||||||
? 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
|
? 'border-emerald-200 bg-emerald-50 hover:bg-emerald-100 text-emerald-700'
|
||||||
: 'border-emerald-200 bg-emerald-50 hover:bg-emerald-100 text-emerald-700'
|
: 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isVerifying ? (
|
{u.status === 'verifying' ? (
|
||||||
<>
|
<>
|
||||||
<span className="h-3 w-3 rounded-full border-2 border-emerald-500 border-b-transparent animate-spin" />
|
<span className="h-3 w-3 rounded-full border-2 border-emerald-500 border-b-transparent animate-spin" />
|
||||||
Verifying...
|
Verifying...
|
||||||
@ -342,17 +281,15 @@ export default function AdminUserVerifyPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
|
||||||
<span className="text-xs text-gray-500 italic">Incomplete steps</span>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{current.length === 0 && !loading && (
|
{current.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="px-4 py-10 text-center text-sm text-gray-500">
|
<td colSpan={7} className="px-4 py-10 text-center text-sm text-gray-500">
|
||||||
No unverified users match current filters.
|
No pending users match current filters.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,113 +0,0 @@
|
|||||||
'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<PendingUser[]>([])
|
|
||||||
const [userStats, setUserStats] = useState<AdminUserStats | null>(null)
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [verifying, setVerifying] = useState<Set<string>>(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -35,11 +35,6 @@ export const API_ENDPOINTS = {
|
|||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
ADMIN_USERS: '/api/admin/users/:id/full',
|
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
|
// API Helper Functions
|
||||||
@ -231,51 +226,6 @@ export interface ApiError {
|
|||||||
code?: string
|
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
|
// Response Types
|
||||||
export interface UserStatus {
|
export interface UserStatus {
|
||||||
emailVerified: boolean
|
emailVerified: boolean
|
||||||
@ -284,33 +234,6 @@ export interface UserStatus {
|
|||||||
contractSigned: boolean
|
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<T = any> {
|
export interface ApiResponse<T = any> {
|
||||||
success: boolean
|
success: boolean
|
||||||
message?: string
|
message?: string
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user