feat: implement user archiving and unarchiving functionality in admin panel
This commit is contained in:
parent
4e9ae3a826
commit
5709f48dc3
@ -5,9 +5,7 @@ import PageLayout from '../../components/PageLayout'
|
|||||||
import UserDetailModal from '../../components/UserDetailModal'
|
import UserDetailModal from '../../components/UserDetailModal'
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
EyeIcon,
|
|
||||||
PencilSquareIcon,
|
PencilSquareIcon,
|
||||||
XMarkIcon,
|
|
||||||
ExclamationTriangleIcon
|
ExclamationTriangleIcon
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
import { useAdminUsers } from '../../hooks/useAdminUsers'
|
import { useAdminUsers } from '../../hooks/useAdminUsers'
|
||||||
@ -15,7 +13,7 @@ import { AdminAPI } from '../../utils/api'
|
|||||||
import useAuthStore from '../../store/authStore'
|
import useAuthStore from '../../store/authStore'
|
||||||
|
|
||||||
type UserType = 'personal' | 'company'
|
type UserType = 'personal' | 'company'
|
||||||
type UserStatus = 'active' | 'pending' | 'disabled'
|
type UserStatus = 'active' | 'pending' | 'disabled' | 'inactive'
|
||||||
type UserRole = 'user' | 'admin'
|
type UserRole = 'user' | 'admin'
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@ -32,7 +30,7 @@ interface User {
|
|||||||
company_name?: string
|
company_name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUSES: UserStatus[] = ['active','pending','disabled']
|
const STATUSES: UserStatus[] = ['active','pending','disabled','inactive']
|
||||||
const TYPES: UserType[] = ['personal','company']
|
const TYPES: UserType[] = ['personal','company']
|
||||||
const ROLES: UserRole[] = ['user','admin']
|
const ROLES: UserRole[] = ['user','admin']
|
||||||
|
|
||||||
@ -101,9 +99,10 @@ export default function AdminUserManagementPage() {
|
|||||||
const fullName = u.user_type === 'company' ? companyName : `${firstName} ${lastName}`
|
const fullName = u.user_type === 'company' ? companyName : `${firstName} ${lastName}`
|
||||||
|
|
||||||
// Map backend status to frontend status
|
// Map backend status to frontend status
|
||||||
// Backend status can be: 'pending', 'active', 'suspended', etc.
|
// Backend status can be: 'pending', 'active', 'suspended', 'inactive', etc.
|
||||||
// is_admin_verified: 1 = verified by admin, 0 = not verified
|
// is_admin_verified: 1 = verified by admin, 0 = not verified
|
||||||
const userStatus: UserStatus = u.is_admin_verified === 1 ? 'active' :
|
const userStatus: UserStatus = u.status === 'inactive' ? 'inactive' :
|
||||||
|
u.is_admin_verified === 1 ? 'active' :
|
||||||
u.status === 'pending' ? 'pending' :
|
u.status === 'pending' ? 'pending' :
|
||||||
u.status === 'suspended' ? 'disabled' :
|
u.status === 'suspended' ? 'disabled' :
|
||||||
'pending' // default fallback
|
'pending' // default fallback
|
||||||
@ -174,7 +173,7 @@ export default function AdminUserManagementPage() {
|
|||||||
]
|
]
|
||||||
const rows = filtered.map(u => {
|
const rows = filtered.map(u => {
|
||||||
// Map backend to friendly values
|
// Map backend to friendly values
|
||||||
const userStatus: 'active'|'pending'|'disabled' =
|
const userStatus: UserStatus = u.status === 'inactive' ? 'inactive' :
|
||||||
u.is_admin_verified === 1 ? 'active' :
|
u.is_admin_verified === 1 ? 'active' :
|
||||||
u.status === 'pending' ? 'pending' :
|
u.status === 'pending' ? 'pending' :
|
||||||
u.status === 'suspended' ? 'disabled' : 'pending'
|
u.status === 'suspended' ? 'disabled' : 'pending'
|
||||||
@ -221,6 +220,7 @@ export default function AdminUserManagementPage() {
|
|||||||
const statusBadge = (s: UserStatus) =>
|
const statusBadge = (s: UserStatus) =>
|
||||||
s==='active' ? badge('Active','green')
|
s==='active' ? badge('Active','green')
|
||||||
: s==='pending' ? badge('Pending','amber')
|
: s==='pending' ? badge('Pending','amber')
|
||||||
|
: s==='inactive' ? badge('Inactive','gray')
|
||||||
: badge('Disabled','rose')
|
: badge('Disabled','rose')
|
||||||
|
|
||||||
const typeBadge = (t: UserType) =>
|
const typeBadge = (t: UserType) =>
|
||||||
@ -229,13 +229,11 @@ export default function AdminUserManagementPage() {
|
|||||||
const roleBadge = (r: UserRole) =>
|
const roleBadge = (r: UserRole) =>
|
||||||
r==='admin' ? badge('Admin','indigo') : badge('User','gray')
|
r==='admin' ? badge('Admin','indigo') : badge('User','gray')
|
||||||
|
|
||||||
// Action stubs
|
// Action handler for opening edit modal
|
||||||
const onView = (id: string) => {
|
const onEdit = (id: string) => {
|
||||||
setSelectedUserId(id)
|
setSelectedUserId(id)
|
||||||
setIsDetailModalOpen(true)
|
setIsDetailModalOpen(true)
|
||||||
}
|
}
|
||||||
const onEdit = (id: string) => console.log('Edit', id)
|
|
||||||
const onDelete = (id: string) => console.log('Delete', id)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
@ -385,7 +383,8 @@ export default function AdminUserManagementPage() {
|
|||||||
: `${u.first_name?.[0] || 'U'}${u.last_name?.[0] || 'U'}`.toUpperCase()
|
: `${u.first_name?.[0] || 'U'}${u.last_name?.[0] || 'U'}`.toUpperCase()
|
||||||
|
|
||||||
// Map backend status to frontend status for display
|
// Map backend status to frontend status for display
|
||||||
const userStatus: UserStatus = u.is_admin_verified === 1 ? 'active' :
|
const userStatus: UserStatus = u.status === 'inactive' ? 'inactive' :
|
||||||
|
u.is_admin_verified === 1 ? 'active' :
|
||||||
u.status === 'pending' ? 'pending' :
|
u.status === 'pending' ? 'pending' :
|
||||||
u.status === 'suspended' ? 'disabled' :
|
u.status === 'suspended' ? 'disabled' :
|
||||||
'pending' // default fallback
|
'pending' // default fallback
|
||||||
@ -420,23 +419,11 @@ export default function AdminUserManagementPage() {
|
|||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => onView(u.id.toString())}
|
onClick={() => onEdit(u.id.toString())}
|
||||||
className="inline-flex items-center gap-1 rounded-md border border-blue-200 bg-blue-50 hover:bg-blue-100 text-blue-700 px-2.5 py-1 text-xs font-medium transition"
|
className="inline-flex items-center gap-1 rounded-md border border-blue-200 bg-blue-50 hover:bg-blue-100 text-blue-700 px-2.5 py-1 text-xs font-medium transition"
|
||||||
>
|
|
||||||
<EyeIcon className="h-4 w-4" /> View
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => onEdit(u.id.toString())}
|
|
||||||
className="inline-flex items-center gap-1 rounded-md border border-amber-200 bg-amber-50 hover:bg-amber-100 text-amber-700 px-2.5 py-1 text-xs font-medium transition"
|
|
||||||
>
|
>
|
||||||
<PencilSquareIcon className="h-4 w-4" /> Edit
|
<PencilSquareIcon className="h-4 w-4" /> Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => onDelete(u.id.toString())}
|
|
||||||
className="inline-flex items-center gap-1 rounded-md border border-red-200 bg-red-50 hover:bg-red-100 text-red-600 px-2.5 py-1 text-xs font-medium transition"
|
|
||||||
>
|
|
||||||
<XMarkIcon className="h-4 w-4" /> Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -486,6 +473,7 @@ export default function AdminUserManagementPage() {
|
|||||||
setSelectedUserId(null)
|
setSelectedUserId(null)
|
||||||
}}
|
}}
|
||||||
userId={selectedUserId}
|
userId={selectedUserId}
|
||||||
|
onUserUpdated={fetchAllUsers}
|
||||||
/>
|
/>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -14,7 +14,10 @@ import {
|
|||||||
BuildingOfficeIcon,
|
BuildingOfficeIcon,
|
||||||
IdentificationIcon,
|
IdentificationIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
XCircleIcon
|
XCircleIcon,
|
||||||
|
PencilSquareIcon,
|
||||||
|
TrashIcon,
|
||||||
|
ExclamationTriangleIcon
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
import { AdminAPI, DetailedUserInfo } from '../utils/api'
|
import { AdminAPI, DetailedUserInfo } from '../utils/api'
|
||||||
import useAuthStore from '../store/authStore'
|
import useAuthStore from '../store/authStore'
|
||||||
@ -23,17 +26,26 @@ interface UserDetailModalProps {
|
|||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
userId: string | null
|
userId: string | null
|
||||||
|
onUserUpdated?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailModalProps) {
|
export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated }: UserDetailModalProps) {
|
||||||
const [userDetails, setUserDetails] = useState<DetailedUserInfo | null>(null)
|
const [userDetails, setUserDetails] = useState<DetailedUserInfo | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [archiving, setArchiving] = useState(false)
|
||||||
|
const [showArchiveConfirm, setShowArchiveConfirm] = useState(false)
|
||||||
|
const [editedProfile, setEditedProfile] = useState<any>(null)
|
||||||
const token = useAuthStore(state => state.accessToken)
|
const token = useAuthStore(state => state.accessToken)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && userId && token) {
|
if (isOpen && userId && token) {
|
||||||
fetchUserDetails()
|
fetchUserDetails()
|
||||||
|
setIsEditing(false)
|
||||||
|
setShowArchiveConfirm(false)
|
||||||
|
setEditedProfile(null)
|
||||||
}
|
}
|
||||||
}, [isOpen, userId, token])
|
}, [isOpen, userId, token])
|
||||||
|
|
||||||
@ -47,6 +59,12 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM
|
|||||||
const response = await AdminAPI.getDetailedUserInfo(token, userId)
|
const response = await AdminAPI.getDetailedUserInfo(token, userId)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setUserDetails(response)
|
setUserDetails(response)
|
||||||
|
// Initialize edited profile with current data
|
||||||
|
if (response.personalProfile) {
|
||||||
|
setEditedProfile(response.personalProfile)
|
||||||
|
} else if (response.companyProfile) {
|
||||||
|
setEditedProfile(response.companyProfile)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.message || 'Failed to fetch user details')
|
throw new Error(response.message || 'Failed to fetch user details')
|
||||||
}
|
}
|
||||||
@ -59,6 +77,104 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleArchiveUser = async () => {
|
||||||
|
if (!userId || !token) return
|
||||||
|
|
||||||
|
setArchiving(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isCurrentlyInactive = userDetails?.userStatus?.status === 'inactive'
|
||||||
|
|
||||||
|
if (isCurrentlyInactive) {
|
||||||
|
// Unarchive user
|
||||||
|
const response = await AdminAPI.unarchiveUser(token, userId)
|
||||||
|
if (response.success) {
|
||||||
|
onClose()
|
||||||
|
if (onUserUpdated) {
|
||||||
|
onUserUpdated()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Failed to unarchive user')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Archive user
|
||||||
|
const response = await AdminAPI.archiveUser(token, userId)
|
||||||
|
if (response.success) {
|
||||||
|
onClose()
|
||||||
|
if (onUserUpdated) {
|
||||||
|
onUserUpdated()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Failed to archive user')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to archive/unarchive user'
|
||||||
|
setError(errorMessage)
|
||||||
|
console.error('UserDetailModal.handleArchiveUser error:', err)
|
||||||
|
} finally {
|
||||||
|
setArchiving(false)
|
||||||
|
setShowArchiveConfirm(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveProfile = async () => {
|
||||||
|
if (!userId || !token || !editedProfile || !userDetails) return
|
||||||
|
|
||||||
|
setSaving(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userType = userDetails.user.user_type
|
||||||
|
const response = await AdminAPI.updateUserProfile(token, userId, editedProfile, userType)
|
||||||
|
if (response.success) {
|
||||||
|
// Refresh user details
|
||||||
|
await fetchUserDetails()
|
||||||
|
setIsEditing(false)
|
||||||
|
if (onUserUpdated) {
|
||||||
|
onUserUpdated()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Failed to update user profile')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to update user profile'
|
||||||
|
setError(errorMessage)
|
||||||
|
console.error('UserDetailModal.handleSaveProfile error:', err)
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleAdminVerification = async () => {
|
||||||
|
if (!userId || !token || !userDetails) return
|
||||||
|
|
||||||
|
setSaving(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newVerificationStatus = userDetails.userStatus?.is_admin_verified === 1 ? 0 : 1
|
||||||
|
// Note: You'll need to implement this API method
|
||||||
|
const response = await AdminAPI.updateUserVerification(token, userId, newVerificationStatus)
|
||||||
|
if (response.success) {
|
||||||
|
// Refresh user details
|
||||||
|
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 formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@ -110,11 +226,11 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-6">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
@ -124,8 +240,8 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl sm:p-6">
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white shadow-xl transition-all w-full max-w-4xl max-h-[85vh] flex flex-col">
|
||||||
<div className="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
|
<div className="absolute right-0 top-0 z-10 pr-4 pt-4">
|
||||||
<button
|
<button
|
||||||
type="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"
|
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"
|
||||||
@ -136,10 +252,17 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:flex sm:items-start">
|
{/* Scrollable Content Area */}
|
||||||
|
<div className="overflow-y-auto px-4 pb-4 pt-5 sm:p-6">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Dialog.Title as="h3" className="text-lg font-semibold leading-6 text-gray-900 mb-6">
|
<Dialog.Title as="h3" className="text-lg font-semibold leading-6 text-gray-900 mb-6 flex items-center gap-2 pr-8">
|
||||||
User Details
|
User Details
|
||||||
|
{isEditing && (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">
|
||||||
|
<PencilSquareIcon className="h-3 w-3" />
|
||||||
|
Edit Mode
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
@ -236,70 +359,232 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM
|
|||||||
<h4 className="text-sm font-medium text-gray-900">Profile Information</h4>
|
<h4 className="text-sm font-medium text-gray-900">Profile Information</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{userDetails.personalProfile && (
|
{isEditing && editedProfile ? (
|
||||||
|
// Edit mode - show input fields
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
{userDetails.personalProfile && (
|
||||||
<span className="font-medium text-gray-700">Name:</span>
|
<>
|
||||||
<span className="ml-2 text-gray-600">
|
<div>
|
||||||
{userDetails.personalProfile.first_name} {userDetails.personalProfile.last_name}
|
<label className="block font-medium text-gray-700 mb-1">First Name</label>
|
||||||
</span>
|
<input
|
||||||
</div>
|
type="text"
|
||||||
{userDetails.personalProfile.phone && (
|
value={editedProfile.first_name || ''}
|
||||||
<div>
|
onChange={(e) => setEditedProfile({...editedProfile, first_name: e.target.value})}
|
||||||
<span className="font-medium text-gray-700">Phone:</span>
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
<span className="ml-2 text-gray-600">{userDetails.personalProfile.phone}</span>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Last Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.last_name || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, last_name: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Phone</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.phone || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, phone: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Date of Birth</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={editedProfile.date_of_birth || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, date_of_birth: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Address</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.address || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, address: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">City</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.city || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, city: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Postal Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.zip_code || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, zip_code: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Country</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.country || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, country: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{userDetails.personalProfile.date_of_birth && (
|
|
||||||
<div>
|
|
||||||
<span className="font-medium text-gray-700">Date of Birth:</span>
|
|
||||||
<span className="ml-2 text-gray-600">{formatDate(userDetails.personalProfile.date_of_birth)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{userDetails.personalProfile.address && (
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<span className="font-medium text-gray-700">Address:</span>
|
|
||||||
<span className="ml-2 text-gray-600">
|
|
||||||
{userDetails.personalProfile.address}, {userDetails.personalProfile.postal_code} {userDetails.personalProfile.city}, {userDetails.personalProfile.country}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{userDetails.companyProfile && (
|
{userDetails.companyProfile && (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
<>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-700">Company Name:</span>
|
<label className="block font-medium text-gray-700 mb-1">Company Name</label>
|
||||||
<span className="ml-2 text-gray-600">{userDetails.companyProfile.company_name}</span>
|
<input
|
||||||
</div>
|
type="text"
|
||||||
{userDetails.companyProfile.tax_id && (
|
value={editedProfile.company_name || ''}
|
||||||
<div>
|
onChange={(e) => setEditedProfile({...editedProfile, company_name: e.target.value})}
|
||||||
<span className="font-medium text-gray-700">Tax ID:</span>
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
<span className="ml-2 text-gray-600">{userDetails.companyProfile.tax_id}</span>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div>
|
||||||
{userDetails.companyProfile.registration_number && (
|
<label className="block font-medium text-gray-700 mb-1">Tax ID</label>
|
||||||
<div>
|
<input
|
||||||
<span className="font-medium text-gray-700">Registration Number:</span>
|
type="text"
|
||||||
<span className="ml-2 text-gray-600">{userDetails.companyProfile.registration_number}</span>
|
value={editedProfile.tax_id || ''}
|
||||||
</div>
|
onChange={(e) => setEditedProfile({...editedProfile, tax_id: e.target.value})}
|
||||||
)}
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
{userDetails.companyProfile.phone && (
|
/>
|
||||||
<div>
|
</div>
|
||||||
<span className="font-medium text-gray-700">Phone:</span>
|
<div>
|
||||||
<span className="ml-2 text-gray-600">{userDetails.companyProfile.phone}</span>
|
<label className="block font-medium text-gray-700 mb-1">Registration Number</label>
|
||||||
</div>
|
<input
|
||||||
)}
|
type="text"
|
||||||
{userDetails.companyProfile.address && (
|
value={editedProfile.registration_number || ''}
|
||||||
<div className="sm:col-span-2">
|
onChange={(e) => setEditedProfile({...editedProfile, registration_number: e.target.value})}
|
||||||
<span className="font-medium text-gray-700">Address:</span>
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
<span className="ml-2 text-gray-600">
|
/>
|
||||||
{userDetails.companyProfile.address}, {userDetails.companyProfile.postal_code} {userDetails.companyProfile.city}, {userDetails.companyProfile.country}
|
</div>
|
||||||
</span>
|
<div>
|
||||||
</div>
|
<label className="block font-medium text-gray-700 mb-1">Phone</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.phone || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, phone: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Address</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.address || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, address: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">City</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.city || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, city: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Postal Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.zip_code || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, zip_code: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Country</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedProfile.country || ''}
|
||||||
|
onChange={(e) => setEditedProfile({...editedProfile, country: e.target.value})}
|
||||||
|
className="w-full rounded border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
// View mode - show readonly data
|
||||||
|
<>
|
||||||
|
{userDetails.personalProfile && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Name:</span>
|
||||||
|
<span className="ml-2 text-gray-600">
|
||||||
|
{userDetails.personalProfile.first_name} {userDetails.personalProfile.last_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{userDetails.personalProfile.phone && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Phone:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.personalProfile.phone}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{userDetails.personalProfile.date_of_birth && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Date of Birth:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{formatDate(userDetails.personalProfile.date_of_birth)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{userDetails.personalProfile.address && (
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<span className="font-medium text-gray-700">Address:</span>
|
||||||
|
<span className="ml-2 text-gray-600">
|
||||||
|
{userDetails.personalProfile.address}, {userDetails.personalProfile.zip_code} {userDetails.personalProfile.city}, {userDetails.personalProfile.country}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{userDetails.companyProfile && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Company Name:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.companyProfile.company_name}</span>
|
||||||
|
</div>
|
||||||
|
{userDetails.companyProfile.tax_id && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Tax ID:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.companyProfile.tax_id}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{userDetails.companyProfile.registration_number && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Registration Number:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.companyProfile.registration_number}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{userDetails.companyProfile.phone && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Phone:</span>
|
||||||
|
<span className="ml-2 text-gray-600">{userDetails.companyProfile.phone}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{userDetails.companyProfile.address && (
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<span className="font-medium text-gray-700">Address:</span>
|
||||||
|
<span className="ml-2 text-gray-600">
|
||||||
|
{userDetails.companyProfile.address}, {userDetails.companyProfile.zip_code} {userDetails.companyProfile.city}, {userDetails.companyProfile.country}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -415,13 +700,155 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<button
|
{showArchiveConfirm ? (
|
||||||
type="button"
|
// Archive/Unarchive Confirmation Dialog
|
||||||
className="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
<div className={`${userDetails?.userStatus?.status === 'inactive' ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'} border rounded-lg p-4 mb-4`}>
|
||||||
onClick={onClose}
|
<div className="flex items-center gap-3 mb-3">
|
||||||
>
|
<ExclamationTriangleIcon className={`h-5 w-5 ${userDetails?.userStatus?.status === 'inactive' ? 'text-green-600' : 'text-red-600'}`} />
|
||||||
Close
|
<h4 className={`text-sm font-medium ${userDetails?.userStatus?.status === 'inactive' ? 'text-green-900' : 'text-red-900'}`}>
|
||||||
</button>
|
{userDetails?.userStatus?.status === 'inactive' ? 'Unarchive User' : 'Archive User'}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<p className={`text-sm ${userDetails?.userStatus?.status === 'inactive' ? 'text-green-700' : 'text-red-700'} mb-4`}>
|
||||||
|
{userDetails?.userStatus?.status === 'inactive'
|
||||||
|
? 'Are you sure you want to unarchive this user? This will reactivate their account.'
|
||||||
|
: 'Are you sure you want to archive this user? This action will disable their account but preserve all their data.'}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleArchiveUser}
|
||||||
|
disabled={archiving}
|
||||||
|
className={`inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||||
|
userDetails?.userStatus?.status === 'inactive'
|
||||||
|
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
|
||||||
|
: 'bg-red-600 hover:bg-red-500 focus-visible:outline-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{archiving ? (
|
||||||
|
<>
|
||||||
|
<div className="h-4 w-4 border-2 border-white border-b-transparent rounded-full animate-spin" />
|
||||||
|
{userDetails?.userStatus?.status === 'inactive' ? 'Unarchiving...' : 'Archiving...'}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
{userDetails?.userStatus?.status === 'inactive' ? 'Unarchive User' : 'Archive User'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowArchiveConfirm(false)}
|
||||||
|
disabled={archiving}
|
||||||
|
className="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Normal action buttons
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSaveProfile}
|
||||||
|
disabled={saving}
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-green-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<div className="h-4 w-4 border-2 border-white border-b-transparent rounded-full animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircleIcon className="h-4 w-4" />
|
||||||
|
Save Changes
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(false)
|
||||||
|
// Reset edited profile to original
|
||||||
|
if (userDetails?.personalProfile) {
|
||||||
|
setEditedProfile(userDetails.personalProfile)
|
||||||
|
} else if (userDetails?.companyProfile) {
|
||||||
|
setEditedProfile(userDetails.companyProfile)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={saving}
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-md bg-gray-200 px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<XCircleIcon className="h-4 w-4" />
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
disabled={saving}
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<PencilSquareIcon className="h-4 w-4" />
|
||||||
|
Edit Profile
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{userDetails?.userStatus && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleToggleAdminVerification}
|
||||||
|
disabled={saving}
|
||||||
|
className={`inline-flex items-center justify-center gap-2 rounded-md px-3 py-2 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowArchiveConfirm(true)}
|
||||||
|
className={`inline-flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-offset-2 ${
|
||||||
|
userDetails?.userStatus?.status === 'inactive'
|
||||||
|
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
|
||||||
|
: 'bg-red-600 hover:bg-red-500 focus-visible:outline-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
{userDetails?.userStatus?.status === 'inactive' ? 'Unarchive User' : 'Archive User'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-md bg-gray-200 px-3 py-2 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>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|||||||
@ -593,7 +593,7 @@ export default function StorePage() {
|
|||||||
}}
|
}}
|
||||||
disabled={!product.inStock}
|
disabled={!product.inStock}
|
||||||
className={`
|
className={`
|
||||||
inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-200
|
inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-offset-2 transition-colors duration-200
|
||||||
${product.inStock
|
${product.inStock
|
||||||
? 'bg-[#8D6B1D] text-white hover:bg-[#7A5E1A] focus-visible:outline-[#8D6B1D]'
|
? 'bg-[#8D6B1D] text-white hover:bg-[#7A5E1A] focus-visible:outline-[#8D6B1D]'
|
||||||
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
@ -689,7 +689,7 @@ export default function StorePage() {
|
|||||||
<button
|
<button
|
||||||
disabled={!product.inStock}
|
disabled={!product.inStock}
|
||||||
className={`
|
className={`
|
||||||
inline-flex items-center rounded-md px-4 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2
|
inline-flex items-center rounded-md px-4 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-offset-2
|
||||||
${product.inStock
|
${product.inStock
|
||||||
? 'bg-[#8D6B1D] text-white hover:bg-[#7A5E1A] focus-visible:outline-[#8D6B1D]'
|
? 'bg-[#8D6B1D] text-white hover:bg-[#7A5E1A] focus-visible:outline-[#8D6B1D]'
|
||||||
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
|
|||||||
@ -41,6 +41,10 @@ export const API_ENDPOINTS = {
|
|||||||
ADMIN_VERIFICATION_PENDING: '/api/admin/verification-pending-users',
|
ADMIN_VERIFICATION_PENDING: '/api/admin/verification-pending-users',
|
||||||
ADMIN_UNVERIFIED_USERS: '/api/admin/unverified-users',
|
ADMIN_UNVERIFIED_USERS: '/api/admin/unverified-users',
|
||||||
ADMIN_VERIFY_USER: '/api/admin/verify-user/:id',
|
ADMIN_VERIFY_USER: '/api/admin/verify-user/:id',
|
||||||
|
ADMIN_ARCHIVE_USER: '/api/admin/archive-user/:id',
|
||||||
|
ADMIN_UNARCHIVE_USER: '/api/admin/unarchive-user/:id',
|
||||||
|
ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id',
|
||||||
|
ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id',
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Helper Functions
|
// API Helper Functions
|
||||||
@ -237,7 +241,8 @@ export class AdminAPI {
|
|||||||
static async getUserStats(token: string) {
|
static async getUserStats(token: string) {
|
||||||
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_USER_STATS, token)
|
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_USER_STATS, token)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch user stats')
|
const error = await response.json().catch(() => ({ message: 'Failed to fetch user stats' }))
|
||||||
|
throw new Error(error.message || 'Failed to fetch user stats')
|
||||||
}
|
}
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
@ -245,7 +250,8 @@ export class AdminAPI {
|
|||||||
static async getUserList(token: string) {
|
static async getUserList(token: string) {
|
||||||
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_USER_LIST, token)
|
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_USER_LIST, token)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch user list')
|
const error = await response.json().catch(() => ({ message: 'Failed to fetch user list' }))
|
||||||
|
throw new Error(error.message || 'Failed to fetch user list')
|
||||||
}
|
}
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
@ -253,7 +259,8 @@ export class AdminAPI {
|
|||||||
static async getVerificationPendingUsers(token: string) {
|
static async getVerificationPendingUsers(token: string) {
|
||||||
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_VERIFICATION_PENDING, token)
|
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_VERIFICATION_PENDING, token)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch pending users')
|
const error = await response.json().catch(() => ({ message: 'Failed to fetch pending users' }))
|
||||||
|
throw new Error(error.message || 'Failed to fetch pending users')
|
||||||
}
|
}
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
@ -261,7 +268,8 @@ export class AdminAPI {
|
|||||||
static async getUnverifiedUsers(token: string) {
|
static async getUnverifiedUsers(token: string) {
|
||||||
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_UNVERIFIED_USERS, token)
|
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_UNVERIFIED_USERS, token)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch unverified users')
|
const error = await response.json().catch(() => ({ message: 'Failed to fetch unverified users' }))
|
||||||
|
throw new Error(error.message || 'Failed to fetch unverified users')
|
||||||
}
|
}
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
@ -270,7 +278,8 @@ export class AdminAPI {
|
|||||||
const endpoint = API_ENDPOINTS.ADMIN_USER_DETAILED.replace(':id', userId)
|
const endpoint = API_ENDPOINTS.ADMIN_USER_DETAILED.replace(':id', userId)
|
||||||
const response = await ApiClient.get(endpoint, token)
|
const response = await ApiClient.get(endpoint, token)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch detailed user info')
|
const error = await response.json().catch(() => ({ message: 'Failed to fetch detailed user info' }))
|
||||||
|
throw new Error(error.message || 'Failed to fetch detailed user info')
|
||||||
}
|
}
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
@ -284,6 +293,46 @@ export class AdminAPI {
|
|||||||
}
|
}
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async archiveUser(token: string, userId: string) {
|
||||||
|
const endpoint = API_ENDPOINTS.ADMIN_ARCHIVE_USER.replace(':id', userId)
|
||||||
|
const response = await ApiClient.patch(endpoint, { status: 'inactive' }, token)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.message || 'Failed to archive user')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async unarchiveUser(token: string, userId: string) {
|
||||||
|
const endpoint = API_ENDPOINTS.ADMIN_UNARCHIVE_USER.replace(':id', userId)
|
||||||
|
const response = await ApiClient.patch(endpoint, { status: 'active' }, token)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.message || 'Failed to unarchive user')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateUserVerification(token: string, userId: string, isVerified: number) {
|
||||||
|
const endpoint = API_ENDPOINTS.ADMIN_UPDATE_USER_VERIFICATION.replace(':id', userId)
|
||||||
|
const response = await ApiClient.patch(endpoint, { is_admin_verified: isVerified }, token)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.message || 'Failed to update user verification')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateUserProfile(token: string, userId: string, profileData: any, userType: string) {
|
||||||
|
const endpoint = API_ENDPOINTS.ADMIN_UPDATE_USER_PROFILE.replace(':id', userId)
|
||||||
|
const response = await ApiClient.patch(endpoint, { profileData, userType }, token)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.message || 'Failed to update user profile')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response Types
|
// Response Types
|
||||||
@ -338,7 +387,7 @@ export interface DetailedUserInfo {
|
|||||||
phone?: string
|
phone?: string
|
||||||
address?: string
|
address?: string
|
||||||
city?: string
|
city?: string
|
||||||
postal_code?: string
|
zip_code?: string
|
||||||
country?: string
|
country?: string
|
||||||
}
|
}
|
||||||
companyProfile?: {
|
companyProfile?: {
|
||||||
@ -349,7 +398,7 @@ export interface DetailedUserInfo {
|
|||||||
phone?: string
|
phone?: string
|
||||||
address?: string
|
address?: string
|
||||||
city?: string
|
city?: string
|
||||||
postal_code?: string
|
zip_code?: string
|
||||||
country?: string
|
country?: string
|
||||||
}
|
}
|
||||||
userStatus?: {
|
userStatus?: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user