profit-planet-frontend/src/app/components/UserDetailModal.tsx

750 lines
41 KiB
TypeScript

'use client'
import { Fragment, useState, useEffect } from 'react'
import { Dialog, Transition, Listbox } from '@headlessui/react'
import {
XMarkIcon,
UserIcon,
DocumentTextIcon,
ShieldCheckIcon,
CalendarIcon,
EnvelopeIcon,
PhoneIcon,
MapPinIcon,
BuildingOfficeIcon,
IdentificationIcon,
CheckCircleIcon,
XCircleIcon,
ChevronUpDownIcon,
CheckIcon
} 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
onUserUpdated?: () => void
}
type UserStatus = 'inactive' | 'pending' | 'active' | 'suspended' | 'archived'
const STATUS_OPTIONS: { value: UserStatus; label: string; color: string }[] = [
{ value: 'pending', label: 'Pending', color: 'amber' },
{ value: 'active', label: 'Active', color: 'green' },
{ value: 'suspended', label: 'Suspended', color: 'rose' },
{ value: 'archived', label: 'Archived', color: 'gray' },
{ value: 'inactive', label: 'Inactive', color: 'gray' }
]
export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated }: UserDetailModalProps) {
const [userDetails, setUserDetails] = useState<DetailedUserInfo | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [saving, setSaving] = useState(false)
const [selectedStatus, setSelectedStatus] = useState<UserStatus>('pending')
const token = useAuthStore(state => state.accessToken)
// Contract preview state (lazy-loaded)
const [previewLoading, setPreviewLoading] = useState(false)
const [previewHtml, setPreviewHtml] = useState<string | null>(null)
const [previewError, setPreviewError] = useState<string | null>(null)
useEffect(() => {
if (isOpen && userId && token) {
fetchUserDetails()
}
}, [isOpen, userId, token])
useEffect(() => {
if (userDetails?.userStatus?.status) {
setSelectedStatus(userDetails.userStatus.status as UserStatus)
}
}, [userDetails])
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 handleStatusChange = async (newStatus: UserStatus) => {
if (!userId || !token || newStatus === selectedStatus) return
setSaving(true)
setError(null)
try {
const response = await AdminAPI.updateUserStatus(token, userId, newStatus)
if (response.success) {
setSelectedStatus(newStatus)
await fetchUserDetails()
if (onUserUpdated) {
onUserUpdated()
}
} else {
throw new Error(response.message || 'Failed to update user status')
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to update user status'
setError(errorMessage)
console.error('UserDetailModal.handleStatusChange error:', err)
} finally {
setSaving(false)
}
}
const handleToggleAdminVerification = async () => {
if (!userId || !token || !userDetails?.userStatus) return
setSaving(true)
setError(null)
try {
const newValue = userDetails.userStatus.is_admin_verified === 1 ? 0 : 1
const response = await AdminAPI.updateUserVerification(token, userId, newValue)
if (response.success) {
await fetchUserDetails()
if (onUserUpdated) {
onUserUpdated()
}
} else {
throw new Error(response.message || 'Failed to update verification status')
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to update verification status'
setError(errorMessage)
console.error('UserDetailModal.handleToggleAdminVerification error:', err)
} finally {
setSaving(false)
}
}
const loadContractPreview = async () => {
if (!userId || !token || !userDetails) return
setPreviewLoading(true)
setPreviewError(null)
try {
const html = await AdminAPI.getContractPreviewHtml(token, String(userId), userDetails.user.user_type)
setPreviewHtml(html)
} catch (e: any) {
console.error('UserDetailModal.loadContractPreview error:', e)
setPreviewError(e?.message || 'Failed to load contract preview')
setPreviewHtml(null)
} finally {
setPreviewLoading(false)
}
}
const formatDate = (dateString: string | undefined | null) => {
if (!dateString) return 'N/A'
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
const formatFileSize = (bytes: number | undefined) => {
if (!bytes) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
}
const getStatusColor = (status: UserStatus) => {
const option = STATUS_OPTIONS.find(opt => opt.value === status)
return option?.color || 'gray'
}
const getStatusBadgeClass = (color: string) => {
const colorMap: Record<string, string> = {
amber: 'bg-amber-100 text-amber-800 border-amber-200',
green: 'bg-green-100 text-green-800 border-green-200',
rose: 'bg-rose-100 text-rose-800 border-rose-200',
gray: 'bg-gray-100 text-gray-800 border-gray-200'
}
return colorMap[color] || colorMap.gray
}
if (!isOpen) return null
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-black/30 backdrop-blur-sm transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-6">
<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 shadow-xl transition-all w-full max-w-5xl max-h-[85vh] flex flex-col">
{/* Close Button */}
<div className="absolute right-0 top-0 z-10 pr-4 pt-4">
<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>
{/* Scrollable Content Area */}
<div className="overflow-y-auto px-4 pb-4 pt-5 sm:p-6">
<div className="w-full">
{loading ? (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
</div>
) : error ? (
<div className="rounded-md bg-red-50 p-4">
<div className="flex">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Error</h3>
<div className="mt-2 text-sm text-red-700">{error}</div>
</div>
</div>
</div>
) : userDetails ? (
<div className="space-y-6">
{/* Header Section with User Info & Status */}
<div className="bg-gradient-to-r from-indigo-500 to-purple-600 rounded-lg px-6 py-8 text-white">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<div className="bg-white/20 backdrop-blur-sm p-4 rounded-full">
{userDetails.user.user_type === 'company' ? (
<BuildingOfficeIcon className="h-10 w-10 text-white" />
) : (
<UserIcon className="h-10 w-10 text-white" />
)}
</div>
<div className="text-left">
<h2 className="text-2xl font-bold">
{userDetails.user.user_type === 'personal'
? `${userDetails.personalProfile?.first_name || ''} ${userDetails.personalProfile?.last_name || ''}`.trim()
: userDetails.companyProfile?.company_name || 'Unknown'}
</h2>
<p className="text-indigo-100 mt-1">{userDetails.user.email}</p>
<div className="flex items-center gap-2 mt-3">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
userDetails.user.user_type === 'personal'
? 'bg-blue-100 text-blue-800'
: 'bg-purple-100 text-purple-800'
}`}>
{userDetails.user.user_type === 'personal' ? 'Personal' : 'Company'}
</span>
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
userDetails.user.role === 'admin' || userDetails.user.role === 'super_admin'
? 'bg-yellow-100 text-yellow-800'
: 'bg-gray-100 text-gray-800'
}`}>
{userDetails.user.role === 'super_admin' ? 'Super Admin' : userDetails.user.role}
</span>
</div>
</div>
</div>
{/* Status Badge */}
{userDetails.userStatus && (
<div className="bg-white rounded-lg px-4 py-3 text-gray-900">
<div className="text-xs text-gray-500 mb-1">Current Status</div>
<div className={`inline-flex items-center px-3 py-1.5 rounded-full text-sm font-semibold border ${
getStatusBadgeClass(getStatusColor(userDetails.userStatus.status as UserStatus))
}`}>
{userDetails.userStatus.status.charAt(0).toUpperCase() + userDetails.userStatus.status.slice(1)}
</div>
</div>
)}
</div>
</div>
{/* Admin Controls Section */}
<div className="bg-gray-50 rounded-lg p-6 border border-gray-200">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<ShieldCheckIcon className="h-5 w-5 text-indigo-600" />
Admin Controls
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Status Dropdown */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Change Status
</label>
<Listbox value={selectedStatus} onChange={handleStatusChange} disabled={saving}>
<div className="relative">
<Listbox.Button className="relative w-full cursor-pointer rounded-lg bg-white py-2.5 pl-3 pr-10 text-left border border-gray-300 hover:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed">
<span className="block truncate font-medium">
{STATUS_OPTIONS.find(opt => opt.value === selectedStatus)?.label || selectedStatus}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{STATUS_OPTIONS.map((option) => (
<Listbox.Option
key={option.value}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
active ? 'bg-indigo-100 text-indigo-900' : 'text-gray-900'
}`
}
value={option.value}
>
{({ selected }) => (
<>
<span className={`block truncate ${selected ? 'font-semibold' : 'font-normal'}`}>
{option.label}
</span>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-indigo-600">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
{/* Admin Verification Toggle */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Admin Verification
</label>
<button
type="button"
onClick={handleToggleAdminVerification}
disabled={saving}
className={`w-full inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${
userDetails.userStatus?.is_admin_verified === 1
? 'bg-amber-600 hover:bg-amber-500 text-white focus-visible:outline-amber-600'
: 'bg-green-600 hover:bg-green-500 text-white focus-visible:outline-green-600'
}`}
>
{saving ? (
<>
<div className="h-4 w-4 border-2 border-white border-b-transparent rounded-full animate-spin" />
Updating...
</>
) : (
<>
<ShieldCheckIcon className="h-4 w-4" />
{userDetails.userStatus?.is_admin_verified === 1 ? 'Unverify User' : 'Verify User'}
</>
)}
</button>
</div>
</div>
</div>
{/* Contract Preview (admin verify flow) */}
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<DocumentTextIcon className="h-5 w-5 text-gray-600" />
Contract Preview
</h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={loadContractPreview}
disabled={previewLoading}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-3 py-2 text-sm disabled:opacity-60"
>
{previewLoading ? 'Loading…' : (previewHtml ? 'Refresh Preview' : 'Load Preview')}
</button>
<button
type="button"
onClick={() => {
if (!previewHtml) return
const blob = new Blob([previewHtml], { type: 'text/html' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
}}
disabled={!previewHtml}
className="inline-flex items-center justify-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-2 text-sm disabled:opacity-60"
>
Open in new tab
</button>
</div>
</div>
<div className="px-6 py-5">
{previewError && (
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 mb-4">
{previewError}
</div>
)}
{previewLoading && (
<div className="flex items-center justify-center h-40 text-sm text-gray-500">
Loading preview
</div>
)}
{!previewLoading && previewHtml && (
<div className="rounded-md border border-gray-200 overflow-hidden">
<iframe
title="Contract Preview"
className="w-full h-[600px] bg-white"
srcDoc={previewHtml}
/>
</div>
)}
{!previewLoading && !previewHtml && !previewError && (
<p className="text-sm text-gray-500">Click "Load Preview" to render the latest active contract template for this user.</p>
)}
</div>
</div>
{/* Profile Information */}
{userDetails.user.user_type === 'personal' && userDetails.personalProfile && (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<UserIcon className="h-5 w-5 text-gray-600" />
Personal Information
</h3>
</div>
<div className="px-6 py-5">
<dl className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-5">
<div>
<dt className="text-sm font-medium text-gray-500 mb-1.5">First Name</dt>
<dd className="text-sm text-gray-900 font-medium">{userDetails.personalProfile.first_name || 'N/A'}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500 mb-1.5">Last Name</dt>
<dd className="text-sm text-gray-900 font-medium">{userDetails.personalProfile.last_name || 'N/A'}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500 mb-1.5">
<PhoneIcon className="h-4 w-4 inline mr-1.5" />
Phone
</dt>
<dd className="text-sm text-gray-900 font-medium">{userDetails.personalProfile.phone || 'N/A'}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500 mb-1.5">
<CalendarIcon className="h-4 w-4 inline mr-1.5" />
Date of Birth
</dt>
<dd className="text-sm text-gray-900 font-medium">{formatDate(userDetails.personalProfile.date_of_birth)}</dd>
</div>
<div className="md:col-span-2">
<dt className="text-sm font-medium text-gray-500 mb-1.5">
<MapPinIcon className="h-4 w-4 inline mr-1.5" />
Address
</dt>
<dd className="text-sm text-gray-900 font-medium">
{userDetails.personalProfile.address || 'N/A'}
{userDetails.personalProfile.city && <>, {userDetails.personalProfile.city}</>}
{userDetails.personalProfile.zip_code && <>, {userDetails.personalProfile.zip_code}</>}
{userDetails.personalProfile.country && <>, {userDetails.personalProfile.country}</>}
</dd>
</div>
</dl>
</div>
</div>
)}
{/* Company Profile Information */}
{userDetails.user.user_type === 'company' && userDetails.companyProfile && (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<BuildingOfficeIcon className="h-5 w-5 text-gray-600" />
Company Information
</h3>
</div>
<div className="px-6 py-5">
<dl className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-5">
<div>
<dt className="text-sm font-medium text-gray-500 mb-1.5">Company Name</dt>
<dd className="text-sm text-gray-900 font-medium">{userDetails.companyProfile.company_name || 'N/A'}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500 mb-1.5">Registration Number</dt>
<dd className="text-sm text-gray-900 font-medium">{userDetails.companyProfile.registration_number || 'N/A'}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500 mb-1.5">Tax ID</dt>
<dd className="text-sm text-gray-900 font-medium">{userDetails.companyProfile.tax_id || 'N/A'}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500 mb-1.5">
<PhoneIcon className="h-4 w-4 inline mr-1.5" />
Phone
</dt>
<dd className="text-sm text-gray-900 font-medium">{userDetails.companyProfile.phone || 'N/A'}</dd>
</div>
<div className="md:col-span-2">
<dt className="text-sm font-medium text-gray-500 mb-1.5">
<MapPinIcon className="h-4 w-4 inline mr-1.5" />
Address
</dt>
<dd className="text-sm text-gray-900 font-medium">
{userDetails.companyProfile.address || 'N/A'}
{userDetails.companyProfile.city && <>, {userDetails.companyProfile.city}</>}
{userDetails.companyProfile.zip_code && <>, {userDetails.companyProfile.zip_code}</>}
{userDetails.companyProfile.country && <>, {userDetails.companyProfile.country}</>}
</dd>
</div>
</dl>
</div>
</div>
)}
{/* Account Status */}
{userDetails.userStatus && (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<CheckCircleIcon className="h-5 w-5 text-gray-600" />
Registration Progress
</h3>
</div>
<div className="px-6 py-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center gap-3">
{userDetails.userStatus.email_verified === 1 ? (
<CheckCircleIcon className="h-6 w-6 text-green-500" />
) : (
<XCircleIcon className="h-6 w-6 text-gray-300" />
)}
<span className="text-sm font-medium text-gray-700">Email Verified</span>
</div>
<div className="flex items-center gap-3">
{userDetails.userStatus.profile_completed === 1 ? (
<CheckCircleIcon className="h-6 w-6 text-green-500" />
) : (
<XCircleIcon className="h-6 w-6 text-gray-300" />
)}
<span className="text-sm font-medium text-gray-700">Profile Completed</span>
</div>
<div className="flex items-center gap-3">
{userDetails.userStatus.documents_uploaded === 1 ? (
<CheckCircleIcon className="h-6 w-6 text-green-500" />
) : (
<XCircleIcon className="h-6 w-6 text-gray-300" />
)}
<span className="text-sm font-medium text-gray-700">Documents Uploaded</span>
</div>
<div className="flex items-center gap-3">
{userDetails.userStatus.contract_signed === 1 ? (
<CheckCircleIcon className="h-6 w-6 text-green-500" />
) : (
<XCircleIcon className="h-6 w-6 text-gray-300" />
)}
<span className="text-sm font-medium text-gray-700">Contract Signed</span>
</div>
</div>
</div>
</div>
)}
{/* Documents Section */}
{(userDetails.documents.length > 0 || userDetails.contracts.length > 0 || userDetails.idDocuments.length > 0) && (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<DocumentTextIcon className="h-5 w-5 text-gray-600" />
Documents ({userDetails.documents.length + userDetails.contracts.length + userDetails.idDocuments.length})
</h3>
</div>
<div className="px-6 py-5 space-y-4">
{/* Regular Documents */}
{userDetails.documents.length > 0 && (
<div>
<h5 className="text-sm font-medium text-gray-700 mb-3">Uploaded Documents</h5>
<div className="space-y-2">
{userDetails.documents.map((doc) => (
<div key={doc.id} className="flex items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-200">
<div className="flex items-center gap-3">
<DocumentTextIcon className="h-5 w-5 text-gray-400" />
<div>
<div className="text-sm font-medium text-gray-900">{doc.file_name}</div>
<div className="text-xs text-gray-500">{formatFileSize(doc.file_size)}</div>
</div>
</div>
<span className="text-xs text-gray-500">{formatDate(doc.uploaded_at)}</span>
</div>
))}
</div>
</div>
)}
{/* Contracts */}
{userDetails.contracts.length > 0 && (
<div>
<h5 className="text-sm font-medium text-gray-700 mb-3">Contracts</h5>
<div className="space-y-2">
{userDetails.contracts.map((contract) => (
<div key={contract.id} className="flex items-center justify-between bg-blue-50 p-3 rounded-lg border border-blue-200">
<div className="flex items-center gap-3">
<DocumentTextIcon className="h-5 w-5 text-blue-600" />
<div>
<div className="text-sm font-medium text-gray-900">{contract.file_name}</div>
<div className="text-xs text-gray-500">{formatFileSize(contract.file_size)}</div>
</div>
</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-sm font-medium text-gray-700 mb-3">ID Documents</h5>
<div className="space-y-4">
{userDetails.idDocuments.map((idDoc) => (
<div key={idDoc.id} className="bg-purple-50 p-4 rounded-lg border border-purple-200">
<div className="flex items-center gap-2 mb-3">
<IdentificationIcon className="h-5 w-5 text-purple-600" />
<span className="text-sm font-medium text-gray-900">{idDoc.document_type}</span>
<span className="text-xs text-gray-500 ml-auto">{formatDate(idDoc.uploaded_at)}</span>
</div>
{(idDoc.frontUrl || idDoc.backUrl) && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{idDoc.frontUrl && (
<div>
<p className="text-xs font-medium text-gray-600 mb-2">Front</p>
<img
src={idDoc.frontUrl}
alt="ID Front"
className="w-full h-40 object-cover rounded border border-gray-300"
/>
</div>
)}
{idDoc.backUrl && (
<div>
<p className="text-xs font-medium text-gray-600 mb-2">Back</p>
<img
src={idDoc.backUrl}
alt="ID Back"
className="w-full h-40 object-cover rounded border border-gray-300"
/>
</div>
)}
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
{/* Permissions */}
{userDetails.permissions.length > 0 && (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<ShieldCheckIcon className="h-5 w-5 text-gray-600" />
Permissions ({userDetails.permissions.length})
</h3>
</div>
<div className="px-6 py-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{userDetails.permissions.map((perm) => (
<div
key={perm.id}
className={`flex items-center gap-3 p-3 rounded-lg border ${
perm.is_active
? 'bg-green-50 border-green-200'
: 'bg-gray-50 border-gray-200'
}`}
>
{perm.is_active ? (
<CheckCircleIcon className="h-5 w-5 text-green-600 flex-shrink-0" />
) : (
<XCircleIcon className="h-5 w-5 text-gray-400 flex-shrink-0" />
)}
<div>
<div className="text-sm font-medium text-gray-900">{perm.name}</div>
{perm.description && (
<div className="text-xs text-gray-500 mt-0.5">{perm.description}</div>
)}
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex justify-end gap-3 pt-4">
<button
type="button"
onClick={onClose}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-gray-200 px-4 py-2.5 text-sm font-semibold text-gray-900 shadow-sm hover:bg-gray-300 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-gray-500"
>
Close
</button>
</div>
</div>
) : null}
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}