From 5709f48dc3cc08f5b294bb88fba52c95c1647544 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Thu, 23 Oct 2025 21:30:29 +0200 Subject: [PATCH] feat: implement user archiving and unarchiving functionality in admin panel --- src/app/admin/user-management/page.tsx | 38 +- src/app/components/UserDetailModal.tsx | 571 +++++++++++++++++++++---- src/app/shop/public/page.tsx | 4 +- src/app/utils/api.ts | 63 ++- 4 files changed, 570 insertions(+), 106 deletions(-) diff --git a/src/app/admin/user-management/page.tsx b/src/app/admin/user-management/page.tsx index 88ffd91..850676a 100644 --- a/src/app/admin/user-management/page.tsx +++ b/src/app/admin/user-management/page.tsx @@ -5,9 +5,7 @@ import PageLayout from '../../components/PageLayout' import UserDetailModal from '../../components/UserDetailModal' import { MagnifyingGlassIcon, - EyeIcon, PencilSquareIcon, - XMarkIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline' import { useAdminUsers } from '../../hooks/useAdminUsers' @@ -15,7 +13,7 @@ import { AdminAPI } from '../../utils/api' import useAuthStore from '../../store/authStore' type UserType = 'personal' | 'company' -type UserStatus = 'active' | 'pending' | 'disabled' +type UserStatus = 'active' | 'pending' | 'disabled' | 'inactive' type UserRole = 'user' | 'admin' interface User { @@ -32,7 +30,7 @@ interface User { company_name?: string } -const STATUSES: UserStatus[] = ['active','pending','disabled'] +const STATUSES: UserStatus[] = ['active','pending','disabled','inactive'] const TYPES: UserType[] = ['personal','company'] const ROLES: UserRole[] = ['user','admin'] @@ -101,9 +99,10 @@ export default function AdminUserManagementPage() { const fullName = u.user_type === 'company' ? companyName : `${firstName} ${lastName}` // 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 - 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 === 'suspended' ? 'disabled' : 'pending' // default fallback @@ -174,7 +173,7 @@ export default function AdminUserManagementPage() { ] const rows = filtered.map(u => { // Map backend to friendly values - const userStatus: 'active'|'pending'|'disabled' = + const userStatus: UserStatus = u.status === 'inactive' ? 'inactive' : u.is_admin_verified === 1 ? 'active' : u.status === 'pending' ? 'pending' : u.status === 'suspended' ? 'disabled' : 'pending' @@ -221,6 +220,7 @@ export default function AdminUserManagementPage() { const statusBadge = (s: UserStatus) => s==='active' ? badge('Active','green') : s==='pending' ? badge('Pending','amber') + : s==='inactive' ? badge('Inactive','gray') : badge('Disabled','rose') const typeBadge = (t: UserType) => @@ -229,13 +229,11 @@ export default function AdminUserManagementPage() { const roleBadge = (r: UserRole) => r==='admin' ? badge('Admin','indigo') : badge('User','gray') - // Action stubs - const onView = (id: string) => { + // Action handler for opening edit modal + const onEdit = (id: string) => { setSelectedUserId(id) setIsDetailModalOpen(true) } - const onEdit = (id: string) => console.log('Edit', id) - const onDelete = (id: string) => console.log('Delete', id) return ( @@ -385,7 +383,8 @@ export default function AdminUserManagementPage() { : `${u.first_name?.[0] || 'U'}${u.last_name?.[0] || 'U'}`.toUpperCase() // 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 === 'suspended' ? 'disabled' : 'pending' // default fallback @@ -420,23 +419,11 @@ export default function AdminUserManagementPage() {
- -
@@ -486,6 +473,7 @@ export default function AdminUserManagementPage() { setSelectedUserId(null) }} userId={selectedUserId} + onUserUpdated={fetchAllUsers} />
) diff --git a/src/app/components/UserDetailModal.tsx b/src/app/components/UserDetailModal.tsx index b591492..f21ccd8 100644 --- a/src/app/components/UserDetailModal.tsx +++ b/src/app/components/UserDetailModal.tsx @@ -14,7 +14,10 @@ import { BuildingOfficeIcon, IdentificationIcon, CheckCircleIcon, - XCircleIcon + XCircleIcon, + PencilSquareIcon, + TrashIcon, + ExclamationTriangleIcon } from '@heroicons/react/24/outline' import { AdminAPI, DetailedUserInfo } from '../utils/api' import useAuthStore from '../store/authStore' @@ -23,17 +26,26 @@ interface UserDetailModalProps { isOpen: boolean onClose: () => void 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(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(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(null) const token = useAuthStore(state => state.accessToken) useEffect(() => { if (isOpen && userId && token) { fetchUserDetails() + setIsEditing(false) + setShowArchiveConfirm(false) + setEditedProfile(null) } }, [isOpen, userId, token]) @@ -47,6 +59,12 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM const response = await AdminAPI.getDetailedUserInfo(token, userId) if (response.success) { setUserDetails(response) + // Initialize edited profile with current data + if (response.personalProfile) { + setEditedProfile(response.personalProfile) + } else if (response.companyProfile) { + setEditedProfile(response.companyProfile) + } } else { 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) => { return new Date(dateString).toLocaleDateString('de-DE', { year: 'numeric', @@ -110,11 +226,11 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
-
+
- -
+ +
-
+ {/* Scrollable Content Area */} +
- + User Details + {isEditing && ( + + + Edit Mode + + )} {loading && ( @@ -236,70 +359,232 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM

Profile Information

- {userDetails.personalProfile && ( + {isEditing && editedProfile ? ( + // Edit mode - show input fields
-
- Name: - - {userDetails.personalProfile.first_name} {userDetails.personalProfile.last_name} - -
- {userDetails.personalProfile.phone && ( -
- Phone: - {userDetails.personalProfile.phone} -
+ {userDetails.personalProfile && ( + <> +
+ + setEditedProfile({...editedProfile, first_name: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, last_name: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, phone: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, date_of_birth: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, address: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, city: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, zip_code: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, country: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+ )} - {userDetails.personalProfile.date_of_birth && ( -
- Date of Birth: - {formatDate(userDetails.personalProfile.date_of_birth)} -
- )} - {userDetails.personalProfile.address && ( -
- Address: - - {userDetails.personalProfile.address}, {userDetails.personalProfile.postal_code} {userDetails.personalProfile.city}, {userDetails.personalProfile.country} - -
+ + {userDetails.companyProfile && ( + <> +
+ + setEditedProfile({...editedProfile, company_name: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, tax_id: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, registration_number: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, phone: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, address: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, city: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, zip_code: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + setEditedProfile({...editedProfile, country: e.target.value})} + className="w-full rounded border-gray-300 px-2 py-1 text-sm" + /> +
+ )}
- )} + ) : ( + // View mode - show readonly data + <> + {userDetails.personalProfile && ( +
+
+ Name: + + {userDetails.personalProfile.first_name} {userDetails.personalProfile.last_name} + +
+ {userDetails.personalProfile.phone && ( +
+ Phone: + {userDetails.personalProfile.phone} +
+ )} + {userDetails.personalProfile.date_of_birth && ( +
+ Date of Birth: + {formatDate(userDetails.personalProfile.date_of_birth)} +
+ )} + {userDetails.personalProfile.address && ( +
+ Address: + + {userDetails.personalProfile.address}, {userDetails.personalProfile.zip_code} {userDetails.personalProfile.city}, {userDetails.personalProfile.country} + +
+ )} +
+ )} - {userDetails.companyProfile && ( -
-
- Company Name: - {userDetails.companyProfile.company_name} -
- {userDetails.companyProfile.tax_id && ( -
- Tax ID: - {userDetails.companyProfile.tax_id} + {userDetails.companyProfile && ( +
+
+ Company Name: + {userDetails.companyProfile.company_name} +
+ {userDetails.companyProfile.tax_id && ( +
+ Tax ID: + {userDetails.companyProfile.tax_id} +
+ )} + {userDetails.companyProfile.registration_number && ( +
+ Registration Number: + {userDetails.companyProfile.registration_number} +
+ )} + {userDetails.companyProfile.phone && ( +
+ Phone: + {userDetails.companyProfile.phone} +
+ )} + {userDetails.companyProfile.address && ( +
+ Address: + + {userDetails.companyProfile.address}, {userDetails.companyProfile.zip_code} {userDetails.companyProfile.city}, {userDetails.companyProfile.country} + +
+ )}
)} - {userDetails.companyProfile.registration_number && ( -
- Registration Number: - {userDetails.companyProfile.registration_number} -
- )} - {userDetails.companyProfile.phone && ( -
- Phone: - {userDetails.companyProfile.phone} -
- )} - {userDetails.companyProfile.address && ( -
- Address: - - {userDetails.companyProfile.address}, {userDetails.companyProfile.postal_code} {userDetails.companyProfile.city}, {userDetails.companyProfile.country} - -
- )} -
+ )}
)} @@ -415,13 +700,155 @@ export default function UserDetailModal({ isOpen, onClose, userId }: UserDetailM
- + {showArchiveConfirm ? ( + // Archive/Unarchive Confirmation Dialog +
+
+ +

+ {userDetails?.userStatus?.status === 'inactive' ? 'Unarchive User' : 'Archive User'} +

+
+

+ {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.'} +

+
+ + +
+
+ ) : ( + // Normal action buttons +
+ {isEditing ? ( + <> + + + + ) : ( + <> + + + {userDetails?.userStatus && ( + + )} + + + + + + )} +
+ )}
diff --git a/src/app/shop/public/page.tsx b/src/app/shop/public/page.tsx index 554684e..cc1aef9 100644 --- a/src/app/shop/public/page.tsx +++ b/src/app/shop/public/page.tsx @@ -593,7 +593,7 @@ export default function StorePage() { }} disabled={!product.inStock} 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 ? 'bg-[#8D6B1D] text-white hover:bg-[#7A5E1A] focus-visible:outline-[#8D6B1D]' : 'bg-gray-100 text-gray-400 cursor-not-allowed' @@ -689,7 +689,7 @@ export default function StorePage() {