admin panel, api integration, auth

This commit is contained in:
rohit 2025-08-06 19:29:54 +05:30
parent aaef6e883b
commit b0c762c57a
16 changed files with 1575 additions and 40 deletions

27
public/favicon.svg Normal file
View File

@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<defs>
<linearGradient id="cloudGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
<stop offset="50%" style="stop-color:#6366F1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
</linearGradient>
<linearGradient id="lightningGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FCD34D;stop-opacity:1" />
<stop offset="100%" style="stop-color:#F59E0B;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="16" cy="16" r="15" fill="url(#cloudGradient)" stroke="#1E40AF" stroke-width="1"/>
<!-- Cloud shape -->
<path d="M8 18c0-3.3 2.7-6 6-6 1.2 0 2.3 0.4 3.2 1 0.8-2.8 3.2-4.8 6.1-5.2 0.1 0 0.2 0 0.3 0 2.8 0 5.1 2.3 5.1 5.1 0 0.1 0 0.2 0 0.3 2.4 0.4 4.3 2.4 4.3 4.9 0 2.7-2.2 4.9-4.9 4.9H12c-2.2 0-4-1.8-4-4z"
fill="white" opacity="0.9"/>
<!-- Lightning bolt -->
<path d="M15 8l-2 8h3l-1 8 6-10h-3l2-6z" fill="url(#lightningGradient)"/>
<!-- Cloud highlights -->
<ellipse cx="12" cy="16" rx="2" ry="1" fill="white" opacity="0.6"/>
<ellipse cx="20" cy="18" rx="1.5" ry="0.8" fill="white" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" type="image/svg+xml" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta

View File

@ -3,9 +3,9 @@
"name": "Cloudtopiaa Channel Partner Dashboard",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
"src": "favicon.svg",
"sizes": "any",
"type": "image/svg+xml"
},
{
"src": "logo192.png",

View File

@ -36,6 +36,14 @@ import ResellerSupport from './pages/reseller/Support';
import ResellerReports from './pages/reseller/Reports';
import ResellerTraining from './pages/reseller/Training';
import ResellerLayout from './components/Layout/ResellerLayout';
// Admin Components
import AdminLayout from './components/Layout/AdminLayout';
import AdminDashboard from './pages/admin/Dashboard';
import VendorRequests from './pages/admin/VendorRequests';
import ChannelPartners from './pages/admin/ChannelPartners';
import AdminUsers from './pages/admin/Users';
import Unauthorized from './pages/Unauthorized';
import CookieConsent from './components/CookieConsent';
import './index.css';
@ -101,91 +109,91 @@ function App() {
{/* Protected Routes - Vendor Only */}
<Route path="/" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Dashboard />
</Layout>
</ProtectedRoute>
} />
<Route path="/resellers" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<ResellersPage />
</Layout>
</ProtectedRoute>
} />
<Route path="/partnerships" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<PartnershipsPage />
</Layout>
</ProtectedRoute>
} />
<Route path="/deals" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<DealsPage />
</Layout>
</ProtectedRoute>
} />
<Route path="/commissions" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<CommissionsPage />
</Layout>
</ProtectedRoute>
} />
<Route path="/product-management" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<ProductManagement />
</Layout>
</ProtectedRoute>
} />
<Route path="/training" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Training />
</Layout>
</ProtectedRoute>
} />
<Route path="/support" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Support />
</Layout>
</ProtectedRoute>
} />
<Route path="/analytics" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Analytics />
</Layout>
</ProtectedRoute>
} />
<Route path="/reports" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Reports />
</Layout>
</ProtectedRoute>
} />
<Route path="/settings" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Settings />
</Layout>
</ProtectedRoute>
} />
<Route path="/targets" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<PlaceholderPage title="Targets" description="Set and track performance targets" />
</Layout>
</ProtectedRoute>
} />
<Route path="/performance" element={
<ProtectedRoute requiredRole="vendor">
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<PlaceholderPage title="Performance" description="Performance metrics and KPIs" />
</Layout>
@ -249,84 +257,114 @@ function App() {
{/* Reseller Dashboard Routes (Separate Service) */}
<Route path="/reseller-dashboard" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<ResellerDashboardMain />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/customers" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<ResellerDashboardCustomers />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/instances" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<ResellerDashboardInstances />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/billing" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Billing & Payments" description="Manage your billing, invoices, and payment methods" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/support" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<Support />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/reports" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Reports & Analytics" description="View detailed reports and analytics" />
</ResellerLayout>
</ProtectedRoute>
} />
{/* Admin Routes */}
<Route path="/admin" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<AdminDashboard />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/admin/vendor-requests" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<VendorRequests />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/admin/channel-partners" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<ChannelPartners />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/admin/users" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<AdminUsers />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/wallet" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Wallet Management" description="Manage your funds, transactions, and payment methods" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/training" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<ResellerTraining />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/marketplace" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Marketplace" description="Browse and purchase cloud services and solutions" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/certifications" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Certifications" description="Manage your professional certifications and achievements" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/knowledge-base" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Knowledge Base" description="Access documentation, guides, and helpful resources" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/settings" element={
<ProtectedRoute requiredRole="reseller">
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Settings" description="Configure your account preferences and system settings" />
</ResellerLayout>

View File

@ -0,0 +1,72 @@
import React from 'react';
import AdminSidebar from './AdminSidebar';
interface AdminLayoutProps {
children: React.ReactNode;
}
const AdminLayout: React.FC<AdminLayoutProps> = ({ children }) => {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50 to-indigo-100 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
<div className="flex h-screen">
{/* Sidebar */}
<div className="flex-shrink-0">
<AdminSidebar />
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Top Navigation Bar */}
<div className="bg-white/90 dark:bg-slate-900/95 backdrop-blur-xl border-b border-slate-200/50 dark:border-slate-700/50 sticky top-0 z-40 shadow-sm">
<div className="px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="w-10 h-10 bg-gradient-to-r from-purple-600 to-indigo-600 rounded-xl flex items-center justify-center">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<div>
<h1 className="text-xl font-semibold text-slate-900 dark:text-white">
Admin Panel
</h1>
<p className="text-sm text-slate-600 dark:text-slate-400">
System Administration & Management
</p>
</div>
</div>
<div className="flex items-center space-x-4">
{/* Notifications */}
<button className="p-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-5 5v-5zM4.19 4.19A4 4 0 004 6v6a4 4 0 004 4h6a4 4 0 004-4V6a4 4 0 00-4-4H6a4 4 0 00-2.81 1.19z" />
</svg>
</button>
{/* User Menu */}
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-medium">A</span>
</div>
<div className="hidden md:block">
<p className="text-sm font-medium text-slate-900 dark:text-white">Admin User</p>
<p className="text-xs text-slate-600 dark:text-slate-400">System Administrator</p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Page Content */}
<div className="flex-1 overflow-auto">
{children}
</div>
</div>
</div>
</div>
);
};
export default AdminLayout;

View File

@ -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 (
<div className={cn(
"flex flex-col h-full bg-gradient-to-b from-slate-900 via-purple-900 to-indigo-900 border-r border-slate-700/50 transition-all duration-300",
isCollapsed ? "w-16" : "w-64"
)}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-slate-700/50">
{!isCollapsed && (
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-lg flex items-center justify-center">
<Shield className="w-5 h-5 text-white" />
</div>
<span className="text-lg font-semibold text-white">
Admin
</span>
</div>
)}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="p-1 rounded-md hover:bg-slate-700/50 transition-colors"
>
{isCollapsed ? (
<Menu className="w-6 h-6 text-slate-400" />
) : (
<X className="w-5 h-5 text-slate-400" />
)}
</button>
</div>
{/* Navigation */}
<nav className={cn(
"flex-1 py-4 space-y-1 overflow-y-auto",
isCollapsed ? "px-3" : "px-2"
)}>
{adminNavigation.map((item) => {
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={cn(
"flex items-center text-sm font-medium rounded-xl transition-all duration-200 group",
isCollapsed ? "px-2 py-3 justify-center" : "px-4 py-3",
isActive
? "bg-gradient-to-r from-purple-500/20 to-indigo-500/20 text-purple-400 border border-purple-500/30 shadow-lg"
: "text-slate-300 hover:bg-slate-800/50 hover:text-white"
)}
>
<item.icon className={cn(
"flex-shrink-0 transition-colors duration-200",
isCollapsed ? "w-6 h-6" : "w-5 h-5",
isActive
? "text-purple-400"
: "text-slate-400 group-hover:text-white"
)} />
{!isCollapsed && (
<span className="ml-3">{item.name}</span>
)}
</Link>
);
})}
</nav>
{/* User Profile & Actions */}
<div className="border-t border-slate-700/50 p-4 space-y-2">
{/* Theme Toggle */}
<button
onClick={() => dispatch(toggleTheme())}
className={cn(
"w-full flex items-center rounded-xl text-sm font-medium transition-all duration-200",
isCollapsed ? "justify-center px-2 py-3" : "px-4 py-3",
"text-slate-300 hover:bg-slate-800/50 hover:text-white"
)}
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{theme === 'dark' ? (
<Sun className="w-6 h-6 text-amber-400" />
) : (
<Moon className="w-6 h-6 text-slate-400" />
)}
{!isCollapsed && (
<span className="ml-3">
{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
</span>
)}
</button>
{/* Logout */}
<button
onClick={handleLogout}
className={cn(
"w-full flex items-center rounded-xl text-sm font-medium transition-all duration-200",
isCollapsed ? "justify-center px-2 py-3" : "px-4 py-3",
"text-red-400 hover:bg-red-500/10 hover:text-red-300"
)}
title="Logout"
>
<LogOut className="w-6 h-6" />
{!isCollapsed && <span className="ml-3">Logout</span>}
</button>
</div>
</div>
);
};
export default AdminSidebar;

View File

@ -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';
}
}

View File

@ -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

View File

@ -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';

View File

@ -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<ChannelPartner[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'pending' | 'suspended'>('all');
const [selectedPartner, setSelectedPartner] = useState<ChannelPartner | null>(null);
const [showModal, setShowModal] = useState(false);
const [editingPartner, setEditingPartner] = useState<ChannelPartner | null>(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<ChannelPartner>) => {
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 (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
<div className="p-6">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Channel Partners
</h1>
<p className="text-slate-600 dark:text-slate-400">
Manage channel partner organizations and their settings
</p>
</div>
{/* Filters and Actions */}
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 mb-6">
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="flex flex-col md:flex-row gap-4 flex-1">
{/* Search */}
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder="Search partners..."
value={searchTerm}
onChange={(e) => 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"
/>
</div>
</div>
{/* Status Filter */}
<div className="flex items-center space-x-2">
<Filter className="w-5 h-5 text-slate-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="px-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"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="suspended">Suspended</option>
</select>
</div>
</div>
{/* Add New Button */}
<button
onClick={() => setShowModal(true)}
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Plus className="w-4 h-4" />
<span>Add Partner</span>
</button>
</div>
</div>
{/* Partners Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredPartners.map((partner) => (
<div key={partner.id} className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-shadow">
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<Building className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h3 className="font-semibold text-slate-900 dark:text-white">
{partner.companyName}
</h3>
<p className="text-sm text-slate-500 dark:text-slate-400">
{partner.companyType}
</p>
</div>
</div>
<div className="flex items-center space-x-1">
<button
onClick={() => setSelectedPartner(partner)}
className="p-1 text-slate-400 hover:text-blue-600 dark:hover:text-blue-400"
title="View Details"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => setEditingPartner(partner)}
className="p-1 text-slate-400 hover:text-green-600 dark:hover:text-green-400"
title="Edit"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(partner.id)}
className="p-1 text-slate-400 hover:text-red-600 dark:hover:text-red-400"
title="Delete"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
{/* Contact Info */}
<div className="space-y-2 mb-4">
<div className="flex items-center text-sm text-slate-600 dark:text-slate-400">
<Mail className="w-4 h-4 mr-2" />
{partner.contactEmail}
</div>
<div className="flex items-center text-sm text-slate-600 dark:text-slate-400">
<Phone className="w-4 h-4 mr-2" />
{partner.contactPhone}
</div>
{partner.website && (
<div className="flex items-center text-sm text-slate-600 dark:text-slate-400">
<Globe className="w-4 h-4 mr-2" />
{partner.website}
</div>
)}
</div>
{/* Status and Tier */}
<div className="flex items-center justify-between mb-4">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(partner.status)}`}>
{partner.status.charAt(0).toUpperCase() + partner.status.slice(1)}
</span>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getTierColor(partner.tier)}`}>
{partner.tier.charAt(0).toUpperCase() + partner.tier.slice(1)}
</span>
</div>
{/* Commission Rate */}
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 dark:text-slate-400">Commission Rate:</span>
<span className="font-semibold text-green-600 dark:text-green-400">
{partner.commissionRate}%
</span>
</div>
{/* Territory */}
{partner.territory && (
<div className="mt-3 flex items-center text-sm text-slate-600 dark:text-slate-400">
<MapPin className="w-4 h-4 mr-2" />
{partner.territory}
</div>
)}
{/* Specializations */}
{partner.specializations && partner.specializations.length > 0 && (
<div className="mt-3">
<p className="text-xs text-slate-500 dark:text-slate-500 mb-1">Specializations:</p>
<div className="flex flex-wrap gap-1">
{partner.specializations.slice(0, 3).map((spec, index) => (
<span key={index} className="px-2 py-1 text-xs bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 rounded">
{spec}
</span>
))}
{partner.specializations.length > 3 && (
<span className="px-2 py-1 text-xs bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 rounded">
+{partner.specializations.length - 3} more
</span>
)}
</div>
</div>
)}
{/* Created Date */}
<div className="mt-4 pt-3 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-center text-xs text-slate-500 dark:text-slate-500">
<Calendar className="w-3 h-3 mr-1" />
Joined {new Date(partner.createdAt).toLocaleDateString()}
</div>
</div>
</div>
))}
</div>
{/* Partner Details Modal */}
{selectedPartner && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">
Partner Details
</h3>
<button
onClick={() => setSelectedPartner(null)}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
</button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Company Name
</label>
<p className="text-slate-900 dark:text-white">{selectedPartner.companyName}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Company Type
</label>
<p className="text-slate-900 dark:text-white">{selectedPartner.companyType}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Contact Email
</label>
<p className="text-slate-900 dark:text-white">{selectedPartner.contactEmail}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Contact Phone
</label>
<p className="text-slate-900 dark:text-white">{selectedPartner.contactPhone}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Website
</label>
<p className="text-slate-900 dark:text-white">{selectedPartner.website || 'N/A'}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Tier
</label>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getTierColor(selectedPartner.tier)}`}>
{selectedPartner.tier.charAt(0).toUpperCase() + selectedPartner.tier.slice(1)}
</span>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Status
</label>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(selectedPartner.status)}`}>
{selectedPartner.status.charAt(0).toUpperCase() + selectedPartner.status.slice(1)}
</span>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Commission Rate
</label>
<p className="text-slate-900 dark:text-white">{selectedPartner.commissionRate}%</p>
</div>
</div>
{selectedPartner.territory && (
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Territory
</label>
<p className="text-slate-900 dark:text-white">{selectedPartner.territory}</p>
</div>
)}
{selectedPartner.specializations && selectedPartner.specializations.length > 0 && (
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Specializations
</label>
<div className="flex flex-wrap gap-2">
{selectedPartner.specializations.map((spec, index) => (
<span key={index} className="px-3 py-1 text-sm bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 rounded-full">
{spec}
</span>
))}
</div>
</div>
)}
<div className="flex items-center justify-between pt-4 border-t border-slate-200 dark:border-slate-700">
<div className="text-sm text-slate-500 dark:text-slate-400">
Created on {new Date(selectedPartner.createdAt).toLocaleDateString()}
</div>
{selectedPartner.approvedAt && (
<div className="text-sm text-slate-500 dark:text-slate-400">
Approved on {new Date(selectedPartner.approvedAt).toLocaleDateString()}
</div>
)}
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default ChannelPartners;

View File

@ -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<DashboardStats>({
totalUsers: 0,
pendingVendors: 0,
totalChannelPartners: 0,
totalResellers: 0,
recentRequests: 0,
approvedToday: 0,
rejectedToday: 0,
revenue: 0
});
const [pendingVendors, setPendingVendors] = useState<PendingVendor[]>([]);
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 (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
<div className="p-6">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Admin Dashboard
</h1>
<p className="text-slate-600 dark:text-slate-400">
Welcome back, {user?.firstName}! Here's what's happening today.
</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* Total Users */}
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Users</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.totalUsers}</p>
</div>
<div className="p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
{/* Pending Vendors */}
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Pending Vendors</p>
<p className="text-2xl font-bold text-amber-600 dark:text-amber-400">{stats.pendingVendors}</p>
</div>
<div className="p-3 bg-amber-100 dark:bg-amber-900 rounded-lg">
<Clock className="w-6 h-6 text-amber-600 dark:text-amber-400" />
</div>
</div>
</div>
{/* Channel Partners */}
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Channel Partners</p>
<p className="text-2xl font-bold text-emerald-600 dark:text-emerald-400">{stats.totalChannelPartners}</p>
</div>
<div className="p-3 bg-emerald-100 dark:bg-emerald-900 rounded-lg">
<Building className="w-6 h-6 text-emerald-600 dark:text-emerald-400" />
</div>
</div>
</div>
{/* Revenue */}
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Monthly Revenue</p>
<p className="text-2xl font-bold text-green-600 dark:text-green-400">${stats.revenue.toLocaleString()}</p>
</div>
<div className="p-3 bg-green-100 dark:bg-green-900 rounded-lg">
<DollarSign className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
{/* Pending Vendor Requests */}
<div className="lg:col-span-2 bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">Pending Vendor Requests</h2>
<Bell className="w-5 h-5 text-amber-500" />
</div>
{pendingVendors.length === 0 ? (
<div className="text-center py-8">
<Clock className="w-12 h-12 text-slate-400 mx-auto mb-4" />
<p className="text-slate-500 dark:text-slate-400">No pending vendor requests</p>
</div>
) : (
<div className="space-y-4">
{pendingVendors.map((vendor) => (
<div key={vendor.id} className="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-700 rounded-lg">
<div className="flex items-center space-x-4">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="font-medium text-slate-900 dark:text-white">
{vendor.firstName} {vendor.lastName}
</p>
<p className="text-sm text-slate-600 dark:text-slate-400">{vendor.email}</p>
<p className="text-xs text-slate-500 dark:text-slate-500">{vendor.company}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => handleApproveVendor(vendor.id)}
className="p-2 bg-green-100 dark:bg-green-900 rounded-lg hover:bg-green-200 dark:hover:bg-green-800 transition-colors"
title="Approve"
>
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
</button>
<button
onClick={() => handleRejectVendor(vendor.id)}
className="p-2 bg-red-100 dark:bg-red-900 rounded-lg hover:bg-red-200 dark:hover:bg-red-800 transition-colors"
title="Reject"
>
<XCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
</button>
<button
className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
title="View Details"
>
<Eye className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Recent Activity */}
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h2 className="text-xl font-semibold text-slate-900 dark:text-white mb-6">Recent Activity</h2>
<div className="space-y-4">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center">
<UserCheck className="w-4 h-4 text-green-600 dark:text-green-400" />
</div>
<div>
<p className="text-sm font-medium text-slate-900 dark:text-white">Vendor Approved</p>
<p className="text-xs text-slate-500 dark:text-slate-400">2 minutes ago</p>
</div>
</div>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center">
<UserX className="w-4 h-4 text-red-600 dark:text-red-400" />
</div>
<div>
<p className="text-sm font-medium text-slate-900 dark:text-white">Vendor Rejected</p>
<p className="text-xs text-slate-500 dark:text-slate-400">15 minutes ago</p>
</div>
</div>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<Activity className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="text-sm font-medium text-slate-900 dark:text-white">New Registration</p>
<p className="text-xs text-slate-500 dark:text-slate-400">1 hour ago</p>
</div>
</div>
</div>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center space-x-3">
<div className="p-3 bg-green-100 dark:bg-green-900 rounded-lg">
<CheckCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Approved Today</p>
<p className="text-2xl font-bold text-green-600 dark:text-green-400">{stats.approvedToday}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center space-x-3">
<div className="p-3 bg-red-100 dark:bg-red-900 rounded-lg">
<XCircle className="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Rejected Today</p>
<p className="text-2xl font-bold text-red-600 dark:text-red-400">{stats.rejectedToday}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center space-x-3">
<div className="p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
<TrendingUp className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Recent Requests</p>
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400">{stats.recentRequests}</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default AdminDashboard;

45
src/pages/admin/Users.tsx Normal file
View File

@ -0,0 +1,45 @@
import React from 'react';
import { Users, Shield, Settings } from 'lucide-react';
const AdminUsers: React.FC = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50 to-indigo-100 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
<div className="p-6">
<div className="mb-8">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
System Users
</h1>
<p className="text-slate-600 dark:text-slate-400">
Manage all system users and their permissions
</p>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl p-12 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="text-center">
<div className="w-16 h-16 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mx-auto mb-4">
<Users className="w-8 h-8 text-purple-600 dark:text-purple-400" />
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">
System Users Management
</h3>
<p className="text-slate-600 dark:text-slate-400 mb-6">
This page will allow you to manage all system users, their roles, and permissions.
</p>
<div className="flex items-center justify-center space-x-4 text-sm text-slate-500 dark:text-slate-400">
<div className="flex items-center space-x-2">
<Shield className="w-4 h-4" />
<span>Role Management</span>
</div>
<div className="flex items-center space-x-2">
<Settings className="w-4 h-4" />
<span>Permissions</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default AdminUsers;

View File

@ -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<VendorRequest[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all');
const [selectedVendor, setSelectedVendor] = useState<VendorRequest | null>(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 (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
<div className="p-6">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Vendor Requests
</h1>
<p className="text-slate-600 dark:text-slate-400">
Review and manage vendor registration requests
</p>
</div>
{/* Filters */}
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 mb-6">
<div className="flex flex-col md:flex-row gap-4">
{/* Search */}
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder="Search vendors..."
value={searchTerm}
onChange={(e) => 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"
/>
</div>
</div>
{/* Status Filter */}
<div className="flex items-center space-x-2">
<Filter className="w-5 h-5 text-slate-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="px-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"
>
<option value="all">All Status</option>
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
</select>
</div>
</div>
</div>
{/* Vendor List */}
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-lg border border-slate-200 dark:border-slate-700">
<div className="p-6 border-b border-slate-200 dark:border-slate-700">
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
Vendor Requests ({filteredVendors.length})
</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 dark:bg-slate-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Vendor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Company
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Role
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-slate-800 divide-y divide-slate-200 dark:divide-slate-700">
{filteredVendors.map((vendor) => (
<tr key={vendor.id} className="hover:bg-slate-50 dark:hover:bg-slate-700">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div className="ml-4">
<div className="text-sm font-medium text-slate-900 dark:text-white">
{vendor.firstName} {vendor.lastName}
</div>
<div className="text-sm text-slate-500 dark:text-slate-400">
{vendor.email}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<Building className="w-4 h-4 text-slate-400 mr-2" />
<span className="text-sm text-slate-900 dark:text-white">
{vendor.company}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="text-sm text-slate-900 dark:text-white">
{vendor.role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(vendor.status)}`}>
{vendor.status.charAt(0).toUpperCase() + vendor.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
<div className="flex items-center">
<Calendar className="w-4 h-4 mr-2" />
{new Date(vendor.createdAt).toLocaleDateString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center space-x-2">
<button
onClick={() => setSelectedVendor(vendor)}
className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
title="View Details"
>
<Eye className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</button>
{vendor.status === 'pending' && (
<>
<button
onClick={() => handleApprove(vendor.id)}
className="p-2 bg-green-100 dark:bg-green-900 rounded-lg hover:bg-green-200 dark:hover:bg-green-800 transition-colors"
title="Approve"
>
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
</button>
<button
onClick={() => {
setSelectedVendor(vendor);
setShowModal(true);
}}
className="p-2 bg-red-100 dark:bg-red-900 rounded-lg hover:bg-red-200 dark:hover:bg-red-800 transition-colors"
title="Reject"
>
<XCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
</button>
</>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Vendor Details Modal */}
{selectedVendor && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">
Vendor Details
</h3>
<button
onClick={() => setSelectedVendor(null)}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
</button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Full Name
</label>
<p className="text-slate-900 dark:text-white">
{selectedVendor.firstName} {selectedVendor.lastName}
</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Email
</label>
<div className="flex items-center">
<Mail className="w-4 h-4 text-slate-400 mr-2" />
<p className="text-slate-900 dark:text-white">{selectedVendor.email}</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Phone
</label>
<div className="flex items-center">
<Phone className="w-4 h-4 text-slate-400 mr-2" />
<p className="text-slate-900 dark:text-white">{selectedVendor.phone}</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Company
</label>
<div className="flex items-center">
<Building className="w-4 h-4 text-slate-400 mr-2" />
<p className="text-slate-900 dark:text-white">{selectedVendor.company}</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Role
</label>
<p className="text-slate-900 dark:text-white">
{selectedVendor.role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
User Type
</label>
<p className="text-slate-900 dark:text-white">
{selectedVendor.userType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</p>
</div>
</div>
{selectedVendor.rejectionReason && (
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Rejection Reason
</label>
<p className="text-red-600 dark:text-red-400">{selectedVendor.rejectionReason}</p>
</div>
)}
<div className="flex items-center justify-between pt-4 border-t border-slate-200 dark:border-slate-700">
<div className="text-sm text-slate-500 dark:text-slate-400">
Registered on {new Date(selectedVendor.createdAt).toLocaleDateString()}
</div>
{selectedVendor.status === 'pending' && (
<div className="flex space-x-2">
<button
onClick={() => handleApprove(selectedVendor.id)}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Approve
</button>
<button
onClick={() => setShowModal(true)}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
>
Reject
</button>
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* Rejection Modal */}
{showModal && selectedVendor && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
Reject Vendor Request
</h3>
<textarea
placeholder="Enter rejection reason..."
className="w-full p-3 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-red-500"
rows={4}
id="rejectionReason"
/>
<div className="flex space-x-2 mt-4">
<button
onClick={() => {
const reason = (document.getElementById('rejectionReason') as HTMLTextAreaElement).value;
if (reason.trim()) {
handleReject(selectedVendor.id, reason);
}
}}
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
>
Reject
</button>
<button
onClick={() => setShowModal(false)}
className="flex-1 px-4 py-2 bg-slate-300 dark:bg-slate-600 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-400 dark:hover:bg-slate-500 transition-colors"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default VendorRequests;

View File

@ -59,13 +59,13 @@ const ResellerLogin: React.FC = () => {
if (user) {
// Check if user has roles property and it's an array
if (user.roles && Array.isArray(user.roles)) {
const hasVendorRole = user.roles.some(role => role.name === 'vendor');
const hasVendorRole = user.roles.some(role => role.name === 'channel_partner_admin');
if (hasVendorRole) {
redirectPath = '/'; // Redirect vendors to vendor dashboard
}
} else if (user.role) {
// Fallback: check the simple role property
if (user.role === 'vendor') {
if (user.role === 'channel_partner_admin') {
redirectPath = '/'; // Redirect vendors to vendor dashboard
}
}

View File

@ -129,7 +129,8 @@ const ResellerSignup: React.FC = () => {
password: formData.password,
phone: formData.phone,
company: formData.company,
role: 'reseller'
role: 'reseller_admin',
userType: 'reseller'
})).unwrap();
// Navigate to common login page with success message

View File

@ -13,7 +13,8 @@ export interface RegisterRequest {
lastName: string;
phone?: string;
company?: string;
role?: 'vendor' | 'reseller' | 'admin';
role?: 'channel_partner_admin' | 'channel_partner_manager' | 'channel_partner_sales' | 'channel_partner_support' | 'channel_partner_finance' | 'channel_partner_analyst' | 'reseller_admin' | 'reseller_manager' | 'reseller_sales' | 'reseller_support' | 'reseller_finance' | 'reseller_analyst' | 'system_admin' | 'system_support' | 'system_analyst' | 'read_only';
userType?: 'channel_partner' | 'reseller' | 'system';
}
export interface AuthResponse {