750 lines
41 KiB
TypeScript
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>
|
|
)
|
|
}
|