v1.0.0-alpha

This commit is contained in:
rohit 2025-08-11 00:47:45 +05:30
parent b0c762c57a
commit 8ac3c89b10
64 changed files with 15078 additions and 2358 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
REACT_APP_API_URL=http://localhost:5000/api

144
package-lock.json generated
View File

@ -20,6 +20,7 @@
"@types/node": "^16.18.126",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@types/socket.io-client": "^1.4.36",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.536.0",
@ -31,6 +32,7 @@
"react-router-dom": "^6.30.1",
"react-scripts": "5.0.1",
"recharts": "^3.1.0",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
@ -3428,6 +3430,12 @@
"@sinonjs/commons": "^1.7.0"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
@ -4282,6 +4290,12 @@
"@types/send": "*"
}
},
"node_modules/@types/socket.io-client": {
"version": "1.4.36",
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz",
"integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==",
"license": "MIT"
},
"node_modules/@types/sockjs": {
"version": "0.3.36",
"resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
@ -7494,6 +7508,66 @@
"node": ">= 0.8"
}
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.2",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
@ -15911,6 +15985,68 @@
"node": ">=8"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/sockjs": {
"version": "0.3.24",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
@ -18435,6 +18571,14 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT"
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -15,6 +15,7 @@
"@types/node": "^16.18.126",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@types/socket.io-client": "^1.4.36",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.536.0",
@ -26,6 +27,7 @@
"react-router-dom": "^6.30.1",
"react-scripts": "5.0.1",
"recharts": "^3.1.0",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"

View File

@ -3,6 +3,7 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-d
import { Provider } from 'react-redux';
import { CookiesProvider } from 'react-cookie';
import { store } from './store';
import { useAppSelector } from './store/hooks';
import { setTheme } from './store/slices/themeSlice';
import ProtectedRoute from './components/ProtectedRoute';
import AuthInitializer from './components/AuthInitializer';
@ -11,8 +12,8 @@ import Toast from './components/Toast';
// Channel Partner Components
import Layout from './components/Layout/Layout';
import Dashboard from './pages/Dashboard';
import ResellersPage from './pages/Resellers';
import PartnershipsPage from './pages/Partnerships';
import ResellerRequestsPage from './pages/Resellers';
import ApprovedResellersPage from './pages/ApprovedResellers/ApprovedResellers';
import DealsPage from './pages/Deals';
import CommissionsPage from './pages/Commissions';
import ProductManagement from './pages/ProductManagement';
@ -22,7 +23,7 @@ import Analytics from './pages/Analytics';
import Reports from './pages/Reports';
import Settings from './pages/Settings';
import Login from './pages/Login';
import Signup from './pages/Signup';
import Signup from './pages/SignupStepwise';
import VerifyEmail from './pages/VerifyEmail';
// Reseller Components
@ -35,6 +36,7 @@ import ResellerBilling from './pages/reseller/Billing';
import ResellerSupport from './pages/reseller/Support';
import ResellerReports from './pages/reseller/Reports';
import ResellerTraining from './pages/reseller/Training';
import Receipts from './pages/reseller/Receipts';
import ResellerLayout from './components/Layout/ResellerLayout';
// Admin Components
@ -43,11 +45,39 @@ 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 Products from './pages/admin/Products';
import AdminAnalytics from './pages/admin/Analytics';
import AdminReports from './pages/admin/Reports';
import AdminSettings from './pages/admin/Settings';
import AdminFeedback from './pages/admin/Feedback';
import RegisteredVendors from './pages/admin/RegisteredVendors';
import Resellers from './pages/admin/Resellers';
import Unauthorized from './pages/Unauthorized';
import CookieConsent from './components/CookieConsent';
import AuthDebug from './components/AuthDebug';
import DeveloperFeedback from './components/DeveloperFeedback';
import socketService from './services/socketService';
import './index.css';
// Component to handle role-based redirects
const RoleBasedRedirect: React.FC = () => {
const { user } = useAppSelector((state) => state.auth);
// Check if user has roles array and find the primary role
const primaryRole = user?.roles?.[0]?.name || user?.role;
if (primaryRole === 'system_admin') {
return <Navigate to="/admin" replace />;
} else if (primaryRole?.startsWith('channel_partner_')) {
return <Navigate to="/dashboard" replace />;
} else if (primaryRole?.startsWith('reseller_')) {
return <Navigate to="/reseller-dashboard" replace />;
} else {
return <Navigate to="/login" replace />;
}
};
// Placeholder components for other pages
const PlaceholderPage: React.FC<{ title: string; description: string }> = ({ title, description }) => (
<div className="space-y-6">
@ -91,15 +121,25 @@ function App() {
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
// Initialize socket connection if user is logged in
const token = localStorage.getItem('accessToken');
if (token) {
socketService.connect(token);
}
return () => {
mediaQuery.removeEventListener('change', handleChange);
socketService.disconnect();
};
}, []);
return (
<Provider store={store}>
<CookiesProvider>
<AuthInitializer>
<Router>
<div className="App">
<Router>
<div className="App">
<Routes>
{/* Public Routes */}
<Route path="/login" element={<Login />} />
@ -107,117 +147,120 @@ function App() {
<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/unauthorized" element={<Unauthorized />} />
{/* Root route - Redirect based on user role */}
<Route path="/" element={<RoleBasedRedirect />} />
{/* Protected Routes - Vendor Only */}
<Route path="/" element={
<Route path="/dashboard" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Dashboard />
</Layout>
<Layout>
<Dashboard />
</Layout>
</ProtectedRoute>
} />
<Route path="/resellers" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<ResellersPage />
</Layout>
<Layout>
<ResellerRequestsPage />
</Layout>
</ProtectedRoute>
} />
<Route path="/partnerships" element={
<Route path="/approved-resellers" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<PartnershipsPage />
</Layout>
<Layout>
<ApprovedResellersPage />
</Layout>
</ProtectedRoute>
} />
<Route path="/deals" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<DealsPage />
</Layout>
<Layout>
<DealsPage />
</Layout>
</ProtectedRoute>
} />
<Route path="/commissions" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<CommissionsPage />
</Layout>
<Layout>
<CommissionsPage />
</Layout>
</ProtectedRoute>
} />
<Route path="/product-management" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<ProductManagement />
</Layout>
<Layout>
<ProductManagement />
</Layout>
</ProtectedRoute>
} />
<Route path="/training" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Training />
</Layout>
<Layout>
<Training />
</Layout>
</ProtectedRoute>
} />
<Route path="/support" element={
<Route path="/support" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Support />
</Layout>
<Layout>
<Support />
</Layout>
</ProtectedRoute>
} />
<Route path="/analytics" element={
} />
<Route path="/analytics" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Analytics />
</Layout>
<Layout>
<Analytics />
</Layout>
</ProtectedRoute>
} />
<Route path="/reports" element={
} />
<Route path="/reports" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Reports />
</Layout>
<Layout>
<Reports />
</Layout>
</ProtectedRoute>
} />
<Route path="/settings" element={
} />
<Route path="/settings" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<Settings />
</Layout>
<Layout>
<Settings />
</Layout>
</ProtectedRoute>
} />
<Route path="/targets" element={
} />
<Route path="/targets" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<PlaceholderPage title="Targets" description="Set and track performance targets" />
</Layout>
<Layout>
<PlaceholderPage title="Targets" description="Set and track performance targets" />
</Layout>
</ProtectedRoute>
} />
<Route path="/performance" element={
} />
<Route path="/performance" element={
<ProtectedRoute requiredRole="channel_partner_admin">
<Layout>
<PlaceholderPage title="Performance" description="Performance metrics and KPIs" />
</Layout>
<Layout>
<PlaceholderPage title="Performance" description="Performance metrics and KPIs" />
</Layout>
</ProtectedRoute>
} />
} />
<Route path="/marketplace" element={
<ProtectedRoute>
<Layout>
<PlaceholderPage title="Marketplace" description="Browse and manage marketplace offerings" />
</Layout>
<Layout>
<PlaceholderPage title="Marketplace" description="Browse and manage marketplace offerings" />
</Layout>
</ProtectedRoute>
} />
<Route path="/certifications" element={
<ProtectedRoute>
<Layout>
<PlaceholderPage title="Certifications" description="Manage certifications and badges" />
</Layout>
<Layout>
<PlaceholderPage title="Certifications" description="Manage certifications and badges" />
</Layout>
</ProtectedRoute>
} />
<Route path="/knowledge-base" element={
<ProtectedRoute>
<Layout>
<PlaceholderPage title="Knowledge Base" description="Access documentation and resources" />
</Layout>
<Layout>
<PlaceholderPage title="Knowledge Base" description="Access documentation and resources" />
</Layout>
</ProtectedRoute>
} />
@ -258,44 +301,51 @@ function App() {
{/* Reseller Dashboard Routes (Separate Service) */}
<Route path="/reseller-dashboard" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<ResellerDashboardMain />
</ResellerLayout>
<ResellerLayout>
<ResellerDashboardMain />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/customers" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<ResellerDashboardCustomers />
</ResellerLayout>
<ResellerLayout>
<ResellerDashboardCustomers />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/instances" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<ResellerDashboardInstances />
</ResellerLayout>
<ResellerLayout>
<ResellerDashboardInstances />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/billing" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Billing & Payments" description="Manage your billing, invoices, and payment methods" />
</ResellerLayout>
<ResellerLayout>
<PlaceholderPage title="Billing & Payments" description="Manage your billing, invoices, and payment methods" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/support" element={
<Route path="/reseller-dashboard/support" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<Support />
</ResellerLayout>
<ResellerLayout>
<Support />
</ResellerLayout>
</ProtectedRoute>
} />
} />
<Route path="/reseller-dashboard/reports" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Reports & Analytics" description="View detailed reports and analytics" />
</ResellerLayout>
<ResellerLayout>
<PlaceholderPage title="Reports & Analytics" description="View detailed reports and analytics" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/receipts" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<Receipts />
</ResellerLayout>
</ProtectedRoute>
} />
@ -328,46 +378,95 @@ function App() {
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/admin/products" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<Products />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/admin/analytics" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<AdminAnalytics />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/admin/reports" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<AdminReports />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/admin/settings" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<AdminSettings />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/admin/feedback" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<AdminFeedback />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/admin/registered-vendors" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<RegisteredVendors />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/admin/resellers" element={
<ProtectedRoute requiredRole="system_admin">
<AdminLayout>
<Resellers />
</AdminLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/wallet" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Wallet Management" description="Manage your funds, transactions, and payment methods" />
</ResellerLayout>
<ResellerLayout>
<PlaceholderPage title="Wallet Management" description="Manage your funds, transactions, and payment methods" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/training" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<ResellerTraining />
</ResellerLayout>
<ResellerLayout>
<ResellerTraining />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/marketplace" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Marketplace" description="Browse and purchase cloud services and solutions" />
</ResellerLayout>
<ResellerLayout>
<PlaceholderPage title="Marketplace" description="Browse and purchase cloud services and solutions" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/certifications" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Certifications" description="Manage your professional certifications and achievements" />
</ResellerLayout>
<ResellerLayout>
<PlaceholderPage title="Certifications" description="Manage your professional certifications and achievements" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/knowledge-base" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Knowledge Base" description="Access documentation, guides, and helpful resources" />
</ResellerLayout>
<ResellerLayout>
<PlaceholderPage title="Knowledge Base" description="Access documentation, guides, and helpful resources" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller-dashboard/settings" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Settings" description="Configure your account preferences and system settings" />
</ResellerLayout>
<ResellerLayout>
<PlaceholderPage title="Settings" description="Configure your account preferences and system settings" />
</ResellerLayout>
</ProtectedRoute>
} />
<Route path="/reseller/wallet" element={
@ -401,13 +500,15 @@ function App() {
</Layout>
} />
{/* Default Route - Redirect to login */}
<Route path="*" element={<Navigate to="/login" replace />} />
{/* Default Route - Redirect based on user role */}
<Route path="*" element={<RoleBasedRedirect />} />
</Routes>
<CookieConsent />
<Toast />
</div>
</Router>
<CookieConsent />
<Toast />
<AuthDebug />
<DeveloperFeedback />
</div>
</Router>
</AuthInitializer>
</CookiesProvider>
</Provider>

View File

@ -0,0 +1,163 @@
import React from 'react';
import { Clock, AlertCircle, Mail, Phone, X } from 'lucide-react';
interface ApprovalStatusModalProps {
isOpen: boolean;
onClose: () => void;
userEmail?: string;
companyName?: string;
}
const ApprovalStatusModal: React.FC<ApprovalStatusModalProps> = ({
isOpen,
onClose,
userEmail,
companyName
}) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-md w-full">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center">
<Clock className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
</div>
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Account Under Review
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
Your registration is being processed
</p>
</div>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X className="h-6 w-6" />
</button>
</div>
{/* Content */}
<div className="p-6">
<div className="mb-6">
<div className="flex items-start space-x-3 mb-4">
<AlertCircle className="h-5 w-5 text-blue-600 mt-0.5" />
<div>
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
Thank you for your registration!
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">
Your account is currently under review by our administrative team.
This process typically takes 1-3 business days. We'll notify you
via email once your account has been approved.
</p>
</div>
</div>
{/* Account Details */}
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 mb-6">
<h4 className="font-medium text-gray-900 dark:text-white mb-3">
Registration Details
</h4>
<div className="space-y-2 text-sm">
{companyName && (
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Company:</span>
<span className="font-medium text-gray-900 dark:text-white">{companyName}</span>
</div>
)}
{userEmail && (
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Email:</span>
<span className="font-medium text-gray-900 dark:text-white">{userEmail}</span>
</div>
)}
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Status:</span>
<span className="font-medium text-yellow-600">Under Review</span>
</div>
</div>
</div>
{/* What happens next */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 dark:text-white mb-3">
What happens next?
</h4>
<div className="space-y-3">
<div className="flex items-start space-x-3">
<div className="w-6 h-6 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-xs font-medium text-blue-600 dark:text-blue-400">1</span>
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">
Our team reviews your business information and documentation
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="w-6 h-6 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-xs font-medium text-blue-600 dark:text-blue-400">2</span>
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">
We verify your business credentials and compliance requirements
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="w-6 h-6 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-xs font-medium text-blue-600 dark:text-blue-400">3</span>
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">
You'll receive an email notification once approved
</p>
</div>
</div>
</div>
</div>
{/* Contact Information */}
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<h4 className="font-medium text-gray-900 dark:text-white mb-3">
Need help? Contact our support team
</h4>
<div className="space-y-2 text-sm">
<div className="flex items-center space-x-2">
<Mail className="h-4 w-4 text-blue-600" />
<span className="text-gray-600 dark:text-gray-400">
support@cloutopiaa.com
</span>
</div>
<div className="flex items-center space-x-2">
<Phone className="h-4 w-4 text-blue-600" />
<span className="text-gray-600 dark:text-gray-400">
+1 (555) 123-4567
</span>
</div>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="flex justify-end p-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={onClose}
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
Close
</button>
</div>
</div>
</div>
);
};
export default ApprovalStatusModal;

View File

@ -0,0 +1,50 @@
import React from 'react';
import { useAppSelector } from '../store/hooks';
import { testAuthPersistence } from '../utils/authTest';
const AuthDebug: React.FC = () => {
const { isAuthenticated, user, isLoading, token, refreshToken, sessionId } = useAppSelector((state) => state.auth);
const handleTestAuth = () => {
testAuthPersistence();
};
const handleClearAuth = () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('sessionId');
window.location.reload();
};
return (
<div className="fixed bottom-4 right-4 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-4 shadow-lg z-50 max-w-sm">
<h3 className="text-sm font-semibold text-slate-900 dark:text-white mb-2">Auth Debug</h3>
<div className="space-y-1 text-xs">
<div>Authenticated: <span className={isAuthenticated ? 'text-green-600' : 'text-red-600'}>{isAuthenticated ? 'Yes' : 'No'}</span></div>
<div>Loading: <span className={isLoading ? 'text-yellow-600' : 'text-gray-600'}>{isLoading ? 'Yes' : 'No'}</span></div>
<div>User: <span className={user ? 'text-green-600' : 'text-red-600'}>{user ? 'Loaded' : 'None'}</span></div>
<div>Token: <span className={token ? 'text-green-600' : 'text-red-600'}>{token ? 'Present' : 'Missing'}</span></div>
<div>Refresh Token: <span className={refreshToken ? 'text-green-600' : 'text-red-600'}>{refreshToken ? 'Present' : 'Missing'}</span></div>
<div>Session ID: <span className={sessionId ? 'text-green-600' : 'text-red-600'}>{sessionId ? 'Present' : 'Missing'}</span></div>
</div>
<div className="mt-3 space-y-1">
<button
onClick={handleTestAuth}
className="w-full px-2 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
>
Test Auth
</button>
<button
onClick={handleClearAuth}
className="w-full px-2 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700"
>
Clear Auth
</button>
</div>
</div>
);
};
export default AuthDebug;

View File

@ -1,11 +1,12 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import { getCurrentUser, refreshUserToken } from '../store/slices/authThunks';
import { setTokens } from '../store/slices/authSlice';
import { setTokens, logout } from '../store/slices/authSlice';
const AuthInitializer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const dispatch = useAppDispatch();
const { isAuthenticated, user } = useAppSelector((state) => state.auth);
const { isAuthenticated, user, isLoading } = useAppSelector((state) => state.auth);
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
const initializeAuth = async () => {
@ -26,29 +27,52 @@ const AuthInitializer: React.FC<{ children: React.ReactNode }> = ({ children })
// Try to get current user
try {
await dispatch(getCurrentUser()).unwrap();
console.log('User loaded successfully');
} catch (error) {
console.log('Failed to get current user, trying to refresh token...', error);
// If getting user fails, try to refresh token
console.log('Failed to get current user, trying to refresh token...');
try {
await dispatch(refreshUserToken()).unwrap();
// Try to get user again after token refresh
await dispatch(getCurrentUser()).unwrap();
console.log('User loaded successfully after token refresh');
} catch (refreshError) {
console.log('Token refresh failed, clearing auth data...');
// Clear invalid tokens
console.log('Token refresh failed, clearing auth data...', refreshError);
// Clear invalid tokens and logout
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('sessionId');
dispatch(logout());
}
}
} else {
console.log('No tokens found in localStorage');
}
} catch (error) {
console.error('Auth initialization error:', error);
// Clear any invalid tokens
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('sessionId');
dispatch(logout());
} finally {
setIsInitialized(true);
}
};
initializeAuth();
}, [dispatch]);
if (!isInitialized) {
initializeAuth();
}
}, [dispatch, isInitialized]);
// Show loading while initializing authentication
if (!isInitialized || (isAuthenticated && !user && isLoading)) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
</div>
);
}
return <>{children}</>;
};

View File

@ -0,0 +1,197 @@
import React from 'react';
import { X, AlertTriangle, Mail, Phone, MessageCircle, Clock, UserCheck, ExternalLink, RefreshCw } from 'lucide-react';
import { cn } from '../utils/cn';
interface BlockedAccountModalProps {
isOpen: boolean;
onClose: () => void;
userEmail: string;
userStatus: string;
}
const BlockedAccountModal: React.FC<BlockedAccountModalProps> = ({
isOpen,
onClose,
userEmail,
userStatus
}) => {
if (!isOpen) return null;
const getStatusMessage = () => {
switch (userStatus) {
case 'inactive':
return {
title: 'Account Deactivated',
message: 'Your account has been deactivated by the administrator. This usually happens when your account has been inactive for an extended period or when requested by you.',
icon: <AlertTriangle className="w-12 h-12 text-red-500" />,
color: 'red',
bgColor: 'bg-red-50 dark:bg-red-900/20',
borderColor: 'border-red-200 dark:border-red-800',
textColor: 'text-red-800 dark:text-red-200',
actionMessage: 'To reactivate your account, please contact your vendor administrator.'
};
case 'suspended':
return {
title: 'Account Suspended',
message: 'Your account has been suspended due to policy violations or security concerns. This is a temporary measure to protect our platform and users.',
icon: <AlertTriangle className="w-12 h-12 text-orange-500" />,
color: 'orange',
bgColor: 'bg-orange-50 dark:bg-orange-900/20',
borderColor: 'border-orange-200 dark:border-orange-800',
textColor: 'text-orange-800 dark:text-orange-200',
actionMessage: 'Please contact your vendor administrator to understand the reason and resolve the issue.'
};
case 'pending':
return {
title: 'Account Pending Approval',
message: 'Your account is currently pending approval from your vendor administrator. This is normal for new registrations and helps maintain platform security.',
icon: <Clock className="w-12 h-12 text-yellow-500" />,
color: 'yellow',
bgColor: 'bg-yellow-50 dark:bg-yellow-900/20',
borderColor: 'border-yellow-200 dark:border-yellow-800',
textColor: 'text-yellow-800 dark:text-yellow-200',
actionMessage: 'You will receive an email notification once your account is approved by your vendor administrator.'
};
default:
return {
title: 'Account Blocked',
message: 'Your account is currently blocked and cannot be used to access the system.',
icon: <AlertTriangle className="w-12 h-12 text-gray-500" />,
color: 'gray',
bgColor: 'bg-gray-50 dark:bg-gray-900/20',
borderColor: 'border-gray-200 dark:border-gray-800',
textColor: 'text-gray-800 dark:text-gray-200',
actionMessage: 'Please contact your vendor administrator to resolve this issue.'
};
}
};
const statusInfo = getStatusMessage();
const handleContactVendor = () => {
const subject = encodeURIComponent(`Account Status Inquiry - ${statusInfo.title}`);
const body = encodeURIComponent(
`Hello Vendor Administrator,\n\nI am inquiring about my account status.\n\nEmail: ${userEmail}\nCurrent Status: ${userStatus}\n\nPlease let me know what additional information you need to resolve this issue.\n\nThank you.`
);
window.open(`mailto:admin@cloudtopiaa.com?subject=${subject}&body=${body}`, '_blank');
};
const handleContactSupport = () => {
const subject = encodeURIComponent(`Account Status Support - ${statusInfo.title}`);
const body = encodeURIComponent(
`Hello Support Team,\n\nI need assistance with my account status.\n\nEmail: ${userEmail}\nCurrent Status: ${userStatus}\n\nPlease help me understand what I need to do to resolve this.\n\nThank you.`
);
window.open(`mailto:support@cloudtopiaa.com?subject=${subject}&body=${body}`, '_blank');
};
const handleRefreshPage = () => {
window.location.reload();
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-3">
{statusInfo.icon}
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{statusInfo.title}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Access Restricted
</p>
</div>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Content */}
<div className="p-6">
<div className={cn(
"p-4 rounded-lg border",
statusInfo.bgColor,
statusInfo.borderColor
)}>
<p className={cn("text-sm", statusInfo.textColor)}>
{statusInfo.message}
</p>
</div>
<div className="mt-6">
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
What you need to do:
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{statusInfo.actionMessage}
</p>
<div className="space-y-3">
<button
onClick={handleContactVendor}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Mail className="w-4 h-4" />
Contact Vendor Administrator
</button>
<button
onClick={handleContactSupport}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
<MessageCircle className="w-4 h-4" />
Contact Support
</button>
<button
onClick={handleRefreshPage}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors"
>
<RefreshCw className="w-4 h-4" />
Refresh Page
</button>
</div>
</div>
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-2">
Need immediate assistance?
</h4>
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-2">
<Phone className="w-4 h-4" />
<span>Call: +1 (555) 123-4567</span>
</div>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4" />
<span>Email: support@cloudtopiaa.com</span>
</div>
<div className="flex items-center gap-2">
<ExternalLink className="w-4 h-4" />
<span>Support Portal: support.cloudtopiaa.com</span>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={onClose}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
Close
</button>
</div>
</div>
</div>
);
};
export default BlockedAccountModal;

View File

@ -36,7 +36,7 @@ const CookieConsent: React.FC = () => {
if (!isVisible) return null;
return (
<div className="fixed inset-0 z-50 flex items-end justify-center p-4 sm:items-center sm:p-6">
<div className="fixed inset-0 z-[9999] flex items-end justify-center p-4 sm:items-center sm:p-6" style={{ backdropFilter: 'blur(4px)' }}>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-300"

View File

@ -0,0 +1,334 @@
import React, { useState } from 'react';
import {
MessageSquare,
Send,
X,
AlertCircle,
CheckCircle,
Clock,
Bug,
Lightbulb,
HelpCircle,
Star
} from 'lucide-react';
interface FeedbackTicket {
id: string;
type: 'bug' | 'feature' | 'question' | 'general';
title: string;
description: string;
priority: 'low' | 'medium' | 'high' | 'critical';
status: 'open' | 'in-progress' | 'resolved' | 'closed';
createdAt: string;
userAgent: string;
url: string;
submittedBy?: string;
rating?: number;
}
const DeveloperFeedback: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [tickets, setTickets] = useState<FeedbackTicket[]>([]);
const [currentTicket, setCurrentTicket] = useState<Partial<FeedbackTicket>>({
type: 'general',
priority: 'medium',
title: '',
description: ''
});
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const ticketTypes = [
{ id: 'bug', label: 'Bug Report', icon: Bug, color: 'text-red-600' },
{ id: 'feature', label: 'Feature Request', icon: Lightbulb, color: 'text-blue-600' },
{ id: 'question', label: 'Question', icon: HelpCircle, color: 'text-green-600' },
{ id: 'general', label: 'General Feedback', icon: Star, color: 'text-yellow-600' }
];
const priorities = [
{ id: 'low', label: 'Low', color: 'text-gray-600' },
{ id: 'medium', label: 'Medium', color: 'text-yellow-600' },
{ id: 'high', label: 'High', color: 'text-orange-600' },
{ id: 'critical', label: 'Critical', color: 'text-red-600' }
];
const handleSubmit = async () => {
if (!currentTicket.title || !currentTicket.description) {
alert('Please fill in all required fields');
return;
}
setSubmitting(true);
try {
const newTicket: FeedbackTicket = {
id: Date.now().toString(),
type: currentTicket.type as 'bug' | 'feature' | 'question' | 'general',
title: currentTicket.title,
description: currentTicket.description,
priority: currentTicket.priority as 'low' | 'medium' | 'high' | 'critical',
status: 'open',
createdAt: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
submittedBy: 'Developer' // This will be overridden by backend with actual user info
};
// API call to submit feedback
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/developer-feedback`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
},
body: JSON.stringify({
type: newTicket.type,
title: newTicket.title,
description: newTicket.description,
priority: newTicket.priority,
userAgent: newTicket.userAgent,
url: newTicket.url,
submittedBy: newTicket.submittedBy
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP ${response.status}: Failed to submit feedback`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || 'Failed to submit feedback');
}
setTickets(prev => [newTicket, ...prev]);
setCurrentTicket({ type: 'general', priority: 'medium', title: '', description: '' });
setSubmitted(true);
setTimeout(() => setSubmitted(false), 3000);
} catch (error) {
console.error('Error submitting feedback:', error);
alert('Failed to submit feedback. Please try again.');
} finally {
setSubmitting(false);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'open': return 'text-blue-600';
case 'in-progress': return 'text-yellow-600';
case 'resolved': return 'text-green-600';
case 'closed': return 'text-gray-600';
default: return 'text-gray-600';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'open': return <Clock className="h-4 w-4" />;
case 'in-progress': return <AlertCircle className="h-4 w-4" />;
case 'resolved': return <CheckCircle className="h-4 w-4" />;
case 'closed': return <X className="h-4 w-4" />;
default: return <Clock className="h-4 w-4" />;
}
};
return (
<>
{/* Floating Button */}
<button
onClick={() => setIsOpen(true)}
className="fixed bottom-20 right-4 bg-blue-600 hover:bg-blue-700 text-white rounded-full p-3 shadow-lg z-[9998] transition-all duration-200"
title="Developer Feedback"
>
<MessageSquare className="h-6 w-6" />
</button>
{/* Modal */}
{isOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<style>
{`
.dev-feedback-scrollbar-hide::-webkit-scrollbar {
display: none;
}
.dev-feedback-scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
`}
</style>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto dev-feedback-scrollbar-hide">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Developer Feedback
</h2>
<button
onClick={() => setIsOpen(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X className="h-6 w-6" />
</button>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Success Message */}
{submitted && (
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-4">
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400 mr-2" />
<span className="text-green-800 dark:text-green-200">
Feedback submitted successfully! Thank you for your input.
</span>
</div>
</div>
)}
{/* Ticket Type Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Ticket Type
</label>
<div className="grid grid-cols-2 gap-3">
{ticketTypes.map((type) => {
const Icon = type.icon;
return (
<button
key={type.id}
onClick={() => setCurrentTicket(prev => ({ ...prev, type: type.id as any }))}
className={`p-3 border rounded-lg text-left transition-colors ${
currentTicket.type === type.id
? 'border-blue-500 bg-blue-50 dark:bg-blue-900'
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'
}`}
>
<div className="flex items-center space-x-2">
<Icon className={`h-5 w-5 ${type.color}`} />
<span className="text-sm font-medium">{type.label}</span>
</div>
</button>
);
})}
</div>
</div>
{/* Priority Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Priority
</label>
<div className="flex space-x-3">
{priorities.map((priority) => (
<button
key={priority.id}
onClick={() => setCurrentTicket(prev => ({ ...prev, priority: priority.id as any }))}
className={`px-3 py-2 border rounded-md text-sm transition-colors ${
currentTicket.priority === priority.id
? 'border-blue-500 bg-blue-50 dark:bg-blue-900'
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'
}`}
>
<span className={priority.color}>{priority.label}</span>
</button>
))}
</div>
</div>
{/* Title */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Title *
</label>
<input
type="text"
value={currentTicket.title}
onChange={(e) => setCurrentTicket(prev => ({ ...prev, title: e.target.value }))}
placeholder="Brief description of the issue or request"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700"
/>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description *
</label>
<textarea
value={currentTicket.description}
onChange={(e) => setCurrentTicket(prev => ({ ...prev, description: e.target.value }))}
placeholder="Please provide detailed information about the issue, feature request, or feedback..."
rows={6}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 resize-none"
/>
</div>
{/* System Info */}
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
System Information (Auto-collected)
</h4>
<div className="text-xs text-gray-600 dark:text-gray-400 space-y-1">
<div>URL: {window.location.href}</div>
<div>User Agent: {navigator.userAgent.substring(0, 100)}...</div>
<div>Timestamp: {new Date().toLocaleString()}</div>
</div>
</div>
{/* Submit Button */}
<div className="flex justify-end space-x-3">
<button
onClick={() => setIsOpen(false)}
className="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={submitting || !currentTicket.title || !currentTicket.description}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
>
{submitting ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
) : (
<Send className="h-4 w-4" />
)}
<span>{submitting ? 'Submitting...' : 'Submit Feedback'}</span>
</button>
</div>
</div>
</div>
</div>
)}
{/* Recent Tickets (if any) */}
{tickets.length > 0 && (
<div className="fixed bottom-20 left-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 max-w-sm z-40">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
Recent Tickets ({tickets.length})
</h3>
<div className="space-y-2 max-h-32 overflow-y-auto">
{tickets.slice(0, 3).map((ticket) => (
<div key={ticket.id} className="text-xs">
<div className="flex items-center justify-between">
<span className="font-medium truncate">{ticket.title}</span>
<div className={`flex items-center ${getStatusColor(ticket.status)}`}>
{getStatusIcon(ticket.status)}
</div>
</div>
<div className="text-gray-500 dark:text-gray-400">
{new Date(ticket.createdAt).toLocaleDateString()}
</div>
</div>
))}
</div>
</div>
)}
</>
);
};
export default DeveloperFeedback;

View File

@ -0,0 +1,383 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { MessageCircle, X, Minimize2, Maximize2, Move } from 'lucide-react';
interface DraggableFeedbackProps {
onClose?: () => void;
}
const DraggableFeedback: React.FC<DraggableFeedbackProps> = ({
onClose
}) => {
const [position, setPosition] = useState({ x: 20, y: 20 });
const [isDragging, setIsDragging] = useState(false);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const [feedback, setFeedback] = useState('');
const [email, setEmail] = useState('');
const [rating, setRating] = useState(0);
const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [isMinimized, setIsMinimized] = useState(false);
// Reset form when component mounts or when user navigates
useEffect(() => {
const resetForm = () => {
setFeedback('');
setEmail('');
setRating(0);
setIsSubmitted(false);
};
// Reset on mount
resetForm();
// Reset when user navigates (page visibility change)
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
resetForm();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
const feedbackRef = useRef<HTMLDivElement>(null);
// Enhanced mouse move handler with no viewport constraints
const handleMouseMove = useCallback((e: MouseEvent) => {
if (isDragging && feedbackRef.current) {
const newX = e.clientX - dragOffset.x;
const newY = e.clientY - dragOffset.y;
// Allow positioning anywhere on screen - no viewport bounds
setPosition({
x: newX,
y: newY
});
}
}, [isDragging, dragOffset]);
const handleMouseUp = useCallback(() => {
setIsDragging(false);
}, []);
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
// Prevent text selection while dragging
document.body.style.userSelect = 'none';
document.body.style.cursor = 'grabbing';
} else {
document.body.style.userSelect = '';
document.body.style.cursor = '';
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.userSelect = '';
document.body.style.cursor = '';
};
}, [isDragging, handleMouseMove, handleMouseUp]);
const handleMouseDown = (e: React.MouseEvent) => {
// Only allow dragging from the header or drag handle
if (feedbackRef.current && (e.target as HTMLElement).closest('[data-draggable]')) {
const rect = feedbackRef.current.getBoundingClientRect();
setDragOffset({
x: e.clientX - rect.left,
y: e.clientY - rect.top
});
setIsDragging(true);
e.preventDefault(); // Prevent text selection
}
};
// Touch event handlers for mobile devices
const handleTouchStart = (e: React.TouchEvent) => {
if (feedbackRef.current && (e.target as HTMLElement).closest('[data-draggable]')) {
const touch = e.touches[0];
const rect = feedbackRef.current.getBoundingClientRect();
setDragOffset({
x: touch.clientX - rect.left,
y: touch.clientY - rect.top
});
setIsDragging(true);
}
};
const handleTouchMove = useCallback((e: TouchEvent) => {
if (isDragging && feedbackRef.current) {
e.preventDefault(); // Prevent scrolling while dragging
const touch = e.touches[0];
const newX = touch.clientX - dragOffset.x;
const newY = touch.clientY - dragOffset.y;
setPosition({
x: newX,
y: newY
});
}
}, [isDragging, dragOffset]);
const handleTouchEnd = useCallback(() => {
setIsDragging(false);
}, []);
useEffect(() => {
if (isDragging) {
document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('touchend', handleTouchEnd);
}
return () => {
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', handleTouchEnd);
};
}, [isDragging, handleTouchMove, handleTouchEnd]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!feedback.trim()) return;
setIsSubmitting(true);
setError('');
try {
// Here you would typically send the feedback to your backend
console.log('Feedback submitted:', { feedback, email, rating });
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
setIsSubmitted(true);
// Reset form after 2 seconds
setTimeout(() => {
setIsSubmitted(false);
setFeedback('');
setEmail('');
setRating(0);
setIsSubmitting(false);
}, 2000);
} catch (error) {
console.error('Error submitting feedback:', error);
setError('Failed to submit feedback. Please try again.');
setIsSubmitting(false);
}
};
// If minimized, show only the header
if (isMinimized) {
return (
<div
ref={feedbackRef}
style={{
position: 'fixed',
left: position.x,
top: position.y,
zIndex: 9999,
width: '300px'
}}
className="bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden"
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
>
<div
className="bg-emerald-600 text-white p-3 cursor-move flex items-center justify-between"
data-draggable="true"
>
<div className="flex items-center space-x-2">
<MessageCircle className="w-5 h-5" />
<span className="font-medium">Feedback</span>
</div>
<div className="flex items-center space-x-1">
<button
onClick={() => setIsMinimized(false)}
className="p-1 hover:bg-emerald-700 rounded transition-colors"
title="Expand"
>
<Maximize2 className="w-4 h-4" />
</button>
{onClose && (
<button
onClick={onClose}
className="p-1 hover:bg-emerald-700 rounded transition-colors"
title="Close"
>
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
</div>
);
}
return (
<>
<style>
{`
.feedback-scrollbar-hide::-webkit-scrollbar {
display: none;
}
.feedback-scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.feedback-dragging {
user-select: none;
cursor: grabbing !important;
}
`}
</style>
<div
ref={feedbackRef}
style={{
position: 'fixed',
left: position.x,
top: position.y,
zIndex: 9999,
width: '300px',
maxHeight: '80vh',
overflow: 'hidden'
}}
className={`bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden ${isDragging ? 'feedback-dragging' : ''}`}
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
>
{/* Header - Draggable area */}
<div
className="bg-emerald-600 text-white p-3 cursor-move flex items-center justify-between"
data-draggable="true"
>
<div className="flex items-center space-x-2">
<Move className="w-4 h-4 opacity-80" />
<MessageCircle className="w-5 h-5" />
<span className="font-medium">Feedback</span>
</div>
<div className="flex items-center space-x-1">
<button
onClick={() => setIsMinimized(true)}
className="p-1 hover:bg-emerald-700 rounded transition-colors"
title="Minimize"
>
<Minimize2 className="w-4 h-4" />
</button>
{onClose && (
<button
onClick={onClose}
className="p-1 hover:bg-emerald-700 rounded transition-colors"
title="Close"
>
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* Content */}
<div
className="p-4 overflow-y-auto feedback-scrollbar-hide"
style={{
maxHeight: 'calc(80vh - 60px)'
}}
>
{isSubmitted ? (
<div className="text-center py-4">
<div className="w-12 h-12 bg-emerald-100 dark:bg-emerald-900 rounded-full flex items-center justify-center mx-auto mb-3">
<MessageCircle className="w-6 h-6 text-emerald-600" />
</div>
<h3 className="font-medium text-slate-900 dark:text-white mb-1">Thank you!</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
Your feedback has been submitted successfully.
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Error Message */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
<p className="text-red-800 dark:text-red-200 text-sm">{error}</p>
</div>
)}
{/* Rating */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
How would you rate your experience?
</label>
<div className="flex space-x-1">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => setRating(star)}
className={`text-2xl ${
star <= rating ? 'text-yellow-400' : 'text-slate-300'
} hover:text-yellow-400 transition-colors`}
>
</button>
))}
</div>
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Email (optional)
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-md bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
placeholder="your@email.com (optional)"
/>
</div>
{/* Feedback */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Your feedback
</label>
<textarea
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
rows={4}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-md bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent resize-none"
placeholder="Tell us what you think..."
required
/>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={!feedback.trim() || isSubmitting}
className="w-full bg-emerald-600 hover:bg-emerald-700 disabled:bg-slate-300 text-white font-medium py-2 px-4 rounded-md transition-colors disabled:cursor-not-allowed flex items-center justify-center"
>
{isSubmitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Submitting...
</>
) : (
'Submit Feedback'
)}
</button>
</form>
)}
</div>
</div>
</>
);
};
export default DraggableFeedback;

View File

@ -0,0 +1,234 @@
import React from 'react';
import { X, AlertTriangle, Mail, Phone, MessageCircle, Clock, UserCheck, ExternalLink, RefreshCw } from 'lucide-react';
import { cn } from '../utils/cn';
interface InactiveUserModalProps {
isOpen: boolean;
onClose: () => void;
userEmail: string;
userStatus: string;
}
const InactiveUserModal: React.FC<InactiveUserModalProps> = ({
isOpen,
onClose,
userEmail,
userStatus
}) => {
if (!isOpen) return null;
const getStatusMessage = () => {
switch (userStatus) {
case 'inactive':
return {
title: 'Account Deactivated',
message: 'Your account has been deactivated by the administrator. This usually happens when your account has been inactive for an extended period or when requested by you.',
icon: <AlertTriangle className="w-12 h-12 text-red-500" />,
color: 'red',
bgColor: 'bg-red-50 dark:bg-red-900/20',
borderColor: 'border-red-200 dark:border-red-800',
textColor: 'text-red-800 dark:text-red-200',
actionMessage: 'To reactivate your account, please contact the administrator with your request.'
};
case 'suspended':
return {
title: 'Account Suspended',
message: 'Your account has been suspended due to policy violations or security concerns. This is a temporary measure to protect our platform and users.',
icon: <AlertTriangle className="w-12 h-12 text-orange-500" />,
color: 'orange',
bgColor: 'bg-orange-50 dark:bg-orange-900/20',
borderColor: 'border-orange-200 dark:border-orange-800',
textColor: 'text-orange-800 dark:text-orange-200',
actionMessage: 'Please contact support to understand the reason and resolve the issue.'
};
case 'pending':
return {
title: 'Account Pending Approval',
message: 'Your account is currently pending approval from the administrator. This is normal for new registrations and helps us maintain platform security.',
icon: <Clock className="w-12 h-12 text-yellow-500" />,
color: 'yellow',
bgColor: 'bg-yellow-50 dark:bg-yellow-900/20',
borderColor: 'border-yellow-200 dark:border-yellow-800',
textColor: 'text-yellow-800 dark:text-yellow-200',
actionMessage: 'You will receive an email notification once your account is approved.'
};
default:
return {
title: 'Account Inactive',
message: 'Your account is currently inactive and cannot be used to access the system.',
icon: <AlertTriangle className="w-12 h-12 text-gray-500" />,
color: 'gray',
bgColor: 'bg-gray-50 dark:bg-gray-900/20',
borderColor: 'border-gray-200 dark:border-gray-800',
textColor: 'text-gray-800 dark:text-gray-200',
actionMessage: 'Please contact the administrator to resolve this issue.'
};
}
};
const statusInfo = getStatusMessage();
const handleContactAdmin = () => {
const subject = encodeURIComponent(`Account Status Inquiry - ${statusInfo.title}`);
const body = encodeURIComponent(
`Hello,\n\nI am inquiring about my account status.\n\nEmail: ${userEmail}\nCurrent Status: ${userStatus}\n\nPlease let me know what additional information you need to resolve this issue.\n\nThank you.`
);
window.open(`mailto:admin@cloudtopiaa.com?subject=${subject}&body=${body}`, '_blank');
};
const handleContactSupport = () => {
const subject = encodeURIComponent(`Account Status Support - ${statusInfo.title}`);
const body = encodeURIComponent(
`Hello Support Team,\n\nI need assistance with my account status.\n\nEmail: ${userEmail}\nCurrent Status: ${userStatus}\n\nPlease help me understand what I need to do to resolve this.\n\nThank you.`
);
window.open(`mailto:support@cloudtopiaa.com?subject=${subject}&body=${body}`, '_blank');
};
const handleRefreshPage = () => {
window.location.reload();
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-3">
{statusInfo.icon}
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{statusInfo.title}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Account: {userEmail}
</p>
</div>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700"
title="Close"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-6">
{/* Status Message */}
<div className={cn(
"p-4 rounded-lg mb-6",
statusInfo.bgColor,
statusInfo.borderColor
)}>
<p className={cn("text-sm leading-relaxed", statusInfo.textColor)}>
{statusInfo.message}
</p>
</div>
{/* Action Message */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2 flex items-center">
<UserCheck className="w-4 h-4 mr-2" />
What You Can Do
</h4>
<p className="text-sm text-blue-800 dark:text-blue-200">
{statusInfo.actionMessage}
</p>
</div>
{/* Contact Information */}
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 mb-6">
<h4 className="font-medium text-gray-900 dark:text-white mb-3">
Contact Information
</h4>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Mail className="w-4 h-4 text-gray-500" />
<span className="text-sm text-gray-600 dark:text-gray-400">
Administrator
</span>
</div>
<a
href="mailto:admin@cloudtopiaa.com"
className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 flex items-center"
>
admin@cloudtopiaa.com
<ExternalLink className="w-3 h-3 ml-1" />
</a>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<MessageCircle className="w-4 h-4 text-gray-500" />
<span className="text-sm text-gray-600 dark:text-gray-400">
Support Team
</span>
</div>
<a
href="mailto:support@cloudtopiaa.com"
className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 flex items-center"
>
support@cloudtopiaa.com
<ExternalLink className="w-3 h-3 ml-1" />
</a>
</div>
<div className="flex items-center space-x-3">
<Phone className="w-4 h-4 text-gray-500" />
<span className="text-sm text-gray-600 dark:text-gray-400">
Phone: +1 (555) 123-4567
</span>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={onClose}
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
Close
</button>
<button
onClick={handleContactAdmin}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center justify-center"
>
<Mail className="w-4 h-4 mr-2" />
Contact Admin
</button>
<button
onClick={handleContactSupport}
className="flex-1 px-4 py-2 bg-emerald-600 text-white rounded-md hover:bg-emerald-700 transition-colors flex items-center justify-center"
>
<MessageCircle className="w-4 h-4 mr-2" />
Support
</button>
</div>
{/* Additional Actions */}
<div className="mt-4 text-center">
<button
onClick={handleRefreshPage}
className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 flex items-center justify-center mx-auto hover:underline"
>
<RefreshCw className="w-3 h-3 mr-1" />
Refresh Page
</button>
</div>
{/* Help Text */}
<div className="mt-6 text-center">
<p className="text-xs text-gray-500 dark:text-gray-400">
If you believe this is an error, please contact our support team immediately.
</p>
</div>
</div>
</div>
</div>
);
};
export default InactiveUserModal;

View File

@ -1,5 +1,6 @@
import React from 'react';
import AdminSidebar from './AdminSidebar';
import NotificationBell from '../NotificationBell';
interface AdminLayoutProps {
children: React.ReactNode;
@ -38,11 +39,7 @@ const AdminLayout: React.FC<AdminLayoutProps> = ({ children }) => {
<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>
<NotificationBell />
{/* User Menu */}
<div className="flex items-center space-x-3">
@ -60,7 +57,7 @@ const AdminLayout: React.FC<AdminLayoutProps> = ({ children }) => {
</div>
{/* Page Content */}
<div className="flex-1 overflow-auto">
<div className="flex-1 overflow-auto min-h-0">
{children}
</div>
</div>

View File

@ -4,7 +4,7 @@ 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
Shield, BarChart3, FileText, UserCheck, UserX, TrendingUp, Activity, Package, MessageSquare
} from 'lucide-react';
import { logout } from '../../store/slices/authSlice';
import { cn } from '../../utils/cn';
@ -12,10 +12,13 @@ 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: 'Registered Vendors', href: '/admin/registered-vendors', icon: Building },
{ name: 'Resellers', href: '/admin/resellers', icon: Users },
{ name: 'System Users', href: '/admin/users', icon: Users },
{ name: 'Products', href: '/admin/products', icon: Package },
{ name: 'Analytics', href: '/admin/analytics', icon: BarChart3 },
{ name: 'Reports', href: '/admin/reports', icon: FileText },
{ name: 'Feedback', href: '/admin/feedback', icon: MessageSquare },
{ name: 'Settings', href: '/admin/settings', icon: Settings },
];

View File

@ -1,4 +1,5 @@
import React from 'react';
import { useAppSelector } from '../../store/hooks';
import Sidebar from './Sidebar';
interface LayoutProps {
@ -6,14 +7,19 @@ interface LayoutProps {
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
const { user } = useAppSelector((state) => state.auth);
return (
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
<Sidebar />
<main className="flex-1 overflow-auto">
<div className="p-6">
{children}
</div>
</main>
<div className="flex-1 flex flex-col overflow-hidden">
{/* Main Content */}
<main className="flex-1 overflow-auto">
<div className="p-6">
{children}
</div>
</main>
</div>
</div>
);
};

View File

@ -22,7 +22,8 @@ import {
Building,
Target,
TrendingUp,
Package
Package,
FileText
} from 'lucide-react';
import { RootState } from '../../store';
import { toggleTheme } from '../../store/slices/themeSlice';
@ -36,6 +37,7 @@ const resellerNavigation = [
{ name: 'Billing', href: '/reseller-dashboard/billing', icon: CreditCard },
{ name: 'Support', href: '/reseller-dashboard/support', icon: Headphones },
{ name: 'Reports', href: '/reseller-dashboard/reports', icon: BarChart3 },
{ name: 'Receipts', href: '/reseller-dashboard/receipts', icon: FileText },
{ name: 'Wallet', href: '/reseller-dashboard/wallet', icon: Wallet },
{ name: 'Training', href: '/reseller-dashboard/training', icon: BookOpen },
{ name: 'Marketplace', href: '/reseller-dashboard/marketplace', icon: ShoppingBag },

View File

@ -35,9 +35,9 @@ import toast from 'react-hot-toast';
const navigation = [
{ name: 'Dashboard', href: '/', icon: Home },
{ name: 'Product Management', href: '/product-management', icon: Package },
{ name: 'Resellers', href: '/resellers', icon: Users },
{ name: 'Partnerships', href: '/partnerships', icon: Handshake },
{ name: 'Deals', href: '/deals', icon: Briefcase },
{ name: 'Reseller Requests', href: '/resellers', icon: Users },
{ name: 'Approved Resellers', href: '/approved-resellers', icon: Handshake },
// { name: 'Deals', href: '/deals', icon: Briefcase },
{ name: 'Commissions', href: '/commissions', icon: Wallet },
{ name: 'Training', href: '/training', icon: BookOpen },
{ name: 'Support', href: '/support', icon: Headphones },

View File

@ -39,11 +39,23 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, size =
};
return (
<div className="fixed inset-0 z-50 flex items-start justify-center p-4 pt-8 overflow-y-auto">
<div className="fixed inset-0 z-[9999] flex items-start justify-center p-4 pt-8 overflow-y-auto modal-scrollbar-hide">
<style>
{`
.modal-scrollbar-hide::-webkit-scrollbar {
display: none;
}
.modal-scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
`}
</style>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
style={{ backdropFilter: 'blur(4px)' }}
/>
{/* Modal */}
@ -65,7 +77,7 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, size =
</div>
{/* Content */}
<div className="p-6 overflow-y-auto max-h-[calc(100vh-200px)] scrollbar-thin">
<div className="p-6 overflow-y-auto max-h-[calc(100vh-200px)] modal-scrollbar-hide">
{children}
</div>
</div>

View File

@ -0,0 +1,91 @@
import React, { useState, useEffect } from 'react';
import { Bell } from 'lucide-react';
import { useAppSelector } from '../store/hooks';
import NotificationPanel from './NotificationPanel';
const NotificationBell: React.FC = () => {
const [unreadCount, setUnreadCount] = useState(0);
const [isPanelOpen, setIsPanelOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { user } = useAppSelector((state) => state.auth);
useEffect(() => {
fetchUnreadCount();
// Set up polling for new notifications
const interval = setInterval(fetchUnreadCount, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, [user]);
const fetchUnreadCount = async () => {
try {
setLoading(true);
// Determine the appropriate endpoint based on user role
let endpoint = '/admin/notifications/stats';
if (user?.role && user.role.startsWith('channel_partner_')) {
endpoint = '/vendors/notifications/stats';
} else if (user?.role && user.role.startsWith('reseller_')) {
endpoint = '/resellers/notifications/stats';
}
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setUnreadCount(data.data?.unreadNotifications || 0);
}
} catch (error) {
console.error('Error fetching notification count:', error);
} finally {
setLoading(false);
}
};
const handleBellClick = () => {
setIsPanelOpen(true);
};
const handlePanelClose = () => {
setIsPanelOpen(false);
// Refresh count when panel closes
fetchUnreadCount();
};
return (
<>
<div className="relative">
<button
onClick={handleBellClick}
className="relative p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
disabled={loading}
>
<Bell className="w-6 h-6" />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center min-w-[20px]">
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
{loading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
</div>
)}
</button>
</div>
<NotificationPanel
isOpen={isPanelOpen}
onClose={handlePanelClose}
/>
</>
);
};
export default NotificationBell;

View File

@ -0,0 +1,394 @@
import React, { useState, useEffect } from 'react';
import { Bell, X, Check, Trash2, Clock, AlertCircle, CheckCircle, XCircle, Users, UserPlus } from 'lucide-react';
import { useAppSelector } from '../store/hooks';
interface Notification {
id: string;
type: 'NEW_VENDOR_REQUEST' | 'NEW_RESELLER_REQUEST' | 'VENDOR_APPROVED' | 'VENDOR_REJECTED' | 'RESELLER_APPROVED' | 'RESELLER_REJECTED' | 'RESELLER_CREATED' | 'RESELLER_ACCOUNT_CREATED' | 'SYSTEM_ALERT' | 'GENERAL';
title: string;
message: string;
data?: any;
isRead: boolean;
priority: 'low' | 'medium' | 'high' | 'critical';
createdAt: string;
sender?: {
id: string;
firstName: string;
lastName: string;
email: string;
};
}
interface NotificationPanelProps {
isOpen: boolean;
onClose: () => void;
}
const NotificationPanel: React.FC<NotificationPanelProps> = ({ isOpen, onClose }) => {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState<'all' | 'unread'>('all');
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deletingNotificationId, setDeletingNotificationId] = useState<string | null>(null);
const { user } = useAppSelector((state) => state.auth);
useEffect(() => {
if (isOpen) {
fetchNotifications();
}
}, [isOpen, activeTab, user]);
const getApiEndpoint = () => {
if (user?.role && user.role.startsWith('channel_partner_')) {
return '/vendors/notifications';
} else if (user?.role && user.role.startsWith('reseller_')) {
return '/resellers/notifications';
} else {
return '/admin/notifications';
}
};
const getMarkAsReadEndpoint = (notificationId: string) => {
if (user?.role && user.role.startsWith('channel_partner_')) {
return `/vendors/notifications/${notificationId}/read`;
} else if (user?.role && user.role.startsWith('reseller_')) {
return `/resellers/notifications/${notificationId}/read`;
} else {
return `/admin/notifications/${notificationId}/read`;
}
};
const getMarkAllAsReadEndpoint = () => {
if (user?.role && user.role.startsWith('channel_partner_')) {
return '/vendors/notifications/read-all';
} else if (user?.role && user.role.startsWith('reseller_')) {
return '/resellers/notifications/read-all';
} else {
return '/admin/notifications/mark-all-read';
}
};
const fetchNotifications = async () => {
try {
setLoading(true);
const endpoint = getApiEndpoint();
const params = new URLSearchParams({
page: '1',
limit: '50',
...(activeTab === 'unread' && { unreadOnly: 'true' })
});
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}?${params}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setNotifications(data.data.notifications);
setUnreadCount(data.data.notifications.filter((n: Notification) => !n.isRead).length);
}
} catch (error) {
console.error('Error fetching notifications:', error);
} finally {
setLoading(false);
}
};
const markAsRead = async (notificationId: string) => {
try {
const endpoint = getMarkAsReadEndpoint(notificationId);
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
setUnreadCount(prev => Math.max(0, prev - 1));
setNotifications(prev =>
prev.map(notif =>
notif.id === notificationId
? { ...notif, isRead: true }
: notif
)
);
}
} catch (error) {
console.error('Error marking notification as read:', error);
}
};
const markAllAsRead = async () => {
try {
const endpoint = getMarkAllAsReadEndpoint();
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
setNotifications(prev => prev.map(notif => ({ ...notif, isRead: true })));
setUnreadCount(0);
}
} catch (error) {
console.error('Error marking all notifications as read:', error);
}
};
const deleteNotification = (notificationId: string) => {
setDeletingNotificationId(notificationId);
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
if (!deletingNotificationId) return;
try {
const endpoint = getApiEndpoint();
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}/${deletingNotificationId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
setNotifications(prev => prev.filter(notif => notif.id !== deletingNotificationId));
setIsDeleteModalOpen(false);
setDeletingNotificationId(null);
}
} catch (error) {
console.error('Error deleting notification:', error);
}
};
const getNotificationIcon = (type: string) => {
switch (type) {
case 'NEW_VENDOR_REQUEST':
return <AlertCircle className="w-5 h-5 text-blue-500" />;
case 'NEW_RESELLER_REQUEST':
return <AlertCircle className="w-5 h-5 text-green-500" />;
case 'VENDOR_APPROVED':
return <CheckCircle className="w-5 h-5 text-green-600" />;
case 'VENDOR_REJECTED':
return <XCircle className="w-5 h-5 text-red-600" />;
case 'RESELLER_APPROVED':
return <CheckCircle className="w-5 h-5 text-green-600" />;
case 'RESELLER_REJECTED':
return <XCircle className="w-5 h-5 text-red-600" />;
case 'RESELLER_CREATED':
return <Users className="w-5 h-5 text-blue-600" />;
case 'RESELLER_ACCOUNT_CREATED':
return <UserPlus className="w-5 h-5 text-green-600" />;
case 'SYSTEM_ALERT':
return <AlertCircle className="w-5 h-5 text-orange-500" />;
default:
return <Bell className="w-5 h-5 text-gray-500" />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'critical':
return 'border-red-500 bg-red-50';
case 'high':
return 'border-orange-500 bg-orange-50';
case 'medium':
return 'border-yellow-500 bg-yellow-50';
case 'low':
return 'border-gray-300 bg-gray-50';
default:
return 'border-gray-300 bg-gray-50';
}
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
if (diffInMinutes < 1) return 'Just now';
if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`;
return date.toLocaleDateString();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-start justify-center z-[9999] p-4 pt-8" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-2xl w-full max-h-[85vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-3">
<Bell className="w-6 h-6 text-gray-600 dark:text-gray-400" />
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Notifications
</h2>
{unreadCount > 0 && (
<span className="bg-red-500 text-white text-xs font-bold px-2 py-1 rounded-full">
{unreadCount}
</span>
)}
</div>
<div className="flex items-center space-x-2">
{unreadCount > 0 && (
<button
onClick={markAllAsRead}
className="text-sm text-blue-600 hover:text-blue-800 dark:hover:text-blue-400"
>
Mark all read
</button>
)}
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setActiveTab('all')}
className={`flex-1 py-3 px-4 text-sm font-medium ${
activeTab === 'all'
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
All
</button>
<button
onClick={() => setActiveTab('unread')}
className={`flex-1 py-3 px-4 text-sm font-medium ${
activeTab === 'unread'
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
Unread ({unreadCount})
</button>
</div>
{/* Notifications List */}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : notifications.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-gray-500 dark:text-gray-400">
<Bell className="w-12 h-12 mb-4 opacity-50" />
<p className="text-lg font-medium">No notifications</p>
<p className="text-sm">You're all caught up!</p>
</div>
) : (
notifications.map((notification) => (
<div
key={notification.id}
className={`p-4 rounded-lg border-l-4 ${getPriorityColor(notification.priority)} ${
!notification.isRead ? 'bg-blue-50 dark:bg-blue-900/20' : ''
}`}
>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 mt-1">
{getNotificationIcon(notification.type)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className={`text-sm font-medium ${
!notification.isRead
? 'text-gray-900 dark:text-white'
: 'text-gray-700 dark:text-gray-300'
}`}>
{notification.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{notification.message}
</p>
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
<span className="flex items-center">
<Clock className="w-3 h-3 mr-1" />
{formatTime(notification.createdAt)}
</span>
{notification.sender && (
<span>
From: {notification.sender.firstName} {notification.sender.lastName}
</span>
)}
</div>
</div>
<div className="flex items-center space-x-1 ml-2">
{!notification.isRead && (
<button
onClick={() => markAsRead(notification.id)}
className="p-1 text-gray-400 hover:text-green-600 dark:hover:text-green-400"
title="Mark as read"
>
<Check className="w-4 h-4" />
</button>
)}
<button
onClick={() => deleteNotification(notification.id)}
className="p-1 text-gray-400 hover:text-red-600 dark:hover:text-red-400"
title="Delete"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</div>
))
)}
</div>
</div>
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">Confirm Delete</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Are you sure you want to delete this notification? This action cannot be undone.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => {
setIsDeleteModalOpen(false);
setDeletingNotificationId(null);
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
Cancel
</button>
<button
onClick={confirmDelete}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Delete Notification
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default NotificationPanel;

View File

@ -1,7 +1,9 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAppSelector, useAppDispatch } from '../store/hooks';
import { getCurrentUser } from '../store/slices/authThunks';
import { logout } from '../store/slices/authSlice';
import BlockedAccountModal from './BlockedAccountModal';
interface ProtectedRouteProps {
children: React.ReactNode;
@ -20,10 +22,11 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
useEffect(() => {
// Check if user is authenticated but no user data
if (isAuthenticated && !user) {
if (isAuthenticated && !user && !isLoading) {
console.log('ProtectedRoute: User authenticated but no user data, fetching user...');
dispatch(getCurrentUser());
}
}, [isAuthenticated, user, dispatch]);
}, [isAuthenticated, user, isLoading, dispatch]);
// Show loading while checking authentication
if (isLoading) {
@ -51,10 +54,21 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
hasRequiredRole = user.role === requiredRole;
} else {
console.warn('User roles not properly loaded:', user);
return <Navigate to="/unauthorized" replace />;
// For system_admin, allow access even if roles are not loaded properly
if (requiredRole === 'system_admin' && user.role === 'system_admin') {
hasRequiredRole = true;
} else {
return <Navigate to="/unauthorized" replace />;
}
}
if (!hasRequiredRole) {
console.log('User does not have required role:', {
userRole: user.role,
requiredRole,
userRoles: user.roles,
primaryRole: user.roles?.[0]?.name
});
return <Navigate to="/unauthorized" replace />;
}
}
@ -65,9 +79,20 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
// return <Navigate to="/verify-email" replace />;
// }
// Check if account is active
if (user && user.status !== 'active') {
return <Navigate to="/account-pending" replace />;
// Check if account is blocked (inactive, pending, or suspended)
if (user && ['inactive', 'pending', 'suspended'].includes(user.status)) {
// Show blocked account modal instead of redirecting
return (
<BlockedAccountModal
isOpen={true}
onClose={() => {
// Force logout when modal is closed
dispatch(logout());
}}
userEmail={user.email}
userStatus={user.status}
/>
);
}
return <>{children}</>;

View File

@ -0,0 +1,358 @@
import React from 'react';
import { X, Building, Mail, Phone, MapPin, Globe, FileText, DollarSign, Users, Calendar } from 'lucide-react';
import { VendorModalProps } from '../types/vendor';
const VendorDetailsModal: React.FC<VendorModalProps> = ({
vendor,
isOpen,
onClose,
onApprove,
onReject
}) => {
console.log('VendorDetailsModal props:', { isOpen, vendor: vendor?.id, vendorName: vendor ? `${vendor.firstName} ${vendor.lastName}` : null });
if (!isOpen || !vendor) return null;
const formatRoleName = (role: string) => {
if (role.startsWith('channel_partner_')) {
return 'Vendor';
} else if (role.startsWith('reseller_')) {
return 'Reseller';
} else if (role.startsWith('system_')) {
return 'System Admin';
}
return role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
};
const formatUserType = (userType: string) => {
if (userType === 'channel_partner') {
return 'Vendor';
} else if (userType === 'reseller') {
return 'Reseller';
}
return userType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount);
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-3">
<Building className="w-6 h-6 text-blue-600" />
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Vendor Details
</h2>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Basic Information */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Basic Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Full Name
</label>
<p className="text-gray-900 dark:text-white">
{vendor.firstName} {vendor.lastName}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email
</label>
<p className="text-gray-900 dark:text-white flex items-center">
<Mail className="w-4 h-4 mr-2" />
{vendor.email}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Phone
</label>
<p className="text-gray-900 dark:text-white flex items-center">
<Phone className="w-4 h-4 mr-2" />
{vendor.phone || 'N/A'}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Company
</label>
<p className="text-gray-900 dark:text-white flex items-center">
<Building className="w-4 h-4 mr-2" />
{vendor.company}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Role
</label>
<p className="text-gray-900 dark:text-white">
{formatRoleName(vendor.role)}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
User Type
</label>
<p className="text-gray-900 dark:text-white">
{formatUserType(vendor.userType)}
</p>
</div>
</div>
</div>
{/* Business Information */}
{(vendor.companyType || vendor.registrationNumber || vendor.gstNumber || vendor.panNumber) && (
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Business Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{vendor.companyType && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Company Type
</label>
<p className="text-gray-900 dark:text-white">
{vendor.companyType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</p>
</div>
)}
{vendor.registrationNumber && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Registration Number
</label>
<p className="text-gray-900 dark:text-white">
{vendor.registrationNumber}
</p>
</div>
)}
{vendor.gstNumber && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
GST Number
</label>
<p className="text-gray-900 dark:text-white">
{vendor.gstNumber}
</p>
</div>
)}
{vendor.panNumber && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
PAN Number
</label>
<p className="text-gray-900 dark:text-white">
{vendor.panNumber}
</p>
</div>
)}
</div>
</div>
)}
{/* Financial Information */}
{(vendor.annualRevenue || vendor.employeeCount || vendor.yearsInBusiness) && (
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Financial Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{vendor.annualRevenue && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Annual Revenue
</label>
<p className="text-gray-900 dark:text-white flex items-center">
<DollarSign className="w-4 h-4 mr-2" />
{formatCurrency(vendor.annualRevenue)}
</p>
</div>
)}
{vendor.employeeCount && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Employee Count
</label>
<p className="text-gray-900 dark:text-white flex items-center">
<Users className="w-4 h-4 mr-2" />
{vendor.employeeCount} employees
</p>
</div>
)}
{vendor.yearsInBusiness && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Years in Business
</label>
<p className="text-gray-900 dark:text-white flex items-center">
<Calendar className="w-4 h-4 mr-2" />
{vendor.yearsInBusiness} years
</p>
</div>
)}
</div>
</div>
)}
{/* Address Information */}
{vendor.address && (
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Address Information
</h3>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address
</label>
<p className="text-gray-900 dark:text-white flex items-start">
<MapPin className="w-4 h-4 mr-2 mt-0.5 flex-shrink-0" />
{vendor.address}
</p>
</div>
</div>
)}
{/* Additional Information */}
{(vendor.website || vendor.businessLicense || vendor.taxId || vendor.industry) && (
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Additional Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{vendor.website && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Website
</label>
<p className="text-gray-900 dark:text-white flex items-center">
<Globe className="w-4 h-4 mr-2" />
<a href={vendor.website} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-800">
{vendor.website}
</a>
</p>
</div>
)}
{vendor.businessLicense && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Business License
</label>
<p className="text-gray-900 dark:text-white flex items-center">
<FileText className="w-4 h-4 mr-2" />
{vendor.businessLicense}
</p>
</div>
)}
{vendor.taxId && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tax ID
</label>
<p className="text-gray-900 dark:text-white">
{vendor.taxId}
</p>
</div>
)}
{vendor.industry && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Industry
</label>
<p className="text-gray-900 dark:text-white">
{vendor.industry}
</p>
</div>
)}
</div>
</div>
)}
{/* Rejection Information */}
{vendor.rejectionReason && (
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Rejection Information
</h3>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Rejection Reason
</label>
<p className="text-gray-900 dark:text-white bg-red-50 dark:bg-red-900/20 p-3 rounded-lg">
{vendor.rejectionReason}
</p>
</div>
</div>
)}
{/* Timestamp */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Timestamp
</h3>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Created At
</label>
<p className="text-gray-900 dark:text-white flex items-center">
<Calendar className="w-4 h-4 mr-2" />
{new Date(vendor.createdAt).toLocaleString()}
</p>
</div>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
Close
</button>
{vendor.status === 'pending' && (
<>
<button
onClick={() => onReject(vendor.id, '')}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700"
>
Reject
</button>
<button
onClick={() => onApprove(vendor.id)}
className="px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700"
>
Approve
</button>
</>
)}
</div>
</div>
</div>
);
};
export default VendorDetailsModal;

View File

@ -0,0 +1,96 @@
import React, { useState } from 'react';
import { X, AlertTriangle } from 'lucide-react';
import { RejectionModalProps } from '../types/vendor';
const VendorRejectionModal: React.FC<RejectionModalProps> = ({
vendor,
isOpen,
onClose,
onReject
}) => {
const [rejectionReason, setRejectionReason] = useState('');
console.log('VendorRejectionModal props:', { isOpen, vendor: vendor?.id, vendorName: vendor ? `${vendor.firstName} ${vendor.lastName}` : null });
if (!isOpen || !vendor) return null;
const handleReject = () => {
if (rejectionReason.trim()) {
onReject(vendor.id, rejectionReason.trim());
setRejectionReason('');
onClose();
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-3">
<AlertTriangle className="w-6 h-6 text-red-600" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Reject Vendor Request
</h3>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-6">
<div className="mb-4">
<p className="text-gray-700 dark:text-gray-300 mb-4">
Are you sure you want to reject the vendor request for{' '}
<span className="font-semibold text-gray-900 dark:text-white">
{vendor.firstName} {vendor.lastName}
</span>
?
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Please provide a reason for rejection. This will be communicated to the vendor.
</p>
</div>
<div className="mb-6">
<label htmlFor="rejectionReason" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Rejection Reason *
</label>
<textarea
id="rejectionReason"
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
rows={4}
placeholder="Please provide a detailed reason for rejection..."
required
/>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
Cancel
</button>
<button
onClick={handleReject}
disabled={!rejectionReason.trim()}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Reject Request
</button>
</div>
</div>
</div>
);
};
export default VendorRejectionModal;

View File

@ -106,7 +106,7 @@ const CommissionTrendsChart: React.FC = () => {
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#6b7280' }}
tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
tickFormatter={(value) => `${((Number(value) || 0) / 1000).toFixed(0)}k`}
/>
<Tooltip content={<CustomTooltip />} />
<Area

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { User, Mail, Phone, Building, MapPin, DollarSign, Award } from 'lucide-react';
import { User, Mail, Phone, Building, MapPin, Briefcase, Globe } from 'lucide-react';
import { cn } from '../../utils/cn';
interface AddResellerFormProps {
@ -9,28 +9,51 @@ interface AddResellerFormProps {
const AddResellerForm: React.FC<AddResellerFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
companyName: '',
contactPerson: '',
firstName: '',
lastName: '',
email: '',
phone: '',
company: '',
userType: '' as 'reseller_admin' | 'reseller_sales' | 'reseller_support' | 'read_only',
region: '',
commissionRate: '',
tier: 'silver',
businessType: '',
address: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [showUserTypeDropdown, setShowUserTypeDropdown] = useState(false);
const [showRegionDropdown, setShowRegionDropdown] = useState(false);
const [showBusinessTypeDropdown, setShowBusinessTypeDropdown] = useState(false);
const userTypes = [
{ value: 'reseller_admin', label: 'Reseller Admin', description: 'Full access to reseller dashboard and management' },
{ value: 'reseller_sales', label: 'Sales Representative', description: 'Access to sales tools and customer management' },
{ value: 'reseller_support', label: 'Support Representative', description: 'Access to support tools and ticket management' },
{ value: 'read_only', label: 'Read Only', description: 'View-only access to reports and data' }
];
const regions = [
'North America', 'South America', 'Europe', 'Asia Pacific',
'Middle East', 'Africa', 'India', 'Australia'
];
const businessTypes = [
'Technology Services', 'IT Consulting', 'Cloud Services',
'Software Development', 'Digital Marketing', 'E-commerce',
'Healthcare IT', 'Financial Services', 'Education Technology',
'Manufacturing', 'Retail', 'Other'
];
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.companyName.trim()) {
newErrors.companyName = 'Company name is required';
if (!formData.firstName.trim()) {
newErrors.firstName = 'First name is required';
}
if (!formData.contactPerson.trim()) {
newErrors.contactPerson = 'Contact person is required';
if (!formData.lastName.trim()) {
newErrors.lastName = 'Last name is required';
}
if (!formData.email.trim()) {
@ -45,17 +68,20 @@ const AddResellerForm: React.FC<AddResellerFormProps> = ({ onSubmit, onCancel })
newErrors.phone = 'Please enter a valid phone number';
}
if (!formData.region.trim()) {
if (!formData.company.trim()) {
newErrors.company = 'Company name is required';
}
if (!formData.userType) {
newErrors.userType = 'User type is required';
}
if (!formData.region) {
newErrors.region = 'Region is required';
}
if (!formData.commissionRate) {
newErrors.commissionRate = 'Commission rate is required';
} else {
const rate = parseFloat(formData.commissionRate);
if (isNaN(rate) || rate < 5 || rate > 20) {
newErrors.commissionRate = 'Commission rate must be between 5% and 20%';
}
if (!formData.businessType) {
newErrors.businessType = 'Business type is required';
}
setErrors(newErrors);
@ -77,7 +103,6 @@ const AddResellerForm: React.FC<AddResellerFormProps> = ({ onSubmit, onCancel })
onSubmit({
...formData,
commissionRate: parseFloat(formData.commissionRate),
id: Date.now().toString(),
status: 'pending',
createdAt: new Date().toISOString(),
@ -99,49 +124,49 @@ const AddResellerForm: React.FC<AddResellerFormProps> = ({ onSubmit, onCancel })
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Company Name */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Building className="inline w-4 h-4 mr-2" />
Company Name *
</label>
<input
type="text"
value={formData.companyName}
onChange={(e) => handleChange('companyName', e.target.value)}
className={cn(
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500",
errors.companyName
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter company name"
/>
{errors.companyName && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.companyName}</p>
)}
</div>
{/* Contact Person */}
{/* First Name */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<User className="inline w-4 h-4 mr-2" />
Contact Person *
First Name *
</label>
<input
type="text"
value={formData.contactPerson}
onChange={(e) => handleChange('contactPerson', e.target.value)}
value={formData.firstName}
onChange={(e) => handleChange('firstName', e.target.value)}
className={cn(
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500",
errors.contactPerson
errors.firstName
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter contact person name"
placeholder="Enter first name"
/>
{errors.contactPerson && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.contactPerson}</p>
{errors.firstName && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.firstName}</p>
)}
</div>
{/* Last Name */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<User className="inline w-4 h-4 mr-2" />
Last Name *
</label>
<input
type="text"
value={formData.lastName}
onChange={(e) => handleChange('lastName', e.target.value)}
className={cn(
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500",
errors.lastName
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter last name"
/>
{errors.lastName && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.lastName}</p>
)}
</div>
@ -191,75 +216,160 @@ const AddResellerForm: React.FC<AddResellerFormProps> = ({ onSubmit, onCancel })
)}
</div>
{/* Region */}
{/* Company */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Building className="inline w-4 h-4 mr-2" />
Company Name *
</label>
<input
type="text"
value={formData.company}
onChange={(e) => handleChange('company', e.target.value)}
className={cn(
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500",
errors.company
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter company name"
/>
{errors.company && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.company}</p>
)}
</div>
{/* User Type */}
<div className="relative">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<User className="inline w-4 h-4 mr-2" />
User Type *
</label>
<div className="relative">
<button
type="button"
onClick={() => setShowUserTypeDropdown(!showUserTypeDropdown)}
className={cn(
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-left flex items-center justify-between",
errors.userType
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
>
{formData.userType ? userTypes.find(t => t.value === formData.userType)?.label : 'Select user type'}
<Globe className="w-4 h-4" />
</button>
{showUserTypeDropdown && (
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-auto">
{userTypes.map((type) => (
<button
key={type.value}
type="button"
onClick={() => {
handleChange('userType', type.value);
setShowUserTypeDropdown(false);
}}
className="w-full px-3 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 border-b border-gray-200 dark:border-gray-600 last:border-b-0"
>
<div className="font-medium text-gray-900 dark:text-white">{type.label}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">{type.description}</div>
</button>
))}
</div>
)}
</div>
{errors.userType && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.userType}</p>
)}
</div>
{/* Region */}
<div className="relative">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<MapPin className="inline w-4 h-4 mr-2" />
Region *
</label>
<select
value={formData.region}
onChange={(e) => handleChange('region', e.target.value)}
className={cn(
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500",
errors.region
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
<div className="relative">
<button
type="button"
onClick={() => setShowRegionDropdown(!showRegionDropdown)}
className={cn(
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-left flex items-center justify-between",
errors.region
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
>
{formData.region || 'Select region'}
<MapPin className="w-4 h-4" />
</button>
{showRegionDropdown && (
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-auto">
{regions.map((region) => (
<button
key={region}
type="button"
onClick={() => {
handleChange('region', region);
setShowRegionDropdown(false);
}}
className="w-full px-3 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 border-b border-gray-200 dark:border-gray-600 last:border-b-0 text-gray-900 dark:text-white"
>
{region}
</button>
))}
</div>
)}
>
<option value="">Select region</option>
<option value="North India">North India</option>
<option value="South India">South India</option>
<option value="East India">East India</option>
<option value="West India">West India</option>
<option value="Central India">Central India</option>
</select>
</div>
{errors.region && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.region}</p>
)}
</div>
{/* Commission Rate */}
<div>
{/* Business Type */}
<div className="relative">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<DollarSign className="inline w-4 h-4 mr-2" />
Commission Rate (%) *
<Briefcase className="inline w-4 h-4 mr-2" />
Business Type *
</label>
<input
type="number"
min="5"
max="20"
step="0.5"
value={formData.commissionRate}
onChange={(e) => handleChange('commissionRate', e.target.value)}
className={cn(
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500",
errors.commissionRate
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter commission rate"
/>
{errors.commissionRate && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.commissionRate}</p>
)}
</div>
<div className="relative">
<button
type="button"
onClick={() => setShowBusinessTypeDropdown(!showBusinessTypeDropdown)}
className={cn(
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-left flex items-center justify-between",
errors.businessType
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
>
{formData.businessType || 'Select business type'}
<Briefcase className="w-4 h-4" />
</button>
{/* Tier */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Award className="inline w-4 h-4 mr-2" />
Tier
</label>
<select
value={formData.tier}
onChange={(e) => handleChange('tier', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-700 dark:text-white"
>
<option value="silver">Silver</option>
<option value="gold">Gold</option>
<option value="platinum">Platinum</option>
</select>
{showBusinessTypeDropdown && (
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-auto">
{businessTypes.map((type) => (
<button
key={type}
type="button"
onClick={() => {
handleChange('businessType', type);
setShowBusinessTypeDropdown(false);
}}
className="w-full px-3 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 border-b border-gray-200 dark:border-gray-600 last:border-b-0 text-gray-900 dark:text-white"
>
{type}
</button>
))}
</div>
)}
</div>
{errors.businessType && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.businessType}</p>
)}
</div>
</div>

View File

@ -0,0 +1,614 @@
import React, { useState, useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { createProduct, updateProductById } from '../../store/slices/productThunks';
import { Product } from '../../services/api';
import toast from 'react-hot-toast';
import {
Package,
DollarSign,
Tag,
FileText,
Image,
X,
Plus,
Trash2,
HelpCircle
} from 'lucide-react';
interface ProductFormProps {
product?: Product | null;
onClose: () => void;
onSuccess?: () => void;
}
const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }) => {
const dispatch = useAppDispatch();
const { isLoading } = useAppSelector((state) => state.product);
const { user } = useAppSelector((state) => state.auth);
const [formData, setFormData] = useState({
name: '',
description: '',
category: 'cloud_storage' as Product['category'],
subcategory: '',
price: 0,
currency: 'USD',
commissionRate: 10,
features: [] as string[],
specifications: {} as Record<string, any>,
images: [] as string[],
documents: [] as string[],
status: 'draft' as Product['status'],
availability: 'available' as Product['availability'],
stockQuantity: -1,
sku: '',
tags: [] as string[],
metadata: {} as Record<string, any>,
purchaseUrl: '',
});
const [newFeature, setNewFeature] = useState('');
const [newTag, setNewTag] = useState('');
const [newSpecKey, setNewSpecKey] = useState('');
const [newSpecValue, setNewSpecValue] = useState('');
const [showSkuHelp, setShowSkuHelp] = useState(false);
const categories = [
{ value: 'cloud_storage', label: 'Cloud Storage' },
{ value: 'cloud_computing', label: 'Cloud Computing' },
{ value: 'cybersecurity', label: 'Cybersecurity' },
{ value: 'data_analytics', label: 'Data Analytics' },
{ value: 'ai_ml', label: 'AI & Machine Learning' },
{ value: 'iot', label: 'Internet of Things' },
{ value: 'blockchain', label: 'Blockchain' },
{ value: 'other', label: 'Other' },
];
const statusOptions = [
{ value: 'draft', label: 'Draft' },
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
{ value: 'discontinued', label: 'Discontinued' },
];
const availabilityOptions = [
{ value: 'available', label: 'Available' },
{ value: 'out_of_stock', label: 'Out of Stock' },
{ value: 'coming_soon', label: 'Coming Soon' },
{ value: 'discontinued', label: 'Discontinued' },
];
useEffect(() => {
if (product) {
setFormData({
name: product.name,
description: product.description || '',
category: product.category,
subcategory: product.subcategory || '',
price: product.price,
currency: product.currency,
commissionRate: product.commissionRate,
features: product.features || [],
specifications: product.specifications || {},
images: product.images || [],
documents: product.documents || [],
status: product.status,
availability: product.availability,
stockQuantity: product.stockQuantity,
sku: product.sku,
tags: product.tags || [],
metadata: product.metadata || {},
purchaseUrl: product.purchaseUrl || '',
});
}
}, [product]);
const handleInputChange = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const addFeature = () => {
if (newFeature.trim()) {
setFormData(prev => ({
...prev,
features: [...prev.features, newFeature.trim()]
}));
setNewFeature('');
}
};
const removeFeature = (index: number) => {
setFormData(prev => ({
...prev,
features: prev.features.filter((_, i) => i !== index)
}));
};
const addTag = () => {
if (newTag.trim()) {
setFormData(prev => ({
...prev,
tags: [...prev.tags, newTag.trim()]
}));
setNewTag('');
}
};
const removeTag = (index: number) => {
setFormData(prev => ({
...prev,
tags: prev.tags.filter((_, i) => i !== index)
}));
};
const addSpecification = () => {
if (newSpecKey.trim() && newSpecValue.trim()) {
setFormData(prev => ({
...prev,
specifications: {
...prev.specifications,
[newSpecKey.trim()]: newSpecValue.trim()
}
}));
setNewSpecKey('');
setNewSpecValue('');
}
};
const removeSpecification = (key: string) => {
setFormData(prev => {
const newSpecs = { ...prev.specifications };
delete newSpecs[key];
return { ...prev, specifications: newSpecs };
});
};
// Form validation
const isFormValid = () => {
return (
formData.name.trim() !== '' &&
formData.price > 0 &&
formData.sku.trim() !== '' &&
formData.commissionRate >= 0 &&
formData.commissionRate <= 100
);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
toast.error('Product name is required');
return;
}
if (formData.price <= 0) {
toast.error('Price must be greater than 0');
return;
}
if (!formData.sku.trim()) {
toast.error('SKU is required');
return;
}
if (formData.commissionRate < 0 || formData.commissionRate > 100) {
toast.error('Commission rate must be between 0% and 100%');
return;
}
try {
const productData = {
...formData,
createdBy: user?.id || 0,
updatedBy: user?.id || 0,
};
if (product) {
await dispatch(updateProductById({ id: product.id, productData })).unwrap();
toast.success('Product updated successfully');
} else {
await dispatch(createProduct(productData)).unwrap();
toast.success('Product created successfully');
}
onSuccess?.();
onClose();
} catch (error: any) {
toast.error(error.message || 'Failed to save product');
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden">
<div className="p-6 border-b border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
{product ? 'Edit Product' : 'Create New Product'}
</h2>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
<X className="h-6 w-6" />
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[calc(90vh-80px)] scrollbar-hide">
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Basic Information */}
<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-2">
Product Name *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
className="w-full px-3 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"
placeholder="Enter product name"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<div className="flex items-center gap-2">
SKU *
<button
type="button"
onClick={() => setShowSkuHelp(!showSkuHelp)}
className="text-slate-400 hover:text-slate-600"
>
<HelpCircle className="h-4 w-4" />
</button>
</div>
</label>
<input
type="text"
value={formData.sku}
onChange={(e) => handleInputChange('sku', e.target.value)}
className="w-full px-3 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"
placeholder="e.g., CS-STORAGE-001"
required
/>
{showSkuHelp && (
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>SKU (Stock Keeping Unit):</strong> A unique identifier for your product.
Use a consistent format like: <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">CATEGORY-TYPE-NUMBER</code>
</p>
<p className="text-sm text-blue-700 dark:text-blue-300 mt-1">
Examples: CS-STORAGE-001, CC-COMPUTE-002, CY-SECURITY-003
</p>
</div>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Description
</label>
<textarea
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={3}
className="w-full px-3 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"
placeholder="Enter product description"
/>
</div>
{/* Category and Subcategory */}
<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-2">
Category *
</label>
<select
value={formData.category}
onChange={(e) => handleInputChange('category', e.target.value)}
className="w-full px-3 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"
required
>
{categories.map(category => (
<option key={category.value} value={category.value}>
{category.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Subcategory
</label>
<input
type="text"
value={formData.subcategory}
onChange={(e) => handleInputChange('subcategory', e.target.value)}
className="w-full px-3 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"
placeholder="Enter subcategory"
/>
</div>
</div>
{/* Pricing */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Price *
</label>
<input
type="number"
step="0.01"
min="0"
value={formData.price}
onChange={(e) => handleInputChange('price', parseFloat(e.target.value) || 0)}
className="w-full px-3 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"
placeholder="0.00"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Currency
</label>
<input
type="text"
value={formData.currency}
onChange={(e) => handleInputChange('currency', e.target.value)}
className="w-full px-3 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"
placeholder="USD"
maxLength={3}
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Commission Rate (%)
</label>
<input
type="number"
step="0.01"
min="0"
max="100"
value={formData.commissionRate}
onChange={(e) => handleInputChange('commissionRate', parseFloat(e.target.value) || 0)}
className="w-full px-3 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"
placeholder="10.00"
/>
</div>
</div>
{/* Status and Availability */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Status
</label>
<select
value={formData.status}
onChange={(e) => handleInputChange('status', e.target.value)}
className="w-full px-3 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"
>
{statusOptions.map(status => (
<option key={status.value} value={status.value}>
{status.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Availability
</label>
<select
value={formData.availability}
onChange={(e) => handleInputChange('availability', e.target.value)}
className="w-full px-3 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"
>
{availabilityOptions.map(availability => (
<option key={availability.value} value={availability.value}>
{availability.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Stock Quantity
</label>
<input
type="number"
min="-1"
value={formData.stockQuantity}
onChange={(e) => handleInputChange('stockQuantity', parseInt(e.target.value) || -1)}
className="w-full px-3 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"
placeholder="-1"
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
Use -1 for unlimited stock
</p>
</div>
</div>
{/* Features */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Features
</label>
<div className="flex gap-2 mb-2">
<input
type="text"
value={newFeature}
onChange={(e) => setNewFeature(e.target.value)}
className="flex-1 px-3 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"
placeholder="Add a feature"
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addFeature())}
/>
<button
type="button"
onClick={addFeature}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<Plus className="h-4 w-4" />
</button>
</div>
<div className="flex flex-wrap gap-2">
{formData.features.map((feature, index) => (
<span
key={index}
className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-sm"
>
{feature}
<button
type="button"
onClick={() => removeFeature(index)}
className="text-blue-600 hover:text-blue-800"
>
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
</div>
{/* Tags */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Tags
</label>
<div className="flex gap-2 mb-2">
<input
type="text"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
className="flex-1 px-3 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"
placeholder="Add a tag"
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addTag())}
/>
<button
type="button"
onClick={addTag}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500"
>
<Plus className="h-4 w-4" />
</button>
</div>
<div className="flex flex-wrap gap-2">
{formData.tags.map((tag, index) => (
<span
key={index}
className="inline-flex items-center gap-1 px-3 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded-full text-sm"
>
{tag}
<button
type="button"
onClick={() => removeTag(index)}
className="text-green-600 hover:text-green-800"
>
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
</div>
{/* Specifications */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Specifications
</label>
<div className="flex gap-2 mb-2">
<input
type="text"
value={newSpecKey}
onChange={(e) => setNewSpecKey(e.target.value)}
className="flex-1 px-3 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"
placeholder="Specification key"
/>
<input
type="text"
value={newSpecValue}
onChange={(e) => setNewSpecValue(e.target.value)}
className="flex-1 px-3 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"
placeholder="Specification value"
/>
<button
type="button"
onClick={addSpecification}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<Plus className="h-4 w-4" />
</button>
</div>
<div className="space-y-2">
{Object.entries(formData.specifications).map(([key, value]) => (
<div
key={key}
className="flex items-center justify-between p-2 bg-slate-50 dark:bg-slate-700 rounded-lg"
>
<span className="text-sm">
<strong>{key}:</strong> {value}
</span>
<button
type="button"
onClick={() => removeSpecification(key)}
className="text-red-600 hover:text-red-800"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
))}
</div>
</div>
{/* Purchase URL */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Purchase URL
</label>
<input
type="url"
value={formData.purchaseUrl}
onChange={(e) => handleInputChange('purchaseUrl', e.target.value)}
className="w-full px-3 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"
placeholder="https://example.com/product"
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
Direct link where customers can purchase this product
</p>
</div>
{/* Form Actions */}
<div className="flex justify-end gap-3 pt-6 border-t border-slate-200 dark:border-slate-700">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-slate-600 dark:text-slate-400 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-500"
>
Cancel
</button>
<button
type="submit"
disabled={isLoading || !isFormValid()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Saving...' : (product ? 'Update Product' : 'Create Product')}
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default ProductForm;

View File

@ -227,6 +227,15 @@
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: rgb(107 114 128);
}
/* Hide scrollbar utility */
.scrollbar-hide {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
}
@layer utilities {

View File

@ -0,0 +1,217 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAppSelector } from '../store/hooks';
import {
AlertTriangle,
Mail,
Phone,
MessageCircle,
Clock,
UserCheck,
ArrowLeft,
RefreshCw
} from 'lucide-react';
import { cn } from '../utils/cn';
const AccountPending: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { user } = useAppSelector((state) => state.auth);
const [isRefreshing, setIsRefreshing] = useState(false);
// Get status from location state or user object
const status = location.state?.status || user?.status || 'pending';
const userEmail = location.state?.email || user?.email || '';
const getStatusInfo = () => {
switch (status) {
case 'inactive':
return {
title: 'Account Deactivated',
message: 'Your account has been deactivated by the administrator.',
icon: <AlertTriangle className="w-16 h-16 text-red-500" />,
color: 'red',
bgColor: 'bg-red-50 dark:bg-red-900/20',
borderColor: 'border-red-200 dark:border-red-800',
textColor: 'text-red-800 dark:text-red-200'
};
case 'suspended':
return {
title: 'Account Suspended',
message: 'Your account has been suspended due to policy violations.',
icon: <AlertTriangle className="w-16 h-16 text-orange-500" />,
color: 'orange',
bgColor: 'bg-orange-50 dark:bg-orange-900/20',
borderColor: 'border-orange-200 dark:border-orange-800',
textColor: 'text-orange-800 dark:text-orange-200'
};
case 'pending':
return {
title: 'Account Pending Approval',
message: 'Your account is currently pending approval from the administrator.',
icon: <Clock className="w-16 h-16 text-yellow-500" />,
color: 'yellow',
bgColor: 'bg-yellow-50 dark:bg-yellow-900/20',
borderColor: 'border-yellow-200 dark:border-yellow-800',
textColor: 'text-yellow-800 dark:text-yellow-200'
};
default:
return {
title: 'Account Status Issue',
message: 'There is an issue with your account status.',
icon: <AlertTriangle className="w-16 h-16 text-gray-500" />,
color: 'gray',
bgColor: 'bg-gray-50 dark:bg-gray-900/20',
borderColor: 'border-gray-200 dark:border-gray-800',
textColor: 'text-gray-800 dark:text-gray-200'
};
}
};
const statusInfo = getStatusInfo();
const handleRefreshStatus = async () => {
setIsRefreshing(true);
// Simulate API call to refresh user status
await new Promise(resolve => setTimeout(resolve, 2000));
setIsRefreshing(false);
// Redirect to login to re-authenticate
navigate('/login', { replace: true });
};
const handleContactAdmin = () => {
const subject = encodeURIComponent('Account Status Inquiry');
const body = encodeURIComponent(
`Hello,\n\nI am inquiring about my account status.\n\nEmail: ${userEmail}\nCurrent Status: ${status}\n\nPlease let me know what additional information you need.\n\nThank you.`
);
window.open(`mailto:admin@cloudtopiaa.com?subject=${subject}&body=${body}`, '_blank');
};
const handleBackToLogin = () => {
navigate('/login', { replace: true });
};
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 flex items-center justify-center p-4">
<div className="w-full max-w-2xl">
{/* Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-600 via-indigo-600 to-purple-600 rounded-3xl shadow-2xl mb-6">
<UserCheck className="w-10 h-10 text-white" />
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Account Status
</h1>
<p className="text-slate-600 dark:text-slate-400">
We need to verify your account before you can proceed
</p>
</div>
{/* Main Content */}
<div className="bg-white/80 dark:bg-slate-800 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700 p-8">
{/* Status Display */}
<div className={cn(
"flex items-center justify-center p-6 rounded-xl mb-6",
statusInfo.bgColor,
statusInfo.borderColor
)}>
<div className="text-center">
{statusInfo.icon}
<h2 className="text-xl font-semibold mt-4 mb-2 text-slate-900 dark:text-white">
{statusInfo.title}
</h2>
<p className={cn("text-sm", statusInfo.textColor)}>
{statusInfo.message}
</p>
</div>
</div>
{/* User Information */}
{userEmail && (
<div className="bg-slate-50 dark:bg-slate-700 rounded-lg p-4 mb-6">
<h3 className="font-medium text-slate-900 dark:text-white mb-2">
Account Details
</h3>
<div className="text-sm text-slate-600 dark:text-slate-400">
<p><strong>Email:</strong> {userEmail}</p>
<p><strong>Status:</strong> <span className="capitalize">{status}</span></p>
</div>
</div>
)}
{/* Contact Information */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-3">
Need Help? Contact Administrator
</h4>
<div className="space-y-3">
<div className="flex items-center space-x-3">
<Mail className="w-4 h-4 text-blue-600" />
<span className="text-sm text-blue-800 dark:text-blue-200">
admin@cloudtopiaa.com
</span>
</div>
<div className="flex items-center space-x-3">
<Phone className="w-4 h-4 text-blue-600" />
<span className="text-sm text-blue-800 dark:text-blue-200">
+1 (555) 123-4567
</span>
</div>
<div className="flex items-center space-x-3">
<MessageCircle className="w-4 h-4 text-blue-600" />
<span className="text-sm text-blue-800 dark:text-blue-200">
Support Chat Available
</span>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={handleBackToLogin}
className="flex-1 flex items-center justify-center px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-700 hover:bg-slate-50 dark:hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Login
</button>
<button
onClick={handleContactAdmin}
className="flex-1 flex items-center justify-center px-4 py-3 bg-blue-600 text-white rounded-xl font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
>
<Mail className="w-4 h-4 mr-2" />
Contact Admin
</button>
<button
onClick={handleRefreshStatus}
disabled={isRefreshing}
className="flex-1 flex items-center justify-center px-4 py-3 bg-emerald-600 text-white rounded-xl font-medium hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<RefreshCw className={cn("w-4 h-4 mr-2", isRefreshing && "animate-spin")} />
{isRefreshing ? 'Checking...' : 'Check Status'}
</button>
</div>
{/* Additional Information */}
<div className="mt-6 text-center">
<p className="text-xs text-slate-500 dark:text-slate-400">
If you believe this is an error, please contact our support team immediately.
</p>
</div>
</div>
{/* Footer */}
<div className="mt-8 text-center">
<p className="text-xs text-slate-500 dark:text-slate-400">
© 2025 Cloudtopiaa. All rights reserved.
</p>
</div>
</div>
</div>
);
};
export default AccountPending;

View File

@ -117,7 +117,7 @@ const Analytics: React.FC = () => {
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">Total Revenue</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
${(analyticsData.totalRevenue / 1000).toFixed(0)}K
${((Number(analyticsData.totalRevenue) || 0) / 1000).toFixed(0)}K
</p>
</div>
<div className="p-3 bg-primary-100 dark:bg-primary-900 rounded-lg">
@ -200,7 +200,7 @@ const Analytics: React.FC = () => {
{analyticsData.revenueByMonth.slice(-4).map((item, index) => (
<div key={index} className="text-center">
<p className="text-sm text-secondary-600 dark:text-secondary-400">{item.month}</p>
<p className="font-semibold text-secondary-900 dark:text-white">${(item.revenue / 1000).toFixed(0)}K</p>
<p className="font-semibold text-secondary-900 dark:text-white">${((Number(item.revenue) || 0) / 1000).toFixed(0)}K</p>
</div>
))}
</div>
@ -271,7 +271,7 @@ const Analytics: React.FC = () => {
</div>
</td>
<td className="py-3 px-4 text-sm text-secondary-900 dark:text-white">
${(reseller.revenue / 1000).toFixed(0)}K
${((Number(reseller.revenue) || 0) / 1000).toFixed(0)}K
</td>
<td className="py-3 px-4 text-sm text-secondary-600 dark:text-secondary-400">
{reseller.deals}
@ -312,7 +312,7 @@ const Analytics: React.FC = () => {
</div>
</div>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
${(region.revenue / 1000).toFixed(0)}K
${((Number(region.revenue) || 0) / 1000).toFixed(0)}K
</p>
<p className="text-sm text-secondary-600 dark:text-secondary-400 mt-1">Revenue</p>
</div>

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { mockPartnerships } from '../../data/mockData';
import React, { useState, useEffect } from 'react';
import { mockResellers } from '../../data/mockData';
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
import Modal from '../../components/Modal';
import AddPartnershipForm from '../../components/forms/AddPartnershipForm';
@ -22,22 +22,31 @@ import {
AlertCircle,
Download,
Mail,
MapPin
MapPin,
Building2
} from 'lucide-react';
import { cn } from '../../utils/cn';
const PartnershipsPage: React.FC = () => {
const ApprovedResellersPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [tierFilter, setTierFilter] = useState('all');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [selectedPartnership, setSelectedPartnership] = useState<any>(null);
const [selectedReseller, setSelectedReseller] = useState<any>(null);
const filteredPartnerships = mockPartnerships.filter(partnership => {
const matchesSearch = partnership.reseller.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || partnership.status === statusFilter;
const matchesTier = tierFilter === 'all' || partnership.tier === tierFilter;
// Set page title
useEffect(() => {
document.title = 'Approved Resellers - Cloudtopiaa';
}, []);
// Filter only approved (active) resellers
const approvedResellers = mockResellers.filter(reseller => reseller.status === 'active');
const filteredResellers = approvedResellers.filter(reseller => {
const matchesSearch = reseller.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || reseller.status === statusFilter;
const matchesTier = tierFilter === 'all' || reseller.tier === tierFilter;
return matchesSearch && matchesStatus && matchesTier;
});
@ -68,42 +77,42 @@ const PartnershipsPage: React.FC = () => {
}
};
const handleAddPartnership = (data: any) => {
console.log('New partnership data:', data);
// Here you would typically make an API call to add the partnership
const handleAddReseller = (data: any) => {
console.log('New reseller data:', data);
// Here you would typically make an API call to add the reseller
// For now, we'll just close the modal
setIsAddModalOpen(false);
// You could also show a success notification here
};
const handleViewPartnership = (partnership: any) => {
setSelectedPartnership(partnership);
const handleViewReseller = (reseller: any) => {
setSelectedReseller(reseller);
setIsDetailModalOpen(true);
};
const handleEditPartnership = (partnership: any) => {
console.log('Edit partnership:', partnership);
alert(`Edit functionality for ${partnership.reseller} partnership - This would open an edit form`);
const handleEditReseller = (reseller: any) => {
console.log('Edit reseller:', reseller);
alert(`Edit functionality for ${reseller.name} - This would open an edit form`);
};
const handleMailPartnership = (partnership: any) => {
console.log('Mail partnership:', partnership);
const mailtoLink = `mailto:${partnership.contactEmail}?subject=Cloudtopiaa Partnership Update`;
const handleMailReseller = (reseller: any) => {
console.log('Mail reseller:', reseller);
const mailtoLink = `mailto:${reseller.email}?subject=Cloudtopiaa Partnership Update`;
window.open(mailtoLink, '_blank');
};
const handleMoreOptions = (partnership: any) => {
console.log('More options for partnership:', partnership);
const handleMoreOptions = (reseller: any) => {
console.log('More options for reseller:', reseller);
const options = [
'View Performance',
'Download Report',
'Send Notification',
'Change Terms',
'Terminate Partnership'
'Suspend Partnership'
];
const selectedOption = prompt(`Select an option for ${partnership.reseller}:\n${options.join('\n')}`);
const selectedOption = prompt(`Select an option for ${reseller.name}:\n${options.join('\n')}`);
if (selectedOption) {
alert(`Selected: ${selectedOption} for ${partnership.reseller}`);
alert(`Selected: ${selectedOption} for ${reseller.name}`);
}
};
@ -113,10 +122,10 @@ const PartnershipsPage: React.FC = () => {
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white">
Partnerships
Approved Resellers
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Manage reseller partnerships and approval workflows
Manage approved reseller partnerships and their performance
</p>
</div>
<div className="flex space-x-3">
@ -124,13 +133,13 @@ const PartnershipsPage: React.FC = () => {
<Download className="w-4 h-4 mr-2" />
Export
</button>
<button
onClick={() => setIsAddModalOpen(true)}
className="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:scale-105 shadow-lg hover:shadow-xl"
>
<Plus className="w-4 h-4 mr-2" />
New Partnership
</button>
<button
onClick={() => setIsAddModalOpen(true)}
className="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:scale-105 shadow-lg hover:shadow-xl"
>
<Plus className="w-4 h-4 mr-2" />
Add Reseller
</button>
</div>
</div>
@ -140,14 +149,14 @@ const PartnershipsPage: React.FC = () => {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Total Partnerships
Total Approved Resellers
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{mockPartnerships.length}
{approvedResellers.length}
</p>
</div>
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
<Handshake className="w-6 h-6 text-primary-600 dark:text-primary-400" />
<Building2 className="w-6 h-6 text-primary-600 dark:text-primary-400" />
</div>
</div>
</div>
@ -156,30 +165,30 @@ const PartnershipsPage: React.FC = () => {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Active Partnerships
Platinum Tier
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{mockPartnerships.filter(p => p.status === 'active').length}
{approvedResellers.filter(r => r.tier === 'platinum').length}
</p>
</div>
<div className="w-12 h-12 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />
</div>
</div>
</div>
<div className="card p-6 bg-white dark:bg-gray-800 hover:shadow-lg">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Total Customers
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{approvedResellers.reduce((sum, r) => sum + r.customers, 0)}
</p>
</div>
<div className="w-12 h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-success-600 dark:text-success-400" />
</div>
</div>
</div>
<div className="card p-6 bg-white dark:bg-gray-800 hover:shadow-lg">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Pending Approvals
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{mockPartnerships.filter(p => p.status === 'pending').length}
</p>
</div>
<div className="w-12 h-12 bg-warning-100 dark:bg-warning-900 rounded-full flex items-center justify-center">
<Clock className="w-6 h-6 text-warning-600 dark:text-warning-400" />
<Users className="w-6 h-6 text-success-600 dark:text-success-400" />
</div>
</div>
</div>
@ -191,7 +200,7 @@ const PartnershipsPage: React.FC = () => {
Total Revenue
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{formatCurrency(mockPartnerships.reduce((sum, p) => sum + p.totalRevenue, 0))}
{formatCurrency(approvedResellers.reduce((sum, r) => sum + r.totalRevenue, 0))}
</p>
</div>
<div className="w-12 h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center">
@ -201,61 +210,6 @@ const PartnershipsPage: React.FC = () => {
</div>
</div>
{/* Pending Approvals Section */}
{mockPartnerships.filter(p => p.status === 'pending').length > 0 && (
<div className="card p-6 bg-white dark:bg-gray-800">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
Pending Approvals
</h3>
<span className="badge badge-warning">
{mockPartnerships.filter(p => p.status === 'pending').length} pending
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{mockPartnerships.filter(p => p.status === 'pending').map((partnership) => (
<div key={partnership.id} className="border border-warning-200 dark:border-warning-700 rounded-lg p-4 bg-warning-50 dark:bg-warning-900/20">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-secondary-900 dark:text-white">
{partnership.reseller}
</h4>
<span className={cn(
"inline-flex items-center px-2 py-1 rounded-full text-xs font-medium",
getTierColor(partnership.tier)
)}>
{partnership.tier.charAt(0).toUpperCase() + partnership.tier.slice(1)}
</span>
</div>
<div className="space-y-2 text-sm text-secondary-600 dark:text-secondary-400">
<div className="flex items-center">
<Users className="w-4 h-4 mr-2" />
{partnership.customers} customers
</div>
<div className="flex items-center">
<DollarSign className="w-4 h-4 mr-2" />
{partnership.commissionRate}% commission
</div>
<div className="flex items-center">
<MapPin className="w-4 h-4 mr-2" />
{partnership.region}
</div>
</div>
<div className="flex space-x-2 mt-4">
<button className="flex-1 inline-flex items-center justify-center px-3 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 transform hover:scale-105">
<CheckCircle className="w-4 h-4 mr-1" />
Approve
</button>
<button className="flex-1 inline-flex items-center justify-center px-3 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200 transform hover:scale-105">
<XCircle className="w-4 h-4 mr-1" />
Reject
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* Filters and Search */}
<div className="card p-6">
<div className="flex flex-col lg:flex-row gap-4">
@ -264,7 +218,7 @@ const PartnershipsPage: React.FC = () => {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-secondary-400" />
<input
type="text"
placeholder="Search partnerships..."
placeholder="Search resellers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pl-10 w-full focus:ring-2 focus:ring-primary-500 transition-all duration-200"
@ -303,12 +257,12 @@ const PartnershipsPage: React.FC = () => {
</div>
</div>
{/* Partnerships Table */}
{/* Resellers Table */}
<div className="card">
<div className="p-6 border-b border-secondary-200 dark:border-secondary-700">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
All Partnerships
All Approved Resellers
</h3>
<div className="flex space-x-2">
<button className="inline-flex items-center px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:scale-105">
@ -342,7 +296,7 @@ const PartnershipsPage: React.FC = () => {
Commission
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Start Date
Last Active
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Actions
@ -350,74 +304,81 @@ const PartnershipsPage: React.FC = () => {
</tr>
</thead>
<tbody className="bg-white dark:bg-secondary-900 divide-y divide-secondary-200 dark:divide-secondary-700">
{filteredPartnerships.map((partnership) => (
<tr key={partnership.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-800">
{filteredResellers.map((reseller) => (
<tr key={reseller.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-800">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{partnership.reseller}
</div>
<div className="text-sm text-secondary-500 dark:text-secondary-400">
{partnership.contactPerson}
</div>
<div className="text-xs text-secondary-400 dark:text-secondary-500">
{partnership.contactEmail}
<div className="flex items-center">
<img
className="h-10 w-10 rounded-full mr-3"
src={reseller.avatar}
alt={reseller.name}
/>
<div>
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{reseller.name}
</div>
<div className="text-sm text-secondary-500 dark:text-secondary-400">
{reseller.email}
</div>
<div className="text-xs text-secondary-400 dark:text-secondary-500">
{reseller.phone}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={cn(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
getStatusColor(partnership.status)
getStatusColor(reseller.status)
)}>
{partnership.status.charAt(0).toUpperCase() + partnership.status.slice(1)}
{reseller.status.charAt(0).toUpperCase() + reseller.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={cn(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
getTierColor(partnership.tier)
getTierColor(reseller.tier)
)}>
{partnership.tier.charAt(0).toUpperCase() + partnership.tier.slice(1)}
{reseller.tier.charAt(0).toUpperCase() + reseller.tier.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{formatCurrency(partnership.totalRevenue)}
{formatCurrency(reseller.totalRevenue)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{formatNumber(partnership.customers)}
{formatNumber(reseller.customers)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{partnership.commissionRate}%
{reseller.commissionRate}%
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-400">
{formatDate(partnership.startDate)}
{formatDate(reseller.lastActive)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center justify-end space-x-2">
<button
onClick={() => handleViewPartnership(partnership)}
onClick={() => handleViewReseller(reseller)}
className="p-2 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-all duration-200 transform hover:scale-110"
title="View Details"
>
<Eye className="w-4 h-4 text-secondary-600 dark:text-secondary-400" />
</button>
<button
onClick={() => handleEditPartnership(partnership)}
onClick={() => handleEditReseller(reseller)}
className="p-2 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-all duration-200 transform hover:scale-110"
title="Edit Partnership"
title="Edit Reseller"
>
<Edit className="w-4 h-4 text-secondary-600 dark:text-secondary-400" />
</button>
<button
onClick={() => handleMailPartnership(partnership)}
onClick={() => handleMailReseller(reseller)}
className="p-2 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-all duration-200 transform hover:scale-110"
title="Send Email"
>
<Mail className="w-4 h-4 text-secondary-600 dark:text-secondary-400" />
</button>
<button
onClick={() => handleMoreOptions(partnership)}
onClick={() => handleMoreOptions(reseller)}
className="p-2 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-all duration-200 transform hover:scale-110"
title="More Options"
>
@ -432,30 +393,30 @@ const PartnershipsPage: React.FC = () => {
</div>
</div>
{/* Add Partnership Modal */}
{/* Add Reseller Modal */}
<Modal
isOpen={isAddModalOpen}
onClose={() => setIsAddModalOpen(false)}
title="Add New Partnership"
title="Add New Reseller"
size="lg"
>
<AddPartnershipForm
onSubmit={handleAddPartnership}
onSubmit={handleAddReseller}
onCancel={() => setIsAddModalOpen(false)}
/>
</Modal>
{/* Partnership Detail Modal */}
{/* Reseller Detail Modal */}
<Modal
isOpen={isDetailModalOpen}
onClose={() => setIsDetailModalOpen(false)}
title="Partnership Details"
title="Reseller Details"
size="lg"
>
{selectedPartnership && (
{selectedReseller && (
<DetailView
type="partnership"
data={selectedPartnership}
type="reseller"
data={selectedReseller}
/>
)}
</Modal>
@ -463,4 +424,4 @@ const PartnershipsPage: React.FC = () => {
);
};
export default PartnershipsPage;
export default ApprovedResellersPage;

View File

@ -8,6 +8,8 @@ import { formatCurrency, formatCurrencyDual, formatNumber, formatRelativeTime, f
import RevenueChart from '../components/charts/RevenueChart';
import ResellerPerformanceChart from '../components/charts/ResellerPerformanceChart';
import DualCurrencyDisplay from '../components/DualCurrencyDisplay';
import NotificationBell from '../components/NotificationBell';
import DraggableFeedback from '../components/DraggableFeedback';
import {
TrendingUp,
Users,
@ -22,7 +24,8 @@ import {
Target,
Briefcase as BriefcaseIcon,
FileText as FileTextIcon,
Package
Package,
MessageCircle
} from 'lucide-react';
import { cn } from '../utils/cn';
@ -31,6 +34,8 @@ const Dashboard: React.FC = () => {
const navigate = useNavigate();
const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard);
const { user } = useAppSelector((state) => state.auth);
const [showFeedback, setShowFeedback] = React.useState(false);
const [feedbackKey, setFeedbackKey] = React.useState(0);
useEffect(() => {
// Initialize dashboard data
@ -114,6 +119,7 @@ const Dashboard: React.FC = () => {
</p>
</div>
<div className="flex items-center space-x-4">
<NotificationBell />
<div className="text-right">
<p className="text-sm text-secondary-600 dark:text-secondary-400">Commission Earned</p>
<DualCurrencyDisplay
@ -186,7 +192,7 @@ const Dashboard: React.FC = () => {
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<p className="text-xs sm:text-sm font-medium text-secondary-600 dark:text-secondary-400 truncate">
Active Partnerships
Active Resellers
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Approved business agreements
@ -202,7 +208,7 @@ const Dashboard: React.FC = () => {
<div className="flex items-center mt-3 sm:mt-4">
<TrendingUp className="w-3 h-3 sm:w-4 sm:h-4 text-success-600 flex-shrink-0" />
<span className="text-xs sm:text-sm text-success-600 ml-1 truncate">
+3 new partnerships
+3 new resellers
</span>
</div>
</div>
@ -396,6 +402,31 @@ const Dashboard: React.FC = () => {
</div>
</div>
</div>
{/* Draggable Feedback Component */}
{showFeedback && (
<DraggableFeedback
key={feedbackKey}
onClose={() => {
setShowFeedback(false);
setFeedbackKey(prev => prev + 1);
}}
/>
)}
{/* Feedback Trigger Button */}
{!showFeedback && (
<button
onClick={() => {
setShowFeedback(true);
setFeedbackKey(prev => prev + 1);
}}
className="fixed bottom-6 right-6 bg-emerald-600 hover:bg-emerald-700 text-white p-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-200 z-50"
title="Send Feedback"
>
<MessageCircle className="w-6 h-6" />
</button>
)}
</div>
);
};

View File

@ -20,6 +20,8 @@ import {
import { RootState } from '../store';
import { toggleTheme } from '../store/slices/themeSlice';
import { cn } from '../utils/cn';
import ApprovalStatusModal from '../components/ApprovalStatusModal';
import BlockedAccountModal from '../components/BlockedAccountModal';
const Login: React.FC = () => {
const [email, setEmail] = useState('');
@ -28,6 +30,12 @@ const Login: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [rememberMe, setRememberMe] = useState(false);
const [showApprovalModal, setShowApprovalModal] = useState(false);
const [pendingUserEmail, setPendingUserEmail] = useState('');
const [pendingUserCompany, setPendingUserCompany] = useState('');
const [showInactiveModal, setShowInactiveModal] = useState(false);
const [inactiveUserEmail, setInactiveUserEmail] = useState('');
const [inactiveUserStatus, setInactiveUserStatus] = useState('');
const navigate = useNavigate();
const location = useLocation();
@ -80,8 +88,30 @@ const Login: React.FC = () => {
navigate(redirectPath, { replace: true });
} catch (err: any) {
const errorMessage = err.message || 'Invalid email or password. Please try again.';
setError(errorMessage);
toast.error(errorMessage);
// Check if the error is related to account status issues
if (errorMessage.includes('Account access blocked') || errorMessage.includes('Status: inactive') || errorMessage.includes('Status: suspended') || errorMessage.includes('Status: pending')) {
// Extract status from error message
let status = 'inactive';
if (errorMessage.includes('Status: suspended')) {
status = 'suspended';
} else if (errorMessage.includes('Status: pending')) {
status = 'pending';
}
setInactiveUserEmail(email);
setInactiveUserStatus(status);
setShowInactiveModal(true);
setError('');
} else if (errorMessage.includes('pending approval') || errorMessage.includes('under review')) {
setPendingUserEmail(email);
setPendingUserCompany(''); // You might want to extract this from the error response
setShowApprovalModal(true);
setError('');
} else {
setError(errorMessage);
toast.error(errorMessage);
}
} finally {
setIsLoading(false);
}
@ -242,30 +272,7 @@ const Login: React.FC = () => {
</button>
</form>
{/* Divider */}
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-300 dark:border-slate-600"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-slate-800 text-slate-500 dark:text-slate-400">
Or continue with
</span>
</div>
</div>
{/* Social Login Buttons */}
<div className="space-y-3">
<button className="w-full flex items-center justify-center px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-700 hover:bg-slate-50 dark:hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200">
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Continue with Google
</button>
</div>
{/* Sign Up Link */}
<div className="mt-6 text-center">
@ -288,6 +295,22 @@ const Login: React.FC = () => {
</p>
</div>
</div>
{/* Approval Status Modal */}
<ApprovalStatusModal
isOpen={showApprovalModal}
onClose={() => setShowApprovalModal(false)}
userEmail={pendingUserEmail}
companyName={pendingUserCompany}
/>
{/* Blocked Account Modal */}
<BlockedAccountModal
isOpen={showInactiveModal}
onClose={() => setShowInactiveModal(false)}
userEmail={inactiveUserEmail}
userStatus={inactiveUserStatus}
/>
</div>
);
};

View File

@ -1,4 +1,15 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import {
fetchProducts,
fetchProductCategories,
fetchProductStats,
deleteProductById
} from '../store/slices/productThunks';
import { setFilters } from '../store/slices/productSlice';
import { Product } from '../services/api';
import ProductForm from '../components/forms/ProductForm';
import toast from 'react-hot-toast';
import {
Package,
DollarSign,
@ -16,530 +27,492 @@ import {
Star,
Zap,
Shield,
Globe
Globe,
Trash2,
MoreVertical,
Grid3X3,
List,
Database,
Cpu,
Lock,
BarChart3,
Brain,
Wifi,
Link
} from 'lucide-react';
interface Product {
id: string;
name: string;
category: string;
basePrice: number;
currentPrice: number;
margin: number;
marginType: 'percentage' | 'fixed';
stock: number;
status: 'active' | 'inactive';
description: string;
featured?: boolean;
}
const mockProducts: Product[] = [
{
id: '1',
name: 'Cloud Hosting Basic',
category: 'Hosting',
basePrice: 29.99,
currentPrice: 39.99,
margin: 33.34,
marginType: 'percentage',
stock: 100,
status: 'active',
description: 'Basic cloud hosting package with 10GB storage and 99.9% uptime guarantee',
featured: true
},
{
id: '2',
name: 'Cloud Hosting Pro',
category: 'Hosting',
basePrice: 59.99,
currentPrice: 79.99,
margin: 33.34,
marginType: 'percentage',
stock: 50,
status: 'active',
description: 'Professional cloud hosting with 50GB storage and advanced features',
featured: true
},
{
id: '3',
name: 'SSL Certificate',
category: 'Security',
basePrice: 49.99,
currentPrice: 69.99,
margin: 40.01,
marginType: 'percentage',
stock: 200,
status: 'active',
description: 'Standard SSL certificate for website security and trust'
},
{
id: '4',
name: 'Domain Registration',
category: 'Domains',
basePrice: 12.99,
currentPrice: 19.99,
margin: 53.89,
marginType: 'percentage',
stock: 1000,
status: 'active',
description: 'Annual domain registration service with free privacy protection'
},
{
id: '5',
name: 'Backup Service',
category: 'Storage',
basePrice: 19.99,
currentPrice: 29.99,
margin: 50.03,
marginType: 'percentage',
stock: 75,
status: 'active',
description: 'Automated backup service with 100GB storage and encryption'
}
];
const ProductManagement: React.FC = () => {
const [products, setProducts] = useState<Product[]>(mockProducts);
const dispatch = useAppDispatch();
const {
products,
pagination,
filters,
isLoading,
error
} = useAppSelector((state) => state.product);
const [showProductForm, setShowProductForm] = useState(false);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [categoryFilter, setCategoryFilter] = useState('all');
const [sortBy, setSortBy] = useState<'name' | 'price' | 'margin'>('name');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const [showPricingModal, setShowPricingModal] = useState(false);
const [showFilters, setShowFilters] = useState(false);
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deletingProductId, setDeletingProductId] = useState<number | null>(null);
const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))];
useEffect(() => {
dispatch(fetchProducts({}));
dispatch(fetchProductCategories());
dispatch(fetchProductStats());
}, [dispatch]);
const filteredAndSortedProducts = products
.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = categoryFilter === 'all' || product.category === categoryFilter;
return matchesSearch && matchesCategory;
})
.sort((a, b) => {
let aValue: string | number;
let bValue: string | number;
switch (sortBy) {
case 'name':
aValue = a.name;
bValue = b.name;
break;
case 'price':
aValue = a.currentPrice;
bValue = b.currentPrice;
break;
case 'margin':
aValue = a.margin;
bValue = b.margin;
break;
default:
aValue = a.name;
bValue = b.name;
}
if (sortOrder === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
const handlePricingUpdate = (productId: string, newMargin: number, marginType: 'percentage' | 'fixed') => {
setProducts(prev => prev.map(product => {
if (product.id === productId) {
const newPrice = marginType === 'percentage'
? product.basePrice * (1 + newMargin / 100)
: product.basePrice + newMargin;
return {
...product,
currentPrice: Math.round(newPrice * 100) / 100,
margin: newMargin,
marginType
};
}
return product;
}));
const handleFilterChange = (key: string, value: string) => {
dispatch(setFilters({ [key]: value }));
dispatch(fetchProducts({ ...filters, [key]: value, page: 1 }));
};
const getMarginColor = (margin: number) => {
if (margin >= 50) return 'text-emerald-600 bg-emerald-50 dark:bg-emerald-900/20 dark:text-emerald-400 border-emerald-200 dark:border-emerald-800';
if (margin >= 30) return 'text-blue-600 bg-blue-50 dark:bg-blue-900/20 dark:text-blue-400 border-blue-200 dark:border-blue-800';
if (margin >= 20) return 'text-amber-600 bg-amber-50 dark:bg-amber-900/20 dark:text-amber-400 border-amber-200 dark:border-amber-800';
return 'text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400 border-red-200 dark:border-red-800';
const handlePageChange = (page: number) => {
dispatch(fetchProducts({ ...filters, page }));
};
const handleSort = (sortBy: string) => {
const sortOrder = filters.sortBy === sortBy && filters.sortOrder === 'ASC' ? 'DESC' : 'ASC';
dispatch(setFilters({ sortBy, sortOrder }));
dispatch(fetchProducts({ ...filters, sortBy, sortOrder }));
};
const handleDeleteProduct = async (productId: number) => {
setDeletingProductId(productId);
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
if (!deletingProductId) return;
try {
await dispatch(deleteProductById(deletingProductId)).unwrap();
toast.success('Product deleted successfully');
setIsDeleteModalOpen(false);
setDeletingProductId(null);
} catch (error: any) {
toast.error(error.message || 'Failed to delete product');
}
};
const handleEditProduct = (product: Product) => {
setSelectedProduct(product);
setShowProductForm(true);
};
const handleCreateProduct = () => {
setSelectedProduct(null);
setShowProductForm(true);
};
const handleProductFormSuccess = () => {
dispatch(fetchProducts(filters));
};
const getCategoryIcon = (category: string) => {
switch (category.toLowerCase()) {
case 'hosting':
return <Zap className="w-4 h-4 text-blue-600 dark:text-blue-400" />;
case 'security':
return <Shield className="w-4 h-4 text-amber-600 dark:text-amber-400" />;
case 'domains':
return <Globe className="w-4 h-4 text-purple-600 dark:text-purple-400" />;
case 'storage':
return <Package className="w-4 h-4 text-red-600 dark:text-red-400" />;
switch (category) {
case 'cloud_storage':
return <Database className="h-5 w-5" />;
case 'cloud_computing':
return <Cpu className="h-5 w-5" />;
case 'cybersecurity':
return <Shield className="h-5 w-5" />;
case 'data_analytics':
return <BarChart3 className="h-5 w-5" />;
case 'ai_ml':
return <Brain className="h-5 w-5" />;
case 'iot':
return <Wifi className="h-5 w-5" />;
case 'blockchain':
return <Link className="h-5 w-5" />;
default:
return <Package className="w-4 h-4 text-gray-600 dark:text-gray-400" />;
return <Package className="h-5 w-5" />;
}
};
const getCategoryColor = (category: string) => {
switch (category.toLowerCase()) {
case 'hosting':
return 'bg-gradient-to-r from-blue-500 to-cyan-500';
case 'security':
return 'bg-gradient-to-r from-amber-500 to-orange-500';
case 'domains':
return 'bg-gradient-to-r from-purple-500 to-pink-500';
case 'storage':
return 'bg-gradient-to-r from-red-500 to-pink-500';
switch (category) {
case 'cloud_storage':
return 'text-blue-600 bg-blue-100 dark:bg-blue-900';
case 'cloud_computing':
return 'text-purple-600 bg-purple-100 dark:bg-purple-900';
case 'cybersecurity':
return 'text-red-600 bg-red-100 dark:bg-red-900';
case 'data_analytics':
return 'text-green-600 bg-green-100 dark:bg-green-900';
case 'ai_ml':
return 'text-orange-600 bg-orange-100 dark:bg-orange-900';
case 'iot':
return 'text-indigo-600 bg-indigo-100 dark:bg-indigo-900';
case 'blockchain':
return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
default:
return 'bg-gradient-to-r from-gray-500 to-gray-600';
return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active':
return 'text-green-600 bg-green-100 dark:bg-green-900';
case 'inactive':
return 'text-red-600 bg-red-100 dark:bg-red-900';
case 'draft':
return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
case 'discontinued':
return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
default:
return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
}
};
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-secondary-900 dark:text-white">
Product & Pricing Management
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Manage your product catalog and customize pricing strategies
</p>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">Product Management</h1>
<p className="text-slate-600 dark:text-slate-400">Manage your product catalog and pricing</p>
</div>
<button className="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-secondary-800">
<Plus className="w-4 h-4 mr-2" />
Add Product
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
<div className="card p-4 sm:p-6">
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<p className="text-xs sm:text-sm font-medium text-secondary-600 dark:text-secondary-400 truncate">
Total Products
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Available in catalog
</p>
<p className="text-lg sm:text-2xl font-bold text-secondary-900 dark:text-white truncate">
{products.length}
</p>
</div>
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
<Package className="w-5 h-5 sm:w-6 sm:h-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
<div className="card p-4 sm:p-6">
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<p className="text-xs sm:text-sm font-medium text-secondary-600 dark:text-secondary-400 truncate">
Avg. Margin
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Average profit margin
</p>
<p className="text-lg sm:text-2xl font-bold text-secondary-900 dark:text-white truncate">
{Math.round(products.reduce((acc, p) => acc + p.margin, 0) / products.length)}%
</p>
</div>
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
<DollarSign className="w-5 h-5 sm:w-6 sm:h-6 text-success-600 dark:text-success-400" />
</div>
</div>
</div>
<div className="card p-4 sm:p-6">
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<p className="text-xs sm:text-sm font-medium text-secondary-600 dark:text-secondary-400 truncate">
Active Products
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Currently available
</p>
<p className="text-lg sm:text-2xl font-bold text-secondary-900 dark:text-white truncate">
{products.filter(p => p.status === 'active').length}
</p>
</div>
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
<TrendingUp className="w-5 h-5 sm:w-6 sm:h-6 text-primary-600 dark:text-primary-400" />
</div>
</div>
</div>
<div className="card p-4 sm:p-6">
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<p className="text-xs sm:text-sm font-medium text-secondary-600 dark:text-secondary-400 truncate">
Categories
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Product categories
</p>
<p className="text-lg sm:text-2xl font-bold text-secondary-900 dark:text-white truncate">
{categories.length - 1}
</p>
</div>
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-warning-100 dark:bg-warning-900 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
<Tag className="w-5 h-5 sm:w-6 sm:h-6 text-warning-600 dark:text-warning-400" />
</div>
<div className="flex items-center gap-3">
{/* View Mode Toggle */}
<div className="flex items-center bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-1">
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded-md transition-colors ${
viewMode === 'grid'
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
}`}
>
<Grid3X3 className="h-4 w-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded-md transition-colors ${
viewMode === 'list'
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
}`}
>
<List className="h-4 w-4" />
</button>
</div>
<button
onClick={handleCreateProduct}
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
>
<Plus className="h-4 w-4" />
Add Product
</button>
</div>
</div>
{/* Filters and Search */}
<div className="card p-4 sm:p-6">
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-slate-200 dark:border-slate-700 p-4">
<div className="flex flex-col lg: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-4 h-4 text-secondary-400" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-400" />
<input
type="text"
placeholder="Search products by name or description..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
placeholder="Search products..."
value={filters.search}
onChange={(e) => handleFilterChange('search', 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>
<div className="flex gap-2">
{/* Category Filter */}
<div className="lg:w-48">
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
value={filters.category}
onChange={(e) => handleFilterChange('category', e.target.value)}
className="w-full px-3 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"
>
{categories.map(category => (
<option key={category} value={category}>
{category === 'all' ? 'All Categories' : category}
</option>
))}
<option value="">All Categories</option>
<option value="cloud_storage">Cloud Storage</option>
<option value="cloud_computing">Cloud Computing</option>
<option value="cybersecurity">Cybersecurity</option>
<option value="data_analytics">Data Analytics</option>
<option value="ai_ml">AI & ML</option>
<option value="iot">IoT</option>
<option value="blockchain">Blockchain</option>
<option value="other">Other</option>
</select>
</div>
{/* Status Filter */}
<div className="lg:w-48">
<select
value={`${sortBy}-${sortOrder}`}
onChange={(e) => {
const [field, order] = e.target.value.split('-');
setSortBy(field as 'name' | 'price' | 'margin');
setSortOrder(order as 'asc' | 'desc');
}}
className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
value={filters.status}
onChange={(e) => handleFilterChange('status', e.target.value)}
className="w-full px-3 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="name-asc">Name A-Z</option>
<option value="name-desc">Name Z-A</option>
<option value="price-asc">Price Low-High</option>
<option value="price-desc">Price High-Low</option>
<option value="margin-asc">Margin Low-High</option>
<option value="margin-desc">Margin High-Low</option>
<option value="">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="draft">Draft</option>
<option value="discontinued">Discontinued</option>
</select>
</div>
</div>
</div>
{/* Product Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 sm:gap-6">
{filteredAndSortedProducts.map((product) => (
<div key={product.id} className={`card p-4 sm:p-6 group hover:shadow-lg transition-all duration-300 ${product.featured ? 'ring-2 ring-primary-500/20' : ''}`}>
{/* Product Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-3">
<div className={`relative ${getCategoryColor(product.category)} p-2 rounded-lg shadow-md`}>
<div className="w-10 h-10 rounded-lg flex items-center justify-center bg-white/20 backdrop-blur-sm">
{getCategoryIcon(product.category)}
</div>
{product.featured && (
<div className="absolute -top-1 -right-1 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full p-0.5 shadow-md">
<Star className="w-2.5 h-2.5 text-white fill-current" />
{/* Products Display */}
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden">
{isLoading ? (
<div className="p-8 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-2 text-slate-600 dark:text-slate-400">Loading products...</p>
</div>
) : products.length === 0 ? (
<div className="p-8 text-center">
<Package className="h-12 w-12 text-slate-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-slate-900 dark:text-white mb-2">No products found</h3>
<p className="text-slate-600 dark:text-slate-400">Get started by creating your first product.</p>
</div>
) : viewMode === 'grid' ? (
/* Grid View */
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{products.map((product) => (
<div key={product.id} className="bg-white dark:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 p-4 hover:shadow-lg transition-shadow">
{/* Product Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
<div className={`p-2 rounded-lg ${getCategoryColor(product.category)}`}>
{getCategoryIcon(product.category)}
</div>
<div>
<h3 className="font-medium text-slate-900 dark:text-white line-clamp-1">{product.name}</h3>
<p className="text-xs text-slate-500 dark:text-slate-400">{product.sku}</p>
</div>
</div>
)}
</div>
<div>
<h3 className="font-semibold text-base text-secondary-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
{product.name}
</h3>
<div className="flex items-center space-x-2 mt-1">
<span className="text-xs text-secondary-600 dark:text-secondary-400">
{product.category}
<div className="flex items-center gap-1">
<button
onClick={() => handleEditProduct(product)}
className="p-1 text-slate-400 hover:text-blue-600 dark:hover:text-blue-400"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleDeleteProduct(product.id)}
className="p-1 text-slate-400 hover:text-red-600 dark:hover:text-red-400"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
{/* Product Details */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">Price:</span>
<span className="font-medium text-slate-900 dark:text-white">
${(Number(product.price) || 0).toFixed(2)}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">Commission:</span>
<span className="font-medium text-slate-900 dark:text-white">
{product.commissionRate}%
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">Stock:</span>
<span className="font-medium text-slate-900 dark:text-white">
{product.stockQuantity === -1 ? 'Unlimited' : product.stockQuantity}
</span>
</div>
</div>
{/* Status Badge */}
<div className="mt-3 pt-3 border-t border-slate-200 dark:border-slate-600">
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${getStatusColor(product.status)}`}>
{product.status}
</span>
</div>
</div>
</div>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${product.status === 'active' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-400' : 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-400'}`}>
{product.status}
</span>
</div>
{/* Product Description */}
<p className="text-sm text-secondary-600 dark:text-secondary-400 mb-4 line-clamp-2">
{product.description}
</p>
{/* Pricing Information */}
<div className="space-y-3 mb-4">
<div className="flex justify-between items-center">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Base Price:</span>
<span className="font-medium text-secondary-900 dark:text-white">${product.basePrice}</span>
</div>
<div className="flex justify-between items-center p-2 bg-primary-50 dark:bg-primary-900/20 rounded-lg">
<span className="text-sm font-medium text-primary-700 dark:text-primary-300">Your Price:</span>
<span className="font-bold text-lg text-primary-600 dark:text-primary-400">
${product.currentPrice}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Profit Margin:</span>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${getMarginColor(product.margin)}`}>
{product.marginType === 'percentage' ? `${product.margin}%` : `$${product.margin}`}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Available Stock:</span>
<span className="font-medium text-secondary-900 dark:text-white">{product.stock}</span>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2 pt-3 border-t border-secondary-200 dark:border-secondary-700">
<button
onClick={() => {
setSelectedProduct(product);
setShowPricingModal(true);
}}
className="flex-1 btn btn-outline btn-sm"
>
<Edit className="w-3 h-3 mr-1" />
Edit Pricing
</button>
<button className="flex-1 btn btn-outline btn-sm">
<Eye className="w-3 h-3 mr-1" />
View Details
</button>
))}
</div>
</div>
))}
) : (
/* List View */
<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">
Product
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Category
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Price
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Commission
</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">
Stock
</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">
{products.map((product) => (
<tr key={product.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">
{getCategoryIcon(product.category)}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-slate-900 dark:text-white">
{product.name}
</div>
<div className="text-sm text-slate-500 dark:text-slate-400">
{product.sku}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center gap-1 px-2 py-1 text-xs font-semibold rounded-full ${getCategoryColor(product.category)}`}>
{getCategoryIcon(product.category)}
{product.category.replace('_', ' ')}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-slate-900 dark:text-white">
${(Number(product.price) || 0).toFixed(2)}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400">
{product.currency}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-slate-900 dark:text-white">
{product.commissionRate}%
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(product.status)}`}>
{product.status.charAt(0).toUpperCase() + product.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
{product.stockQuantity === -1 ? 'Unlimited' : product.stockQuantity}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => handleEditProduct(product)}
className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteProduct(product.id)}
className="text-red-600 hover:text-red-900 dark:hover:text-red-400"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Pagination */}
{pagination.totalPages > 1 && (
<div className="px-6 py-3 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div className="text-sm text-slate-700 dark:text-slate-300">
Showing {((pagination.currentPage - 1) * pagination.itemsPerPage) + 1} to{' '}
{Math.min(pagination.currentPage * pagination.itemsPerPage, pagination.totalItems)} of{' '}
{pagination.totalItems} results
</div>
<div className="flex gap-2">
<button
onClick={() => handlePageChange(pagination.currentPage - 1)}
disabled={pagination.currentPage === 1}
className="px-3 py-1 text-sm border border-slate-300 dark:border-slate-600 rounded disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<span className="px-3 py-1 text-sm text-slate-700 dark:text-slate-300">
Page {pagination.currentPage} of {pagination.totalPages}
</span>
<button
onClick={() => handlePageChange(pagination.currentPage + 1)}
disabled={pagination.currentPage === pagination.totalPages}
className="px-3 py-1 text-sm border border-slate-300 dark:border-slate-600 rounded disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
</div>
)}
</div>
{/* Pricing Modal */}
{showPricingModal && selectedProduct && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-secondary-800 rounded-2xl p-8 w-full max-w-lg mx-4 shadow-2xl border border-secondary-200/50 dark:border-secondary-700/50">
<div className="flex items-center space-x-4 mb-6">
<div className={`${getCategoryColor(selectedProduct.category)} p-3 rounded-xl`}>
<div className="w-10 h-10 rounded-lg flex items-center justify-center bg-white/20 backdrop-blur-sm">
{getCategoryIcon(selectedProduct.category)}
</div>
</div>
<div>
<h3 className="text-xl font-bold text-secondary-900 dark:text-white">
Edit Pricing
</h3>
<p className="text-secondary-600 dark:text-secondary-400">
{selectedProduct.name}
</p>
</div>
</div>
{/* Product Form Modal */}
{showProductForm && (
<ProductForm
product={selectedProduct}
onClose={() => setShowProductForm(false)}
onSuccess={handleProductFormSuccess}
/>
)}
<div className="space-y-6">
<div>
<label className="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-3">
Margin Type
</label>
<div className="flex gap-4">
<label className="flex items-center p-3 border border-secondary-300 dark:border-secondary-600 rounded-xl cursor-pointer hover:bg-secondary-50 dark:hover:bg-secondary-800/50 transition-colors">
<input
type="radio"
name="marginType"
value="percentage"
checked={selectedProduct.marginType === 'percentage'}
onChange={() => setSelectedProduct({...selectedProduct, marginType: 'percentage'})}
className="mr-3"
/>
<div>
<div className="font-medium text-secondary-900 dark:text-white">Percentage</div>
<div className="text-xs text-secondary-500 dark:text-secondary-400">Add margin as %</div>
</div>
</label>
<label className="flex items-center p-3 border border-secondary-300 dark:border-secondary-600 rounded-xl cursor-pointer hover:bg-secondary-50 dark:hover:bg-secondary-800/50 transition-colors">
<input
type="radio"
name="marginType"
value="fixed"
checked={selectedProduct.marginType === 'fixed'}
onChange={() => setSelectedProduct({...selectedProduct, marginType: 'fixed'})}
className="mr-3"
/>
<div>
<div className="font-medium text-secondary-900 dark:text-white">Fixed Amount</div>
<div className="text-xs text-secondary-500 dark:text-secondary-400">Add fixed $ amount</div>
</div>
</label>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-3">
Margin Value
</label>
<div className="relative">
<input
type="number"
value={selectedProduct.margin}
onChange={(e) => setSelectedProduct({...selectedProduct, margin: parseFloat(e.target.value) || 0})}
className="w-full px-4 py-3 border border-secondary-300 dark:border-secondary-600 rounded-xl bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-300"
step={selectedProduct.marginType === 'percentage' ? 0.01 : 0.01}
min="0"
/>
<span className="absolute right-4 top-1/2 transform -translate-y-1/2 text-secondary-500 font-medium">
{selectedProduct.marginType === 'percentage' ? '%' : '$'}
</span>
</div>
</div>
<div className="bg-gradient-to-r from-secondary-50 to-secondary-100 dark:from-secondary-800/50 dark:to-secondary-700/50 p-4 rounded-xl border border-secondary-200 dark:border-secondary-600">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-secondary-600 dark:text-secondary-400">Base Price:</span>
<span className="font-semibold text-secondary-900 dark:text-white">${selectedProduct.basePrice}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-secondary-600 dark:text-secondary-400">New Price:</span>
<span className="font-bold text-lg text-primary-600 dark:text-primary-400">
${selectedProduct.marginType === 'percentage'
? (selectedProduct.basePrice * (1 + selectedProduct.margin / 100)).toFixed(2)
: (selectedProduct.basePrice + selectedProduct.margin).toFixed(2)
}
</span>
</div>
</div>
</div>
<div className="flex gap-4 mt-8">
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">Confirm Delete</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Are you sure you want to delete this product? This action cannot be undone.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => setShowPricingModal(false)}
className="flex-1 btn btn-outline btn-lg"
onClick={() => {
setIsDeleteModalOpen(false);
setDeletingProductId(null);
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
Cancel
</button>
<button
onClick={() => {
handlePricingUpdate(selectedProduct.id, selectedProduct.margin, selectedProduct.marginType);
setShowPricingModal(false);
}}
className="flex-1 btn btn-primary btn-lg shadow-lg hover:shadow-xl transition-all duration-300"
onClick={confirmDelete}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Update Pricing
Delete Product
</button>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../store/hooks';
import { registerUser } from '../store/slices/authThunks';
@ -15,151 +15,341 @@ import {
Sun,
Moon,
ArrowRight,
ArrowLeft,
AlertCircle,
Cloud,
Zap
Zap,
MapPin,
Globe,
FileText,
Briefcase,
Users,
TrendingUp,
Calendar,
Hash,
CheckCircle,
Circle
} from 'lucide-react';
import { useAppSelector } from '../store/hooks';
import { RootState } from '../store';
import { toggleTheme } from '../store/slices/themeSlice';
import { cn } from '../utils/cn';
interface FormData {
firstName: string;
lastName: string;
email: string;
password: string;
confirmPassword: string;
phone: string;
company: string;
companyType: 'corporation' | 'llc' | 'partnership' | 'sole_proprietorship' | 'other' | '';
registrationNumber: string;
gstNumber: string;
panNumber: string;
address: string;
website: string;
businessLicense: string;
taxId: string;
industry: string;
yearsInBusiness: string;
annualRevenue: string;
employeeCount: string;
}
const Signup: React.FC = () => {
const [formData, setFormData] = useState({
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<FormData>({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
phone: '',
company: ''
company: '',
companyType: '',
registrationNumber: '',
gstNumber: '',
panNumber: '',
address: '',
website: '',
businessLicense: '',
taxId: '',
industry: '',
yearsInBusiness: '',
annualRevenue: '',
employeeCount: ''
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [agreedToTerms, setAgreedToTerms] = useState(false);
const [showCompanyTypeDropdown, setShowCompanyTypeDropdown] = useState(false);
const [showIndustryDropdown, setShowIndustryDropdown] = useState(false);
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme);
const totalSteps = 4;
const companyTypes = [
{ value: 'corporation', label: 'Corporation' },
{ value: 'llc', label: 'LLC' },
{ value: 'partnership', label: 'Partnership' },
{ value: 'sole_proprietorship', label: 'Sole Proprietorship' },
{ value: 'other', label: 'Other' }
];
const industries = [
'Technology Services', 'IT Consulting', 'Cloud Services',
'Software Development', 'Digital Marketing', 'E-commerce',
'Healthcare IT', 'Financial Services', 'Education Technology',
'Manufacturing', 'Retail', 'Telecommunications',
'Energy', 'Transportation', 'Real Estate',
'Media & Entertainment', 'Professional Services', 'Other'
];
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;
if (!target.closest('.dropdown-container')) {
setShowCompanyTypeDropdown(false);
setShowIndustryDropdown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
setError(''); // Clear error when user starts typing
};
const validateStep = (step: number): boolean => {
setError('');
switch (step) {
case 1:
if (!validateName(formData.firstName)) {
setError('First name must be between 2 and 50 characters');
return false;
}
if (!validateName(formData.lastName)) {
setError('Last name must be between 2 and 50 characters');
return false;
}
if (!validateEmail(formData.email)) {
setError('Please enter a valid email address');
return false;
}
if (!validatePhoneNumber(formData.phone)) {
setError('Please enter a valid phone number');
return false;
}
break;
case 2:
if (!validatePassword(formData.password)) {
setError('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
return false;
}
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
return false;
}
if (!formData.company || formData.company.trim().length === 0) {
setError('Company name is required');
return false;
}
break;
case 3:
if (!formData.companyType) {
setError('Company type is required');
return false;
}
if (!formData.registrationNumber || formData.registrationNumber.trim().length === 0) {
setError('Registration number is required');
return false;
}
if (!formData.address || formData.address.trim().length === 0) {
setError('Business address is mandatory for vendor registration');
return false;
}
if (formData.address.trim().length < 10) {
setError(`Business address must be at least 10 characters long. Current length: ${formData.address.trim().length} characters`);
return false;
}
break;
case 4:
if (!formData.industry || formData.industry.trim().length === 0) {
setError('Industry is required');
return false;
}
if (!formData.yearsInBusiness || formData.yearsInBusiness.trim().length === 0) {
setError('Years in business is required');
return false;
}
if (!formData.employeeCount || formData.employeeCount.trim().length === 0) {
setError('Number of employees is required');
return false;
}
if (isNaN(Number(formData.yearsInBusiness)) || Number(formData.yearsInBusiness) < 0) {
setError('Years in business must be a valid number');
return false;
}
if (isNaN(Number(formData.employeeCount)) || Number(formData.employeeCount) < 1) {
setError('Number of employees must be at least 1');
return false;
}
if (formData.annualRevenue && (isNaN(Number(formData.annualRevenue)) || Number(formData.annualRevenue) < 0)) {
setError('Annual revenue must be a valid number');
return false;
}
if (!agreedToTerms) {
setError('Please agree to the terms and conditions');
return false;
}
break;
}
return true;
};
const nextStep = () => {
if (validateStep(currentStep)) {
setCurrentStep(prev => Math.min(prev + 1, totalSteps));
}
};
const prevStep = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
setError('');
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
// Validation
if (!validateName(formData.firstName)) {
setError('First name must be between 2 and 50 characters');
return;
}
if (!validateName(formData.lastName)) {
setError('Last name must be between 2 and 50 characters');
return;
}
if (!validateEmail(formData.email)) {
setError('Please enter a valid email address');
return;
}
if (!validatePassword(formData.password)) {
setError('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
return;
}
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
return;
}
if (formData.phone && !validatePhoneNumber(formData.phone)) {
setError('Please enter a valid phone number (7-15 digits, optional + prefix)');
return;
}
if (!agreedToTerms) {
setError('Please agree to the terms and conditions');
if (!validateStep(currentStep)) {
return;
}
setIsLoading(true);
try {
await dispatch(registerUser({
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
password: formData.password,
phone: formData.phone,
company: formData.company,
role: 'channel_partner_admin',
userType: 'channel_partner'
})).unwrap();
try {
// Prepare the registration data, only including fields with actual values
const registrationData: any = {
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
password: formData.password,
phone: formData.phone,
company: formData.company,
// Vendor-specific fields
companyType: formData.companyType || undefined,
registrationNumber: formData.registrationNumber,
address: formData.address,
industry: formData.industry,
yearsInBusiness: formData.yearsInBusiness,
employeeCount: formData.employeeCount,
role: 'channel_partner_admin',
userType: 'channel_partner'
};
// Navigate to login page with success message
navigate('/login', {
state: {
message: 'Registration successful! You can now login.'
}
});
} catch (err: any) {
const errorMessage = err.message || 'An error occurred during signup. Please try again.';
setError(errorMessage);
toast.error(errorMessage);
} finally {
setIsLoading(false);
// Only add optional fields if they have values
if (formData.gstNumber && formData.gstNumber.trim()) {
registrationData.gstNumber = formData.gstNumber;
}
if (formData.panNumber && formData.panNumber.trim()) {
registrationData.panNumber = formData.panNumber;
}
if (formData.website && formData.website.trim()) {
registrationData.website = formData.website;
}
if (formData.businessLicense && formData.businessLicense.trim()) {
registrationData.businessLicense = formData.businessLicense;
}
if (formData.taxId && formData.taxId.trim()) {
registrationData.taxId = formData.taxId;
}
if (formData.annualRevenue && formData.annualRevenue.trim()) {
registrationData.annualRevenue = formData.annualRevenue;
}
await dispatch(registerUser(registrationData)).unwrap();
navigate('/login', {
state: {
message: 'Registration successful! You can now login.'
}
});
} catch (err: any) {
const errorMessage = err.message || 'An error occurred during signup. Please try again.';
setError(errorMessage);
toast.error(errorMessage);
} finally {
setIsLoading(false);
}
};
const handleThemeToggle = () => {
dispatch(toggleTheme());
};
const renderStepIndicator = () => {
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 flex items-center justify-center p-4">
{/* Theme Toggle */}
<button
onClick={handleThemeToggle}
className="fixed top-6 right-6 p-3 rounded-full bg-white/80 dark:bg-slate-800 backdrop-blur-sm border border-slate-200 dark:border-slate-700 shadow-lg hover:shadow-xl transition-all duration-300 z-50"
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{theme === 'dark' ? (
<Sun className="w-5 h-5 text-amber-500" />
) : (
<Moon className="w-5 h-5 text-slate-600" />
)}
</button>
<div className="w-full max-w-2xl">
{/* Logo and Header */}
<div className="text-center mb-8">
<div className="relative inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-600 via-indigo-600 to-purple-600 rounded-3xl shadow-2xl mb-6 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent"></div>
<div className="relative flex items-center justify-center">
<div className="relative">
<Cloud className="w-10 h-10 text-white drop-shadow-lg" />
<Zap className="w-5 h-5 text-yellow-300 absolute -top-1 -right-1 drop-shadow-lg" />
<div className="flex items-center justify-center mb-8">
{Array.from({ length: totalSteps }, (_, index) => (
<div key={index} className="flex items-center">
<div className={cn(
"flex items-center justify-center w-8 h-8 rounded-full border-2 transition-all duration-200",
currentStep > index + 1
? "bg-green-500 border-green-500 text-white"
: currentStep === index + 1
? "bg-blue-500 border-blue-500 text-white"
: "bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-500"
)}>
{currentStep > index + 1 ? (
<CheckCircle className="w-5 h-5" />
) : (
<span className="text-sm font-medium">{index + 1}</span>
)}
</div>
{index < totalSteps - 1 && (
<div className={cn(
"w-12 h-0.5 mx-2 transition-all duration-200",
currentStep > index + 1 ? "bg-green-500" : "bg-slate-300 dark:bg-slate-600"
)} />
)}
</div>
))}
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Join Cloudtopiaa Connect
</h1>
);
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Personal Information
</h2>
<p className="text-slate-600 dark:text-slate-400">
Partner with us to offer next-gen cloud services.
Let's start with your basic information
</p>
</div>
{/* Signup Form */}
<div className="bg-white/80 dark:bg-slate-800 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700 p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Name Fields */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
@ -202,7 +392,6 @@ const Signup: React.FC = () => {
</div>
</div>
{/* Email and Phone */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
@ -244,29 +433,21 @@ const Signup: React.FC = () => {
</div>
</div>
</div>
</div>
);
{/* Company */}
case 2:
return (
<div className="space-y-6">
<div>
<label htmlFor="company" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Company Name
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Building className="h-5 w-5 text-slate-400" />
</div>
<input
id="company"
type="text"
value={formData.company}
onChange={(e) => handleInputChange('company', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter company name"
required
/>
</div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Account Security
</h2>
<p className="text-slate-600 dark:text-slate-400">
Create a secure password and provide company information
</p>
</div>
{/* Password Fields */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
@ -331,6 +512,317 @@ const Signup: React.FC = () => {
</div>
</div>
<div>
<label htmlFor="company" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Company Name
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Building className="h-5 w-5 text-slate-400" />
</div>
<input
id="company"
type="text"
value={formData.company}
onChange={(e) => handleInputChange('company', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter company name"
required
/>
</div>
</div>
</div>
);
case 3:
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Business Details
</h2>
<p className="text-slate-600 dark:text-slate-400">
Provide your business registration and contact information
</p>
</div>
<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-2">
Company Type
</label>
<div className="relative dropdown-container">
<button
type="button"
onClick={() => setShowCompanyTypeDropdown(!showCompanyTypeDropdown)}
className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
>
<div className="flex items-center">
<Briefcase className="h-5 w-5 text-slate-400 mr-3" />
<span className={formData.companyType ? 'text-slate-900 dark:text-white' : 'text-slate-500 dark:text-slate-400'}>
{formData.companyType ? companyTypes.find(t => t.value === formData.companyType)?.label : 'Select company type'}
</span>
</div>
<svg className="h-5 w-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showCompanyTypeDropdown && (
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-xl shadow-lg max-h-48 overflow-y-auto">
{companyTypes.map((type) => (
<button
key={type.value}
type="button"
onClick={() => {
handleInputChange('companyType', type.value);
setShowCompanyTypeDropdown(false);
}}
className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
>
{type.label}
</button>
))}
</div>
)}
</div>
</div>
<div>
<label htmlFor="registrationNumber" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Registration Number
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<FileText className="h-5 w-5 text-slate-400" />
</div>
<input
id="registrationNumber"
type="text"
value={formData.registrationNumber}
onChange={(e) => handleInputChange('registrationNumber', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., 1234567890"
required
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="gstNumber" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
GST Number (Optional)
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Hash className="h-5 w-5 text-slate-400" />
</div>
<input
id="gstNumber"
type="text"
value={formData.gstNumber}
onChange={(e) => handleInputChange('gstNumber', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., 24ABCDE1234F1Z9"
/>
</div>
</div>
<div>
<label htmlFor="panNumber" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
PAN Number (Optional)
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Hash className="h-5 w-5 text-slate-400" />
</div>
<input
id="panNumber"
type="text"
value={formData.panNumber}
onChange={(e) => handleInputChange('panNumber', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., ABCDE1234F"
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="address" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Business Address
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MapPin className="h-5 w-5 text-slate-400" />
</div>
<input
id="address"
type="text"
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter complete business address (min. 10 characters)"
required
/>
</div>
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-slate-500 dark:text-slate-400">
Minimum 10 characters required
</span>
<span className={cn(
"text-xs font-medium",
formData.address.length < 10
? "text-red-500"
: formData.address.length >= 10
? "text-green-500"
: "text-slate-500 dark:text-slate-400"
)}>
{formData.address.length}/10
</span>
</div>
</div>
<div>
<label htmlFor="website" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Website (Optional)
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Globe className="h-5 w-5 text-slate-400" />
</div>
<input
id="website"
type="url"
value={formData.website}
onChange={(e) => handleInputChange('website', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="https://example.com"
/>
</div>
</div>
</div>
</div>
);
case 4:
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Business Profile
</h2>
<p className="text-slate-600 dark:text-slate-400">
Complete your business profile and agree to terms
</p>
</div>
<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-2">
Industry
</label>
<div className="relative dropdown-container">
<button
type="button"
onClick={() => setShowIndustryDropdown(!showIndustryDropdown)}
className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
>
<div className="flex items-center">
<TrendingUp className="h-5 w-5 text-slate-400 mr-3" />
<span className={formData.industry ? 'text-slate-900 dark:text-white' : 'text-slate-500 dark:text-slate-400'}>
{formData.industry || 'Select industry'}
</span>
</div>
<svg className="h-5 w-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showIndustryDropdown && (
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-xl shadow-lg max-h-48 overflow-y-auto">
{industries.map((industry) => (
<button
key={industry}
type="button"
onClick={() => {
handleInputChange('industry', industry);
setShowIndustryDropdown(false);
}}
className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
>
{industry}
</button>
))}
</div>
)}
</div>
</div>
<div>
<label htmlFor="yearsInBusiness" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Years in Business
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Calendar className="h-5 w-5 text-slate-400" />
</div>
<input
id="yearsInBusiness"
type="number"
value={formData.yearsInBusiness}
onChange={(e) => handleInputChange('yearsInBusiness', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., 5"
required
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="annualRevenue" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Annual Revenue (Optional)
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<TrendingUp className="h-5 w-5 text-slate-400" />
</div>
<input
id="annualRevenue"
type="number"
value={formData.annualRevenue}
onChange={(e) => handleInputChange('annualRevenue', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., 1000000"
/>
</div>
</div>
<div>
<label htmlFor="employeeCount" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Number of Employees
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Users className="h-5 w-5 text-slate-400" />
</div>
<input
id="employeeCount"
type="number"
value={formData.employeeCount}
onChange={(e) => handleInputChange('employeeCount', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., 50"
required
/>
</div>
</div>
</div>
{/* Terms and Conditions */}
<div className="flex items-start">
<input
@ -351,27 +843,101 @@ const Signup: React.FC = () => {
</Link>
</label>
</div>
</div>
);
default:
return null;
}
};
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 flex items-center justify-center p-4">
{/* Theme Toggle */}
<button
onClick={handleThemeToggle}
className="fixed top-6 right-6 p-3 rounded-full bg-white/80 dark:bg-slate-800 backdrop-blur-sm border border-slate-200 dark:border-slate-700 shadow-lg hover:shadow-xl transition-all duration-300 z-50"
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{theme === 'dark' ? (
<Sun className="w-5 h-5 text-amber-500" />
) : (
<Moon className="w-5 h-5 text-slate-600" />
)}
</button>
<div className="w-full max-w-2xl">
{/* Logo and Header */}
<div className="text-center mb-8">
<div className="relative inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-600 via-indigo-600 to-purple-600 rounded-3xl shadow-2xl mb-6 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent"></div>
<div className="relative flex items-center justify-center">
<div className="relative">
<Cloud className="w-10 h-10 text-white drop-shadow-lg" />
<Zap className="w-5 h-5 text-yellow-300 absolute -top-1 -right-1 drop-shadow-lg" />
</div>
</div>
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Join Cloudtopiaa Connect
</h1>
<p className="text-slate-600 dark:text-slate-400">
Partner with us to offer next-gen cloud services.
</p>
</div>
{/* Step Indicator */}
{renderStepIndicator()}
{/* Signup Form */}
<div className="bg-white/80 dark:bg-slate-800 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700 p-8">
<form onSubmit={handleSubmit}>
{renderStepContent()}
{/* Error Message */}
{error && (
<div className="flex items-center p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-center p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg mt-6">
<AlertCircle className="h-5 w-5 text-red-500 mr-2 flex-shrink-0" />
<span className="text-sm text-red-700 dark:text-red-400">{error}</span>
</div>
)}
{/* Submit Button */}
{/* Navigation Buttons */}
<div className="flex items-center justify-between mt-8">
<button
type="button"
onClick={prevStep}
disabled={currentStep === 1}
className={cn(
"flex items-center px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200",
currentStep === 1 && "opacity-50 cursor-not-allowed"
)}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Previous
</button>
{currentStep < totalSteps ? (
<button
type="button"
onClick={nextStep}
className="flex items-center px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
>
Next
<ArrowRight className="ml-2 h-4 w-4" />
</button>
) : (
<button
type="submit"
disabled={isLoading}
className={cn(
"w-full flex items-center justify-center px-4 py-3 border border-transparent rounded-xl font-medium text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-lg hover:shadow-xl",
"flex items-center px-6 py-2 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-lg font-medium hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-lg hover:shadow-xl",
isLoading && "opacity-75 cursor-not-allowed"
)}
>
{isLoading ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Creating account...
</div>
) : (
@ -381,6 +947,8 @@ const Signup: React.FC = () => {
</div>
)}
</button>
)}
</div>
</form>
{/* Sign In Link */}

View File

@ -0,0 +1,989 @@
import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../store/hooks';
import { registerUser } from '../store/slices/authThunks';
import toast from 'react-hot-toast';
import { validatePhoneNumber, validateEmail, validatePassword, validateName } from '../utils/validation';
import {
Eye,
EyeOff,
Mail,
Lock,
User,
Building,
Phone,
Sun,
Moon,
ArrowRight,
ArrowLeft,
AlertCircle,
Cloud,
Zap,
MapPin,
Globe,
FileText,
Briefcase,
Users,
TrendingUp,
Calendar,
Hash,
CheckCircle
} from 'lucide-react';
import { useAppSelector } from '../store/hooks';
import { RootState } from '../store';
import { toggleTheme } from '../store/slices/themeSlice';
import { cn } from '../utils/cn';
interface FormData {
firstName: string;
lastName: string;
email: string;
password: string;
confirmPassword: string;
phone: string;
company: string;
companyType: 'corporation' | 'llc' | 'partnership' | 'sole_proprietorship' | 'other' | '';
registrationNumber: string;
gstNumber: string;
panNumber: string;
address: string;
website: string;
businessLicense: string;
taxId: string;
industry: string;
yearsInBusiness: string;
annualRevenue: string;
employeeCount: string;
}
const SignupStepwise: React.FC = () => {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<FormData>({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
phone: '',
company: '',
companyType: '',
registrationNumber: '',
gstNumber: '',
panNumber: '',
address: '',
website: '',
businessLicense: '',
taxId: '',
industry: '',
yearsInBusiness: '',
annualRevenue: '',
employeeCount: ''
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [agreedToTerms, setAgreedToTerms] = useState(false);
const [showCompanyTypeDropdown, setShowCompanyTypeDropdown] = useState(false);
const [showIndustryDropdown, setShowIndustryDropdown] = useState(false);
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme);
const totalSteps = 4;
const companyTypes = [
{ value: 'corporation', label: 'Corporation' },
{ value: 'llc', label: 'LLC' },
{ value: 'partnership', label: 'Partnership' },
{ value: 'sole_proprietorship', label: 'Sole Proprietorship' },
{ value: 'other', label: 'Other' }
];
const industries = [
'Technology Services', 'IT Consulting', 'Cloud Services',
'Software Development', 'Digital Marketing', 'E-commerce',
'Healthcare IT', 'Financial Services', 'Education Technology',
'Manufacturing', 'Retail', 'Telecommunications',
'Energy', 'Transportation', 'Real Estate',
'Media & Entertainment', 'Professional Services', 'Other'
];
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;
if (!target.closest('.dropdown-container')) {
setShowCompanyTypeDropdown(false);
setShowIndustryDropdown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
setError(''); // Clear error when user starts typing
};
const validateStep = (step: number): boolean => {
setError('');
switch (step) {
case 1:
if (!validateName(formData.firstName)) {
setError('First name must be between 2 and 50 characters');
return false;
}
if (!validateName(formData.lastName)) {
setError('Last name must be between 2 and 50 characters');
return false;
}
if (!validateEmail(formData.email)) {
setError('Please enter a valid email address');
return false;
}
if (!validatePhoneNumber(formData.phone)) {
setError('Please enter a valid phone number');
return false;
}
break;
case 2:
if (!validatePassword(formData.password)) {
setError('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
return false;
}
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
return false;
}
if (!formData.company || formData.company.trim().length === 0) {
setError('Company name is required');
return false;
}
break;
case 3:
if (!formData.companyType) {
setError('Company type is required');
return false;
}
if (!formData.registrationNumber || formData.registrationNumber.trim().length === 0) {
setError('Registration number is required');
return false;
}
if (!formData.address || formData.address.trim().length === 0) {
setError('Business address is required');
return false;
}
if (formData.address.trim().length < 10) {
setError(`Business address must be at least 10 characters long. Current length: ${formData.address.trim().length} characters`);
return false;
}
break;
case 4:
if (!formData.industry || formData.industry.trim().length === 0) {
setError('Industry is required');
return false;
}
if (!formData.yearsInBusiness || formData.yearsInBusiness.trim().length === 0) {
setError('Years in business is required');
return false;
}
if (!formData.employeeCount || formData.employeeCount.trim().length === 0) {
setError('Number of employees is required');
return false;
}
if (isNaN(Number(formData.yearsInBusiness)) || Number(formData.yearsInBusiness) < 0) {
setError('Years in business must be a valid number');
return false;
}
if (isNaN(Number(formData.employeeCount)) || Number(formData.employeeCount) < 1) {
setError('Number of employees must be at least 1');
return false;
}
if (formData.annualRevenue && (isNaN(Number(formData.annualRevenue)) || Number(formData.annualRevenue) < 0)) {
setError('Annual revenue must be a valid number');
return false;
}
if (!agreedToTerms) {
setError('Please agree to the terms and conditions');
return false;
}
break;
}
return true;
};
const nextStep = () => {
if (validateStep(currentStep)) {
setCurrentStep(prev => Math.min(prev + 1, totalSteps));
}
};
const prevStep = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
setError('');
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateStep(currentStep)) {
return;
}
setIsLoading(true);
try {
// Prepare the registration data, only including fields with actual values
const registrationData: any = {
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
password: formData.password,
phone: formData.phone,
company: formData.company,
companyType: formData.companyType || undefined,
registrationNumber: formData.registrationNumber,
address: formData.address,
industry: formData.industry,
yearsInBusiness: formData.yearsInBusiness,
employeeCount: formData.employeeCount,
role: 'channel_partner_admin',
userType: 'channel_partner'
};
// Only add optional fields if they have values
if (formData.gstNumber && formData.gstNumber.trim()) {
registrationData.gstNumber = formData.gstNumber;
}
if (formData.panNumber && formData.panNumber.trim()) {
registrationData.panNumber = formData.panNumber;
}
if (formData.website && formData.website.trim()) {
registrationData.website = formData.website;
}
if (formData.businessLicense && formData.businessLicense.trim()) {
registrationData.businessLicense = formData.businessLicense;
}
if (formData.taxId && formData.taxId.trim()) {
registrationData.taxId = formData.taxId;
}
if (formData.annualRevenue && formData.annualRevenue.trim()) {
registrationData.annualRevenue = formData.annualRevenue;
}
await dispatch(registerUser(registrationData)).unwrap();
navigate('/login', {
state: {
message: 'Registration successful! You can now login.'
}
});
} catch (err: any) {
const errorMessage = err.message || 'An error occurred during signup. Please try again.';
setError(errorMessage);
toast.error(errorMessage);
} finally {
setIsLoading(false);
}
};
const handleThemeToggle = () => {
dispatch(toggleTheme());
};
const renderStepIndicator = () => {
return (
<div className="flex items-center justify-center mb-8">
{Array.from({ length: totalSteps }, (_, index) => (
<div key={index} className="flex items-center">
<div className={cn(
"flex items-center justify-center w-8 h-8 rounded-full border-2 transition-all duration-200",
currentStep > index + 1
? "bg-green-500 border-green-500 text-white"
: currentStep === index + 1
? "bg-blue-500 border-blue-500 text-white"
: "bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-500"
)}>
{currentStep > index + 1 ? (
<CheckCircle className="w-5 h-5" />
) : (
<span className="text-sm font-medium">{index + 1}</span>
)}
</div>
{index < totalSteps - 1 && (
<div className={cn(
"w-12 h-0.5 mx-2 transition-all duration-200",
currentStep > index + 1 ? "bg-green-500" : "bg-slate-300 dark:bg-slate-600"
)} />
)}
</div>
))}
</div>
);
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Personal Information
</h2>
<p className="text-slate-600 dark:text-slate-400">
Let's start with your basic information
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
First Name
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-slate-400" />
</div>
<input
id="firstName"
type="text"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter first name"
required
/>
</div>
</div>
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Last Name
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-slate-400" />
</div>
<input
id="lastName"
type="text"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter last name"
required
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Email Address
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="h-5 w-5 text-slate-400" />
</div>
<input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter email address"
required
/>
</div>
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Phone Number
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Phone className="h-5 w-5 text-slate-400" />
</div>
<input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter phone number"
required
/>
</div>
</div>
</div>
</div>
);
case 2:
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Account Security
</h2>
<p className="text-slate-600 dark:text-slate-400">
Create a secure password and provide company information
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Password
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-slate-400" />
</div>
<input
id="password"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Create password"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300" />
) : (
<Eye className="h-5 w-5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300" />
)}
</button>
</div>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Confirm Password
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-slate-400" />
</div>
<input
id="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
value={formData.confirmPassword}
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Confirm password"
required
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showConfirmPassword ? (
<EyeOff className="h-5 w-5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300" />
) : (
<Eye className="h-5 w-5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300" />
)}
</button>
</div>
</div>
</div>
<div>
<label htmlFor="company" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Company Name
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Building className="h-5 w-5 text-slate-400" />
</div>
<input
id="company"
type="text"
value={formData.company}
onChange={(e) => handleInputChange('company', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter company name"
required
/>
</div>
</div>
</div>
);
case 3:
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Business Details
</h2>
<p className="text-slate-600 dark:text-slate-400">
Provide your business registration and contact information
</p>
</div>
<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-2">
Company Type
</label>
<div className="relative dropdown-container">
<button
type="button"
onClick={() => setShowCompanyTypeDropdown(!showCompanyTypeDropdown)}
className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
>
<div className="flex items-center">
<Briefcase className="h-5 w-5 text-slate-400 mr-3" />
<span className={formData.companyType ? 'text-slate-900 dark:text-white' : 'text-slate-500 dark:text-slate-400'}>
{formData.companyType ? companyTypes.find(t => t.value === formData.companyType)?.label : 'Select company type'}
</span>
</div>
<svg className="h-5 w-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showCompanyTypeDropdown && (
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-xl shadow-lg max-h-48 overflow-y-auto">
{companyTypes.map((type) => (
<button
key={type.value}
type="button"
onClick={() => {
handleInputChange('companyType', type.value);
setShowCompanyTypeDropdown(false);
}}
className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
>
{type.label}
</button>
))}
</div>
)}
</div>
</div>
<div>
<label htmlFor="registrationNumber" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Registration Number
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<FileText className="h-5 w-5 text-slate-400" />
</div>
<input
id="registrationNumber"
type="text"
value={formData.registrationNumber}
onChange={(e) => handleInputChange('registrationNumber', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., 1234567890"
required
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="gstNumber" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
GST Number (Optional)
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Hash className="h-5 w-5 text-slate-400" />
</div>
<input
id="gstNumber"
type="text"
value={formData.gstNumber}
onChange={(e) => handleInputChange('gstNumber', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., 24ABCDE1234F1Z9"
/>
</div>
</div>
<div>
<label htmlFor="panNumber" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
PAN Number (Optional)
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Hash className="h-5 w-5 text-slate-400" />
</div>
<input
id="panNumber"
type="text"
value={formData.panNumber}
onChange={(e) => handleInputChange('panNumber', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., ABCDE1234F"
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="address" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Business Address
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MapPin className="h-5 w-5 text-slate-400" />
</div>
<input
id="address"
type="text"
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter complete business address (min. 10 characters)"
required
/>
</div>
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-slate-500 dark:text-slate-400">
Minimum 10 characters required
</span>
<span className={cn(
"text-xs font-medium",
formData.address.length < 10
? "text-red-500"
: formData.address.length >= 10
? "text-green-500"
: "text-slate-500 dark:text-slate-400"
)}>
{formData.address.length}/10
</span>
</div>
</div>
<div>
<label htmlFor="website" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Website (Optional)
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Globe className="h-5 w-5 text-slate-400" />
</div>
<input
id="website"
type="url"
value={formData.website}
onChange={(e) => handleInputChange('website', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="https://example.com"
/>
</div>
</div>
</div>
</div>
);
case 4:
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Business Profile
</h2>
<p className="text-slate-600 dark:text-slate-400">
Complete your business profile and agree to terms
</p>
</div>
<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-2">
Industry
</label>
<div className="relative dropdown-container">
<button
type="button"
onClick={() => setShowIndustryDropdown(!showIndustryDropdown)}
className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
>
<div className="flex items-center">
<TrendingUp className="h-5 w-5 text-slate-400 mr-3" />
<span className={formData.industry ? 'text-slate-900 dark:text-white' : 'text-slate-500 dark:text-slate-400'}>
{formData.industry || 'Select industry'}
</span>
</div>
<svg className="h-5 w-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showIndustryDropdown && (
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-xl shadow-lg max-h-48 overflow-y-auto">
{industries.map((industry) => (
<button
key={industry}
type="button"
onClick={() => {
handleInputChange('industry', industry);
setShowIndustryDropdown(false);
}}
className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
>
{industry}
</button>
))}
</div>
)}
</div>
</div>
<div>
<label htmlFor="yearsInBusiness" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Years in Business
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Calendar className="h-5 w-5 text-slate-400" />
</div>
<input
id="yearsInBusiness"
type="number"
value={formData.yearsInBusiness}
onChange={(e) => handleInputChange('yearsInBusiness', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., 5"
required
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="annualRevenue" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Annual Revenue (Optional)
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<TrendingUp className="h-5 w-5 text-slate-400" />
</div>
<input
id="annualRevenue"
type="number"
value={formData.annualRevenue}
onChange={(e) => handleInputChange('annualRevenue', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., 1000000"
/>
</div>
</div>
<div>
<label htmlFor="employeeCount" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Number of Employees
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Users className="h-5 w-5 text-slate-400" />
</div>
<input
id="employeeCount"
type="number"
value={formData.employeeCount}
onChange={(e) => handleInputChange('employeeCount', e.target.value)}
className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="e.g., 50"
required
/>
</div>
</div>
</div>
{/* Terms and Conditions */}
<div className="flex items-start">
<input
id="terms"
type="checkbox"
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-slate-300 dark:border-slate-600 rounded mt-1"
/>
<label htmlFor="terms" className="ml-2 text-sm text-slate-700 dark:text-slate-300">
I agree to the{' '}
<Link to="/terms" className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
Terms and Conditions
</Link>{' '}
and{' '}
<Link to="/privacy" className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
Privacy Policy
</Link>
</label>
</div>
</div>
);
default:
return null;
}
};
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 flex items-center justify-center p-4">
{/* Theme Toggle */}
<button
onClick={handleThemeToggle}
className="fixed top-6 right-6 p-3 rounded-full bg-white/80 dark:bg-slate-800 backdrop-blur-sm border border-slate-200 dark:border-slate-700 shadow-lg hover:shadow-xl transition-all duration-300 z-50"
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{theme === 'dark' ? (
<Sun className="w-5 h-5 text-amber-500" />
) : (
<Moon className="w-5 h-5 text-slate-600" />
)}
</button>
<div className="w-full max-w-2xl">
{/* Logo and Header */}
<div className="text-center mb-8">
<div className="relative inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-600 via-indigo-600 to-purple-600 rounded-3xl shadow-2xl mb-6 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent"></div>
<div className="relative flex items-center justify-center">
<div className="relative">
<Cloud className="w-10 h-10 text-white drop-shadow-lg" />
<Zap className="w-5 h-5 text-yellow-300 absolute -top-1 -right-1 drop-shadow-lg" />
</div>
</div>
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Join Cloudtopiaa Connect
</h1>
<p className="text-slate-600 dark:text-slate-400">
Partner with us to offer next-gen cloud services.
</p>
</div>
{/* Step Indicator */}
{renderStepIndicator()}
{/* Signup Form */}
<div className="bg-white/80 dark:bg-slate-800 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700 p-8">
<form onSubmit={handleSubmit}>
{renderStepContent()}
{/* Error Message */}
{error && (
<div className="flex items-center p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg mt-6">
<AlertCircle className="h-5 w-5 text-red-500 mr-2 flex-shrink-0" />
<span className="text-sm text-red-700 dark:text-red-400">{error}</span>
</div>
)}
{/* Navigation Buttons */}
<div className="flex items-center justify-between mt-8">
<button
type="button"
onClick={prevStep}
disabled={currentStep === 1}
className={cn(
"flex items-center px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200",
currentStep === 1 && "opacity-50 cursor-not-allowed"
)}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Previous
</button>
{currentStep < totalSteps ? (
<button
type="button"
onClick={nextStep}
className="flex items-center px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
>
Next
<ArrowRight className="ml-2 h-4 w-4" />
</button>
) : (
<button
type="submit"
disabled={isLoading}
className={cn(
"flex items-center px-6 py-2 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-lg font-medium hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-lg hover:shadow-xl",
isLoading && "opacity-75 cursor-not-allowed"
)}
>
{isLoading ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Creating account...
</div>
) : (
<div className="flex items-center">
Create Account
<ArrowRight className="ml-2 h-4 w-4" />
</div>
)}
</button>
)}
</div>
</form>
{/* Sign In Link */}
<div className="mt-6 text-center">
<p className="text-sm text-slate-600 dark:text-slate-400">
Already have an account?{' '}
<Link
to="/login"
className="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors duration-200"
>
Sign in here
</Link>
</p>
</div>
{/* Switch to Reseller */}
<div className="mt-4 text-center">
<p className="text-sm text-slate-600 dark:text-slate-400">
Are you a Reseller?{' '}
<Link
to="/reseller/signup"
className="font-medium text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 transition-colors duration-200"
>
Sign up here
</Link>
</p>
</div>
</div>
{/* Footer */}
<div className="mt-8 text-center">
<p className="text-xs text-slate-500 dark:text-slate-400">
© 2025 Cloudtopiaa. All rights reserved.
</p>
</div>
</div>
</div>
);
};
export default SignupStepwise;

View File

@ -70,6 +70,8 @@ const Training: React.FC = () => {
const [selectedModuleForAdd, setSelectedModuleForAdd] = useState<string>('');
const [showAnalytics, setShowAnalytics] = useState(false);
const [currentView, setCurrentView] = useState<'training' | 'admin' | 'analytics'>('training');
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deletingItem, setDeletingItem] = useState<{ type: 'module' | 'video' | 'material', id: string, moduleId?: string } | null>(null);
const trainingModules: TrainingModule[] = [
{
@ -319,23 +321,43 @@ const Training: React.FC = () => {
};
const handleDeleteModule = (moduleId: string) => {
if (window.confirm('Are you sure you want to delete this training module? This action cannot be undone.')) {
// Handle module deletion
console.log('Deleting module:', moduleId);
}
setDeletingItem({ type: 'module', id: moduleId });
setIsDeleteModalOpen(true);
};
const handleDeleteVideo = (moduleId: string, videoId: string) => {
if (window.confirm('Are you sure you want to delete this video?')) {
// Handle video deletion
console.log('Deleting video:', videoId, 'from module:', moduleId);
}
setDeletingItem({ type: 'video', id: videoId, moduleId });
setIsDeleteModalOpen(true);
};
const handleDeleteMaterial = (moduleId: string, materialId: string) => {
if (window.confirm('Are you sure you want to delete this material?')) {
// Handle material deletion
console.log('Deleting material:', materialId, 'from module:', moduleId);
setDeletingItem({ type: 'material', id: materialId, moduleId });
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
if (!deletingItem) return;
try {
if (deletingItem.type === 'module') {
// Handle module deletion
console.log('Deleting module:', deletingItem.id);
// Here you would typically make an API call to delete the module
} else if (deletingItem.type === 'video') {
// Handle video deletion
console.log('Deleting video:', deletingItem.id, 'from module:', deletingItem.moduleId);
// Here you would typically make an API call to delete the video
} else if (deletingItem.type === 'material') {
// Handle material deletion
console.log('Deleting material:', deletingItem.id, 'from module:', deletingItem.moduleId);
// Here you would typically make an API call to delete the material
}
// Close modal and reset state
setIsDeleteModalOpen(false);
setDeletingItem(null);
} catch (error) {
console.error('Error deleting item:', error);
}
};
@ -358,10 +380,8 @@ const Training: React.FC = () => {
};
const handleModuleDelete = (id: string) => {
if (window.confirm('Are you sure you want to delete this module? This action cannot be undone.')) {
console.log('Deleting module:', id);
// Here you would typically update your state or make an API call
}
setDeletingItem({ type: 'module', id });
setIsDeleteModalOpen(true);
};
const handleVideoAdd = (moduleId: string, video: Omit<TrainingVideo, 'id'>) => {
@ -380,10 +400,8 @@ const Training: React.FC = () => {
};
const handleVideoDelete = (moduleId: string, videoId: string) => {
if (window.confirm('Are you sure you want to delete this video?')) {
console.log('Deleting video:', moduleId, videoId);
// Here you would typically update your state or make an API call
}
setDeletingItem({ type: 'video', id: videoId, moduleId });
setIsDeleteModalOpen(true);
};
const handleMaterialAdd = (moduleId: string, material: Omit<TrainingMaterial, 'id'>) => {
@ -402,10 +420,8 @@ const Training: React.FC = () => {
};
const handleMaterialDelete = (moduleId: string, materialId: string) => {
if (window.confirm('Are you sure you want to delete this material?')) {
console.log('Deleting material:', moduleId, materialId);
// Here you would typically update your state or make an API call
}
setDeletingItem({ type: 'material', id: materialId, moduleId });
setIsDeleteModalOpen(true);
};
const handleExportReport = (type: 'pdf' | 'excel' | 'csv') => {
@ -1134,6 +1150,37 @@ const Training: React.FC = () => {
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">Confirm Delete</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
{deletingItem?.type === 'module' && 'Are you sure you want to delete this training module? This action cannot be undone.'}
{deletingItem?.type === 'video' && 'Are you sure you want to delete this video? This action cannot be undone.'}
{deletingItem?.type === 'material' && 'Are you sure you want to delete this material? This action cannot be undone.'}
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => {
setIsDeleteModalOpen(false);
setDeletingItem(null);
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
Cancel
</button>
<button
onClick={confirmDelete}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Delete {deletingItem?.type === 'module' ? 'Module' : deletingItem?.type === 'video' ? 'Video' : 'Material'}
</button>
</div>
</div>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,289 @@
import React, { useState, useEffect } from 'react';
import {
BarChart3,
Users,
Building,
Package,
TrendingUp,
TrendingDown,
DollarSign,
Activity,
Calendar,
Target
} from 'lucide-react';
interface AnalyticsData {
userGrowth: {
total: number;
growth: number;
trend: 'up' | 'down';
};
revenue: {
total: number;
growth: number;
trend: 'up' | 'down';
};
vendorApprovals: {
total: number;
pending: number;
approved: number;
rejected: number;
};
productPerformance: {
totalProducts: number;
activeProducts: number;
topPerforming: Array<{
name: string;
revenue: number;
sales: number;
}>;
};
systemMetrics: {
uptime: number;
responseTime: number;
errorRate: number;
};
}
const AdminAnalytics: React.FC = () => {
const [analyticsData, setAnalyticsData] = useState<AnalyticsData>({
userGrowth: { total: 0, growth: 0, trend: 'up' },
revenue: { total: 0, growth: 0, trend: 'up' },
vendorApprovals: { total: 0, pending: 0, approved: 0, rejected: 0 },
productPerformance: { totalProducts: 0, activeProducts: 0, topPerforming: [] },
systemMetrics: { uptime: 0, responseTime: 0, errorRate: 0 }
});
const [loading, setLoading] = useState(true);
const [timeRange, setTimeRange] = useState('30d');
useEffect(() => {
fetchAnalyticsData();
}, [timeRange]);
const fetchAnalyticsData = async () => {
try {
setLoading(true);
// Mock data for now - replace with actual API call
const mockData: AnalyticsData = {
userGrowth: {
total: 1247,
growth: 12.5,
trend: 'up'
},
revenue: {
total: 284750,
growth: 8.3,
trend: 'up'
},
vendorApprovals: {
total: 156,
pending: 23,
approved: 128,
rejected: 5
},
productPerformance: {
totalProducts: 89,
activeProducts: 67,
topPerforming: [
{ name: 'Cloud Storage Pro', revenue: 45000, sales: 156 },
{ name: 'Database Hosting', revenue: 38000, sales: 89 },
{ name: 'Load Balancer', revenue: 32000, sales: 67 }
]
},
systemMetrics: {
uptime: 99.9,
responseTime: 245,
errorRate: 0.1
}
};
setAnalyticsData(mockData);
} catch (error) {
console.error('Error fetching analytics data:', error);
} finally {
setLoading(false);
}
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
};
const getTrendIcon = (trend: 'up' | 'down') => {
return trend === 'up' ? (
<TrendingUp className="h-4 w-4 text-green-600" />
) : (
<TrendingDown className="h-4 w-4 text-red-600" />
);
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="p-6 space-y-6 max-w-full">
{/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center space-y-4 sm:space-y-0">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Analytics Dashboard</h1>
<p className="text-gray-600 dark:text-gray-400">System-wide performance and insights</p>
</div>
<div className="flex items-center space-x-2">
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md bg-white dark:bg-gray-800 dark:border-gray-600"
>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
<option value="1y">Last year</option>
</select>
</div>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<h3 className="text-sm font-medium">Total Users</h3>
<Users className="h-4 w-4 text-gray-500" />
</div>
<div className="pt-2">
<div className="text-2xl font-bold">{(analyticsData.userGrowth.total || 0).toLocaleString()}</div>
<div className="flex items-center text-xs text-gray-500">
{getTrendIcon(analyticsData.userGrowth.trend)}
<span className="ml-1">{analyticsData.userGrowth.growth}% from last month</span>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<h3 className="text-sm font-medium">Total Revenue</h3>
<DollarSign className="h-4 w-4 text-gray-500" />
</div>
<div className="pt-2">
<div className="text-2xl font-bold">{formatCurrency(analyticsData.revenue.total)}</div>
<div className="flex items-center text-xs text-gray-500">
{getTrendIcon(analyticsData.revenue.trend)}
<span className="ml-1">{analyticsData.revenue.growth}% from last month</span>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<h3 className="text-sm font-medium">Active Products</h3>
<Package className="h-4 w-4 text-gray-500" />
</div>
<div className="pt-2">
<div className="text-2xl font-bold">{analyticsData.productPerformance.activeProducts}</div>
<div className="text-xs text-gray-500">
of {analyticsData.productPerformance.totalProducts} total products
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<h3 className="text-sm font-medium">System Uptime</h3>
<Activity className="h-4 w-4 text-gray-500" />
</div>
<div className="pt-2">
<div className="text-2xl font-bold">{analyticsData.systemMetrics.uptime}%</div>
<div className="text-xs text-gray-500">
Avg response: {analyticsData.systemMetrics.responseTime}ms
</div>
</div>
</div>
</div>
{/* Vendor Approvals */}
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="mb-4">
<h3 className="flex items-center text-lg font-semibold">
<Building className="h-5 w-5 mr-2" />
Vendor Approval Status
</h3>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm">Total Requests</span>
<span className="font-semibold">{analyticsData.vendorApprovals.total}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-yellow-600">Pending</span>
<span className="font-semibold">{analyticsData.vendorApprovals.pending}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-green-600">Approved</span>
<span className="font-semibold">{analyticsData.vendorApprovals.approved}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-red-600">Rejected</span>
<span className="font-semibold">{analyticsData.vendorApprovals.rejected}</span>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="mb-4">
<h3 className="flex items-center text-lg font-semibold">
<BarChart3 className="h-5 w-5 mr-2" />
Top Performing Products
</h3>
</div>
<div className="space-y-4">
{analyticsData.productPerformance.topPerforming.map((product, index) => (
<div key={index} className="flex justify-between items-center">
<div>
<div className="font-medium text-sm">{product.name}</div>
<div className="text-xs text-gray-500">{product.sales} sales</div>
</div>
<div className="text-right">
<div className="font-semibold">{formatCurrency(product.revenue)}</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* System Health */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="mb-4">
<h3 className="flex items-center text-lg font-semibold">
<Activity className="h-5 w-5 mr-2" />
System Health Metrics
</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 lg:gap-6">
<div className="text-center">
<div className="text-2xl font-bold text-green-600">{analyticsData.systemMetrics.uptime}%</div>
<div className="text-sm text-gray-500">Uptime</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">{analyticsData.systemMetrics.responseTime}ms</div>
<div className="text-sm text-gray-500">Avg Response Time</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-orange-600">{analyticsData.systemMetrics.errorRate}%</div>
<div className="text-sm text-gray-500">Error Rate</div>
</div>
</div>
</div>
</div>
);
};
export default AdminAnalytics;

View File

@ -40,6 +40,8 @@ const ChannelPartners: React.FC = () => {
const [selectedPartner, setSelectedPartner] = useState<ChannelPartner | null>(null);
const [showModal, setShowModal] = useState(false);
const [editingPartner, setEditingPartner] = useState<ChannelPartner | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deletingPartnerId, setDeletingPartnerId] = useState<string | null>(null);
useEffect(() => {
fetchChannelPartners();
@ -47,9 +49,10 @@ const ChannelPartners: React.FC = () => {
const fetchChannelPartners = async () => {
try {
const response = await fetch('/api/admin/channel-partners', {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/channel-partners`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
@ -65,31 +68,38 @@ const ChannelPartners: React.FC = () => {
};
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')}`
}
});
setDeletingPartnerId(partnerId);
setIsDeleteModalOpen(true);
};
if (response.ok) {
fetchChannelPartners();
const confirmDelete = async () => {
if (!deletingPartnerId) return;
try {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/channel-partners/${deletingPartnerId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
}
} catch (error) {
console.error('Error deleting channel partner:', error);
});
if (response.ok) {
fetchChannelPartners();
setIsDeleteModalOpen(false);
setDeletingPartnerId(null);
}
} 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}`, {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/channel-partners/${partnerId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
},
body: JSON.stringify(updateData)
});
@ -318,7 +328,7 @@ const ChannelPartners: React.FC = () => {
{/* Partner Details Modal */}
{selectedPartner && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<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">
@ -426,6 +436,35 @@ const ChannelPartners: React.FC = () => {
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">Confirm Delete</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Are you sure you want to delete this channel partner? This action cannot be undone.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => {
setIsDeleteModalOpen(false);
setDeletingPartnerId(null);
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
Cancel
</button>
<button
onClick={confirmDelete}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Delete Partner
</button>
</div>
</div>
</div>
)}
</div>
</div>
);

View File

@ -15,41 +15,69 @@ import {
Bell
} from 'lucide-react';
import { useAppSelector } from '../../store/hooks';
import { VendorRequest } from '../../types/vendor';
import VendorDetailsModal from '../../components/VendorDetailsModal';
import VendorRejectionModal from '../../components/VendorRejectionModal';
interface DashboardStats {
totalUsers: number;
pendingVendors: number;
totalChannelPartners: number;
totalRegisteredVendors: number;
totalResellers: number;
recentRequests: number;
approvedToday: number;
rejectedToday: number;
revenue: number;
userBreakdown: {
systemAdmins: number;
vendors: number;
resellers: number;
inactiveUsers: number;
};
todayStats: {
approvedToday: number;
rejectedToday: number;
};
recentActivity: Array<{
id: number;
type: string;
title: string;
message: string;
createdAt: string;
priority: string;
}>;
}
interface PendingVendor {
id: string;
firstName: string;
lastName: string;
email: string;
company: string;
createdAt: string;
status: 'pending' | 'approved' | 'rejected';
}
// Use the shared VendorRequest interface instead of PendingVendor
const AdminDashboard: React.FC = () => {
const [stats, setStats] = useState<DashboardStats>({
totalUsers: 0,
pendingVendors: 0,
totalChannelPartners: 0,
totalRegisteredVendors: 0,
totalResellers: 0,
recentRequests: 0,
approvedToday: 0,
rejectedToday: 0,
revenue: 0
revenue: 0,
userBreakdown: {
systemAdmins: 0,
vendors: 0,
resellers: 0,
inactiveUsers: 0
},
todayStats: {
approvedToday: 0,
rejectedToday: 0
},
recentActivity: []
});
const [pendingVendors, setPendingVendors] = useState<PendingVendor[]>([]);
const [pendingVendors, setPendingVendors] = useState<VendorRequest[]>([]);
const [loading, setLoading] = useState(true);
const [selectedVendor, setSelectedVendor] = useState<VendorRequest | null>(null);
const [showVendorModal, setShowVendorModal] = useState(false);
const [rejectionReason, setRejectionReason] = useState('');
const [showRejectionModal, setShowRejectionModal] = useState(false);
const { user } = useAppSelector((state) => state.auth);
useEffect(() => {
@ -59,27 +87,28 @@ const AdminDashboard: React.FC = () => {
const fetchDashboardData = async () => {
try {
// Fetch dashboard stats
const statsResponse = await fetch('/api/admin/dashboard', {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/dashboard`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
const statsData = await statsResponse.json();
const statsData = await response.json();
if (statsData.success) {
setStats(statsData.data);
setStats(statsData.data.stats);
}
// Fetch pending vendors
const vendorsResponse = await fetch('/api/admin/pending-vendors', {
const vendorsResponse = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/pending-vendors`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
}
});
const vendorsData = await vendorsResponse.json();
if (vendorsData.success) {
setPendingVendors(vendorsData.data.slice(0, 5)); // Show only first 5
setPendingVendors(vendorsData.data.pendingRequests?.slice(0, 5) || []); // Show only first 5
}
} catch (error) {
console.error('Error fetching dashboard data:', error);
@ -90,11 +119,11 @@ const AdminDashboard: React.FC = () => {
const handleApproveVendor = async (vendorId: string) => {
try {
const response = await fetch(`/api/admin/vendors/${vendorId}/approve`, {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/vendors/${vendorId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
},
body: JSON.stringify({ reason: 'Approved by admin' })
});
@ -107,25 +136,40 @@ const AdminDashboard: React.FC = () => {
}
};
const handleRejectVendor = async (vendorId: string) => {
const handleRejectVendor = async (vendorId: string, reason: string = 'Rejected by admin') => {
try {
const response = await fetch(`/api/admin/vendors/${vendorId}/reject`, {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/vendors/${vendorId}/reject`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
},
body: JSON.stringify({ reason: 'Rejected by admin' })
body: JSON.stringify({ reason })
});
if (response.ok) {
fetchDashboardData(); // Refresh data
setShowRejectionModal(false);
setRejectionReason('');
}
} catch (error) {
console.error('Error rejecting vendor:', error);
}
};
const handleViewVendorDetails = (vendor: VendorRequest) => {
setSelectedVendor(vendor);
setShowVendorModal(true);
};
const handleRejectWithReason = (vendorId: string) => {
const vendor = pendingVendors.find(v => v.id === vendorId);
if (vendor) {
setSelectedVendor(vendor);
setShowRejectionModal(true);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
@ -151,12 +195,30 @@ const AdminDashboard: React.FC = () => {
<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 className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-2">Total Users</p>
<p className="text-3xl font-bold text-slate-900 dark:text-white mb-4">{stats.totalUsers || 0}</p>
<div className="space-y-2">
<div className="flex justify-between items-center py-1 px-2 bg-slate-50 dark:bg-slate-700 rounded-md">
<span className="text-xs text-slate-600 dark:text-slate-400">System Admins</span>
<span className="text-xs font-semibold text-blue-600 dark:text-blue-400">{stats.userBreakdown?.systemAdmins || 0}</span>
</div>
<div className="flex justify-between items-center py-1 px-2 bg-slate-50 dark:bg-slate-700 rounded-md">
<span className="text-xs text-slate-600 dark:text-slate-400">Vendors</span>
<span className="text-xs font-semibold text-green-600 dark:text-green-400">{stats.userBreakdown?.vendors || 0}</span>
</div>
<div className="flex justify-between items-center py-1 px-2 bg-slate-50 dark:bg-slate-700 rounded-md">
<span className="text-xs text-slate-600 dark:text-slate-400">Resellers</span>
<span className="text-xs font-semibold text-purple-600 dark:text-purple-400">{stats.userBreakdown?.resellers || 0}</span>
</div>
<div className="flex justify-between items-center py-1 px-2 bg-red-50 dark:bg-red-900/20 rounded-md">
<span className="text-xs text-slate-600 dark:text-slate-400">Inactive Users</span>
<span className="text-xs font-semibold text-red-600 dark:text-red-400">{stats.userBreakdown?.inactiveUsers || 0}</span>
</div>
</div>
</div>
<div className="p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
<div className="p-3 bg-blue-100 dark:bg-blue-900 rounded-lg ml-4">
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
@ -167,7 +229,7 @@ const AdminDashboard: React.FC = () => {
<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>
<p className="text-2xl font-bold text-amber-600 dark:text-amber-400">{stats.pendingVendors || 0}</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" />
@ -179,8 +241,8 @@ const AdminDashboard: React.FC = () => {
<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>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Registered Vendors</p>
<p className="text-2xl font-bold text-emerald-600 dark:text-emerald-400">{stats.totalRegisteredVendors || 0}</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" />
@ -193,7 +255,7 @@ const AdminDashboard: React.FC = () => {
<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>
<p className="text-2xl font-bold text-green-600 dark:text-green-400">${(stats.revenue || 0).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" />
@ -241,13 +303,14 @@ const AdminDashboard: React.FC = () => {
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
</button>
<button
onClick={() => handleRejectVendor(vendor.id)}
onClick={() => handleRejectWithReason(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
onClick={() => handleViewVendorDetails(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"
>
@ -265,35 +328,71 @@ const AdminDashboard: React.FC = () => {
<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>
{stats.recentActivity && stats.recentActivity.length > 0 ? (
stats.recentActivity.slice(0, 5).map((activity) => {
const getActivityIcon = (type: string) => {
switch (type) {
case 'NEW_VENDOR_REQUEST':
return <Users className="w-4 h-4 text-blue-600 dark:text-blue-400" />;
case 'VENDOR_APPROVED':
return <UserCheck className="w-4 h-4 text-green-600 dark:text-green-400" />;
case 'VENDOR_REJECTED':
return <UserX className="w-4 h-4 text-red-600 dark:text-red-400" />;
case 'NEW_RESELLER_REQUEST':
return <Building className="w-4 h-4 text-purple-600 dark:text-purple-400" />;
default:
return <Activity className="w-4 h-4 text-gray-600 dark:text-gray-400" />;
}
};
<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>
const getActivityColor = (type: string) => {
switch (type) {
case 'NEW_VENDOR_REQUEST':
return 'bg-blue-100 dark:bg-blue-900';
case 'VENDOR_APPROVED':
return 'bg-green-100 dark:bg-green-900';
case 'VENDOR_REJECTED':
return 'bg-red-100 dark:bg-red-900';
case 'NEW_RESELLER_REQUEST':
return 'bg-purple-100 dark:bg-purple-900';
default:
return 'bg-gray-100 dark:bg-gray-900';
}
};
<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" />
const formatTimeAgo = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
if (diffInMinutes < 1) return 'Just now';
if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)} hours ago`;
return `${Math.floor(diffInMinutes / 1440)} days ago`;
};
return (
<div key={activity.id} className="flex items-center space-x-3">
<div className={`w-8 h-8 ${getActivityColor(activity.type)} rounded-full flex items-center justify-center`}>
{getActivityIcon(activity.type)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-900 dark:text-white truncate">
{activity.title}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
{formatTimeAgo(activity.createdAt)}
</p>
</div>
</div>
);
})
) : (
<div className="text-center py-4">
<Activity className="w-8 h-8 text-slate-400 mx-auto mb-2" />
<p className="text-sm text-slate-500 dark:text-slate-400">No recent activity</p>
</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>
@ -307,7 +406,7 @@ const AdminDashboard: React.FC = () => {
</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>
<p className="text-2xl font-bold text-green-600 dark:text-green-400">{stats.todayStats?.approvedToday || 0}</p>
</div>
</div>
</div>
@ -319,7 +418,7 @@ const AdminDashboard: React.FC = () => {
</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>
<p className="text-2xl font-bold text-red-600 dark:text-red-400">{stats.todayStats?.rejectedToday || 0}</p>
</div>
</div>
</div>
@ -337,6 +436,246 @@ const AdminDashboard: React.FC = () => {
</div>
</div>
</div>
{/* Vendor Details Modal */}
{showVendorModal && selectedVendor && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-2xl p-6 max-w-md w-full mx-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">Vendor Details</h3>
<button
onClick={() => setShowVendorModal(false)}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
<XCircle className="w-5 h-5" />
</button>
</div>
<div className="space-y-4 max-h-96 overflow-y-auto">
{/* Basic Information */}
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Name</p>
<p className="text-slate-900 dark:text-white font-medium">{selectedVendor.firstName} {selectedVendor.lastName}</p>
</div>
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Email</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.email}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Phone</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.phone || 'Not provided'}</p>
</div>
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Company</p>
<p className="text-slate-900 dark:text-white font-medium">{selectedVendor.company}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Role</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.role ? selectedVendor.role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) : 'Vendor'}</p>
</div>
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Registration Date</p>
<p className="text-slate-900 dark:text-white">{new Date(selectedVendor.createdAt).toLocaleDateString()}</p>
</div>
</div>
{/* Business Information */}
{(selectedVendor.companyType || selectedVendor.industry || selectedVendor.yearsInBusiness) && (
<div className="border-t border-slate-200 dark:border-slate-700 pt-4">
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">Business Information</h4>
<div className="grid grid-cols-2 gap-4">
{selectedVendor.companyType && (
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Company Type</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.companyType}</p>
</div>
)}
{selectedVendor.industry && (
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Industry</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.industry}</p>
</div>
)}
{selectedVendor.yearsInBusiness && (
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Years in Business</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.yearsInBusiness} years</p>
</div>
)}
{selectedVendor.employeeCount && (
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Employee Count</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.employeeCount}</p>
</div>
)}
</div>
</div>
)}
{/* Financial Information */}
{(selectedVendor.annualRevenue || selectedVendor.taxId) && (
<div className="border-t border-slate-200 dark:border-slate-700 pt-4">
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">Financial Information</h4>
<div className="grid grid-cols-2 gap-4">
{selectedVendor.annualRevenue && (
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Annual Revenue</p>
<p className="text-slate-900 dark:text-white">${selectedVendor.annualRevenue.toLocaleString()}</p>
</div>
)}
{selectedVendor.taxId && (
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Tax ID</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.taxId}</p>
</div>
)}
</div>
</div>
)}
{/* Legal Information */}
{(selectedVendor.registrationNumber || selectedVendor.gstNumber || selectedVendor.panNumber || selectedVendor.businessLicense) && (
<div className="border-t border-slate-200 dark:border-slate-700 pt-4">
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">Legal Information</h4>
<div className="grid grid-cols-2 gap-4">
{selectedVendor.registrationNumber && (
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Registration Number</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.registrationNumber}</p>
</div>
)}
{selectedVendor.gstNumber && (
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">GST Number</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.gstNumber}</p>
</div>
)}
{selectedVendor.panNumber && (
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">PAN Number</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.panNumber}</p>
</div>
)}
{selectedVendor.businessLicense && (
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Business License</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.businessLicense}</p>
</div>
)}
</div>
</div>
)}
{/* Address Information */}
{selectedVendor.address && (
<div className="border-t border-slate-200 dark:border-slate-700 pt-4">
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">Address Information</h4>
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-1">Address</p>
<p className="text-slate-900 dark:text-white">{selectedVendor.address}</p>
</div>
</div>
)}
{/* Rejection Reason */}
{selectedVendor.rejectionReason && (
<div className="border-t border-slate-200 dark:border-slate-700 pt-4">
<h4 className="text-sm font-semibold text-red-600 dark:text-red-400 mb-3">Rejection Information</h4>
<div>
<p className="text-sm font-medium text-red-600 dark:text-red-400 mb-1">Rejection Reason</p>
<p className="text-red-600 dark:text-red-400">{selectedVendor.rejectionReason}</p>
</div>
</div>
)}
</div>
<div className="flex space-x-3 mt-6">
<button
onClick={() => handleApproveVendor(selectedVendor.id)}
className="flex-1 bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 transition-colors"
>
Approve
</button>
<button
onClick={() => {
setShowVendorModal(false);
handleRejectWithReason(selectedVendor.id);
}}
className="flex-1 bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 transition-colors"
>
Reject
</button>
</div>
</div>
</div>
)}
{/* Rejection Reason Modal */}
{showRejectionModal && selectedVendor && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-2xl p-6 max-w-md w-full mx-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">Reject Vendor</h3>
<button
onClick={() => setShowRejectionModal(false)}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
<XCircle className="w-5 h-5" />
</button>
</div>
<div className="mb-4">
<p className="text-sm text-slate-600 dark:text-slate-400 mb-2">
Rejecting: {selectedVendor.firstName} {selectedVendor.lastName} ({selectedVendor.company})
</p>
<label className="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-2">
Reason for rejection
</label>
<textarea
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
className="w-full p-3 border border-slate-300 dark:border-slate-600 rounded-md bg-white dark:bg-slate-700 text-slate-900 dark:text-white"
rows={3}
placeholder="Enter reason for rejection..."
/>
</div>
<div className="flex space-x-3">
<button
onClick={() => setShowRejectionModal(false)}
className="flex-1 bg-slate-300 dark:bg-slate-600 text-slate-700 dark:text-slate-300 py-2 px-4 rounded-md hover:bg-slate-400 dark:hover:bg-slate-500 transition-colors"
>
Cancel
</button>
<button
onClick={() => handleRejectVendor(selectedVendor.id, rejectionReason || 'Rejected by admin')}
className="flex-1 bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 transition-colors"
>
Reject
</button>
</div>
</div>
</div>
)}
{/* Shared Vendor Modals */}
<VendorDetailsModal
vendor={selectedVendor}
isOpen={showVendorModal}
onClose={() => setShowVendorModal(false)}
onApprove={handleApproveVendor}
onReject={handleRejectVendor}
/>
<VendorRejectionModal
vendor={selectedVendor}
isOpen={showRejectionModal}
onClose={() => setShowRejectionModal(false)}
onReject={handleRejectVendor}
/>
</div>
);
};

View File

@ -0,0 +1,433 @@
import React, { useState, useEffect } from 'react';
import {
MessageSquare,
AlertCircle,
CheckCircle,
Clock,
Filter,
Search,
Eye,
XCircle,
Star,
Bug,
Lightbulb,
HelpCircle
} from 'lucide-react';
interface Feedback {
id: number;
type: 'bug' | 'feature' | 'question' | 'general';
priority: 'low' | 'medium' | 'high' | 'critical';
status: 'open' | 'in_progress' | 'resolved' | 'closed';
title: string;
description: string;
submittedBy: string;
submittedAt: string;
assignedTo?: string;
resolvedAt?: string;
resolution?: string;
systemInfo?: any;
}
const AdminFeedback: React.FC = () => {
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
const [loading, setLoading] = useState(true);
const [selectedFeedback, setSelectedFeedback] = useState<Feedback | null>(null);
const [showModal, setShowModal] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'open' | 'in_progress' | 'resolved' | 'closed'>('all');
const [typeFilter, setTypeFilter] = useState<'all' | 'bug' | 'feature' | 'question' | 'general'>('all');
const [priorityFilter, setPriorityFilter] = useState<'all' | 'low' | 'medium' | 'high' | 'critical'>('all');
useEffect(() => {
fetchFeedbacks();
}, []);
const fetchFeedbacks = async () => {
try {
setLoading(true);
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/developer-feedback`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setFeedbacks(data.data || []);
}
} catch (error) {
console.error('Error fetching feedbacks:', error);
} finally {
setLoading(false);
}
};
const updateFeedbackStatus = async (feedbackId: number, status: string, resolution?: string) => {
try {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/developer-feedback/${feedbackId}/status`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status, resolution })
});
if (response.ok) {
fetchFeedbacks();
setShowModal(false);
}
} catch (error) {
console.error('Error updating feedback status:', error);
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'bug':
return <Bug className="w-4 h-4 text-red-500" />;
case 'feature':
return <Lightbulb className="w-4 h-4 text-blue-500" />;
case 'question':
return <HelpCircle className="w-4 h-4 text-purple-500" />;
default:
return <MessageSquare className="w-4 h-4 text-gray-500" />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'critical':
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
case 'high':
return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200';
case 'medium':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
case 'low':
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'open':
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
case 'in_progress':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
case 'resolved':
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
case 'closed':
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
}
};
const filteredFeedbacks = feedbacks.filter(feedback => {
const matchesSearch = feedback.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
feedback.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
feedback.submittedBy.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || feedback.status === statusFilter;
const matchesType = typeFilter === 'all' || feedback.type === typeFilter;
const matchesPriority = priorityFilter === 'all' || feedback.priority === priorityFilter;
return matchesSearch && matchesStatus && matchesType && matchesPriority;
});
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
return (
<div className="p-6 space-y-6 max-w-full">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Developer Feedback & Issues
</h1>
<p className="text-gray-600 dark:text-gray-400">
Manage feedback, bug reports, and feature requests from developers
</p>
</div>
{/* Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
{/* Search */}
<div className="lg:col-span-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Search feedback..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
/>
</div>
</div>
{/* Status Filter */}
<div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="all">All Status</option>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="resolved">Resolved</option>
<option value="closed">Closed</option>
</select>
</div>
{/* Type Filter */}
<div>
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value as any)}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="all">All Types</option>
<option value="bug">Bug</option>
<option value="feature">Feature</option>
<option value="question">Question</option>
<option value="general">General</option>
</select>
</div>
{/* Priority Filter */}
<div>
<select
value={priorityFilter}
onChange={(e) => setPriorityFilter(e.target.value as any)}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="all">All Priorities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
</div>
</div>
{/* Feedback List */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Feedback ({filteredFeedbacks.length})
</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Title
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Priority
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Submitted By
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredFeedbacks.map((feedback) => (
<tr key={feedback.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getTypeIcon(feedback.type)}
<span className="ml-2 text-sm text-gray-900 dark:text-white capitalize">
{feedback.type}
</span>
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{feedback.title}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 truncate max-w-xs">
{feedback.description}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getPriorityColor(feedback.priority)}`}>
{feedback.priority}
</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(feedback.status)}`}>
{feedback.status === 'in_progress' ? 'In Progress' : feedback.status.replace('_', ' ')}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{feedback.submittedBy}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{new Date(feedback.submittedAt).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => {
setSelectedFeedback(feedback);
setShowModal(true);
}}
className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
title="View Details"
>
<Eye className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Feedback Details Modal */}
{showModal && selectedFeedback && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Feedback Details
</h3>
<button
onClick={() => setShowModal(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XCircle className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Type
</label>
<div className="flex items-center">
{getTypeIcon(selectedFeedback.type)}
<span className="ml-2 text-gray-900 dark:text-white capitalize">
{selectedFeedback.type}
</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Priority
</label>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getPriorityColor(selectedFeedback.priority)}`}>
{selectedFeedback.priority}
</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Title
</label>
<p className="text-gray-900 dark:text-white font-medium">
{selectedFeedback.title}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description
</label>
<p className="text-gray-900 dark:text-white whitespace-pre-wrap">
{selectedFeedback.description}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Submitted By
</label>
<p className="text-gray-900 dark:text-white">
{selectedFeedback.submittedBy}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Submitted At
</label>
<p className="text-gray-900 dark:text-white">
{new Date(selectedFeedback.submittedAt).toLocaleString()}
</p>
</div>
</div>
{selectedFeedback.systemInfo && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
System Information
</label>
<pre className="bg-gray-100 dark:bg-gray-700 p-3 rounded text-xs text-gray-900 dark:text-white overflow-x-auto">
{JSON.stringify(selectedFeedback.systemInfo, null, 2)}
</pre>
</div>
)}
<div className="border-t pt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Update Status
</label>
<div className="flex space-x-2">
<select
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
defaultValue={selectedFeedback.status}
onChange={(e) => {
updateFeedbackStatus(selectedFeedback.id, e.target.value);
}}
>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="resolved">Resolved</option>
<option value="closed">Closed</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default AdminFeedback;

View File

@ -0,0 +1,920 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Plus, Search, Edit, Trash2, Eye, Pencil } from 'lucide-react';
import { apiService, Product } from '../../services/api';
const Products: React.FC = () => {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [categoryFilter, setCategoryFilter] = useState<string>('all');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [vendorFilter, setVendorFilter] = useState<string>('all');
const [vendors, setVendors] = useState<Array<{ id: number; firstName: string; lastName: string; company?: string }>>([]);
const [vendorMap, setVendorMap] = useState<Record<number, { firstName: string; lastName: string; company?: string }>>({});
const [viewMode, setViewMode] = useState<'grid' | 'list'>('list');
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deletingProductId, setDeletingProductId] = useState<number | null>(null);
const [editingProductId, setEditingProductId] = useState<number | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [totalItems, setTotalItems] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [formData, setFormData] = useState({
name: '',
description: '',
category: '' as Product['category'],
price: '',
commissionRate: '',
status: 'draft' as Product['status'],
availability: 'available' as Product['availability'],
features: [] as string[],
specifications: {},
purchaseUrl: ''
});
const categories = [
{ value: 'cloud_storage', label: 'Cloud Storage' },
{ value: 'cloud_computing', label: 'Cloud Computing' },
{ value: 'cybersecurity', label: 'Cybersecurity' },
{ value: 'data_analytics', label: 'Data Analytics' },
{ value: 'ai_ml', label: 'AI & Machine Learning' },
{ value: 'iot', label: 'Internet of Things' },
{ value: 'blockchain', label: 'Blockchain' },
{ value: 'other', label: 'Other' }
];
const statuses = [
{ value: 'draft', label: 'Draft' },
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
{ value: 'discontinued', label: 'Discontinued' }
];
const availabilities = [
{ value: 'available', label: 'Available' },
{ value: 'out_of_stock', label: 'Out of Stock' },
{ value: 'coming_soon', label: 'Coming Soon' },
{ value: 'discontinued', label: 'Discontinued' }
];
const fetchProducts = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await apiService.getAllProducts({
page: currentPage,
limit: itemsPerPage,
category: categoryFilter === 'all' ? undefined : categoryFilter,
status: statusFilter === 'all' ? undefined : statusFilter,
vendor: vendorFilter === 'all' ? undefined : vendorFilter,
search: searchTerm || undefined,
sortBy: 'createdAt',
sortOrder: 'desc'
});
if (response.success) {
setProducts(response.data.products);
setTotalItems(response.data.pagination.totalItems);
setTotalPages(response.data.pagination.totalPages);
} else {
setError('Failed to fetch products');
}
} catch (error) {
console.error('Error fetching products:', error);
setError(error instanceof Error ? error.message : 'Failed to fetch products');
} finally {
setLoading(false);
}
}, [currentPage, itemsPerPage, categoryFilter, statusFilter, vendorFilter, searchTerm]);
const fetchVendors = useCallback(async () => {
try {
const response = await apiService.getActiveVendors();
if (response.success) {
setVendors(response.data);
// Build vendor map for quick lookup
const vendorMapData: Record<number, { firstName: string; lastName: string; company?: string }> = {};
response.data.forEach(vendor => {
vendorMapData[vendor.id] = {
firstName: vendor.firstName,
lastName: vendor.lastName,
company: vendor.company
};
});
setVendorMap(vendorMapData);
}
} catch (error) {
console.error('Error fetching vendors:', error);
}
}, []);
useEffect(() => {
fetchProducts();
}, [fetchProducts]);
useEffect(() => {
fetchVendors();
}, [fetchVendors]);
useEffect(() => {
if (categoryFilter !== '' || statusFilter !== '' || searchTerm !== '' || vendorFilter !== 'all') {
fetchProducts();
}
}, [categoryFilter, statusFilter, searchTerm, vendorFilter, fetchProducts]);
const handleCreateProduct = async () => {
try {
await apiService.createProduct({
...formData,
price: parseFloat(formData.price),
commissionRate: parseFloat(formData.commissionRate),
category: formData.category || 'other'
});
alert('Product created successfully');
setIsCreateModalOpen(false);
setFormData({
name: '',
description: '',
category: 'other',
price: '',
commissionRate: '',
status: 'draft',
availability: 'available',
features: [],
specifications: {},
purchaseUrl: ''
});
fetchProducts();
} catch (error) {
alert('Error creating product');
}
};
const handleUpdateProduct = async () => {
if (!editingProductId) return;
try {
await apiService.updateProduct(editingProductId, {
...formData,
price: parseFloat(formData.price),
commissionRate: parseFloat(formData.commissionRate),
category: formData.category || 'other'
});
alert('Product updated successfully');
setIsEditModalOpen(false);
setEditingProductId(null);
fetchProducts();
} catch (error) {
alert('Error updating product');
}
};
const handleDeleteProduct = async (productId: number) => {
setDeletingProductId(productId);
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
if (!deletingProductId) return;
try {
await apiService.deleteProduct(deletingProductId);
alert('Product deleted successfully');
fetchProducts();
setIsDeleteModalOpen(false);
setDeletingProductId(null);
} catch (error) {
alert('Error deleting product');
}
};
const filteredProducts = products.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(product.sku && product.sku.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesCategory = !categoryFilter || product.category === categoryFilter;
const matchesStatus = !statusFilter || product.status === statusFilter;
return matchesSearch && matchesCategory && matchesStatus;
});
const getStatusBadge = (status: string) => {
const statusConfig = {
draft: { color: 'bg-gray-100 text-gray-800', label: 'Draft' },
active: { color: 'bg-green-100 text-green-800', label: 'Active' },
inactive: { color: 'bg-yellow-100 text-yellow-800', label: 'Inactive' },
discontinued: { color: 'bg-red-100 text-red-800', label: 'Discontinued' }
};
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.draft;
return <span className={`px-2 py-1 rounded-full text-xs font-medium ${config.color}`}>{config.label}</span>;
};
const getAvailabilityBadge = (availability: string) => {
const availabilityConfig = {
available: { color: 'bg-green-100 text-green-800', label: 'Available' },
out_of_stock: { color: 'bg-red-100 text-red-800', label: 'Out of Stock' },
coming_soon: { color: 'bg-blue-100 text-blue-800', label: 'Coming Soon' },
discontinued: { color: 'bg-gray-100 text-gray-800', label: 'Discontinued' }
};
const config = availabilityConfig[availability as keyof typeof availabilityConfig] || availabilityConfig.available;
return <span className={`px-2 py-1 rounded-full text-xs font-medium ${config.color}`}>{config.label}</span>;
};
if (loading) {
return (
<div className="p-6">
<div className="flex items-center justify-center h-64">
<div className="text-lg text-gray-900 dark:text-white">Loading products...</div>
</div>
</div>
);
}
return (
<div className="p-6 space-y-6 max-w-full">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Products</h1>
<button
onClick={() => setIsCreateModalOpen(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
>
<Plus className="w-5 h-5" />
Add Product
</button>
</div>
{/* Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-4">
<input
type="text"
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="min-w-[150px] px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="all">All Categories</option>
{categories.map((category) => (
<option key={category.value} value={category.value}>
{category.label}
</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="min-w-[150px] px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="all">All Statuses</option>
<option value="draft">Draft</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="discontinued">Discontinued</option>
</select>
</div>
{/* Vendor filter on separate line */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<select
value={vendorFilter}
onChange={(e) => setVendorFilter(e.target.value)}
className="min-w-[200px] px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="all">All Vendors</option>
{vendors.map((vendor) => (
<option key={vendor.id} value={vendor.id.toString()}>
{vendor.company ? `${vendor.company} (${vendor.firstName} ${vendor.lastName})` : `${vendor.firstName} ${vendor.lastName}`}
</option>
))}
</select>
</div>
{/* View Toggle */}
<div className="flex justify-end mt-4">
<div className="flex border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
<button
onClick={() => setViewMode('list')}
className={`px-3 py-2 text-sm font-medium ${
viewMode === 'list'
? 'bg-blue-600 text-white'
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600'
}`}
>
List
</button>
<button
onClick={() => setViewMode('grid')}
className={`px-3 py-2 text-sm font-medium ${
viewMode === 'grid'
? 'bg-blue-600 text-white'
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600'
}`}
>
Grid
</button>
</div>
</div>
</div>
{/* Loading State */}
{loading && (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)}
{/* Error Display */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
<div className="flex">
<div className="flex-shrink-0">
<Trash2 className="h-5 w-5 text-red-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">Error</h3>
<div className="mt-2 text-sm text-red-700 dark:text-red-300">{error}</div>
</div>
</div>
</div>
)}
{/* Products Display */}
{!loading && products.length === 0 ? (
<div className="text-center py-12">
<div className="text-gray-500 dark:text-gray-400">No products found</div>
</div>
) : viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{products.map((product) => (
<div
key={product.id}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-md transition-shadow"
>
<div className="p-4">
<div className="flex justify-between items-start mb-2">
<h3 className="font-semibold text-gray-900 dark:text-white truncate">
{product.name}
</h3>
<div className="flex gap-2">
{(product.isAdminCreated || product.source === 'admin') && (
<span className="px-2 py-1 text-xs font-medium rounded-full bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-400">
Admin
</span>
)}
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
product.status === 'active'
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
: product.status === 'inactive'
? 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400'
: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400'
}`}
>
{product.status}
</span>
</div>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
{product.description}
</p>
<div className="space-y-2 mb-4">
<div className="flex justify-between text-sm">
<span className="text-gray-500 dark:text-gray-400">Category:</span>
<span className="text-gray-900 dark:text-white font-medium">{product.category}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500 dark:text-gray-400">Price:</span>
<span className="text-gray-900 dark:text-white font-medium">${product.price}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500 dark:text-gray-400">Vendor:</span>
<span className="text-gray-500 dark:text-gray-400">
{product.isAdminCreated || product.source === 'admin' ? (
<span className="text-blue-600 dark:text-blue-400 font-medium">From Cloudtopiaa</span>
) : product.creator ? (
product.creator.company ?
`${product.creator.company} (${product.creator.firstName} ${product.creator.lastName})` :
`${product.creator.firstName} ${product.creator.lastName}`
) : (
'Unknown Vendor'
)}
</span>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => {
setFormData({
name: product.name,
description: product.description || '',
category: product.category,
price: product.price.toString(),
commissionRate: product.commissionRate.toString(),
status: product.status,
availability: product.availability,
features: product.features || [],
specifications: product.specifications || {},
purchaseUrl: product.purchaseUrl || ''
});
setEditingProductId(product.id);
setIsEditModalOpen(true);
}}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
>
<Pencil className="w-4 h-4" />
</button>
{(product.isAdminCreated || product.source === 'admin') ? (
<button
disabled
className="p-2 text-gray-400 dark:text-gray-500 cursor-not-allowed opacity-50"
title="Admin-created products cannot be deleted"
>
<Trash2 className="w-4 h-4" />
</button>
) : (
<button
onClick={() => handleDeleteProduct(product.id)}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</div>
</div>
))}
</div>
) : (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-sm">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Product
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Vendor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Category
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Price
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Commission
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{products.map((product) => (
<tr key={product.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-lg bg-blue-100 dark:bg-blue-900 flex items-center justify-center">
<svg className="h-6 w-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
</div>
<div className="ml-4">
<div className="flex items-center gap-2">
<div className="text-sm font-medium text-gray-900 dark:text-white">{product.name}</div>
{(product.isAdminCreated || product.source === 'admin') && (
<span className="px-2 py-1 text-xs font-medium rounded-full bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-400">
Admin
</span>
)}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">{product.sku || 'N/A'}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
product.isAdminCreated || product.source === 'admin'
? 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200'
: 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
}`}>
{product.isAdminCreated || product.source === 'admin' ? (
'From Cloudtopiaa'
) : product.creator ? (
product.creator.company ?
`${product.creator.company} (${product.creator.firstName} ${product.creator.lastName})` :
`${product.creator.firstName} ${product.creator.lastName}`
) : (
'Unknown Vendor'
)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
{product.category.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
${product.price}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{product.commissionRate}%
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(product.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => {
setFormData({
name: product.name,
description: product.description || '',
category: product.category,
price: product.price.toString(),
commissionRate: product.commissionRate.toString(),
status: product.status,
availability: product.availability,
features: product.features || [],
specifications: product.specifications || {},
purchaseUrl: product.purchaseUrl || ''
});
setEditingProductId(product.id);
setIsEditModalOpen(true);
}}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
>
<Pencil className="w-4 h-4" />
</button>
{(product.isAdminCreated || product.source === 'admin') ? (
<button
disabled
className="p-2 text-gray-400 dark:text-gray-500 cursor-not-allowed opacity-50"
title="Admin-created products cannot be deleted"
>
<Trash2 className="w-4 h-4" />
</button>
) : (
<button
onClick={() => handleDeleteProduct(product.id)}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Pagination */}
{totalItems > itemsPerPage && (
<div className="flex justify-center mt-8">
<nav className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
{Array.from({ length: Math.ceil(totalItems / itemsPerPage) }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-2 text-sm font-medium rounded-lg ${
currentPage === page
? 'bg-blue-600 text-white'
: 'text-gray-500 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, Math.ceil(totalItems / itemsPerPage)))}
disabled={currentPage === Math.ceil(totalItems / itemsPerPage)}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</nav>
</div>
)}
{/* Create Product Modal */}
{isCreateModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<h2 className="text-xl font-bold mb-4">Create New Product</h2>
<pre className="bg-gray-100 dark:bg-gray-700 p-3 rounded-lg text-sm text-gray-700 dark:text-gray-300 mb-4 font-mono">
{`Note: Products created by admin will be:
Available to ALL vendors
Non-deletable by vendors
Displayed as "From Cloudtopiaa" in vendor portal`}
</pre>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Product Name</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Enter product name"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Description</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Enter product description"
rows={3}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Category</label>
<select
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value as Product['category'] })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="">Select category</option>
{categories.map((category) => (
<option key={category.value} value={category.value}>
{category.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Price</label>
<input
type="number"
step="0.01"
value={formData.price}
onChange={(e) => setFormData({ ...formData, price: e.target.value })}
placeholder="0.00"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Commission Rate (%)</label>
<input
type="number"
step="0.01"
value={formData.commissionRate}
onChange={(e) => setFormData({ ...formData, commissionRate: e.target.value })}
placeholder="10.0"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Status</label>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as Product['status'] })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
{statuses.map((status) => (
<option key={status.value} value={status.value}>
{status.label}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Availability</label>
<select
value={formData.availability}
onChange={(e) => setFormData({ ...formData, availability: e.target.value as Product['availability'] })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
{availabilities.map((availability) => (
<option key={availability.value} value={availability.value}>
{availability.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Purchase URL</label>
<input
type="url"
value={formData.purchaseUrl}
onChange={(e) => setFormData({ ...formData, purchaseUrl: e.target.value })}
placeholder="https://example.com/product"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div className="flex justify-end space-x-2 pt-4">
<button
onClick={() => setIsCreateModalOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 dark:text-white"
>
Cancel
</button>
<button
onClick={handleCreateProduct}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Create Product
</button>
</div>
</div>
</div>
</div>
)}
{/* Edit Product Modal */}
{isEditModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<h2 className="text-xl font-bold mb-4">Edit Product</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Product Name</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Enter product name"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Description</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Enter product description"
rows={3}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Category</label>
<select
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value as Product['category'] })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
{categories.map((category) => (
<option key={category.value} value={category.value}>
{category.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Price</label>
<input
type="number"
step="0.01"
value={formData.price}
onChange={(e) => setFormData({ ...formData, price: e.target.value })}
placeholder="0.00"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Commission Rate (%)</label>
<input
type="number"
step="0.01"
value={formData.commissionRate}
onChange={(e) => setFormData({ ...formData, commissionRate: e.target.value })}
placeholder="10.0"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Status</label>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as Product['status'] })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
{statuses.map((status) => (
<option key={status.value} value={status.value}>
{status.label}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Availability</label>
<select
value={formData.availability}
onChange={(e) => setFormData({ ...formData, availability: e.target.value as Product['availability'] })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
{availabilities.map((availability) => (
<option key={availability.value} value={availability.value}>
{availability.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Purchase URL</label>
<input
type="url"
value={formData.purchaseUrl}
onChange={(e) => setFormData({ ...formData, purchaseUrl: e.target.value })}
placeholder="https://example.com/product"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div className="flex justify-end space-x-2 pt-4">
<button
onClick={() => setIsEditModalOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 dark:text-white"
>
Cancel
</button>
<button
onClick={handleUpdateProduct}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Update Product
</button>
</div>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">Confirm Delete</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Are you sure you want to delete this product? This action cannot be undone.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => {
setIsDeleteModalOpen(false);
setDeletingProductId(null);
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
Cancel
</button>
<button
onClick={confirmDelete}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Delete Product
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Products;

View File

@ -0,0 +1,568 @@
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 RegisteredVendor {
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 RegisteredVendors: React.FC = () => {
const [vendors, setVendors] = useState<RegisteredVendor[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'pending' | 'suspended'>('all');
const [selectedVendor, setSelectedVendor] = useState<RegisteredVendor | null>(null);
const [showModal, setShowModal] = useState(false);
const [editingVendor, setEditingVendor] = useState<RegisteredVendor | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deletingVendorId, setDeletingVendorId] = useState<string | null>(null);
useEffect(() => {
fetchRegisteredVendors();
}, []);
const fetchRegisteredVendors = async () => {
try {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/registered-vendors`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
setVendors(data.data);
}
} catch (error) {
console.error('Error fetching registered vendors:', error);
} finally {
setLoading(false);
}
};
const handleDelete = async (vendorId: string) => {
setDeletingVendorId(vendorId);
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
if (!deletingVendorId) return;
try {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/registered-vendors/${deletingVendorId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
}
});
if (response.ok) {
fetchRegisteredVendors();
setIsDeleteModalOpen(false);
setDeletingVendorId(null);
}
} catch (error) {
console.error('Error deleting registered vendor:', error);
}
};
const handleUpdate = async (vendorId: string, updateData: Partial<RegisteredVendor>) => {
try {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/registered-vendors/${vendorId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
},
body: JSON.stringify(updateData)
});
if (response.ok) {
fetchRegisteredVendors();
setEditingVendor(null);
}
} catch (error) {
console.error('Error updating registered vendor:', error);
}
};
const getStatusColor = (status?: string) => {
switch (status) {
case 'active':
return 'text-green-600 bg-green-100 dark:bg-green-900';
case 'pending':
return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
case 'suspended':
return 'text-red-600 bg-red-100 dark:bg-red-900';
default:
return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
}
};
const getTierColor = (tier?: string) => {
switch (tier) {
case 'diamond':
return 'text-purple-600 bg-purple-100 dark:bg-purple-900';
case 'platinum':
return 'text-blue-600 bg-blue-100 dark:bg-blue-900';
case 'gold':
return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
case 'silver':
return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
case 'bronze':
return 'text-orange-600 bg-orange-100 dark:bg-orange-900';
default:
return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
}
};
const filteredVendors = vendors.filter(vendor => {
const matchesSearch = vendor.companyName.toLowerCase().includes(searchTerm.toLowerCase()) ||
vendor.contactEmail.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || vendor.status === statusFilter;
return matchesSearch && matchesStatus;
});
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="p-6 space-y-6 max-w-full">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Registered Vendors
</h1>
<p className="text-gray-600 dark:text-gray-400">
Manage vendors who have partnered with us
</p>
</div>
{/* Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex flex-col lg: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 text-gray-400 w-4 h-4" />
<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-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-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-4 h-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-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>
</div>
{/* Vendors Table */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Vendor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Contact
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Tier
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Commission
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Joined
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredVendors.map((vendor) => (
<tr key={vendor.id} className="hover:bg-gray-50 dark:hover:bg-gray-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">
<Building className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{vendor.companyName}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{vendor.companyType}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">
<div className="flex items-center">
<Mail className="w-4 h-4 mr-2 text-gray-400" />
{vendor.contactEmail}
</div>
<div className="flex items-center mt-1">
<Phone className="w-4 h-4 mr-2 text-gray-400" />
{vendor.contactPhone}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getTierColor(vendor.tier)}`}>
{vendor.tier?.charAt(0).toUpperCase() + vendor.tier?.slice(1) || 'Unknown'}
</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) || 'Unknown'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{vendor.commissionRate}%
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{new Date(vendor.createdAt).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => {
setSelectedVendor(vendor);
setShowModal(true);
}}
className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => setEditingVendor(vendor)}
className="text-green-600 hover:text-green-900 dark:hover:text-green-400"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(vendor.id)}
className="text-red-600 hover:text-red-900 dark:hover:text-red-400"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Vendor Details Modal */}
{showModal && selectedVendor && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Vendor Details
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Company Name
</label>
<p className="text-gray-900 dark:text-white">{selectedVendor.companyName}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Company Type
</label>
<p className="text-gray-900 dark:text-white">{selectedVendor.companyType}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Contact Email
</label>
<p className="text-gray-900 dark:text-white">{selectedVendor.contactEmail}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Contact Phone
</label>
<p className="text-gray-900 dark:text-white">{selectedVendor.contactPhone}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Website
</label>
<p className="text-gray-900 dark:text-white">{selectedVendor.website || 'N/A'}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tier
</label>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getTierColor(selectedVendor.tier)}`}>
{selectedVendor.tier?.charAt(0).toUpperCase() + selectedVendor.tier?.slice(1) || 'Unknown'}
</span>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(selectedVendor.status)}`}>
{selectedVendor.status?.charAt(0).toUpperCase() + selectedVendor.status?.slice(1) || 'Unknown'}
</span>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Commission Rate
</label>
<p className="text-gray-900 dark:text-white">{selectedVendor.commissionRate}%</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Created At
</label>
<p className="text-gray-900 dark:text-white">{new Date(selectedVendor.createdAt).toLocaleString()}</p>
</div>
</div>
</div>
<div className="flex justify-end p-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={() => setShowModal(false)}
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
Close
</button>
</div>
</div>
</div>
)}
{/* Edit Vendor Modal */}
{editingVendor && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Edit Vendor
</h3>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const updateData = {
companyName: formData.get('companyName') as string,
contactEmail: formData.get('contactEmail') as string,
contactPhone: formData.get('contactPhone') as string,
website: (formData.get('website') as string) || undefined,
tier: formData.get('tier') as string,
status: formData.get('status') as string,
commissionRate: parseFloat(formData.get('commissionRate') as string)
};
handleUpdate(editingVendor.id, updateData);
}}>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Company Name
</label>
<input
type="text"
name="companyName"
defaultValue={editingVendor.companyName}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Contact Email
</label>
<input
type="email"
name="contactEmail"
defaultValue={editingVendor.contactEmail}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Contact Phone
</label>
<input
type="tel"
name="contactPhone"
defaultValue={editingVendor.contactPhone}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Website
</label>
<input
type="url"
name="website"
defaultValue={editingVendor.website}
placeholder="https://example.com (optional)"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tier
</label>
<select
name="tier"
defaultValue={editingVendor.tier}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
required
>
<option value="bronze">Bronze</option>
<option value="silver">Silver</option>
<option value="gold">Gold</option>
<option value="platinum">Platinum</option>
<option value="diamond">Diamond</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
name="status"
defaultValue={editingVendor.status}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
required
>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="suspended">Suspended</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Commission Rate (%)
</label>
<input
type="number"
name="commissionRate"
defaultValue={editingVendor.commissionRate}
min="0"
max="100"
step="0.1"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
required
/>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
onClick={() => setEditingVendor(null)}
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Update Vendor
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">Confirm Delete</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Are you sure you want to delete this registered vendor? This action cannot be undone.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => {
setIsDeleteModalOpen(false);
setDeletingVendorId(null);
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
Cancel
</button>
<button
onClick={confirmDelete}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Delete Vendor
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default RegisteredVendors;

379
src/pages/admin/Reports.tsx Normal file
View File

@ -0,0 +1,379 @@
import React, { useState, useEffect } from 'react';
import {
FileText,
Download,
Calendar,
Filter,
Users,
Building,
Package,
DollarSign,
TrendingUp,
TrendingDown,
BarChart3,
PieChart,
Activity
} from 'lucide-react';
interface ReportData {
userReports: {
totalUsers: number;
newUsers: number;
activeUsers: number;
userGrowth: number;
};
vendorReports: {
totalVendors: number;
pendingVendors: number;
approvedVendors: number;
rejectedVendors: number;
};
revenueReports: {
totalRevenue: number;
monthlyRevenue: number;
revenueGrowth: number;
topProducts: Array<{
name: string;
revenue: number;
sales: number;
}>;
};
systemReports: {
uptime: number;
responseTime: number;
errorRate: number;
activeSessions: number;
};
}
const AdminReports: React.FC = () => {
const [reportData, setReportData] = useState<ReportData>({
userReports: { totalUsers: 0, newUsers: 0, activeUsers: 0, userGrowth: 0 },
vendorReports: { totalVendors: 0, pendingVendors: 0, approvedVendors: 0, rejectedVendors: 0 },
revenueReports: { totalRevenue: 0, monthlyRevenue: 0, revenueGrowth: 0, topProducts: [] },
systemReports: { uptime: 0, responseTime: 0, errorRate: 0, activeSessions: 0 }
});
const [loading, setLoading] = useState(true);
const [reportType, setReportType] = useState('overview');
const [dateRange, setDateRange] = useState('30d');
useEffect(() => {
fetchReportData();
}, [reportType, dateRange]);
const fetchReportData = async () => {
try {
setLoading(true);
// Mock data for now - replace with actual API call
const mockData: ReportData = {
userReports: {
totalUsers: 1247,
newUsers: 89,
activeUsers: 892,
userGrowth: 12.5
},
vendorReports: {
totalVendors: 156,
pendingVendors: 23,
approvedVendors: 128,
rejectedVendors: 5
},
revenueReports: {
totalRevenue: 284750,
monthlyRevenue: 45600,
revenueGrowth: 8.3,
topProducts: [
{ name: 'Cloud Storage Pro', revenue: 45000, sales: 156 },
{ name: 'Database Hosting', revenue: 38000, sales: 89 },
{ name: 'Load Balancer', revenue: 32000, sales: 67 },
{ name: 'CDN Service', revenue: 28000, sales: 45 },
{ name: 'Backup Solution', revenue: 25000, sales: 34 }
]
},
systemReports: {
uptime: 99.9,
responseTime: 245,
errorRate: 0.1,
activeSessions: 456
}
};
setReportData(mockData);
} catch (error) {
console.error('Error fetching report data:', error);
} finally {
setLoading(false);
}
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
};
const getGrowthIcon = (growth: number) => {
return growth >= 0 ? (
<TrendingUp className="h-4 w-4 text-green-600" />
) : (
<TrendingDown className="h-4 w-4 text-red-600" />
);
};
const handleExportReport = (type: string) => {
// Mock export functionality
console.log(`Exporting ${type} report for ${dateRange}`);
// In real implementation, this would trigger a download
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="p-6 space-y-6 max-w-full">
{/* Header */}
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center space-y-4 lg:space-y-0">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Reports & Analytics</h1>
<p className="text-gray-600 dark:text-gray-400">Comprehensive system reports and insights</p>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center space-y-2 sm:space-y-0 sm:space-x-2">
<select
value={reportType}
onChange={(e) => setReportType(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md bg-white dark:bg-gray-800 dark:border-gray-600"
>
<option value="overview">Overview</option>
<option value="users">User Reports</option>
<option value="vendors">Vendor Reports</option>
<option value="revenue">Revenue Reports</option>
<option value="system">System Reports</option>
</select>
<select
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md bg-white dark:bg-gray-800 dark:border-gray-600"
>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
<option value="1y">Last year</option>
</select>
<button
onClick={() => handleExportReport(reportType)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center space-x-2"
>
<Download className="h-4 w-4" />
<span>Export</span>
</button>
</div>
</div>
{/* Overview Report */}
{reportType === 'overview' && (
<div className="space-y-6">
{/* Key Metrics */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Users</p>
<p className="text-2xl font-bold">{(reportData.userReports.totalUsers || 0).toLocaleString()}</p>
<div className="flex items-center text-xs text-gray-500">
{getGrowthIcon(reportData.userReports.userGrowth)}
<span className="ml-1">{reportData.userReports.userGrowth}% growth</span>
</div>
</div>
<Users className="h-8 w-8 text-blue-600" />
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Revenue</p>
<p className="text-2xl font-bold">{formatCurrency(reportData.revenueReports.totalRevenue)}</p>
<div className="flex items-center text-xs text-gray-500">
{getGrowthIcon(reportData.revenueReports.revenueGrowth)}
<span className="ml-1">{reportData.revenueReports.revenueGrowth}% growth</span>
</div>
</div>
<DollarSign className="h-8 w-8 text-green-600" />
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Active Vendors</p>
<p className="text-2xl font-bold">{reportData.vendorReports.approvedVendors}</p>
<p className="text-xs text-gray-500">{reportData.vendorReports.pendingVendors} pending</p>
</div>
<Building className="h-8 w-8 text-purple-600" />
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">System Uptime</p>
<p className="text-2xl font-bold">{reportData.systemReports.uptime}%</p>
<p className="text-xs text-gray-500">{reportData.systemReports.activeSessions} active sessions</p>
</div>
<Activity className="h-8 w-8 text-orange-600" />
</div>
</div>
</div>
{/* Top Products */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center">
<BarChart3 className="h-5 w-5 mr-2" />
Top Performing Products
</h3>
</div>
<div className="space-y-4">
{reportData.revenueReports.topProducts.map((product, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<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">
<span className="text-sm font-semibold text-blue-600 dark:text-blue-400">{index + 1}</span>
</div>
<div>
<p className="font-medium">{product.name}</p>
<p className="text-sm text-gray-500">{product.sales} sales</p>
</div>
</div>
<div className="text-right">
<p className="font-semibold">{formatCurrency(product.revenue)}</p>
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* User Reports */}
{reportType === 'users' && (
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<Users className="h-5 w-5 mr-2" />
User Statistics
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 lg:gap-6">
<div className="text-center">
<div className="text-3xl font-bold text-blue-600">{reportData.userReports.totalUsers}</div>
<div className="text-sm text-gray-500">Total Users</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-green-600">{reportData.userReports.newUsers}</div>
<div className="text-sm text-gray-500">New This Month</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-purple-600">{reportData.userReports.activeUsers}</div>
<div className="text-sm text-gray-500">Active Users</div>
</div>
</div>
</div>
</div>
)}
{/* Vendor Reports */}
{reportType === 'vendors' && (
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<Building className="h-5 w-5 mr-2" />
Vendor Statistics
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
<div className="text-center">
<div className="text-3xl font-bold text-blue-600">{reportData.vendorReports.totalVendors}</div>
<div className="text-sm text-gray-500">Total Vendors</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-yellow-600">{reportData.vendorReports.pendingVendors}</div>
<div className="text-sm text-gray-500">Pending</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-green-600">{reportData.vendorReports.approvedVendors}</div>
<div className="text-sm text-gray-500">Approved</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-red-600">{reportData.vendorReports.rejectedVendors}</div>
<div className="text-sm text-gray-500">Rejected</div>
</div>
</div>
</div>
</div>
)}
{/* Revenue Reports */}
{reportType === 'revenue' && (
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<DollarSign className="h-5 w-5 mr-2" />
Revenue Statistics
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 lg:gap-6">
<div className="text-center">
<div className="text-3xl font-bold text-green-600">{formatCurrency(reportData.revenueReports.totalRevenue)}</div>
<div className="text-sm text-gray-500">Total Revenue</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-blue-600">{formatCurrency(reportData.revenueReports.monthlyRevenue)}</div>
<div className="text-sm text-gray-500">This Month</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-purple-600">{reportData.revenueReports.revenueGrowth}%</div>
<div className="text-sm text-gray-500">Growth Rate</div>
</div>
</div>
</div>
</div>
)}
{/* System Reports */}
{reportType === 'system' && (
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<Activity className="h-5 w-5 mr-2" />
System Health
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
<div className="text-center">
<div className="text-3xl font-bold text-green-600">{reportData.systemReports.uptime}%</div>
<div className="text-sm text-gray-500">Uptime</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-blue-600">{reportData.systemReports.responseTime}ms</div>
<div className="text-sm text-gray-500">Response Time</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-orange-600">{reportData.systemReports.errorRate}%</div>
<div className="text-sm text-gray-500">Error Rate</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-purple-600">{reportData.systemReports.activeSessions}</div>
<div className="text-sm text-gray-500">Active Sessions</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default AdminReports;

View File

@ -0,0 +1,520 @@
import React, { useState, useEffect } from 'react';
import {
Users,
Building,
Plus,
Edit,
Trash2,
Eye,
Search,
Filter,
MapPin,
Mail,
Phone,
Globe,
TrendingUp,
Calendar,
Link
} from 'lucide-react';
interface Reseller {
id: string;
companyName: string;
contactEmail: string;
contactPhone: string;
website: string;
tier: string;
status: string;
commissionRate: number;
channelPartnerId: string;
channelPartner?: {
id: string;
companyName: string;
status: string;
};
users?: Array<{
id: string;
firstName: string;
lastName: string;
email: string;
role: string;
status: string;
}>;
createdAt: string;
approvedAt?: string;
}
const Resellers: React.FC = () => {
const [resellers, setResellers] = useState<Reseller[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'pending' | 'suspended'>('all');
const [vendorFilter, setVendorFilter] = useState<string>('all');
const [vendors, setVendors] = useState<Array<{id: string, company: string}>>([]);
const [selectedReseller, setSelectedReseller] = useState<Reseller | null>(null);
const [showModal, setShowModal] = useState(false);
const [editingReseller, setEditingReseller] = useState<Reseller | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deletingResellerId, setDeletingResellerId] = useState<string | null>(null);
useEffect(() => {
fetchResellers();
fetchVendors();
}, []);
const fetchVendors = async () => {
try {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/active-vendors`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
setVendors(data.data);
}
} catch (error) {
console.error('Error fetching vendors:', error);
}
};
const fetchResellers = async () => {
try {
const params = new URLSearchParams();
if (vendorFilter !== 'all') {
params.append('vendor', vendorFilter);
}
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/resellers?${params}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
setResellers(data.data);
}
} catch (error) {
console.error('Error fetching resellers:', error);
} finally {
setLoading(false);
}
};
// Refetch resellers when vendor filter changes
useEffect(() => {
if (vendors.length > 0) {
fetchResellers();
}
}, [vendorFilter]);
const handleDelete = async (resellerId: string) => {
setDeletingResellerId(resellerId);
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
if (!deletingResellerId) return;
try {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/resellers/${deletingResellerId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
}
});
if (response.ok) {
fetchResellers();
setIsDeleteModalOpen(false);
setDeletingResellerId(null);
}
} catch (error) {
console.error('Error deleting reseller:', error);
}
};
const handleUpdate = async (resellerId: string, updateData: Partial<Reseller>) => {
try {
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/resellers/${resellerId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
},
body: JSON.stringify(updateData)
});
if (response.ok) {
fetchResellers();
setEditingReseller(null);
}
} catch (error) {
console.error('Error updating reseller:', error);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active':
return 'text-green-600 bg-green-100 dark:bg-green-900';
case 'pending':
return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
case 'suspended':
return 'text-red-600 bg-red-100 dark:bg-red-900';
default:
return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
}
};
const getTierColor = (tier: string) => {
switch (tier) {
case 'diamond':
return 'text-purple-600 bg-purple-100 dark:bg-purple-900';
case 'platinum':
return 'text-blue-600 bg-blue-100 dark:bg-blue-900';
case 'gold':
return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
case 'silver':
return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
case 'bronze':
return 'text-orange-600 bg-orange-100 dark:bg-orange-900';
default:
return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
}
};
const filteredResellers = resellers.filter(reseller => {
const matchesSearch = reseller.companyName.toLowerCase().includes(searchTerm.toLowerCase()) ||
reseller.contactEmail.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || reseller.status === statusFilter;
const matchesVendor = vendorFilter === 'all' || reseller.channelPartnerId === vendorFilter;
return matchesSearch && matchesStatus && matchesVendor;
});
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="p-6 space-y-6 max-w-full">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Resellers
</h1>
<p className="text-gray-600 dark:text-gray-400">
Manage resellers who register with our vendors
</p>
</div>
{/* Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex flex-col lg: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 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Search resellers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-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-4 h-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-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>
{/* Vendor Filter */}
<div className="flex items-center space-x-2">
<Building className="w-4 h-4 text-gray-400" />
<select
value={vendorFilter}
onChange={(e) => setVendorFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Vendors</option>
{vendors.map((vendor) => (
<option key={vendor.id} value={vendor.id}>
{vendor.company}
</option>
))}
</select>
</div>
</div>
</div>
{/* Resellers Table */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Reseller
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Contact
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Vendor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Tier
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Commission
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Joined
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredResellers.map((reseller) => (
<tr key={reseller.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-10 h-10 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center">
<Users className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{reseller.companyName}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Reseller
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">
<div className="flex items-center">
<Mail className="w-4 h-4 mr-2 text-gray-400" />
{reseller.contactEmail}
</div>
<div className="flex items-center mt-1">
<Phone className="w-4 h-4 mr-2 text-gray-400" />
{reseller.contactPhone}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<Link className="w-4 h-4 mr-2 text-gray-400" />
<span className="text-sm text-gray-900 dark:text-white">
{reseller.channelPartner?.companyName || 'Unknown Vendor'}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getTierColor(reseller.tier)}`}>
{reseller.tier?.charAt(0).toUpperCase() + reseller.tier?.slice(1) || 'Unknown'}
</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(reseller.status)}`}>
{reseller.status?.charAt(0).toUpperCase() + reseller.status?.slice(1) || 'Unknown'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{reseller.commissionRate}%
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{new Date(reseller.createdAt).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => {
setSelectedReseller(reseller);
setShowModal(true);
}}
className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => setEditingReseller(reseller)}
className="text-green-600 hover:text-green-900 dark:hover:text-green-400"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(reseller.id)}
className="text-red-600 hover:text-red-900 dark:hover:text-red-400"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Reseller Details Modal */}
{showModal && selectedReseller && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Reseller Details
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Company Name
</label>
<p className="text-gray-900 dark:text-white">{selectedReseller.companyName}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Contact Email
</label>
<p className="text-gray-900 dark:text-white">{selectedReseller.contactEmail}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Contact Phone
</label>
<p className="text-gray-900 dark:text-white">{selectedReseller.contactPhone}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Website
</label>
<p className="text-gray-900 dark:text-white">{selectedReseller.website || 'N/A'}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Associated Vendor
</label>
<p className="text-gray-900 dark:text-white">{selectedReseller.channelPartner?.companyName || 'Unknown Vendor'}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tier
</label>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getTierColor(selectedReseller.tier)}`}>
{selectedReseller.tier?.charAt(0).toUpperCase() + selectedReseller.tier?.slice(1) || 'Unknown'}
</span>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(selectedReseller.status)}`}>
{selectedReseller.status?.charAt(0).toUpperCase() + selectedReseller.status?.slice(1) || 'Unknown'}
</span>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Commission Rate
</label>
<p className="text-gray-900 dark:text-white">{selectedReseller.commissionRate}%</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Created At
</label>
<p className="text-gray-900 dark:text-white">{new Date(selectedReseller.createdAt).toLocaleString()}</p>
</div>
{selectedReseller.users && selectedReseller.users.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Associated Users
</label>
<div className="space-y-2">
{selectedReseller.users.map((user) => (
<div key={user.id} className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded">
<span className="text-sm text-gray-900 dark:text-white">
{user.firstName} {user.lastName} ({user.email})
</span>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(user.status)}`}>
{user.status}
</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
<div className="flex justify-end p-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={() => setShowModal(false)}
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
Close
</button>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">Confirm Delete</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Are you sure you want to delete this reseller? This action cannot be undone.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => {
setIsDeleteModalOpen(false);
setDeletingResellerId(null);
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
Cancel
</button>
<button
onClick={confirmDelete}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Delete Reseller
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Resellers;

View File

@ -0,0 +1,617 @@
import React, { useState, useEffect } from 'react';
import {
Settings,
Save,
Shield,
Bell,
Database,
Globe,
Users,
Key,
Eye,
EyeOff,
CheckCircle,
AlertCircle
} from 'lucide-react';
interface SystemSettings {
general: {
siteName: string;
siteDescription: string;
maintenanceMode: boolean;
defaultLanguage: string;
timezone: string;
};
security: {
sessionTimeout: number;
maxLoginAttempts: number;
requireTwoFactor: boolean;
passwordMinLength: number;
enableAuditLog: boolean;
};
email: {
smtpHost: string;
smtpPort: number;
smtpUser: string;
smtpPassword: string;
fromEmail: string;
fromName: string;
};
notifications: {
emailNotifications: boolean;
pushNotifications: boolean;
vendorApprovalAlerts: boolean;
systemAlerts: boolean;
reportAlerts: boolean;
};
integrations: {
enableAnalytics: boolean;
enableLogging: boolean;
enableBackup: boolean;
backupFrequency: string;
};
}
const AdminSettings: React.FC = () => {
const [settings, setSettings] = useState<SystemSettings>({
general: {
siteName: 'Cloutopiaa Reseller Portal',
siteDescription: 'Cloud services reseller management platform',
maintenanceMode: false,
defaultLanguage: 'en',
timezone: 'UTC'
},
security: {
sessionTimeout: 30,
maxLoginAttempts: 5,
requireTwoFactor: false,
passwordMinLength: 8,
enableAuditLog: true
},
email: {
smtpHost: 'smtp.gmail.com',
smtpPort: 587,
smtpUser: '',
smtpPassword: '',
fromEmail: 'noreply@cloutopiaa.com',
fromName: 'Cloutopiaa Admin'
},
notifications: {
emailNotifications: true,
pushNotifications: true,
vendorApprovalAlerts: true,
systemAlerts: true,
reportAlerts: false
},
integrations: {
enableAnalytics: true,
enableLogging: true,
enableBackup: true,
backupFrequency: 'daily'
}
});
const [activeTab, setActiveTab] = useState('general');
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
useEffect(() => {
fetchSettings();
}, []);
const fetchSettings = async () => {
try {
setLoading(true);
// Mock API call - replace with actual implementation
// const response = await fetch('/api/admin/settings');
// const data = await response.json();
// setSettings(data);
} catch (error) {
console.error('Error fetching settings:', error);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
try {
setSaveStatus('saving');
// Mock API call - replace with actual implementation
// const response = await fetch('/api/admin/settings', {
// method: 'PUT',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(settings)
// });
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
setSaveStatus('success');
setTimeout(() => setSaveStatus('idle'), 3000);
} catch (error) {
console.error('Error saving settings:', error);
setSaveStatus('error');
setTimeout(() => setSaveStatus('idle'), 3000);
}
};
const updateSetting = (section: keyof SystemSettings, key: string, value: any) => {
setSettings(prev => ({
...prev,
[section]: {
...prev[section],
[key]: value
}
}));
};
const tabs = [
{ id: 'general', name: 'General', icon: Settings },
{ id: 'security', name: 'Security', icon: Shield },
{ id: 'email', name: 'Email', icon: Bell },
{ id: 'notifications', name: 'Notifications', icon: Bell },
{ id: 'integrations', name: 'Integrations', icon: Database }
];
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="p-6 space-y-6 max-w-full">
{/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center space-y-4 sm:space-y-0">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">System Settings</h1>
<p className="text-gray-600 dark:text-gray-400">Configure system-wide settings and preferences</p>
</div>
<button
onClick={handleSave}
disabled={saveStatus === 'saving'}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 flex items-center space-x-2"
>
{saveStatus === 'saving' ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
) : saveStatus === 'success' ? (
<CheckCircle className="h-4 w-4" />
) : saveStatus === 'error' ? (
<AlertCircle className="h-4 w-4" />
) : (
<Save className="h-4 w-4" />
)}
<span>
{saveStatus === 'saving' ? 'Saving...' :
saveStatus === 'success' ? 'Saved!' :
saveStatus === 'error' ? 'Error' : 'Save Changes'}
</span>
</button>
</div>
{/* Tabs */}
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex flex-wrap space-x-4 lg:space-x-8">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 whitespace-nowrap ${
activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Icon className="h-4 w-4" />
<span>{tab.name}</span>
</button>
);
})}
</nav>
</div>
{/* Tab Content */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
{/* General Settings */}
{activeTab === 'general' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold flex items-center">
<Settings className="h-5 w-5 mr-2" />
General Settings
</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Site Name
</label>
<input
type="text"
value={settings.general.siteName}
onChange={(e) => updateSetting('general', 'siteName', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Site Description
</label>
<input
type="text"
value={settings.general.siteDescription}
onChange={(e) => updateSetting('general', 'siteDescription', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Default Language
</label>
<select
value={settings.general.defaultLanguage}
onChange={(e) => updateSetting('general', 'defaultLanguage', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Timezone
</label>
<select
value={settings.general.timezone}
onChange={(e) => updateSetting('general', 'timezone', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="UTC">UTC</option>
<option value="EST">Eastern Time</option>
<option value="PST">Pacific Time</option>
<option value="GMT">GMT</option>
</select>
</div>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="maintenanceMode"
checked={settings.general.maintenanceMode}
onChange={(e) => updateSetting('general', 'maintenanceMode', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="maintenanceMode" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Enable Maintenance Mode
</label>
</div>
</div>
)}
{/* Security Settings */}
{activeTab === 'security' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold flex items-center">
<Shield className="h-5 w-5 mr-2" />
Security Settings
</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Session Timeout (minutes)
</label>
<input
type="number"
value={settings.security.sessionTimeout}
onChange={(e) => updateSetting('security', 'sessionTimeout', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Max Login Attempts
</label>
<input
type="number"
value={settings.security.maxLoginAttempts}
onChange={(e) => updateSetting('security', 'maxLoginAttempts', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Minimum Password Length
</label>
<input
type="number"
value={settings.security.passwordMinLength}
onChange={(e) => updateSetting('security', 'passwordMinLength', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<input
type="checkbox"
id="requireTwoFactor"
checked={settings.security.requireTwoFactor}
onChange={(e) => updateSetting('security', 'requireTwoFactor', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="requireTwoFactor" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Require Two-Factor Authentication
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="enableAuditLog"
checked={settings.security.enableAuditLog}
onChange={(e) => updateSetting('security', 'enableAuditLog', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="enableAuditLog" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Enable Audit Logging
</label>
</div>
</div>
</div>
)}
{/* Email Settings */}
{activeTab === 'email' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold flex items-center">
<Bell className="h-5 w-5 mr-2" />
Email Configuration
</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
SMTP Host
</label>
<input
type="text"
value={settings.email.smtpHost}
onChange={(e) => updateSetting('email', 'smtpHost', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
SMTP Port
</label>
<input
type="number"
value={settings.email.smtpPort}
onChange={(e) => updateSetting('email', 'smtpPort', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
SMTP Username
</label>
<input
type="text"
value={settings.email.smtpUser}
onChange={(e) => updateSetting('email', 'smtpUser', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
SMTP Password
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={settings.email.smtpPassword}
onChange={(e) => updateSetting('email', 'smtpPassword', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
From Email
</label>
<input
type="email"
value={settings.email.fromEmail}
onChange={(e) => updateSetting('email', 'fromEmail', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
From Name
</label>
<input
type="text"
value={settings.email.fromName}
onChange={(e) => updateSetting('email', 'fromName', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
)}
{/* Notification Settings */}
{activeTab === 'notifications' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold flex items-center">
<Bell className="h-5 w-5 mr-2" />
Notification Preferences
</h3>
<div className="space-y-4">
<div className="flex items-center">
<input
type="checkbox"
id="emailNotifications"
checked={settings.notifications.emailNotifications}
onChange={(e) => updateSetting('notifications', 'emailNotifications', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="emailNotifications" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Enable Email Notifications
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="pushNotifications"
checked={settings.notifications.pushNotifications}
onChange={(e) => updateSetting('notifications', 'pushNotifications', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="pushNotifications" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Enable Push Notifications
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="vendorApprovalAlerts"
checked={settings.notifications.vendorApprovalAlerts}
onChange={(e) => updateSetting('notifications', 'vendorApprovalAlerts', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="vendorApprovalAlerts" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Vendor Approval Alerts
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="systemAlerts"
checked={settings.notifications.systemAlerts}
onChange={(e) => updateSetting('notifications', 'systemAlerts', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="systemAlerts" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
System Alerts
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="reportAlerts"
checked={settings.notifications.reportAlerts}
onChange={(e) => updateSetting('notifications', 'reportAlerts', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="reportAlerts" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Report Generation Alerts
</label>
</div>
</div>
</div>
)}
{/* Integration Settings */}
{activeTab === 'integrations' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold flex items-center">
<Database className="h-5 w-5 mr-2" />
System Integrations
</h3>
<div className="space-y-4">
<div className="flex items-center">
<input
type="checkbox"
id="enableAnalytics"
checked={settings.integrations.enableAnalytics}
onChange={(e) => updateSetting('integrations', 'enableAnalytics', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="enableAnalytics" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Enable Analytics Tracking
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="enableLogging"
checked={settings.integrations.enableLogging}
onChange={(e) => updateSetting('integrations', 'enableLogging', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="enableLogging" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Enable System Logging
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="enableBackup"
checked={settings.integrations.enableBackup}
onChange={(e) => updateSetting('integrations', 'enableBackup', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="enableBackup" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Enable Automated Backups
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Backup Frequency
</label>
<select
value={settings.integrations.backupFrequency}
onChange={(e) => updateSetting('integrations', 'backupFrequency', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default AdminSettings;

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useAppSelector } from '../../store/hooks';
import {
Users,
Clock,
@ -11,94 +12,171 @@ import {
Building,
Mail,
Phone,
MapPin
MapPin,
AlertCircle
} 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;
}
import { VendorRequest } from '../../types/vendor';
import VendorDetailsModal from '../../components/VendorDetailsModal';
import VendorRejectionModal from '../../components/VendorRejectionModal';
const VendorRequests: React.FC = () => {
const [vendors, setVendors] = useState<VendorRequest[]>([]);
const [vendorRequests, setVendorRequests] = 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);
const [rejectionReason, setRejectionReason] = useState('');
const [showRejectionModal, setShowRejectionModal] = useState(false);
const [error, setError] = useState<string | null>(null);
// Get auth state from Redux
const { isAuthenticated, token, user } = useAppSelector((state) => state.auth);
// Debug: Log auth state
console.log('Auth state:', { isAuthenticated, hasToken: !!token, user });
console.log('Modal states:', { showModal, showRejectionModal, selectedVendor: selectedVendor?.id });
useEffect(() => {
fetchVendorRequests();
}, []);
// Check authentication on component mount
useEffect(() => {
if (!isAuthenticated || !token) {
console.error('User not authenticated');
setError('Please log in to access this page');
}
}, [isAuthenticated, token]);
const fetchVendorRequests = async () => {
try {
const response = await fetch('/api/admin/pending-vendors', {
setLoading(true);
// Debug: Check if token exists
const token = localStorage.getItem('accessToken');
console.log('Token exists:', !!token);
console.log('Token length:', token ? token.length : 0);
console.log('Redux token exists:', !!token);
if (!token) {
console.error('No token found in localStorage');
setError('Authentication required. Please log in again.');
return;
}
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/pending-vendors`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
// Debug: Log response details
console.log('Response status:', response.status);
console.log('Response headers:', Object.fromEntries(response.headers.entries()));
const data = await response.json();
console.log('Response data:', data);
if (!response.ok) {
throw new Error(data.message || 'Failed to fetch vendor requests');
}
if (data.success) {
setVendors(data.data);
setVendorRequests(data.data.pendingRequests || []);
} else {
setError(data.message || 'Failed to fetch vendor requests');
}
} catch (error) {
} catch (error: any) {
console.error('Error fetching vendor requests:', error);
setError(error.message || 'Failed to fetch vendor requests');
} finally {
setLoading(false);
}
};
const handleApprove = async (vendorId: string) => {
const approveVendorRequest = async (userId: string) => {
try {
const response = await fetch(`/api/admin/vendors/${vendorId}/approve`, {
const token = localStorage.getItem('accessToken');
if (!token) {
console.error('No token found');
return;
}
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/vendors/${userId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ reason: 'Approved by admin' })
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
fetchVendorRequests();
} else {
const errorData = await response.json();
console.error('Failed to approve vendor request:', errorData);
}
} catch (error) {
console.error('Error approving vendor:', error);
}
};
const handleReject = async (vendorId: string, reason: string) => {
const rejectVendorRequest = async (userId: string, reason: string) => {
try {
const response = await fetch(`/api/admin/vendors/${vendorId}/reject`, {
const token = localStorage.getItem('accessToken');
if (!token) {
console.error('No token found');
return;
}
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/vendors/${userId}/reject`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ reason })
});
if (response.ok) {
fetchVendorRequests();
setShowModal(false);
setShowRejectionModal(false);
} else {
const errorData = await response.json();
console.error('Failed to reject vendor request:', errorData);
}
} catch (error) {
console.error('Error rejecting vendor:', error);
}
};
const filteredVendors = vendors.filter(vendor => {
const getStatusColor = (status: string) => {
switch (status) {
case 'pending':
return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
case 'approved':
return 'text-green-600 bg-green-100 dark:bg-green-900';
case 'rejected':
return 'text-red-600 bg-red-100 dark:bg-red-900';
default:
return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
}
};
const formatRoleName = (role: string) => {
if (role.startsWith('channel_partner_')) {
return 'Vendor';
} else if (role.startsWith('reseller_')) {
return 'Reseller';
} else if (role.startsWith('system_')) {
return 'System Admin';
}
return role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
};
const filteredVendors = vendorRequests.filter(vendor => {
const matchesSearch = vendor.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
vendor.lastName.toLowerCase().includes(searchTerm.toLowerCase()) ||
vendor.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
@ -109,325 +187,192 @@ const VendorRequests: React.FC = () => {
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 className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></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 className="p-6 space-y-6 max-w-full">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Vendor Requests
</h1>
<p className="text-gray-600 dark:text-gray-400">
Review and manage vendor registration requests
</p>
</div>
{/* Error Display */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
<div className="flex items-center">
<AlertCircle className="w-5 h-5 text-red-400 mr-2" />
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
</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>
{/* Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex flex-col lg: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 text-gray-400 w-4 h-4" />
<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-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
/>
</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>
{/* Status Filter */}
<div className="flex items-center space-x-2">
<Filter className="w-4 h-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="all" className="bg-white dark:bg-gray-700 text-gray-900 dark:text-white">All Status</option>
<option value="pending" className="bg-white dark:bg-gray-700 text-gray-900 dark:text-white">Pending</option>
<option value="approved" className="bg-white dark:bg-gray-700 text-gray-900 dark:text-white">Approved</option>
<option value="rejected" className="bg-white dark:bg-gray-700 text-gray-900 dark:text-white">Rejected</option>
</select>
</div>
</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>
{/* Vendor List */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Vendor Requests ({filteredVendors.length})
</h2>
</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="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Vendor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Company
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Role
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredVendors.map((vendor) => (
<tr key={vendor.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<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 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-gray-900 dark:text-white">
{vendor.firstName} {vendor.lastName}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{vendor.email}
</div>
</div>
</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>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">{vendor.company}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">
{formatRoleName(vendor.role)}
</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' && (
</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-gray-500 dark:text-gray-400">
{new Date(vendor.createdAt).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<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"
onClick={() => {
console.log('View button clicked for vendor:', vendor);
setSelectedVendor(vendor);
setShowModal(true);
console.log('Modal state set to true');
}}
className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
>
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
<Eye className="w-4 h-4" />
</button>
{vendor.status === 'pending' && (
<>
<button
onClick={() => approveVendorRequest(vendor.id)}
className="text-green-600 hover:text-green-900 dark:hover:text-green-400"
>
<CheckCircle className="w-4 h-4" />
</button>
<button
onClick={() => {
setSelectedVendor(vendor);
setShowRejectionModal(true);
}}
className="text-red-600 hover:text-red-900 dark:hover:text-red-400"
>
<XCircle className="w-4 h-4" />
</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>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Shared Vendor Modals */}
<VendorDetailsModal
vendor={selectedVendor}
isOpen={showModal}
onClose={() => setShowModal(false)}
onApprove={approveVendorRequest}
onReject={rejectVendorRequest}
/>
<VendorRejectionModal
vendor={selectedVendor}
isOpen={showRejectionModal}
onClose={() => setShowRejectionModal(false)}
onReject={rejectVendorRequest}
/>
</div>
);
};

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import {
Cloud,
Package,
Plus,
Search,
Filter,
@ -13,160 +13,248 @@ import {
Clock,
CheckCircle,
XCircle,
AlertTriangle
AlertTriangle,
Eye,
ShoppingCart,
Star,
Tag
} from 'lucide-react';
import { apiService, Product } from '../../services/api';
interface Instance {
id: string;
name: string;
type: string;
status: 'running' | 'stopped' | 'starting' | 'stopping' | 'error';
region: string;
cpu: string;
memory: string;
storage: string;
ipAddress: string;
customer: string;
monthlyCost: number;
createdAt: string;
lastStarted: string;
interface AvailableProduct extends Product {
vendorName?: string;
vendorCompany?: string;
stockAvailable?: number;
commissionEarned?: number;
}
const mockInstances: Instance[] = [
{
id: '1',
name: 'web-server-01',
type: 't3.medium',
status: 'running',
region: 'us-east-1',
cpu: '2 vCPU',
memory: '4 GB',
storage: '20 GB SSD',
ipAddress: '192.168.1.100',
customer: 'TechCorp Solutions',
monthlyCost: 35.50,
createdAt: '2024-12-01T00:00:00Z',
lastStarted: '2025-01-15T08:00:00Z'
},
{
id: '2',
name: 'db-server-01',
type: 't3.large',
status: 'running',
region: 'us-east-1',
cpu: '2 vCPU',
memory: '8 GB',
storage: '100 GB SSD',
ipAddress: '192.168.1.101',
customer: 'DataFlow Inc',
monthlyCost: 70.25,
createdAt: '2024-11-15T00:00:00Z',
lastStarted: '2025-01-14T06:30:00Z'
},
{
id: '3',
name: 'app-server-01',
type: 't3.small',
status: 'stopped',
region: 'us-west-2',
cpu: '2 vCPU',
memory: '2 GB',
storage: '20 GB SSD',
ipAddress: '192.168.1.102',
customer: 'CloudTech Ltd',
monthlyCost: 17.75,
createdAt: '2024-10-20T00:00:00Z',
lastStarted: '2025-01-10T14:20:00Z'
},
{
id: '4',
name: 'cache-server-01',
type: 't3.micro',
status: 'running',
region: 'us-east-1',
cpu: '2 vCPU',
memory: '1 GB',
storage: '8 GB SSD',
ipAddress: '192.168.1.103',
customer: 'InnovateSoft',
monthlyCost: 8.90,
createdAt: '2024-12-10T00:00:00Z',
lastStarted: '2025-01-15T09:15:00Z'
},
{
id: '5',
name: 'backup-server-01',
type: 't3.medium',
status: 'error',
region: 'us-west-2',
cpu: '2 vCPU',
memory: '4 GB',
storage: '500 GB SSD',
ipAddress: '192.168.1.104',
customer: 'NetSolutions',
monthlyCost: 45.00,
createdAt: '2024-09-05T00:00:00Z',
lastStarted: '2025-01-12T22:45:00Z'
}
];
const Instances: React.FC = () => {
const [instances, setInstances] = useState<Instance[]>(mockInstances);
const AvailableProducts: React.FC = () => {
const [products, setProducts] = useState<AvailableProduct[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [categoryFilter, setCategoryFilter] = useState<string>('all');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [regionFilter, setRegionFilter] = useState<string>('all');
const [priceFilter, setPriceFilter] = useState<string>('all');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(12);
const [totalItems, setTotalItems] = useState(0);
const filteredInstances = instances.filter(instance => {
const matchesSearch = instance.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
instance.customer.toLowerCase().includes(searchTerm.toLowerCase()) ||
instance.ipAddress.includes(searchTerm);
const matchesStatus = statusFilter === 'all' || instance.status === statusFilter;
const matchesRegion = regionFilter === 'all' || instance.region === regionFilter;
return matchesSearch && matchesStatus && matchesRegion;
// Mock data for demonstration - in real app this would come from API
const mockProducts: AvailableProduct[] = [
{
id: 1,
name: 'AWS EC2 t3.medium Instance',
description: 'High-performance cloud computing instance with 2 vCPUs and 4GB RAM',
category: 'cloud_computing',
price: 35.50,
currency: 'USD',
commissionRate: 15,
status: 'active',
availability: 'available',
stockQuantity: 100,
sku: 'AWS-EC2-T3M',
vendorName: 'John Smith',
vendorCompany: 'CloudTech Solutions',
stockAvailable: 100,
commissionEarned: 5.33,
createdBy: 1,
createdAt: '2024-12-01T00:00:00Z',
updatedAt: '2024-12-01T00:00:00Z'
},
{
id: 2,
name: 'Google Cloud Storage 100GB',
description: 'Scalable cloud storage solution with high availability and redundancy',
category: 'cloud_storage',
price: 20.00,
currency: 'USD',
commissionRate: 12,
status: 'active',
availability: 'available',
stockQuantity: 500,
sku: 'GCS-100GB',
vendorName: 'Sarah Johnson',
vendorCompany: 'DataFlow Inc',
stockAvailable: 500,
commissionEarned: 2.40,
createdBy: 1,
createdAt: '2024-11-15T00:00:00Z',
updatedAt: '2024-11-15T00:00:00Z'
},
{
id: 3,
name: 'Microsoft Azure Security Center',
description: 'Advanced threat protection and security management for cloud workloads',
category: 'cybersecurity',
price: 150.00,
currency: 'USD',
commissionRate: 18,
status: 'active',
availability: 'available',
stockQuantity: 50,
sku: 'AZURE-SEC',
vendorName: 'Mike Chen',
vendorCompany: 'SecureNet Pro',
stockAvailable: 50,
commissionEarned: 27.00,
createdBy: 1,
createdAt: '2024-10-20T00:00:00Z',
updatedAt: '2024-10-20T00:00:00Z'
},
{
id: 4,
name: 'IBM Watson AI Platform',
description: 'Enterprise AI and machine learning platform with cognitive services',
category: 'ai_ml',
price: 500.00,
currency: 'USD',
commissionRate: 20,
status: 'active',
availability: 'available',
stockQuantity: 25,
sku: 'IBM-WATSON',
vendorName: 'Lisa Wang',
vendorCompany: 'AI Innovations',
stockAvailable: 25,
commissionEarned: 100.00,
createdBy: 1,
createdAt: '2024-12-10T00:00:00Z',
updatedAt: '2024-12-10T00:00:00Z'
},
{
id: 5,
name: 'Oracle Database Cloud Service',
description: 'Fully managed Oracle database service with automated backups',
category: 'data_analytics',
price: 300.00,
currency: 'USD',
commissionRate: 16,
status: 'active',
availability: 'available',
stockQuantity: 30,
sku: 'ORACLE-DB',
vendorName: 'David Brown',
vendorCompany: 'Database Experts',
stockAvailable: 30,
commissionEarned: 48.00,
createdBy: 1,
createdAt: '2024-09-05T00:00:00Z',
updatedAt: '2024-09-05T00:00:00Z'
},
{
id: 6,
name: 'Cisco IoT Gateway',
description: 'Industrial IoT gateway for edge computing and device management',
category: 'iot',
price: 250.00,
currency: 'USD',
commissionRate: 14,
status: 'active',
availability: 'available',
stockQuantity: 40,
sku: 'CISCO-IOT',
vendorName: 'Alex Rodriguez',
vendorCompany: 'IoT Solutions',
stockAvailable: 40,
commissionEarned: 35.00,
createdBy: 1,
createdAt: '2024-11-20T00:00:00Z',
updatedAt: '2024-11-20T00:00:00Z'
}
];
useEffect(() => {
// In real app, fetch products from API
// fetchProducts();
setProducts(mockProducts);
setTotalItems(mockProducts.length);
setLoading(false);
}, []);
const categories = [
{ value: 'cloud_storage', label: 'Cloud Storage' },
{ value: 'cloud_computing', label: 'Cloud Computing' },
{ value: 'cybersecurity', label: 'Cybersecurity' },
{ value: 'data_analytics', label: 'Data Analytics' },
{ value: 'ai_ml', label: 'AI & Machine Learning' },
{ value: 'iot', label: 'Internet of Things' },
{ value: 'blockchain', label: 'Blockchain' },
{ value: 'other', label: 'Other' }
];
const priceRanges = [
{ value: 'all', label: 'All Prices' },
{ value: '0-50', label: '$0 - $50' },
{ value: '51-100', label: '$51 - $100' },
{ value: '101-200', label: '$101 - $200' },
{ value: '201-500', label: '$201 - $500' },
{ value: '500+', label: '$500+' }
];
const filteredProducts = products.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.sku.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = categoryFilter === 'all' || product.category === categoryFilter;
const matchesStatus = statusFilter === 'all' || product.status === statusFilter;
let matchesPrice = true;
if (priceFilter !== 'all') {
const [min, max] = priceFilter.split('-').map(Number);
if (max) {
matchesPrice = product.price >= min && product.price <= max;
} else {
matchesPrice = product.price >= min;
}
}
return matchesSearch && matchesCategory && matchesStatus && matchesPrice;
});
const getStatusColor = (status: string) => {
switch (status) {
case 'running':
return 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-300';
case 'stopped':
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
case 'starting':
case 'stopping':
return 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-300';
case 'error':
return 'bg-danger-100 text-danger-800 dark:bg-danger-900 dark:text-danger-300';
const getCategoryColor = (category: string) => {
switch (category) {
case 'cloud_storage':
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
case 'cloud_computing':
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
case 'cybersecurity':
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
case 'data_analytics':
return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300';
case 'ai_ml':
return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300';
case 'iot':
return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300';
case 'blockchain':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'running':
return <CheckCircle className="w-4 h-4" />;
case 'stopped':
return <XCircle className="w-4 h-4" />;
case 'starting':
case 'stopping':
return <Clock className="w-4 h-4" />;
case 'error':
return <AlertTriangle className="w-4 h-4" />;
const getCategoryIcon = (category: string) => {
switch (category) {
case 'cloud_storage':
return <Database className="w-4 h-4" />;
case 'cloud_computing':
return <Server className="w-4 h-4" />;
case 'cybersecurity':
return <Shield className="w-4 h-4" />;
case 'data_analytics':
return <Globe className="w-4 h-4" />;
case 'ai_ml':
return <Zap className="w-4 h-4" />;
case 'iot':
return <Package className="w-4 h-4" />;
case 'blockchain':
return <Tag className="w-4 h-4" />;
default:
return <Clock className="w-4 h-4" />;
return <Package className="w-4 h-4" />;
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
@ -176,7 +264,51 @@ const Instances: React.FC = () => {
}).format(amount);
};
const totalMonthlyCost = instances.reduce((sum, instance) => sum + instance.monthlyCost, 0);
const totalProducts = products.length;
const totalValue = products.reduce((sum, product) => sum + product.price, 0);
const totalCommission = products.reduce((sum, product) => sum + (product.price * product.commissionRate / 100), 0);
const availableCategories = new Set(products.map(p => p.category)).size;
const handleViewProduct = (product: AvailableProduct) => {
// Handle viewing product details
console.log('Viewing product:', product);
};
const handleAddToCart = (product: AvailableProduct) => {
// Handle adding product to cart
console.log('Adding to cart:', product);
};
const handleContactVendor = (product: AvailableProduct) => {
// Handle contacting vendor
console.log('Contacting vendor for:', product);
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
);
}
if (error) {
return (
<div className="text-center py-12">
<AlertTriangle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Error loading products
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">{error}</p>
<button
onClick={() => window.location.reload()}
className="btn btn-primary"
>
Try Again
</button>
</div>
);
}
return (
<div className="space-y-6">
@ -184,16 +316,34 @@ const Instances: React.FC = () => {
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-secondary-900 dark:text-white">
Cloud Instances
Available Products
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Manage your cloud infrastructure and instances
Browse and manage products from your assigned vendor
</p>
</div>
<button className="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-secondary-800">
<Plus className="w-4 h-4 mr-2" />
Create Instance
</button>
<div className="flex items-center space-x-3">
<button
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
className="btn btn-outline btn-sm"
>
{viewMode === 'grid' ? (
<>
<Package className="w-4 h-4 mr-2" />
Grid View
</>
) : (
<>
<Server className="w-4 h-4 mr-2" />
List View
</>
)}
</button>
<button className="btn btn-primary btn-sm">
<Plus className="w-4 h-4 mr-2" />
Request Product
</button>
</div>
</div>
{/* Stats Cards */}
@ -202,14 +352,14 @@ const Instances: React.FC = () => {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Total Instances
Total Products
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{instances.length}
{totalProducts}
</p>
</div>
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
<Server className="w-6 h-6 text-primary-600 dark:text-primary-400" />
<Package className="w-6 h-6 text-primary-600 dark:text-primary-400" />
</div>
</div>
</div>
@ -218,14 +368,14 @@ const Instances: React.FC = () => {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Running Instances
Total Value
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{instances.filter(i => i.status === 'running').length}
{formatCurrency(totalValue)}
</p>
</div>
<div className="w-12 h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-success-600 dark:text-success-400" />
<Zap className="w-6 h-6 text-success-600 dark:text-success-400" />
</div>
</div>
</div>
@ -234,14 +384,14 @@ const Instances: React.FC = () => {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Monthly Cost
Commission Potential
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{formatCurrency(totalMonthlyCost)}
{formatCurrency(totalCommission)}
</p>
</div>
<div className="w-12 h-12 bg-warning-100 dark:bg-warning-900 rounded-full flex items-center justify-center">
<Zap className="w-6 h-6 text-warning-600 dark:text-warning-400" />
<Star className="w-6 h-6 text-warning-600 dark:text-warning-400" />
</div>
</div>
</div>
@ -250,14 +400,14 @@ const Instances: React.FC = () => {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Active Customers
Categories
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{new Set(instances.map(i => i.customer)).size}
{availableCategories}
</p>
</div>
<div className="w-12 h-12 bg-secondary-100 dark:bg-secondary-900 rounded-full flex items-center justify-center">
<Globe className="w-6 h-6 text-secondary-600 dark:text-secondary-400" />
<Tag className="w-6 h-6 text-secondary-600 dark:text-secondary-400" />
</div>
</div>
</div>
@ -271,7 +421,7 @@ const Instances: React.FC = () => {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-secondary-400 w-4 h-4" />
<input
type="text"
placeholder="Search instances..."
placeholder="Search products by name, description, or SKU..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
@ -280,132 +430,246 @@ const Instances: React.FC = () => {
</div>
<div className="flex gap-2">
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="all">All Categories</option>
{categories.map(category => (
<option key={category.value} value={category.value}>
{category.label}
</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="all">All Status</option>
<option value="running">Running</option>
<option value="stopped">Stopped</option>
<option value="starting">Starting</option>
<option value="stopping">Stopping</option>
<option value="error">Error</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="draft">Draft</option>
</select>
<select
value={regionFilter}
onChange={(e) => setRegionFilter(e.target.value)}
value={priceFilter}
onChange={(e) => setPriceFilter(e.target.value)}
className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="all">All Regions</option>
<option value="us-east-1">US East (N. Virginia)</option>
<option value="us-west-2">US West (Oregon)</option>
<option value="eu-west-1">Europe (Ireland)</option>
<option value="ap-southeast-1">Asia Pacific (Singapore)</option>
{priceRanges.map(range => (
<option key={range.value} value={range.value}>
{range.label}
</option>
))}
</select>
</div>
</div>
</div>
{/* Instances Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 sm:gap-6">
{filteredInstances.map((instance) => (
<div key={instance.id} className="card p-4 sm:p-6 hover:shadow-lg transition-shadow duration-200">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-1">
{instance.name}
</h3>
<p className="text-sm text-secondary-600 dark:text-secondary-400">
{instance.customer}
</p>
</div>
<button className="text-secondary-400 hover:text-secondary-600 dark:hover:text-secondary-300">
<MoreVertical className="w-4 h-4" />
</button>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Status</span>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(instance.status)}`}>
{getStatusIcon(instance.status)}
<span className="ml-1 capitalize">{instance.status}</span>
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Type</span>
<span className="text-sm font-medium text-secondary-900 dark:text-white">{instance.type}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Region</span>
<span className="text-sm font-medium text-secondary-900 dark:text-white">{instance.region}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">IP Address</span>
<span className="text-sm font-mono text-secondary-900 dark:text-white">{instance.ipAddress}</span>
</div>
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="text-center p-2 bg-secondary-50 dark:bg-secondary-800 rounded">
<div className="font-medium text-secondary-900 dark:text-white">{instance.cpu}</div>
<div className="text-secondary-500 dark:text-secondary-400">CPU</div>
{/* Products Display */}
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 sm:gap-6">
{filteredProducts.map((product) => (
<div key={product.id} className="card p-4 sm:p-6 hover:shadow-lg transition-shadow duration-200">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-2">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getCategoryColor(product.category)}`}>
{getCategoryIcon(product.category)}
<span className="ml-1 capitalize">
{categories.find(c => c.value === product.category)?.label}
</span>
</span>
</div>
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-1">
{product.name}
</h3>
<p className="text-sm text-secondary-600 dark:text-secondary-400 mb-2">
{product.description}
</p>
<div className="flex items-center space-x-4 text-xs text-secondary-500 dark:text-secondary-400">
<span>SKU: {product.sku}</span>
<span>Stock: {product.stockAvailable}</span>
</div>
</div>
<div className="text-center p-2 bg-secondary-50 dark:bg-secondary-800 rounded">
<div className="font-medium text-secondary-900 dark:text-white">{instance.memory}</div>
<div className="text-secondary-500 dark:text-secondary-400">Memory</div>
<button className="text-secondary-400 hover:text-secondary-600 dark:hover:text-secondary-300">
<MoreVertical className="w-4 h-4" />
</button>
</div>
<div className="space-y-3 mb-4">
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Vendor</span>
<span className="text-sm font-medium text-secondary-900 dark:text-white">
{product.vendorCompany}
</span>
</div>
<div className="text-center p-2 bg-secondary-50 dark:bg-secondary-800 rounded">
<div className="font-medium text-secondary-900 dark:text-white">{instance.storage}</div>
<div className="text-secondary-500 dark:text-secondary-400">Storage</div>
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Commission Rate</span>
<span className="text-sm font-medium text-success-600 dark:text-success-400">
{product.commissionRate}%
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Monthly Cost</span>
<span className="text-lg font-bold text-secondary-900 dark:text-white">
{formatCurrency(product.price)}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Commission Earned</span>
<span className="text-sm font-bold text-success-600 dark:text-success-400">
{formatCurrency(product.commissionEarned || 0)}
</span>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-secondary-200 dark:border-secondary-700">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Monthly Cost</span>
<span className="text-lg font-bold text-secondary-900 dark:text-white">
{formatCurrency(instance.monthlyCost)}
</span>
</div>
<div className="flex items-center justify-between text-xs text-secondary-500 dark:text-secondary-400">
<span>Created: {formatDate(instance.createdAt)}</span>
<span>Last: {formatDate(instance.lastStarted)}</span>
<div className="flex gap-2">
<button
onClick={() => handleViewProduct(product)}
className="flex-1 px-3 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors duration-200"
>
<Eye className="w-4 h-4 mr-2" />
View Details
</button>
<button
onClick={() => handleAddToCart(product)}
className="px-3 py-2 text-sm font-medium text-secondary-700 dark:text-secondary-300 border border-secondary-300 dark:border-secondary-600 hover:bg-secondary-50 dark:hover:bg-secondary-800 rounded-lg transition-colors duration-200"
>
<ShoppingCart className="w-4 h-4" />
</button>
</div>
</div>
<div className="flex gap-2 mt-4">
<button className="flex-1 px-3 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors duration-200">
Manage
</button>
<button className="px-3 py-2 text-sm font-medium text-secondary-700 dark:text-secondary-300 border border-secondary-300 dark:border-secondary-600 hover:bg-secondary-50 dark:hover:bg-secondary-800 rounded-lg transition-colors duration-200">
<Shield className="w-4 h-4" />
</button>
</div>
))}
</div>
) : (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-700">
<thead className="bg-secondary-50 dark:bg-secondary-800">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Product
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Category
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Vendor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Price
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Commission
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Stock
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-secondary-900 divide-y divide-secondary-200 dark:divide-secondary-700">
{filteredProducts.map((product) => (
<tr key={product.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-800">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{product.name}
</div>
<div className="text-sm text-secondary-500 dark:text-secondary-400">
{product.sku}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getCategoryColor(product.category)}`}>
{getCategoryIcon(product.category)}
<span className="ml-1 capitalize">
{categories.find(c => c.value === product.category)?.label}
</span>
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-secondary-900 dark:text-white">
{product.vendorCompany}
</div>
<div className="text-sm text-secondary-500 dark:text-secondary-400">
{product.vendorName}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{formatCurrency(product.price)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-success-600 dark:text-success-400">
{product.commissionRate}%
</div>
<div className="text-xs text-secondary-500 dark:text-secondary-400">
{formatCurrency(product.commissionEarned || 0)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-secondary-900 dark:text-white">
{product.stockAvailable}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => handleViewProduct(product)}
className="text-primary-600 hover:text-primary-900 dark:hover:text-primary-400"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => handleAddToCart(product)}
className="text-secondary-600 hover:text-secondary-900 dark:hover:text-secondary-400"
>
<ShoppingCart className="w-4 h-4" />
</button>
<button
onClick={() => handleContactVendor(product)}
className="text-warning-600 hover:text-warning-900 dark:hover:text-warning-400"
>
<MoreVertical className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
</div>
)}
{/* Empty State */}
{filteredInstances.length === 0 && (
{filteredProducts.length === 0 && (
<div className="card p-12 text-center">
<Cloud className="w-16 h-16 text-secondary-400 mx-auto mb-4" />
<Package className="w-16 h-16 text-secondary-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-2">
No instances found
No products found
</h3>
<p className="text-secondary-600 dark:text-secondary-400 mb-4">
{searchTerm || statusFilter !== 'all' || regionFilter !== 'all'
{searchTerm || categoryFilter !== 'all' || statusFilter !== 'all' || priceFilter !== 'all'
? 'Try adjusting your filters or search terms.'
: 'Get started by creating your first cloud instance.'
: 'No products are currently available from your assigned vendor.'
}
</p>
<button className="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg transition-colors duration-200">
<button className="btn btn-primary">
<Plus className="w-4 h-4 mr-2" />
Create Instance
Request Product
</button>
</div>
)}
@ -413,4 +677,4 @@ const Instances: React.FC = () => {
);
};
export default Instances;
export default AvailableProducts;

View File

@ -18,6 +18,7 @@ import { useAppSelector } from '../../store/hooks';
import { RootState } from '../../store';
import { toggleTheme } from '../../store/slices/themeSlice';
import { cn } from '../../utils/cn';
import BlockedAccountModal from '../../components/BlockedAccountModal';
const ResellerLogin: React.FC = () => {
const [email, setEmail] = useState('');
@ -26,6 +27,9 @@ const ResellerLogin: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [rememberMe, setRememberMe] = useState(false);
const [showInactiveModal, setShowInactiveModal] = useState(false);
const [inactiveUserEmail, setInactiveUserEmail] = useState('');
const [inactiveUserStatus, setInactiveUserStatus] = useState('');
const navigate = useNavigate();
const location = useLocation();
@ -75,8 +79,25 @@ const ResellerLogin: React.FC = () => {
navigate(redirectPath, { replace: true });
} catch (err: any) {
const errorMessage = err.message || 'Invalid email or password. Please try again.';
setError(errorMessage);
toast.error(errorMessage);
// Check if the error is related to account status issues
if (errorMessage.includes('Account access blocked') || errorMessage.includes('Status: inactive') || errorMessage.includes('Status: suspended') || errorMessage.includes('Status: pending')) {
// Extract status from error message
let status = 'inactive';
if (errorMessage.includes('Status: suspended')) {
status = 'suspended';
} else if (errorMessage.includes('Status: pending')) {
status = 'pending';
}
setInactiveUserEmail(email);
setInactiveUserStatus(status);
setShowInactiveModal(true);
setError('');
} else {
setError(errorMessage);
toast.error(errorMessage);
}
} finally {
setIsLoading(false);
}
@ -223,30 +244,7 @@ const ResellerLogin: React.FC = () => {
</button>
</form>
{/* Divider */}
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-300 dark:border-slate-600"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-slate-800 text-slate-500 dark:text-slate-400">
Or continue with
</span>
</div>
</div>
{/* Social Login Buttons */}
<div className="space-y-3">
<button className="w-full flex items-center justify-center px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-700 hover:bg-slate-50 dark:hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 transition-all duration-200">
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Continue with Google
</button>
</div>
{/* Sign Up Link */}
<div className="mt-6 text-center">
@ -282,6 +280,14 @@ const ResellerLogin: React.FC = () => {
</p>
</div>
</div>
{/* Blocked Account Modal */}
<BlockedAccountModal
isOpen={showInactiveModal}
onClose={() => setShowInactiveModal(false)}
userEmail={inactiveUserEmail}
userStatus={inactiveUserStatus}
/>
</div>
);
};

View File

@ -0,0 +1,827 @@
import React, { useState, useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import {
FileText,
Upload,
Plus,
Search,
Filter,
Eye,
Download,
Trash2,
Calendar,
DollarSign,
User,
CheckCircle,
XCircle,
Clock,
AlertCircle,
MoreVertical,
RefreshCw
} from 'lucide-react';
import { Receipt, ReceiptUploadData, ReceiptFilters } from '../../types/receipt';
import {
fetchReceipts,
uploadReceipt,
deleteReceipt,
downloadReceipt,
setFilters,
clearFilters,
setSelectedReceipt,
clearError,
selectReceipts,
selectReceiptPagination,
selectReceiptFilters,
selectReceiptsLoading,
selectReceiptsUploading,
selectReceiptsError,
selectReceiptStats
} from '../../store/slices/receiptSlice';
import toast from 'react-hot-toast';
import { cn } from '../../utils/cn';
const Receipts: React.FC = () => {
const dispatch = useAppDispatch();
const { user } = useAppSelector((state) => state.auth);
// Redux selectors
const receipts = useAppSelector(selectReceipts);
const pagination = useAppSelector(selectReceiptPagination);
const filters = useAppSelector(selectReceiptFilters);
const isLoading = useAppSelector(selectReceiptsLoading);
const isUploading = useAppSelector(selectReceiptsUploading);
const error = useAppSelector(selectReceiptsError);
const stats = useAppSelector(selectReceiptStats);
const [showUploadModal, setShowUploadModal] = useState(false);
const [selectedReceipt, setSelectedReceipt] = useState<Receipt | null>(null);
const [uploadForm, setUploadForm] = useState<ReceiptUploadData>({
orderId: 0,
clientName: '',
clientEmail: '',
clientPhone: '',
clientAddress: '',
saleAmount: 0,
currency: 'USD',
saleDate: new Date().toISOString().split('T')[0],
paymentMethod: '',
description: '',
receiptFile: null
});
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deletingReceiptId, setDeletingReceiptId] = useState<number | null>(null);
useEffect(() => {
dispatch(fetchReceipts({}));
}, [dispatch]);
// Clear error when component unmounts or error changes
useEffect(() => {
if (error) {
toast.error(error);
dispatch(clearError());
}
}, [error, dispatch]);
const handleUpload = async (e: React.FormEvent) => {
e.preventDefault();
if (!uploadForm.receiptFile) {
toast.error('Please select a receipt file');
return;
}
try {
const formData = new FormData();
// Append all form fields
Object.entries(uploadForm).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
formData.append(key, value);
}
});
const result = await dispatch(uploadReceipt(formData)).unwrap();
if (result.success) {
toast.success('Receipt uploaded successfully');
setShowUploadModal(false);
resetUploadForm();
dispatch(fetchReceipts({}));
}
} catch (error) {
console.error('Error uploading receipt:', error);
toast.error('Failed to upload receipt');
}
};
const resetUploadForm = () => {
setUploadForm({
orderId: 0,
clientName: '',
clientEmail: '',
clientPhone: '',
clientAddress: '',
saleAmount: 0,
currency: 'USD',
saleDate: new Date().toISOString().split('T')[0],
paymentMethod: '',
description: '',
receiptFile: null
});
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setUploadForm(prev => ({ ...prev, receiptFile: file }));
}
};
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value;
dispatch(setFilters({ search: searchTerm, page: 1 }));
};
const handleDeleteReceipt = async (receiptId: number) => {
setDeletingReceiptId(receiptId);
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
if (!deletingReceiptId) return;
try {
const result = await dispatch(deleteReceipt(deletingReceiptId)).unwrap();
if (result.success) {
toast.success('Receipt deleted successfully');
dispatch(fetchReceipts({}));
setIsDeleteModalOpen(false);
setDeletingReceiptId(null);
}
} catch (error) {
console.error('Error deleting receipt:', error);
toast.error('Failed to delete receipt');
}
};
const handleDownloadReceipt = async (receiptId: number) => {
try {
const result = await dispatch(downloadReceipt(receiptId)).unwrap();
const url = window.URL.createObjectURL(result.blob);
const a = document.createElement('a');
a.href = url;
a.download = `receipt-${receiptId}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error downloading receipt:', error);
toast.error('Failed to download receipt');
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'approved':
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
case 'rejected':
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
case 'pending':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
case 'disputed':
return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200';
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'approved':
return <CheckCircle className="w-4 h-4" />;
case 'rejected':
return <XCircle className="w-4 h-4" />;
case 'pending':
return <Clock className="w-4 h-4" />;
case 'disputed':
return <AlertCircle className="w-4 h-4" />;
default:
return <Clock className="w-4 h-4" />;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 via-teal-50 to-cyan-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Receipt Management
</h1>
<p className="text-gray-600 dark:text-gray-400">
Upload and manage your sales receipts for commission tracking
</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Receipts</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">{stats.totalReceipts}</p>
</div>
<div className="p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
<FileText className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Pending Approval</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{stats.pendingApproval}
</p>
</div>
<div className="p-3 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
<Clock className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Approved</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{stats.approved}
</p>
</div>
<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>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Sales</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
${stats.totalSales.toLocaleString()}
</p>
</div>
<div className="p-3 bg-emerald-100 dark:bg-emerald-900 rounded-lg">
<DollarSign className="w-6 h-6 text-emerald-600 dark:text-emerald-400" />
</div>
</div>
</div>
</div>
{/* Actions Bar */}
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700 mb-6">
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between">
<div className="flex flex-col sm:flex-row gap-4 flex-1">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Search receipts..."
value={filters.search || ''}
onChange={handleSearch}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
/>
</div>
<select
value={filters.status || ''}
onChange={(e) => dispatch(setFilters({ status: e.target.value, page: 1 }))}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
>
<option value="">All Status</option>
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
<option value="disputed">Disputed</option>
</select>
</div>
<button
onClick={() => setShowUploadModal(true)}
disabled={isUploading}
className="inline-flex items-center gap-2 px-6 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isUploading ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
Uploading...
</>
) : (
<>
<Plus className="w-4 h-4" />
Upload Receipt
</>
)}
</button>
</div>
</div>
{/* Receipts Table */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
{isLoading ? (
<div className="p-8 text-center">
<RefreshCw className="w-8 h-8 text-gray-400 animate-spin mx-auto mb-4" />
<p className="text-gray-500 dark:text-gray-400">Loading receipts...</p>
</div>
) : receipts.length === 0 ? (
<div className="p-8 text-center">
<FileText className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No receipts found</h3>
<p className="text-gray-500 dark:text-gray-400 mb-4">
Get started by uploading your first receipt
</p>
<button
onClick={() => setShowUploadModal(true)}
className="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors"
>
<Upload className="w-4 h-4" />
Upload Receipt
</button>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Receipt
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Client
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Amount
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{receipts.map((receipt) => (
<tr key={receipt.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-lg bg-emerald-100 dark:bg-emerald-900 flex items-center justify-center">
<FileText className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{receipt.receiptNumber}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Order #{receipt.orderId}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{receipt.clientName}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{receipt.clientEmail}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900 dark:text-white">
${receipt.saleAmount.toLocaleString()}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{receipt.currency}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">
{new Date(receipt.saleDate).toLocaleDateString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={cn(
"inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-full",
getStatusColor(receipt.status)
)}>
{getStatusIcon(receipt.status)}
{receipt.status.charAt(0).toUpperCase() + receipt.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center gap-2">
<button
onClick={() => setSelectedReceipt(receipt)}
className="text-emerald-600 hover:text-emerald-900 dark:text-emerald-400 dark:hover:text-emerald-300"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => handleDownloadReceipt(receipt.id)}
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
>
<Download className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteReceipt(receipt.id)}
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Pagination */}
{pagination && pagination.totalPages > 1 && (
<div className="mt-6 flex items-center justify-between">
<div className="text-sm text-gray-700 dark:text-gray-300">
Showing {((pagination.currentPage - 1) * pagination.itemsPerPage) + 1} to{' '}
{Math.min(pagination.currentPage * pagination.itemsPerPage, pagination.totalItems)} of{' '}
{pagination.totalItems} results
</div>
<div className="flex gap-2">
<button
onClick={() => dispatch(setFilters({ page: pagination.currentPage - 1 }))}
disabled={pagination.currentPage === 1}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() => dispatch(setFilters({ page: pagination.currentPage + 1 }))}
disabled={pagination.currentPage === pagination.totalPages}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)}
</div>
{/* Upload Modal */}
{showUploadModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Upload New Receipt
</h2>
<button
onClick={() => setShowUploadModal(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XCircle className="w-6 h-6" />
</button>
</div>
<form onSubmit={handleUpload} 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-gray-700 dark:text-gray-300 mb-2">
Order ID
</label>
<input
type="number"
value={uploadForm.orderId || ''}
onChange={(e) => setUploadForm(prev => ({ ...prev, orderId: parseInt(e.target.value) || 0 }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Client Name
</label>
<input
type="text"
value={uploadForm.clientName}
onChange={(e) => setUploadForm(prev => ({ ...prev, clientName: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Client Email
</label>
<input
type="email"
value={uploadForm.clientEmail}
onChange={(e) => setUploadForm(prev => ({ ...prev, clientEmail: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Client Phone
</label>
<input
type="tel"
value={uploadForm.clientPhone}
onChange={(e) => setUploadForm(prev => ({ ...prev, clientPhone: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Sale Amount
</label>
<input
type="number"
step="0.01"
value={uploadForm.saleAmount || ''}
onChange={(e) => setUploadForm(prev => ({ ...prev, saleAmount: parseFloat(e.target.value) || 0 }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Currency
</label>
<select
value={uploadForm.currency}
onChange={(e) => setUploadForm(prev => ({ ...prev, currency: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="GBP">GBP</option>
<option value="INR">INR</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Sale Date
</label>
<input
type="date"
value={uploadForm.saleDate}
onChange={(e) => setUploadForm(prev => ({ ...prev, saleDate: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Payment Method
</label>
<input
type="text"
value={uploadForm.paymentMethod}
onChange={(e) => setUploadForm(prev => ({ ...prev, paymentMethod: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
placeholder="e.g., Credit Card, Bank Transfer"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Client Address
</label>
<textarea
value={uploadForm.clientAddress}
onChange={(e) => setUploadForm(prev => ({ ...prev, clientAddress: e.target.value }))}
rows={3}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
<textarea
value={uploadForm.description}
onChange={(e) => setUploadForm(prev => ({ ...prev, description: e.target.value }))}
rows={3}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
placeholder="Additional details about the sale..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Receipt File
</label>
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-6 text-center">
<input
type="file"
onChange={handleFileChange}
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx"
className="hidden"
id="receiptFile"
required
/>
<label htmlFor="receiptFile" className="cursor-pointer">
<Upload className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-500 dark:text-gray-400">
{uploadForm.receiptFile ? uploadForm.receiptFile.name : 'Click to upload or drag and drop'}
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
PDF, JPG, PNG, DOC, DOCX (Max 10MB)
</p>
</label>
</div>
</div>
<div className="flex justify-end gap-3 pt-4">
<button
type="button"
onClick={() => setShowUploadModal(false)}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={isUploading}
className="px-6 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isUploading ? 'Uploading...' : 'Upload Receipt'}
</button>
</div>
</form>
</div>
</div>
)}
{/* Receipt Detail Modal */}
{selectedReceipt && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Receipt Details
</h2>
<button
onClick={() => setSelectedReceipt(null)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XCircle className="w-6 h-6" />
</button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Receipt Number</label>
<p className="text-sm text-gray-900 dark:text-white">{selectedReceipt.receiptNumber}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Order ID</label>
<p className="text-sm text-gray-900 dark:text-white">{selectedReceipt.orderId}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Client Name</label>
<p className="text-sm text-gray-900 dark:text-white">{selectedReceipt.clientName}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Sale Amount</label>
<p className="text-sm text-gray-900 dark:text-white">
${selectedReceipt.saleAmount.toLocaleString()} {selectedReceipt.currency}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Sale Date</label>
<p className="text-sm text-gray-900 dark:text-white">
{new Date(selectedReceipt.saleDate).toLocaleDateString()}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Status</label>
<span className={cn(
"inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-full",
getStatusColor(selectedReceipt.status)
)}>
{getStatusIcon(selectedReceipt.status)}
{selectedReceipt.status.charAt(0).toUpperCase() + selectedReceipt.status.slice(1)}
</span>
</div>
</div>
{selectedReceipt.clientEmail && (
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Client Email</label>
<p className="text-sm text-gray-900 dark:text-white">{selectedReceipt.clientEmail}</p>
</div>
)}
{selectedReceipt.clientPhone && (
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Client Phone</label>
<p className="text-sm text-gray-900 dark:text-white">{selectedReceipt.clientPhone}</p>
</div>
)}
{selectedReceipt.clientAddress && (
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Client Address</label>
<p className="text-sm text-gray-900 dark:text-white">{selectedReceipt.clientAddress}</p>
</div>
)}
{selectedReceipt.description && (
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Description</label>
<p className="text-sm text-gray-900 dark:text-white">{selectedReceipt.description}</p>
</div>
)}
{selectedReceipt.rejectionReason && (
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">Rejection Reason</label>
<p className="text-sm text-red-600 dark:text-red-400">{selectedReceipt.rejectionReason}</p>
</div>
)}
<div className="pt-4 flex gap-3">
<button
onClick={() => handleDownloadReceipt(selectedReceipt.id)}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Download className="w-4 h-4 inline mr-2" />
Download
</button>
<button
onClick={() => setSelectedReceipt(null)}
className="flex-1 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
Close
</button>
</div>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">Confirm Delete</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Are you sure you want to delete this receipt? This action cannot be undone.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => {
setIsDeleteModalOpen(false);
setDeletingReceiptId(null);
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
Cancel
</button>
<button
onClick={confirmDelete}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Delete Receipt
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Receipts;

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,19 @@ export interface RegisterRequest {
lastName: string;
phone?: string;
company?: string;
// Vendor-specific fields
companyType?: 'corporation' | 'llc' | 'partnership' | 'sole_proprietorship' | 'other';
registrationNumber?: string;
gstNumber?: string;
panNumber?: string;
address?: string;
website?: string;
businessLicense?: string;
taxId?: string;
industry?: string;
yearsInBusiness?: string | number;
annualRevenue?: string | number;
employeeCount?: string | number;
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';
}
@ -66,6 +79,127 @@ export interface User {
}>;
}
export interface Product {
id: number;
name: string;
description?: string;
category: 'cloud_storage' | 'cloud_computing' | 'cybersecurity' | 'data_analytics' | 'ai_ml' | 'iot' | 'blockchain' | 'other';
subcategory?: string;
price: number;
currency: string;
commissionRate: number;
features?: string[];
specifications?: Record<string, any>;
images?: string[];
documents?: string[];
status: 'draft' | 'active' | 'inactive' | 'discontinued';
availability: 'available' | 'out_of_stock' | 'coming_soon' | 'discontinued';
stockQuantity: number;
sku: string;
tags?: string[];
metadata?: Record<string, any>;
createdBy: number;
updatedBy?: number;
createdAt: string;
updatedAt: string;
creator?: {
id: number;
firstName: string;
lastName: string;
email: string;
company?: string;
};
updater?: {
id: number;
firstName: string;
lastName: string;
email: string;
};
vendor?: {
id: number;
firstName: string;
lastName: string;
email: string;
company?: string;
};
isAdminCreated?: boolean;
source?: 'admin' | 'vendor';
purchaseUrl?: string;
}
export interface TrainingCategory {
id: number;
name: string;
description?: string;
icon?: string;
color?: string;
sortOrder: number;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface TrainingVideo {
id: number;
moduleId: number;
title: string;
description?: string;
youtubeUrl?: string;
duration?: string;
thumbnail?: string;
sortOrder: number;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface TrainingMaterial {
id: number;
moduleId: number;
title: string;
description?: string;
type: 'PDF' | 'PPT' | 'DOC' | 'VIDEO';
downloadUrl?: string;
fileSize?: string;
sortOrder: number;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface TrainingModule {
id: number;
title: string;
description?: string;
duration?: string;
level: 'Beginner' | 'Intermediate' | 'Advanced';
categoryId?: number;
thumbnailUrl?: string;
isActive: boolean;
sortOrder: number;
createdBy?: number;
createdAt: string;
updatedAt: string;
category?: TrainingCategory;
videos?: TrainingVideo[];
materials?: TrainingMaterial[];
userProgress?: {
status: 'not_started' | 'in_progress' | 'completed';
progressPercentage: number;
timeSpent: number;
completedAt?: string;
};
}
export interface TrainingProgress {
moduleId: number;
videoId?: number;
materialId?: number;
status: 'not_started' | 'in_progress' | 'completed';
progressPercentage: number;
timeSpent: number;
}
class ApiService {
private baseURL: string;
@ -101,6 +235,14 @@ class ApiService {
const data = await response.json();
if (!response.ok) {
// Handle account status errors
if (response.status === 403 && data.errorCode) {
const error = new Error(data.message || 'Account is not active');
(error as any).errorCode = data.errorCode;
(error as any).status = data.status;
throw error;
}
throw new Error(data.message || 'API request failed');
}
@ -207,6 +349,271 @@ class ApiService {
body: JSON.stringify({ currentPassword, newPassword }),
});
}
// Vendor operations
async getAvailableVendorCompanies(): Promise<{ success: boolean; data: Array<{ id: number; company: string; firstName: string; lastName: string; email: string }> }> {
return this.request<{ success: boolean; data: Array<{ id: number; company: string; firstName: string; lastName: string; email: string }> }>('/public/vendors/available-companies');
}
// Reseller operations
async getResellerUserTypes(): Promise<{ success: boolean; data: Array<{ value: string; label: string; description: string; permissions: string[] }> }> {
return this.request<{ success: boolean; data: Array<{ value: string; label: string; description: string; permissions: string[] }> }>('/public/reseller/user-types');
}
async getPendingResellerRequests(): Promise<{ success: boolean; data: User[] }> {
return this.request<{ success: boolean; data: User[] }>('/vendors/pending-resellers');
}
async getVendorResellers(): Promise<{ success: boolean; data: User[] }> {
return this.request<{ success: boolean; data: User[] }>('/vendors/resellers');
}
async createReseller(resellerData: {
firstName: string;
lastName: string;
email: string;
phone: string;
company: string;
userType: 'reseller_admin' | 'reseller_sales' | 'reseller_support' | 'read_only';
region: string;
businessType: string;
address?: string;
}): Promise<{ success: boolean; message: string; data: any }> {
return this.request<{ success: boolean; message: string; data: any }>('/vendors/resellers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(resellerData),
});
}
async getVendorDashboardStats(): Promise<{ success: boolean; data: any }> {
return this.request<{ success: boolean; data: any }>('/vendors/dashboard/stats');
}
async getVendorProducts(params?: {
page?: number;
limit?: number;
category?: string;
status?: string;
search?: string;
}): Promise<{ success: boolean; data: { products: Product[]; pagination: any } }> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) queryParams.append(key, value.toString());
});
}
return this.request<{ success: boolean; data: { products: Product[]; pagination: any } }>(`/vendors/products?${queryParams}`);
}
async getVendorCommissions(params?: {
page?: number;
limit?: number;
status?: string;
dateRange?: string;
}): Promise<{ success: boolean; data: { commissions: any[]; pagination: any } }> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) queryParams.append(key, value.toString());
});
}
return this.request<{ success: boolean; data: { commissions: any[]; pagination: any } }>(`/vendors/commissions?${queryParams}`);
}
async approveResellerRequest(userId: number): Promise<{ success: boolean; message: string }> {
return this.request<{ success: boolean; message: string }>(`/vendors/resellers/${userId}/approve`, {
method: 'POST'
});
}
async rejectResellerRequest(userId: number, reason: string): Promise<{ success: boolean; message: string }> {
return this.request<{ success: boolean; message: string }>(`/vendors/resellers/${userId}/reject`, {
method: 'POST',
body: JSON.stringify({ reason })
});
}
// Product management
async getAllProducts(params?: {
page?: number;
limit?: number;
category?: string;
status?: string;
search?: string;
vendor?: string;
sortBy?: string;
sortOrder?: string;
}): Promise<{ success: boolean; data: { products: Product[]; pagination: any } }> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) queryParams.append(key, value.toString());
});
}
return this.request<{ success: boolean; data: { products: Product[]; pagination: any } }>(`/products?${queryParams}`);
}
async getProductById(id: number): Promise<{ success: boolean; data: Product }> {
return this.request<{ success: boolean; data: Product }>(`/products/${id}`);
}
async createProduct(productData: Partial<Product>): Promise<{ success: boolean; data: Product }> {
return this.request<{ success: boolean; data: Product }>('/products', {
method: 'POST',
body: JSON.stringify(productData),
});
}
async updateProduct(id: number, productData: Partial<Product>): Promise<{ success: boolean; data: Product }> {
return this.request<{ success: boolean; data: Product }>(`/products/${id}`, {
method: 'PUT',
body: JSON.stringify(productData),
});
}
async deleteProduct(id: number): Promise<{ success: boolean; message: string }> {
return this.request<{ success: boolean; message: string }>(`/products/${id}`, {
method: 'DELETE',
});
}
async getProductCategories(): Promise<{ success: boolean; data: string[] }> {
return this.request<{ success: boolean; data: string[] }>('/products/categories');
}
async getProductStats(): Promise<{ success: boolean; data: any }> {
return this.request<{ success: boolean; data: any }>('/products/stats');
}
async getActiveVendors(): Promise<{ success: boolean; data: Array<{ id: number; firstName: string; lastName: string; company?: string }> }> {
return this.request<{ success: boolean; data: Array<{ id: number; firstName: string; lastName: string; company?: string }> }>('/products/vendors');
}
async getVendorById(id: number): Promise<{ success: boolean; data: { id: number; firstName: string; lastName: string; email: string; company?: string } }> {
return this.request<{ success: boolean; data: { id: number; firstName: string; lastName: string; email: string; company?: string } }>(`/vendors/${id}`);
}
// Receipt management
async uploadReceipt(data: FormData): Promise<{ success: boolean; data: any; message: string }> {
return this.request<{ success: boolean; data: any; message: string }>('/receipts/upload', {
method: 'POST',
body: data,
});
}
async getResellerReceipts(params?: {
page?: number;
limit?: number;
status?: string;
startDate?: string;
endDate?: string;
}): Promise<{ success: boolean; data: any[]; pagination?: any }> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) queryParams.append(key, value.toString());
});
}
return this.request<{ success: boolean; data: any[]; pagination?: any }>(`/receipts/reseller?${queryParams}`);
}
async getVendorReceipts(params?: {
page?: number;
limit?: number;
status?: string;
resellerId?: number;
startDate?: string;
endDate?: string;
}): Promise<{ success: boolean; data: any[]; pagination?: any }> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) queryParams.append(key, value.toString());
});
}
return this.request<{ success: boolean; data: any[]; pagination?: any }>(`/receipts/vendor?${queryParams}`);
}
async getReceiptById(id: number): Promise<{ success: boolean; data: any }> {
return this.request<{ success: boolean; data: any }>(`/receipts/reseller/${id}`);
}
async getVendorReceiptById(id: number): Promise<{ success: boolean; data: any }> {
return this.request<{ success: boolean; data: any }>(`/receipts/vendor/${id}`);
}
async updateReceiptStatus(id: number, statusUpdate: { status: string; rejectionReason?: string }): Promise<{ success: boolean; data: any; message: string }> {
return this.request<{ success: boolean; data: any; message: string }>(`/receipts/vendor/${id}/status`, {
method: 'PUT',
body: JSON.stringify(statusUpdate),
});
}
async downloadReceipt(id: number): Promise<Blob> {
const response = await fetch(`${this.baseURL}/receipts/${id}/download`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
}
async deleteReceipt(id: number): Promise<{ success: boolean; message: string }> {
return this.request<{ success: boolean; message: string }>(`/receipts/reseller/${id}`, {
method: 'DELETE',
});
}
// Get vendor products with current stock quantities
async getVendorProductsWithStock(params?: {
page?: number;
limit?: number;
category?: string;
status?: string;
search?: string;
}): Promise<{ success: boolean; data: { products: any[]; pagination: any } }> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) queryParams.append(key, value.toString());
});
}
return this.request<{ success: boolean; data: { products: any[]; pagination: any } }>(`/receipts/vendor/products/stock?${queryParams}`);
}
// Get products shared by vendor for reseller with current stock
async getResellerVendorProducts(params: {
vendorId: number;
page?: number;
limit?: number;
category?: string;
status?: string;
search?: string;
}): Promise<{ success: boolean; data: { products: any[]; pagination: any } }> {
const queryParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) queryParams.append(key, value.toString());
});
return this.request<{ success: boolean; data: { products: any[]; pagination: any } }>(`/receipts/reseller/vendor/products?${queryParams}`);
}
// Manually update product stock quantity
async updateProductStock(productId: number, stockQuantity: number): Promise<{ success: boolean; data: any; message: string }> {
return this.request<{ success: boolean; data: any; message: string }>(`/receipts/vendor/products/${productId}/stock`, {
method: 'PUT',
body: JSON.stringify({ stockQuantity }),
});
}
}
export const apiService = new ApiService();

View File

@ -0,0 +1,187 @@
import io from 'socket.io-client';
class SocketService {
private socket: any = null;
private isConnected = false;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
// Event listeners
private listeners: Map<string, Function[]> = new Map();
connect(token: string) {
if (this.socket && this.isConnected) {
return;
}
try {
this.socket = io(process.env.REACT_APP_SOCKET_URL || 'http://localhost:5000', {
auth: {
token
},
transports: ['websocket', 'polling'],
timeout: 20000,
reconnection: true,
reconnectionAttempts: this.maxReconnectAttempts,
reconnectionDelay: this.reconnectDelay
});
this.setupEventListeners();
} catch (error) {
console.error('Socket connection error:', error);
}
}
private setupEventListeners() {
if (!this.socket) return;
this.socket.on('connect', () => {
console.log('Socket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
this.emit('socket_connected', { timestamp: new Date().toISOString() });
});
this.socket.on('disconnect', (reason: string) => {
console.log('Socket disconnected:', reason);
this.isConnected = false;
if (reason === 'io server disconnect') {
// Server disconnected us, try to reconnect
this.socket?.connect();
}
});
this.socket.on('connect_error', (error: Error) => {
console.error('Socket connection error:', error);
this.reconnectAttempts++;
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
}
});
this.socket.on('reconnect', (attemptNumber: number) => {
console.log('Socket reconnected after', attemptNumber, 'attempts');
this.isConnected = true;
this.reconnectAttempts = 0;
});
this.socket.on('reconnect_error', (error: Error) => {
console.error('Socket reconnection error:', error);
});
// Handle notification events
this.socket.on('NEW_VENDOR_REQUEST', (data: any) => {
this.triggerListeners('NEW_VENDOR_REQUEST', data);
});
this.socket.on('NEW_RESELLER_REQUEST', (data: any) => {
this.triggerListeners('NEW_RESELLER_REQUEST', data);
});
this.socket.on('VENDOR_APPROVED', (data: any) => {
this.triggerListeners('VENDOR_APPROVED', data);
});
this.socket.on('VENDOR_REJECTED', (data: any) => {
this.triggerListeners('VENDOR_REJECTED', data);
});
this.socket.on('RESELLER_CREATED', (data: any) => {
this.triggerListeners('RESELLER_CREATED', data);
});
this.socket.on('RESELLER_ACCOUNT_CREATED', (data: any) => {
this.triggerListeners('RESELLER_ACCOUNT_CREATED', data);
});
this.socket.on('SYSTEM_ALERT', (data: any) => {
this.triggerListeners('SYSTEM_ALERT', data);
});
// Handle general notifications
this.socket.on('notification', (data: any) => {
this.triggerListeners('notification', data);
});
}
disconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
this.isConnected = false;
this.listeners.clear();
}
}
emit(event: string, data: any) {
if (this.socket && this.isConnected) {
this.socket.emit(event, data);
} else {
console.warn('Socket not connected, cannot emit event:', event);
}
}
on(event: string, callback: Function) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)?.push(callback);
}
off(event: string, callback?: Function) {
if (!callback) {
this.listeners.delete(event);
} else {
const callbacks = this.listeners.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
}
private triggerListeners(event: string, data: any) {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('Error in socket event listener:', error);
}
});
}
}
isSocketConnected(): boolean {
return this.isConnected;
}
// Join specific rooms
joinRoom(room: string) {
this.emit('join-room', room);
}
leaveRoom(room: string) {
this.emit('leave-room', room);
}
// Get connection status
getConnectionStatus() {
return {
isConnected: this.isConnected,
reconnectAttempts: this.reconnectAttempts,
maxReconnectAttempts: this.maxReconnectAttempts
};
}
}
// Create singleton instance
const socketService = new SocketService();
export default socketService;

View File

@ -3,6 +3,8 @@ import themeReducer from './slices/themeSlice';
import authReducer from './slices/authSlice';
import dashboardReducer from './slices/dashboardSlice';
import resellerDashboardReducer from './reseller/dashboardSlice';
import productReducer from './slices/productSlice';
import receiptReducer from './slices/receiptSlice';
export const store = configureStore({
reducer: {
@ -10,6 +12,8 @@ export const store = configureStore({
auth: authReducer,
dashboard: dashboardReducer,
resellerDashboard: resellerDashboardReducer,
product: productReducer,
receipts: receiptReducer,
},
});

View File

@ -13,12 +13,12 @@ interface AuthState {
const initialState: AuthState = {
user: null,
isAuthenticated: false,
isAuthenticated: !!localStorage.getItem('accessToken'),
isLoading: false,
error: null,
token: null,
refreshToken: null,
sessionId: null,
token: localStorage.getItem('accessToken'),
refreshToken: localStorage.getItem('refreshToken'),
sessionId: localStorage.getItem('sessionId'),
};
const authSlice = createSlice({
@ -38,6 +38,11 @@ const authSlice = createSlice({
state.sessionId = action.payload.sessionId;
state.isAuthenticated = true;
state.error = null;
state.isLoading = false;
// Store tokens in localStorage
localStorage.setItem('accessToken', action.payload.token);
localStorage.setItem('refreshToken', action.payload.refreshToken);
localStorage.setItem('sessionId', action.payload.sessionId);
},
logout: (state) => {
state.user = null;
@ -46,6 +51,11 @@ const authSlice = createSlice({
state.sessionId = null;
state.isAuthenticated = false;
state.error = null;
state.isLoading = false;
// Clear localStorage
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('sessionId');
},
updateUser: (state, action: PayloadAction<Partial<User>>) => {
if (state.user) {
@ -56,10 +66,21 @@ const authSlice = createSlice({
state.token = action.payload.token;
state.refreshToken = action.payload.refreshToken;
state.sessionId = action.payload.sessionId;
// Set authenticated to true when tokens are set
state.isAuthenticated = true;
// Store tokens in localStorage
localStorage.setItem('accessToken', action.payload.token);
localStorage.setItem('refreshToken', action.payload.refreshToken);
localStorage.setItem('sessionId', action.payload.sessionId);
},
setUser: (state, action: PayloadAction<User>) => {
state.user = action.payload;
state.isAuthenticated = true;
state.error = null;
},
},
});
export const { setLoading, setError, loginSuccess, logout, updateUser, setTokens } = authSlice.actions;
export const { setLoading, setError, loginSuccess, logout, updateUser, setTokens, setUser } = authSlice.actions;
export type { User };
export default authSlice.reducer;

View File

@ -1,6 +1,6 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { apiService, LoginRequest, RegisterRequest } from '../../services/api';
import { setLoading, setError, loginSuccess, logout, setTokens } from './authSlice';
import { setLoading, setError, loginSuccess, logout, setTokens, setUser } from './authSlice';
export const loginUser = createAsyncThunk(
'auth/login',
@ -12,12 +12,13 @@ export const loginUser = createAsyncThunk(
const response = await apiService.login(credentials);
if (response.success && response.data) {
// Store tokens in localStorage
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
localStorage.setItem('sessionId', response.data.sessionId);
// Check if user account is blocked
const userStatus = response.data.user.status;
if (['inactive', 'pending', 'suspended'].includes(userStatus)) {
throw new Error(`Account access blocked. Status: ${userStatus}. Please contact your vendor administrator.`);
}
// Dispatch login success
// Dispatch login success (tokens will be stored in the slice)
dispatch(loginSuccess({
user: response.data.user,
token: response.data.accessToken,
@ -122,19 +123,11 @@ export const logoutUser = createAsyncThunk(
await apiService.logout(sessionId);
}
// Clear localStorage
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('sessionId');
// Dispatch logout
// Dispatch logout (localStorage will be cleared in the slice)
dispatch(logout());
} catch (error) {
console.error('Logout error:', error);
// Still logout locally even if API call fails
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('sessionId');
dispatch(logout());
}
}
@ -150,6 +143,15 @@ export const getCurrentUser = createAsyncThunk(
const response = await apiService.getCurrentUser();
if (response.success) {
// Check if user account is blocked
const userStatus = response.data.status;
if (['inactive', 'pending', 'suspended'].includes(userStatus)) {
// Force logout if account is blocked
dispatch(logout());
throw new Error(`Account access blocked. Status: ${userStatus}. Please contact your vendor administrator.`);
}
dispatch(setUser(response.data));
return response.data;
} else {
throw new Error('Failed to get current user');
@ -178,12 +180,17 @@ export const refreshUserToken = createAsyncThunk(
const response = await apiService.refreshToken(refreshToken);
if (response.success && response.data) {
// Update tokens in localStorage
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
localStorage.setItem('sessionId', response.data.sessionId);
// Check if user account is blocked (if user data is included in refresh response)
if (response.data.user) {
const userStatus = response.data.user.status;
if (['inactive', 'pending', 'suspended'].includes(userStatus)) {
// Force logout if account is blocked
dispatch(logout());
throw new Error(`Account access blocked. Status: ${userStatus}. Please contact your vendor administrator.`);
}
}
// Update tokens in store
// Update tokens in store (localStorage will be updated in the slice)
dispatch(setTokens({
token: response.data.accessToken,
refreshToken: response.data.refreshToken,

View File

@ -0,0 +1,117 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Product } from '../../services/api';
interface ProductState {
products: Product[];
currentProduct: Product | null;
categories: string[];
stats: any;
pagination: {
currentPage: number;
totalPages: number;
totalItems: number;
itemsPerPage: number;
};
filters: {
category: string;
status: string;
search: string;
sortBy: string;
sortOrder: string;
};
isLoading: boolean;
error: string | null;
}
const initialState: ProductState = {
products: [],
currentProduct: null,
categories: [],
stats: null,
pagination: {
currentPage: 1,
totalPages: 1,
totalItems: 0,
itemsPerPage: 10,
},
filters: {
category: '',
status: '',
search: '',
sortBy: 'createdAt',
sortOrder: 'DESC',
},
isLoading: false,
error: null,
};
const productSlice = createSlice({
name: 'product',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
setProducts: (state, action: PayloadAction<Product[]>) => {
state.products = action.payload;
},
setCurrentProduct: (state, action: PayloadAction<Product | null>) => {
state.currentProduct = action.payload;
},
setCategories: (state, action: PayloadAction<string[]>) => {
state.categories = action.payload;
},
setStats: (state, action: PayloadAction<any>) => {
state.stats = action.payload;
},
setPagination: (state, action: PayloadAction<any>) => {
state.pagination = action.payload;
},
setFilters: (state, action: PayloadAction<Partial<ProductState['filters']>>) => {
state.filters = { ...state.filters, ...action.payload };
},
addProduct: (state, action: PayloadAction<Product>) => {
state.products.unshift(action.payload);
},
updateProduct: (state, action: PayloadAction<Product>) => {
const index = state.products.findIndex(p => p.id === action.payload.id);
if (index !== -1) {
state.products[index] = action.payload;
}
if (state.currentProduct?.id === action.payload.id) {
state.currentProduct = action.payload;
}
},
removeProduct: (state, action: PayloadAction<number>) => {
state.products = state.products.filter(p => p.id !== action.payload);
if (state.currentProduct?.id === action.payload) {
state.currentProduct = null;
}
},
clearProducts: (state) => {
state.products = [];
state.currentProduct = null;
state.pagination = initialState.pagination;
},
},
});
export const {
setLoading,
setError,
setProducts,
setCurrentProduct,
setCategories,
setStats,
setPagination,
setFilters,
addProduct,
updateProduct,
removeProduct,
clearProducts,
} = productSlice.actions;
export default productSlice.reducer;

View File

@ -0,0 +1,192 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { apiService, Product } from '../../services/api';
import {
setLoading,
setError,
setProducts,
setCurrentProduct,
setCategories,
setStats,
setPagination,
addProduct,
updateProduct,
removeProduct,
} from './productSlice';
export const fetchProducts = createAsyncThunk(
'product/fetchProducts',
async (params: {
page?: number;
limit?: number;
category?: string;
status?: string;
search?: string;
sortBy?: string;
sortOrder?: string;
} = {}, { dispatch }) => {
try {
dispatch(setLoading(true));
dispatch(setError(null));
const response = await apiService.getAllProducts(params);
if (response.success) {
dispatch(setProducts(response.data.products));
dispatch(setPagination(response.data.pagination));
return response.data;
} else {
throw new Error('Failed to fetch products');
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to fetch products';
dispatch(setError(errorMessage));
throw error;
} finally {
dispatch(setLoading(false));
}
}
);
export const fetchProductById = createAsyncThunk(
'product/fetchProductById',
async (id: number, { dispatch }) => {
try {
dispatch(setLoading(true));
dispatch(setError(null));
const response = await apiService.getProductById(id);
if (response.success) {
dispatch(setCurrentProduct(response.data));
return response.data;
} else {
throw new Error('Failed to fetch product');
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to fetch product';
dispatch(setError(errorMessage));
throw error;
} finally {
dispatch(setLoading(false));
}
}
);
export const createProduct = createAsyncThunk(
'product/createProduct',
async (productData: Partial<Product>, { dispatch }) => {
try {
dispatch(setLoading(true));
dispatch(setError(null));
const response = await apiService.createProduct(productData);
if (response.success) {
dispatch(addProduct(response.data));
return response.data;
} else {
throw new Error('Failed to create product');
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to create product';
dispatch(setError(errorMessage));
throw error;
} finally {
dispatch(setLoading(false));
}
}
);
export const updateProductById = createAsyncThunk(
'product/updateProductById',
async ({ id, productData }: { id: number; productData: Partial<Product> }, { dispatch }) => {
try {
dispatch(setLoading(true));
dispatch(setError(null));
const response = await apiService.updateProduct(id, productData);
if (response.success) {
dispatch(updateProduct(response.data));
return response.data;
} else {
throw new Error('Failed to update product');
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to update product';
dispatch(setError(errorMessage));
throw error;
} finally {
dispatch(setLoading(false));
}
}
);
export const deleteProductById = createAsyncThunk(
'product/deleteProductById',
async (id: number, { dispatch }) => {
try {
dispatch(setLoading(true));
dispatch(setError(null));
const response = await apiService.deleteProduct(id);
if (response.success) {
dispatch(removeProduct(id));
return response;
} else {
throw new Error('Failed to delete product');
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to delete product';
dispatch(setError(errorMessage));
throw error;
} finally {
dispatch(setLoading(false));
}
}
);
export const fetchProductCategories = createAsyncThunk(
'product/fetchProductCategories',
async (_, { dispatch }) => {
try {
dispatch(setError(null));
const response = await apiService.getProductCategories();
if (response.success) {
dispatch(setCategories(response.data));
return response.data;
} else {
throw new Error('Failed to fetch product categories');
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to fetch product categories';
dispatch(setError(errorMessage));
throw error;
}
}
);
export const fetchProductStats = createAsyncThunk(
'product/fetchProductStats',
async (_, { dispatch }) => {
try {
dispatch(setError(null));
const response = await apiService.getProductStats();
if (response.success) {
dispatch(setStats(response.data));
return response.data;
} else {
throw new Error('Failed to fetch product stats');
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to fetch product stats';
dispatch(setError(errorMessage));
throw error;
}
}
);

View File

@ -0,0 +1,232 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { Receipt, ReceiptUploadData, ReceiptFilters, ReceiptResponse } from '../../types/receipt';
import apiService from '../../services/api';
// Async thunks
export const fetchReceipts = createAsyncThunk(
'receipts/fetchReceipts',
async (filters: ReceiptFilters = {}) => {
const response = await apiService.getResellerReceipts(filters);
if (!response.success) {
throw new Error('Failed to fetch receipts');
}
return response;
}
);
export const uploadReceipt = createAsyncThunk(
'receipts/uploadReceipt',
async (data: FormData) => {
const response = await apiService.uploadReceipt(data);
if (!response.success) {
throw new Error(response.message || 'Failed to upload receipt');
}
return response;
}
);
export const deleteReceipt = createAsyncThunk(
'receipts/deleteReceipt',
async (receiptId: number) => {
const response = await apiService.deleteReceipt(receiptId);
if (!response.success) {
throw new Error(response.message || 'Failed to delete receipt');
}
return { receiptId, ...response };
}
);
export const downloadReceipt = createAsyncThunk(
'receipts/downloadReceipt',
async (receiptId: number) => {
const blob = await apiService.downloadReceipt(receiptId);
return { receiptId, blob };
}
);
// State interface
interface ReceiptState {
receipts: Receipt[];
pagination: {
currentPage: number;
totalPages: number;
totalItems: number;
itemsPerPage: number;
} | null;
filters: ReceiptFilters;
isLoading: boolean;
isUploading: boolean;
error: string | null;
selectedReceipt: Receipt | null;
stats: {
totalReceipts: number;
pendingApproval: number;
approved: number;
totalSales: number;
};
}
// Initial state
const initialState: ReceiptState = {
receipts: [],
pagination: null,
filters: {
status: '',
startDate: '',
endDate: '',
page: 1,
limit: 10
},
isLoading: false,
isUploading: false,
error: null,
selectedReceipt: null,
stats: {
totalReceipts: 0,
pendingApproval: 0,
approved: 0,
totalSales: 0
}
};
// Slice
const receiptSlice = createSlice({
name: 'receipts',
initialState,
reducers: {
setFilters: (state, action: PayloadAction<Partial<ReceiptFilters>>) => {
state.filters = { ...state.filters, ...action.payload };
// Reset to first page when filters change
if (action.payload.page === undefined) {
state.filters.page = 1;
}
},
clearFilters: (state) => {
state.filters = {
status: '',
startDate: '',
endDate: '',
page: 1,
limit: 10
};
},
setSelectedReceipt: (state, action: PayloadAction<Receipt | null>) => {
state.selectedReceipt = action.payload;
},
clearError: (state) => {
state.error = null;
},
updateReceiptStatus: (state, action: PayloadAction<{ receiptId: number; status: string; rejectionReason?: string }>) => {
const receipt = state.receipts.find(r => r.id === action.payload.receiptId);
if (receipt) {
receipt.status = action.payload.status as any;
if (action.payload.rejectionReason) {
receipt.rejectionReason = action.payload.rejectionReason;
}
}
},
calculateStats: (state) => {
const receipts = state.receipts;
state.stats = {
totalReceipts: receipts.length,
pendingApproval: receipts.filter(r => r.status === 'pending').length,
approved: receipts.filter(r => r.status === 'approved').length,
totalSales: receipts.reduce((sum, r) => sum + r.saleAmount, 0)
};
}
},
extraReducers: (builder) => {
// Fetch receipts
builder
.addCase(fetchReceipts.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchReceipts.fulfilled, (state, action) => {
state.isLoading = false;
state.receipts = action.payload.data || [];
state.pagination = action.payload.pagination || null;
// Calculate stats
const receipts = action.payload.data || [];
state.stats = {
totalReceipts: receipts.length,
pendingApproval: receipts.filter(r => r.status === 'pending').length,
approved: receipts.filter(r => r.status === 'approved').length,
totalSales: receipts.reduce((sum, r) => sum + r.saleAmount, 0)
};
})
.addCase(fetchReceipts.rejected, (state, action) => {
state.isLoading = false;
state.error = action.error.message || 'Failed to fetch receipts';
});
// Upload receipt
builder
.addCase(uploadReceipt.pending, (state) => {
state.isUploading = true;
state.error = null;
})
.addCase(uploadReceipt.fulfilled, (state, action) => {
state.isUploading = false;
// Add the new receipt to the list
if (action.payload.data) {
state.receipts.unshift(action.payload.data);
// Recalculate stats
const receipts = state.receipts;
state.stats = {
totalReceipts: receipts.length,
pendingApproval: receipts.filter(r => r.status === 'pending').length,
approved: receipts.filter(r => r.status === 'approved').length,
totalSales: receipts.reduce((sum, r) => sum + r.saleAmount, 0)
};
}
})
.addCase(uploadReceipt.rejected, (state, action) => {
state.isUploading = false;
state.error = action.error.message || 'Failed to upload receipt';
});
// Delete receipt
builder
.addCase(deleteReceipt.fulfilled, (state, action) => {
state.receipts = state.receipts.filter(r => r.id !== action.payload.receiptId);
// Recalculate stats
const receipts = state.receipts;
state.stats = {
totalReceipts: receipts.length,
pendingApproval: receipts.filter(r => r.status === 'pending').length,
approved: receipts.filter(r => r.status === 'approved').length,
totalSales: receipts.reduce((sum, r) => sum + r.saleAmount, 0)
};
});
// Download receipt
builder
.addCase(downloadReceipt.fulfilled, (state, action) => {
// Handle download success (usually no state changes needed)
});
}
});
// Export actions
export const {
setFilters,
clearFilters,
setSelectedReceipt,
clearError,
updateReceiptStatus,
calculateStats
} = receiptSlice.actions;
// Export selectors
export const selectReceipts = (state: { receipts: ReceiptState }) => state.receipts.receipts;
export const selectReceiptPagination = (state: { receipts: ReceiptState }) => state.receipts.pagination;
export const selectReceiptFilters = (state: { receipts: ReceiptState }) => state.receipts.filters;
export const selectReceiptsLoading = (state: { receipts: ReceiptState }) => state.receipts.isLoading;
export const selectReceiptsUploading = (state: { receipts: ReceiptState }) => state.receipts.isUploading;
export const selectReceiptsError = (state: { receipts: ReceiptState }) => state.receipts.error;
export const selectSelectedReceipt = (state: { receipts: ReceiptState }) => state.receipts.selectedReceipt;
export const selectReceiptStats = (state: { receipts: ReceiptState }) => state.receipts.stats;
// Export reducer
export default receiptSlice.reducer;

View File

@ -12,10 +12,10 @@ const getInitialTheme = (): Theme => {
const savedTheme = localStorage.getItem('theme') as Theme;
if (savedTheme) return savedTheme;
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
return systemTheme;
// Default to dark theme instead of system preference
return 'dark';
}
return 'light';
return 'dark';
};
const initialState: ThemeState = {

104
src/types/receipt.ts Normal file
View File

@ -0,0 +1,104 @@
export interface Receipt {
id: number;
receiptNumber: string;
orderId: number;
resellerId: number;
vendorId: number;
channelPartnerId: number;
clientName: string;
clientEmail?: string;
clientPhone?: string;
clientAddress?: string;
saleAmount: number;
currency: string;
saleDate: string;
paymentMethod?: string;
paymentStatus: 'pending' | 'paid' | 'partial' | 'failed';
receiptFile: string;
receiptFileType?: string;
receiptFileSize?: number;
description?: string;
status: 'pending' | 'approved' | 'rejected' | 'disputed';
approvedBy?: number;
approvedAt?: string;
rejectionReason?: string;
metadata?: Record<string, any>;
createdAt: string;
updatedAt: string;
// Related data
order?: {
id: number;
orderNumber: string;
items?: Array<{
id: number;
product: {
id: number;
name: string;
sku: string;
price: number;
};
quantity: number;
unitPrice: number;
}>;
};
vendor?: {
id: number;
firstName: string;
lastName: string;
email: string;
company?: string;
};
reseller?: {
id: number;
companyName: string;
contactEmail: string;
contactPhone: string;
};
approver?: {
id: number;
firstName: string;
lastName: string;
email: string;
};
}
export interface ReceiptUploadData {
orderId: number;
clientName: string;
clientEmail?: string;
clientPhone?: string;
clientAddress?: string;
saleAmount: number;
currency?: string;
saleDate?: string;
paymentMethod?: string;
description?: string;
receiptFile?: File | null;
}
export interface ReceiptFilters {
status?: string;
startDate?: string;
endDate?: string;
resellerId?: number;
search?: string;
page?: number;
limit?: number;
}
export interface ReceiptResponse {
success: boolean;
data: Receipt[];
pagination?: {
currentPage: number;
totalPages: number;
totalItems: number;
itemsPerPage: number;
};
}
export interface ReceiptStatusUpdate {
status: 'approved' | 'rejected';
rejectionReason?: string;
}

41
src/types/vendor.ts Normal file
View File

@ -0,0 +1,41 @@
export 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;
// Vendor-specific fields from User model
companyType?: string;
registrationNumber?: string;
gstNumber?: string;
panNumber?: string;
address?: string;
website?: string;
businessLicense?: string;
taxId?: string;
industry?: string;
yearsInBusiness?: number;
annualRevenue?: number;
employeeCount?: number;
}
export interface VendorModalProps {
vendor: VendorRequest | null;
isOpen: boolean;
onClose: () => void;
onApprove: (vendorId: string) => void;
onReject: (vendorId: string, reason: string) => void;
}
export interface RejectionModalProps {
vendor: VendorRequest | null;
isOpen: boolean;
onClose: () => void;
onReject: (vendorId: string, reason: string) => void;
}

38
src/utils/authTest.ts Normal file
View File

@ -0,0 +1,38 @@
// Utility to test authentication persistence
export const testAuthPersistence = () => {
console.log('=== Testing Authentication Persistence ===');
// Check localStorage
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
const sessionId = localStorage.getItem('sessionId');
console.log('localStorage tokens:');
console.log('- accessToken:', accessToken ? 'Present' : 'Missing');
console.log('- refreshToken:', refreshToken ? 'Present' : 'Missing');
console.log('- sessionId:', sessionId ? 'Present' : 'Missing');
// Check if tokens are valid (not expired)
if (accessToken) {
try {
const payload = JSON.parse(atob(accessToken.split('.')[1]));
const expiry = new Date(payload.exp * 1000);
const now = new Date();
console.log('Token expiry:', expiry.toISOString());
console.log('Current time:', now.toISOString());
console.log('Token expired:', expiry < now);
} catch (error) {
console.log('Error parsing token:', error);
}
}
console.log('==========================================');
};
// Function to simulate page reload
export const simulatePageReload = () => {
console.log('Simulating page reload...');
// Clear any in-memory state but keep localStorage
window.location.reload();
};

View File

@ -1,7 +1,7 @@
// Phone number validation
export const validatePhoneNumber = (phone: string): boolean => {
// Allow international format with optional +, digits only, 7-15 digits total
const phoneRegex = /^[\+]?[1-9][\d]{6,14}$/;
const phoneRegex = /^[+]?[1-9][\d]{6,14}$/;
return phoneRegex.test(phone);
};