diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..db99cb0 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/index.html b/public/index.html index 4f1c266..a2e5635 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + @@ -249,84 +257,114 @@ function App() { {/* Reseller Dashboard Routes (Separate Service) */} + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Admin Routes */} + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + diff --git a/src/components/Layout/AdminLayout.tsx b/src/components/Layout/AdminLayout.tsx new file mode 100644 index 0000000..a88323f --- /dev/null +++ b/src/components/Layout/AdminLayout.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import AdminSidebar from './AdminSidebar'; + +interface AdminLayoutProps { + children: React.ReactNode; +} + +const AdminLayout: React.FC = ({ children }) => { + return ( +
+
+ {/* Sidebar */} +
+ +
+ + {/* Main Content */} +
+ {/* Top Navigation Bar */} +
+
+
+
+
+ + + +
+
+

+ Admin Panel +

+

+ System Administration & Management +

+
+
+ +
+ {/* Notifications */} + + + {/* User Menu */} +
+
+ A +
+
+

Admin User

+

System Administrator

+
+
+
+
+
+
+ + {/* Page Content */} +
+ {children} +
+
+
+
+ ); +}; + +export default AdminLayout; \ No newline at end of file diff --git a/src/components/Layout/AdminSidebar.tsx b/src/components/Layout/AdminSidebar.tsx new file mode 100644 index 0000000..4193e31 --- /dev/null +++ b/src/components/Layout/AdminSidebar.tsx @@ -0,0 +1,137 @@ +import React, { useState } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { useAppSelector, useAppDispatch } from '../../store/hooks'; +import { toggleTheme } from '../../store/slices/themeSlice'; +import { + Home, Users, Building, Clock, Settings, Menu, X, Sun, Moon, LogOut, + Shield, BarChart3, FileText, UserCheck, UserX, TrendingUp, Activity +} from 'lucide-react'; +import { logout } from '../../store/slices/authSlice'; +import { cn } from '../../utils/cn'; + +const adminNavigation = [ + { name: 'Dashboard', href: '/admin', icon: Home }, + { name: 'Vendor Requests', href: '/admin/vendor-requests', icon: Clock }, + { name: 'Channel Partners', href: '/admin/channel-partners', icon: Building }, + { name: 'System Users', href: '/admin/users', icon: Users }, + { name: 'Analytics', href: '/admin/analytics', icon: BarChart3 }, + { name: 'Reports', href: '/admin/reports', icon: FileText }, + { name: 'Settings', href: '/admin/settings', icon: Settings }, +]; + +const AdminSidebar: React.FC = () => { + const [isCollapsed, setIsCollapsed] = useState(false); + const location = useLocation(); + const dispatch = useAppDispatch(); + const { theme } = useAppSelector((state) => state.theme); + + const handleLogout = () => { + dispatch(logout()); + }; + + return ( +
+ {/* Header */} +
+ {!isCollapsed && ( +
+
+ +
+ + Admin + +
+ )} + +
+ + {/* Navigation */} + + + {/* User Profile & Actions */} +
+ {/* Theme Toggle */} + + + {/* Logout */} + +
+
+ ); +}; + +export default AdminSidebar; \ No newline at end of file diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 247aa42..5738c1e 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -64,13 +64,13 @@ const Login: React.FC = () => { if (user) { // Check if user has roles property and it's an array if (user.roles && Array.isArray(user.roles)) { - const hasResellerRole = user.roles.some(role => role.name === 'reseller'); + const hasResellerRole = user.roles.some(role => role.name === 'reseller_admin'); if (hasResellerRole) { redirectPath = '/reseller-dashboard'; } } else if (user.role) { // Fallback: check the simple role property - if (user.role === 'reseller') { + if (user.role === 'reseller_admin') { redirectPath = '/reseller-dashboard'; } } diff --git a/src/pages/Signup.tsx b/src/pages/Signup.tsx index f01ab40..ed6d27b 100644 --- a/src/pages/Signup.tsx +++ b/src/pages/Signup.tsx @@ -98,7 +98,8 @@ const Signup: React.FC = () => { password: formData.password, phone: formData.phone, company: formData.company, - role: 'vendor' + role: 'channel_partner_admin', + userType: 'channel_partner' })).unwrap(); // Navigate to login page with success message diff --git a/src/pages/Unauthorized.tsx b/src/pages/Unauthorized.tsx index d176164..35bfcdc 100644 --- a/src/pages/Unauthorized.tsx +++ b/src/pages/Unauthorized.tsx @@ -15,12 +15,12 @@ const Unauthorized: React.FC = () => { // Check if user has roles property and it's an array if (user.roles && Array.isArray(user.roles)) { - hasResellerRole = user.roles.some(role => role.name === 'reseller'); - hasVendorRole = user.roles.some(role => role.name === 'vendor'); + hasResellerRole = user.roles.some(role => role.name === 'reseller_admin'); + hasVendorRole = user.roles.some(role => role.name === 'channel_partner_admin'); } else if (user.role) { // Fallback: check the simple role property - hasResellerRole = user.role === 'reseller'; - hasVendorRole = user.role === 'vendor'; + hasResellerRole = user.role === 'reseller_admin'; + hasVendorRole = user.role === 'channel_partner_admin'; } else { console.warn('User roles not properly loaded:', user); return '/login'; diff --git a/src/pages/admin/ChannelPartners.tsx b/src/pages/admin/ChannelPartners.tsx new file mode 100644 index 0000000..82398d8 --- /dev/null +++ b/src/pages/admin/ChannelPartners.tsx @@ -0,0 +1,434 @@ +import React, { useState, useEffect } from 'react'; +import { + Building, + Users, + Plus, + Edit, + Trash2, + Eye, + Search, + Filter, + MapPin, + Mail, + Phone, + Globe, + TrendingUp, + Calendar +} from 'lucide-react'; + +interface ChannelPartner { + id: string; + companyName: string; + companyType: string; + contactEmail: string; + contactPhone: string; + website: string; + tier: string; + status: string; + commissionRate: number; + territory: string; + specializations: string[]; + createdAt: string; + approvedAt?: string; +} + +const ChannelPartners: React.FC = () => { + const [partners, setPartners] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'pending' | 'suspended'>('all'); + const [selectedPartner, setSelectedPartner] = useState(null); + const [showModal, setShowModal] = useState(false); + const [editingPartner, setEditingPartner] = useState(null); + + useEffect(() => { + fetchChannelPartners(); + }, []); + + const fetchChannelPartners = async () => { + try { + const response = await fetch('/api/admin/channel-partners', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + const data = await response.json(); + + if (data.success) { + setPartners(data.data); + } + } catch (error) { + console.error('Error fetching channel partners:', error); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (partnerId: string) => { + if (window.confirm('Are you sure you want to delete this channel partner?')) { + try { + const response = await fetch(`/api/admin/channel-partners/${partnerId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + + if (response.ok) { + fetchChannelPartners(); + } + } catch (error) { + console.error('Error deleting channel partner:', error); + } + } + }; + + const handleUpdate = async (partnerId: string, updateData: Partial) => { + try { + const response = await fetch(`/api/admin/channel-partners/${partnerId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify(updateData) + }); + + if (response.ok) { + fetchChannelPartners(); + setEditingPartner(null); + } + } catch (error) { + console.error('Error updating channel partner:', error); + } + }; + + const filteredPartners = partners.filter(partner => { + const matchesSearch = partner.companyName.toLowerCase().includes(searchTerm.toLowerCase()) || + partner.contactEmail.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesStatus = statusFilter === 'all' || partner.status === statusFilter; + + return matchesSearch && matchesStatus; + }); + + const getStatusColor = (status: string) => { + switch (status) { + case 'active': return 'text-green-600 bg-green-100 dark:text-green-400 dark:bg-green-900'; + case 'pending': return 'text-amber-600 bg-amber-100 dark:text-amber-400 dark:bg-amber-900'; + case 'suspended': return 'text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-900'; + default: return 'text-slate-600 bg-slate-100 dark:text-slate-400 dark:bg-slate-700'; + } + }; + + const getTierColor = (tier: string) => { + switch (tier) { + case 'diamond': return 'text-purple-600 bg-purple-100 dark:text-purple-400 dark:bg-purple-900'; + case 'platinum': return 'text-gray-600 bg-gray-100 dark:text-gray-400 dark:bg-gray-900'; + case 'gold': return 'text-yellow-600 bg-yellow-100 dark:text-yellow-400 dark:bg-yellow-900'; + case 'silver': return 'text-slate-600 bg-slate-100 dark:text-slate-400 dark:bg-slate-700'; + case 'bronze': return 'text-orange-600 bg-orange-100 dark:text-orange-400 dark:bg-orange-900'; + default: return 'text-slate-600 bg-slate-100 dark:text-slate-400 dark:bg-slate-700'; + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+

+ Channel Partners +

+

+ Manage channel partner organizations and their settings +

+
+ + {/* Filters and Actions */} +
+
+
+ {/* Search */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + {/* Status Filter */} +
+ + +
+
+ + {/* Add New Button */} + +
+
+ + {/* Partners Grid */} +
+ {filteredPartners.map((partner) => ( +
+ {/* Header */} +
+
+
+ +
+
+

+ {partner.companyName} +

+

+ {partner.companyType} +

+
+
+
+ + + +
+
+ + {/* Contact Info */} +
+
+ + {partner.contactEmail} +
+
+ + {partner.contactPhone} +
+ {partner.website && ( +
+ + {partner.website} +
+ )} +
+ + {/* Status and Tier */} +
+ + {partner.status.charAt(0).toUpperCase() + partner.status.slice(1)} + + + {partner.tier.charAt(0).toUpperCase() + partner.tier.slice(1)} + +
+ + {/* Commission Rate */} +
+ Commission Rate: + + {partner.commissionRate}% + +
+ + {/* Territory */} + {partner.territory && ( +
+ + {partner.territory} +
+ )} + + {/* Specializations */} + {partner.specializations && partner.specializations.length > 0 && ( +
+

Specializations:

+
+ {partner.specializations.slice(0, 3).map((spec, index) => ( + + {spec} + + ))} + {partner.specializations.length > 3 && ( + + +{partner.specializations.length - 3} more + + )} +
+
+ )} + + {/* Created Date */} +
+
+ + Joined {new Date(partner.createdAt).toLocaleDateString()} +
+
+
+ ))} +
+ + {/* Partner Details Modal */} + {selectedPartner && ( +
+
+
+

+ Partner Details +

+ +
+ +
+
+
+ +

{selectedPartner.companyName}

+
+
+ +

{selectedPartner.companyType}

+
+
+ +

{selectedPartner.contactEmail}

+
+
+ +

{selectedPartner.contactPhone}

+
+
+ +

{selectedPartner.website || 'N/A'}

+
+
+ + + {selectedPartner.tier.charAt(0).toUpperCase() + selectedPartner.tier.slice(1)} + +
+
+ + + {selectedPartner.status.charAt(0).toUpperCase() + selectedPartner.status.slice(1)} + +
+
+ +

{selectedPartner.commissionRate}%

+
+
+ + {selectedPartner.territory && ( +
+ +

{selectedPartner.territory}

+
+ )} + + {selectedPartner.specializations && selectedPartner.specializations.length > 0 && ( +
+ +
+ {selectedPartner.specializations.map((spec, index) => ( + + {spec} + + ))} +
+
+ )} + +
+
+ Created on {new Date(selectedPartner.createdAt).toLocaleDateString()} +
+ {selectedPartner.approvedAt && ( +
+ Approved on {new Date(selectedPartner.approvedAt).toLocaleDateString()} +
+ )} +
+
+
+
+ )} +
+
+ ); +}; + +export default ChannelPartners; \ No newline at end of file diff --git a/src/pages/admin/Dashboard.tsx b/src/pages/admin/Dashboard.tsx new file mode 100644 index 0000000..f0e7778 --- /dev/null +++ b/src/pages/admin/Dashboard.tsx @@ -0,0 +1,344 @@ +import React, { useState, useEffect } from 'react'; +import { + Users, + Building, + Clock, + CheckCircle, + XCircle, + TrendingUp, + DollarSign, + Activity, + Eye, + UserCheck, + UserX, + Settings, + Bell +} from 'lucide-react'; +import { useAppSelector } from '../../store/hooks'; + +interface DashboardStats { + totalUsers: number; + pendingVendors: number; + totalChannelPartners: number; + totalResellers: number; + recentRequests: number; + approvedToday: number; + rejectedToday: number; + revenue: number; +} + +interface PendingVendor { + id: string; + firstName: string; + lastName: string; + email: string; + company: string; + createdAt: string; + status: 'pending' | 'approved' | 'rejected'; +} + +const AdminDashboard: React.FC = () => { + const [stats, setStats] = useState({ + totalUsers: 0, + pendingVendors: 0, + totalChannelPartners: 0, + totalResellers: 0, + recentRequests: 0, + approvedToday: 0, + rejectedToday: 0, + revenue: 0 + }); + const [pendingVendors, setPendingVendors] = useState([]); + const [loading, setLoading] = useState(true); + const { user } = useAppSelector((state) => state.auth); + + useEffect(() => { + fetchDashboardData(); + }, []); + + const fetchDashboardData = async () => { + try { + // Fetch dashboard stats + const statsResponse = await fetch('/api/admin/dashboard', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + const statsData = await statsResponse.json(); + + if (statsData.success) { + setStats(statsData.data); + } + + // Fetch pending vendors + const vendorsResponse = await fetch('/api/admin/pending-vendors', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + const vendorsData = await vendorsResponse.json(); + + if (vendorsData.success) { + setPendingVendors(vendorsData.data.slice(0, 5)); // Show only first 5 + } + } catch (error) { + console.error('Error fetching dashboard data:', error); + } finally { + setLoading(false); + } + }; + + const handleApproveVendor = async (vendorId: string) => { + try { + const response = await fetch(`/api/admin/vendors/${vendorId}/approve`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ reason: 'Approved by admin' }) + }); + + if (response.ok) { + fetchDashboardData(); // Refresh data + } + } catch (error) { + console.error('Error approving vendor:', error); + } + }; + + const handleRejectVendor = async (vendorId: string) => { + try { + const response = await fetch(`/api/admin/vendors/${vendorId}/reject`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ reason: 'Rejected by admin' }) + }); + + if (response.ok) { + fetchDashboardData(); // Refresh data + } + } catch (error) { + console.error('Error rejecting vendor:', error); + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+

+ Admin Dashboard +

+

+ Welcome back, {user?.firstName}! Here's what's happening today. +

+
+ + {/* Stats Grid */} +
+ {/* Total Users */} +
+
+
+

Total Users

+

{stats.totalUsers}

+
+
+ +
+
+
+ + {/* Pending Vendors */} +
+
+
+

Pending Vendors

+

{stats.pendingVendors}

+
+
+ +
+
+
+ + {/* Channel Partners */} +
+
+
+

Channel Partners

+

{stats.totalChannelPartners}

+
+
+ +
+
+
+ + {/* Revenue */} +
+
+
+

Monthly Revenue

+

${stats.revenue.toLocaleString()}

+
+
+ +
+
+
+
+ + {/* Quick Actions */} +
+ {/* Pending Vendor Requests */} +
+
+

Pending Vendor Requests

+ +
+ + {pendingVendors.length === 0 ? ( +
+ +

No pending vendor requests

+
+ ) : ( +
+ {pendingVendors.map((vendor) => ( +
+
+
+ +
+
+

+ {vendor.firstName} {vendor.lastName} +

+

{vendor.email}

+

{vendor.company}

+
+
+
+ + + +
+
+ ))} +
+ )} +
+ + {/* Recent Activity */} +
+

Recent Activity

+ +
+
+
+ +
+
+

Vendor Approved

+

2 minutes ago

+
+
+ +
+
+ +
+
+

Vendor Rejected

+

15 minutes ago

+
+
+ +
+
+ +
+
+

New Registration

+

1 hour ago

+
+
+
+
+
+ + {/* Quick Stats */} +
+
+
+
+ +
+
+

Approved Today

+

{stats.approvedToday}

+
+
+
+ +
+
+
+ +
+
+

Rejected Today

+

{stats.rejectedToday}

+
+
+
+ +
+
+
+ +
+
+

Recent Requests

+

{stats.recentRequests}

+
+
+
+
+
+
+ ); +}; + +export default AdminDashboard; \ No newline at end of file diff --git a/src/pages/admin/Users.tsx b/src/pages/admin/Users.tsx new file mode 100644 index 0000000..bdbe84c --- /dev/null +++ b/src/pages/admin/Users.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Users, Shield, Settings } from 'lucide-react'; + +const AdminUsers: React.FC = () => { + return ( +
+
+
+

+ System Users +

+

+ Manage all system users and their permissions +

+
+ +
+
+
+ +
+

+ System Users Management +

+

+ This page will allow you to manage all system users, their roles, and permissions. +

+
+
+ + Role Management +
+
+ + Permissions +
+
+
+
+
+
+ ); +}; + +export default AdminUsers; \ No newline at end of file diff --git a/src/pages/admin/VendorRequests.tsx b/src/pages/admin/VendorRequests.tsx new file mode 100644 index 0000000..4fc2e0c --- /dev/null +++ b/src/pages/admin/VendorRequests.tsx @@ -0,0 +1,435 @@ +import React, { useState, useEffect } from 'react'; +import { + Users, + Clock, + CheckCircle, + XCircle, + Eye, + Search, + Filter, + Calendar, + Building, + Mail, + Phone, + MapPin +} from 'lucide-react'; + +interface VendorRequest { + id: string; + firstName: string; + lastName: string; + email: string; + phone: string; + company: string; + role: string; + userType: string; + status: 'pending' | 'approved' | 'rejected'; + createdAt: string; + rejectionReason?: string; +} + +const VendorRequests: React.FC = () => { + const [vendors, setVendors] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all'); + const [selectedVendor, setSelectedVendor] = useState(null); + const [showModal, setShowModal] = useState(false); + + useEffect(() => { + fetchVendorRequests(); + }, []); + + const fetchVendorRequests = async () => { + try { + const response = await fetch('/api/admin/pending-vendors', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + const data = await response.json(); + + if (data.success) { + setVendors(data.data); + } + } catch (error) { + console.error('Error fetching vendor requests:', error); + } finally { + setLoading(false); + } + }; + + const handleApprove = async (vendorId: string) => { + try { + const response = await fetch(`/api/admin/vendors/${vendorId}/approve`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ reason: 'Approved by admin' }) + }); + + if (response.ok) { + fetchVendorRequests(); + } + } catch (error) { + console.error('Error approving vendor:', error); + } + }; + + const handleReject = async (vendorId: string, reason: string) => { + try { + const response = await fetch(`/api/admin/vendors/${vendorId}/reject`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ reason }) + }); + + if (response.ok) { + fetchVendorRequests(); + setShowModal(false); + } + } catch (error) { + console.error('Error rejecting vendor:', error); + } + }; + + const filteredVendors = vendors.filter(vendor => { + const matchesSearch = vendor.firstName.toLowerCase().includes(searchTerm.toLowerCase()) || + vendor.lastName.toLowerCase().includes(searchTerm.toLowerCase()) || + vendor.email.toLowerCase().includes(searchTerm.toLowerCase()) || + vendor.company.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesStatus = statusFilter === 'all' || vendor.status === statusFilter; + + return matchesSearch && matchesStatus; + }); + + const getStatusColor = (status: string) => { + switch (status) { + case 'pending': return 'text-amber-600 bg-amber-100 dark:text-amber-400 dark:bg-amber-900'; + case 'approved': return 'text-green-600 bg-green-100 dark:text-green-400 dark:bg-green-900'; + case 'rejected': return 'text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-900'; + default: return 'text-slate-600 bg-slate-100 dark:text-slate-400 dark:bg-slate-700'; + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+

+ Vendor Requests +

+

+ Review and manage vendor registration requests +

+
+ + {/* Filters */} +
+
+ {/* Search */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + {/* Status Filter */} +
+ + +
+
+
+ + {/* Vendor List */} +
+
+

+ Vendor Requests ({filteredVendors.length}) +

+
+ +
+ + + + + + + + + + + + + {filteredVendors.map((vendor) => ( + + + + + + + + + ))} + +
+ Vendor + + Company + + Role + + Status + + Date + + Actions +
+
+
+ +
+
+
+ {vendor.firstName} {vendor.lastName} +
+
+ {vendor.email} +
+
+
+
+
+ + + {vendor.company} + +
+
+ + {vendor.role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} + + + + {vendor.status.charAt(0).toUpperCase() + vendor.status.slice(1)} + + +
+ + {new Date(vendor.createdAt).toLocaleDateString()} +
+
+
+ + {vendor.status === 'pending' && ( + <> + + + + )} +
+
+
+
+ + {/* Vendor Details Modal */} + {selectedVendor && ( +
+
+
+

+ Vendor Details +

+ +
+ +
+
+
+ +

+ {selectedVendor.firstName} {selectedVendor.lastName} +

+
+
+ +
+ +

{selectedVendor.email}

+
+
+
+ +
+ +

{selectedVendor.phone}

+
+
+
+ +
+ +

{selectedVendor.company}

+
+
+
+ +

+ {selectedVendor.role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} +

+
+
+ +

+ {selectedVendor.userType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} +

+
+
+ + {selectedVendor.rejectionReason && ( +
+ +

{selectedVendor.rejectionReason}

+
+ )} + +
+
+ Registered on {new Date(selectedVendor.createdAt).toLocaleDateString()} +
+ {selectedVendor.status === 'pending' && ( +
+ + +
+ )} +
+
+
+
+ )} + + {/* Rejection Modal */} + {showModal && selectedVendor && ( +
+
+

+ Reject Vendor Request +

+