feat: add UserDetailModal and integrate detailed user view in admin pages
This commit is contained in:
parent
3ee6e90128
commit
d4f5196146
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useMemo, useState, useEffect, useCallback } from 'react'
|
import { useMemo, useState, useEffect, useCallback } from 'react'
|
||||||
import PageLayout from '../../components/PageLayout'
|
import PageLayout from '../../components/PageLayout'
|
||||||
|
import UserDetailModal from '../../components/UserDetailModal'
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
@ -88,6 +89,10 @@ export default function AdminUserManagementPage() {
|
|||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const PAGE_SIZE = 10
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
// Modal state
|
||||||
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false)
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
return allUsers.filter(u => {
|
return allUsers.filter(u => {
|
||||||
const firstName = u.first_name || ''
|
const firstName = u.first_name || ''
|
||||||
@ -225,7 +230,10 @@ export default function AdminUserManagementPage() {
|
|||||||
r==='admin' ? badge('Admin','indigo') : badge('User','gray')
|
r==='admin' ? badge('Admin','indigo') : badge('User','gray')
|
||||||
|
|
||||||
// Action stubs
|
// Action stubs
|
||||||
const onView = (id: string) => console.log('View', id)
|
const onView = (id: string) => {
|
||||||
|
setSelectedUserId(id)
|
||||||
|
setIsDetailModalOpen(true)
|
||||||
|
}
|
||||||
const onEdit = (id: string) => console.log('Edit', id)
|
const onEdit = (id: string) => console.log('Edit', id)
|
||||||
const onDelete = (id: string) => console.log('Delete', id)
|
const onDelete = (id: string) => console.log('Delete', id)
|
||||||
|
|
||||||
@ -469,6 +477,16 @@ export default function AdminUserManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* User Detail Modal */}
|
||||||
|
<UserDetailModal
|
||||||
|
isOpen={isDetailModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDetailModalOpen(false)
|
||||||
|
setSelectedUserId(null)
|
||||||
|
}}
|
||||||
|
userId={selectedUserId}
|
||||||
|
/>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import { useMemo, useState, useEffect } from 'react'
|
import { useMemo, useState, useEffect } from 'react'
|
||||||
import PageLayout from '../../components/PageLayout'
|
import PageLayout from '../../components/PageLayout'
|
||||||
|
import UserDetailModal from '../../components/UserDetailModal'
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ExclamationTriangleIcon
|
ExclamationTriangleIcon,
|
||||||
|
EyeIcon
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
import { useAdminUsers } from '../../hooks/useAdminUsers'
|
import { useAdminUsers } from '../../hooks/useAdminUsers'
|
||||||
import { PendingUser } from '../../utils/api'
|
import { PendingUser } from '../../utils/api'
|
||||||
@ -58,6 +60,10 @@ export default function AdminUserVerifyPage() {
|
|||||||
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)
|
||||||
|
|
||||||
|
// Modal state
|
||||||
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false)
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
|
||||||
|
|
||||||
const applyFilters = (e: React.FormEvent) => {
|
const applyFilters = (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setPage(1)
|
setPage(1)
|
||||||
@ -295,30 +301,42 @@ export default function AdminUserVerifyPage() {
|
|||||||
<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">{createdDate}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
{isReadyToVerify ? (
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleVerifyUser(u.id.toString())}
|
onClick={() => {
|
||||||
disabled={isVerifying}
|
setSelectedUserId(u.id.toString())
|
||||||
className={`inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs font-medium transition
|
setIsDetailModalOpen(true)
|
||||||
${isVerifying
|
}}
|
||||||
? 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
|
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"
|
||||||
: 'border-emerald-200 bg-emerald-50 hover:bg-emerald-100 text-emerald-700'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{isVerifying ? (
|
<EyeIcon className="h-4 w-4" /> View
|
||||||
<>
|
|
||||||
<span className="h-3 w-3 rounded-full border-2 border-emerald-500 border-b-transparent animate-spin" />
|
|
||||||
Verifying...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CheckIcon className="h-4 w-4" /> Verify
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
) : (
|
|
||||||
<span className="text-xs text-gray-500 italic">Incomplete steps</span>
|
{isReadyToVerify ? (
|
||||||
)}
|
<button
|
||||||
|
onClick={() => handleVerifyUser(u.id.toString())}
|
||||||
|
disabled={isVerifying}
|
||||||
|
className={`inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs font-medium transition
|
||||||
|
${isVerifying
|
||||||
|
? '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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isVerifying ? (
|
||||||
|
<>
|
||||||
|
<span className="h-3 w-3 rounded-full border-2 border-emerald-500 border-b-transparent animate-spin" />
|
||||||
|
Verifying...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckIcon className="h-4 w-4" /> Verify
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-500 italic">Incomplete steps</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
@ -358,6 +376,16 @@ export default function AdminUserVerifyPage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* User Detail Modal */}
|
||||||
|
<UserDetailModal
|
||||||
|
isOpen={isDetailModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDetailModalOpen(false)
|
||||||
|
setSelectedUserId(null)
|
||||||
|
}}
|
||||||
|
userId={selectedUserId}
|
||||||
|
/>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
433
src/app/components/UserDetailModal.tsx
Normal file
433
src/app/components/UserDetailModal.tsx
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Fragment, useState, useEffect } from 'react'
|
||||||
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
import {
|
||||||
|
XMarkIcon,
|
||||||
|
UserIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
EnvelopeIcon,
|
||||||
|
PhoneIcon,
|
||||||
|
MapPinIcon,
|
||||||
|
BuildingOfficeIcon,
|
||||||
|
IdentificationIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
XCircleIcon
|
||||||
|
} from '@heroicons/react/24/outline'
|
||||||
|
import { AdminAPI, DetailedUserInfo } from '../utils/api'
|
||||||
|
import useAuthStore from '../store/authStore'
|
||||||
|
|
||||||
|
interface UserDetailModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
userId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailModalProps) {
|
||||||
|
const [userDetails, setUserDetails] = useState<DetailedUserInfo | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const token = useAuthStore(state => state.accessToken)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && userId && token) {
|
||||||
|
fetchUserDetails()
|
||||||
|
}
|
||||||
|
}, [isOpen, userId, token])
|
||||||
|
|
||||||
|
const fetchUserDetails = async () => {
|
||||||
|
if (!userId || !token) return
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await AdminAPI.getDetailedUserInfo(token, userId)
|
||||||
|
if (response.success) {
|
||||||
|
setUserDetails(response)
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Failed to fetch user details')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch user details'
|
||||||
|
setError(errorMessage)
|
||||||
|
console.error('UserDetailModal.fetchUserDetails error:', err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusBadge = ({ status, verified }: { status: boolean, verified?: boolean }) => {
|
||||||
|
if (verified) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">
|
||||||
|
<CheckCircleIcon className="h-3 w-3" />
|
||||||
|
Verified
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ${
|
||||||
|
status
|
||||||
|
? 'bg-green-100 text-green-700'
|
||||||
|
: 'bg-red-100 text-red-700'
|
||||||
|
}`}>
|
||||||
|
{status ? <CheckCircleIcon className="h-3 w-3" /> : <XCircleIcon className="h-3 w-3" />}
|
||||||
|
{status ? 'Complete' : 'Incomplete'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl sm:p-6">
|
||||||
|
<div className="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:flex sm:items-start">
|
||||||
|
<div className="w-full">
|
||||||
|
<Dialog.Title as="h3" className="text-lg font-semibold leading-6 text-gray-900 mb-6">
|
||||||
|
User Details
|
||||||
|
</Dialog.Title>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="h-8 w-8 rounded-full border-2 border-blue-500 border-b-transparent animate-spin" />
|
||||||
|
<span className="ml-3 text-gray-600">Loading user details...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-red-50 p-4 mb-6">
|
||||||
|
<div className="text-sm text-red-700">{error}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{userDetails && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Basic User Info */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<UserIcon className="h-5 w-5 text-gray-600" />
|
||||||
|
<h4 className="text-sm font-medium text-gray-900">Basic Information</h4>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Email:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.user.email}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Type:</span>
|
||||||
|
<span className="ml-2 text-gray-600 capitalize">{userDetails.user.user_type}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Role:</span>
|
||||||
|
<span className="ml-2 text-gray-600 capitalize">{userDetails.user.role}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Created:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{formatDate(userDetails.user.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
{userDetails.user.last_login_at && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Last Login:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{formatDate(userDetails.user.last_login_at)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Verification Status */}
|
||||||
|
{userDetails.userStatus && (
|
||||||
|
<div className="bg-blue-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<ShieldCheckIcon className="h-5 w-5 text-blue-600" />
|
||||||
|
<h4 className="text-sm font-medium text-gray-900">Verification Status</h4>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-700">Email</span>
|
||||||
|
<StatusBadge status={userDetails.userStatus.email_verified === 1} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-700">Profile</span>
|
||||||
|
<StatusBadge status={userDetails.userStatus.profile_completed === 1} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-700">Documents</span>
|
||||||
|
<StatusBadge status={userDetails.userStatus.documents_uploaded === 1} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-700">Contract</span>
|
||||||
|
<StatusBadge status={userDetails.userStatus.contract_signed === 1} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-700">Admin Verified</span>
|
||||||
|
<StatusBadge
|
||||||
|
status={userDetails.userStatus.is_admin_verified === 1}
|
||||||
|
verified={userDetails.userStatus.is_admin_verified === 1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Profile Information */}
|
||||||
|
{(userDetails.personalProfile || userDetails.companyProfile) && (
|
||||||
|
<div className="bg-green-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
{userDetails.user.user_type === 'personal' ? (
|
||||||
|
<UserIcon className="h-5 w-5 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<BuildingOfficeIcon className="h-5 w-5 text-green-600" />
|
||||||
|
)}
|
||||||
|
<h4 className="text-sm font-medium text-gray-900">Profile Information</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{userDetails.personalProfile && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Name:</span>
|
||||||
|
<span className="ml-2 text-gray-600">
|
||||||
|
{userDetails.personalProfile.first_name} {userDetails.personalProfile.last_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{userDetails.personalProfile.phone && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Phone:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.personalProfile.phone}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{userDetails.personalProfile.date_of_birth && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Date of Birth:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{formatDate(userDetails.personalProfile.date_of_birth)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{userDetails.personalProfile.address && (
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<span className="font-medium text-gray-700">Address:</span>
|
||||||
|
<span className="ml-2 text-gray-600">
|
||||||
|
{userDetails.personalProfile.address}, {userDetails.personalProfile.postal_code} {userDetails.personalProfile.city}, {userDetails.personalProfile.country}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{userDetails.companyProfile && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Company Name:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.companyProfile.company_name}</span>
|
||||||
|
</div>
|
||||||
|
{userDetails.companyProfile.tax_id && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Tax ID:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.companyProfile.tax_id}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{userDetails.companyProfile.registration_number && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Registration Number:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.companyProfile.registration_number}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{userDetails.companyProfile.phone && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Phone:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.companyProfile.phone}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{userDetails.companyProfile.address && (
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<span className="font-medium text-gray-700">Address:</span>
|
||||||
|
<span className="ml-2 text-gray-600">
|
||||||
|
{userDetails.companyProfile.address}, {userDetails.companyProfile.postal_code} {userDetails.companyProfile.city}, {userDetails.companyProfile.country}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Documents */}
|
||||||
|
{(userDetails.documents.length > 0 || userDetails.contracts.length > 0 || userDetails.idDocuments.length > 0) && (
|
||||||
|
<div className="bg-purple-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<DocumentTextIcon className="h-5 w-5 text-purple-600" />
|
||||||
|
<h4 className="text-sm font-medium text-gray-900">Documents</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Regular Documents */}
|
||||||
|
{userDetails.documents.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h5 className="text-xs font-medium text-gray-700 mb-2">Uploaded Documents</h5>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{userDetails.documents.map((doc) => (
|
||||||
|
<div key={doc.id} className="flex items-center justify-between bg-white p-2 rounded border">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-gray-900">{doc.file_name}</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-2">({formatFileSize(doc.file_size)})</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500">{formatDate(doc.uploaded_at)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Contracts */}
|
||||||
|
{userDetails.contracts.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h5 className="text-xs font-medium text-gray-700 mb-2">Contracts</h5>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{userDetails.contracts.map((contract) => (
|
||||||
|
<div key={contract.id} className="flex items-center justify-between bg-white p-2 rounded border">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-gray-900">{contract.file_name}</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-2">({formatFileSize(contract.file_size)})</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500">{formatDate(contract.uploaded_at)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ID Documents */}
|
||||||
|
{userDetails.idDocuments.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h5 className="text-xs font-medium text-gray-700 mb-2">ID Documents</h5>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{userDetails.idDocuments.map((idDoc) => (
|
||||||
|
<div key={idDoc.id} className="bg-white p-3 rounded border">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<IdentificationIcon className="h-4 w-4 text-gray-600" />
|
||||||
|
<span className="text-sm font-medium text-gray-900">{idDoc.document_type}</span>
|
||||||
|
<span className="text-xs text-gray-500">{formatDate(idDoc.uploaded_at)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
{idDoc.frontUrl && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-700 mb-1">Front:</p>
|
||||||
|
<img
|
||||||
|
src={idDoc.frontUrl}
|
||||||
|
alt="ID Front"
|
||||||
|
className="max-w-full h-32 object-contain border rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{idDoc.backUrl && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-700 mb-1">Back:</p>
|
||||||
|
<img
|
||||||
|
src={idDoc.backUrl}
|
||||||
|
alt="ID Back"
|
||||||
|
className="max-w-full h-32 object-contain border rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Permissions */}
|
||||||
|
{userDetails.permissions.length > 0 && (
|
||||||
|
<div className="bg-indigo-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<ShieldCheckIcon className="h-5 w-5 text-indigo-600" />
|
||||||
|
<h4 className="text-sm font-medium text-gray-900">Permissions</h4>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
{userDetails.permissions.map((permission) => (
|
||||||
|
<div key={permission.id} className="bg-white p-2 rounded border">
|
||||||
|
<div className="text-sm font-medium text-gray-900">{permission.name}</div>
|
||||||
|
{permission.description && (
|
||||||
|
<div className="text-xs text-gray-600">{permission.description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 sm:mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -35,6 +35,7 @@ export const API_ENDPOINTS = {
|
|||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
ADMIN_USERS: '/api/admin/users/:id/full',
|
ADMIN_USERS: '/api/admin/users/:id/full',
|
||||||
|
ADMIN_USER_DETAILED: '/api/admin/users/:id/detailed',
|
||||||
ADMIN_USER_STATS: '/api/admin/user-stats',
|
ADMIN_USER_STATS: '/api/admin/user-stats',
|
||||||
ADMIN_USER_LIST: '/api/admin/user-list',
|
ADMIN_USER_LIST: '/api/admin/user-list',
|
||||||
ADMIN_VERIFICATION_PENDING: '/api/admin/verification-pending-users',
|
ADMIN_VERIFICATION_PENDING: '/api/admin/verification-pending-users',
|
||||||
@ -265,6 +266,15 @@ export class AdminAPI {
|
|||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getDetailedUserInfo(token: string, userId: string) {
|
||||||
|
const endpoint = API_ENDPOINTS.ADMIN_USER_DETAILED.replace(':id', userId)
|
||||||
|
const response = await ApiClient.get(endpoint, token)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch detailed user info')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
static async verifyUser(token: string, userId: string, permissions: string[] = []) {
|
static async verifyUser(token: string, userId: string, permissions: string[] = []) {
|
||||||
const endpoint = API_ENDPOINTS.ADMIN_VERIFY_USER.replace(':id', userId)
|
const endpoint = API_ENDPOINTS.ADMIN_VERIFY_USER.replace(':id', userId)
|
||||||
const response = await ApiClient.post(endpoint, { permissions }, token)
|
const response = await ApiClient.post(endpoint, { permissions }, token)
|
||||||
@ -311,6 +321,81 @@ export interface PendingUser {
|
|||||||
company_name?: string
|
company_name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DetailedUserInfo {
|
||||||
|
user: {
|
||||||
|
id: number
|
||||||
|
email: string
|
||||||
|
user_type: 'personal' | 'company'
|
||||||
|
role: 'user' | 'admin'
|
||||||
|
created_at: string
|
||||||
|
last_login_at: string | null
|
||||||
|
}
|
||||||
|
personalProfile?: {
|
||||||
|
user_id: number
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
date_of_birth?: string
|
||||||
|
phone?: string
|
||||||
|
address?: string
|
||||||
|
city?: string
|
||||||
|
postal_code?: string
|
||||||
|
country?: string
|
||||||
|
}
|
||||||
|
companyProfile?: {
|
||||||
|
user_id: number
|
||||||
|
company_name: string
|
||||||
|
tax_id?: string
|
||||||
|
registration_number?: string
|
||||||
|
phone?: string
|
||||||
|
address?: string
|
||||||
|
city?: string
|
||||||
|
postal_code?: string
|
||||||
|
country?: string
|
||||||
|
}
|
||||||
|
userStatus?: {
|
||||||
|
user_id: number
|
||||||
|
status: string
|
||||||
|
email_verified: number
|
||||||
|
profile_completed: number
|
||||||
|
documents_uploaded: number
|
||||||
|
contract_signed: number
|
||||||
|
is_admin_verified: number
|
||||||
|
admin_verified_at?: string
|
||||||
|
}
|
||||||
|
permissions: Array<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
is_active: boolean
|
||||||
|
}>
|
||||||
|
documents: Array<{
|
||||||
|
id: number
|
||||||
|
user_id: number
|
||||||
|
document_type: string
|
||||||
|
file_name: string
|
||||||
|
file_size: number
|
||||||
|
uploaded_at: string
|
||||||
|
}>
|
||||||
|
contracts: Array<{
|
||||||
|
id: number
|
||||||
|
user_id: number
|
||||||
|
document_type: string
|
||||||
|
file_name: string
|
||||||
|
file_size: number
|
||||||
|
uploaded_at: string
|
||||||
|
}>
|
||||||
|
idDocuments: Array<{
|
||||||
|
id: number
|
||||||
|
user_id: number
|
||||||
|
document_type: string
|
||||||
|
front_object_storage_id?: string
|
||||||
|
back_object_storage_id?: string
|
||||||
|
frontUrl?: string
|
||||||
|
backUrl?: string
|
||||||
|
uploaded_at: 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