admin panel, api integration, auth
This commit is contained in:
parent
aaef6e883b
commit
b0c762c57a
27
public/favicon.svg
Normal file
27
public/favicon.svg
Normal 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 |
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
88
src/App.tsx
88
src/App.tsx
@ -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>
|
||||
|
||||
72
src/components/Layout/AdminLayout.tsx
Normal file
72
src/components/Layout/AdminLayout.tsx
Normal 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;
|
||||
137
src/components/Layout/AdminSidebar.tsx
Normal file
137
src/components/Layout/AdminSidebar.tsx
Normal 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;
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
|
||||
434
src/pages/admin/ChannelPartners.tsx
Normal file
434
src/pages/admin/ChannelPartners.tsx
Normal 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;
|
||||
344
src/pages/admin/Dashboard.tsx
Normal file
344
src/pages/admin/Dashboard.tsx
Normal 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
45
src/pages/admin/Users.tsx
Normal 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;
|
||||
435
src/pages/admin/VendorRequests.tsx
Normal file
435
src/pages/admin/VendorRequests.tsx
Normal 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;
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user