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

856 lines
47 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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'
type ContractFileItem = {
key: string
filename: string
documentId?: number | null
contract_type?: 'contract' | 'gdpr' | string | null
}
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, per contract type)
const [activePreviewTab, setActivePreviewTab] = useState<'contract' | 'gdpr'>('contract')
const [previewState, setPreviewState] = useState({
contract: { loading: false, html: null as string | null, error: null as string | null, warning: null as string | null },
gdpr: { loading: false, html: null as string | null, error: null as string | null, warning: null as string | null },
})
const [contractFiles, setContractFiles] = useState<{ contract: ContractFileItem[]; gdpr: ContractFileItem[] }>({
contract: [],
gdpr: []
})
const [docsLoading, setDocsLoading] = useState(false)
const [moveLoading, setMoveLoading] = useState<Record<string, boolean>>({})
const [selectedFile, setSelectedFile] = useState<{ contract?: string; gdpr?: string }>({})
const missingIdOrContract = !!userDetails?.userStatus && (
userDetails.userStatus.documents_uploaded !== 1 ||
userDetails.userStatus.contract_signed !== 1
)
const storageMissing = !!userDetails?.storageStatus && (
userDetails.storageStatus.idDocumentsPresent === false ||
userDetails.storageStatus.contractPresent === false
)
const canVerifyByStatus = !!(userDetails?.userStatus
&& userDetails.userStatus.email_verified === 1
&& userDetails.userStatus.profile_completed === 1
&& userDetails.userStatus.documents_uploaded === 1
&& userDetails.userStatus.contract_signed === 1)
const canVerify = storageMissing ? (canVerifyByStatus && !storageMissing) : canVerifyByStatus
useEffect(() => {
if (isOpen && userId && token) {
fetchUserDetails()
loadContractFiles()
}
}, [isOpen, userId, token])
useEffect(() => {
if (!isOpen || !userId || !token || !userDetails) return
loadContractFiles()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userDetails])
useEffect(() => {
if (!isOpen) return
setActivePreviewTab('contract')
setPreviewState({
contract: { loading: false, html: null, error: null, warning: null },
gdpr: { loading: false, html: null, error: null, warning: null }
})
setContractFiles({ contract: [], gdpr: [] })
setSelectedFile({})
}, [isOpen, userId])
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 (contractType: 'contract' | 'gdpr', documentId?: number, objectKey?: string) => {
if (!userId || !token || !userDetails) return
setPreviewState((prev) => ({
...prev,
[contractType]: { ...prev[contractType], loading: true, error: null, warning: null }
}))
try {
const result = await AdminAPI.getContractPreviewHtml(token, String(userId), userDetails.user.user_type, contractType, documentId, objectKey)
setPreviewState((prev) => ({
...prev,
[contractType]: { loading: false, html: result.html, error: null, warning: result.warning || null }
}))
} catch (e: any) {
console.error('UserDetailModal.loadContractPreview error:', e)
setPreviewState((prev) => ({
...prev,
[contractType]: { loading: false, html: null, error: e?.message || 'Failed to load contract preview', warning: e?.warning || null }
}))
}
}
const loadContractFiles = async () => {
if (!userId || !token) return
setDocsLoading(true)
try {
const result = await AdminAPI.listContractFiles(token, String(userId), userDetails?.user?.user_type)
const contract = Array.isArray(result?.contract) ? result.contract : []
const gdpr = Array.isArray(result?.gdpr) ? result.gdpr : []
setContractFiles({ contract, gdpr })
setSelectedFile((prev) => ({
contract: prev.contract || contract[0]?.key || undefined,
gdpr: prev.gdpr || gdpr[0]?.key || undefined
}))
} catch (e) {
console.error('UserDetailModal.loadContractFiles error:', e)
setContractFiles({ contract: [], gdpr: [] })
} finally {
setDocsLoading(false)
}
}
const moveContractDoc = async (documentId: number | undefined, targetType: 'contract' | 'gdpr', filename?: string | null, objectKey?: string) => {
if (!userId || !token) return
const label = targetType === 'gdpr' ? 'GDPR' : 'Contract'
const name = filename ? `\n\nFile: ${filename}` : ''
const ok = window.confirm(`Move this document to ${label}?${name}`)
if (!ok) return
const loadingKey = objectKey || String(documentId || '')
setMoveLoading((prev) => ({ ...prev, [loadingKey]: true }))
try {
await AdminAPI.moveContractDocument(token, String(userId), documentId, targetType, objectKey)
await loadContractFiles()
} catch (e) {
console.error('UserDetailModal.moveContractDoc error:', e)
} finally {
setMoveLoading((prev) => ({ ...prev, [loadingKey]: 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 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>
{missingIdOrContract && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 mb-4">
ID documents or a signed contract are missing for this user. The users verification status should be checked.
</div>
)}
{storageMissing && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 mb-4">
ID documents or a signed contract are missing from object storage. The users verification status should be checked.
</div>
)}
{missingIdOrContract && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 mb-4">
ID documents or a signed contract are missing for this user. The users verification status should be checked.
</div>
)}
<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 text-black">
<span className="block truncate font-medium text-black">
{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>
{userDetails?.userStatus && (
<p className="text-xs text-gray-500 mb-2">
{canVerify
? 'All steps completed. You can verify this user.'
: 'User has not yet completed all required steps.'}
</p>
)}
<button
type="button"
onClick={handleToggleAdminVerification}
disabled={saving || !canVerify}
title={!canVerify ? 'Complete all steps and ensure files are present in object storage before admin verification' : undefined}
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">
<div className="flex items-center gap-3">
<DocumentTextIcon className="h-5 w-5 text-gray-600" />
<div className="flex items-center gap-2">
<span className="text-lg font-semibold text-gray-900">Contract Preview</span>
<div className="flex items-center gap-1 rounded-full border border-gray-200 bg-white px-1">
{(['contract','gdpr'] as const).map((tab) => (
<button
key={tab}
type="button"
onClick={() => setActivePreviewTab(tab)}
className={`px-2.5 py-1 text-xs rounded-full transition ${activePreviewTab === tab ? 'bg-indigo-600 text-white shadow' : 'text-gray-700 hover:bg-gray-100'}`}
>
{tab === 'contract' ? 'Contract' : 'GDPR'}
</button>
))}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
const files = contractFiles[activePreviewTab] || []
const selectedKey = selectedFile[activePreviewTab] || files[0]?.key
const item = files.find((f) => f.key === selectedKey) || files[0]
loadContractPreview(activePreviewTab, item?.documentId || undefined, item?.key)
}}
disabled={previewState[activePreviewTab].loading}
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"
>
{previewState[activePreviewTab].loading ? 'Loading…' : 'Preview'}
</button>
<button
type="button"
onClick={() => {
const current = previewState[activePreviewTab];
if (!current?.html) return;
const blob = new Blob([current.html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
window.open(url, '_blank', 'noopener,noreferrer');
}}
disabled={!previewState[activePreviewTab]?.html}
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">
{(() => {
const files = contractFiles[activePreviewTab] || []
const selectedKey = selectedFile[activePreviewTab] || files[0]?.key
const selectedItem = files.find((f) => f.key === selectedKey) || files[0]
const moveTarget = activePreviewTab === 'contract' ? 'gdpr' : 'contract'
const isMoving = selectedItem?.key ? !!moveLoading[selectedItem.key] : false
return (
<div className="mb-4">
<div className="flex items-center justify-between gap-2">
<div className="text-sm font-semibold text-gray-900">Files in {activePreviewTab.toUpperCase()}</div>
<button
type="button"
onClick={() => loadContractFiles()}
disabled={docsLoading}
className="inline-flex items-center justify-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-1.5 text-xs disabled:opacity-60"
>
{docsLoading ? 'Refreshing…' : 'Refresh'}
</button>
</div>
{docsLoading && (
<div className="mt-2 text-xs text-gray-500">Loading files</div>
)}
{!docsLoading && files.length === 0 && (
<div className="mt-2 text-xs text-gray-500">No files found in this folder.</div>
)}
{!docsLoading && files.length > 0 && (
<>
{files.length > 1 && (
<div className="mt-2 flex flex-wrap gap-2">
{files.map((f) => (
<button
key={f.key}
type="button"
onClick={() => {
setSelectedFile((prev) => ({ ...prev, [activePreviewTab]: f.key }))
loadContractPreview(activePreviewTab, f.documentId || undefined, f.key)
}}
className={`px-2.5 py-1 text-xs rounded-md border transition ${selectedKey === f.key ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'}`}
>
{f.filename}
</button>
))}
</div>
)}
{selectedItem && (
<div className="mt-2 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 text-xs text-gray-600">
<div className="truncate">Selected: {selectedItem.filename}</div>
{files.length >= 1 && (
<button
type="button"
onClick={() => moveContractDoc(selectedItem.documentId || undefined, moveTarget as 'contract' | 'gdpr', selectedItem.filename, selectedItem.key)}
disabled={isMoving}
className="inline-flex items-center justify-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-1.5 text-xs disabled:opacity-60"
>
{isMoving ? 'Moving…' : `Move to ${moveTarget.toUpperCase()}`}
</button>
)}
</div>
)}
</>
)}
</div>
)
})()}
{previewState[activePreviewTab].warning && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 mb-4">
{previewState[activePreviewTab].warning}
</div>
)}
{previewState[activePreviewTab].error && (
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 mb-4">
{previewState[activePreviewTab].error}
</div>
)}
{previewState[activePreviewTab].loading && (
<div className="flex items-center justify-center h-40 text-sm text-gray-500">
Loading preview
</div>
)}
{!previewState[activePreviewTab].loading && previewState[activePreviewTab].html && (
<div className="rounded-md border border-gray-200 overflow-hidden">
<iframe
title={`Contract Preview ${activePreviewTab}`}
className="w-full h-[600px] bg-white"
srcDoc={previewState[activePreviewTab].html || ''}
/>
</div>
)}
{!previewState[activePreviewTab].loading && !previewState[activePreviewTab].html && !previewState[activePreviewTab].error && (
<p className="text-sm text-gray-500">Click Preview to render the latest 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>
)}
{/* 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>
)
}