From d4f5196146876bd0dfcdcc77b978f2ba64d08e92 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Wed, 22 Oct 2025 18:29:49 +0200 Subject: [PATCH] feat: add UserDetailModal and integrate detailed user view in admin pages --- src/app/admin/user-management/page.tsx | 20 +- src/app/admin/user-verify/page.tsx | 72 ++-- src/app/components/UserDetailModal.tsx | 433 +++++++++++++++++++++++++ src/app/utils/api.ts | 85 +++++ 4 files changed, 587 insertions(+), 23 deletions(-) create mode 100644 src/app/components/UserDetailModal.tsx diff --git a/src/app/admin/user-management/page.tsx b/src/app/admin/user-management/page.tsx index 69b41e1..88ffd91 100644 --- a/src/app/admin/user-management/page.tsx +++ b/src/app/admin/user-management/page.tsx @@ -2,6 +2,7 @@ import { useMemo, useState, useEffect, useCallback } from 'react' import PageLayout from '../../components/PageLayout' +import UserDetailModal from '../../components/UserDetailModal' import { MagnifyingGlassIcon, EyeIcon, @@ -88,6 +89,10 @@ export default function AdminUserManagementPage() { const [page, setPage] = useState(1) const PAGE_SIZE = 10 + // Modal state + const [isDetailModalOpen, setIsDetailModalOpen] = useState(false) + const [selectedUserId, setSelectedUserId] = useState(null) + const filtered = useMemo(() => { return allUsers.filter(u => { const firstName = u.first_name || '' @@ -225,7 +230,10 @@ export default function AdminUserManagementPage() { r==='admin' ? badge('Admin','indigo') : badge('User','gray') // 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 onDelete = (id: string) => console.log('Delete', id) @@ -469,6 +477,16 @@ export default function AdminUserManagementPage() { + + {/* User Detail Modal */} + { + setIsDetailModalOpen(false) + setSelectedUserId(null) + }} + userId={selectedUserId} + /> ) } \ No newline at end of file diff --git a/src/app/admin/user-verify/page.tsx b/src/app/admin/user-verify/page.tsx index cdd800c..669af2a 100644 --- a/src/app/admin/user-verify/page.tsx +++ b/src/app/admin/user-verify/page.tsx @@ -2,10 +2,12 @@ import { useMemo, useState, useEffect } from 'react' import PageLayout from '../../components/PageLayout' +import UserDetailModal from '../../components/UserDetailModal' import { MagnifyingGlassIcon, CheckIcon, - ExclamationTriangleIcon + ExclamationTriangleIcon, + EyeIcon } from '@heroicons/react/24/outline' import { useAdminUsers } from '../../hooks/useAdminUsers' import { PendingUser } from '../../utils/api' @@ -58,6 +60,10 @@ export default function AdminUserVerifyPage() { const totalPages = Math.max(1, Math.ceil(filtered.length / perPage)) const current = filtered.slice((page - 1) * perPage, page * perPage) + // Modal state + const [isDetailModalOpen, setIsDetailModalOpen] = useState(false) + const [selectedUserId, setSelectedUserId] = useState(null) + const applyFilters = (e: React.FormEvent) => { e.preventDefault() setPage(1) @@ -295,30 +301,42 @@ export default function AdminUserVerifyPage() { {roleBadge(u.role)} {createdDate} - {isReadyToVerify ? ( +
- ) : ( - Incomplete steps - )} + + {isReadyToVerify ? ( + + ) : ( + Incomplete steps + )} +
) @@ -358,6 +376,16 @@ export default function AdminUserVerifyPage() { + + {/* User Detail Modal */} + { + setIsDetailModalOpen(false) + setSelectedUserId(null) + }} + userId={selectedUserId} + /> ) } \ No newline at end of file diff --git a/src/app/components/UserDetailModal.tsx b/src/app/components/UserDetailModal.tsx new file mode 100644 index 0000000..b591492 --- /dev/null +++ b/src/app/components/UserDetailModal.tsx @@ -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(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(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 ( + + + Verified + + ) + } + return ( + + {status ? : } + {status ? 'Complete' : 'Incomplete'} + + ) + } + + return ( + + + +
+ + +
+
+ + +
+ +
+ +
+
+ + User Details + + + {loading && ( +
+
+ Loading user details... +
+ )} + + {error && ( +
+
{error}
+
+ )} + + {userDetails && ( +
+ {/* Basic User Info */} +
+
+ +

Basic Information

+
+
+
+ Email: + {userDetails.user.email} +
+
+ Type: + {userDetails.user.user_type} +
+
+ Role: + {userDetails.user.role} +
+
+ Created: + {formatDate(userDetails.user.created_at)} +
+ {userDetails.user.last_login_at && ( +
+ Last Login: + {formatDate(userDetails.user.last_login_at)} +
+ )} +
+
+ + {/* Verification Status */} + {userDetails.userStatus && ( +
+
+ +

Verification Status

+
+
+
+ Email + +
+
+ Profile + +
+
+ Documents + +
+
+ Contract + +
+
+ Admin Verified + +
+
+
+ )} + + {/* Profile Information */} + {(userDetails.personalProfile || userDetails.companyProfile) && ( +
+
+ {userDetails.user.user_type === 'personal' ? ( + + ) : ( + + )} +

Profile Information

+
+ + {userDetails.personalProfile && ( +
+
+ Name: + + {userDetails.personalProfile.first_name} {userDetails.personalProfile.last_name} + +
+ {userDetails.personalProfile.phone && ( +
+ Phone: + {userDetails.personalProfile.phone} +
+ )} + {userDetails.personalProfile.date_of_birth && ( +
+ Date of Birth: + {formatDate(userDetails.personalProfile.date_of_birth)} +
+ )} + {userDetails.personalProfile.address && ( +
+ Address: + + {userDetails.personalProfile.address}, {userDetails.personalProfile.postal_code} {userDetails.personalProfile.city}, {userDetails.personalProfile.country} + +
+ )} +
+ )} + + {userDetails.companyProfile && ( +
+
+ Company Name: + {userDetails.companyProfile.company_name} +
+ {userDetails.companyProfile.tax_id && ( +
+ Tax ID: + {userDetails.companyProfile.tax_id} +
+ )} + {userDetails.companyProfile.registration_number && ( +
+ Registration Number: + {userDetails.companyProfile.registration_number} +
+ )} + {userDetails.companyProfile.phone && ( +
+ Phone: + {userDetails.companyProfile.phone} +
+ )} + {userDetails.companyProfile.address && ( +
+ Address: + + {userDetails.companyProfile.address}, {userDetails.companyProfile.postal_code} {userDetails.companyProfile.city}, {userDetails.companyProfile.country} + +
+ )} +
+ )} +
+ )} + + {/* Documents */} + {(userDetails.documents.length > 0 || userDetails.contracts.length > 0 || userDetails.idDocuments.length > 0) && ( +
+
+ +

Documents

+
+ + {/* Regular Documents */} + {userDetails.documents.length > 0 && ( +
+
Uploaded Documents
+
+ {userDetails.documents.map((doc) => ( +
+
+ {doc.file_name} + ({formatFileSize(doc.file_size)}) +
+ {formatDate(doc.uploaded_at)} +
+ ))} +
+
+ )} + + {/* Contracts */} + {userDetails.contracts.length > 0 && ( +
+
Contracts
+
+ {userDetails.contracts.map((contract) => ( +
+
+ {contract.file_name} + ({formatFileSize(contract.file_size)}) +
+ {formatDate(contract.uploaded_at)} +
+ ))} +
+
+ )} + + {/* ID Documents */} + {userDetails.idDocuments.length > 0 && ( +
+
ID Documents
+
+ {userDetails.idDocuments.map((idDoc) => ( +
+
+ + {idDoc.document_type} + {formatDate(idDoc.uploaded_at)} +
+
+ {idDoc.frontUrl && ( +
+

Front:

+ ID Front +
+ )} + {idDoc.backUrl && ( +
+

Back:

+ ID Back +
+ )} +
+
+ ))} +
+
+ )} +
+ )} + + {/* Permissions */} + {userDetails.permissions.length > 0 && ( +
+
+ +

Permissions

+
+
+ {userDetails.permissions.map((permission) => ( +
+
{permission.name}
+ {permission.description && ( +
{permission.description}
+ )} +
+ ))} +
+
+ )} +
+ )} +
+
+ +
+ +
+ + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/app/utils/api.ts b/src/app/utils/api.ts index ff0bba8..f2aedc0 100644 --- a/src/app/utils/api.ts +++ b/src/app/utils/api.ts @@ -35,6 +35,7 @@ export const API_ENDPOINTS = { // Admin ADMIN_USERS: '/api/admin/users/:id/full', + ADMIN_USER_DETAILED: '/api/admin/users/:id/detailed', ADMIN_USER_STATS: '/api/admin/user-stats', ADMIN_USER_LIST: '/api/admin/user-list', ADMIN_VERIFICATION_PENDING: '/api/admin/verification-pending-users', @@ -265,6 +266,15 @@ export class AdminAPI { 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[] = []) { const endpoint = API_ENDPOINTS.ADMIN_VERIFY_USER.replace(':id', userId) const response = await ApiClient.post(endpoint, { permissions }, token) @@ -311,6 +321,81 @@ export interface PendingUser { 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 { success: boolean message?: string