Compare commits

..

No commits in common. "92d96d0644a22b15e9baca79c22b13f737f21c70" and "c16ce3093cd805875e39e0b4caa5a66078e00b0c" have entirely different histories.

5 changed files with 164 additions and 588 deletions

View File

@ -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
@ -44,38 +36,6 @@ export default function AdminDashboardPage() {
memory: '0.1 / 7.8', memory: '0.1 / 7.8',
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>
@ -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

View File

@ -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']
export default function AdminUserManagementPage() { // Helpers
const { isAdmin } = useAdminUsers() const rand = (arr: any[]) => arr[Math.floor(Math.random() * arr.length)]
const token = useAuthStore(state => state.accessToken) const daysAgo = (d: number) => {
const [isClient, setIsClient] = useState(false) const dt = new Date()
dt.setDate(dt.getDate() - d)
// State for all users (not just pending) return dt.toISOString().slice(0,10)
const [allUsers, setAllUsers] = useState<User[]>([]) }
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// Handle client-side mounting
useEffect(() => {
setIsClient(true)
}, [])
// Fetch all users from backend
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 export default function AdminUserManagementPage() {
// Mock users (stable memo)
const users = useMemo<User[]>(() => {
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']
const list: User[] = []
for (let i=0;i<101;i++){
const fn = rand(firstNames)
const ln = rand(lastNames)
const createdDays = Math.floor(Math.random()*15)
const loginDays = Math.random() > 0.2 ? Math.floor(Math.random()*15) : null
list.push({
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
}, [])
// Filters
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,68 +72,20 @@ 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 || '' (fRole==='all'||u.role===fRole) &&
const fullName = u.user_type === 'company' ? companyName : `${firstName} ${lastName}` (
!search.trim() ||
// Map backend status to frontend status u.email.toLowerCase().includes(search.toLowerCase()) ||
// Backend status can be: 'pending', 'active', 'suspended', etc. `${u.firstName} ${u.lastName}`.toLowerCase().includes(search.toLowerCase())
// 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) &&
(
!search.trim() ||
u.email.toLowerCase().includes(search.toLowerCase()) ||
fullName.toLowerCase().includes(search.toLowerCase())
)
) )
}) )
}, [allUsers, search, fType, fStatus, fRole]) }, [users, 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()
@ -223,24 +158,7 @@ export default function AdminUserManagementPage() {
</p> </p>
</div> </div>
{/* Error Message */} {/* Filter Card */}
{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 */}
<form <form
onSubmit={applyFilter} onSubmit={applyFilter}
className="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 px-4 sm:px-6 py-5 flex flex-col gap-4" className="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 px-4 sm:px-6 py-5 flex flex-col gap-4"
@ -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

View File

@ -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 || '' (fRole === 'all' || u.role === fRole) &&
const fullName = u.user_type === 'company' ? companyName : `${firstName} ${lastName}` (
!search.trim() ||
return ( u.email.toLowerCase().includes(search.toLowerCase()) ||
(fType === 'all' || u.user_type === fType) && `${u.firstName} ${u.lastName}`.toLowerCase().includes(search.toLowerCase())
(fRole === 'all' || u.role === fRole) &&
(
!search.trim() ||
u.email.toLowerCase().includes(search.toLowerCase()) ||
fullName.toLowerCase().includes(search.toLowerCase())
)
) )
}) )
}, [pendingUsers, search, fType, fRole]) }, [users, 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>
)} )}

View File

@ -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)
}
}

View File

@ -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