initial commit
This commit is contained in:
commit
fcc9942ea1
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal 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
156
README.md
Normal 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
18475
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
61
package.json
Normal file
61
package.json
Normal 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
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
public/index.html
Normal file
43
public/index.html
Normal 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
BIN
public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal 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
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
38
src/App.css
Normal file
38
src/App.css
Normal 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
9
src/App.test.tsx
Normal 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
310
src/App.tsx
Normal 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;
|
||||
160
src/components/CookieConsent.tsx
Normal file
160
src/components/CookieConsent.tsx
Normal 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;
|
||||
359
src/components/DetailView.tsx
Normal file
359
src/components/DetailView.tsx
Normal 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;
|
||||
34
src/components/DualCurrencyDisplay.tsx
Normal file
34
src/components/DualCurrencyDisplay.tsx
Normal 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;
|
||||
21
src/components/Layout/Layout.tsx
Normal file
21
src/components/Layout/Layout.tsx
Normal 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;
|
||||
187
src/components/Layout/Sidebar.tsx
Normal file
187
src/components/Layout/Sidebar.tsx
Normal 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
76
src/components/Modal.tsx
Normal 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;
|
||||
239
src/components/MoreOptionsDropdown.tsx
Normal file
239
src/components/MoreOptionsDropdown.tsx
Normal 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;
|
||||
161
src/components/charts/CommissionTrendsChart.tsx
Normal file
161
src/components/charts/CommissionTrendsChart.tsx
Normal 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;
|
||||
68
src/components/charts/ResellerPerformanceChart.tsx
Normal file
68
src/components/charts/ResellerPerformanceChart.tsx
Normal 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;
|
||||
120
src/components/charts/RevenueChart.tsx
Normal file
120
src/components/charts/RevenueChart.tsx
Normal 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;
|
||||
344
src/components/forms/AddDealForm.tsx
Normal file
344
src/components/forms/AddDealForm.tsx
Normal 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;
|
||||
346
src/components/forms/AddPartnershipForm.tsx
Normal file
346
src/components/forms/AddPartnershipForm.tsx
Normal 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;
|
||||
302
src/components/forms/AddResellerForm.tsx
Normal file
302
src/components/forms/AddResellerForm.tsx
Normal 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;
|
||||
248
src/components/forms/EditResellerForm.tsx
Normal file
248
src/components/forms/EditResellerForm.tsx
Normal 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;
|
||||
206
src/components/forms/MailComposeForm.tsx
Normal file
206
src/components/forms/MailComposeForm.tsx
Normal 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;
|
||||
30
src/components/reseller/README.md
Normal file
30
src/components/reseller/README.md
Normal 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
|
||||
3
src/components/reseller/index.ts
Normal file
3
src/components/reseller/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Reseller Components Index
|
||||
export { default as ResellerLayout } from './layout/ResellerLayout';
|
||||
export { default as ResellerSidebar } from './layout/ResellerSidebar';
|
||||
87
src/components/reseller/layout/ResellerLayout.tsx
Normal file
87
src/components/reseller/layout/ResellerLayout.tsx
Normal 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;
|
||||
162
src/components/reseller/layout/ResellerSidebar.tsx
Normal file
162
src/components/reseller/layout/ResellerSidebar.tsx
Normal 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
471
src/data/mockData.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
2
src/data/reseller/index.ts
Normal file
2
src/data/reseller/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// Reseller Data Index
|
||||
export * from './mockData';
|
||||
145
src/data/reseller/mockData.ts
Normal file
145
src/data/reseller/mockData.ts
Normal 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
225
src/index.css
Normal 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
19
src/index.tsx
Normal 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
1
src/logo.svg
Normal 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 |
439
src/pages/Commissions/index.tsx
Normal file
439
src/pages/Commissions/index.tsx
Normal 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
407
src/pages/Dashboard.tsx
Normal 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
490
src/pages/Deals/index.tsx
Normal 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
261
src/pages/Login.tsx
Normal 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;
|
||||
466
src/pages/Partnerships/index.tsx
Normal file
466
src/pages/Partnerships/index.tsx
Normal 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;
|
||||
552
src/pages/ProductManagement.tsx
Normal file
552
src/pages/ProductManagement.tsx
Normal 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;
|
||||
501
src/pages/Resellers/index.tsx
Normal file
501
src/pages/Resellers/index.tsx
Normal 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
392
src/pages/Signup.tsx
Normal 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;
|
||||
32
src/pages/reseller/Billing.tsx
Normal file
32
src/pages/reseller/Billing.tsx
Normal 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;
|
||||
410
src/pages/reseller/Customers.tsx
Normal file
410
src/pages/reseller/Customers.tsx
Normal 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;
|
||||
323
src/pages/reseller/Dashboard.tsx
Normal file
323
src/pages/reseller/Dashboard.tsx
Normal 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;
|
||||
416
src/pages/reseller/Instances.tsx
Normal file
416
src/pages/reseller/Instances.tsx
Normal 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;
|
||||
273
src/pages/reseller/Login.tsx
Normal file
273
src/pages/reseller/Login.tsx
Normal 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;
|
||||
32
src/pages/reseller/Reports.tsx
Normal file
32
src/pages/reseller/Reports.tsx
Normal 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;
|
||||
548
src/pages/reseller/Signup.tsx
Normal file
548
src/pages/reseller/Signup.tsx
Normal 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;
|
||||
32
src/pages/reseller/Support.tsx
Normal file
32
src/pages/reseller/Support.tsx
Normal 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
1
src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
15
src/reportWebVitals.ts
Normal file
15
src/reportWebVitals.ts
Normal 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
5
src/setupTests.ts
Normal 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
5
src/store/hooks.ts
Normal 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
17
src/store/index.ts
Normal 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;
|
||||
60
src/store/reseller/dashboardSlice.ts
Normal file
60
src/store/reseller/dashboardSlice.ts
Normal 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;
|
||||
72
src/store/slices/authSlice.ts
Normal file
72
src/store/slices/authSlice.ts
Normal 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;
|
||||
85
src/store/slices/dashboardSlice.ts
Normal file
85
src/store/slices/dashboardSlice.ts
Normal 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;
|
||||
52
src/store/slices/themeSlice.ts
Normal file
52
src/store/slices/themeSlice.ts
Normal 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
6
src/utils/cn.ts
Normal 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
159
src/utils/format.ts
Normal 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)}%`;
|
||||
};
|
||||
91
src/utils/reseller/format.ts
Normal file
91
src/utils/reseller/format.ts
Normal 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';
|
||||
}
|
||||
};
|
||||
2
src/utils/reseller/index.ts
Normal file
2
src/utils/reseller/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// Reseller Utils Index
|
||||
export * from './format';
|
||||
123
tailwind.config.js
Normal file
123
tailwind.config.js
Normal 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
26
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user