initial commit

This commit is contained in:
rohit 2025-08-04 19:39:59 +05:30
commit fcc9942ea1
70 changed files with 29458 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

156
README.md Normal file
View File

@ -0,0 +1,156 @@
# Reseller Portal
A comprehensive portal that combines both Channel Partner and Reseller dashboards in a single application.
## 🚀 Features
### Channel Partner Dashboard
- **Dashboard Overview**: Analytics, stats, and performance metrics
- **Reseller Management**: Manage reseller partners and relationships
- **Partnerships**: Handle partnership requests and approvals
- **Deals Management**: Create and track business deals
- **Commissions**: Track commission earnings and payouts
- **Product Management**: Manage products and pricing strategies
- **Training**: Schedule and manage training programs
- **Support**: Handle support tickets and requests
- **Analytics**: Advanced analytics and insights
- **Reports**: Generate detailed reports
- **Settings**: Account configuration and preferences
### Reseller Dashboard
- **Dashboard Overview**: Performance metrics and revenue tracking
- **Customer Management**: Manage customer relationships
- **Cloud Instances**: Manage cloud infrastructure
- **Billing & Payments**: Handle billing and payment processing
- **Support Center**: Submit and track support tickets
- **Reports & Analytics**: View performance reports
- **Wallet Management**: Manage funds and transactions
- **Training Center**: Access training materials
- **Marketplace**: Browse and purchase services
- **Certifications**: Manage professional certifications
- **Knowledge Base**: Access documentation and guides
## 🎨 Design Features
- **Modern UI/UX**: Clean, professional design with glass morphism effects
- **Dark/Light Theme**: Toggle between themes with persistent preferences
- **Responsive Design**: Optimized for desktop, tablet, and mobile
- **Gradient Backgrounds**: Beautiful gradient themes for each dashboard
- **Interactive Elements**: Hover effects, animations, and smooth transitions
## 🔐 Authentication
### Channel Partner
- **Login**: `/login`
- **Signup**: `/signup`
- **Theme**: Blue/Indigo gradient
### Reseller
- **Login**: `/reseller/login`
- **Signup**: `/reseller/signup` (with user type selection)
- **Theme**: Emerald/Teal gradient
## 🛠️ Technology Stack
- **React 19**: Latest React with hooks and functional components
- **TypeScript**: Type-safe development
- **Redux Toolkit**: State management
- **React Router**: Client-side routing
- **Tailwind CSS**: Utility-first styling
- **Lucide React**: Beautiful SVG icons
- **Recharts**: Data visualization
- **React Cookie**: Cookie management
## 📁 Project Structure
```
src/
├── components/
│ ├── charts/ # Chart components
│ ├── Layout/ # Layout components
│ └── ...
├── pages/
│ ├── reseller/ # Reseller dashboard pages
│ │ ├── Dashboard.tsx
│ │ ├── Customers.tsx
│ │ ├── Instances.tsx
│ │ ├── Billing.tsx
│ │ ├── Support.tsx
│ │ ├── Reports.tsx
│ │ ├── Login.tsx
│ │ └── Signup.tsx
│ ├── Dashboard.tsx # Channel Partner dashboard
│ ├── Login.tsx # Channel Partner login
│ ├── Signup.tsx # Channel Partner signup
│ └── ...
├── store/
│ ├── slices/ # Redux slices
│ └── ...
├── data/
│ └── mockData.ts # Mock data for both dashboards
└── utils/
└── ...
```
## 🚀 Getting Started
1. **Install Dependencies**
```bash
npm install
```
2. **Start Development Server**
```bash
npm start
```
3. **Access the Application**
- Channel Partner Dashboard: `http://localhost:3000/login`
- Reseller Dashboard: `http://localhost:3000/reseller/login`
## 🎯 Key Routes
### Channel Partner Routes
- `/` - Main dashboard
- `/login` - Login page
- `/signup` - Signup page
- `/resellers` - Reseller management
- `/partnerships` - Partnership management
- `/deals` - Deals management
- `/commissions` - Commission tracking
- `/product-management` - Product management
### Reseller Routes
- `/reseller` - Main dashboard
- `/reseller/login` - Login page
- `/reseller/signup` - Signup page
- `/reseller/customers` - Customer management
- `/reseller/instances` - Cloud instances
- `/reseller/billing` - Billing & payments
- `/reseller/support` - Support center
- `/reseller/reports` - Reports & analytics
## 🎨 Theme System
The application supports both light and dark themes with:
- **Persistent theme preferences**
- **System preference detection**
- **Smooth theme transitions**
- **Theme-specific color schemes**
## 📱 Responsive Design
- **Desktop**: Full-featured layout with sidebar navigation
- **Tablet**: Optimized layout with collapsible sidebar
- **Mobile**: Mobile-first design with bottom navigation
## 🔧 Customization
- **Colors**: Easily customizable color schemes in Tailwind config
- **Components**: Modular component architecture for easy customization
- **Data**: Mock data can be replaced with real API endpoints
- **Routing**: Flexible routing system for adding new features
## 📄 License
This project is part of the CloudTopiaa Reseller Portal system.

18475
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

61
package.json Normal file
View File

@ -0,0 +1,61 @@
{
"name": "reseller-portal",
"version": "0.1.0",
"private": true,
"dependencies": {
"@headlessui/react": "^2.2.7",
"@reduxjs/toolkit": "^2.8.2",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.536.0",
"react": "^19.1.1",
"react-cookie": "^8.0.1",
"react-dom": "^19.1.1",
"react-redux": "^9.2.0",
"react-router-dom": "^6.30.1",
"react-scripts": "5.0.1",
"recharts": "^3.1.0",
"tailwind-merge": "^3.3.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"tailwindcss": "^3.4.0"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

43
public/index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Cloudtopiaa Channel Partner Dashboard - Manage resellers, partnerships, deals, and commissions"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Channel Partner Dashboard</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "Channel Partner",
"name": "Cloudtopiaa Channel Partner Dashboard",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

38
src/App.css Normal file
View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

9
src/App.test.tsx Normal file
View File

@ -0,0 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

310
src/App.tsx Normal file
View File

@ -0,0 +1,310 @@
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { Provider } from 'react-redux';
import { CookiesProvider } from 'react-cookie';
import { store } from './store';
import { setTheme } from './store/slices/themeSlice';
import { useAppSelector } from './store/hooks';
// 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 DealsPage from './pages/Deals';
import CommissionsPage from './pages/Commissions';
import ProductManagement from './pages/ProductManagement';
import Login from './pages/Login';
import Signup from './pages/Signup';
// Reseller Components
import ResellerLogin from './pages/reseller/Login';
import ResellerSignup from './pages/reseller/Signup';
import ResellerDashboardMain from './pages/reseller/Dashboard';
import ResellerDashboardCustomers from './pages/reseller/Customers';
import ResellerDashboardInstances from './pages/reseller/Instances';
import ResellerBilling from './pages/reseller/Billing';
import ResellerSupport from './pages/reseller/Support';
import ResellerReports from './pages/reseller/Reports';
import ResellerLayout from './components/reseller/layout/ResellerLayout';
import CookieConsent from './components/CookieConsent';
import './index.css';
// Placeholder components for other pages
const PlaceholderPage: React.FC<{ title: string; description: string }> = ({ title, description }) => (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white">{title}</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">{description}</p>
</div>
<div className="card p-12">
<div className="text-center">
<div className="w-16 h-16 bg-secondary-100 dark:bg-secondary-800 rounded-full flex items-center justify-center mx-auto mb-4">
<div className="w-8 h-8 bg-secondary-400 rounded"></div>
</div>
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-2">
Coming Soon
</h3>
<p className="text-secondary-600 dark:text-secondary-400">
This page is under development and will be available soon.
</p>
</div>
</div>
</div>
);
function App() {
useEffect(() => {
// Initialize theme from localStorage
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
store.dispatch(setTheme(savedTheme as 'light' | 'dark'));
} else {
// Check system preference
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
store.dispatch(setTheme(systemTheme));
}
// Listen for system theme changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
const newTheme = e.matches ? 'dark' : 'light';
store.dispatch(setTheme(newTheme));
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
return (
<Provider store={store}>
<CookiesProvider>
<Router>
<div className="App">
<Routes>
{/* Channel Partner Routes */}
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/" element={
<Layout>
<Dashboard />
</Layout>
} />
<Route path="/resellers" element={
<Layout>
<ResellersPage />
</Layout>
} />
<Route path="/partnerships" element={
<Layout>
<PartnershipsPage />
</Layout>
} />
<Route path="/deals" element={
<Layout>
<DealsPage />
</Layout>
} />
<Route path="/commissions" element={
<Layout>
<CommissionsPage />
</Layout>
} />
<Route path="/product-management" element={
<Layout>
<ProductManagement />
</Layout>
} />
<Route path="/training" element={
<Layout>
<PlaceholderPage title="Training" description="Manage reseller training programs and certifications" />
</Layout>
} />
<Route path="/support" element={
<Layout>
<PlaceholderPage title="Support" description="Handle support requests and tickets" />
</Layout>
} />
<Route path="/analytics" element={
<Layout>
<PlaceholderPage title="Analytics" description="Advanced analytics and insights" />
</Layout>
} />
<Route path="/reports" element={
<Layout>
<PlaceholderPage title="Reports" description="Generate and view detailed reports" />
</Layout>
} />
<Route path="/targets" element={
<Layout>
<PlaceholderPage title="Targets" description="Set and track performance targets" />
</Layout>
} />
<Route path="/performance" element={
<Layout>
<PlaceholderPage title="Performance" description="Performance metrics and KPIs" />
</Layout>
} />
<Route path="/marketplace" element={
<Layout>
<PlaceholderPage title="Marketplace" description="Browse and manage marketplace offerings" />
</Layout>
} />
<Route path="/certifications" element={
<Layout>
<PlaceholderPage title="Certifications" description="Manage certifications and badges" />
</Layout>
} />
<Route path="/knowledge-base" element={
<Layout>
<PlaceholderPage title="Knowledge Base" description="Access documentation and resources" />
</Layout>
} />
<Route path="/settings" element={
<Layout>
<PlaceholderPage title="Settings" description="Configure your account and preferences" />
</Layout>
} />
{/* Reseller Routes */}
<Route path="/reseller/login" element={<ResellerLogin />} />
<Route path="/reseller/signup" element={<ResellerSignup />} />
<Route path="/reseller" element={
<Layout>
<ResellerDashboardMain />
</Layout>
} />
<Route path="/reseller/customers" element={
<Layout>
<ResellerDashboardCustomers />
</Layout>
} />
<Route path="/reseller/instances" element={
<Layout>
<ResellerDashboardInstances />
</Layout>
} />
<Route path="/reseller/billing" element={
<Layout>
<ResellerBilling />
</Layout>
} />
<Route path="/reseller/support" element={
<Layout>
<ResellerSupport />
</Layout>
} />
<Route path="/reseller/reports" element={
<Layout>
<ResellerReports />
</Layout>
} />
{/* Reseller Dashboard Routes (Separate Service) */}
<Route path="/reseller-dashboard" element={
<ResellerLayout>
<ResellerDashboardMain />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/customers" element={
<ResellerLayout>
<ResellerDashboardCustomers />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/instances" element={
<ResellerLayout>
<ResellerDashboardInstances />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/billing" element={
<ResellerLayout>
<PlaceholderPage title="Billing & Payments" description="Manage your billing, invoices, and payment methods" />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/support" element={
<ResellerLayout>
<PlaceholderPage title="Support Center" description="Get help and submit support tickets" />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/reports" element={
<ResellerLayout>
<PlaceholderPage title="Reports & Analytics" description="View detailed reports and analytics" />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/wallet" element={
<ResellerLayout>
<PlaceholderPage title="Wallet Management" description="Manage your funds, transactions, and payment methods" />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/training" element={
<ResellerLayout>
<PlaceholderPage title="Training Center" description="Access training materials, courses, and certifications" />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/marketplace" element={
<ResellerLayout>
<PlaceholderPage title="Marketplace" description="Browse and purchase cloud services and solutions" />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/certifications" element={
<ResellerLayout>
<PlaceholderPage title="Certifications" description="Manage your professional certifications and achievements" />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/knowledge-base" element={
<ResellerLayout>
<PlaceholderPage title="Knowledge Base" description="Access documentation, guides, and helpful resources" />
</ResellerLayout>
} />
<Route path="/reseller-dashboard/settings" element={
<ResellerLayout>
<PlaceholderPage title="Settings" description="Configure your account preferences and system settings" />
</ResellerLayout>
} />
<Route path="/reseller/wallet" element={
<Layout>
<PlaceholderPage title="Wallet Management" description="Manage your funds, transactions, and payment methods" />
</Layout>
} />
<Route path="/reseller/training" element={
<Layout>
<PlaceholderPage title="Training Center" description="Access training materials, courses, and certifications" />
</Layout>
} />
<Route path="/reseller/marketplace" element={
<Layout>
<PlaceholderPage title="Marketplace" description="Browse and purchase cloud services and solutions" />
</Layout>
} />
<Route path="/reseller/certifications" element={
<Layout>
<PlaceholderPage title="Certifications" description="Manage your professional certifications and achievements" />
</Layout>
} />
<Route path="/reseller/knowledge-base" element={
<Layout>
<PlaceholderPage title="Knowledge Base" description="Access documentation, guides, and helpful resources" />
</Layout>
} />
<Route path="/reseller/settings" element={
<Layout>
<PlaceholderPage title="Settings" description="Configure your account preferences and system settings" />
</Layout>
} />
{/* Default Route */}
<Route path="*" element={
<Layout>
<Dashboard />
</Layout>
} />
</Routes>
<CookieConsent />
</div>
</Router>
</CookiesProvider>
</Provider>
);
}
export default App;

View File

@ -0,0 +1,160 @@
import React, { useState, useEffect } from 'react';
import { useCookies } from 'react-cookie';
import { X, Shield, Settings, Info } from 'lucide-react';
import { cn } from '../utils/cn';
const CookieConsent: React.FC = () => {
const [cookies, setCookie] = useCookies(['cookieConsent']);
const [isVisible, setIsVisible] = useState(false);
const [showDetails, setShowDetails] = useState(false);
useEffect(() => {
// Show consent popup if not already accepted
if (!cookies.cookieConsent) {
setTimeout(() => setIsVisible(true), 1000);
}
}, [cookies.cookieConsent]);
const handleAccept = () => {
setCookie('cookieConsent', 'accepted', {
path: '/',
maxAge: 365 * 24 * 60 * 60, // 1 year
sameSite: 'strict'
});
setIsVisible(false);
};
const handleDecline = () => {
setCookie('cookieConsent', 'declined', {
path: '/',
maxAge: 365 * 24 * 60 * 60, // 1 year
sameSite: 'strict'
});
setIsVisible(false);
};
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">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-300"
onClick={handleDecline}
/>
{/* Cookie Consent Card */}
<div className={cn(
"relative w-full max-w-2xl bg-white dark:bg-gray-800 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700",
"transform transition-all duration-500 ease-out",
isVisible ? "translate-y-0 opacity-100 scale-100" : "translate-y-4 opacity-0 scale-95"
)}>
{/* 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-primary-100 dark:bg-primary-900 rounded-xl flex items-center justify-center">
<Shield className="w-6 h-6 text-primary-600 dark:text-primary-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Cookie & Privacy Settings
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
We value your privacy
</p>
</div>
</div>
<button
onClick={handleDecline}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<X className="w-5 h-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* Content */}
<div className="p-6">
<div className="space-y-4">
<p className="text-gray-700 dark:text-gray-300 leading-relaxed">
We use cookies and similar technologies to provide, protect, and improve our services.
This includes essential cookies for site functionality, analytics cookies to understand
how you use our platform, and marketing cookies to provide personalized content.
</p>
{/* Cookie Categories */}
<div className="space-y-3">
<div 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-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
<Shield className="w-4 h-4 text-green-600 dark:text-green-400" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">Essential Cookies</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Required for site functionality</p>
</div>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">Always Active</div>
</div>
<div 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-lg flex items-center justify-center">
<Settings className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">Analytics Cookies</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Help us improve our services</p>
</div>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">Optional</div>
</div>
<div 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-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center">
<Info className="w-4 h-4 text-purple-600 dark:text-purple-400" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">Marketing Cookies</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Personalized content and ads</p>
</div>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">Optional</div>
</div>
</div>
{/* Privacy Policy Link */}
<div className="text-sm text-gray-600 dark:text-gray-400">
By continuing to use our site, you agree to our{' '}
<a href="/privacy" className="text-primary-600 dark:text-primary-400 hover:underline">
Privacy Policy
</a>{' '}
and{' '}
<a href="/terms" className="text-primary-600 dark:text-primary-400 hover:underline">
Terms of Service
</a>.
</div>
</div>
</div>
{/* Actions */}
<div className="flex flex-col sm:flex-row gap-3 p-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleAccept}
className="flex-1 px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-xl transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
>
Accept All Cookies
</button>
<button
onClick={handleDecline}
className="flex-1 px-6 py-3 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-xl transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
>
Decline Non-Essential
</button>
</div>
</div>
</div>
);
};
export default CookieConsent;

View File

@ -0,0 +1,359 @@
import React from 'react';
import { User, Mail, Phone, Building, MapPin, DollarSign, Award, Calendar, TrendingUp, Users } from 'lucide-react';
import { formatCurrency, formatDate } from '../utils/format';
interface DetailViewProps {
type: 'reseller' | 'partnership' | 'deal';
data: any;
}
const DetailView: React.FC<DetailViewProps> = ({ type, data }) => {
// Safety check for data
if (!data) {
return (
<div className="text-center py-8">
<p className="text-gray-500">No data available</p>
</div>
);
}
const renderResellerDetails = () => (
<div className="space-y-8">
{/* Enhanced Header with Gradient Background */}
<div className="relative overflow-hidden rounded-xl bg-gradient-to-r from-primary-50 to-primary-100 dark:from-primary-900/20 dark:to-primary-800/20 p-6 border border-primary-200/50 dark:border-primary-700/50">
<div className="flex items-center space-x-6">
<div className="relative">
<div className="w-20 h-20 bg-gradient-to-br from-primary-500 to-primary-600 rounded-full flex items-center justify-center shadow-lg">
<User className="w-10 h-10 text-white" />
</div>
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-green-500 rounded-full border-2 border-white dark:border-gray-800"></div>
</div>
<div className="flex-1">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-1">{data.name}</h3>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-2">{data.email}</p>
<div className="flex items-center space-x-4">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
data.tier === 'platinum' ? 'bg-gradient-to-r from-yellow-400 to-yellow-500 text-white shadow-md' :
data.tier === 'gold' ? 'bg-gradient-to-r from-yellow-400 to-yellow-500 text-white shadow-md' :
'bg-gradient-to-r from-gray-400 to-gray-500 text-white shadow-md'
}`}>
{data.tier.charAt(0).toUpperCase() + data.tier.slice(1)} Tier
</span>
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
data.status === 'active' ? 'bg-gradient-to-r from-green-400 to-green-500 text-white shadow-md' :
data.status === 'pending' ? 'bg-gradient-to-r from-yellow-400 to-yellow-500 text-white shadow-md' :
'bg-gradient-to-r from-red-400 to-red-500 text-white shadow-md'
}`}>
{data.status.charAt(0).toUpperCase() + data.status.slice(1)}
</span>
</div>
</div>
</div>
</div>
{/* Contact Information Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 border border-gray-200/60 dark:border-gray-700/60 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
<Mail className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Email</p>
<p className="text-base font-semibold text-gray-900 dark:text-white">{data.email}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 border border-gray-200/60 dark:border-gray-700/60 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center">
<Phone className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Phone</p>
<p className="text-base font-semibold text-gray-900 dark:text-white">{data.phone}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 border border-gray-200/60 dark:border-gray-700/60 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center">
<MapPin className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Region</p>
<p className="text-base font-semibold text-gray-900 dark:text-white">{data.region}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 border border-gray-200/60 dark:border-gray-700/60 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900/50 rounded-lg flex items-center justify-center">
<Award className="w-6 h-6 text-orange-600 dark:text-orange-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Commission Rate</p>
<p className="text-base font-semibold text-gray-900 dark:text-white">{data.commissionRate}%</p>
</div>
</div>
</div>
</div>
{/* Enhanced Performance Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-xl p-6 border border-green-200/50 dark:border-green-700/50 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-green-600 dark:text-green-400 uppercase tracking-wide">Total Revenue</p>
<p className="text-2xl font-bold text-green-700 dark:text-green-300 mt-1">{formatCurrency(data.totalRevenue)}</p>
</div>
<div className="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center">
<DollarSign className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
</div>
</div>
<div className="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-xl p-6 border border-blue-200/50 dark:border-blue-700/50 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wide">Customers</p>
<p className="text-2xl font-bold text-blue-700 dark:text-blue-300 mt-1">{data.customers}</p>
</div>
<div className="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
<div className="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-6 border border-purple-200/50 dark:border-purple-700/50 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-purple-600 dark:text-purple-400 uppercase tracking-wide">Commission Rate</p>
<p className="text-2xl font-bold text-purple-700 dark:text-purple-300 mt-1">{data.commissionRate}%</p>
</div>
<div className="w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
</div>
</div>
</div>
{/* Enhanced Status Footer */}
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-6 border border-gray-200/60 dark:border-gray-700/60">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Last Active</span>
</div>
<span className="text-sm text-gray-600 dark:text-gray-400">{data.lastActive ? formatDate(data.lastActive) : 'N/A'}</span>
</div>
<div className="text-right">
<p className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Partner Since</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">{data.createdAt ? formatDate(data.createdAt) : 'N/A'}</p>
</div>
</div>
</div>
</div>
);
const renderPartnershipDetails = () => (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center space-x-4">
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<Building className="w-8 h-8 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">{data.reseller}</h3>
<p className="text-gray-600 dark:text-gray-400">{data.partnershipType} Partnership</p>
</div>
</div>
{/* Partnership Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center space-x-3">
<Award className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Tier</p>
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
data.tier === 'platinum' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300' :
data.tier === 'gold' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300' :
'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300'
}`}>
{data.tier.charAt(0).toUpperCase() + data.tier.slice(1)}
</span>
</div>
</div>
<div className="flex items-center space-x-3">
<MapPin className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Region</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{data.region}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<DollarSign className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Commission Rate</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{data.commissionRate}%</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Users className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Customers</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{data.customers}</p>
</div>
</div>
</div>
{/* Performance Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<div className="flex items-center space-x-2">
<DollarSign className="w-5 h-5 text-green-600" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Total Revenue</p>
<p className="text-lg font-semibold text-green-600">{formatCurrency(data.totalRevenue)}</p>
</div>
</div>
</div>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<div className="flex items-center space-x-2">
<Calendar className="w-5 h-5 text-blue-600" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Start Date</p>
<p className="text-sm text-blue-600">{data.startDate ? formatDate(data.startDate) : 'N/A'}</p>
</div>
</div>
</div>
</div>
{/* Status */}
<div className="flex items-center space-x-3">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
data.status === 'active' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300' :
data.status === 'pending' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300' :
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'
}`}>
{data.status.charAt(0).toUpperCase() + data.status.slice(1)}
</span>
</div>
</div>
);
const renderDealDetails = () => (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center space-x-4">
<div className="w-16 h-16 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center">
<DollarSign className="w-8 h-8 text-green-600 dark:text-green-400" />
</div>
<div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">{data.title}</h3>
<p className="text-gray-600 dark:text-gray-400">{data.customer}</p>
</div>
</div>
{/* Deal Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center space-x-3">
<Building className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Reseller</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{data.reseller}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<DollarSign className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Deal Value</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{formatCurrency(data.value)}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Award className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Commission Rate</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{data.commissionRate}%</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Calendar className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Expected Close</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{data.expectedCloseDate ? formatDate(data.expectedCloseDate) : 'N/A'}</p>
</div>
</div>
</div>
{/* Deal Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">Deal Type</p>
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300">
{data.dealType.charAt(0).toUpperCase() + data.dealType.slice(1)}
</span>
</div>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">Stage</p>
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300">
{data.stage.charAt(0).toUpperCase() + data.stage.slice(1)}
</span>
</div>
</div>
{/* Products */}
{data.products && (
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">Products/Services</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{data.products}</p>
</div>
)}
{/* Description */}
{data.description && (
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">Description</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{data.description}</p>
</div>
)}
{/* Status */}
<div className="flex items-center space-x-3">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
data.status === 'active' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300' :
data.status === 'pending' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300' :
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'
}`}>
{data.status.charAt(0).toUpperCase() + data.status.slice(1)}
</span>
<span className="text-sm text-gray-500">Created {data.createdAt ? formatDate(data.createdAt) : 'N/A'}</span>
</div>
</div>
);
const renderContent = () => {
switch (type) {
case 'reseller':
return renderResellerDetails();
case 'partnership':
return renderPartnershipDetails();
case 'deal':
return renderDealDetails();
default:
return null;
}
};
return (
<div className="max-w-2xl mx-auto">
{renderContent()}
</div>
);
};
export default DetailView;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { formatCurrencyDualDisplay } from '../utils/format';
interface DualCurrencyDisplayProps {
amount: number;
currency?: 'USD' | 'INR';
className?: string;
showSecondary?: boolean;
}
const DualCurrencyDisplay: React.FC<DualCurrencyDisplayProps> = ({
amount,
currency = 'INR',
className = '',
showSecondary = true
}) => {
const formatted = formatCurrencyDualDisplay(amount, currency);
return (
<div className={`flex flex-col ${className}`}>
<span className="text-lg font-semibold text-gray-900 dark:text-white">
{formatted.primary}
</span>
{showSecondary && (
<span
className="text-sm text-gray-500 dark:text-gray-400"
dangerouslySetInnerHTML={{ __html: formatted.secondary }}
/>
)}
</div>
);
};
export default DualCurrencyDisplay;

View File

@ -0,0 +1,21 @@
import React from 'react';
import Sidebar from './Sidebar';
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
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>
);
};
export default Layout;

View File

@ -0,0 +1,187 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useAppSelector, useAppDispatch } from '../../store/hooks';
import {
Home,
Users,
Briefcase,
FileText,
Headphones,
BarChart3,
Settings,
Wallet,
BookOpen,
ShoppingBag,
Award,
HelpCircle,
Menu,
X,
Sun,
Moon,
LogOut,
Building,
Handshake,
Target,
TrendingUp,
Package
} from 'lucide-react';
import { RootState } from '../../store';
import { toggleTheme } from '../../store/slices/themeSlice';
import { logout } from '../../store/slices/authSlice';
import { cn } from '../../utils/cn';
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: 'Commissions', href: '/commissions', icon: Wallet },
{ name: 'Training', href: '/training', icon: BookOpen },
{ name: 'Support', href: '/support', icon: Headphones },
{ name: 'Analytics', href: '/analytics', icon: BarChart3 },
{ name: 'Reports', href: '/reports', icon: FileText },
{ name: 'Targets', href: '/targets', icon: Target },
{ name: 'Performance', href: '/performance', icon: TrendingUp },
{ name: 'Marketplace', href: '/marketplace', icon: ShoppingBag },
{ name: 'Certifications', href: '/certifications', icon: Award },
{ name: 'Knowledge Base', href: '/knowledge-base', icon: HelpCircle },
{ name: 'Settings', href: '/settings', icon: Settings },
];
const Sidebar: React.FC = () => {
const [isCollapsed, setIsCollapsed] = useState(false);
const location = useLocation();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme);
const { user } = useAppSelector((state: RootState) => state.auth);
const handleLogout = () => {
dispatch(logout());
};
return (
<div className={cn(
"flex flex-col h-full bg-white dark:bg-gray-800 border-r border-secondary-200 dark:border-secondary-700 transition-all duration-300",
isCollapsed ? "w-16" : "w-64"
)}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-secondary-200 dark:border-secondary-700">
{!isCollapsed && (
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gradient-to-r from-primary-600 to-primary-400 rounded-lg flex items-center justify-center">
<Building className="w-5 h-5 text-white" />
</div>
<span className="text-lg font-semibold text-secondary-900 dark:text-white">
Channel Partners
</span>
</div>
)}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="p-1 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-colors"
>
{isCollapsed ? (
<Menu className="w-5 h-5 text-secondary-600 dark:text-secondary-400" />
) : (
<X className="w-5 h-5 text-secondary-600 dark:text-secondary-400" />
)}
</button>
</div>
{/* Navigation */}
<nav className="flex-1 px-2 py-4 space-y-1 overflow-y-auto">
{navigation.map((item) => {
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={cn(
"flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors group",
isActive
? "bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300"
: "text-secondary-700 hover:bg-secondary-100 hover:text-secondary-900 dark:text-secondary-300 dark:hover:bg-secondary-800 dark:hover:text-white"
)}
>
<item.icon className={cn(
"flex-shrink-0 w-5 h-5 transition-colors",
isActive
? "text-primary-600 dark:text-primary-400"
: "text-secondary-500 group-hover:text-secondary-700 dark:text-secondary-400 dark:group-hover:text-white"
)} />
{!isCollapsed && (
<span className="ml-3">{item.name}</span>
)}
</Link>
);
})}
</nav>
{/* User Profile & Actions */}
<div className="border-t border-secondary-200 dark:border-secondary-700 p-4">
{/* Theme Toggle */}
<div className="flex items-center justify-between mb-4">
{!isCollapsed && (
<span className="text-sm text-secondary-600 dark:text-secondary-400">
Theme
</span>
)}
<button
onClick={() => dispatch(toggleTheme())}
className="p-2 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-colors"
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{theme === 'dark' ? (
<Sun className="w-5 h-5 text-secondary-600 dark:text-secondary-400" />
) : (
<Moon className="w-5 h-5 text-secondary-600 dark:text-secondary-400" />
)}
</button>
</div>
{/* User Profile */}
{user && (
<div className="flex items-center space-x-3">
<div className="flex-shrink-0">
<img
className="w-8 h-8 rounded-full object-cover bg-secondary-200 dark:bg-secondary-700"
src={user.avatar}
alt={user.name}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
target.nextElementSibling?.classList.remove('hidden');
}}
/>
<div className="w-8 h-8 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center hidden">
<span className="text-xs font-medium text-primary-600 dark:text-primary-400">
{user.name.split(' ').map(n => n[0]).join('')}
</span>
</div>
</div>
{!isCollapsed && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-secondary-900 dark:text-white truncate">
{user.name}
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-400 truncate">
{user.company}
</p>
</div>
)}
<button
onClick={handleLogout}
className="p-1 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-colors"
title="Logout"
>
<LogOut className="w-4 h-4 text-secondary-600 dark:text-secondary-400" />
</button>
</div>
)}
</div>
</div>
);
};
export default Sidebar;

76
src/components/Modal.tsx Normal file
View File

@ -0,0 +1,76 @@
import React, { useEffect } from 'react';
import { X } from 'lucide-react';
import { cn } from '../utils/cn';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl';
}
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, size = 'md' }) => {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
};
return (
<div className="fixed inset-0 z-50 flex items-start justify-center p-4 pt-8 overflow-y-auto">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className={cn(
"relative w-full bg-white/95 dark:bg-gray-800/95 backdrop-blur-md rounded-2xl shadow-2xl border border-gray-200/20 dark:border-gray-700/20 my-8",
sizeClasses[size]
)}>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200/60 dark:border-gray-700/60 bg-gray-50/30 dark:bg-gray-800/30 rounded-t-2xl">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
{title}
</h3>
<button
onClick={onClose}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-105"
>
<X className="w-5 h-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto max-h-[calc(100vh-200px)] scrollbar-thin">
{children}
</div>
</div>
</div>
);
};
export default Modal;

View File

@ -0,0 +1,239 @@
import React, { useState, useRef, useEffect } from 'react';
import {
Eye,
Download,
Bell,
Award,
UserX,
Settings,
BarChart3,
FileText,
Trash2,
Edit,
Mail
} from 'lucide-react';
interface MoreOptionsDropdownProps {
item: any;
itemType: 'reseller' | 'partnership' | 'deal';
onViewPerformance?: (item: any) => void;
onDownloadReport?: (item: any) => void;
onSendNotification?: (item: any) => void;
onChangeTier?: (item: any) => void;
onDeactivate?: (item: any) => void;
onEdit?: (item: any) => void;
onMail?: (item: any) => void;
onDelete?: (item: any) => void;
onViewDetails?: (item: any) => void;
onClose: () => void;
}
const MoreOptionsDropdown: React.FC<MoreOptionsDropdownProps> = ({
item,
itemType,
onViewPerformance,
onDownloadReport,
onSendNotification,
onChangeTier,
onDeactivate,
onEdit,
onMail,
onDelete,
onViewDetails,
onClose
}) => {
const [isOpen, setIsOpen] = useState(true);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
onClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [onClose]);
const handleOptionClick = (action: string) => {
setIsOpen(false);
onClose();
switch (action) {
case 'view-performance':
onViewPerformance?.(item);
break;
case 'download-report':
onDownloadReport?.(item);
break;
case 'send-notification':
onSendNotification?.(item);
break;
case 'change-tier':
onChangeTier?.(item);
break;
case 'deactivate':
onDeactivate?.(item);
break;
case 'edit':
onEdit?.(item);
break;
case 'mail':
onMail?.(item);
break;
case 'delete':
onDelete?.(item);
break;
case 'view-details':
onViewDetails?.(item);
break;
default:
break;
}
};
const getOptions = () => {
const baseOptions = [
{
id: 'view-details',
label: 'View Details',
icon: Eye,
className: 'text-blue-600 dark:text-blue-400'
},
{
id: 'view-performance',
label: 'View Performance',
icon: BarChart3,
className: 'text-green-600 dark:text-green-400'
},
{
id: 'download-report',
label: 'Download Report',
icon: Download,
className: 'text-purple-600 dark:text-purple-400'
},
{
id: 'send-notification',
label: 'Send Notification',
icon: Bell,
className: 'text-orange-600 dark:text-orange-400'
}
];
if (itemType === 'reseller') {
return [
...baseOptions,
{
id: 'change-tier',
label: 'Change Tier',
icon: Award,
className: 'text-yellow-600 dark:text-yellow-400'
},
{
id: 'deactivate',
label: 'Deactivate Account',
icon: UserX,
className: 'text-red-600 dark:text-red-400'
}
];
} else if (itemType === 'partnership') {
return [
...baseOptions,
{
id: 'change-tier',
label: 'Change Terms',
icon: Settings,
className: 'text-yellow-600 dark:text-yellow-400'
},
{
id: 'deactivate',
label: 'Terminate Partnership',
icon: UserX,
className: 'text-red-600 dark:text-red-400'
}
];
} else if (itemType === 'deal') {
return [
...baseOptions,
{
id: 'change-tier',
label: 'Change Stage',
icon: Settings,
className: 'text-yellow-600 dark:text-yellow-400'
},
{
id: 'deactivate',
label: 'Close Deal',
icon: FileText,
className: 'text-green-600 dark:text-green-400'
}
];
}
return baseOptions;
};
if (!isOpen) return null;
return (
<div
ref={dropdownRef}
className="absolute right-0 top-8 z-50 w-56 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2"
>
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
<p className="text-sm font-medium text-gray-900 dark:text-white">
{itemType === 'reseller' ? item.name : itemType === 'partnership' ? item.reseller : item.title}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{itemType === 'reseller' ? 'Reseller' : itemType === 'partnership' ? 'Partnership' : 'Deal'} Options
</p>
</div>
<div className="py-1">
{getOptions().map((option) => {
const IconComponent = option.icon;
return (
<button
key={option.id}
onClick={() => handleOptionClick(option.id)}
className="w-full flex items-center px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<IconComponent className={`w-4 h-4 mr-3 ${option.className}`} />
{option.label}
</button>
);
})}
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-1">
<button
onClick={() => handleOptionClick('edit')}
className="w-full flex items-center px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<Edit className="w-4 h-4 mr-3 text-blue-600 dark:text-blue-400" />
Edit
</button>
<button
onClick={() => handleOptionClick('mail')}
className="w-full flex items-center px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<Mail className="w-4 h-4 mr-3 text-green-600 dark:text-green-400" />
Send Email
</button>
<button
onClick={() => handleOptionClick('delete')}
className="w-full flex items-center px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
<Trash2 className="w-4 h-4 mr-3" />
Delete
</button>
</div>
</div>
);
};
export default MoreOptionsDropdown;

View File

@ -0,0 +1,161 @@
import React from 'react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LineChart,
Line,
BarChart,
Bar
} from 'recharts';
import { formatCurrency } from '../../utils/format';
// Mock data for commission trends
const commissionTrendsData = [
{ month: 'Jan', earned: 125000, paid: 98000, pending: 27000 },
{ month: 'Feb', earned: 145000, paid: 120000, pending: 25000 },
{ month: 'Mar', earned: 168000, paid: 135000, pending: 33000 },
{ month: 'Apr', earned: 189000, paid: 155000, pending: 34000 },
{ month: 'May', earned: 210000, paid: 175000, pending: 35000 },
{ month: 'Jun', earned: 195000, paid: 160000, pending: 35000 },
{ month: 'Jul', earned: 225000, paid: 185000, pending: 40000 },
{ month: 'Aug', earned: 245000, paid: 200000, pending: 45000 },
{ month: 'Sep', earned: 268000, paid: 220000, pending: 48000 },
{ month: 'Oct', earned: 289000, paid: 240000, pending: 49000 },
{ month: 'Nov', earned: 312000, paid: 260000, pending: 52000 },
{ month: 'Dec', earned: 335000, paid: 280000, pending: 55000 },
];
const CommissionTrendsChart: React.FC = () => {
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">
{label}
</p>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600 dark:text-gray-400">Earned:</span>
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
{formatCurrency(payload[0]?.value || 0)}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600 dark:text-gray-400">Paid:</span>
<span className="text-sm font-semibold text-blue-600 dark:text-blue-400">
{formatCurrency(payload[1]?.value || 0)}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600 dark:text-gray-400">Pending:</span>
<span className="text-sm font-semibold text-orange-600 dark:text-orange-400">
{formatCurrency(payload[2]?.value || 0)}
</span>
</div>
</div>
</div>
);
}
return null;
};
return (
<div className="w-full h-full">
<ResponsiveContainer width="100%" height={300}>
<AreaChart
data={commissionTrendsData}
margin={{
top: 20,
right: 30,
left: 20,
bottom: 5,
}}
>
<defs>
<linearGradient id="colorEarned" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#10b981" stopOpacity={0.1}/>
</linearGradient>
<linearGradient id="colorPaid" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0.1}/>
</linearGradient>
<linearGradient id="colorPending" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0.1}/>
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke="#374151"
strokeOpacity={0.1}
vertical={false}
/>
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#6b7280' }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#6b7280' }}
tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="earned"
stackId="1"
stroke="#10b981"
strokeWidth={2}
fill="url(#colorEarned)"
name="Earned"
/>
<Area
type="monotone"
dataKey="paid"
stackId="1"
stroke="#3b82f6"
strokeWidth={2}
fill="url(#colorPaid)"
name="Paid"
/>
<Area
type="monotone"
dataKey="pending"
stackId="1"
stroke="#f59e0b"
strokeWidth={2}
fill="url(#colorPending)"
name="Pending"
/>
</AreaChart>
</ResponsiveContainer>
{/* Legend */}
<div className="flex items-center justify-center space-x-6 mt-4">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span className="text-sm text-gray-600 dark:text-gray-400">Earned</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span className="text-sm text-gray-600 dark:text-gray-400">Paid</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-orange-500 rounded-full"></div>
<span className="text-sm text-gray-600 dark:text-gray-400">Pending</span>
</div>
</div>
</div>
);
};
export default CommissionTrendsChart;

View File

@ -0,0 +1,68 @@
import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
const data = [
{ month: 'Jan', customers: 12, revenue: 2400, instances: 8 },
{ month: 'Feb', customers: 19, revenue: 3200, instances: 12 },
{ month: 'Mar', customers: 15, revenue: 2800, instances: 10 },
{ month: 'Apr', customers: 22, revenue: 4100, instances: 15 },
{ month: 'May', customers: 28, revenue: 5200, instances: 18 },
{ month: 'Jun', customers: 35, revenue: 6800, instances: 22 },
];
const ResellerPerformanceChart: React.FC = () => {
return (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" opacity={0.1} />
<XAxis
dataKey="month"
stroke="#6B7280"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#6B7280"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip
contentStyle={{
backgroundColor: 'rgba(255, 255, 255, 0.95)',
border: 'none',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
}}
/>
<Line
type="monotone"
dataKey="customers"
stroke="#10B981"
strokeWidth={2}
dot={{ fill: '#10B981', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#10B981', strokeWidth: 2 }}
/>
<Line
type="monotone"
dataKey="revenue"
stroke="#3B82F6"
strokeWidth={2}
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#3B82F6', strokeWidth: 2 }}
/>
<Line
type="monotone"
dataKey="instances"
stroke="#F59E0B"
strokeWidth={2}
dot={{ fill: '#F59E0B', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#F59E0B', strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
);
};
export default ResellerPerformanceChart;

View File

@ -0,0 +1,120 @@
import React from 'react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Area,
AreaChart
} from 'recharts';
import { useAppSelector } from '../../store/hooks';
const data = [
{ month: 'Jan', revenue: 3200000, commission: 480000 },
{ month: 'Feb', revenue: 3800000, commission: 570000 },
{ month: 'Mar', revenue: 4200000, commission: 630000 },
{ month: 'Apr', revenue: 3900000, commission: 585000 },
{ month: 'May', revenue: 4500000, commission: 675000 },
{ month: 'Jun', revenue: 4800000, commission: 720000 },
{ month: 'Jul', revenue: 5200000, commission: 780000 },
{ month: 'Aug', revenue: 4900000, commission: 735000 },
{ month: 'Sep', revenue: 5400000, commission: 810000 },
{ month: 'Oct', revenue: 5800000, commission: 870000 },
{ month: 'Nov', revenue: 6200000, commission: 930000 },
{ month: 'Dec', revenue: 4560000, commission: 684000 },
];
const RevenueChart: React.FC = () => {
const { theme } = useAppSelector((state) => state.theme);
const isDark = theme === 'dark';
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
notation: 'compact',
maximumFractionDigits: 1,
}).format(value);
};
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">
{label}
</p>
<div className="space-y-1">
<p className="text-sm text-blue-600 dark:text-blue-400">
Revenue: {formatCurrency(payload[0].value)}
</p>
<p className="text-sm text-green-600 dark:text-green-400">
Commission: {formatCurrency(payload[1].value)}
</p>
</div>
</div>
);
}
return null;
};
return (
<div className="w-full h-full">
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
<defs>
<linearGradient id="revenueGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3B82F6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3B82F6" stopOpacity={0.05} />
</linearGradient>
<linearGradient id="commissionGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10B981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10B981" stopOpacity={0.05} />
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke={isDark ? '#374151' : '#E5E7EB'}
opacity={0.3}
/>
<XAxis
dataKey="month"
stroke={isDark ? '#9CA3AF' : '#6B7280'}
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke={isDark ? '#9CA3AF' : '#6B7280'}
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => formatCurrency(value)}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="revenue"
stroke="#3B82F6"
strokeWidth={2}
fill="url(#revenueGradient)"
fillOpacity={1}
/>
<Area
type="monotone"
dataKey="commission"
stroke="#10B981"
strokeWidth={2}
fill="url(#commissionGradient)"
fillOpacity={1}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
};
export default RevenueChart;

View File

@ -0,0 +1,344 @@
import React, { useState } from 'react';
import { Building, User, DollarSign, Calendar, FileText, Target, Percent } from 'lucide-react';
import { cn } from '../../utils/cn';
interface AddDealFormProps {
onSubmit: (data: any) => void;
onCancel: () => void;
}
const AddDealForm: React.FC<AddDealFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
dealName: '',
customerName: '',
reseller: '',
dealValue: '',
commissionRate: '',
dealType: 'new',
stage: 'prospecting',
expectedCloseDate: '',
description: '',
products: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.dealName.trim()) {
newErrors.dealName = 'Deal name is required';
}
if (!formData.customerName.trim()) {
newErrors.customerName = 'Customer name is required';
}
if (!formData.reseller.trim()) {
newErrors.reseller = 'Reseller is required';
}
if (!formData.dealValue) {
newErrors.dealValue = 'Deal value is required';
} else {
const value = parseFloat(formData.dealValue);
if (isNaN(value) || value <= 0) {
newErrors.dealValue = 'Deal value must be a positive number';
}
}
if (!formData.commissionRate) {
newErrors.commissionRate = 'Commission rate is required';
} else {
const rate = parseFloat(formData.commissionRate);
if (isNaN(rate) || rate < 0 || rate > 100) {
newErrors.commissionRate = 'Commission rate must be between 0% and 100%';
}
}
if (!formData.expectedCloseDate) {
newErrors.expectedCloseDate = 'Expected close date is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
onSubmit({
...formData,
dealValue: parseFloat(formData.dealValue),
commissionRate: parseFloat(formData.commissionRate),
id: Date.now().toString(),
status: 'active',
createdAt: new Date().toISOString(),
probability: 50,
});
} catch (error) {
console.error('Error adding deal:', error);
} finally {
setIsSubmitting(false);
}
};
const handleChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Deal Name */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<FileText className="inline w-4 h-4 mr-2" />
Deal Name *
</label>
<input
type="text"
value={formData.dealName}
onChange={(e) => handleChange('dealName', 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.dealName
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter deal name"
/>
{errors.dealName && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.dealName}</p>
)}
</div>
{/* Customer 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" />
Customer Name *
</label>
<input
type="text"
value={formData.customerName}
onChange={(e) => handleChange('customerName', 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.customerName
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter customer name"
/>
{errors.customerName && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.customerName}</p>
)}
</div>
{/* Reseller */}
<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" />
Reseller *
</label>
<select
value={formData.reseller}
onChange={(e) => handleChange('reseller', 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.reseller
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
>
<option value="">Select reseller</option>
<option value="TechCorp Solutions">TechCorp Solutions</option>
<option value="Digital Partners">Digital Partners</option>
<option value="Cloud Solutions Inc">Cloud Solutions Inc</option>
<option value="IT Services Pro">IT Services Pro</option>
<option value="TechBridge">TechBridge</option>
</select>
{errors.reseller && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.reseller}</p>
)}
</div>
{/* Deal Value */}
<div>
<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" />
Deal Value () *
</label>
<input
type="number"
min="0"
step="0.01"
value={formData.dealValue}
onChange={(e) => handleChange('dealValue', 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.dealValue
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter deal value"
/>
{errors.dealValue && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.dealValue}</p>
)}
</div>
{/* Commission Rate */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Percent className="inline w-4 h-4 mr-2" />
Commission Rate (%) *
</label>
<input
type="number"
min="0"
max="100"
step="0.1"
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>
{/* Deal Type */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Target className="inline w-4 h-4 mr-2" />
Deal Type
</label>
<select
value={formData.dealType}
onChange={(e) => handleChange('dealType', 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="new">New Business</option>
<option value="upsell">Upsell</option>
<option value="renewal">Renewal</option>
<option value="expansion">Expansion</option>
</select>
</div>
{/* Stage */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Target className="inline w-4 h-4 mr-2" />
Stage
</label>
<select
value={formData.stage}
onChange={(e) => handleChange('stage', 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="prospecting">Prospecting</option>
<option value="qualification">Qualification</option>
<option value="proposal">Proposal</option>
<option value="negotiation">Negotiation</option>
<option value="closed-won">Closed Won</option>
<option value="closed-lost">Closed Lost</option>
</select>
</div>
{/* Expected Close Date */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Calendar className="inline w-4 h-4 mr-2" />
Expected Close Date *
</label>
<input
type="date"
value={formData.expectedCloseDate}
onChange={(e) => handleChange('expectedCloseDate', 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.expectedCloseDate
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
/>
{errors.expectedCloseDate && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.expectedCloseDate}</p>
)}
</div>
</div>
{/* Products */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<FileText className="inline w-4 h-4 mr-2" />
Products/Services
</label>
<textarea
value={formData.products}
onChange={(e) => handleChange('products', e.target.value)}
rows={2}
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"
placeholder="Enter products or services included in this deal"
/>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<FileText className="inline w-4 h-4 mr-2" />
Deal Description
</label>
<textarea
value={formData.description}
onChange={(e) => handleChange('description', e.target.value)}
rows={3}
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"
placeholder="Enter deal description"
/>
</div>
{/* Actions */}
<div className="flex justify-end space-x-3 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Adding...' : 'Add Deal'}
</button>
</div>
</form>
);
};
export default AddDealForm;

View File

@ -0,0 +1,346 @@
import React, { useState } from 'react';
import { Building, User, Mail, Phone, MapPin, DollarSign, Calendar, FileText } from 'lucide-react';
import { cn } from '../../utils/cn';
interface AddPartnershipFormProps {
onSubmit: (data: any) => void;
onCancel: () => void;
}
const AddPartnershipForm: React.FC<AddPartnershipFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
companyName: '',
contactPerson: '',
email: '',
phone: '',
region: '',
commissionRate: '',
tier: 'silver',
partnershipType: 'reseller',
startDate: '',
endDate: '',
description: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.companyName.trim()) {
newErrors.companyName = 'Company name is required';
}
if (!formData.contactPerson.trim()) {
newErrors.contactPerson = 'Contact person is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Please enter a valid email address';
}
if (!formData.phone.trim()) {
newErrors.phone = 'Phone number is required';
}
if (!formData.region.trim()) {
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 > 25) {
newErrors.commissionRate = 'Commission rate must be between 5% and 25%';
}
}
if (!formData.startDate) {
newErrors.startDate = 'Start date is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
onSubmit({
...formData,
commissionRate: parseFloat(formData.commissionRate),
id: Date.now().toString(),
status: 'pending',
createdAt: new Date().toISOString(),
customers: 0,
totalRevenue: 0,
});
} catch (error) {
console.error('Error adding partnership:', error);
} finally {
setIsSubmitting(false);
}
};
const handleChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
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 */}
<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 *
</label>
<input
type="text"
value={formData.contactPerson}
onChange={(e) => handleChange('contactPerson', 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
? "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"
/>
{errors.contactPerson && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.contactPerson}</p>
)}
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Mail className="inline w-4 h-4 mr-2" />
Email Address *
</label>
<input
type="email"
value={formData.email}
onChange={(e) => handleChange('email', 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.email
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter email address"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.email}</p>
)}
</div>
{/* Phone */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Phone className="inline w-4 h-4 mr-2" />
Phone Number *
</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => handleChange('phone', 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.phone
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter phone number"
/>
{errors.phone && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.phone}</p>
)}
</div>
{/* Region */}
<div>
<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"
)}
>
<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>
{errors.region && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.region}</p>
)}
</div>
{/* Commission Rate */}
<div>
<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 (%) *
</label>
<input
type="number"
min="5"
max="25"
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>
{/* Partnership Type */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<FileText className="inline w-4 h-4 mr-2" />
Partnership Type
</label>
<select
value={formData.partnershipType}
onChange={(e) => handleChange('partnershipType', 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="reseller">Reseller</option>
<option value="distributor">Distributor</option>
<option value="channel-partner">Channel Partner</option>
<option value="affiliate">Affiliate</option>
</select>
</div>
{/* Start Date */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Calendar className="inline w-4 h-4 mr-2" />
Start Date *
</label>
<input
type="date"
value={formData.startDate}
onChange={(e) => handleChange('startDate', 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.startDate
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
/>
{errors.startDate && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.startDate}</p>
)}
</div>
{/* End Date */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Calendar className="inline w-4 h-4 mr-2" />
End Date
</label>
<input
type="date"
value={formData.endDate}
onChange={(e) => handleChange('endDate', 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"
/>
</div>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<FileText className="inline w-4 h-4 mr-2" />
Partnership Description
</label>
<textarea
value={formData.description}
onChange={(e) => handleChange('description', e.target.value)}
rows={3}
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"
placeholder="Enter partnership description"
/>
</div>
{/* Actions */}
<div className="flex justify-end space-x-3 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Adding...' : 'Add Partnership'}
</button>
</div>
</form>
);
};
export default AddPartnershipForm;

View File

@ -0,0 +1,302 @@
import React, { useState } from 'react';
import { User, Mail, Phone, Building, MapPin, DollarSign, Award } from 'lucide-react';
import { cn } from '../../utils/cn';
interface AddResellerFormProps {
onSubmit: (data: any) => void;
onCancel: () => void;
}
const AddResellerForm: React.FC<AddResellerFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
companyName: '',
contactPerson: '',
email: '',
phone: '',
region: '',
commissionRate: '',
tier: 'silver',
address: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.companyName.trim()) {
newErrors.companyName = 'Company name is required';
}
if (!formData.contactPerson.trim()) {
newErrors.contactPerson = 'Contact person is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Please enter a valid email address';
}
if (!formData.phone.trim()) {
newErrors.phone = 'Phone number is required';
} else if (!/^[\+]?[1-9][\d]{0,15}$/.test(formData.phone.replace(/\s/g, ''))) {
newErrors.phone = 'Please enter a valid phone number';
}
if (!formData.region.trim()) {
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%';
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
onSubmit({
...formData,
commissionRate: parseFloat(formData.commissionRate),
id: Date.now().toString(),
status: 'pending',
createdAt: new Date().toISOString(),
});
} catch (error) {
console.error('Error adding reseller:', error);
} finally {
setIsSubmitting(false);
}
};
const handleChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
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 */}
<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 *
</label>
<input
type="text"
value={formData.contactPerson}
onChange={(e) => handleChange('contactPerson', 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
? "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"
/>
{errors.contactPerson && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.contactPerson}</p>
)}
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Mail className="inline w-4 h-4 mr-2" />
Email Address *
</label>
<input
type="email"
value={formData.email}
onChange={(e) => handleChange('email', 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.email
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter email address"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.email}</p>
)}
</div>
{/* Phone */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Phone className="inline w-4 h-4 mr-2" />
Phone Number *
</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => handleChange('phone', 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.phone
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
)}
placeholder="Enter phone number"
/>
{errors.phone && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.phone}</p>
)}
</div>
{/* Region */}
<div>
<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"
)}
>
<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>
{errors.region && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.region}</p>
)}
</div>
{/* Commission Rate */}
<div>
<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 (%) *
</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>
{/* 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>
</div>
</div>
{/* Address */}
<div>
<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" />
Address
</label>
<textarea
value={formData.address}
onChange={(e) => handleChange('address', e.target.value)}
rows={3}
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"
placeholder="Enter company address"
/>
</div>
{/* Actions */}
<div className="flex justify-end space-x-3 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Adding...' : 'Add Reseller'}
</button>
</div>
</form>
);
};
export default AddResellerForm;

View File

@ -0,0 +1,248 @@
import React, { useState, useEffect } from 'react';
import { X } from 'lucide-react';
interface EditResellerFormProps {
reseller: any;
onSubmit: (data: any) => void;
onCancel: () => void;
}
const EditResellerForm: React.FC<EditResellerFormProps> = ({ reseller, onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
region: '',
commissionRate: '',
tier: '',
status: ''
});
const [errors, setErrors] = useState<{[key: string]: string}>({});
useEffect(() => {
if (reseller) {
setFormData({
name: reseller.name || '',
email: reseller.email || '',
phone: reseller.phone || '',
region: reseller.region || '',
commissionRate: reseller.commissionRate?.toString() || '',
tier: reseller.tier || '',
status: reseller.status || ''
});
}
}, [reseller]);
const validateForm = () => {
const newErrors: {[key: string]: string} = {};
if (!formData.name.trim()) {
newErrors.name = 'Company name is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.phone.trim()) {
newErrors.phone = 'Phone number is required';
}
if (!formData.region.trim()) {
newErrors.region = 'Region is required';
}
if (!formData.commissionRate) {
newErrors.commissionRate = 'Commission rate is required';
} else if (isNaN(Number(formData.commissionRate)) || Number(formData.commissionRate) < 0 || Number(formData.commissionRate) > 100) {
newErrors.commissionRate = 'Commission rate must be between 0 and 100';
}
if (!formData.tier) {
newErrors.tier = 'Tier is required';
}
if (!formData.status) {
newErrors.status = 'Status is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
onSubmit({
...reseller,
...formData,
commissionRate: Number(formData.commissionRate)
});
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
return (
<div className="space-y-6">
<form onSubmit={handleSubmit} 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-1">
Company Name *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.name ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
placeholder="Enter company name"
/>
{errors.name && <p className="text-red-500 text-xs mt-1">{errors.name}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email *
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.email ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
placeholder="Enter email address"
/>
{errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Phone *
</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.phone ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
placeholder="Enter phone number"
/>
{errors.phone && <p className="text-red-500 text-xs mt-1">{errors.phone}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Region *
</label>
<input
type="text"
name="region"
value={formData.region}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.region ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
placeholder="Enter region"
/>
{errors.region && <p className="text-red-500 text-xs mt-1">{errors.region}</p>}
</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"
value={formData.commissionRate}
onChange={handleChange}
min="0"
max="100"
step="0.1"
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.commissionRate ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
placeholder="Enter commission rate"
/>
{errors.commissionRate && <p className="text-red-500 text-xs mt-1">{errors.commissionRate}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tier *
</label>
<select
name="tier"
value={formData.tier}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.tier ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
>
<option value="">Select tier</option>
<option value="platinum">Platinum</option>
<option value="gold">Gold</option>
<option value="silver">Silver</option>
</select>
{errors.tier && <p className="text-red-500 text-xs mt-1">{errors.tier}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status *
</label>
<select
name="status"
value={formData.status}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.status ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
>
<option value="">Select status</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="inactive">Inactive</option>
</select>
{errors.status && <p className="text-red-500 text-xs mt-1">{errors.status}</p>}
</div>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Update Reseller
</button>
</div>
</form>
</div>
);
};
export default EditResellerForm;

View File

@ -0,0 +1,206 @@
import React, { useState } from 'react';
import { X, Send, Paperclip } from 'lucide-react';
interface MailComposeFormProps {
recipient: any;
onSend: (data: any) => void;
onCancel: () => void;
}
const MailComposeForm: React.FC<MailComposeFormProps> = ({ recipient, onSend, onCancel }) => {
const [formData, setFormData] = useState({
to: recipient?.email || '',
subject: '',
message: '',
cc: '',
bcc: ''
});
const [errors, setErrors] = useState<{[key: string]: string}>({});
const validateForm = () => {
const newErrors: {[key: string]: string} = {};
if (!formData.to.trim()) {
newErrors.to = 'Recipient email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.to)) {
newErrors.to = 'Recipient email is invalid';
}
if (!formData.subject.trim()) {
newErrors.subject = 'Subject is required';
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
}
if (formData.cc && !/\S+@\S+\.\S+/.test(formData.cc)) {
newErrors.cc = 'CC email is invalid';
}
if (formData.bcc && !/\S+@\S+\.\S+/.test(formData.bcc)) {
newErrors.bcc = 'BCC email is invalid';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
onSend(formData);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleSendViaEmail = () => {
const mailtoLink = `mailto:${formData.to}?subject=${encodeURIComponent(formData.subject)}&body=${encodeURIComponent(formData.message)}`;
window.open(mailtoLink, '_blank');
};
return (
<div className="space-y-6">
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>To:</strong> {recipient?.name || 'Unknown'} ({recipient?.email || 'No email'})
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
To *
</label>
<input
type="email"
name="to"
value={formData.to}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.to ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
placeholder="Enter recipient email"
/>
{errors.to && <p className="text-red-500 text-xs mt-1">{errors.to}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Subject *
</label>
<input
type="text"
name="subject"
value={formData.subject}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.subject ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
placeholder="Enter email subject"
/>
{errors.subject && <p className="text-red-500 text-xs mt-1">{errors.subject}</p>}
</div>
<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">
CC
</label>
<input
type="email"
name="cc"
value={formData.cc}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.cc ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
placeholder="Enter CC email"
/>
{errors.cc && <p className="text-red-500 text-xs mt-1">{errors.cc}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
BCC
</label>
<input
type="email"
name="bcc"
value={formData.bcc}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.bcc ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white`}
placeholder="Enter BCC email"
/>
{errors.bcc && <p className="text-red-500 text-xs mt-1">{errors.bcc}</p>}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Message *
</label>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
rows={8}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${
errors.message ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} bg-white dark:bg-gray-800 text-gray-900 dark:text-white resize-none`}
placeholder="Enter your message here..."
/>
{errors.message && <p className="text-red-500 text-xs mt-1">{errors.message}</p>}
</div>
<div className="flex items-center space-x-3">
<button
type="button"
className="flex items-center space-x-2 px-3 py-2 text-sm text-gray-600 dark:text-gray-400 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
>
<Paperclip className="w-4 h-4" />
<span>Attach File</span>
</button>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Cancel
</button>
<button
type="button"
onClick={handleSendViaEmail}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center space-x-2"
>
<Send className="w-4 h-4" />
<span>Send via Email Client</span>
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 flex items-center space-x-2"
>
<Send className="w-4 h-4" />
<span>Send</span>
</button>
</div>
</form>
</div>
);
};
export default MailComposeForm;

View File

@ -0,0 +1,30 @@
# Reseller Components
This folder contains all reseller-specific components and frontend logic, segregated from the main channel partner components to avoid confusion.
## Structure
```
reseller/
├── layout/ # Reseller-specific layout components
│ ├── ResellerLayout.tsx
│ └── ResellerSidebar.tsx
├── charts/ # Reseller-specific chart components
├── forms/ # Reseller-specific form components
└── index.ts # Component exports
```
## Usage
Import reseller components from the main components directory:
```typescript
import { ResellerLayout, ResellerSidebar } from '../components/reseller';
```
## Key Features
- **ResellerLayout**: Main layout wrapper for reseller dashboard
- **ResellerSidebar**: Navigation sidebar specific to reseller features
- **Segregated Logic**: All reseller-specific logic is contained here
- **Clean Architecture**: Separate from channel partner components

View File

@ -0,0 +1,3 @@
// Reseller Components Index
export { default as ResellerLayout } from './layout/ResellerLayout';
export { default as ResellerSidebar } from './layout/ResellerSidebar';

View File

@ -0,0 +1,87 @@
import React from 'react';
import ResellerSidebar from './ResellerSidebar';
interface ResellerLayoutProps {
children: React.ReactNode;
}
const ResellerLayout: React.FC<ResellerLayoutProps> = ({ children }) => {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-emerald-50 to-teal-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
<ResellerSidebar />
{/* Main Content */}
<div className="ml-64 transition-all duration-300">
<div className="min-h-screen">
{/* Top Navigation Bar */}
<div className="bg-white/90 dark:bg-slate-900/95 backdrop-blur-xl border-b border-slate-200/50 dark:border-slate-700/50 sticky top-0 z-40 shadow-sm">
<div className="flex items-center justify-between px-8 py-5 h-20">
{/* Left Side - Logo and Title */}
<div className="flex items-center space-x-5">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 via-teal-500 to-cyan-500 rounded-xl flex items-center justify-center flex-shrink-0 shadow-lg">
<div className="w-5 h-5 bg-white rounded-md shadow-sm"></div>
</div>
<div className="flex flex-col justify-center">
<h1 className="text-xl font-bold text-slate-900 dark:text-white leading-tight tracking-tight">
Reseller Portal
</h1>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 leading-tight">
Cloud Services Management
</p>
</div>
</div>
{/* Right Side - Search, Notifications, User */}
<div className="flex items-center space-x-6">
{/* Search Bar */}
<div className="relative flex-shrink-0">
<input
type="text"
placeholder="Search anything..."
className="w-72 pl-12 pr-6 py-3 bg-slate-50 dark:bg-slate-800/80 border border-slate-200/50 dark:border-slate-600/50 rounded-xl text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500/50 transition-all duration-200 backdrop-blur-sm"
/>
<div className="absolute left-4 top-1/2 transform -translate-y-1/2">
<svg className="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
{/* Notifications */}
<button className="relative p-3 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100/80 dark:hover:bg-slate-700/50 rounded-xl transition-all duration-200 flex-shrink-0 group">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-5 5v-5zM4.5 19.5h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" />
</svg>
<span className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-white dark:border-slate-900"></span>
</button>
{/* Divider */}
<div className="w-px h-8 bg-slate-200 dark:bg-slate-700"></div>
{/* User Menu */}
<div className="flex items-center space-x-4 flex-shrink-0 group cursor-pointer">
<div className="text-right flex flex-col justify-center">
<p className="text-sm font-semibold text-slate-900 dark:text-white leading-tight">John Reseller</p>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 leading-tight">Tech Solutions Inc</p>
</div>
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 via-teal-500 to-cyan-500 rounded-xl flex items-center justify-center flex-shrink-0 shadow-lg group-hover:shadow-xl transition-all duration-200">
<span className="text-white text-sm font-bold">JR</span>
</div>
</div>
</div>
</div>
</div>
{/* Page Content */}
<div className="p-6">
<div className="max-w-7xl mx-auto">
{children}
</div>
</div>
</div>
</div>
</div>
);
};
export default ResellerLayout;

View File

@ -0,0 +1,162 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useAppSelector, useAppDispatch } from '../../../store/hooks';
import { toggleTheme } from '../../../store/slices/themeSlice';
import {
Home,
Users,
Cloud,
CreditCard,
Headphones,
BarChart3,
Wallet,
BookOpen,
ShoppingBag,
Award,
HelpCircle,
Settings,
Sun,
Moon,
ChevronLeft,
ChevronRight,
LogOut,
User,
Bell
} from 'lucide-react';
import { cn } from '../../../utils/cn';
const ResellerSidebar: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const location = useLocation();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state) => state.theme);
const { user } = useAppSelector((state) => state.auth);
const navigation = [
{ name: 'Dashboard', href: '/reseller-dashboard', icon: Home },
{ name: 'Customers', href: '/reseller-dashboard/customers', icon: Users },
{ name: 'Cloud Instances', href: '/reseller-dashboard/instances', icon: Cloud },
{ 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: 'Wallet', href: '/reseller-dashboard/wallet', icon: Wallet },
{ name: 'Training', href: '/reseller-dashboard/training', icon: BookOpen },
{ name: 'Marketplace', href: '/reseller-dashboard/marketplace', icon: ShoppingBag },
{ name: 'Certifications', href: '/reseller-dashboard/certifications', icon: Award },
{ name: 'Knowledge Base', href: '/reseller-dashboard/knowledge-base', icon: HelpCircle },
{ name: 'Settings', href: '/reseller-dashboard/settings', icon: Settings },
];
const isActive = (href: string) => {
return location.pathname === href;
};
return (
<div className={cn(
"fixed left-0 top-0 h-full bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 border-r border-slate-700/50 transition-all duration-300 z-50",
collapsed ? "w-16" : "w-64"
)}>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 h-16 border-b border-slate-700/50">
{!collapsed ? (
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center flex-shrink-0">
<Cloud className="w-5 h-5 text-white" />
</div>
<div className="flex flex-col justify-center">
<h1 className="text-lg font-bold text-white leading-tight">Reseller Portal</h1>
<p className="text-xs text-slate-400 leading-tight">Cloud Services</p>
</div>
</div>
) : (
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center flex-shrink-0">
<Cloud className="w-5 h-5 text-white" />
</div>
)}
<button
onClick={() => setCollapsed(!collapsed)}
className="p-1 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors duration-200 flex-shrink-0"
>
{collapsed ? <ChevronRight className="w-5 h-5" /> : <ChevronLeft className="w-5 h-5" />}
</button>
</div>
{/* User Profile */}
{!collapsed && (
<div className="p-4 border-b border-slate-700/50">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-white" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">{user?.name || 'John Reseller'}</p>
<p className="text-xs text-slate-400 truncate">{user?.company || 'Tech Solutions Inc'}</p>
</div>
<button className="p-1 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors duration-200">
<Bell className="w-5 h-5" />
</button>
</div>
</div>
)}
{/* Navigation */}
<nav className="flex-1 p-4 space-y-3 overflow-y-auto">
{navigation.map((item) => (
<Link
key={item.name}
to={item.href}
className={cn(
"flex items-center rounded-xl text-sm font-medium transition-all duration-200 group",
collapsed ? "justify-center px-3 py-4" : "px-4 py-3",
isActive(item.href)
? "bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 border border-emerald-500/30 shadow-lg"
: "text-slate-300 hover:bg-slate-800/50 hover:text-white"
)}
>
<item.icon className={cn(
"w-6 h-6 transition-colors duration-200",
isActive(item.href) ? "text-emerald-400" : "text-slate-400 group-hover:text-white"
)} />
{!collapsed && (
<span className="ml-3">{item.name}</span>
)}
</Link>
))}
</nav>
{/* Footer */}
<div className="p-4 border-t border-slate-700/50 space-y-2">
{/* Theme Toggle */}
<button
onClick={() => dispatch(toggleTheme())}
className={cn(
"w-full flex items-center rounded-xl text-sm font-medium text-slate-300 hover:bg-slate-800/50 hover:text-white transition-all duration-200",
collapsed ? "justify-center px-3 py-4" : "px-4 py-3"
)}
>
{theme === 'dark' ? (
<Sun className="w-6 h-6 text-amber-400" />
) : (
<Moon className="w-6 h-6 text-slate-400" />
)}
{!collapsed && (
<span className="ml-3">
{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
</span>
)}
</button>
{/* Logout */}
<button className={cn(
"w-full flex items-center rounded-xl text-sm font-medium text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-all duration-200",
collapsed ? "justify-center px-3 py-4" : "px-4 py-3"
)}>
<LogOut className="w-6 h-6" />
{!collapsed && <span className="ml-3">Logout</span>}
</button>
</div>
</div>
);
};
export default ResellerSidebar;

471
src/data/mockData.ts Normal file
View File

@ -0,0 +1,471 @@
import { DashboardStats, RecentActivity, QuickAction } from '../store/slices/dashboardSlice';
import { User } from '../store/slices/authSlice';
export const mockUser: User = {
id: '1',
email: 'yasha.khandelwal@channelpartners.com',
name: 'Yasha Khandelwal',
role: 'channel_partner',
company: 'Tech4biz Solutions',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face',
tier: 'platinum',
isVerified: true,
twoFactorEnabled: true,
region: 'North India',
commissionRate: 15,
};
export const mockDashboardStats: DashboardStats = {
totalRevenue: 4560000,
totalResellers: 89,
activePartnerships: 67,
pendingApprovals: 8,
monthlyGrowth: 34.2,
commissionEarned: 684000,
averageDealSize: 51200,
conversionRate: 78.5,
currency: 'INR',
};
export const mockRecentActivities: RecentActivity[] = [
{
id: '1',
type: 'reseller_added',
title: 'New Reseller Partnership',
description: 'Softude from Indore has been approved as a new reseller partner',
timestamp: '2025-01-15T10:30:00Z',
},
{
id: '2',
type: 'deal_closed',
title: 'Major Deal Closed',
description: 'Closed enterprise deal worth ₹2.5M with DataFlow Solutions',
timestamp: '2025-01-15T09:15:00Z',
amount: 2500000,
currency: 'INR',
},
{
id: '3',
type: 'commission_earned',
title: 'Commission Earned',
description: 'Earned commission of ₹375,000 from CloudTech Ltd partnership',
timestamp: '2025-01-15T08:45:00Z',
amount: 375000,
currency: 'INR',
},
{
id: '4',
type: 'partnership_approved',
title: 'Partnership Approved',
description: 'Softude partnership has been approved and activated',
timestamp: '2025-01-14T16:20:00Z',
},
{
id: '5',
type: 'training_completed',
title: 'Training Completed',
description: 'Completed advanced training module for 12 reseller partners including Softude',
timestamp: '2025-01-14T14:10:00Z',
},
];
export const mockResellerRecentActivities: RecentActivity[] = [
{
id: '1',
type: 'customer_added',
title: 'New Customer Added',
description: 'Successfully onboarded TechCorp Solutions as a new customer',
timestamp: '2025-01-15T10:30:00Z',
amount: 50000,
currency: 'USD',
},
{
id: '2',
type: 'instance_created',
title: 'Cloud Instance Created',
description: 'Created new cloud instance for DataFlow Inc with 8GB RAM configuration',
timestamp: '2025-01-15T09:15:00Z',
amount: 1200,
currency: 'USD',
},
{
id: '3',
type: 'payment_received',
title: 'Payment Received',
description: 'Monthly payment of $2,500 received from CloudTech Solutions',
timestamp: '2025-01-15T08:45:00Z',
amount: 2500,
currency: 'USD',
},
{
id: '4',
type: 'support_ticket',
title: 'Support Ticket Resolved',
description: 'Successfully resolved high-priority support ticket for InnovateSoft',
timestamp: '2025-01-15T08:00:00Z',
},
{
id: '5',
type: 'training_completed',
title: 'Training Completed',
description: 'Completed advanced cloud services certification training',
timestamp: '2025-01-14T16:30:00Z',
},
];
export const mockQuickActions: QuickAction[] = [
{
id: 'add-reseller',
title: 'Add Reseller',
description: 'Onboard a new reseller partner',
icon: 'UserPlus',
action: '/resellers/new',
color: 'primary',
},
{
id: 'product-management',
title: 'Product Management',
description: 'Manage products and pricing',
icon: 'Package',
action: '/product-management',
color: 'success',
},
{
id: 'approve-partnership',
title: 'Approve Partnership',
description: 'Review and approve pending partnerships',
icon: 'CheckCircle',
action: '/partnerships/approve',
color: 'warning',
},
{
id: 'create-deal',
title: 'Create Deal',
description: 'Create a new business deal',
icon: 'Briefcase',
action: '/deals/new',
color: 'danger',
},
{
id: 'training-session',
title: 'Training Session',
description: 'Schedule training for resellers',
icon: 'GraduationCap',
action: '/training/schedule',
color: 'primary',
},
{
id: 'commission-report',
title: 'Commission Report',
description: 'Generate commission reports',
icon: 'FileText',
action: '/reports/commission',
color: 'secondary',
},
];
export const mockResellerQuickActions = [
{
id: 'add-customer',
title: 'Add New Customer',
description: 'Onboard a new customer',
icon: 'UserPlus',
action: '/reseller/customers',
color: 'primary',
},
{
id: 'create-instance',
title: 'Create Instance',
description: 'Set up new cloud instance',
icon: 'Cloud',
action: '/reseller/instances',
color: 'success',
},
{
id: 'billing',
title: 'Manage Billing',
description: 'Handle billing and payments',
icon: 'CreditCard',
action: '/reseller/billing',
color: 'warning',
},
{
id: 'support',
title: 'Support Ticket',
description: 'Create support request',
icon: 'Headphones',
action: '/reseller/support',
color: 'danger',
},
{
id: 'training',
title: 'Training Center',
description: 'Access training materials',
icon: 'GraduationCap',
action: '/reseller/training',
color: 'primary',
},
{
id: 'reports',
title: 'View Reports',
description: 'Generate performance reports',
icon: 'FileText',
action: '/reseller/reports',
color: 'secondary',
},
];
export const mockResellers = [
{
id: '6',
name: 'Softude',
email: 'contact@softude.com',
phone: '+91-98765-43215',
status: 'active',
tier: 'gold',
totalRevenue: 1500000,
lastActive: '2025-01-15T10:30:00Z',
customers: 50,
commissionRate: 13,
region: 'Indore',
avatar: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=150&h=150&fit=crop',
},
{
id: '1',
name: 'TechCorp Solutions',
email: 'contact@techcorp.com',
phone: '+91-98765-43210',
status: 'active',
tier: 'gold',
totalRevenue: 1250000,
lastActive: '2025-01-15T10:30:00Z',
customers: 45,
commissionRate: 12,
region: 'Mumbai',
avatar: 'https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=150&h=150&fit=crop',
},
{
id: '2',
name: 'DataFlow Solutions',
email: 'hello@dataflow.com',
phone: '+91-98765-43211',
status: 'active',
tier: 'platinum',
totalRevenue: 890000,
lastActive: '2025-01-15T09:15:00Z',
customers: 32,
commissionRate: 15,
region: 'Delhi',
avatar: 'https://images.unsplash.com/photo-1556761175-b413da4baf72?w=150&h=150&fit=crop',
},
{
id: '3',
name: 'CloudTech Ltd',
email: 'info@cloudtech.com',
phone: '+91-98765-43212',
status: 'pending',
tier: 'silver',
totalRevenue: 450000,
lastActive: '2025-01-15T08:45:00Z',
customers: 18,
commissionRate: 10,
region: 'Bangalore',
avatar: 'https://images.unsplash.com/photo-1563013544-824ae1b704d3?w=150&h=150&fit=crop',
},
{
id: '4',
name: 'InnovateSoft Solutions',
email: 'hello@innovatesoft.com',
phone: '+91-98765-43213',
status: 'active',
tier: 'gold',
totalRevenue: 750000,
lastActive: '2025-01-14T16:20:00Z',
customers: 28,
commissionRate: 12,
region: 'Chennai',
avatar: 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=150&h=150&fit=crop',
},
{
id: '5',
name: 'Digital Dynamics',
email: 'contact@digitaldynamics.com',
phone: '+91-98765-43214',
status: 'active',
tier: 'platinum',
totalRevenue: 2100000,
lastActive: '2025-01-14T14:10:00Z',
customers: 67,
commissionRate: 15,
region: 'Hyderabad',
avatar: 'https://images.unsplash.com/photo-1551434678-e076c223a692?w=150&h=150&fit=crop',
},
];
export const mockDeals = [
{
id: '1',
title: 'Enterprise Cloud Migration',
reseller: 'TechCorp Solutions',
customer: 'ABC Corporation',
value: 2500000,
status: 'closed',
stage: 'completed',
probability: 100,
expectedCloseDate: '2025-01-15T00:00:00Z',
commission: 300000,
createdAt: '2025-01-10T10:30:00Z',
},
{
id: '2',
title: 'Data Center Expansion',
reseller: 'DataFlow Solutions',
customer: 'XYZ Industries',
value: 1800000,
status: 'negotiation',
stage: 'proposal',
probability: 75,
expectedCloseDate: '2025-01-25T00:00:00Z',
commission: 270000,
createdAt: '2025-01-12T09:15:00Z',
},
{
id: '3',
title: 'SaaS Platform License',
reseller: 'CloudTech Ltd',
customer: 'Startup Inc',
value: 450000,
status: 'prospecting',
stage: 'qualification',
probability: 40,
expectedCloseDate: '2025-02-15T00:00:00Z',
commission: 67500,
createdAt: '2025-01-08T08:45:00Z',
},
{
id: '4',
title: 'Security Infrastructure',
reseller: 'InnovateSoft Solutions',
customer: 'Bank Ltd',
value: 3200000,
status: 'closed',
stage: 'completed',
probability: 100,
expectedCloseDate: '2025-01-05T00:00:00Z',
commission: 480000,
createdAt: '2025-01-05T16:20:00Z',
},
{
id: '5',
title: 'DevOps Automation',
reseller: 'Digital Dynamics',
customer: 'Tech Startup',
value: 850000,
status: 'negotiation',
stage: 'demo',
probability: 60,
expectedCloseDate: '2025-01-30T00:00:00Z',
commission: 127500,
createdAt: '2025-01-03T14:10:00Z',
},
{
id: '6',
title: 'Digital Transformation Project',
reseller: 'Softude',
customer: 'Manufacturing Corp',
value: 1200000,
status: 'negotiation',
stage: 'proposal',
probability: 80,
expectedCloseDate: '2025-02-10T00:00:00Z',
commission: 156000,
createdAt: '2025-01-15T10:30:00Z',
},
];
export const mockPartnerships = [
{
id: '1',
reseller: 'TechCorp Solutions',
status: 'active',
tier: 'gold',
startDate: '2023-06-15T00:00:00Z',
commissionRate: 12,
totalRevenue: 1250000,
customers: 45,
region: 'Mumbai',
contactPerson: 'John Doe',
contactEmail: 'john@techcorp.com',
contactPhone: '+91-98765-43210',
},
{
id: '2',
reseller: 'DataFlow Solutions',
status: 'active',
tier: 'platinum',
startDate: '2023-03-20T00:00:00Z',
commissionRate: 15,
totalRevenue: 890000,
customers: 32,
region: 'Delhi',
contactPerson: 'Jane Smith',
contactEmail: 'jane@dataflow.com',
contactPhone: '+91-98765-43211',
},
{
id: '3',
reseller: 'CloudTech Ltd',
status: 'pending',
tier: 'silver',
startDate: '2025-01-10T00:00:00Z',
commissionRate: 10,
totalRevenue: 0,
customers: 0,
region: 'Bangalore',
contactPerson: 'Mike Johnson',
contactEmail: 'mike@cloudtech.com',
contactPhone: '+91-98765-43212',
},
{
id: '4',
reseller: 'InnovateSoft Solutions',
status: 'active',
tier: 'gold',
startDate: '2023-09-05T00:00:00Z',
commissionRate: 12,
totalRevenue: 750000,
customers: 28,
region: 'Chennai',
contactPerson: 'Sarah Wilson',
contactEmail: 'sarah@innovatesoft.com',
contactPhone: '+91-98765-43213',
},
{
id: '5',
reseller: 'Digital Dynamics',
status: 'active',
tier: 'platinum',
startDate: '2023-01-15T00:00:00Z',
commissionRate: 15,
totalRevenue: 2100000,
customers: 67,
region: 'Hyderabad',
contactPerson: 'David Brown',
contactEmail: 'david@digitaldynamics.com',
contactPhone: '+91-98765-43214',
},
{
id: '6',
reseller: 'Softude',
status: 'active',
tier: 'gold',
startDate: '2025-01-15T00:00:00Z',
commissionRate: 13,
totalRevenue: 1500000,
customers: 50,
region: 'Indore',
contactPerson: 'Raj Patel',
contactEmail: 'raj@softude.com',
contactPhone: '+91-98765-43215',
},
];

View File

@ -0,0 +1,2 @@
// Reseller Data Index
export * from './mockData';

View File

@ -0,0 +1,145 @@
export interface ResellerRecentActivity {
id: string;
type: 'customer_added' | 'instance_created' | 'payment_received' | 'support_ticket' | 'training_completed';
title: string;
description: string;
timestamp: string;
amount?: number;
currency?: 'USD' | 'INR';
}
export interface ResellerQuickAction {
id: string;
title: string;
description: string;
icon: string;
action: string;
color: string;
}
export interface ResellerDashboardStats {
totalRevenue: number;
activeCustomers: number;
cloudInstances: number;
commissionRate: number;
monthlyGrowth: number;
currency?: 'USD' | 'INR';
}
export const mockResellerDashboardStats: ResellerDashboardStats = {
totalRevenue: 4560000,
activeCustomers: 89,
cloudInstances: 67,
commissionRate: 15.0,
monthlyGrowth: 34.2,
currency: 'INR',
};
export const mockResellerRecentActivities: ResellerRecentActivity[] = [
{
id: '1',
type: 'customer_added',
title: 'New Customer Added',
description: 'Successfully onboarded TechCorp Solutions as a new customer',
timestamp: '2025-01-15T10:30:00Z',
amount: 50000,
currency: 'USD',
},
{
id: '2',
type: 'instance_created',
title: 'Cloud Instance Created',
description: 'Created new cloud instance for DataFlow Inc with 8GB RAM configuration',
timestamp: '2025-01-15T09:15:00Z',
amount: 1200,
currency: 'USD',
},
{
id: '3',
type: 'payment_received',
title: 'Payment Received',
description: 'Monthly payment of $2,500 received from CloudTech Solutions',
timestamp: '2025-01-15T08:45:00Z',
amount: 2500,
currency: 'USD',
},
{
id: '4',
type: 'support_ticket',
title: 'Support Ticket Resolved',
description: 'Successfully resolved high-priority support ticket for InnovateSoft',
timestamp: '2025-01-15T08:00:00Z',
},
{
id: '5',
type: 'training_completed',
title: 'Training Completed',
description: 'Completed advanced cloud services certification training',
timestamp: '2025-01-14T16:30:00Z',
},
];
export const mockResellerQuickActions: ResellerQuickAction[] = [
{
id: 'add-customer',
title: 'Add New Customer',
description: 'Onboard a new customer',
icon: 'UserPlus',
action: '/reseller-dashboard/customers/new',
color: 'primary',
},
{
id: 'create-instance',
title: 'Create Instance',
description: 'Set up new cloud instance',
icon: 'Cloud',
action: '/reseller-dashboard/instances/new',
color: 'success',
},
{
id: 'manage-billing',
title: 'Manage Billing',
description: 'Handle billing and payments',
icon: 'CreditCard',
action: '/reseller-dashboard/billing',
color: 'warning',
},
{
id: 'support-ticket',
title: 'Support Ticket',
description: 'Create support request',
icon: 'Headphones',
action: '/reseller-dashboard/support',
color: 'danger',
},
{
id: 'training-center',
title: 'Training Center',
description: 'Access training materials',
icon: 'BookOpen',
action: '/reseller-dashboard/training',
color: 'primary',
},
{
id: 'view-reports',
title: 'View Reports',
description: 'Generate performance reports',
icon: 'BarChart3',
action: '/reseller-dashboard/reports',
color: 'secondary',
},
];
export const mockResellerUser = {
id: '1',
email: 'john@techsolutions.com',
name: 'John Reseller',
role: 'reseller_admin' as const,
company: 'Tech Solutions Inc',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop',
tier: 'gold' as const,
isVerified: true,
twoFactorEnabled: false,
region: 'North America',
commissionRate: 12,
};

225
src/index.css Normal file
View File

@ -0,0 +1,225 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
@apply border-secondary-200 dark:border-secondary-700;
font-family: 'Inter', sans-serif !important;
}
body {
@apply bg-gray-50 dark:bg-gray-900 text-secondary-900 dark:text-white font-sans antialiased;
font-family: 'Inter', sans-serif !important;
}
html {
@apply scroll-smooth;
}
/* Modern Typography Scale */
h1 {
@apply text-3xl sm:text-4xl font-bold tracking-tight;
}
h2 {
@apply text-2xl sm:text-3xl font-semibold tracking-tight;
}
h3 {
@apply text-xl sm:text-2xl font-semibold;
}
h4 {
@apply text-lg sm:text-xl font-medium;
}
/* Enhanced Focus States */
*:focus-visible {
@apply outline-none ring-2 ring-primary-500 ring-offset-2 ring-offset-white dark:ring-offset-gray-900;
}
}
@layer components {
/* Modern Button System */
.btn {
@apply inline-flex items-center justify-center rounded-lg font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-white dark:ring-offset-gray-900;
}
.btn-primary {
@apply bg-gradient-to-r from-primary-600 to-primary-500 text-white shadow-sm hover:shadow-md hover:from-primary-700 hover:to-primary-600 active:scale-95 active:shadow-lg;
}
.btn-secondary {
@apply bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white border border-gray-300 dark:border-gray-600 shadow-sm hover:shadow-md active:scale-95;
}
.btn-outline {
@apply bg-transparent border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 shadow-sm hover:shadow-md active:scale-95;
}
.btn-ghost {
@apply bg-transparent text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 active:scale-95;
}
.btn-sm {
@apply h-8 px-3 text-xs;
}
.btn-md {
@apply h-10 px-4 py-2 text-sm;
}
.btn-lg {
@apply h-12 px-6 py-3 text-base;
}
/* Modern Card Design */
.card {
@apply rounded-xl border border-gray-200/60 dark:border-gray-700/60 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm shadow-sm hover:shadow-md transition-all duration-300;
}
/* Enhanced Input System */
.input {
@apply flex h-10 w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-all duration-200 ring-offset-white dark:ring-offset-gray-900 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50;
}
.badge {
@apply inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2;
}
.badge-primary {
@apply border-transparent bg-primary-600 text-white hover:bg-primary-700;
}
.badge-secondary {
@apply border-transparent bg-secondary-100 text-secondary-900 hover:bg-secondary-200 dark:bg-secondary-800 dark:text-secondary-100 dark:hover:bg-secondary-700;
}
.badge-success {
@apply border-transparent bg-success-100 text-success-800 hover:bg-success-200 dark:bg-success-900 dark:text-success-100 dark:hover:bg-success-800;
}
.badge-warning {
@apply border-transparent bg-warning-100 text-warning-800 hover:bg-warning-200 dark:bg-warning-900 dark:text-warning-100 dark:hover:bg-warning-800;
}
.badge-danger {
@apply border-transparent bg-danger-100 text-danger-800 hover:bg-danger-200 dark:bg-danger-900 dark:text-danger-100 dark:hover:bg-danger-800;
}
/* Modern Table Design */
.table {
@apply w-full border-collapse;
}
.table th {
@apply px-6 py-4 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider border-b border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/50;
}
.table td {
@apply px-6 py-4 text-sm text-gray-900 dark:text-white border-b border-gray-100 dark:border-gray-800;
}
.table tbody tr {
@apply hover:bg-gray-50/50 dark:hover:bg-gray-700/50 transition-colors duration-150;
}
/* Modern Modal Design */
.modal-overlay {
@apply fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4;
}
.modal-content {
@apply bg-white dark:bg-gray-800 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden border border-gray-200/20 dark:border-gray-700/20;
}
/* Enhanced Dropdown */
.dropdown {
@apply absolute right-0 top-8 z-50 w-56 bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 py-2 backdrop-blur-sm;
}
/* Loading States */
.skeleton {
@apply animate-pulse bg-gray-200 dark:bg-gray-700 rounded;
}
/* Scrollbar Styling */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: rgb(156 163 175) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgb(156 163 175);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: rgb(107 114 128);
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.gradient-text {
@apply bg-gradient-to-r from-primary-600 to-primary-400 bg-clip-text text-transparent;
}
.glass-effect {
@apply backdrop-blur-sm bg-white/80 dark:bg-secondary-900/80 border border-white/20 dark:border-secondary-700/20;
}
/* Hover Effects */
.hover\:scale-105:hover {
transform: scale(1.05);
}
.hover\:scale-110:hover {
transform: scale(1.1);
}
/* Button hover effects */
.btn-hover-lift {
transition: all 0.2s ease-in-out;
}
.btn-hover-lift:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
/* Line clamp utilities */
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.line-clamp-3 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
}

19
src/index.tsx Normal file
View File

@ -0,0 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
src/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,439 @@
import React, { useState } from 'react';
import { formatCurrency, formatNumber, formatDate, formatPercentage } from '../../utils/format';
import CommissionTrendsChart from '../../components/charts/CommissionTrendsChart';
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
import {
Search,
Filter,
Download,
TrendingUp,
DollarSign,
Calendar,
Users,
Target,
BarChart3,
PieChart,
Activity,
Award,
Clock,
CheckCircle,
AlertCircle
} from 'lucide-react';
import { cn } from '../../utils/cn';
// Mock commission data
const mockCommissions = [
{
id: '1',
reseller: 'TechCorp Solutions',
deal: 'Enterprise Cloud Migration',
amount: 300000,
commissionRate: 12,
commissionEarned: 36000,
status: 'paid',
paidDate: '2024-01-15T00:00:00Z',
dealValue: 2500000,
},
{
id: '2',
reseller: 'DataFlow Solutions',
deal: 'Data Center Expansion',
amount: 270000,
commissionRate: 15,
commissionEarned: 40500,
status: 'pending',
paidDate: null,
dealValue: 1800000,
},
{
id: '3',
reseller: 'CloudTech Ltd',
deal: 'SaaS Platform License',
amount: 67500,
commissionRate: 10,
commissionEarned: 6750,
status: 'processing',
paidDate: null,
dealValue: 450000,
},
{
id: '4',
reseller: 'InnovateSoft Solutions',
deal: 'Security Infrastructure',
amount: 480000,
commissionRate: 12,
commissionEarned: 57600,
status: 'paid',
paidDate: '2024-01-10T00:00:00Z',
dealValue: 3200000,
},
{
id: '5',
reseller: 'Digital Dynamics',
deal: 'DevOps Automation',
amount: 127500,
commissionRate: 15,
commissionEarned: 19125,
status: 'paid',
paidDate: '2024-01-12T00:00:00Z',
dealValue: 850000,
},
];
const CommissionsPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [dateFilter, setDateFilter] = useState('all');
const filteredCommissions = mockCommissions.filter(commission => {
const matchesSearch = commission.reseller.toLowerCase().includes(searchTerm.toLowerCase()) ||
commission.deal.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || commission.status === statusFilter;
return matchesSearch && matchesStatus;
});
const totalCommissionEarned = mockCommissions.reduce((sum, c) => sum + c.commissionEarned, 0);
const totalPaid = mockCommissions.filter(c => c.status === 'paid').reduce((sum, c) => sum + c.commissionEarned, 0);
const totalPending = mockCommissions.filter(c => c.status === 'pending').reduce((sum, c) => sum + c.commissionEarned, 0);
const avgCommissionRate = mockCommissions.reduce((sum, c) => sum + c.commissionRate, 0) / mockCommissions.length;
const getStatusColor = (status: string) => {
switch (status) {
case 'paid':
return 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-300';
case 'pending':
return 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-300';
case 'processing':
return 'bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-300';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'paid':
return <CheckCircle className="w-4 h-4" />;
case 'pending':
return <Clock className="w-4 h-4" />;
case 'processing':
return <Activity className="w-4 h-4" />;
default:
return <AlertCircle className="w-4 h-4" />;
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white">
Commissions
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Track your commission earnings and payments
</p>
</div>
<div className="flex space-x-3">
<button className="inline-flex items-center px-4 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">
<Download className="w-4 h-4 mr-2" />
Export Report
</button>
<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 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">
<BarChart3 className="w-4 h-4 mr-2" />
Generate Report
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="card p-6 bg-white dark:bg-gray-800">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Total Earned
</p>
<DualCurrencyDisplay
amount={totalCommissionEarned}
currency="INR"
className="text-2xl"
/>
</div>
<div className="w-12 h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center">
<DollarSign 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">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Total Paid
</p>
<DualCurrencyDisplay
amount={totalPaid}
currency="INR"
className="text-2xl"
/>
</div>
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-primary-600 dark:text-primary-400" />
</div>
</div>
</div>
<div className="card p-6 bg-white dark:bg-gray-800">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Pending
</p>
<DualCurrencyDisplay
amount={totalPending}
currency="INR"
className="text-2xl"
/>
</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" />
</div>
</div>
</div>
<div className="card p-6 bg-white dark:bg-gray-800">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Avg Commission Rate
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{formatPercentage(avgCommissionRate)}
</p>
</div>
<div className="w-12 h-12 bg-secondary-100 dark:bg-secondary-900 rounded-full flex items-center justify-center">
<Target className="w-6 h-6 text-secondary-600 dark:text-secondary-400" />
</div>
</div>
</div>
</div>
{/* Commission Analytics */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<div className="card p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
Commission Trends
</h3>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600 dark:text-gray-400">Last 12 months</span>
</div>
</div>
<div className="h-80">
<CommissionTrendsChart />
</div>
</div>
</div>
<div className="card p-6">
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-4">
Top Earners
</h3>
<div className="space-y-4">
{mockCommissions
.sort((a, b) => b.commissionEarned - a.commissionEarned)
.slice(0, 5)
.map((commission, index) => (
<div key={commission.id} className="flex items-center justify-between p-3 bg-secondary-50 dark:bg-secondary-800 rounded-lg">
<div className="flex items-center">
<div className={cn(
"w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-medium mr-3",
index === 0 && "bg-gradient-to-r from-yellow-400 to-yellow-600",
index === 1 && "bg-gradient-to-r from-gray-400 to-gray-600",
index === 2 && "bg-gradient-to-r from-orange-400 to-orange-600",
index > 2 && "bg-secondary-400"
)}>
{index + 1}
</div>
<div>
<p className="font-medium text-secondary-900 dark:text-white text-sm">
{commission.reseller}
</p>
<p className="text-xs text-secondary-600 dark:text-secondary-400">
{commission.deal}
</p>
</div>
</div>
<div className="text-right">
<DualCurrencyDisplay
amount={commission.commissionEarned}
currency="INR"
className="text-sm"
/>
<p className="text-xs text-secondary-600 dark:text-secondary-400">
{commission.commissionRate}%
</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* Filters and Search */}
<div className="card p-6">
<div className="flex flex-col lg:flex-row gap-4">
<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-secondary-400" />
<input
type="text"
placeholder="Search commissions..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pl-10 w-full"
/>
</div>
</div>
<div className="flex gap-4">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="input"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="processing">Processing</option>
</select>
<select
value={dateFilter}
onChange={(e) => setDateFilter(e.target.value)}
className="input"
>
<option value="all">All Time</option>
<option value="this_month">This Month</option>
<option value="last_month">Last Month</option>
<option value="this_quarter">This Quarter</option>
</select>
<button className="btn btn-outline btn-md">
<Filter className="w-4 h-4 mr-2" />
Filters
</button>
</div>
</div>
</div>
{/* Commissions 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">
Commission History
</h3>
<div className="flex space-x-2">
<button className="btn btn-outline btn-sm">
<Download className="w-4 h-4 mr-2" />
Export
</button>
</div>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<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">
Reseller
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Deal
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Deal Value
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Commission Rate
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Commission Earned
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Paid Date
</th>
<th className="px-6 py-3 text-right 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">
{filteredCommissions.map((commission) => (
<tr key={commission.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-800 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{commission.reseller}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-secondary-900 dark:text-white">
{commission.deal}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<DualCurrencyDisplay
amount={commission.dealValue}
currency="INR"
className="text-sm"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{commission.commissionRate}%
</td>
<td className="px-6 py-4 whitespace-nowrap">
<DualCurrencyDisplay
amount={commission.commissionEarned}
currency="INR"
className="text-sm font-medium"
/>
</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(commission.status)
)}>
{getStatusIcon(commission.status)}
<span className="ml-1">
{commission.status.charAt(0).toUpperCase() + commission.status.slice(1)}
</span>
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-400">
{commission.paidDate ? formatDate(commission.paidDate) : '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button className="btn btn-outline btn-sm">
View Details
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
};
export default CommissionsPage;

407
src/pages/Dashboard.tsx Normal file
View File

@ -0,0 +1,407 @@
import React, { useEffect } from 'react';
import { useAppSelector, useAppDispatch } from '../store/hooks';
import { useNavigate } from 'react-router-dom';
import { setStats, setRecentActivities, setQuickActions } from '../store/slices/dashboardSlice';
import { loginSuccess } from '../store/slices/authSlice';
import { mockDashboardStats, mockRecentActivities, mockQuickActions, mockUser } from '../data/mockData';
import { formatCurrency, formatCurrencyDual, formatNumber, formatRelativeTime, formatPercentage } from '../utils/format';
import RevenueChart from '../components/charts/RevenueChart';
import ResellerPerformanceChart from '../components/charts/ResellerPerformanceChart';
import DualCurrencyDisplay from '../components/DualCurrencyDisplay';
import {
TrendingUp,
Users,
Handshake,
FileText,
DollarSign,
UserPlus,
CheckCircle,
Briefcase,
GraduationCap,
BarChart3,
Target,
Briefcase as BriefcaseIcon,
FileText as FileTextIcon,
Package
} from 'lucide-react';
import { cn } from '../utils/cn';
const Dashboard: React.FC = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard);
useEffect(() => {
// Initialize with mock data
dispatch(loginSuccess({
user: mockUser,
token: 'mock-token',
refreshToken: 'mock-refresh-token'
}));
dispatch(setStats(mockDashboardStats));
dispatch(setRecentActivities(mockRecentActivities));
dispatch(setQuickActions(mockQuickActions));
}, [dispatch]);
const getActivityIcon = (type: string) => {
switch (type) {
case 'reseller_added':
return <UserPlus className="w-5 h-5 text-success-600" />;
case 'deal_closed':
return <BriefcaseIcon className="w-5 h-5 text-primary-600" />;
case 'commission_earned':
return <DollarSign className="w-5 h-5 text-success-600" />;
case 'partnership_approved':
return <CheckCircle className="w-5 h-5 text-success-600" />;
case 'training_completed':
return <GraduationCap className="w-5 h-5 text-warning-600" />;
default:
return <FileTextIcon className="w-5 h-5 text-secondary-600" />;
}
};
const getQuickActionColor = (color: string) => {
switch (color) {
case 'primary':
return 'bg-primary-100 text-primary-700 hover:bg-primary-200 dark:bg-primary-900 dark:text-primary-300 dark:hover:bg-primary-800';
case 'success':
return 'bg-success-100 text-success-700 hover:bg-success-200 dark:bg-success-900 dark:text-success-300 dark:hover:bg-success-800';
case 'warning':
return 'bg-warning-100 text-warning-700 hover:bg-warning-200 dark:bg-warning-900 dark:text-warning-300 dark:hover:bg-warning-800';
case 'danger':
return 'bg-danger-100 text-danger-700 hover:bg-danger-200 dark:bg-danger-900 dark:text-danger-300 dark:hover:bg-danger-800';
case 'secondary':
return 'bg-secondary-100 text-secondary-700 hover:bg-secondary-200 dark:bg-secondary-900 dark:text-secondary-300 dark:hover:bg-secondary-800';
default:
return 'bg-primary-100 text-primary-700 hover:bg-primary-200 dark:bg-primary-900 dark:text-primary-300 dark:hover:bg-primary-800';
}
};
const handleQuickAction = (action: any) => {
switch (action.id) {
case 'add-reseller':
navigate('/resellers');
break;
case 'product-management':
navigate('/product-management');
break;
case 'approve-partnership':
navigate('/partnerships');
break;
case 'create-deal':
navigate('/deals');
break;
case 'training-session':
navigate('/training');
break;
case 'commission-report':
navigate('/commissions');
break;
case 'performance-analytics':
navigate('/analytics');
break;
default:
break;
}
};
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>
<h1 className="text-2xl sm:text-3xl font-bold text-secondary-900 dark:text-white">
Welcome back, Yasha!
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Here's your channel partner performance overview.
</p>
</div>
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="text-sm text-secondary-600 dark:text-secondary-400">Commission Earned</p>
<DualCurrencyDisplay
amount={stats.commissionEarned}
currency={stats.currency}
className="text-xl sm:text-2xl"
/>
</div>
<div className="w-12 h-12 bg-gradient-to-r from-primary-600 to-primary-400 rounded-full flex items-center justify-center">
<DollarSign className="w-6 h-6 text-white" />
</div>
</div>
</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 Revenue
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Total sales from all resellers
</p>
<DualCurrencyDisplay
amount={stats.totalRevenue}
currency={stats.currency}
className="text-lg sm:text-2xl truncate"
/>
</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 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">
+{stats.monthlyGrowth}% from last month
</span>
</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 Resellers
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Currently active partner companies
</p>
<p className="text-lg sm:text-2xl font-bold text-secondary-900 dark:text-white truncate">
{formatNumber(stats.totalResellers)}
</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">
<Users className="w-5 h-5 sm:w-6 sm:h-6 text-primary-600 dark:text-primary-400" />
</div>
</div>
<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">
+8 new this month
</span>
</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 Partnerships
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Approved business agreements
</p>
<p className="text-lg sm:text-2xl font-bold text-secondary-900 dark:text-white truncate">
{formatNumber(stats.activePartnerships)}
</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">
<Handshake className="w-5 h-5 sm:w-6 sm:h-6 text-warning-600 dark:text-warning-400" />
</div>
</div>
<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
</span>
</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">
Conversion Rate
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Lead to deal success rate
</p>
<p className="text-lg sm:text-2xl font-bold text-secondary-900 dark:text-white truncate">
{formatPercentage(stats.conversionRate)}
</p>
</div>
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-danger-100 dark:bg-danger-900 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
<Target className="w-5 h-5 sm:w-6 sm:h-6 text-danger-600 dark:text-danger-400" />
</div>
</div>
<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">
+5.2% from last month
</span>
</div>
</div>
</div>
{/* Additional Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 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">
Average Deal Size
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Mean value per closed deal
</p>
<DualCurrencyDisplay
amount={stats.averageDealSize}
currency={stats.currency}
className="text-lg sm:text-xl truncate"
/>
</div>
<div className="w-10 h-10 bg-secondary-100 dark:bg-secondary-900 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
<Briefcase className="w-5 h-5 text-secondary-600 dark:text-secondary-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">
Pending Approvals
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Awaiting admin review
</p>
<p className="text-lg sm:text-xl font-bold text-secondary-900 dark:text-white truncate">
{formatNumber(stats.pendingApprovals)}
</p>
</div>
<div className="w-10 h-10 bg-warning-100 dark:bg-warning-900 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
<FileText className="w-5 h-5 text-warning-600 dark:text-warning-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">
Commission Rate
</p>
<p className="text-xs text-secondary-500 dark:text-secondary-500 mb-1">
Standard partner commission
</p>
<p className="text-lg sm:text-xl font-bold text-secondary-900 dark:text-white truncate">
{formatPercentage(15)}
</p>
</div>
<div className="w-10 h-10 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 text-success-600 dark:text-success-400" />
</div>
</div>
</div>
</div>
{/* Quick Actions & Recent Activities */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
{/* Quick Actions */}
<div className="lg:col-span-1">
<div className="card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold text-secondary-900 dark:text-white mb-3 sm:mb-4">
Quick Actions
</h3>
<div className="space-y-2 sm:space-y-3">
{quickActions.map((action) => (
<button
key={action.id}
onClick={() => handleQuickAction(action)}
className={cn(
"w-full flex items-center p-2 sm:p-3 rounded-lg transition-colors cursor-pointer",
getQuickActionColor(action.color)
)}
>
<div className="w-6 h-6 sm:w-8 sm:h-8 rounded-full bg-white/20 flex items-center justify-center mr-2 sm:mr-3 flex-shrink-0">
{action.icon === 'UserPlus' && <UserPlus className="w-3 h-3 sm:w-4 sm:h-4" />}
{action.icon === 'Package' && <Package className="w-3 h-3 sm:w-4 sm:h-4" />}
{action.icon === 'CheckCircle' && <CheckCircle className="w-3 h-3 sm:w-4 sm:h-4" />}
{action.icon === 'Briefcase' && <Briefcase className="w-3 h-3 sm:w-4 sm:h-4" />}
{action.icon === 'GraduationCap' && <GraduationCap className="w-3 h-3 sm:w-4 sm:h-4" />}
{action.icon === 'FileText' && <FileTextIcon className="w-3 h-3 sm:w-4 sm:h-4" />}
{action.icon === 'BarChart3' && <BarChart3 className="w-3 h-3 sm:w-4 sm:h-4" />}
</div>
<div className="text-left min-w-0 flex-1">
<p className="font-medium text-sm sm:text-base truncate">{action.title}</p>
<p className="text-xs sm:text-sm opacity-80 truncate">{action.description}</p>
</div>
</button>
))}
</div>
</div>
</div>
{/* Recent Activities */}
<div className="lg:col-span-2">
<div className="card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold text-secondary-900 dark:text-white mb-3 sm:mb-4">
Recent Activities
</h3>
<div className="space-y-3 sm:space-y-4">
{recentActivities.map((activity) => (
<div key={activity.id} className="flex items-start space-x-2 sm:space-x-3">
<div className="flex-shrink-0 mt-1">
{getActivityIcon(activity.type)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-secondary-900 dark:text-white truncate">
{activity.title}
</p>
<p className="text-xs sm:text-sm text-secondary-600 dark:text-secondary-400 line-clamp-2">
{activity.description}
</p>
{activity.amount && (
<div className="mt-1">
<DualCurrencyDisplay
amount={activity.amount}
currency={activity.currency}
className="text-xs sm:text-sm font-medium text-success-600"
/>
</div>
)}
<p className="text-xs text-secondary-500 dark:text-secondary-500 mt-1">
{formatRelativeTime(activity.timestamp)}
</p>
</div>
</div>
))}
</div>
<div className="mt-4 sm:mt-6 pt-3 sm:pt-4 border-t border-secondary-200 dark:border-secondary-700">
<button className="text-xs sm:text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium">
View all activities
</button>
</div>
</div>
</div>
</div>
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
<div className="card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold text-secondary-900 dark:text-white mb-3 sm:mb-4">
Revenue Overview
</h3>
<div className="h-48 sm:h-64">
<RevenueChart />
</div>
</div>
<div className="card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold text-secondary-900 dark:text-white mb-3 sm:mb-4">
Reseller Performance
</h3>
<div className="h-48 sm:h-64">
<ResellerPerformanceChart />
</div>
</div>
</div>
</div>
);
};
export default Dashboard;

490
src/pages/Deals/index.tsx Normal file
View File

@ -0,0 +1,490 @@
import React, { useState } from 'react';
import { mockDeals } from '../../data/mockData';
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
import Modal from '../../components/Modal';
import AddDealForm from '../../components/forms/AddDealForm';
import DetailView from '../../components/DetailView';
import {
Search,
Filter,
Plus,
MoreVertical,
Eye,
Edit,
TrendingUp,
DollarSign,
Calendar,
Target,
Users,
Briefcase,
CheckCircle,
Clock,
AlertCircle,
Download,
Mail
} from 'lucide-react';
import { cn } from '../../utils/cn';
const DealsPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [stageFilter, setStageFilter] = useState('all');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [selectedDeal, setSelectedDeal] = useState<any>(null);
const filteredDeals = mockDeals.filter(deal => {
const matchesSearch = deal.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
deal.reseller.toLowerCase().includes(searchTerm.toLowerCase()) ||
deal.customer.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || deal.status === statusFilter;
const matchesStage = stageFilter === 'all' || deal.stage === stageFilter;
return matchesSearch && matchesStatus && matchesStage;
});
const getStatusColor = (status: string) => {
switch (status) {
case 'closed':
return 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-300';
case 'negotiation':
return 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-300';
case 'prospecting':
return 'bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-300';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
}
};
const getStageColor = (stage: string) => {
switch (stage) {
case 'completed':
return 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-300';
case 'proposal':
return 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-300';
case 'demo':
return 'bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-300';
case 'qualification':
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
}
};
const getProbabilityColor = (probability: number) => {
if (probability >= 80) return 'text-success-600 dark:text-success-400';
if (probability >= 60) return 'text-warning-600 dark:text-warning-400';
if (probability >= 40) return 'text-primary-600 dark:text-primary-400';
return 'text-secondary-600 dark:text-secondary-400';
};
const handleAddDeal = (data: any) => {
console.log('New deal data:', data);
// Here you would typically make an API call to add the deal
// For now, we'll just close the modal
setIsAddModalOpen(false);
// You could also show a success notification here
};
const handleViewDeal = (deal: any) => {
setSelectedDeal(deal);
setIsDetailModalOpen(true);
};
const handleEditDeal = (deal: any) => {
console.log('Edit deal:', deal);
alert(`Edit functionality for ${deal.title} - This would open an edit form`);
};
const handleMailDeal = (deal: any) => {
console.log('Mail deal:', deal);
const mailtoLink = `mailto:${deal.customer}@example.com?subject=Deal Update: ${deal.title}`;
window.open(mailtoLink, '_blank');
};
const handleMoreOptions = (deal: any) => {
console.log('More options for deal:', deal);
const options = [
'View Details',
'Download Proposal',
'Send Follow-up',
'Change Stage',
'Close Deal'
];
const selectedOption = prompt(`Select an option for ${deal.title}:\n${options.join('\n')}`);
if (selectedOption) {
alert(`Selected: ${selectedOption} for ${deal.title}`);
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white">
Deals
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Track and manage business deals and opportunities
</p>
</div>
<div className="flex space-x-3">
<button className="inline-flex items-center px-4 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">
<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 Deal
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="card p-6 bg-white dark:bg-gray-800">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Total Deals
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{mockDeals.length}
</p>
</div>
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
<Briefcase className="w-6 h-6 text-primary-600 dark:text-primary-400" />
</div>
</div>
</div>
<div className="card p-6 bg-white dark:bg-gray-800">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Total Value
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{formatCurrency(mockDeals.reduce((sum, d) => sum + d.value, 0))}
</p>
</div>
<div className="w-12 h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center">
<DollarSign 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">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Closed Deals
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{mockDeals.filter(d => d.status === 'closed').length}
</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">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Avg Deal Size
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{formatCurrency(mockDeals.reduce((sum, d) => sum + d.value, 0) / mockDeals.length)}
</p>
</div>
<div className="w-12 h-12 bg-warning-100 dark:bg-warning-900 rounded-full flex items-center justify-center">
<Target className="w-6 h-6 text-warning-600 dark:text-warning-400" />
</div>
</div>
</div>
</div>
{/* Pipeline Overview */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div className="card p-6">
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-4">
Pipeline Overview
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Prospecting</span>
<span className="text-sm font-medium text-secondary-900 dark:text-white">
{mockDeals.filter(d => d.stage === 'qualification').length}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Demo</span>
<span className="text-sm font-medium text-secondary-900 dark:text-white">
{mockDeals.filter(d => d.stage === 'demo').length}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Proposal</span>
<span className="text-sm font-medium text-secondary-900 dark:text-white">
{mockDeals.filter(d => d.stage === 'proposal').length}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-secondary-600 dark:text-secondary-400">Closed</span>
<span className="text-sm font-medium text-secondary-900 dark:text-white">
{mockDeals.filter(d => d.stage === 'completed').length}
</span>
</div>
</div>
</div>
<div className="lg:col-span-3">
<div className="card p-6">
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-4">
Recent Activity
</h3>
<div className="space-y-3">
{mockDeals.slice(0, 3).map((deal) => (
<div key={deal.id} className="flex items-center justify-between p-3 bg-secondary-50 dark:bg-secondary-800 rounded-lg">
<div>
<p className="font-medium text-secondary-900 dark:text-white">{deal.title}</p>
<p className="text-sm text-secondary-600 dark:text-secondary-400">
{deal.reseller} {deal.customer}
</p>
</div>
<div className="text-right">
<p className="font-medium text-secondary-900 dark:text-white">
{formatCurrency(deal.value)}
</p>
<p className="text-sm text-secondary-600 dark:text-secondary-400">
{deal.probability}% probability
</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="card p-6">
<div className="flex flex-col lg:flex-row gap-4">
<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-secondary-400" />
<input
type="text"
placeholder="Search deals..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pl-10 w-full"
/>
</div>
</div>
<div className="flex gap-4">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="input"
>
<option value="all">All Status</option>
<option value="closed">Closed</option>
<option value="negotiation">Negotiation</option>
<option value="prospecting">Prospecting</option>
</select>
<select
value={stageFilter}
onChange={(e) => setStageFilter(e.target.value)}
className="input"
>
<option value="all">All Stages</option>
<option value="completed">Completed</option>
<option value="proposal">Proposal</option>
<option value="demo">Demo</option>
<option value="qualification">Qualification</option>
</select>
<button className="btn btn-outline">
<Filter className="w-4 h-4 mr-2" />
More Filters
</button>
</div>
</div>
</div>
{/* Deals 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 Deals
</h3>
<div className="flex space-x-2">
<button className="btn btn-outline btn-sm">
<Download className="w-4 h-4 mr-2" />
Export
</button>
</div>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<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">
Deal
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Reseller
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Customer
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Value
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Stage
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Probability
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Expected Close
</th>
<th className="px-6 py-3 text-right 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">
{filteredDeals.map((deal) => (
<tr key={deal.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-800 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{deal.title}
</div>
<div className="text-sm text-secondary-500 dark:text-secondary-400">
Created {formatDate(deal.createdAt)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{deal.reseller}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{deal.customer}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{formatCurrency(deal.value)}
</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(deal.status)
)}>
{deal.status.charAt(0).toUpperCase() + deal.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",
getStageColor(deal.stage)
)}>
{deal.stage.charAt(0).toUpperCase() + deal.stage.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={cn("text-sm font-medium", getProbabilityColor(deal.probability))}>
{deal.probability}%
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-400">
{formatDate(deal.expectedCloseDate)}
</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={() => handleViewDeal(deal)}
className="p-1 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-colors"
title="View Details"
>
<Eye className="w-4 h-4 text-secondary-600 dark:text-secondary-400" />
</button>
<button
onClick={() => handleEditDeal(deal)}
className="p-1 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-colors"
title="Edit Deal"
>
<Edit className="w-4 h-4 text-secondary-600 dark:text-secondary-400" />
</button>
<button
onClick={() => handleMailDeal(deal)}
className="p-1 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-colors"
title="Send Email"
>
<Mail className="w-4 h-4 text-secondary-600 dark:text-secondary-400" />
</button>
<button
onClick={() => handleMoreOptions(deal)}
className="p-1 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-colors"
title="More Options"
>
<MoreVertical className="w-4 h-4 text-secondary-600 dark:text-secondary-400" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Add Deal Modal */}
<Modal
isOpen={isAddModalOpen}
onClose={() => setIsAddModalOpen(false)}
title="Add New Deal"
size="lg"
>
<AddDealForm
onSubmit={handleAddDeal}
onCancel={() => setIsAddModalOpen(false)}
/>
</Modal>
{/* Deal Detail Modal */}
<Modal
isOpen={isDetailModalOpen}
onClose={() => setIsDetailModalOpen(false)}
title="Deal Details"
size="lg"
>
{selectedDeal && (
<DetailView
type="deal"
data={selectedDeal}
/>
)}
</Modal>
</div>
);
};
export default DealsPage;

261
src/pages/Login.tsx Normal file
View File

@ -0,0 +1,261 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../store/hooks';
import { loginSuccess } from '../store/slices/authSlice';
import {
Eye,
EyeOff,
Mail,
Lock,
Building,
Sun,
Moon,
ArrowRight,
CheckCircle,
AlertCircle
} from 'lucide-react';
import { useAppSelector } from '../store/hooks';
import { RootState } from '../store';
import { toggleTheme } from '../store/slices/themeSlice';
import { cn } from '../utils/cn';
const Login: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [rememberMe, setRememberMe] = useState(false);
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
// Mock login success
dispatch(loginSuccess({
user: {
id: '1',
email: email,
name: 'Yasha Khandelwal',
role: 'channel_partner',
company: 'Tech4biz Solutions',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face',
tier: 'platinum',
isVerified: true,
twoFactorEnabled: true,
region: 'North India',
commissionRate: 15,
},
token: 'mock-token',
refreshToken: 'mock-refresh-token'
}));
navigate('/');
} catch (err) {
setError('Invalid email or password. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleThemeToggle = () => {
dispatch(toggleTheme());
};
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/80 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-md">
{/* Logo and Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl shadow-lg mb-6">
<Building className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Channel Partners
</h1>
<p className="text-slate-600 dark:text-slate-400">
Welcome back! Please sign in to your account.
</p>
</div>
{/* Login Form */}
<div className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700/20 p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Email Field */}
<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={email}
onChange={(e) => setEmail(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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter your email"
required
/>
</div>
</div>
{/* Password Field */}
<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={password}
onChange={(e) => setPassword(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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter your 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>
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between">
<label className="flex items-center">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-slate-300 dark:border-slate-600 rounded"
/>
<span className="ml-2 text-sm text-slate-700 dark:text-slate-300">
Remember me
</span>
</label>
<Link
to="/forgot-password"
className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 font-medium transition-colors duration-200"
>
Forgot password?
</Link>
</div>
{/* 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">
<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 */}
<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",
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>
Signing in...
</div>
) : (
<div className="flex items-center">
Sign in
<ArrowRight className="ml-2 h-4 w-4" />
</div>
)}
</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">
<p className="text-sm text-slate-600 dark:text-slate-400">
Don't have an account?{' '}
<Link
to="/signup"
className="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-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">
© 2024 Channel Partners. All rights reserved.
</p>
</div>
</div>
</div>
);
};
export default Login;

View File

@ -0,0 +1,466 @@
import React, { useState } from 'react';
import { mockPartnerships } from '../../data/mockData';
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
import Modal from '../../components/Modal';
import AddPartnershipForm from '../../components/forms/AddPartnershipForm';
import DetailView from '../../components/DetailView';
import {
Search,
Filter,
Plus,
MoreVertical,
Eye,
Edit,
CheckCircle,
XCircle,
Clock,
TrendingUp,
Users,
DollarSign,
Calendar,
Handshake,
AlertCircle,
Download,
Mail,
MapPin
} from 'lucide-react';
import { cn } from '../../utils/cn';
const PartnershipsPage: 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 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;
return matchesSearch && matchesStatus && matchesTier;
});
const getStatusColor = (status: string) => {
switch (status) {
case 'active':
return 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-300';
case 'pending':
return 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-300';
case 'suspended':
return 'bg-danger-100 text-danger-800 dark:bg-danger-900 dark:text-danger-300';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
}
};
const getTierColor = (tier: string) => {
switch (tier) {
case 'platinum':
return 'bg-gradient-to-r from-yellow-400 to-yellow-600 text-white';
case 'gold':
return 'bg-gradient-to-r from-yellow-500 to-yellow-700 text-white';
case 'silver':
return 'bg-gradient-to-r from-gray-400 to-gray-600 text-white';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
}
};
const handleAddPartnership = (data: any) => {
console.log('New partnership data:', data);
// Here you would typically make an API call to add the partnership
// For now, we'll just close the modal
setIsAddModalOpen(false);
// You could also show a success notification here
};
const handleViewPartnership = (partnership: any) => {
setSelectedPartnership(partnership);
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 handleMailPartnership = (partnership: any) => {
console.log('Mail partnership:', partnership);
const mailtoLink = `mailto:${partnership.contactEmail}?subject=Cloudtopiaa Partnership Update`;
window.open(mailtoLink, '_blank');
};
const handleMoreOptions = (partnership: any) => {
console.log('More options for partnership:', partnership);
const options = [
'View Performance',
'Download Report',
'Send Notification',
'Change Terms',
'Terminate Partnership'
];
const selectedOption = prompt(`Select an option for ${partnership.reseller}:\n${options.join('\n')}`);
if (selectedOption) {
alert(`Selected: ${selectedOption} for ${partnership.reseller}`);
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white">
Partnerships
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Manage reseller partnerships and approval workflows
</p>
</div>
<div className="flex space-x-3">
<button className="inline-flex items-center px-4 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">
<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>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<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 Partnerships
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{mockPartnerships.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" />
</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">
Active Partnerships
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{mockPartnerships.filter(p => p.status === 'active').length}
</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" />
</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 Revenue
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{formatCurrency(mockPartnerships.reduce((sum, p) => sum + p.totalRevenue, 0))}
</p>
</div>
<div className="w-12 h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center">
<DollarSign className="w-6 h-6 text-success-600 dark:text-success-400" />
</div>
</div>
</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">
<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-secondary-400" />
<input
type="text"
placeholder="Search partnerships..."
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"
/>
</div>
</div>
<div className="flex gap-3">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="input focus:ring-2 focus:ring-primary-500 transition-all duration-200"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="suspended">Suspended</option>
</select>
<select
value={tierFilter}
onChange={(e) => setTierFilter(e.target.value)}
className="input focus:ring-2 focus:ring-primary-500 transition-all duration-200"
>
<option value="all">All Tiers</option>
<option value="platinum">Platinum</option>
<option value="gold">Gold</option>
<option value="silver">Silver</option>
</select>
<button className="inline-flex items-center px-3 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:scale-105">
<Filter className="w-4 h-4 mr-2" />
Filters
</button>
</div>
</div>
</div>
{/* Partnerships 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
</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">
<Download className="w-4 h-4 mr-2" />
Export
</button>
</div>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<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">
Reseller
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Tier
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Revenue
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Customers
</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">
Start Date
</th>
<th className="px-6 py-3 text-right 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">
{filteredPartnerships.map((partnership) => (
<tr key={partnership.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>
</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)
)}>
{partnership.status.charAt(0).toUpperCase() + partnership.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)
)}>
{partnership.tier.charAt(0).toUpperCase() + partnership.tier.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{formatCurrency(partnership.totalRevenue)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{formatNumber(partnership.customers)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{partnership.commissionRate}%
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-400">
{formatDate(partnership.startDate)}
</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)}
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)}
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"
>
<Edit className="w-4 h-4 text-secondary-600 dark:text-secondary-400" />
</button>
<button
onClick={() => handleMailPartnership(partnership)}
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)}
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"
>
<MoreVertical className="w-4 h-4 text-secondary-600 dark:text-secondary-400" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Add Partnership Modal */}
<Modal
isOpen={isAddModalOpen}
onClose={() => setIsAddModalOpen(false)}
title="Add New Partnership"
size="lg"
>
<AddPartnershipForm
onSubmit={handleAddPartnership}
onCancel={() => setIsAddModalOpen(false)}
/>
</Modal>
{/* Partnership Detail Modal */}
<Modal
isOpen={isDetailModalOpen}
onClose={() => setIsDetailModalOpen(false)}
title="Partnership Details"
size="lg"
>
{selectedPartnership && (
<DetailView
type="partnership"
data={selectedPartnership}
/>
)}
</Modal>
</div>
);
};
export default PartnershipsPage;

View File

@ -0,0 +1,552 @@
import React, { useState } from 'react';
import {
Package,
DollarSign,
TrendingUp,
Filter,
Search,
SortAsc,
SortDesc,
Edit,
Eye,
Plus,
Minus,
Percent,
Tag,
Star,
Zap,
Shield,
Globe
} 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 [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 categories = ['all', ...Array.from(new Set(products.map(p => p.category)))];
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 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 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" />;
default:
return <Package className="w-4 h-4 text-gray-600 dark:text-gray-400" />;
}
};
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';
default:
return 'bg-gradient-to-r from-gray-500 to-gray-600';
}
};
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>
<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>
</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>
</div>
</div>
{/* Filters and Search */}
<div className="card p-4 sm:p-6">
<div className="flex flex-col lg:flex-row gap-4">
<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" />
<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"
/>
</div>
</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-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
{categories.map(category => (
<option key={category} value={category}>
{category === 'all' ? 'All Categories' : category}
</option>
))}
</select>
<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"
>
<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>
</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" />
</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}
</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>
))}
</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>
<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">
<button
onClick={() => setShowPricingModal(false)}
className="flex-1 btn btn-outline btn-lg"
>
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"
>
Update Pricing
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default ProductManagement;

View File

@ -0,0 +1,501 @@
import React, { useState } from 'react';
import { mockResellers } from '../../data/mockData';
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
import Modal from '../../components/Modal';
import AddResellerForm from '../../components/forms/AddResellerForm';
import EditResellerForm from '../../components/forms/EditResellerForm';
import MailComposeForm from '../../components/forms/MailComposeForm';
import MoreOptionsDropdown from '../../components/MoreOptionsDropdown';
import DetailView from '../../components/DetailView';
import {
Search,
Filter,
Plus,
MoreVertical,
Eye,
Edit,
Download,
Mail,
MapPin,
TrendingUp,
Users,
DollarSign,
Calendar
} from 'lucide-react';
import { cn } from '../../utils/cn';
const ResellersPage: 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 [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isMailModalOpen, setIsMailModalOpen] = useState(false);
const [selectedReseller, setSelectedReseller] = useState<any>(null);
const [showMoreOptions, setShowMoreOptions] = useState<string | null>(null);
const filteredResellers = mockResellers.filter(reseller => {
const matchesSearch = reseller.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
reseller.email.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || reseller.status === statusFilter;
const matchesTier = tierFilter === 'all' || reseller.tier === tierFilter;
return matchesSearch && matchesStatus && matchesTier;
});
const getStatusColor = (status: string) => {
switch (status) {
case 'active':
return 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-300';
case 'pending':
return 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-300';
case 'inactive':
return 'bg-danger-100 text-danger-800 dark:bg-danger-900 dark:text-danger-300';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
}
};
const getTierColor = (tier: string) => {
switch (tier) {
case 'platinum':
return 'bg-gradient-to-r from-yellow-400 to-yellow-600 text-white';
case 'gold':
return 'bg-gradient-to-r from-yellow-500 to-yellow-700 text-white';
case 'silver':
return 'bg-gradient-to-r from-gray-400 to-gray-600 text-white';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
}
};
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 handleViewReseller = (reseller: any) => {
setSelectedReseller(reseller);
setIsDetailModalOpen(true);
};
const handleEditReseller = (reseller: any) => {
setSelectedReseller(reseller);
setIsEditModalOpen(true);
};
const handleMailReseller = (reseller: any) => {
setSelectedReseller(reseller);
setIsMailModalOpen(true);
};
const handleMoreOptions = (reseller: any) => {
setShowMoreOptions(showMoreOptions === reseller.id ? null : reseller.id);
};
const handleViewPerformance = (reseller: any) => {
console.log('View performance for:', reseller.name);
alert(`Viewing performance metrics for ${reseller.name}`);
};
const handleDownloadReport = (reseller: any) => {
console.log('Download report for:', reseller.name);
alert(`Downloading report for ${reseller.name}`);
};
const handleSendNotification = (reseller: any) => {
console.log('Send notification to:', reseller.name);
alert(`Sending notification to ${reseller.name}`);
};
const handleChangeTier = (reseller: any) => {
console.log('Change tier for:', reseller.name);
alert(`Changing tier for ${reseller.name}`);
};
const handleDeactivate = (reseller: any) => {
console.log('Deactivate:', reseller.name);
if (window.confirm(`Are you sure you want to deactivate ${reseller.name}?`)) {
alert(`${reseller.name} has been deactivated`);
}
};
const handleDelete = (reseller: any) => {
console.log('Delete:', reseller.name);
if (window.confirm(`Are you sure you want to delete ${reseller.name}? This action cannot be undone.`)) {
alert(`${reseller.name} has been deleted`);
}
};
const handleSendMail = (mailData: any) => {
console.log('Sending mail:', mailData);
alert('Email sent successfully!');
setIsMailModalOpen(false);
};
const handleUpdateReseller = (updatedData: any) => {
console.log('Updating reseller:', updatedData);
alert('Reseller updated successfully!');
setIsEditModalOpen(false);
};
return (
<div className="space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="space-y-2">
<h1 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white tracking-tight">
Resellers
</h1>
<p className="text-gray-600 dark:text-gray-400 text-lg">
Manage your reseller partnerships and performance
</p>
</div>
<button
onClick={() => setIsAddModalOpen(true)}
className="btn btn-primary btn-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-300"
>
<Plus className="w-5 h-5 mr-2" />
Add New Reseller
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="card p-6 hover:shadow-lg transition-all duration-300 group">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide">
Total Resellers
</p>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{mockResellers.length}
</p>
<p className="text-xs text-gray-500 dark:text-gray-500">
Currently active partner companies
</p>
</div>
<div className="w-14 h-14 bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform duration-300">
<Users className="w-7 h-7 text-white" />
</div>
</div>
</div>
<div className="card p-6 hover:shadow-lg transition-all duration-300 group">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide">
Active Resellers
</p>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{mockResellers.filter(r => r.status === 'active').length}
</p>
<p className="text-xs text-gray-500 dark:text-gray-500">
Approved and active partners
</p>
</div>
<div className="w-14 h-14 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform duration-300">
<TrendingUp className="w-7 h-7 text-white" />
</div>
</div>
</div>
<div className="card p-6 hover:shadow-lg transition-all duration-300 group">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide">
Total Revenue
</p>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{formatCurrency(mockResellers.reduce((sum, r) => sum + r.totalRevenue, 0))}
</p>
<p className="text-xs text-gray-500 dark:text-gray-500">
Total sales from all resellers
</p>
</div>
<div className="w-14 h-14 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform duration-300">
<DollarSign className="w-7 h-7 text-white" />
</div>
</div>
</div>
<div className="card p-6 hover:shadow-lg transition-all duration-300 group">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide">
Pending Approvals
</p>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{mockResellers.filter(r => r.status === 'pending').length}
</p>
<p className="text-xs text-gray-500 dark:text-gray-500">
Awaiting admin review
</p>
</div>
<div className="w-14 h-14 bg-gradient-to-br from-orange-500 to-orange-600 rounded-xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform duration-300">
<Calendar className="w-7 h-7 text-white" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="card p-6">
<div className="flex flex-col lg:flex-row gap-4">
<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-gray-400" />
<input
type="text"
placeholder="Search resellers by name or email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pl-10 w-full"
/>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="input min-w-[140px]"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="inactive">Inactive</option>
</select>
<select
value={tierFilter}
onChange={(e) => setTierFilter(e.target.value)}
className="input min-w-[140px]"
>
<option value="all">All Tiers</option>
<option value="platinum">Platinum</option>
<option value="gold">Gold</option>
<option value="silver">Silver</option>
</select>
<button className="btn btn-outline btn-md">
<Filter className="w-4 h-4 mr-2" />
Filters
</button>
</div>
</div>
</div>
{/* Resellers Table */}
<div className="card overflow-hidden">
<div className="p-6 border-b border-gray-200/60 dark:border-gray-700/60 bg-gray-50/30 dark:bg-gray-800/30">
<div className="flex items-center justify-between">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
Reseller Partners
</h3>
<button className="btn btn-outline btn-sm">
<Download className="w-4 h-4 mr-2" />
Export
</button>
</div>
</div>
<div className="overflow-x-auto scrollbar-thin">
<table className="table w-full">
<thead>
<tr>
<th>Reseller</th>
<th>Status</th>
<th>Tier</th>
<th>Revenue</th>
<th>Customers</th>
<th>Commission</th>
<th>Last Active</th>
<th className="text-right">Actions</th>
</tr>
</thead>
<tbody>
{filteredResellers.map((reseller) => (
<tr key={reseller.id} className="hover:bg-gray-50/50 dark:hover:bg-gray-700/50 transition-colors duration-150">
<td>
<div className="flex items-center">
<img
className="w-10 h-10 rounded-full object-cover border-2 border-gray-200 dark:border-gray-700"
src={reseller.avatar}
alt={reseller.name}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
target.nextElementSibling?.classList.remove('hidden');
}}
/>
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-semibold text-sm hidden">
{reseller.name.split(' ').map(n => n[0]).join('')}
</div>
<div className="ml-4">
<div className="text-sm font-semibold text-gray-900 dark:text-white">
{reseller.name}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{reseller.email}
</div>
<div className="flex items-center text-xs text-gray-400 dark:text-gray-500 mt-1">
<MapPin className="w-3 h-3 mr-1" />
{reseller.region}
</div>
</div>
</div>
</td>
<td>
<span className={cn(
"badge",
getStatusColor(reseller.status)
)}>
{reseller.status.charAt(0).toUpperCase() + reseller.status.slice(1)}
</span>
</td>
<td>
<span className={cn(
"badge",
getTierColor(reseller.tier)
)}>
{reseller.tier.charAt(0).toUpperCase() + reseller.tier.slice(1)}
</span>
</td>
<td className="text-sm font-semibold text-gray-900 dark:text-white">
{formatCurrency(reseller.totalRevenue)}
</td>
<td className="text-sm text-gray-900 dark:text-white">
{formatNumber(reseller.customers)}
</td>
<td className="text-sm text-gray-900 dark:text-white">
{reseller.commissionRate}%
</td>
<td className="text-sm text-gray-500 dark:text-gray-400">
{formatDate(reseller.lastActive)}
</td>
<td className="text-right">
<div className="flex items-center justify-end space-x-2 relative">
<button
onClick={() => handleViewReseller(reseller)}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
title="View Details"
>
<Eye className="w-4 h-4 text-gray-600 dark:text-gray-400" />
</button>
<button
onClick={() => handleEditReseller(reseller)}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
title="Edit Reseller"
>
<Edit className="w-4 h-4 text-gray-600 dark:text-gray-400" />
</button>
<button
onClick={() => handleMailReseller(reseller)}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
title="Send Email"
>
<Mail className="w-4 h-4 text-gray-600 dark:text-gray-400" />
</button>
<div className="relative">
<button
onClick={() => handleMoreOptions(reseller)}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
title="More Options"
>
<MoreVertical className="w-4 h-4 text-gray-600 dark:text-gray-400" />
</button>
{showMoreOptions === reseller.id && (
<MoreOptionsDropdown
item={reseller}
itemType="reseller"
onViewPerformance={handleViewPerformance}
onDownloadReport={handleDownloadReport}
onSendNotification={handleSendNotification}
onChangeTier={handleChangeTier}
onDeactivate={handleDeactivate}
onEdit={handleEditReseller}
onMail={handleMailReseller}
onDelete={handleDelete}
onViewDetails={handleViewReseller}
onClose={() => setShowMoreOptions(null)}
/>
)}
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Add Reseller Modal */}
<Modal
isOpen={isAddModalOpen}
onClose={() => setIsAddModalOpen(false)}
title="Add New Reseller"
size="lg"
>
<AddResellerForm
onSubmit={handleAddReseller}
onCancel={() => setIsAddModalOpen(false)}
/>
</Modal>
{/* Reseller Detail Modal */}
<Modal
isOpen={isDetailModalOpen}
onClose={() => setIsDetailModalOpen(false)}
title="Reseller Details"
size="lg"
>
{selectedReseller && (
<DetailView
type="reseller"
data={selectedReseller}
/>
)}
</Modal>
{/* Edit Reseller Modal */}
<Modal
isOpen={isEditModalOpen}
onClose={() => setIsEditModalOpen(false)}
title="Edit Reseller"
size="lg"
>
{selectedReseller && (
<EditResellerForm
reseller={selectedReseller}
onSubmit={handleUpdateReseller}
onCancel={() => setIsEditModalOpen(false)}
/>
)}
</Modal>
{/* Mail Compose Modal */}
<Modal
isOpen={isMailModalOpen}
onClose={() => setIsMailModalOpen(false)}
title="Compose Email"
size="lg"
>
{selectedReseller && (
<MailComposeForm
recipient={selectedReseller}
onSend={handleSendMail}
onCancel={() => setIsMailModalOpen(false)}
/>
)}
</Modal>
</div>
);
};
export default ResellersPage;

392
src/pages/Signup.tsx Normal file
View File

@ -0,0 +1,392 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../store/hooks';
import { loginSuccess } from '../store/slices/authSlice';
import {
Eye,
EyeOff,
Mail,
Lock,
User,
Building,
Phone,
Sun,
Moon,
ArrowRight,
AlertCircle
} from 'lucide-react';
import { useAppSelector } from '../store/hooks';
import { RootState } from '../store';
import { toggleTheme } from '../store/slices/themeSlice';
import { cn } from '../utils/cn';
const Signup: React.FC = () => {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
phone: '',
company: ''
});
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 navigate = useNavigate();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
return;
}
if (!agreedToTerms) {
setError('Please agree to the terms and conditions');
return;
}
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
// Mock signup success
dispatch(loginSuccess({
user: {
id: '1',
email: formData.email,
name: `${formData.firstName} ${formData.lastName}`,
role: 'channel_partner',
company: formData.company,
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face',
tier: 'platinum',
isVerified: true,
twoFactorEnabled: true,
region: 'Global',
commissionRate: 15,
},
token: 'mock-token',
refreshToken: 'mock-refresh-token'
}));
navigate('/');
} catch (err) {
setError('An error occurred during signup. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleThemeToggle = () => {
dispatch(toggleTheme());
};
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/80 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="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl shadow-lg mb-6">
<Building className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Join Channel Partners
</h1>
<p className="text-slate-600 dark:text-slate-400">
Create your channel partner account and start managing resellers.
</p>
</div>
{/* Signup Form */}
<div className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700/20 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">
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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 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>
{/* 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">
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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 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>
{/* Company */}
<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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter company name"
required
/>
</div>
</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">
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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 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>
{/* 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>
{/* 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">
<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 */}
<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",
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>
Creating account...
</div>
) : (
<div className="flex items-center">
Create Account
<ArrowRight className="ml-2 h-4 w-4" />
</div>
)}
</button>
</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">
© 2024 Channel Partners. All rights reserved.
</p>
</div>
</div>
</div>
);
};
export default Signup;

View File

@ -0,0 +1,32 @@
import React from 'react';
const Billing: React.FC = () => {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white">
Billing & Payments
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Manage your billing, invoices, and payment methods
</p>
</div>
<div className="card p-12">
<div className="text-center">
<div className="w-16 h-16 bg-secondary-100 dark:bg-secondary-800 rounded-full flex items-center justify-center mx-auto mb-4">
<div className="w-8 h-8 bg-secondary-400 rounded"></div>
</div>
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-2">
Coming Soon
</h3>
<p className="text-secondary-600 dark:text-secondary-400">
Billing and payment features will be available soon.
</p>
</div>
</div>
</div>
);
};
export default Billing;

View File

@ -0,0 +1,410 @@
import React, { useState } from 'react';
import {
Users,
UserPlus,
Search,
Filter,
MoreVertical,
Mail,
Phone,
MapPin,
Calendar,
DollarSign,
TrendingUp,
CheckCircle,
XCircle,
Clock
} from 'lucide-react';
interface Customer {
id: string;
name: string;
email: string;
phone: string;
company: string;
status: 'active' | 'inactive' | 'pending';
totalSpent: number;
lastPurchase: string;
joinDate: string;
location: string;
avatar: string;
}
const mockCustomers: Customer[] = [
{
id: '1',
name: 'John Smith',
email: 'john.smith@techcorp.com',
phone: '+1 (555) 123-4567',
company: 'TechCorp Solutions',
status: 'active',
totalSpent: 12500,
lastPurchase: '2025-01-15T10:30:00Z',
joinDate: '2024-03-15T00:00:00Z',
location: 'New York, NY',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face'
},
{
id: '2',
name: 'Sarah Johnson',
email: 'sarah.johnson@dataflow.com',
phone: '+1 (555) 234-5678',
company: 'DataFlow Inc',
status: 'active',
totalSpent: 8900,
lastPurchase: '2025-01-14T14:20:00Z',
joinDate: '2024-05-20T00:00:00Z',
location: 'San Francisco, CA',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face'
},
{
id: '3',
name: 'Mike Wilson',
email: 'mike.wilson@cloudtech.com',
phone: '+1 (555) 345-6789',
company: 'CloudTech Ltd',
status: 'pending',
totalSpent: 0,
lastPurchase: '',
joinDate: '2025-01-10T00:00:00Z',
location: 'Austin, TX',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face'
},
{
id: '4',
name: 'Emily Davis',
email: 'emily.davis@innovate.com',
phone: '+1 (555) 456-7890',
company: 'InnovateSoft',
status: 'active',
totalSpent: 15600,
lastPurchase: '2025-01-13T09:15:00Z',
joinDate: '2024-02-10T00:00:00Z',
location: 'Seattle, WA',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face'
},
{
id: '5',
name: 'David Brown',
email: 'david.brown@netsol.com',
phone: '+1 (555) 567-8901',
company: 'NetSolutions',
status: 'inactive',
totalSpent: 3200,
lastPurchase: '2024-11-20T16:45:00Z',
joinDate: '2024-08-15T00:00:00Z',
location: 'Chicago, IL',
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=150&h=150&fit=crop&crop=face'
}
];
const Customers: React.FC = () => {
const [customers, setCustomers] = useState<Customer[]>(mockCustomers);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [sortBy, setSortBy] = useState<string>('name');
const filteredCustomers = customers.filter(customer => {
const matchesSearch = customer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
customer.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
customer.company.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || customer.status === statusFilter;
return matchesSearch && matchesStatus;
});
const sortedCustomers = [...filteredCustomers].sort((a, b) => {
switch (sortBy) {
case 'name':
return a.name.localeCompare(b.name);
case 'company':
return a.company.localeCompare(b.company);
case 'totalSpent':
return b.totalSpent - a.totalSpent;
case 'joinDate':
return new Date(b.joinDate).getTime() - new Date(a.joinDate).getTime();
default:
return 0;
}
});
const getStatusColor = (status: string) => {
switch (status) {
case 'active':
return 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-300';
case 'inactive':
return 'bg-danger-100 text-danger-800 dark:bg-danger-900 dark:text-danger-300';
case 'pending':
return 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-300';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'active':
return <CheckCircle className="w-4 h-4" />;
case 'inactive':
return <XCircle className="w-4 h-4" />;
case 'pending':
return <Clock className="w-4 h-4" />;
default:
return <Clock className="w-4 h-4" />;
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
};
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>
<h1 className="text-2xl sm:text-3xl font-bold text-secondary-900 dark:text-white">
Customer Management
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Manage your customer relationships and accounts
</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">
<UserPlus className="w-4 h-4 mr-2" />
Add Customer
</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>
<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">
{customers.length}
</p>
</div>
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
<Users className="w-6 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>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Active Customers
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{customers.filter(c => c.status === 'active').length}
</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-4 sm:p-6">
<div className="flex items-center justify-between">
<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">
{formatCurrency(customers.reduce((sum, c) => sum + c.totalSpent, 0))}
</p>
</div>
<div className="w-12 h-12 bg-warning-100 dark:bg-warning-900 rounded-full flex items-center justify-center">
<DollarSign className="w-6 h-6 text-warning-600 dark:text-warning-400" />
</div>
</div>
</div>
<div className="card p-4 sm:p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Avg. Customer Value
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{formatCurrency(customers.filter(c => c.totalSpent > 0).reduce((sum, c) => sum + c.totalSpent, 0) / customers.filter(c => c.totalSpent > 0).length || 0)}
</p>
</div>
<div className="w-12 h-12 bg-secondary-100 dark:bg-secondary-900 rounded-full flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-secondary-600 dark:text-secondary-400" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="card p-4 sm:p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<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 customers..."
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"
/>
</div>
</div>
<div className="flex gap-2">
<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="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(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="name">Sort by Name</option>
<option value="company">Sort by Company</option>
<option value="totalSpent">Sort by Revenue</option>
<option value="joinDate">Sort by Join Date</option>
</select>
</div>
</div>
</div>
{/* Customers Table */}
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<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">
Customer
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Company
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Total Spent
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Last Purchase
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider">
Join Date
</th>
<th className="px-6 py-3 text-right 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">
{sortedCustomers.map((customer) => (
<tr key={customer.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-800">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<img
className="h-10 w-10 rounded-full"
src={customer.avatar}
alt={customer.name}
/>
<div className="ml-4">
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{customer.name}
</div>
<div className="text-sm text-secondary-500 dark:text-secondary-400">
{customer.email}
</div>
<div className="text-sm text-secondary-500 dark:text-secondary-400 flex items-center">
<Phone className="w-3 h-3 mr-1" />
{customer.phone}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-secondary-900 dark:text-white">
{customer.company}
</div>
<div className="text-sm text-secondary-500 dark:text-secondary-400 flex items-center">
<MapPin className="w-3 h-3 mr-1" />
{customer.location}
</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 ${getStatusColor(customer.status)}`}>
{getStatusIcon(customer.status)}
<span className="ml-1 capitalize">{customer.status}</span>
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{formatCurrency(customer.totalSpent)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-400">
{customer.lastPurchase ? formatDate(customer.lastPurchase) : 'No purchases'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-400">
{formatDate(customer.joinDate)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button className="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300">
<MoreVertical className="w-4 h-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Pagination */}
<div className="flex items-center justify-between">
<div className="text-sm text-secondary-700 dark:text-secondary-300">
Showing {sortedCustomers.length} of {customers.length} customers
</div>
<div className="flex items-center space-x-2">
<button className="px-3 py-1 text-sm text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300">
Previous
</button>
<span className="px-3 py-1 text-sm text-secondary-900 dark:text-white bg-primary-100 dark:bg-primary-900 rounded">
1
</span>
<button className="px-3 py-1 text-sm text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300">
Next
</button>
</div>
</div>
</div>
);
};
export default Customers;

View File

@ -0,0 +1,323 @@
import React, { useEffect } from 'react';
import { useAppSelector, useAppDispatch } from '../../store/hooks';
import { useNavigate } from 'react-router-dom';
import { setStats, setRecentActivities, setQuickActions } from '../../store/slices/dashboardSlice';
import { loginSuccess } from '../../store/slices/authSlice';
import { mockDashboardStats, mockRecentActivities, mockResellerQuickActions, mockResellerRecentActivities, mockUser } from '../../data/mockData';
import { formatNumber, formatRelativeTime, formatPercentage } from '../../utils/format';
import RevenueChart from '../../components/charts/RevenueChart';
import ResellerPerformanceChart from '../../components/charts/ResellerPerformanceChart';
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
import {
TrendingUp,
Users,
Cloud,
DollarSign,
UserPlus,
CheckCircle,
Briefcase,
GraduationCap,
BarChart3,
CreditCard,
Headphones,
ShoppingBag,
Award,
HelpCircle,
Settings,
Wallet,
BookOpen,
Zap,
Target,
Star,
ArrowUpRight,
Activity
} from 'lucide-react';
import { cn } from '../../utils/cn';
const ResellerDashboard: React.FC = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard);
useEffect(() => {
// Initialize with mock data
dispatch(loginSuccess({
user: {
...mockUser,
role: 'reseller_admin',
company: 'Tech Solutions Inc'
},
token: 'mock-token',
refreshToken: 'mock-refresh-token'
}));
dispatch(setStats(mockDashboardStats));
dispatch(setRecentActivities(mockResellerRecentActivities));
dispatch(setQuickActions(mockResellerQuickActions));
}, [dispatch]);
const handleQuickAction = (action: any) => {
switch (action.id) {
case 'add-customer':
navigate('/reseller-dashboard/customers');
break;
case 'create-instance':
navigate('/reseller-dashboard/instances');
break;
case 'billing':
navigate('/reseller-dashboard/billing');
break;
case 'support':
navigate('/reseller-dashboard/support');
break;
case 'training':
navigate('/reseller-dashboard/training');
break;
case 'reports':
navigate('/reseller-dashboard/reports');
break;
case 'wallet':
navigate('/reseller-dashboard/wallet');
break;
case 'marketplace':
navigate('/reseller-dashboard/marketplace');
break;
case 'certifications':
navigate('/reseller-dashboard/certifications');
break;
case 'knowledge-base':
navigate('/reseller-dashboard/knowledge-base');
break;
case 'settings':
navigate('/reseller-dashboard/settings');
break;
default:
break;
}
};
return (
<div className="space-y-8">
{/* Welcome Section */}
<div className="bg-gradient-to-r from-emerald-600 via-teal-600 to-cyan-600 rounded-3xl p-8 text-white relative overflow-hidden">
<div className="absolute inset-0 bg-black/10"></div>
<div className="relative z-10">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2">Welcome back, John! 👋</h1>
<p className="text-emerald-100 text-lg">Here's what's happening with your cloud services business today.</p>
</div>
<div className="hidden lg:flex items-center space-x-4">
<div className="text-center">
<div className="text-2xl font-bold">{formatNumber(stats.totalResellers)}</div>
<div className="text-emerald-100 text-sm">Active Customers</div>
</div>
<div className="text-center">
<DualCurrencyDisplay
amount={stats.commissionEarned}
currency={stats.currency}
className="text-2xl font-bold text-white"
/>
<div className="text-emerald-100 text-sm">Monthly Revenue</div>
</div>
</div>
</div>
</div>
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -translate-y-32 translate-x-32"></div>
<div className="absolute bottom-0 left-0 w-48 h-48 bg-white/5 rounded-full translate-y-24 -translate-x-24"></div>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300 group">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<DollarSign className="w-6 h-6 text-white" />
</div>
<TrendingUp className="w-5 h-5 text-green-500" />
</div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
<DualCurrencyDisplay
amount={stats.totalRevenue}
currency={stats.currency}
className="text-2xl"
/>
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Total Revenue</p>
<div className="flex items-center mt-2 text-green-600 text-sm">
<ArrowUpRight className="w-4 h-4 mr-1" />
+{stats.monthlyGrowth}% from last month
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300 group">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Users className="w-6 h-6 text-white" />
</div>
<Activity className="w-5 h-5 text-emerald-500" />
</div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
{formatNumber(stats.totalResellers)}
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Active Customers</p>
<div className="flex items-center mt-2 text-emerald-600 text-sm">
<ArrowUpRight className="w-4 h-4 mr-1" />
+5 new this month
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300 group">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-purple-500 to-purple-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Cloud className="w-6 h-6 text-white" />
</div>
<Zap className="w-5 h-5 text-purple-500" />
</div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
{formatNumber(stats.activePartnerships)}
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Cloud Instances</p>
<div className="flex items-center mt-2 text-purple-600 text-sm">
<ArrowUpRight className="w-4 h-4 mr-1" />
+12 new instances
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300 group">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-orange-500 to-orange-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Target className="w-6 h-6 text-white" />
</div>
<Star className="w-5 h-5 text-orange-500" />
</div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
{formatPercentage(15)}
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Commission Rate</p>
<div className="flex items-center mt-2 text-orange-600 text-sm">
<CheckCircle className="w-4 h-4 mr-1" />
Premium tier
</div>
</div>
</div>
{/* Quick Actions & Recent Activity */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Quick Actions */}
<div className="lg:col-span-1">
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
<Zap className="w-5 h-5 mr-2 text-emerald-500" />
Quick Actions
</h3>
<div className="space-y-3">
{quickActions.slice(0, 6).map((action) => (
<button
key={action.id}
onClick={() => handleQuickAction(action)}
className="w-full flex items-center p-4 rounded-xl bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800 hover:from-emerald-50 hover:to-teal-50 dark:hover:from-emerald-900/20 dark:hover:to-teal-900/20 transition-all duration-300 group border border-slate-200 dark:border-slate-600"
>
<div className="w-10 h-10 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center mr-4 group-hover:scale-110 transition-transform duration-300">
{action.icon === 'UserPlus' && <UserPlus className="w-5 h-5 text-white" />}
{action.icon === 'Cloud' && <Cloud className="w-5 h-5 text-white" />}
{action.icon === 'CreditCard' && <CreditCard className="w-5 h-5 text-white" />}
{action.icon === 'Headphones' && <Headphones className="w-5 h-5 text-white" />}
{action.icon === 'GraduationCap' && <GraduationCap className="w-5 h-5 text-white" />}
{action.icon === 'BarChart3' && <BarChart3 className="w-5 h-5 text-white" />}
{action.icon === 'Wallet' && <Wallet className="w-5 h-5 text-white" />}
{action.icon === 'ShoppingBag' && <ShoppingBag className="w-5 h-5 text-white" />}
{action.icon === 'Award' && <Award className="w-5 h-5 text-white" />}
{action.icon === 'HelpCircle' && <HelpCircle className="w-5 h-5 text-white" />}
{action.icon === 'Settings' && <Settings className="w-5 h-5 text-white" />}
{action.icon === 'BookOpen' && <BookOpen className="w-5 h-5 text-white" />}
</div>
<div className="text-left flex-1">
<p className="font-semibold text-slate-900 dark:text-white group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors duration-300">
{action.title}
</p>
<p className="text-sm text-slate-600 dark:text-slate-400">{action.description}</p>
</div>
<ArrowUpRight className="w-4 h-4 text-slate-400 group-hover:text-emerald-500 transition-colors duration-300" />
</button>
))}
</div>
</div>
</div>
{/* Recent Activity */}
<div className="lg:col-span-2">
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
<Activity className="w-5 h-5 mr-2 text-emerald-500" />
Recent Activity
</h3>
<div className="space-y-4">
{recentActivities.map((activity) => (
<div key={activity.id} className="flex items-start p-4 rounded-xl bg-slate-50 dark:bg-slate-700/50 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<div className="w-10 h-10 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center mr-4 flex-shrink-0">
{activity.type === 'customer_added' && <UserPlus className="w-5 h-5 text-white" />}
{activity.type === 'instance_created' && <Cloud className="w-5 h-5 text-white" />}
{activity.type === 'payment_received' && <CreditCard className="w-5 h-5 text-white" />}
{activity.type === 'support_ticket' && <Headphones className="w-5 h-5 text-white" />}
{activity.type === 'training_completed' && <GraduationCap className="w-5 h-5 text-white" />}
</div>
<div className="flex-1 min-w-0">
<p className="font-semibold text-slate-900 dark:text-white mb-1">
{activity.title}
</p>
<p className="text-sm text-slate-600 dark:text-slate-400 mb-2">
{activity.description}
</p>
{activity.amount && (
<div className="mb-2">
<DualCurrencyDisplay
amount={activity.amount}
currency={activity.currency}
className="text-sm font-semibold text-emerald-600"
/>
</div>
)}
<p className="text-xs text-slate-500 dark:text-slate-400">
{formatRelativeTime(activity.timestamp)}
</p>
</div>
</div>
))}
</div>
<div className="mt-6 pt-4 border-t border-slate-200 dark:border-slate-700">
<button className="text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 font-medium flex items-center">
View all activities
<ArrowUpRight className="w-4 h-4 ml-1" />
</button>
</div>
</div>
</div>
</div>
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
<BarChart3 className="w-5 h-5 mr-2 text-emerald-500" />
Revenue Overview
</h3>
<div className="h-64">
<RevenueChart />
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
<Target className="w-5 h-5 mr-2 text-emerald-500" />
Customer Performance
</h3>
<div className="h-64">
<ResellerPerformanceChart />
</div>
</div>
</div>
</div>
);
};
export default ResellerDashboard;

View File

@ -0,0 +1,416 @@
import React, { useState } from 'react';
import {
Cloud,
Plus,
Search,
Filter,
MoreVertical,
Server,
Database,
Globe,
Shield,
Zap,
Clock,
CheckCircle,
XCircle,
AlertTriangle
} from 'lucide-react';
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;
}
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 [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [regionFilter, setRegionFilter] = useState<string>('all');
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;
});
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';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-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" />;
default:
return <Clock 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',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
};
const totalMonthlyCost = instances.reduce((sum, instance) => sum + instance.monthlyCost, 0);
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>
<h1 className="text-2xl sm:text-3xl font-bold text-secondary-900 dark:text-white">
Cloud Instances
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Manage your cloud infrastructure and instances
</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>
{/* 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>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Total Instances
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{instances.length}
</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" />
</div>
</div>
</div>
<div className="card p-4 sm:p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Running Instances
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{instances.filter(i => i.status === 'running').length}
</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-4 sm:p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Monthly Cost
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{formatCurrency(totalMonthlyCost)}
</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" />
</div>
</div>
</div>
<div className="card p-4 sm:p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Active Customers
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{new Set(instances.map(i => i.customer)).size}
</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" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="card p-4 sm:p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<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..."
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"
/>
</div>
</div>
<div className="flex gap-2">
<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>
</select>
<select
value={regionFilter}
onChange={(e) => setRegionFilter(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>
</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>
</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>
</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>
</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>
</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>
{/* Empty State */}
{filteredInstances.length === 0 && (
<div className="card p-12 text-center">
<Cloud 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
</h3>
<p className="text-secondary-600 dark:text-secondary-400 mb-4">
{searchTerm || statusFilter !== 'all' || regionFilter !== 'all'
? 'Try adjusting your filters or search terms.'
: 'Get started by creating your first cloud instance.'
}
</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">
<Plus className="w-4 h-4 mr-2" />
Create Instance
</button>
</div>
)}
</div>
);
};
export default Instances;

View File

@ -0,0 +1,273 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../../store/hooks';
import { loginSuccess } from '../../store/slices/authSlice';
import {
Eye,
EyeOff,
Mail,
Lock,
Users,
Sun,
Moon,
ArrowRight,
AlertCircle
} from 'lucide-react';
import { useAppSelector } from '../../store/hooks';
import { RootState } from '../../store';
import { toggleTheme } from '../../store/slices/themeSlice';
import { cn } from '../../utils/cn';
const ResellerLogin: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [rememberMe, setRememberMe] = useState(false);
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
// Mock login success
dispatch(loginSuccess({
user: {
id: '1',
email: email,
name: 'John Reseller',
role: 'reseller_admin',
company: 'Tech Solutions Inc',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face',
tier: 'gold',
isVerified: true,
twoFactorEnabled: false,
region: 'North America',
commissionRate: 12,
},
token: 'mock-token',
refreshToken: 'mock-refresh-token'
}));
navigate('/reseller');
} catch (err) {
setError('Invalid email or password. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleThemeToggle = () => {
dispatch(toggleTheme());
};
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 via-teal-50 to-cyan-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/80 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-md">
{/* Logo and Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-emerald-600 to-teal-600 rounded-2xl shadow-lg mb-6">
<Users className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Reseller Portal
</h1>
<p className="text-slate-600 dark:text-slate-400">
Welcome back! Please sign in to your account.
</p>
</div>
{/* Login Form */}
<div className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700/20 p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Email Field */}
<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={email}
onChange={(e) => setEmail(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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
placeholder="Enter your email"
required
/>
</div>
</div>
{/* Password Field */}
<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={password}
onChange={(e) => setPassword(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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
placeholder="Enter your 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>
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between">
<label className="flex items-center">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="h-4 w-4 text-emerald-600 focus:ring-emerald-500 border-slate-300 dark:border-slate-600 rounded"
/>
<span className="ml-2 text-sm text-slate-700 dark:text-slate-300">
Remember me
</span>
</label>
<Link
to="/reseller/forgot-password"
className="text-sm text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 font-medium transition-colors duration-200"
>
Forgot password?
</Link>
</div>
{/* 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">
<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 */}
<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-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-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>
Signing in...
</div>
) : (
<div className="flex items-center">
Sign in
<ArrowRight className="ml-2 h-4 w-4" />
</div>
)}
</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">
<p className="text-sm text-slate-600 dark:text-slate-400">
Don't have an account?{' '}
<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>
{/* Switch to Channel Partner */}
<div className="mt-4 text-center">
<p className="text-sm text-slate-600 dark:text-slate-400">
Are you a Channel Partner?{' '}
<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>
</div>
{/* Footer */}
<div className="mt-8 text-center">
<p className="text-xs text-slate-500 dark:text-slate-400">
© 2024 CloudTopiaa Reseller Portal. All rights reserved.
</p>
</div>
</div>
</div>
);
};
export default ResellerLogin;

View File

@ -0,0 +1,32 @@
import React from 'react';
const Reports: React.FC = () => {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white">
Reports & Analytics
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
View detailed reports and analytics
</p>
</div>
<div className="card p-12">
<div className="text-center">
<div className="w-16 h-16 bg-secondary-100 dark:bg-secondary-800 rounded-full flex items-center justify-center mx-auto mb-4">
<div className="w-8 h-8 bg-secondary-400 rounded"></div>
</div>
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-2">
Coming Soon
</h3>
<p className="text-secondary-600 dark:text-secondary-400">
Reports and analytics features will be available soon.
</p>
</div>
</div>
</div>
);
};
export default Reports;

View File

@ -0,0 +1,548 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../../store/hooks';
import { loginSuccess } from '../../store/slices/authSlice';
import {
Eye,
EyeOff,
Mail,
Lock,
User,
Building,
Phone,
Sun,
Moon,
ArrowRight,
AlertCircle,
ChevronDown,
Globe,
Users,
Briefcase
} from 'lucide-react';
import { useAppSelector } from '../../store/hooks';
import { RootState } from '../../store';
import { toggleTheme } from '../../store/slices/themeSlice';
import { cn } from '../../utils/cn';
const ResellerSignup: React.FC = () => {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
phone: '',
company: '',
userType: '' as 'reseller_admin' | 'sales_agent' | 'support_agent' | 'read_only' | '',
region: '',
businessType: ''
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [showUserTypeDropdown, setShowUserTypeDropdown] = useState(false);
const [showRegionDropdown, setShowRegionDropdown] = useState(false);
const [showBusinessTypeDropdown, setShowBusinessTypeDropdown] = useState(false);
const [agreedToTerms, setAgreedToTerms] = useState(false);
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme);
const userTypes = [
{ value: 'reseller_admin', label: 'Reseller Admin', icon: Users, description: 'Sell cloud services to customers' },
{ value: 'sales_agent', label: 'Sales Agent', icon: Building, description: 'Manage sales and customer relationships' },
{ value: 'support_agent', label: 'Support Agent', icon: Briefcase, description: 'Provide customer support services' }
];
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 handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
return;
}
if (!agreedToTerms) {
setError('Please agree to the terms and conditions');
return;
}
if (!formData.userType) {
setError('Please select a user type');
return;
}
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
// Mock signup success
dispatch(loginSuccess({
user: {
id: '1',
email: formData.email,
name: `${formData.firstName} ${formData.lastName}`,
role: formData.userType,
company: formData.company,
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face',
tier: 'silver',
isVerified: false,
twoFactorEnabled: false,
region: formData.region,
commissionRate: 10,
},
token: 'mock-token',
refreshToken: 'mock-refresh-token'
}));
navigate('/reseller');
} catch (err) {
setError('An error occurred during signup. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleThemeToggle = () => {
dispatch(toggleTheme());
};
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 via-teal-50 to-cyan-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/80 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="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-emerald-600 to-teal-600 rounded-2xl shadow-lg mb-6">
<Users className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Join Our Network
</h1>
<p className="text-slate-600 dark:text-slate-400">
Create your reseller account and start your journey with us.
</p>
</div>
{/* Signup Form */}
<div className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm rounded-2xl shadow-2xl border border-white/20 dark:border-slate-700/20 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">
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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
placeholder="Enter last name"
required
/>
</div>
</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">
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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
placeholder="Enter phone number"
required
/>
</div>
</div>
</div>
{/* Company and User Type */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
placeholder="Enter company name"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
I want to become a
</label>
<div className="relative">
<button
type="button"
onClick={() => setShowUserTypeDropdown(!showUserTypeDropdown)}
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-emerald-500 focus:border-transparent transition-all duration-200"
>
<span className={formData.userType ? 'text-slate-900 dark:text-white' : 'text-slate-500 dark:text-slate-400'}>
{formData.userType ? userTypes.find(t => t.value === formData.userType)?.label : 'Select user type'}
</span>
<ChevronDown className="h-5 w-5 text-slate-400" />
</button>
{showUserTypeDropdown && (
<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">
{userTypes.map((type) => (
<button
key={type.value}
type="button"
onClick={() => {
handleInputChange('userType', type.value);
setShowUserTypeDropdown(false);
}}
className="w-full flex items-center p-3 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
>
<type.icon className="h-5 w-5 text-emerald-600 mr-3" />
<div className="text-left">
<div className="font-medium text-slate-900 dark:text-white">{type.label}</div>
<div className="text-sm text-slate-500 dark:text-slate-400">{type.description}</div>
</div>
</button>
))}
</div>
)}
</div>
</div>
</div>
{/* Region and Business Type */}
<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">
Region
</label>
<div className="relative">
<button
type="button"
onClick={() => setShowRegionDropdown(!showRegionDropdown)}
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-emerald-500 focus:border-transparent transition-all duration-200"
>
<div className="flex items-center">
<Globe className="h-5 w-5 text-slate-400 mr-3" />
<span className={formData.region ? 'text-slate-900 dark:text-white' : 'text-slate-500 dark:text-slate-400'}>
{formData.region || 'Select region'}
</span>
</div>
<ChevronDown className="h-5 w-5 text-slate-400" />
</button>
{showRegionDropdown && (
<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">
{regions.map((region) => (
<button
key={region}
type="button"
onClick={() => {
handleInputChange('region', region);
setShowRegionDropdown(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"
>
{region}
</button>
))}
</div>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Business Type
</label>
<div className="relative">
<button
type="button"
onClick={() => setShowBusinessTypeDropdown(!showBusinessTypeDropdown)}
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-emerald-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.businessType ? 'text-slate-900 dark:text-white' : 'text-slate-500 dark:text-slate-400'}>
{formData.businessType || 'Select business type'}
</span>
</div>
<ChevronDown className="h-5 w-5 text-slate-400" />
</button>
{showBusinessTypeDropdown && (
<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">
{businessTypes.map((type) => (
<button
key={type}
type="button"
onClick={() => {
handleInputChange('businessType', type);
setShowBusinessTypeDropdown(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}
</button>
))}
</div>
)}
</div>
</div>
</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">
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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-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 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-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>
{/* 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-emerald-600 focus:ring-emerald-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-emerald-600 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 font-medium">
Terms and Conditions
</Link>{' '}
and{' '}
<Link to="/privacy" className="text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 font-medium">
Privacy Policy
</Link>
</label>
</div>
{/* 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">
<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 */}
<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-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-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>
Creating account...
</div>
) : (
<div className="flex items-center">
Create Account
<ArrowRight className="ml-2 h-4 w-4" />
</div>
)}
</button>
</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="/reseller/login"
className="font-medium text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 transition-colors duration-200"
>
Sign in here
</Link>
</p>
</div>
{/* Switch to Channel Partner */}
<div className="mt-4 text-center">
<p className="text-sm text-slate-600 dark:text-slate-400">
Are you a Channel Partner?{' '}
<Link
to="/signup"
className="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-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">
© 2024 CloudTopiaa Reseller Portal. All rights reserved.
</p>
</div>
</div>
</div>
);
};
export default ResellerSignup;

View File

@ -0,0 +1,32 @@
import React from 'react';
const Support: React.FC = () => {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white">
Support Center
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Get help and submit support tickets
</p>
</div>
<div className="card p-12">
<div className="text-center">
<div className="w-16 h-16 bg-secondary-100 dark:bg-secondary-800 rounded-full flex items-center justify-center mx-auto mb-4">
<div className="w-8 h-8 bg-secondary-400 rounded"></div>
</div>
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-2">
Coming Soon
</h3>
<p className="text-secondary-600 dark:text-secondary-400">
Support center features will be available soon.
</p>
</div>
</div>
</div>
);
};
export default Support;

1
src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

15
src/reportWebVitals.ts Normal file
View File

@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
src/setupTests.ts Normal file
View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

5
src/store/hooks.ts Normal file
View File

@ -0,0 +1,5 @@
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './index';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

17
src/store/index.ts Normal file
View File

@ -0,0 +1,17 @@
import { configureStore } from '@reduxjs/toolkit';
import themeReducer from './slices/themeSlice';
import authReducer from './slices/authSlice';
import dashboardReducer from './slices/dashboardSlice';
import resellerDashboardReducer from './reseller/dashboardSlice';
export const store = configureStore({
reducer: {
theme: themeReducer,
auth: authReducer,
dashboard: dashboardReducer,
resellerDashboard: resellerDashboardReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@ -0,0 +1,60 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ResellerDashboardStats, ResellerRecentActivity, ResellerQuickAction } from '../../data/reseller/mockData';
interface ResellerDashboardState {
stats: ResellerDashboardStats;
recentActivities: ResellerRecentActivity[];
quickActions: ResellerQuickAction[];
isLoading: boolean;
error: string | null;
}
const initialState: ResellerDashboardState = {
stats: {
totalRevenue: 0,
activeCustomers: 0,
cloudInstances: 0,
commissionRate: 0,
monthlyGrowth: 0,
},
recentActivities: [],
quickActions: [],
isLoading: false,
error: null,
};
const resellerDashboardSlice = createSlice({
name: 'resellerDashboard',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
setStats: (state, action: PayloadAction<ResellerDashboardStats>) => {
state.stats = action.payload;
},
setRecentActivities: (state, action: PayloadAction<ResellerRecentActivity[]>) => {
state.recentActivities = action.payload;
},
setQuickActions: (state, action: PayloadAction<ResellerQuickAction[]>) => {
state.quickActions = action.payload;
},
updateStats: (state, action: PayloadAction<Partial<ResellerDashboardStats>>) => {
state.stats = { ...state.stats, ...action.payload };
},
},
});
export const {
setLoading,
setError,
setStats,
setRecentActivities,
setQuickActions,
updateStats
} = resellerDashboardSlice.actions;
export default resellerDashboardSlice.reducer;

View File

@ -0,0 +1,72 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface User {
id: string;
email: string;
name: string;
role: 'channel_partner' | 'sales_manager' | 'account_manager' | 'read_only' | 'reseller_admin' | 'sales_agent' | 'support_agent';
company: string;
avatar?: string;
tier: 'silver' | 'gold' | 'platinum';
isVerified: boolean;
twoFactorEnabled: boolean;
region: string;
commissionRate: number;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
token: string | null;
refreshToken: string | null;
}
const initialState: AuthState = {
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
token: null,
refreshToken: null,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
loginSuccess: (state, action: PayloadAction<{ user: User; token: string; refreshToken: string }>) => {
state.user = action.payload.user;
state.token = action.payload.token;
state.refreshToken = action.payload.refreshToken;
state.isAuthenticated = true;
state.error = null;
},
logout: (state) => {
state.user = null;
state.token = null;
state.refreshToken = null;
state.isAuthenticated = false;
state.error = null;
},
updateUser: (state, action: PayloadAction<Partial<User>>) => {
if (state.user) {
state.user = { ...state.user, ...action.payload };
}
},
setTokens: (state, action: PayloadAction<{ token: string; refreshToken: string }>) => {
state.token = action.payload.token;
state.refreshToken = action.payload.refreshToken;
},
},
});
export const { setLoading, setError, loginSuccess, logout, updateUser, setTokens } = authSlice.actions;
export default authSlice.reducer;

View File

@ -0,0 +1,85 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface DashboardStats {
totalRevenue: number;
totalResellers: number;
activePartnerships: number;
pendingApprovals: number;
monthlyGrowth: number;
commissionEarned: number;
averageDealSize: number;
conversionRate: number;
currency?: 'USD' | 'INR';
}
export interface RecentActivity {
id: string;
type: 'reseller_added' | 'deal_closed' | 'commission_earned' | 'partnership_approved' | 'training_completed' | 'customer_added' | 'instance_created' | 'payment_received' | 'support_ticket';
title: string;
description: string;
timestamp: string;
amount?: number;
currency?: 'USD' | 'INR';
}
export interface QuickAction {
id: string;
title: string;
description: string;
icon: string;
action: string;
color: string;
}
interface DashboardState {
stats: DashboardStats;
recentActivities: RecentActivity[];
quickActions: QuickAction[];
isLoading: boolean;
error: string | null;
}
const initialState: DashboardState = {
stats: {
totalRevenue: 0,
totalResellers: 0,
activePartnerships: 0,
pendingApprovals: 0,
monthlyGrowth: 0,
commissionEarned: 0,
averageDealSize: 0,
conversionRate: 0,
},
recentActivities: [],
quickActions: [],
isLoading: false,
error: null,
};
const dashboardSlice = createSlice({
name: 'dashboard',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
setStats: (state, action: PayloadAction<DashboardStats>) => {
state.stats = action.payload;
},
setRecentActivities: (state, action: PayloadAction<RecentActivity[]>) => {
state.recentActivities = action.payload;
},
setQuickActions: (state, action: PayloadAction<QuickAction[]>) => {
state.quickActions = action.payload;
},
updateStats: (state, action: PayloadAction<Partial<DashboardStats>>) => {
state.stats = { ...state.stats, ...action.payload };
},
},
});
export const { setLoading, setError, setStats, setRecentActivities, setQuickActions, updateStats } = dashboardSlice.actions;
export default dashboardSlice.reducer;

View File

@ -0,0 +1,52 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export type Theme = 'light' | 'dark';
interface ThemeState {
theme: Theme;
systemTheme: Theme;
}
const getInitialTheme = (): Theme => {
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme') as Theme;
if (savedTheme) return savedTheme;
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
return systemTheme;
}
return 'light';
};
const initialState: ThemeState = {
theme: getInitialTheme(),
systemTheme: 'light',
};
const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
setTheme: (state, action: PayloadAction<Theme>) => {
state.theme = action.payload;
if (typeof window !== 'undefined') {
localStorage.setItem('theme', action.payload);
document.documentElement.classList.toggle('dark', action.payload === 'dark');
}
},
setSystemTheme: (state, action: PayloadAction<Theme>) => {
state.systemTheme = action.payload;
},
toggleTheme: (state) => {
const newTheme = state.theme === 'light' ? 'dark' : 'light';
state.theme = newTheme;
if (typeof window !== 'undefined') {
localStorage.setItem('theme', newTheme);
document.documentElement.classList.toggle('dark', newTheme === 'dark');
}
},
},
});
export const { setTheme, setSystemTheme, toggleTheme } = themeSlice.actions;
export default themeSlice.reducer;

6
src/utils/cn.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

159
src/utils/format.ts Normal file
View File

@ -0,0 +1,159 @@
export const formatCurrency = (amount: number, currency: 'USD' | 'INR' = 'INR'): string => {
const options = {
style: 'currency' as const,
currency: currency,
minimumFractionDigits: 0,
maximumFractionDigits: 2,
};
return new Intl.NumberFormat(currency === 'INR' ? 'en-IN' : 'en-US', options).format(amount);
};
export const formatCurrencyDual = (amount: number, currency: 'USD' | 'INR' = 'INR'): { primary: string; secondary: string } => {
if (currency === 'INR') {
const usdAmount = amount / 83; // Approximate conversion rate
return {
primary: new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount),
secondary: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(usdAmount)
};
} else {
const inrAmount = amount * 83; // Approximate conversion rate
return {
primary: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount),
secondary: new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(inrAmount)
};
}
};
export const formatCurrencyDualDisplay = (amount: number, currency: 'USD' | 'INR' = 'INR'): { primary: string; secondary: string } => {
if (currency === 'INR') {
const usdAmount = amount / 83; // Approximate conversion rate
return {
primary: `${new Intl.NumberFormat('en-IN').format(amount)} rs`,
secondary: `<small>${new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(usdAmount)}</small>`
};
} else {
const inrAmount = amount * 83; // Approximate conversion rate
return {
primary: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount),
secondary: `${new Intl.NumberFormat('en-IN').format(inrAmount)} rs`
};
}
};
export const formatCurrencyCompact = (amount: number, currency: 'USD' | 'INR' = 'INR'): string => {
const options = {
style: 'currency' as const,
currency: currency,
notation: 'compact' as const,
minimumFractionDigits: 1,
maximumFractionDigits: 1,
};
return new Intl.NumberFormat(currency === 'INR' ? 'en-IN' : 'en-US', options).format(amount);
};
export const formatNumber = (num: number): string => {
return new Intl.NumberFormat('en-IN').format(num);
};
export const formatDate = (date: string | Date): string => {
try {
const dateObj = new Date(date);
if (isNaN(dateObj.getTime())) {
return 'Invalid Date';
}
return new Intl.DateTimeFormat('en-IN', {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(dateObj);
} catch (error) {
console.error('Error formatting date:', error, 'Date value:', date);
return 'Invalid Date';
}
};
export const formatDateTime = (date: string | Date): string => {
try {
const dateObj = new Date(date);
if (isNaN(dateObj.getTime())) {
return 'Invalid Date';
}
return new Intl.DateTimeFormat('en-IN', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(dateObj);
} catch (error) {
console.error('Error formatting date time:', error, 'Date value:', date);
return 'Invalid Date';
}
};
export const formatRelativeTime = (date: string | Date): string => {
try {
const now = new Date();
const targetDate = new Date(date);
if (isNaN(targetDate.getTime())) {
return 'Invalid Date';
}
const diffInSeconds = Math.floor((now.getTime() - targetDate.getTime()) / 1000);
if (diffInSeconds < 60) return 'Just now';
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)}d ago`;
return formatDate(date);
} catch (error) {
console.error('Error formatting relative time:', error, 'Date value:', date);
return 'Invalid Date';
}
};
export const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
export const formatPercentage = (value: number, decimals = 1): string => {
return `${value.toFixed(decimals)}%`;
};

View File

@ -0,0 +1,91 @@
// Reseller-specific formatting utilities
export const formatResellerCurrency = (amount: number, currency: 'USD' | 'INR' = 'INR'): string => {
if (currency === 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
} else {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
}
};
export const formatResellerNumber = (num: number): string => {
return new Intl.NumberFormat('en-IN').format(num);
};
export const formatResellerPercentage = (value: number): string => {
return `${value.toFixed(1)}%`;
};
export const formatResellerDate = (dateString: string): string => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
day: 'numeric',
month: 'short',
year: 'numeric',
}).format(date);
};
export const formatResellerRelativeTime = (dateString: string): string => {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (diffInSeconds < 60) {
return 'Just now';
} else if (diffInSeconds < 3600) {
const minutes = Math.floor(diffInSeconds / 60);
return `${minutes}m ago`;
} else if (diffInSeconds < 86400) {
const hours = Math.floor(diffInSeconds / 3600);
return `${hours}h ago`;
} else if (diffInSeconds < 2592000) {
const days = Math.floor(diffInSeconds / 86400);
return `${days}d ago`;
} else {
return formatResellerDate(dateString);
}
};
export const getResellerActivityIcon = (type: string): string => {
switch (type) {
case 'customer_added':
return 'UserPlus';
case 'instance_created':
return 'Cloud';
case 'payment_received':
return 'CreditCard';
case 'support_ticket':
return 'Headphones';
case 'training_completed':
return 'GraduationCap';
default:
return 'Activity';
}
};
export const getResellerQuickActionColor = (color: string): string => {
switch (color) {
case 'primary':
return 'from-emerald-500 to-teal-500';
case 'success':
return 'from-green-500 to-emerald-500';
case 'warning':
return 'from-yellow-500 to-orange-500';
case 'danger':
return 'from-red-500 to-pink-500';
case 'secondary':
return 'from-slate-500 to-gray-500';
default:
return 'from-emerald-500 to-teal-500';
}
};

View File

@ -0,0 +1,2 @@
// Reseller Utils Index
export * from './format';

123
tailwind.config.js Normal file
View File

@ -0,0 +1,123 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
// Light theme colors
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
},
secondary: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
},
success: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
},
warning: {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
},
danger: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
// Dark theme colors
dark: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
}
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
animation: {
'fade-in': 'fadeIn 0.5s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
'scale-in': 'scaleIn 0.2s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
slideDown: {
'0%': { transform: 'translateY(-10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
scaleIn: {
'0%': { transform: 'scale(0.95)', opacity: '0' },
'100%': { transform: 'scale(1)', opacity: '1' },
},
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}

26
tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}