initial commit

This commit is contained in:
rohit 2025-08-03 18:49:28 +05:30
parent 7aab857c84
commit f42148ac9b
37 changed files with 6637 additions and 99 deletions

289
README.md
View File

@ -1,46 +1,283 @@
# Getting Started with Create React App # Cloudtopiaa Reseller Dashboard
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). A comprehensive dashboard for resellers to manage their customers, track commissions, and monitor business performance in the Cloudtopiaa ecosystem.
## Available Scripts ## 🏢 **Project Overview**
In the project directory, you can run: The Reseller Dashboard is designed for individual reseller companies to manage their customer relationships, track their performance, and monitor their commissions earned through the Cloudtopiaa partnership program.
### `npm start` ## 🎯 **Key Features**
Runs the app in the development mode.\ ### **Dashboard Metrics & Analytics**
Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - **Real-time Performance Tracking**
- **Dual Currency Support** (INR & USD)
- **Customer Management**
- **Revenue Analytics**
The page will reload if you make edits.\ ### **Customer Management**
You will also see any lint errors in the console. - **Customer Onboarding**
- **Instance Management**
- **Payment Tracking**
- **Support Ticket System**
### `npm test` ### **Business Operations**
- **Revenue Tracking**
- **Commission Monitoring**
- **Invoice Management**
- **Performance Analytics**
Launches the test runner in the interactive watch mode.\ ### **User Experience**
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - **Dark/Light Theme Support**
- **Responsive Design**
- **Smooth Animations & Transitions**
- **Cookie Consent Management**
### `npm run build` ## 📊 **Key Metrics Explained**
Builds the app for production to the `build` folder.\ ### **Total Revenue (₹28,47,500)**
It correctly bundles React in production mode and optimizes the build for the best performance. **What it means:** The total revenue generated from all customer subscriptions and services.
The build is minified and the filenames include the hashes.\ **What it tells you:** Your overall business performance and growth trajectory.
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. ### **Total Customers (156)**
**What it means:** The number of active customers you're currently serving.
### `npm run eject` **What it tells you:** Your customer base size and market reach.
**Note: this is a one-way operation. Once you `eject`, you cant go back!** ### **Active Instances (89)**
**What it means:** The number of active cloud service instances currently running for your customers.
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. **What it tells you:** Your service utilization and customer engagement levels.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own. ### **Pending Invoices (12)**
**What it means:** The number of invoices that are awaiting payment from customers.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it. **What it tells you:** Your accounts receivable status and cash flow situation.
## Learn More ## 🚀 **Business Intelligence Insights**
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). ### **Revenue Growth (23.5%)**
- **Positive growth** indicates expanding business
- **Monthly comparison** shows performance trends
- **Helps identify** successful strategies
To learn React, check out the [React documentation](https://reactjs.org/). ### **Customer Acquisition**
- **156 total customers** shows market penetration
- **89 active instances** indicates service utilization
- **12 pending invoices** requires attention for cash flow
### **Performance Indicators**
- **High customer count** with good revenue suggests strong market position
- **Active instances** show customer engagement
- **Pending invoices** need follow-up for timely payments
## 🛠 **Technical Stack**
### **Frontend**
- **React 18** with TypeScript
- **Redux Toolkit** for state management
- **React Router DOM** for routing
- **Tailwind CSS** for styling
- **Lucide React** for icons
- **Recharts** for data visualization
### **UI/UX Features**
- **Responsive Design** (Mobile-first approach)
- **Dark/Light Theme** with system preference detection
- **Smooth Animations** and transitions
- **Cookie Consent** management
- **Dual Currency** display (INR & USD)
### **Development Tools**
- **PostCSS** with Autoprefixer
- **TypeScript** for type safety
- **ESLint** for code quality
- **Create React App** setup
## 📁 **Project Structure**
```
reseller-dashboard/
├── public/
├── src/
│ ├── components/
│ │ ├── Layout/
│ │ │ ├── Layout.tsx
│ │ │ └── Sidebar.tsx
│ │ └── CookieConsent.tsx
│ ├── pages/
│ │ ├── Dashboard.tsx
│ │ └── PlaceholderPage.tsx
│ ├── store/
│ │ ├── slices/
│ │ │ ├── authSlice.ts
│ │ │ ├── dashboardSlice.ts
│ │ │ └── themeSlice.ts
│ │ ├── hooks.ts
│ │ └── index.ts
│ ├── utils/
│ │ ├── cn.ts
│ │ └── format.ts
│ ├── data/
│ │ └── mockData.ts
│ ├── App.tsx
│ └── index.tsx
├── package.json
├── tailwind.config.js
└── README.md
```
## 🚀 **Getting Started**
### **Prerequisites**
- Node.js (v16 or higher)
- npm or yarn
### **Installation**
1. **Clone the repository**
```bash
git clone <repository-url>
cd reseller-dashboard
```
2. **Install dependencies**
```bash
npm install
```
3. **Start the development server**
```bash
npm start
```
4. **Open your browser**
Navigate to `http://localhost:3000`
### **Available Scripts**
- `npm start` - Start development server
- `npm build` - Build for production
- `npm test` - Run tests
- `npm eject` - Eject from Create React App
## 🎨 **Theme Configuration**
The dashboard supports both light and dark themes with automatic system preference detection.
### **Theme Features**
- **Automatic Detection** of system theme preference
- **Manual Toggle** via sidebar button
- **Persistent Storage** of user preference
- **Smooth Transitions** between themes
### **Color Scheme**
- **Primary:** Blue gradient (#0EA5E9 to #0284C7)
- **Success:** Green (#10B981)
- **Warning:** Yellow (#F59E0B)
- **Danger:** Red (#EF4444)
- **Secondary:** Gray scale
## 🔐 **Authentication & Security**
### **Features**
- **JWT Token** management
- **Refresh Token** support
- **Session Management**
- **Secure Logout**
### **User Roles**
- **Reseller Admin** (Current implementation)
- **Reseller Staff** (Future implementation)
- **Customer Support** (Future implementation)
## 🌐 **Internationalization**
### **Currency Support**
- **INR (₹)** - Indian Rupees
- **USD ($)** - US Dollars
- **Automatic Conversion** (1 USD ≈ 83 INR)
- **Dual Display** with primary currency prominent
### **Formatting**
- **Number formatting** with locale support
- **Date formatting** with relative time
- **Currency formatting** with proper symbols
## 📱 **Responsive Design**
### **Breakpoints**
- **Mobile:** < 640px
- **Tablet:** 640px - 1024px
- **Desktop:** > 1024px
### **Features**
- **Mobile-first** approach
- **Touch-friendly** interface
- **Collapsible sidebar** for mobile
- **Optimized layouts** for all screen sizes
## 🔧 **Development Guidelines**
### **Code Style**
- **TypeScript** for type safety
- **ESLint** configuration
- **Prettier** formatting
- **Component-based** architecture
### **State Management**
- **Redux Toolkit** for global state
- **Local state** for component-specific data
- **Persistent storage** for user preferences
### **Performance**
- **Lazy loading** for routes
- **Optimized images** and assets
- **Efficient re-renders** with React.memo
- **Bundle optimization**
## 🚀 **Deployment**
### **Build for Production**
```bash
npm run build
```
### **Deployment Options**
- **Netlify** - Static site hosting
- **Vercel** - React app deployment
- **AWS S3** - Static website hosting
- **Docker** - Containerized deployment
## 📞 **Support & Documentation**
### **Additional Resources**
- **API Documentation** (Coming soon)
- **User Guide** (Coming soon)
- **Developer Documentation** (Coming soon)
### **Contact**
For support and questions, please contact the development team.
---
## 🎯 **Business Impact**
This Reseller Dashboard provides:
1. **Customer Management** - Manage all customer relationships efficiently
2. **Revenue Tracking** - Monitor income and growth patterns
3. **Commission Monitoring** - Track earnings from Cloudtopiaa partnership
4. **Performance Analytics** - Data-driven business decisions
5. **Operational Efficiency** - Streamlined customer service processes
---
## 🔗 **Related Projects**
- **[Channel Partner Dashboard](../channel-partner-dashboard/)** - Admin panel for managing resellers
- **[Backend API](../backend/)** - Server-side implementation (Coming soon)
---
**Built with ❤️ for Cloudtopiaa's Reseller Program**

930
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,10 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "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/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.4", "@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
@ -11,9 +15,16 @@
"@types/node": "^16.18.126", "@types/node": "^16.18.126",
"@types/react": "^19.1.9", "@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7", "@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": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-redux": "^9.2.0",
"react-router-dom": "^6.30.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"recharts": "^3.1.0",
"tailwind-merge": "^3.3.1",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
@ -40,5 +51,10 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"tailwindcss": "^3.4.0"
} }
} }

6
postcss.config.js Normal file
View File

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

View File

@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="Cloudtopiaa Reseller Portal - Manage your cloud services and customers"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <!--
@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL. 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`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>Reseller</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,6 +1,6 @@
{ {
"short_name": "React App", "short_name": "Reseller",
"name": "Create React App Sample", "name": "Cloudtopiaa Reseller Portal",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",

View File

@ -1,25 +1,109 @@
import React from 'react'; import React, { useEffect } from 'react';
import logo from './logo.svg'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import './App.css'; import { Provider } from 'react-redux';
import { store } from './store';
import { setTheme } from './store/slices/themeSlice';
import Layout from './components/Layout/Layout';
import Dashboard from './pages/Dashboard';
import Customers from './pages/Customers';
import Instances from './pages/Instances';
import Billing from './pages/Billing';
import Support from './pages/Support';
import Reports from './pages/Reports';
import PlaceholderPage from './components/PlaceholderPage';
import {
Wallet,
BookOpen,
ShoppingBag,
Award,
HelpCircle,
Settings
} from 'lucide-react';
import './index.css';
function App() { 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 ( return (
<div className="App"> <Provider store={store}>
<header className="App-header"> <Router>
<img src={logo} className="App-logo" alt="logo" /> <div className="App">
<p> <Layout>
Edit <code>src/App.tsx</code> and save to reload. <Routes>
</p> <Route path="/" element={<Dashboard />} />
<a <Route path="/customers" element={<Customers />} />
className="App-link" <Route path="/instances" element={<Instances />} />
href="https://reactjs.org" <Route path="/billing" element={<Billing />} />
target="_blank" <Route path="/support" element={<Support />} />
rel="noopener noreferrer" <Route path="/reports" element={<Reports />} />
> <Route path="/wallet" element={
Learn React <PlaceholderPage
</a> title="Wallet Management"
</header> description="Manage your funds, transactions, and payment methods"
</div> icon={Wallet}
/>
} />
<Route path="/training" element={
<PlaceholderPage
title="Training Center"
description="Access training materials, courses, and certifications"
icon={BookOpen}
/>
} />
<Route path="/marketplace" element={
<PlaceholderPage
title="Marketplace"
description="Browse and purchase cloud services and solutions"
icon={ShoppingBag}
/>
} />
<Route path="/certifications" element={
<PlaceholderPage
title="Certifications"
description="Manage your professional certifications and achievements"
icon={Award}
/>
} />
<Route path="/knowledge-base" element={
<PlaceholderPage
title="Knowledge Base"
description="Access documentation, guides, and helpful resources"
icon={HelpCircle}
/>
} />
<Route path="/settings" element={
<PlaceholderPage
title="Settings"
description="Configure your account preferences and system settings"
icon={Settings}
/>
} />
<Route path="*" element={<Dashboard />} />
</Routes>
</Layout>
</div>
</Router>
</Provider>
); );
} }

View File

@ -0,0 +1,286 @@
import React from 'react';
import { User, Mail, Phone, Building, MapPin, DollarSign, Award, Calendar, TrendingUp, Users, Server, Headphones, Clock, AlertCircle } from 'lucide-react';
import { formatDate } from '../utils/format';
import DualCurrencyDisplay from './DualCurrencyDisplay';
interface DetailViewProps {
type: 'customer' | 'instance' | 'invoice' | 'ticket';
data: any;
}
const DetailView: React.FC<DetailViewProps> = ({ type, data }) => {
if (!data) {
return (
<div className="text-center py-8">
<p className="text-slate-500">No data available</p>
</div>
);
}
const renderCustomerDetails = () => (
<div className="space-y-8">
{/* Enhanced Header */}
<div className="relative overflow-hidden rounded-xl bg-gradient-to-r from-emerald-50 to-emerald-100 dark:from-emerald-900/20 dark:to-emerald-800/20 p-6 border border-emerald-200/50 dark:border-emerald-700/50">
<div className="flex items-center space-x-6">
<div className="relative">
<img
src={data.avatar}
alt={data.name}
className="w-20 h-20 rounded-full object-cover border-4 border-white dark:border-slate-800 shadow-lg"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
target.nextElementSibling?.classList.remove('hidden');
}}
/>
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center border-4 border-white dark:border-slate-800 shadow-lg hidden">
<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-slate-800"></div>
</div>
<div className="flex-1">
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">{data.name}</h3>
<p className="text-lg text-slate-600 dark:text-slate-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 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="card-elevated p-6">
<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-slate-500 dark:text-slate-400 uppercase tracking-wide">Email</p>
<p className="text-base font-semibold text-slate-900 dark:text-white">{data.email}</p>
</div>
</div>
</div>
<div className="card-elevated p-6">
<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-slate-500 dark:text-slate-400 uppercase tracking-wide">Phone</p>
<p className="text-base font-semibold text-slate-900 dark:text-white">{data.phone}</p>
</div>
</div>
</div>
</div>
{/* Performance Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="card-elevated p-6">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-emerald-100 dark:bg-emerald-900/50 rounded-lg flex items-center justify-center">
<DollarSign className="w-6 h-6 text-emerald-600 dark:text-emerald-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">Total Spent</p>
<DualCurrencyDisplay
amount={data.totalSpent || 0}
currency="INR"
className="text-lg font-bold"
/>
</div>
</div>
</div>
<div className="card-elevated p-6">
<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">
<Server className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">Active Instances</p>
<p className="text-lg font-bold text-slate-900 dark:text-white">{data.activeInstances || 0}</p>
</div>
</div>
</div>
<div className="card-elevated p-6">
<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">
<TrendingUp className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">Last Activity</p>
<p className="text-lg font-bold text-slate-900 dark:text-white">{formatDate(data.lastActivity || new Date())}</p>
</div>
</div>
</div>
</div>
</div>
);
const renderTicketDetails = () => (
<div className="space-y-8">
{/* Enhanced Header */}
<div className="relative overflow-hidden rounded-xl bg-gradient-to-r from-amber-50 to-amber-100 dark:from-amber-900/20 dark:to-amber-800/20 p-6 border border-amber-200/50 dark:border-amber-700/50">
<div className="flex items-center space-x-6">
<div className="relative">
<div className="w-20 h-20 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center border-4 border-white dark:border-slate-800 shadow-lg">
<Headphones className="w-10 h-10 text-white" />
</div>
</div>
<div className="flex-1">
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">{data.id}</h3>
<p className="text-lg text-slate-600 dark:text-slate-400 mb-2">{data.subject}</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.priority === 'high' ? 'bg-gradient-to-r from-red-400 to-red-500 text-white shadow-md' :
data.priority === 'medium' ? 'bg-gradient-to-r from-amber-400 to-amber-500 text-white shadow-md' :
'bg-gradient-to-r from-green-400 to-green-500 text-white shadow-md'
}`}>
{data.priority.charAt(0).toUpperCase() + data.priority.slice(1)} Priority
</span>
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
data.status === 'open' ? 'bg-gradient-to-r from-amber-400 to-amber-500 text-white shadow-md' :
data.status === 'in_progress' ? 'bg-gradient-to-r from-blue-400 to-blue-500 text-white shadow-md' :
data.status === 'resolved' ? 'bg-gradient-to-r from-green-400 to-green-500 text-white shadow-md' :
'bg-gradient-to-r from-gray-400 to-gray-500 text-white shadow-md'
}`}>
{data.status.replace('_', ' ').charAt(0).toUpperCase() + data.status.replace('_', ' ').slice(1)}
</span>
</div>
</div>
</div>
</div>
{/* Ticket Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="card-elevated p-6">
<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">
<User className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">Customer</p>
<p className="text-base font-semibold text-slate-900 dark:text-white">{data.customer}</p>
</div>
</div>
</div>
<div className="card-elevated p-6">
<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">
<User className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">Assigned To</p>
<p className="text-base font-semibold text-slate-900 dark:text-white">{data.assignedTo}</p>
</div>
</div>
</div>
</div>
{/* Description */}
<div className="card-elevated p-6">
<h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Description</h4>
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">{data.description}</p>
</div>
{/* Ticket Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="card-elevated p-6">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-emerald-100 dark:bg-emerald-900/50 rounded-lg flex items-center justify-center">
<Calendar className="w-6 h-6 text-emerald-600 dark:text-emerald-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">Created</p>
<p className="text-lg font-bold text-slate-900 dark:text-white">{formatDate(data.createdAt)}</p>
</div>
</div>
</div>
<div className="card-elevated p-6">
<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">
<Clock className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">Response Time</p>
<p className="text-lg font-bold text-slate-900 dark:text-white">{data.responseTime || '2 hours'}</p>
</div>
</div>
</div>
<div className="card-elevated p-6">
<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">
<AlertCircle className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">Category</p>
<p className="text-lg font-bold text-slate-900 dark:text-white">{data.category || 'Technical'}</p>
</div>
</div>
</div>
</div>
</div>
);
const renderContent = () => {
switch (type) {
case 'customer':
return renderCustomerDetails();
case 'instance':
return (
<div className="space-y-8">
<div className="text-center py-8">
<Server className="w-16 h-16 text-slate-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">Instance Details</h3>
<p className="text-slate-600 dark:text-slate-400">Instance details will be displayed here</p>
</div>
</div>
);
case 'invoice':
return (
<div className="space-y-8">
<div className="text-center py-8">
<DollarSign className="w-16 h-16 text-slate-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">Invoice Details</h3>
<p className="text-slate-600 dark:text-slate-400">Invoice details will be displayed here</p>
</div>
</div>
);
case 'ticket':
return renderTicketDetails();
default:
return (
<div className="text-center py-8">
<p className="text-slate-500">Unknown type</p>
</div>
);
}
};
return (
<div className="max-w-4xl mx-auto">
{renderContent()}
</div>
);
};
export default DetailView;

View File

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

View File

@ -0,0 +1,19 @@
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-gradient-to-br from-slate-50 via-white to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-900/20 overflow-hidden">
<Sidebar />
<main className="flex-1 overflow-y-auto overflow-x-hidden min-w-0">
{children}
</main>
</div>
);
};
export default Layout;

View File

@ -0,0 +1,189 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useAppSelector, useAppDispatch } from '../../store/hooks';
import {
Home,
Users,
Server,
FileText,
Headphones,
BarChart3,
Settings,
Wallet,
BookOpen,
ShoppingBag,
Award,
HelpCircle,
Menu,
X,
Sun,
Moon,
LogOut,
Building
} 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: 'Customers', href: '/customers', icon: Users },
{ name: 'Instances', href: '/instances', icon: Server },
{ name: 'Billing', href: '/billing', icon: FileText },
{ name: 'Support', href: '/support', icon: Headphones },
{ name: 'Reports', href: '/reports', icon: BarChart3 },
{ name: 'Wallet', href: '/wallet', icon: Wallet },
{ name: 'Training', href: '/training', icon: BookOpen },
{ 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/95 dark:bg-slate-800/95 backdrop-blur-xl border-r border-slate-200/60 dark:border-slate-700/60 transition-all duration-300 shadow-xl",
isCollapsed ? "w-16" : "w-64"
)}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-slate-200/60 dark:border-slate-700/60">
{!isCollapsed && (
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-xl flex items-center justify-center shadow-lg">
<Building className="w-5 h-5 text-white" />
</div>
<span className="text-lg font-semibold text-slate-900 dark:text-white">
Reseller
</span>
</div>
)}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="p-1 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
>
{isCollapsed ? (
<Menu className="w-5 h-5 text-slate-600 dark:text-slate-400" />
) : (
<X className="w-5 h-5 text-slate-600 dark:text-slate-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-xl transition-all duration-200 group hover:scale-105",
isActive
? "bg-gradient-to-r from-emerald-500/20 to-emerald-600/20 text-emerald-700 dark:text-emerald-300 border border-emerald-200/50 dark:border-emerald-700/50"
: "text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-700 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-slate-200/60 dark:border-slate-700/60 p-4">
{/* Theme Toggle */}
<div className="flex items-center justify-between mb-4">
{!isCollapsed && (
<span className="text-sm text-slate-600 dark:text-slate-400">
Theme
</span>
)}
<button
onClick={() => dispatch(toggleTheme())}
className="p-2 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-700 transition-all duration-200 hover:scale-105"
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{theme === 'dark' ? (
<Sun className="w-5 h-5 text-slate-600 dark:text-slate-400" />
) : (
<Moon className="w-5 h-5 text-slate-600 dark:text-slate-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-slate-200 dark:bg-slate-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-emerald-100 dark:bg-emerald-900 flex items-center justify-center hidden">
<span className="text-xs font-medium text-emerald-600 dark:text-emerald-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-slate-900 dark:text-white truncate">
{user.name}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 truncate">
{user.company}
</p>
<div className="flex items-center mt-1">
<span className={cn(
"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium shadow-sm",
user.tier === 'platinum' && "bg-gradient-to-r from-yellow-400 to-yellow-600 text-white",
user.tier === 'gold' && "bg-gradient-to-r from-yellow-500 to-yellow-700 text-white",
user.tier === 'silver' && "bg-gradient-to-r from-gray-400 to-gray-600 text-white"
)}>
{user.tier.charAt(0).toUpperCase() + user.tier.slice(1)}
</span>
</div>
</div>
)}
<button
onClick={handleLogout}
className="p-1 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-700 transition-all duration-200 hover:scale-105"
title="Logout"
>
<LogOut className="w-4 h-4 text-slate-600 dark:text-slate-400" />
</button>
</div>
)}
</div>
</div>
);
};
export default Sidebar;

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

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

View File

@ -0,0 +1,118 @@
import React, { useEffect, useRef } from 'react';
import {
Eye,
Download,
Send,
UserCheck,
UserX,
Trash2,
Mail,
TrendingUp,
Bell,
Award
} from 'lucide-react';
interface MoreOptionsDropdownProps {
itemType: 'customer' | 'instance' | 'invoice' | 'ticket';
onViewPerformance: () => void;
onDownloadReport: () => void;
onSendNotification: () => void;
onChangeTier: () => void;
onDeactivate: () => void;
onDelete: () => void;
onSendMail: () => void;
onClose: () => void;
}
const MoreOptionsDropdown: React.FC<MoreOptionsDropdownProps> = ({
itemType,
onViewPerformance,
onDownloadReport,
onSendNotification,
onChangeTier,
onDeactivate,
onDelete,
onSendMail,
onClose
}) => {
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
onClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [onClose]);
const getOptions = () => {
switch (itemType) {
case 'customer':
return [
{ icon: Eye, label: 'View Performance', onClick: onViewPerformance },
{ icon: Download, label: 'Download Report', onClick: onDownloadReport },
{ icon: Bell, label: 'Send Notification', onClick: onSendNotification },
{ icon: Award, label: 'Change Tier', onClick: onChangeTier },
{ icon: Mail, label: 'Send Email', onClick: onSendMail },
{ icon: UserX, label: 'Deactivate', onClick: onDeactivate },
{ icon: Trash2, label: 'Delete', onClick: onDelete },
];
case 'instance':
return [
{ icon: Eye, label: 'View Details', onClick: onViewPerformance },
{ icon: Download, label: 'Download Logs', onClick: onDownloadReport },
{ icon: Bell, label: 'Send Alert', onClick: onSendNotification },
{ icon: Trash2, label: 'Delete', onClick: onDelete },
];
case 'invoice':
return [
{ icon: Eye, label: 'View Details', onClick: onViewPerformance },
{ icon: Download, label: 'Download PDF', onClick: onDownloadReport },
{ icon: Mail, label: 'Send Reminder', onClick: onSendMail },
{ icon: Trash2, label: 'Delete', onClick: onDelete },
];
case 'ticket':
return [
{ icon: Eye, label: 'View Details', onClick: onViewPerformance },
{ icon: Download, label: 'Download Logs', onClick: onDownloadReport },
{ icon: Bell, label: 'Send Notification', onClick: onSendNotification },
{ icon: Award, label: 'Change Priority', onClick: onChangeTier },
{ icon: UserX, label: 'Close Ticket', onClick: onDeactivate },
{ icon: Mail, label: 'Send Email', onClick: onSendMail },
{ icon: Trash2, label: 'Delete', onClick: onDelete },
];
default:
return [];
}
};
return (
<div
ref={dropdownRef}
className="absolute right-0 top-full mt-2 w-56 bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 backdrop-blur-xl z-50"
>
<div className="py-2">
{getOptions().map((option, index) => (
<button
key={index}
onClick={() => {
option.onClick();
onClose();
}}
className="w-full flex items-center space-x-3 px-4 py-3 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors duration-200"
>
<option.icon className="w-4 h-4" />
<span>{option.label}</span>
</button>
))}
</div>
</div>
);
};
export default MoreOptionsDropdown;

View File

@ -0,0 +1,115 @@
import React from 'react';
import {
Construction,
ArrowLeft,
Home
} from 'lucide-react';
import { Link } from 'react-router-dom';
interface PlaceholderPageProps {
title: string;
description: string;
icon?: React.ComponentType<any>;
}
const PlaceholderPage: React.FC<PlaceholderPageProps> = ({ title, description, icon: Icon = Construction }) => {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-900/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="text-center">
{/* Icon */}
<div className="w-24 h-24 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-full flex items-center justify-center mx-auto mb-8 shadow-2xl">
<Icon className="w-12 h-12 text-white" />
</div>
{/* Title */}
<h1 className="text-4xl lg:text-5xl font-bold text-slate-900 dark:text-white mb-4">
{title}
</h1>
{/* Description */}
<p className="text-xl text-slate-600 dark:text-slate-400 mb-8 max-w-2xl mx-auto">
{description}
</p>
{/* Status Badge */}
<div className="inline-flex items-center px-4 py-2 rounded-full bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 text-sm font-medium mb-12">
<Construction className="w-4 h-4 mr-2" />
Coming Soon
</div>
{/* Features Preview */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<div className="card-elevated p-6 text-center">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center mx-auto mb-4">
<span className="text-white font-bold text-lg">1</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">
Advanced Features
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
Powerful tools and functionality designed for resellers
</p>
</div>
<div className="card-elevated p-6 text-center">
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center mx-auto mb-4">
<span className="text-white font-bold text-lg">2</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">
Real-time Analytics
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
Live data and insights to optimize your business
</p>
</div>
<div className="card-elevated p-6 text-center">
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center mx-auto mb-4">
<span className="text-white font-bold text-lg">3</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">
Seamless Integration
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
Connect with your existing tools and workflows
</p>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4">
<Link
to="/"
className="btn btn-primary btn-lg"
>
<Home className="w-5 h-5 mr-2" />
Back to Dashboard
</Link>
<button
onClick={() => window.history.back()}
className="btn btn-secondary btn-lg"
>
<ArrowLeft className="w-5 h-5 mr-2" />
Go Back
</button>
</div>
{/* Progress Indicator */}
<div className="mt-12">
<div className="flex items-center justify-center space-x-2 mb-4">
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div>
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div>
</div>
<p className="text-sm text-slate-500 dark:text-slate-400">
Our team is working hard to bring you this feature
</p>
</div>
</div>
</div>
</div>
);
};
export default PlaceholderPage;

View File

@ -0,0 +1,253 @@
import React, { useState } from 'react';
import { X } from 'lucide-react';
import { cn } from '../../utils/cn';
interface AddCustomerFormProps {
onSubmit: (data: any) => void;
onCancel: () => void;
}
const AddCustomerForm: React.FC<AddCustomerFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
company: '',
tier: 'silver',
address: '',
city: '',
state: '',
country: 'India',
postalCode: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = () => {
const newErrors: Record<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.company.trim()) {
newErrors.company = 'Company name is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
onSubmit(formData);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Company Name *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className={cn(
"input w-full",
errors.name && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
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-slate-700 dark:text-slate-300 mb-1">
Contact Person *
</label>
<input
type="text"
name="company"
value={formData.company}
onChange={handleChange}
className={cn(
"input w-full",
errors.company && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter contact person name"
/>
{errors.company && <p className="text-red-500 text-xs mt-1">{errors.company}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Email Address *
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className={cn(
"input w-full",
errors.email && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
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-slate-700 dark:text-slate-300 mb-1">
Phone Number *
</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
className={cn(
"input w-full",
errors.phone && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
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-slate-700 dark:text-slate-300 mb-1">
Customer Tier
</label>
<select
name="tier"
value={formData.tier}
onChange={handleChange}
className="input w-full"
>
<option value="silver">Silver</option>
<option value="gold">Gold</option>
<option value="platinum">Platinum</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Country
</label>
<select
name="country"
value={formData.country}
onChange={handleChange}
className="input w-full"
>
<option value="India">India</option>
<option value="United States">United States</option>
<option value="United Kingdom">United Kingdom</option>
<option value="Canada">Canada</option>
<option value="Australia">Australia</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Address
</label>
<textarea
name="address"
value={formData.address}
onChange={handleChange}
rows={3}
className="input w-full resize-none"
placeholder="Enter full address"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
City
</label>
<input
type="text"
name="city"
value={formData.city}
onChange={handleChange}
className="input w-full"
placeholder="Enter city"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
State
</label>
<input
type="text"
name="state"
value={formData.state}
onChange={handleChange}
className="input w-full"
placeholder="Enter state"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Postal Code
</label>
<input
type="text"
name="postalCode"
value={formData.postalCode}
onChange={handleChange}
className="input w-full"
placeholder="Enter postal code"
/>
</div>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onCancel}
className="btn btn-secondary"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
>
Add Customer
</button>
</div>
</form>
);
};
export default AddCustomerForm;

View File

@ -0,0 +1,196 @@
import React, { useState } from 'react';
import { cn } from '../../utils/cn';
interface AddInstanceFormProps {
onSubmit: (data: any) => void;
onCancel: () => void;
}
const AddInstanceForm: React.FC<AddInstanceFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
name: '',
type: 'Ubuntu Server 22.04 LTS',
region: 'Mumbai',
cpu: '2 vCPU',
memory: '4 GB',
storage: '80 GB SSD',
customer: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = 'Instance name is required';
}
if (!formData.customer.trim()) {
newErrors.customer = 'Customer is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
onSubmit(formData);
}
};
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 (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Instance Name *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className={cn(
"input w-full",
errors.name && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter instance 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-slate-700 dark:text-slate-300 mb-1">
Customer *
</label>
<input
type="text"
name="customer"
value={formData.customer}
onChange={handleChange}
className={cn(
"input w-full",
errors.customer && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter customer name"
/>
{errors.customer && <p className="text-red-500 text-xs mt-1">{errors.customer}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Instance Type
</label>
<select
name="type"
value={formData.type}
onChange={handleChange}
className="input w-full"
>
<option value="Ubuntu Server 22.04 LTS">Ubuntu Server 22.04 LTS</option>
<option value="CentOS 8">CentOS 8</option>
<option value="Windows Server 2022">Windows Server 2022</option>
<option value="Debian 11">Debian 11</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Region
</label>
<select
name="region"
value={formData.region}
onChange={handleChange}
className="input w-full"
>
<option value="Mumbai">Mumbai</option>
<option value="Delhi">Delhi</option>
<option value="Bangalore">Bangalore</option>
<option value="Chennai">Chennai</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
CPU
</label>
<select
name="cpu"
value={formData.cpu}
onChange={handleChange}
className="input w-full"
>
<option value="1 vCPU">1 vCPU</option>
<option value="2 vCPU">2 vCPU</option>
<option value="4 vCPU">4 vCPU</option>
<option value="8 vCPU">8 vCPU</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Memory
</label>
<select
name="memory"
value={formData.memory}
onChange={handleChange}
className="input w-full"
>
<option value="2 GB">2 GB</option>
<option value="4 GB">4 GB</option>
<option value="8 GB">8 GB</option>
<option value="16 GB">16 GB</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Storage
</label>
<select
name="storage"
value={formData.storage}
onChange={handleChange}
className="input w-full"
>
<option value="40 GB SSD">40 GB SSD</option>
<option value="80 GB SSD">80 GB SSD</option>
<option value="160 GB SSD">160 GB SSD</option>
<option value="320 GB SSD">320 GB SSD</option>
</select>
</div>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onCancel}
className="btn btn-secondary"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
>
Create Instance
</button>
</div>
</form>
);
};
export default AddInstanceForm;

View File

@ -0,0 +1,121 @@
import React, { useState } from 'react';
import { cn } from '../../utils/cn';
interface AddInvoiceFormProps {
onSubmit: (data: any) => void;
onCancel: () => void;
}
const AddInvoiceForm: React.FC<AddInvoiceFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
customer: '',
amount: '',
dueDate: '',
items: [{ name: '', quantity: 1, rate: '', amount: '' }]
});
const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.customer.trim()) {
newErrors.customer = 'Customer is required';
}
if (!formData.amount.trim()) {
newErrors.amount = 'Amount is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
onSubmit(formData);
}
};
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 (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Customer *
</label>
<input
type="text"
name="customer"
value={formData.customer}
onChange={handleChange}
className={cn(
"input w-full",
errors.customer && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter customer name"
/>
{errors.customer && <p className="text-red-500 text-xs mt-1">{errors.customer}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Amount *
</label>
<input
type="number"
name="amount"
value={formData.amount}
onChange={handleChange}
className={cn(
"input w-full",
errors.amount && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter amount"
/>
{errors.amount && <p className="text-red-500 text-xs mt-1">{errors.amount}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Due Date
</label>
<input
type="date"
name="dueDate"
value={formData.dueDate}
onChange={handleChange}
className="input w-full"
/>
</div>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onCancel}
className="btn btn-secondary"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
>
Create Invoice
</button>
</div>
</form>
);
};
export default AddInvoiceForm;

View File

@ -0,0 +1,147 @@
import React, { useState } from 'react';
import { cn } from '../../utils/cn';
interface AddTicketFormProps {
onSubmit: (data: any) => void;
onCancel: () => void;
}
const AddTicketForm: React.FC<AddTicketFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
customer: '',
subject: '',
description: '',
priority: 'medium',
assignedTo: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.customer.trim()) {
newErrors.customer = 'Customer is required';
}
if (!formData.subject.trim()) {
newErrors.subject = 'Subject is required';
}
if (!formData.description.trim()) {
newErrors.description = 'Description is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
onSubmit(formData);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Customer *
</label>
<input
type="text"
name="customer"
value={formData.customer}
onChange={handleChange}
className={cn(
"input w-full",
errors.customer && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter customer name"
/>
{errors.customer && <p className="text-red-500 text-xs mt-1">{errors.customer}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Priority
</label>
<select
name="priority"
value={formData.priority}
onChange={handleChange}
className="input w-full"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Subject *
</label>
<input
type="text"
name="subject"
value={formData.subject}
onChange={handleChange}
className={cn(
"input w-full",
errors.subject && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter ticket subject"
/>
{errors.subject && <p className="text-red-500 text-xs mt-1">{errors.subject}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Description *
</label>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
rows={4}
className={cn(
"input w-full resize-none",
errors.description && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter detailed description"
/>
{errors.description && <p className="text-red-500 text-xs mt-1">{errors.description}</p>}
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onCancel}
className="btn btn-secondary"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
>
Create Ticket
</button>
</div>
</form>
);
};
export default AddTicketForm;

View File

@ -0,0 +1,270 @@
import React, { useState, useEffect } from 'react';
import { cn } from '../../utils/cn';
interface EditCustomerFormProps {
customer: any;
onSubmit: (data: any) => void;
onCancel: () => void;
}
const EditCustomerForm: React.FC<EditCustomerFormProps> = ({ customer, onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
company: '',
tier: 'silver',
address: '',
city: '',
state: '',
country: 'India',
postalCode: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
useEffect(() => {
if (customer) {
setFormData({
name: customer.name || '',
email: customer.email || '',
phone: customer.phone || '',
company: customer.company || '',
tier: customer.tier || 'silver',
address: customer.address || '',
city: customer.city || '',
state: customer.state || '',
country: customer.country || 'India',
postalCode: customer.postalCode || ''
});
}
}, [customer]);
const validateForm = () => {
const newErrors: Record<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.company.trim()) {
newErrors.company = 'Company name is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
onSubmit(formData);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Company Name *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className={cn(
"input w-full",
errors.name && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
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-slate-700 dark:text-slate-300 mb-1">
Contact Person *
</label>
<input
type="text"
name="company"
value={formData.company}
onChange={handleChange}
className={cn(
"input w-full",
errors.company && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter contact person name"
/>
{errors.company && <p className="text-red-500 text-xs mt-1">{errors.company}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Email Address *
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className={cn(
"input w-full",
errors.email && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
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-slate-700 dark:text-slate-300 mb-1">
Phone Number *
</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
className={cn(
"input w-full",
errors.phone && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
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-slate-700 dark:text-slate-300 mb-1">
Customer Tier
</label>
<select
name="tier"
value={formData.tier}
onChange={handleChange}
className="input w-full"
>
<option value="silver">Silver</option>
<option value="gold">Gold</option>
<option value="platinum">Platinum</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Country
</label>
<select
name="country"
value={formData.country}
onChange={handleChange}
className="input w-full"
>
<option value="India">India</option>
<option value="United States">United States</option>
<option value="United Kingdom">United Kingdom</option>
<option value="Canada">Canada</option>
<option value="Australia">Australia</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Address
</label>
<textarea
name="address"
value={formData.address}
onChange={handleChange}
rows={3}
className="input w-full resize-none"
placeholder="Enter full address"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
City
</label>
<input
type="text"
name="city"
value={formData.city}
onChange={handleChange}
className="input w-full"
placeholder="Enter city"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
State
</label>
<input
type="text"
name="state"
value={formData.state}
onChange={handleChange}
className="input w-full"
placeholder="Enter state"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Postal Code
</label>
<input
type="text"
name="postalCode"
value={formData.postalCode}
onChange={handleChange}
className="input w-full"
placeholder="Enter postal code"
/>
</div>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onCancel}
className="btn btn-secondary"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
>
Update Customer
</button>
</div>
</form>
);
};
export default EditCustomerForm;

View File

@ -0,0 +1,211 @@
import React, { useState, useEffect } from 'react';
import { cn } from '../../utils/cn';
interface EditInstanceFormProps {
instance: any;
onSubmit: (data: any) => void;
onCancel: () => void;
}
const EditInstanceForm: React.FC<EditInstanceFormProps> = ({ instance, onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
name: '',
type: '',
region: '',
cpu: '',
memory: '',
storage: '',
customer: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
useEffect(() => {
if (instance) {
setFormData({
name: instance.name || '',
type: instance.type || '',
region: instance.region || '',
cpu: instance.cpu || '',
memory: instance.memory || '',
storage: instance.storage || '',
customer: instance.customer || ''
});
}
}, [instance]);
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = 'Instance name is required';
}
if (!formData.customer.trim()) {
newErrors.customer = 'Customer is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
onSubmit(formData);
}
};
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 (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Instance Name *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className={cn(
"input w-full",
errors.name && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter instance 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-slate-700 dark:text-slate-300 mb-1">
Customer *
</label>
<input
type="text"
name="customer"
value={formData.customer}
onChange={handleChange}
className={cn(
"input w-full",
errors.customer && "border-red-500 focus:border-red-500 focus:ring-red-500"
)}
placeholder="Enter customer name"
/>
{errors.customer && <p className="text-red-500 text-xs mt-1">{errors.customer}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Instance Type
</label>
<select
name="type"
value={formData.type}
onChange={handleChange}
className="input w-full"
>
<option value="Ubuntu Server 22.04 LTS">Ubuntu Server 22.04 LTS</option>
<option value="CentOS 8">CentOS 8</option>
<option value="Windows Server 2022">Windows Server 2022</option>
<option value="Debian 11">Debian 11</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Region
</label>
<select
name="region"
value={formData.region}
onChange={handleChange}
className="input w-full"
>
<option value="Mumbai">Mumbai</option>
<option value="Delhi">Delhi</option>
<option value="Bangalore">Bangalore</option>
<option value="Chennai">Chennai</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
CPU
</label>
<select
name="cpu"
value={formData.cpu}
onChange={handleChange}
className="input w-full"
>
<option value="1 vCPU">1 vCPU</option>
<option value="2 vCPU">2 vCPU</option>
<option value="4 vCPU">4 vCPU</option>
<option value="8 vCPU">8 vCPU</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Memory
</label>
<select
name="memory"
value={formData.memory}
onChange={handleChange}
className="input w-full"
>
<option value="2 GB">2 GB</option>
<option value="4 GB">4 GB</option>
<option value="8 GB">8 GB</option>
<option value="16 GB">16 GB</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Storage
</label>
<select
name="storage"
value={formData.storage}
onChange={handleChange}
className="input w-full"
>
<option value="40 GB SSD">40 GB SSD</option>
<option value="80 GB SSD">80 GB SSD</option>
<option value="160 GB SSD">160 GB SSD</option>
<option value="320 GB SSD">320 GB SSD</option>
</select>
</div>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onCancel}
className="btn btn-secondary"
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
>
Update Instance
</button>
</div>
</form>
);
};
export default EditInstanceForm;

View File

@ -0,0 +1,205 @@
import React, { useState } from 'react';
import { Send, Paperclip } from 'lucide-react';
interface MailComposeFormProps {
recipient: string;
onSubmit: (data: any) => void;
onCancel: () => void;
}
const MailComposeForm: React.FC<MailComposeFormProps> = ({ recipient, onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
to: recipient || '',
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()) {
onSubmit(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 || 'No email'}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
To *
</label>
<input
type="email"
name="to"
value={formData.to}
onChange={handleChange}
className={`input w-full ${
errors.to ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''
}`}
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-slate-700 dark:text-slate-300 mb-1">
Subject *
</label>
<input
type="text"
name="subject"
value={formData.subject}
onChange={handleChange}
className={`input w-full ${
errors.subject ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''
}`}
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-slate-700 dark:text-slate-300 mb-1">
CC
</label>
<input
type="email"
name="cc"
value={formData.cc}
onChange={handleChange}
className={`input w-full ${
errors.cc ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''
}`}
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-slate-700 dark:text-slate-300 mb-1">
BCC
</label>
<input
type="email"
name="bcc"
value={formData.bcc}
onChange={handleChange}
className={`input w-full ${
errors.bcc ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''
}`}
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-slate-700 dark:text-slate-300 mb-1">
Message *
</label>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
rows={8}
className={`input w-full resize-none ${
errors.message ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''
}`}
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-slate-600 dark:text-slate-400 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700"
>
<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="btn btn-secondary"
>
Cancel
</button>
<button
type="button"
onClick={handleSendViaEmail}
className="btn btn-outline"
>
<Send className="w-4 h-4 mr-2" />
Send via Email Client
</button>
<button
type="submit"
className="btn btn-primary"
>
<Send className="w-4 h-4 mr-2" />
Send
</button>
</div>
</form>
</div>
);
};
export default MailComposeForm;

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

@ -0,0 +1,331 @@
import { DashboardStats, RecentActivity, QuickAction } from '../store/slices/dashboardSlice';
import { User } from '../store/slices/authSlice';
export const mockUser: User = {
id: '1',
email: 'alex.rodriguez@techsolutions.com',
name: 'Alex Rodriguez',
role: 'reseller_admin',
company: 'Tech Solutions Inc.',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face',
tier: 'platinum',
isVerified: true,
twoFactorEnabled: true,
};
export const mockDashboardStats: DashboardStats = {
totalRevenue: 2847500,
totalCustomers: 156,
activeInstances: 89,
pendingInvoices: 12,
monthlyGrowth: 23.5,
commissionEarned: 284750,
currency: 'INR',
};
export const mockRecentActivities: RecentActivity[] = [
{
id: '1',
type: 'customer_added',
title: 'New Customer Added',
description: 'ABC Technologies Pvt Ltd has been added as a new customer',
timestamp: '2024-01-15T10:30:00Z',
},
{
id: '2',
type: 'instance_created',
title: 'Instance Created',
description: 'Created Ubuntu Server 22.04 LTS instance for TechStart Inc',
timestamp: '2024-01-15T09:15:00Z',
},
{
id: '3',
type: 'payment_received',
title: 'Payment Received',
description: 'Received payment of ₹45,000 from DataFlow Solutions',
timestamp: '2024-01-15T08:45:00Z',
amount: 45000,
currency: 'INR',
},
{
id: '4',
type: 'support_ticket',
title: 'Support Ticket Resolved',
description: 'Resolved high-priority ticket #SR-2024-001 for CloudTech Ltd',
timestamp: '2024-01-14T16:20:00Z',
},
{
id: '5',
type: 'customer_added',
title: 'New Customer Added',
description: 'InnovateSoft Solutions has been added as a new customer',
timestamp: '2024-01-14T14:10:00Z',
},
];
export const mockQuickActions: QuickAction[] = [
{
id: '1',
title: 'Add Customer',
description: 'Create a new customer account',
icon: 'UserPlus',
action: '/customers/new',
color: 'primary',
},
{
id: '2',
title: 'Create Instance',
description: 'Provision a new cloud instance',
icon: 'Server',
action: '/instances/new',
color: 'success',
},
{
id: '3',
title: 'Generate Invoice',
description: 'Create and send invoice to customer',
icon: 'FileText',
action: '/billing/invoices/new',
color: 'warning',
},
{
id: '4',
title: 'Support Ticket',
description: 'Create a new support ticket',
icon: 'Headphones',
action: '/support/tickets/new',
color: 'danger',
},
{
id: '5',
title: 'Add Funds',
description: 'Add funds to your wallet',
icon: 'Wallet',
action: '/billing/wallet/add',
color: 'primary',
},
{
id: '6',
title: 'View Reports',
description: 'Access detailed analytics and reports',
icon: 'BarChart3',
action: '/reports',
color: 'secondary',
},
];
export const mockCustomers = [
{
id: '1',
name: 'ABC Technologies Pvt Ltd',
email: 'contact@abctech.com',
phone: '+91-98765-43210',
status: 'active',
tier: 'gold',
totalSpent: 450000,
lastActive: '2024-01-15T10:30:00Z',
instances: 12,
avatar: 'https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=150&h=150&fit=crop',
},
{
id: '2',
name: 'TechStart Inc',
email: 'hello@techstart.com',
phone: '+91-98765-43211',
status: 'active',
tier: 'silver',
totalSpent: 125000,
lastActive: '2024-01-15T09:15:00Z',
instances: 5,
avatar: 'https://images.unsplash.com/photo-1551434678-e076c223a692?w=150&h=150&fit=crop',
},
{
id: '3',
name: 'DataFlow Solutions',
email: 'info@dataflow.com',
phone: '+91-98765-43212',
status: 'active',
tier: 'platinum',
totalSpent: 890000,
lastActive: '2024-01-15T08:45:00Z',
instances: 25,
avatar: 'https://images.unsplash.com/photo-1556761175-b413da4baf72?w=150&h=150&fit=crop',
},
{
id: '4',
name: 'CloudTech Ltd',
email: 'support@cloudtech.com',
phone: '+91-98765-43213',
status: 'pending',
tier: 'silver',
totalSpent: 75000,
lastActive: '2024-01-14T16:20:00Z',
instances: 3,
avatar: 'https://images.unsplash.com/photo-1563013544-824ae1b704d3?w=150&h=150&fit=crop',
},
{
id: '5',
name: 'InnovateSoft Solutions',
email: 'hello@innovatesoft.com',
phone: '+91-98765-43214',
status: 'active',
tier: 'gold',
totalSpent: 320000,
lastActive: '2024-01-14T14:10:00Z',
instances: 18,
avatar: 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=150&h=150&fit=crop',
},
];
export const mockInstances = [
{
id: '1',
name: 'Web Server - ABC Tech',
type: 'Ubuntu Server 22.04 LTS',
status: 'running',
region: 'Mumbai',
cpu: '2 vCPU',
memory: '4 GB',
storage: '80 GB SSD',
ip: '192.168.1.100',
customer: 'ABC Technologies Pvt Ltd',
createdAt: '2024-01-10T10:30:00Z',
monthlyCost: 2500,
},
{
id: '2',
name: 'Database Server - TechStart',
type: 'CentOS 8',
status: 'running',
region: 'Delhi',
cpu: '4 vCPU',
memory: '8 GB',
storage: '160 GB SSD',
ip: '192.168.1.101',
customer: 'TechStart Inc',
createdAt: '2024-01-12T09:15:00Z',
monthlyCost: 4500,
},
{
id: '3',
name: 'Load Balancer - DataFlow',
type: 'Ubuntu Server 22.04 LTS',
status: 'stopped',
region: 'Bangalore',
cpu: '2 vCPU',
memory: '4 GB',
storage: '80 GB SSD',
ip: '192.168.1.102',
customer: 'DataFlow Solutions',
createdAt: '2024-01-08T08:45:00Z',
monthlyCost: 2000,
},
{
id: '4',
name: 'Application Server - CloudTech',
type: 'Windows Server 2022',
status: 'running',
region: 'Mumbai',
cpu: '8 vCPU',
memory: '16 GB',
storage: '320 GB SSD',
ip: '192.168.1.103',
customer: 'CloudTech Ltd',
createdAt: '2024-01-05T16:20:00Z',
monthlyCost: 8500,
},
{
id: '5',
name: 'Development Server - InnovateSoft',
type: 'Ubuntu Server 22.04 LTS',
status: 'running',
region: 'Delhi',
cpu: '4 vCPU',
memory: '8 GB',
storage: '160 GB SSD',
ip: '192.168.1.104',
customer: 'InnovateSoft Solutions',
createdAt: '2024-01-03T14:10:00Z',
monthlyCost: 4000,
},
];
export const mockInvoices = [
{
id: 'INV-2024-001',
customer: 'ABC Technologies Pvt Ltd',
amount: 45000,
status: 'paid',
dueDate: '2024-01-20T00:00:00Z',
issuedDate: '2024-01-01T00:00:00Z',
paidDate: '2024-01-15T08:45:00Z',
items: [
{ name: 'Web Server - ABC Tech', quantity: 1, rate: 2500, amount: 2500 },
{ name: 'Database Server - ABC Tech', quantity: 1, rate: 4500, amount: 4500 },
{ name: 'Load Balancer - ABC Tech', quantity: 1, rate: 2000, amount: 2000 },
],
},
{
id: 'INV-2024-002',
customer: 'TechStart Inc',
amount: 12500,
status: 'pending',
dueDate: '2024-01-25T00:00:00Z',
issuedDate: '2024-01-05T00:00:00Z',
paidDate: null,
items: [
{ name: 'Database Server - TechStart', quantity: 1, rate: 4500, amount: 4500 },
{ name: 'Development Server - TechStart', quantity: 1, rate: 4000, amount: 4000 },
],
},
{
id: 'INV-2024-003',
customer: 'DataFlow Solutions',
amount: 89000,
status: 'paid',
dueDate: '2024-01-15T00:00:00Z',
issuedDate: '2024-01-01T00:00:00Z',
paidDate: '2024-01-15T08:45:00Z',
items: [
{ name: 'Load Balancer - DataFlow', quantity: 1, rate: 2000, amount: 2000 },
{ name: 'Application Server - DataFlow', quantity: 1, rate: 8500, amount: 8500 },
{ name: 'Database Cluster - DataFlow', quantity: 1, rate: 15000, amount: 15000 },
],
},
];
export const mockSupportTickets = [
{
id: 'SR-2024-001',
customer: 'CloudTech Ltd',
subject: 'High CPU Usage on Application Server',
description: 'Our application server is experiencing high CPU usage and slow response times.',
status: 'resolved',
priority: 'high',
assignedTo: 'John Doe',
createdAt: '2024-01-14T10:00:00Z',
resolvedAt: '2024-01-14T16:20:00Z',
},
{
id: 'SR-2024-002',
customer: 'ABC Technologies Pvt Ltd',
subject: 'Database Connection Issues',
description: 'Unable to connect to the database server from our application.',
status: 'open',
priority: 'medium',
assignedTo: 'Jane Smith',
createdAt: '2024-01-15T11:30:00Z',
resolvedAt: null,
},
{
id: 'SR-2024-003',
customer: 'TechStart Inc',
subject: 'Billing Query',
description: 'Need clarification on the latest invoice charges.',
status: 'in_progress',
priority: 'low',
assignedTo: 'Mike Johnson',
createdAt: '2024-01-13T14:15:00Z',
resolvedAt: null,
},
];

View File

@ -1,13 +1,237 @@
body { @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
margin: 0; @tailwind base;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', @tailwind components;
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', @tailwind utilities;
sans-serif;
-webkit-font-smoothing: antialiased; @layer base {
-moz-osx-font-smoothing: grayscale; * {
@apply border-gray-200 dark:border-gray-700;
font-family: 'Inter', sans-serif !important;
}
body {
@apply bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-white font-sans antialiased;
font-family: 'Inter', sans-serif !important;
overflow-x: hidden;
max-width: 100vw;
}
html {
@apply scroll-smooth;
overflow-x: hidden;
}
/* 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-emerald-500 ring-offset-2 ring-offset-white dark:ring-offset-slate-900;
}
} }
code { @layer components {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', /* Modern Button System - Emerald Theme */
monospace; .btn {
@apply inline-flex items-center justify-center rounded-xl font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-white dark:ring-offset-slate-900;
}
.btn-primary {
@apply bg-gradient-to-r from-emerald-600 to-emerald-500 text-white shadow-lg hover:shadow-xl hover:from-emerald-700 hover:to-emerald-600 active:scale-95 active:shadow-xl;
}
.btn-secondary {
@apply bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-900 dark:text-white border border-slate-300 dark:border-slate-600 shadow-sm hover:shadow-md active:scale-95;
}
.btn-outline {
@apply bg-transparent border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 shadow-sm hover:shadow-md active:scale-95;
}
.btn-ghost {
@apply bg-transparent text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 active:scale-95;
}
.btn-sm {
@apply h-9 px-3 text-sm;
}
.btn-md {
@apply h-11 px-4 py-2.5 text-sm;
}
.btn-lg {
@apply h-14 px-6 py-3 text-base;
}
/* Modern Card Design - Glass Morphism */
.card {
@apply rounded-2xl border border-slate-200/60 dark:border-slate-700/60 bg-white/90 dark:bg-slate-800/90 backdrop-blur-xl shadow-xl hover:shadow-2xl transition-all duration-300;
}
.card-elevated {
@apply rounded-2xl border border-slate-200/60 dark:border-slate-700/60 bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl shadow-2xl hover:shadow-2xl transition-all duration-300;
}
/* Enhanced Input System */
.input {
@apply flex h-11 w-full rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-4 py-2.5 text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition-all duration-200 ring-offset-white dark:ring-offset-slate-900 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50;
}
/* Modern Badge System */
.badge {
@apply inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2;
}
.badge-primary {
@apply border-transparent bg-gradient-to-r from-emerald-500 to-emerald-600 text-white shadow-md;
}
.badge-secondary {
@apply border-transparent bg-slate-100 text-slate-800 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600;
}
.badge-success {
@apply border-transparent bg-gradient-to-r from-green-500 to-green-600 text-white shadow-md;
}
.badge-warning {
@apply border-transparent bg-gradient-to-r from-amber-500 to-amber-600 text-white shadow-md;
}
.badge-danger {
@apply border-transparent bg-gradient-to-r from-red-500 to-red-600 text-white shadow-md;
}
/* Modern Table Design */
.table {
@apply w-full min-w-0;
}
.table th {
@apply px-4 py-3 text-left text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wider border-b border-slate-200 dark:border-slate-700 whitespace-nowrap;
}
.table td {
@apply px-4 py-3 text-sm text-slate-900 dark:text-white border-b border-slate-100 dark:border-slate-800 break-words;
}
.table tbody tr {
@apply hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors duration-200;
}
/* Modern Modal Design */
.modal-overlay {
@apply fixed inset-0 z-50 bg-black/60 backdrop-blur-sm;
}
.modal-content {
@apply bg-white/95 dark:bg-slate-800/95 backdrop-blur-xl rounded-3xl shadow-2xl border border-slate-200/20 dark:border-slate-700/20;
}
/* Enhanced Dropdown */
.dropdown {
@apply bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 backdrop-blur-xl;
}
/* Skeleton Loading */
.skeleton {
@apply animate-pulse bg-slate-200 dark:bg-slate-700 rounded-lg;
}
/* Custom Scrollbar */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: rgb(203 213 225) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgb(203 213 225);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: rgb(148 163 184);
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.gradient-text {
@apply bg-gradient-to-r from-emerald-600 to-emerald-400 bg-clip-text text-transparent;
}
.glass-effect {
@apply backdrop-blur-xl bg-white/80 dark:bg-slate-800/80 border border-white/20 dark:border-slate-700/20;
}
.hover\:scale-105:hover {
transform: scale(1.05);
}
.hover\:scale-110:hover {
transform: scale(1.1);
}
.btn-hover-lift {
@apply transition-all duration-300;
}
.btn-hover-lift:hover {
@apply transform -translate-y-1 shadow-xl;
}
/* Prevent horizontal overflow */
.no-horizontal-scroll {
overflow-x: hidden;
max-width: 100%;
}
.table-responsive {
@apply overflow-x-auto overflow-y-hidden;
scrollbar-width: thin;
scrollbar-color: rgb(203 213 225) transparent;
}
.table-responsive::-webkit-scrollbar {
height: 6px;
}
.table-responsive::-webkit-scrollbar-track {
background: transparent;
}
.table-responsive::-webkit-scrollbar-thumb {
background-color: rgb(203 213 225);
border-radius: 3px;
}
.table-responsive::-webkit-scrollbar-thumb:hover {
background-color: rgb(148 163 184);
}
} }

345
src/pages/Billing/index.tsx Normal file
View File

@ -0,0 +1,345 @@
import React, { useState } from 'react';
import {
FileText,
Search,
Filter,
Plus,
Eye,
Download,
Mail,
MoreHorizontal,
DollarSign,
Calendar,
User,
CheckCircle,
Clock,
AlertCircle,
TrendingUp,
CreditCard,
Receipt
} from 'lucide-react';
import { mockInvoices } from '../../data/mockData';
import { formatDate } from '../../utils/format';
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
import Modal from '../../components/Modal';
import DetailView from '../../components/DetailView';
import AddInvoiceForm from '../../components/forms/AddInvoiceForm';
import MailComposeForm from '../../components/forms/MailComposeForm';
import MoreOptionsDropdown from '../../components/MoreOptionsDropdown';
const Billing: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedStatus, setSelectedStatus] = useState('all');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [isMailModalOpen, setIsMailModalOpen] = useState(false);
const [showMoreOptions, setShowMoreOptions] = useState<string | null>(null);
const [selectedInvoice, setSelectedInvoice] = useState<any>(null);
const filteredInvoices = mockInvoices.filter(invoice => {
const matchesSearch = invoice.customer.toLowerCase().includes(searchTerm.toLowerCase()) ||
invoice.id.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = selectedStatus === 'all' || invoice.status === selectedStatus;
return matchesSearch && matchesStatus;
});
const stats = {
total: mockInvoices.length,
paid: mockInvoices.filter(i => i.status === 'paid').length,
pending: mockInvoices.filter(i => i.status === 'pending').length,
totalAmount: mockInvoices.reduce((sum, i) => sum + i.amount, 0),
paidAmount: mockInvoices.filter(i => i.status === 'paid').reduce((sum, i) => sum + i.amount, 0)
};
const handleAddInvoice = (data: any) => {
console.log('Adding invoice:', data);
setIsAddModalOpen(false);
};
const handleViewInvoice = (invoice: any) => {
setSelectedInvoice(invoice);
setIsDetailModalOpen(true);
};
const handleMailInvoice = (invoice: any) => {
setSelectedInvoice(invoice);
setIsMailModalOpen(true);
};
const handleMoreOptions = (invoiceId: string) => {
setShowMoreOptions(showMoreOptions === invoiceId ? null : invoiceId);
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'paid':
return <span className="badge badge-success">Paid</span>;
case 'pending':
return <span className="badge badge-warning">Pending</span>;
case 'overdue':
return <span className="badge badge-danger">Overdue</span>;
default:
return <span className="badge badge-secondary">{status}</span>;
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'paid':
return <CheckCircle className="w-4 h-4 text-green-600" />;
case 'pending':
return <Clock className="w-4 h-4 text-amber-600" />;
case 'overdue':
return <AlertCircle className="w-4 h-4 text-red-600" />;
default:
return <Clock className="w-4 h-4 text-slate-600" />;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-900/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0 mb-8">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white">
Billing & Invoices
</h1>
<p className="text-slate-600 dark:text-slate-400 mt-2">
Manage invoices, payments, and billing information
</p>
</div>
<button
onClick={() => setIsAddModalOpen(true)}
className="btn btn-primary btn-md"
>
<Plus className="w-5 h-5 mr-2" />
Create Invoice
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 sm:gap-6 mb-8">
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Invoices</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.total}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-slate-500 to-slate-600 rounded-xl flex items-center justify-center">
<FileText className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Paid</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.paid}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Pending</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.pending}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center">
<Clock className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Amount</p>
<DualCurrencyDisplay
amount={stats.totalAmount}
currency="INR"
className="text-2xl font-bold"
/>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-xl flex items-center justify-center">
<DollarSign className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Paid Amount</p>
<DualCurrencyDisplay
amount={stats.paidAmount}
currency="INR"
className="text-2xl font-bold"
/>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
<CreditCard className="w-6 h-6 text-white" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="card-elevated p-6 mb-8">
<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 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder="Search invoices..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pl-10 w-full"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="input"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="overdue">Overdue</option>
</select>
<button className="btn btn-secondary btn-md">
<Filter className="w-4 h-4 mr-2" />
Filters
</button>
</div>
</div>
</div>
{/* Invoices Table */}
<div className="card-elevated overflow-hidden">
<div className="table-responsive">
<table className="table w-full">
<thead>
<tr>
<th>Invoice</th>
<th>Customer</th>
<th>Amount</th>
<th>Status</th>
<th>Due Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filteredInvoices.map((invoice) => (
<tr key={invoice.id}>
<td>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-lg flex items-center justify-center">
<FileText className="w-5 h-5 text-white" />
</div>
<div>
<p className="font-semibold text-slate-900 dark:text-white">{invoice.id}</p>
<p className="text-sm text-slate-600 dark:text-slate-400">{formatDate(invoice.issuedDate)}</p>
</div>
</div>
</td>
<td>
<p className="font-medium text-slate-900 dark:text-white">{invoice.customer}</p>
</td>
<td>
<DualCurrencyDisplay
amount={invoice.amount}
currency="INR"
className="font-semibold"
/>
</td>
<td>
<div className="flex items-center space-x-2">
{getStatusIcon(invoice.status)}
{getStatusBadge(invoice.status)}
</div>
</td>
<td>
<div className="flex items-center space-x-2">
<Calendar className="w-4 h-4 text-slate-400" />
<span className="text-sm">{formatDate(invoice.dueDate)}</span>
</div>
</td>
<td>
<div className="flex items-center space-x-2">
<button
onClick={() => handleViewInvoice(invoice)}
className="btn btn-ghost btn-sm"
title="View Details"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => handleMailInvoice(invoice)}
className="btn btn-ghost btn-sm"
title="Send Email"
>
<Mail className="w-4 h-4" />
</button>
<div className="relative">
<button
onClick={() => handleMoreOptions(invoice.id)}
className="btn btn-ghost btn-sm"
title="More Options"
>
<MoreHorizontal className="w-4 h-4" />
</button>
{showMoreOptions === invoice.id && (
<MoreOptionsDropdown
itemType="invoice"
onViewPerformance={() => handleViewInvoice(invoice)}
onDownloadReport={() => console.log('Download PDF')}
onSendNotification={() => console.log('Send reminder')}
onChangeTier={() => console.log('Change tier')}
onDeactivate={() => console.log('Mark as paid')}
onDelete={() => console.log('Delete invoice')}
onSendMail={() => handleMailInvoice(invoice)}
onClose={() => setShowMoreOptions(null)}
/>
)}
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Modals */}
<Modal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} title="Create New Invoice">
<AddInvoiceForm onSubmit={handleAddInvoice} onCancel={() => setIsAddModalOpen(false)} />
</Modal>
<Modal isOpen={isDetailModalOpen} onClose={() => setIsDetailModalOpen(false)} title="Invoice Details">
{selectedInvoice && (
<DetailView
type="invoice"
data={selectedInvoice}
/>
)}
</Modal>
<Modal isOpen={isMailModalOpen} onClose={() => setIsMailModalOpen(false)} title="Send Email">
{selectedInvoice && <MailComposeForm recipient={selectedInvoice.customer} onSubmit={() => setIsMailModalOpen(false)} onCancel={() => setIsMailModalOpen(false)} />}
</Modal>
</div>
</div>
);
};
export default Billing;

View File

@ -0,0 +1,354 @@
import React, { useState } from 'react';
import {
Users,
Search,
Filter,
Plus,
Eye,
Edit,
Mail,
MoreHorizontal,
Download,
Send,
UserCheck,
UserX,
Building,
Phone,
Mail as MailIcon,
Calendar,
DollarSign,
Server
} from 'lucide-react';
import { mockCustomers } from '../../data/mockData';
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
import { formatDate, formatNumber } from '../../utils/format';
import { cn } from '../../utils/cn';
import Modal from '../../components/Modal';
import DetailView from '../../components/DetailView';
import AddCustomerForm from '../../components/forms/AddCustomerForm';
import EditCustomerForm from '../../components/forms/EditCustomerForm';
import MailComposeForm from '../../components/forms/MailComposeForm';
import MoreOptionsDropdown from '../../components/MoreOptionsDropdown';
const Customers: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedStatus, setSelectedStatus] = useState('all');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isMailModalOpen, setIsMailModalOpen] = useState(false);
const [showMoreOptions, setShowMoreOptions] = useState<string | null>(null);
const [selectedCustomer, setSelectedCustomer] = useState<any>(null);
const filteredCustomers = mockCustomers.filter(customer => {
const matchesSearch = customer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
customer.email.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = selectedStatus === 'all' || customer.status === selectedStatus;
return matchesSearch && matchesStatus;
});
const stats = {
total: mockCustomers.length,
active: mockCustomers.filter(c => c.status === 'active').length,
pending: mockCustomers.filter(c => c.status === 'pending').length,
totalRevenue: mockCustomers.reduce((sum, c) => sum + c.totalSpent, 0)
};
const handleAddCustomer = (data: any) => {
console.log('Adding customer:', data);
setIsAddModalOpen(false);
};
const handleViewCustomer = (customer: any) => {
setSelectedCustomer(customer);
setIsDetailModalOpen(true);
};
const handleEditCustomer = (customer: any) => {
setSelectedCustomer(customer);
setIsEditModalOpen(true);
};
const handleMailCustomer = (customer: any) => {
setSelectedCustomer(customer);
setIsMailModalOpen(true);
};
const handleMoreOptions = (customerId: string) => {
setShowMoreOptions(showMoreOptions === customerId ? null : customerId);
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'active':
return <span className="badge badge-success">Active</span>;
case 'pending':
return <span className="badge badge-warning">Pending</span>;
case 'inactive':
return <span className="badge badge-danger">Inactive</span>;
default:
return <span className="badge badge-secondary">{status}</span>;
}
};
const getTierBadge = (tier: string) => {
switch (tier) {
case 'platinum':
return <span className="badge badge-primary">Platinum</span>;
case 'gold':
return <span className="badge badge-warning">Gold</span>;
case 'silver':
return <span className="badge badge-secondary">Silver</span>;
default:
return <span className="badge badge-secondary">{tier}</span>;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-900/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0 mb-8">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white">
Customers
</h1>
<p className="text-slate-600 dark:text-slate-400 mt-1">
Manage your customer relationships and accounts
</p>
</div>
<button
onClick={() => setIsAddModalOpen(true)}
className="btn btn-primary btn-lg"
>
<Plus className="w-5 h-5 mr-2" />
Add New Customer
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-8">
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Customers</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.total}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-slate-500 to-slate-600 rounded-xl flex items-center justify-center">
<Users className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Active Customers</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.active}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
<Users className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Pending</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.pending}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center">
<Users className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Revenue</p>
<DualCurrencyDisplay
amount={stats.totalRevenue}
currency="INR"
className="text-2xl font-bold"
/>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
<DollarSign className="w-6 h-6 text-white" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="card-elevated p-6 mb-8">
<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 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder="Search customers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pl-10 w-full"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="input"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="inactive">Inactive</option>
</select>
<button className="btn btn-secondary btn-md">
<Filter className="w-4 h-4 mr-2" />
Filters
</button>
</div>
</div>
</div>
{/* Customers Table */}
<div className="card-elevated overflow-hidden">
<div className="table-responsive">
<table className="table">
<thead>
<tr>
<th>Customer</th>
<th>Contact</th>
<th>Status</th>
<th>Tier</th>
<th>Total Spent</th>
<th>Instances</th>
<th>Last Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filteredCustomers.map((customer) => (
<tr key={customer.id}>
<td>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-lg flex items-center justify-center">
<Building className="w-5 h-5 text-white" />
</div>
<div>
<p className="font-semibold text-slate-900 dark:text-white">{customer.name}</p>
<p className="text-sm text-slate-500 dark:text-slate-400">{customer.email}</p>
</div>
</div>
</td>
<td>
<div className="flex items-center space-x-2">
<Phone className="w-4 h-4 text-slate-400" />
<p className="text-sm text-slate-600 dark:text-slate-400">{customer.phone}</p>
</div>
</td>
<td>
{getStatusBadge(customer.status)}
</td>
<td>
{getTierBadge(customer.tier)}
</td>
<td>
<DualCurrencyDisplay
amount={customer.totalSpent}
currency="INR"
className="font-semibold"
/>
</td>
<td>
<div className="flex items-center space-x-2">
<Server className="w-4 h-4 text-slate-400" />
<span className="text-sm font-medium">{customer.instances}</span>
</div>
</td>
<td>
<div className="flex items-center space-x-1">
<Calendar className="w-4 h-4 text-slate-400" />
<span className="text-sm">{formatDate(customer.lastActive)}</span>
</div>
</td>
<td>
<div className="flex items-center space-x-2">
<button
onClick={() => handleViewCustomer(customer)}
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
title="View Details"
>
<Eye className="w-4 h-4 text-slate-600 dark:text-slate-400" />
</button>
<button
onClick={() => handleEditCustomer(customer)}
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
title="Edit Customer"
>
<Edit className="w-4 h-4 text-slate-600 dark:text-slate-400" />
</button>
<button
onClick={() => handleMailCustomer(customer)}
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
title="Send Email"
>
<Mail className="w-4 h-4 text-slate-600 dark:text-slate-400" />
</button>
<div className="relative">
<button
onClick={() => handleMoreOptions(customer.id)}
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
title="More Options"
>
<MoreHorizontal className="w-4 h-4 text-slate-600 dark:text-slate-400" />
</button>
{showMoreOptions === customer.id && (
<MoreOptionsDropdown
itemType="customer"
onViewPerformance={() => handleViewCustomer(customer)}
onDownloadReport={() => console.log('Download report')}
onSendNotification={() => console.log('Send notification')}
onChangeTier={() => console.log('Change tier')}
onDeactivate={() => console.log('Deactivate customer')}
onDelete={() => console.log('Delete customer')}
onSendMail={() => handleMailCustomer(customer)}
onClose={() => setShowMoreOptions(null)}
/>
)}
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Modals */}
<Modal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} title="Add New Customer">
<AddCustomerForm onSubmit={handleAddCustomer} onCancel={() => setIsAddModalOpen(false)} />
</Modal>
<Modal isOpen={isDetailModalOpen} onClose={() => setIsDetailModalOpen(false)} title="Customer Details">
{selectedCustomer && <DetailView data={selectedCustomer} type="customer" />}
</Modal>
<Modal isOpen={isEditModalOpen} onClose={() => setIsEditModalOpen(false)} title="Edit Customer">
{selectedCustomer && <EditCustomerForm customer={selectedCustomer} onSubmit={handleEditCustomer} onCancel={() => setIsEditModalOpen(false)} />}
</Modal>
<Modal isOpen={isMailModalOpen} onClose={() => setIsMailModalOpen(false)} title="Send Email">
{selectedCustomer && <MailComposeForm recipient={selectedCustomer.name} onSubmit={() => setIsMailModalOpen(false)} onCancel={() => setIsMailModalOpen(false)} />}
</Modal>
</div>
</div>
);
};
export default Customers;

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

@ -0,0 +1,354 @@
import React, { useEffect } from 'react';
import { useAppSelector, useAppDispatch } from '../store/hooks';
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 } from '../utils/format';
import DualCurrencyDisplay from '../components/DualCurrencyDisplay';
import {
TrendingUp,
Users,
Server,
FileText,
DollarSign,
UserPlus,
Server as ServerIcon,
FileText as FileTextIcon,
Headphones,
Wallet,
BarChart3,
User,
Calendar,
Activity
} from 'lucide-react';
import { cn } from '../utils/cn';
const Dashboard: React.FC = () => {
const dispatch = useAppDispatch();
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 'customer_added':
return <UserPlus className="w-5 h-5 text-emerald-600" />;
case 'instance_created':
return <ServerIcon className="w-5 h-5 text-emerald-600" />;
case 'payment_received':
return <DollarSign className="w-5 h-5 text-emerald-600" />;
case 'support_ticket':
return <Headphones className="w-5 h-5 text-amber-600" />;
default:
return <FileTextIcon className="w-5 h-5 text-slate-600" />;
}
};
const getQuickActionColor = (color: string) => {
switch (color) {
case 'primary':
return 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900 dark:text-emerald-300 dark:hover:bg-emerald-800';
case 'success':
return 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900 dark:text-green-300 dark:hover:bg-green-800';
case 'warning':
return 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900 dark:text-amber-300 dark:hover:bg-amber-800';
case 'danger':
return 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900 dark:text-red-300 dark:hover:bg-red-800';
case 'secondary':
return 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800';
default:
return 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900 dark:text-emerald-300 dark:hover:bg-emerald-800';
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-900/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Enhanced Header with Hero Section */}
<div className="mb-12">
<div className="card-elevated p-8 bg-gradient-to-r from-emerald-500/10 to-emerald-600/10 dark:from-emerald-500/20 dark:to-emerald-600/20 border-emerald-200/50 dark:border-emerald-700/50">
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between space-y-6 lg:space-y-0">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-4">
<div className="w-12 h-12 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-2xl flex items-center justify-center shadow-lg">
<User className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-3xl lg:text-4xl font-bold bg-gradient-to-r from-slate-900 to-emerald-700 dark:from-white dark:to-emerald-300 bg-clip-text text-transparent">
Welcome back, Alex!
</h1>
<p className="text-slate-600 dark:text-slate-300 text-lg">
Here's your business overview for today
</p>
</div>
</div>
<div className="flex flex-wrap gap-4">
<div className="flex items-center space-x-2 text-slate-600 dark:text-slate-400">
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<span className="text-sm font-medium">Active Status</span>
</div>
<div className="flex items-center space-x-2 text-slate-600 dark:text-slate-400">
<Calendar className="w-4 h-4" />
<span className="text-sm">{new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</span>
</div>
</div>
</div>
<div className="card p-6 bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl border-emerald-200/50 dark:border-emerald-700/50">
<div className="text-center">
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 mb-2">Monthly Revenue</p>
<DualCurrencyDisplay
amount={2847500}
currency={stats.currency}
className="text-2xl lg:text-3xl font-bold"
/>
<div className="flex items-center justify-center mt-3">
<TrendingUp className="w-4 h-4 text-emerald-600 mr-1" />
<span className="text-sm text-emerald-600 font-medium">+23.5% this month</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Enhanced Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
<div className="card-elevated p-6 group hover:scale-105 transition-all duration-300">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 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-6 h-6 text-white" />
</div>
<div className="text-right">
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Revenue</p>
<DualCurrencyDisplay
amount={stats.totalRevenue}
currency={stats.currency}
className="text-xl font-bold"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<TrendingUp className="w-4 h-4 text-emerald-600" />
<span className="text-sm font-medium text-emerald-600">
+{stats.monthlyGrowth}%
</span>
</div>
<span className="text-xs text-slate-500 dark:text-slate-400">vs last month</span>
</div>
</div>
<div className="card-elevated p-6 group hover:scale-105 transition-all duration-300">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform duration-300">
<Users className="w-6 h-6 text-white" />
</div>
<div className="text-right">
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Customers</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">
{formatNumber(stats.totalCustomers)}
</p>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<TrendingUp className="w-4 h-4 text-blue-600" />
<span className="text-sm font-medium text-blue-600">+12</span>
</div>
<span className="text-xs text-slate-500 dark:text-slate-400">new this month</span>
</div>
</div>
<div className="card-elevated p-6 group hover:scale-105 transition-all duration-300">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform duration-300">
<Server className="w-6 h-6 text-white" />
</div>
<div className="text-right">
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Active Instances</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">
{formatNumber(stats.activeInstances)}
</p>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<TrendingUp className="w-4 h-4 text-purple-600" />
<span className="text-sm font-medium text-purple-600">+5</span>
</div>
<span className="text-xs text-slate-500 dark:text-slate-400">new instances</span>
</div>
</div>
<div className="card-elevated p-6 group hover:scale-105 transition-all duration-300">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform duration-300">
<FileText className="w-6 h-6 text-white" />
</div>
<div className="text-right">
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Commission Earned</p>
<DualCurrencyDisplay
amount={stats.commissionEarned}
currency={stats.currency}
className="text-xl font-bold"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<TrendingUp className="w-4 h-4 text-amber-600" />
<span className="text-sm font-medium text-amber-600">+18%</span>
</div>
<span className="text-xs text-slate-500 dark:text-slate-400">vs last month</span>
</div>
</div>
</div>
{/* Enhanced Quick Actions & Recent Activities */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
{/* Quick Actions */}
<div className="lg:col-span-1">
<div className="card-elevated p-6">
<div className="flex items-center space-x-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-xl flex items-center justify-center">
<BarChart3 className="w-5 h-5 text-white" />
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">
Quick Actions
</h3>
</div>
<div className="space-y-3">
{quickActions.map((action) => (
<button
key={action.id}
className={cn(
"w-full flex items-center p-4 rounded-xl transition-all duration-300 group hover:scale-105",
getQuickActionColor(action.color)
)}
>
<div className="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center mr-4 group-hover:scale-110 transition-transform duration-300">
{action.icon === 'UserPlus' && <UserPlus className="w-5 h-5" />}
{action.icon === 'Server' && <ServerIcon className="w-5 h-5" />}
{action.icon === 'FileText' && <FileTextIcon className="w-5 h-5" />}
{action.icon === 'Headphones' && <Headphones className="w-5 h-5" />}
{action.icon === 'Wallet' && <Wallet className="w-5 h-5" />}
{action.icon === 'BarChart3' && <BarChart3 className="w-5 h-5" />}
</div>
<div className="text-left flex-1">
<p className="font-semibold text-base">{action.title}</p>
<p className="text-sm opacity-80">{action.description}</p>
</div>
</button>
))}
</div>
</div>
</div>
{/* Recent Activities */}
<div className="lg:col-span-2">
<div className="card-elevated p-6">
<div className="flex items-center space-x-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
<Activity className="w-5 h-5 text-white" />
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">
Recent Activities
</h3>
</div>
<div className="space-y-4">
{recentActivities.map((activity) => (
<div key={activity.id} className="flex items-start space-x-4 p-4 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors duration-200">
<div className="flex-shrink-0 mt-1">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500/20 to-emerald-600/20 rounded-xl flex items-center justify-center">
{getActivityIcon(activity.type)}
</div>
</div>
<div className="flex-1 min-w-0">
<p className="text-base 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-sm text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 font-semibold hover:scale-105 transition-transform duration-200">
View all activities
</button>
</div>
</div>
</div>
</div>
{/* Enhanced Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="card-elevated p-6">
<div className="flex items-center space-x-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-xl flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-white" />
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">
Revenue Overview
</h3>
</div>
<div className="h-64 bg-gradient-to-br from-emerald-50 to-emerald-100 dark:from-emerald-900/20 dark:to-emerald-800/20 rounded-xl flex items-center justify-center border border-emerald-200/50 dark:border-emerald-700/50">
<div className="text-center">
<TrendingUp className="w-12 h-12 text-emerald-400 mx-auto mb-3" />
<p className="text-emerald-600 dark:text-emerald-400 font-medium">
Revenue chart will be added here
</p>
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center space-x-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
<Users className="w-5 h-5 text-white" />
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">
Customer Growth
</h3>
</div>
<div className="h-64 bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-xl flex items-center justify-center border border-blue-200/50 dark:border-blue-700/50">
<div className="text-center">
<Users className="w-12 h-12 text-blue-400 mx-auto mb-3" />
<p className="text-blue-600 dark:text-blue-400 font-medium">
Customer growth chart will be added here
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@ -0,0 +1,350 @@
import React, { useState } from 'react';
import {
Server,
Search,
Filter,
Plus,
Eye,
Edit,
Play,
Pause,
Trash2,
MoreHorizontal,
Download,
RefreshCw,
Activity,
Cpu,
HardDrive,
Wifi,
Calendar,
DollarSign
} from 'lucide-react';
import { mockInstances } from '../../data/mockData';
import { formatDate, formatNumber } from '../../utils/format';
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
import Modal from '../../components/Modal';
import DetailView from '../../components/DetailView';
import AddInstanceForm from '../../components/forms/AddInstanceForm';
import EditInstanceForm from '../../components/forms/EditInstanceForm';
import MoreOptionsDropdown from '../../components/MoreOptionsDropdown';
const Instances: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedStatus, setSelectedStatus] = useState('all');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [showMoreOptions, setShowMoreOptions] = useState<string | null>(null);
const [selectedInstance, setSelectedInstance] = useState<any>(null);
const filteredInstances = mockInstances.filter(instance => {
const matchesSearch = instance.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
instance.customer.toLowerCase().includes(searchTerm.toLowerCase()) ||
instance.type.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = selectedStatus === 'all' || instance.status === selectedStatus;
return matchesSearch && matchesStatus;
});
const stats = {
total: mockInstances.length,
running: mockInstances.filter(i => i.status === 'running').length,
stopped: mockInstances.filter(i => i.status === 'stopped').length,
totalCost: mockInstances.reduce((sum, i) => sum + i.monthlyCost, 0)
};
const handleAddInstance = (data: any) => {
console.log('Adding instance:', data);
setIsAddModalOpen(false);
};
const handleViewInstance = (instance: any) => {
setSelectedInstance(instance);
setIsDetailModalOpen(true);
};
const handleEditInstance = (instance: any) => {
setSelectedInstance(instance);
setIsEditModalOpen(true);
};
const handleMoreOptions = (instanceId: string) => {
setShowMoreOptions(showMoreOptions === instanceId ? null : instanceId);
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'running':
return <span className="badge badge-success">Running</span>;
case 'stopped':
return <span className="badge badge-warning">Stopped</span>;
case 'starting':
return <span className="badge badge-primary">Starting</span>;
case 'stopping':
return <span className="badge badge-warning">Stopping</span>;
default:
return <span className="badge badge-secondary">{status}</span>;
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'running':
return <Play className="w-4 h-4 text-green-600" />;
case 'stopped':
return <Pause className="w-4 h-4 text-amber-600" />;
default:
return <Activity className="w-4 h-4 text-slate-600" />;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-900/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0 mb-8">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white">
Cloud Instances
</h1>
<p className="text-slate-600 dark:text-slate-400 mt-2">
Manage your cloud infrastructure and server instances
</p>
</div>
<button
onClick={() => setIsAddModalOpen(true)}
className="btn btn-primary btn-md"
>
<Plus className="w-5 h-5 mr-2" />
Add Instance
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-8">
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Instances</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.total}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-slate-500 to-slate-600 rounded-xl flex items-center justify-center">
<Server className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Running</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.running}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
<Play className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Stopped</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.stopped}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center">
<Pause className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Monthly Cost</p>
<DualCurrencyDisplay
amount={stats.totalCost}
currency="INR"
className="text-2xl font-bold"
/>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-xl flex items-center justify-center">
<DollarSign className="w-6 h-6 text-white" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="card-elevated p-6 mb-8">
<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 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder="Search instances..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pl-10 w-full"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="input"
>
<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>
</select>
<button className="btn btn-secondary btn-md">
<Filter className="w-4 h-4 mr-2" />
Filters
</button>
</div>
</div>
</div>
{/* Instances Table */}
<div className="card-elevated overflow-hidden">
<div className="table-responsive">
<table className="table w-full">
<thead>
<tr>
<th>Instance</th>
<th>Customer</th>
<th>Type</th>
<th>Status</th>
<th>Resources</th>
<th>Monthly Cost</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filteredInstances.map((instance) => (
<tr key={instance.id}>
<td>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-lg flex items-center justify-center">
<Server className="w-5 h-5 text-white" />
</div>
<div>
<p className="font-semibold text-slate-900 dark:text-white">{instance.name}</p>
<p className="text-sm text-slate-600 dark:text-slate-400">{instance.id}</p>
</div>
</div>
</td>
<td>
<p className="font-medium text-slate-900 dark:text-white">{instance.customer}</p>
</td>
<td>
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">{instance.type}</p>
</td>
<td>
<div className="flex items-center space-x-2">
{getStatusIcon(instance.status)}
{getStatusBadge(instance.status)}
</div>
</td>
<td>
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Cpu className="w-4 h-4 text-slate-400" />
<span className="text-sm">{instance.cpu} vCPU</span>
</div>
<div className="flex items-center space-x-2">
<HardDrive className="w-4 h-4 text-slate-400" />
<span className="text-sm">{instance.memory} GB RAM</span>
</div>
</div>
</td>
<td>
<DualCurrencyDisplay
amount={instance.monthlyCost}
currency="INR"
className="font-semibold"
/>
</td>
<td>
<div className="flex items-center space-x-2">
<button
onClick={() => handleViewInstance(instance)}
className="btn btn-ghost btn-sm"
title="View Details"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => handleEditInstance(instance)}
className="btn btn-ghost btn-sm"
title="Edit Instance"
>
<Edit className="w-4 h-4" />
</button>
<div className="relative">
<button
onClick={() => handleMoreOptions(instance.id)}
className="btn btn-ghost btn-sm"
title="More Options"
>
<MoreHorizontal className="w-4 h-4" />
</button>
{showMoreOptions === instance.id && (
<MoreOptionsDropdown
itemType="instance"
onViewPerformance={() => handleViewInstance(instance)}
onDownloadReport={() => console.log('Download logs')}
onSendNotification={() => console.log('Send alert')}
onChangeTier={() => console.log('Change tier')}
onDeactivate={() => console.log('Stop instance')}
onDelete={() => console.log('Delete instance')}
onSendMail={() => console.log('Send email')}
onClose={() => setShowMoreOptions(null)}
/>
)}
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Modals */}
<Modal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} title="Add New Instance">
<AddInstanceForm onSubmit={handleAddInstance} onCancel={() => setIsAddModalOpen(false)} />
</Modal>
<Modal isOpen={isDetailModalOpen} onClose={() => setIsDetailModalOpen(false)} title="Instance Details">
{selectedInstance && (
<DetailView
type="instance"
data={selectedInstance}
/>
)}
</Modal>
<Modal isOpen={isEditModalOpen} onClose={() => setIsEditModalOpen(false)} title="Edit Instance">
{selectedInstance && (
<EditInstanceForm
instance={selectedInstance}
onSubmit={(data) => {
console.log('Updating instance:', data);
setIsEditModalOpen(false);
}}
onCancel={() => setIsEditModalOpen(false)}
/>
)}
</Modal>
</div>
</div>
);
};
export default Instances;

302
src/pages/Reports/index.tsx Normal file
View File

@ -0,0 +1,302 @@
import React, { useState } from 'react';
import {
BarChart3,
TrendingUp,
Download,
Calendar,
DollarSign,
Users,
Server,
FileText,
Filter,
RefreshCw,
Eye,
Share2
} from 'lucide-react';
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
const Reports: React.FC = () => {
const [selectedPeriod, setSelectedPeriod] = useState('month');
const [selectedReport, setSelectedReport] = useState('revenue');
const reportData = {
revenue: {
current: 2847500,
previous: 2300000,
growth: 23.8,
trend: 'up'
},
customers: {
current: 156,
previous: 142,
growth: 9.9,
trend: 'up'
},
instances: {
current: 89,
previous: 76,
growth: 17.1,
trend: 'up'
},
tickets: {
current: 23,
previous: 28,
growth: -17.9,
trend: 'down'
}
};
const reports = [
{
id: 'revenue',
name: 'Revenue Report',
description: 'Monthly revenue analysis and trends',
icon: DollarSign,
color: 'emerald'
},
{
id: 'customers',
name: 'Customer Report',
description: 'Customer growth and retention analysis',
icon: Users,
color: 'blue'
},
{
id: 'instances',
name: 'Instance Report',
description: 'Cloud instance usage and performance',
icon: Server,
color: 'purple'
},
{
id: 'support',
name: 'Support Report',
description: 'Support ticket analysis and resolution times',
icon: FileText,
color: 'amber'
}
];
const getReportIcon = (color: string) => {
const colorClasses = {
emerald: 'bg-gradient-to-br from-emerald-500 to-emerald-600',
blue: 'bg-gradient-to-br from-blue-500 to-blue-600',
purple: 'bg-gradient-to-br from-purple-500 to-purple-600',
amber: 'bg-gradient-to-br from-amber-500 to-amber-600'
};
return colorClasses[color as keyof typeof colorClasses] || colorClasses.emerald;
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-900/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0 mb-8">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white">
Reports & Analytics
</h1>
<p className="text-slate-600 dark:text-slate-400 mt-2">
Comprehensive business insights and performance metrics
</p>
</div>
<div className="flex items-center space-x-3">
<button className="btn btn-secondary btn-md">
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</button>
<button className="btn btn-primary btn-md">
<Download className="w-4 h-4 mr-2" />
Export All
</button>
</div>
</div>
{/* Filters */}
<div className="card-elevated p-6 mb-8">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Time Period
</label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="input w-full"
>
<option value="week">Last Week</option>
<option value="month">Last Month</option>
<option value="quarter">Last Quarter</option>
<option value="year">Last Year</option>
</select>
</div>
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Report Type
</label>
<select
value={selectedReport}
onChange={(e) => setSelectedReport(e.target.value)}
className="input w-full"
>
<option value="revenue">Revenue Report</option>
<option value="customers">Customer Report</option>
<option value="instances">Instance Report</option>
<option value="support">Support Report</option>
</select>
</div>
</div>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-8">
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Revenue</p>
<DualCurrencyDisplay
amount={reportData.revenue.current}
currency="INR"
className="text-2xl font-bold"
/>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-xl flex items-center justify-center">
<DollarSign className="w-6 h-6 text-white" />
</div>
</div>
<div className="flex items-center mt-3">
<TrendingUp className={`w-4 h-4 mr-1 ${reportData.revenue.trend === 'up' ? 'text-emerald-600' : 'text-red-600'}`} />
<span className={`text-sm font-medium ${reportData.revenue.trend === 'up' ? 'text-emerald-600' : 'text-red-600'}`}>
{reportData.revenue.growth}% vs last period
</span>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Customers</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{reportData.customers.current}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
<Users className="w-6 h-6 text-white" />
</div>
</div>
<div className="flex items-center mt-3">
<TrendingUp className={`w-4 h-4 mr-1 ${reportData.customers.trend === 'up' ? 'text-emerald-600' : 'text-red-600'}`} />
<span className={`text-sm font-medium ${reportData.customers.trend === 'up' ? 'text-emerald-600' : 'text-red-600'}`}>
{reportData.customers.growth}% vs last period
</span>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Active Instances</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{reportData.instances.current}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center">
<Server className="w-6 h-6 text-white" />
</div>
</div>
<div className="flex items-center mt-3">
<TrendingUp className={`w-4 h-4 mr-1 ${reportData.instances.trend === 'up' ? 'text-emerald-600' : 'text-red-600'}`} />
<span className={`text-sm font-medium ${reportData.instances.trend === 'up' ? 'text-emerald-600' : 'text-red-600'}`}>
{reportData.instances.growth}% vs last period
</span>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Open Tickets</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{reportData.tickets.current}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center">
<FileText className="w-6 h-6 text-white" />
</div>
</div>
<div className="flex items-center mt-3">
<TrendingUp className={`w-4 h-4 mr-1 ${reportData.tickets.trend === 'up' ? 'text-emerald-600' : 'text-red-600'}`} />
<span className={`text-sm font-medium ${reportData.tickets.trend === 'up' ? 'text-emerald-600' : 'text-red-600'}`}>
{Math.abs(reportData.tickets.growth)}% vs last period
</span>
</div>
</div>
</div>
{/* Report Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{reports.map((report) => (
<div key={report.id} className="card-elevated p-6 hover:scale-105 transition-transform duration-300">
<div className="flex items-start justify-between mb-4">
<div className={`w-12 h-12 ${getReportIcon(report.color)} rounded-xl flex items-center justify-center`}>
<report.icon className="w-6 h-6 text-white" />
</div>
<div className="flex items-center space-x-2">
<button className="btn btn-ghost btn-sm">
<Eye className="w-4 h-4" />
</button>
<button className="btn btn-ghost btn-sm">
<Download className="w-4 h-4" />
</button>
<button className="btn btn-ghost btn-sm">
<Share2 className="w-4 h-4" />
</button>
</div>
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">
{report.name}
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400 mb-4">
{report.description}
</p>
<div className="flex items-center justify-between">
<span className="text-xs text-slate-500 dark:text-slate-400">
Last updated: {new Date().toLocaleDateString()}
</span>
<button className="btn btn-primary btn-sm">
Generate Report
</button>
</div>
</div>
))}
</div>
{/* Chart Placeholder */}
<div className="card-elevated p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">
Revenue Trend
</h3>
<div className="flex items-center space-x-2">
<button className="btn btn-outline btn-sm">
<Calendar className="w-4 h-4 mr-2" />
Last 30 Days
</button>
<button className="btn btn-outline btn-sm">
<Download className="w-4 h-4 mr-2" />
Export
</button>
</div>
</div>
<div className="h-64 bg-gradient-to-br from-emerald-50 to-emerald-100 dark:from-emerald-900/20 dark:to-emerald-800/20 rounded-xl flex items-center justify-center border border-emerald-200/50 dark:border-emerald-700/50">
<div className="text-center">
<BarChart3 className="w-12 h-12 text-emerald-400 mx-auto mb-3" />
<p className="text-emerald-600 dark:text-emerald-400 font-medium">
Revenue trend chart will be displayed here
</p>
<p className="text-sm text-emerald-500 dark:text-emerald-500 mt-2">
Interactive charts and analytics coming soon
</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default Reports;

362
src/pages/Support/index.tsx Normal file
View File

@ -0,0 +1,362 @@
import React, { useState } from 'react';
import {
Headphones,
Search,
Filter,
Plus,
Eye,
MessageSquare,
Clock,
CheckCircle,
AlertCircle,
MoreHorizontal,
User,
Calendar,
Tag,
TrendingUp,
MessageCircle,
Phone,
Mail
} from 'lucide-react';
import { mockSupportTickets } from '../../data/mockData';
import { formatDate } from '../../utils/format';
import Modal from '../../components/Modal';
import DetailView from '../../components/DetailView';
import AddTicketForm from '../../components/forms/AddTicketForm';
import MailComposeForm from '../../components/forms/MailComposeForm';
import MoreOptionsDropdown from '../../components/MoreOptionsDropdown';
const Support: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedStatus, setSelectedStatus] = useState('all');
const [selectedPriority, setSelectedPriority] = useState('all');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [isMailModalOpen, setIsMailModalOpen] = useState(false);
const [showMoreOptions, setShowMoreOptions] = useState<string | null>(null);
const [selectedTicket, setSelectedTicket] = useState<any>(null);
const filteredTickets = mockSupportTickets.filter(ticket => {
const matchesSearch = ticket.customer.toLowerCase().includes(searchTerm.toLowerCase()) ||
ticket.subject.toLowerCase().includes(searchTerm.toLowerCase()) ||
ticket.id.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = selectedStatus === 'all' || ticket.status === selectedStatus;
const matchesPriority = selectedPriority === 'all' || ticket.priority === selectedPriority;
return matchesSearch && matchesStatus && matchesPriority;
});
const stats = {
total: mockSupportTickets.length,
open: mockSupportTickets.filter(t => t.status === 'open').length,
inProgress: mockSupportTickets.filter(t => t.status === 'in_progress').length,
resolved: mockSupportTickets.filter(t => t.status === 'resolved').length,
highPriority: mockSupportTickets.filter(t => t.priority === 'high').length
};
const handleAddTicket = (data: any) => {
console.log('Adding ticket:', data);
setIsAddModalOpen(false);
};
const handleViewTicket = (ticket: any) => {
setSelectedTicket(ticket);
setIsDetailModalOpen(true);
};
const handleMailTicket = (ticket: any) => {
setSelectedTicket(ticket);
setIsMailModalOpen(true);
};
const handleMoreOptions = (ticketId: string) => {
setShowMoreOptions(showMoreOptions === ticketId ? null : ticketId);
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'open':
return <span className="badge badge-warning">Open</span>;
case 'in_progress':
return <span className="badge badge-primary">In Progress</span>;
case 'resolved':
return <span className="badge badge-success">Resolved</span>;
case 'closed':
return <span className="badge badge-secondary">Closed</span>;
default:
return <span className="badge badge-secondary">{status}</span>;
}
};
const getPriorityBadge = (priority: string) => {
switch (priority) {
case 'high':
return <span className="badge badge-danger">High</span>;
case 'medium':
return <span className="badge badge-warning">Medium</span>;
case 'low':
return <span className="badge badge-success">Low</span>;
default:
return <span className="badge badge-secondary">{priority}</span>;
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'open':
return <Clock className="w-4 h-4 text-amber-600" />;
case 'in_progress':
return <MessageCircle className="w-4 h-4 text-blue-600" />;
case 'resolved':
return <CheckCircle className="w-4 h-4 text-green-600" />;
default:
return <Clock className="w-4 h-4 text-slate-600" />;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-900/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0 mb-8">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white">
Support Tickets
</h1>
<p className="text-slate-600 dark:text-slate-400 mt-2">
Manage customer support requests and tickets
</p>
</div>
<button
onClick={() => setIsAddModalOpen(true)}
className="btn btn-primary btn-md"
>
<Plus className="w-5 h-5 mr-2" />
Create Ticket
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 sm:gap-6 mb-8">
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Tickets</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.total}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-slate-500 to-slate-600 rounded-xl flex items-center justify-center">
<Headphones className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Open</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.open}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center">
<Clock className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">In Progress</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.inProgress}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-xl flex items-center justify-center">
<MessageCircle className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Resolved</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.resolved}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="card-elevated p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">High Priority</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.highPriority}</p>
</div>
<div className="w-12 h-12 bg-gradient-to-br from-red-500 to-red-600 rounded-xl flex items-center justify-center">
<AlertCircle className="w-6 h-6 text-white" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="card-elevated p-6 mb-8">
<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 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder="Search tickets..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pl-10 w-full"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="input"
>
<option value="all">All Status</option>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="resolved">Resolved</option>
<option value="closed">Closed</option>
</select>
<select
value={selectedPriority}
onChange={(e) => setSelectedPriority(e.target.value)}
className="input"
>
<option value="all">All Priority</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<button className="btn btn-secondary btn-md">
<Filter className="w-4 h-4 mr-2" />
Filters
</button>
</div>
</div>
</div>
{/* Tickets Table */}
<div className="card-elevated overflow-hidden">
<div className="table-responsive">
<table className="table w-full">
<thead>
<tr>
<th>Ticket</th>
<th>Customer</th>
<th>Subject</th>
<th>Status</th>
<th>Priority</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filteredTickets.map((ticket) => (
<tr key={ticket.id}>
<td>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-lg flex items-center justify-center">
<Headphones className="w-5 h-5 text-white" />
</div>
<div>
<p className="font-semibold text-slate-900 dark:text-white">{ticket.id}</p>
<p className="text-sm text-slate-600 dark:text-slate-400">{formatDate(ticket.createdAt)}</p>
</div>
</div>
</td>
<td>
<p className="font-medium text-slate-900 dark:text-white">{ticket.customer}</p>
</td>
<td>
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">{ticket.subject}</p>
</td>
<td>
{getStatusBadge(ticket.status)}
</td>
<td>
{getPriorityBadge(ticket.priority)}
</td>
<td>
<div className="flex items-center space-x-2">
<Calendar className="w-4 h-4 text-slate-400" />
<span className="text-sm">{formatDate(ticket.createdAt)}</span>
</div>
</td>
<td>
<div className="flex items-center space-x-2">
<button
onClick={() => handleViewTicket(ticket)}
className="btn btn-ghost btn-sm"
title="View Details"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => handleMailTicket(ticket)}
className="btn btn-ghost btn-sm"
title="Send Email"
>
<Mail className="w-4 h-4" />
</button>
<div className="relative">
<button
onClick={() => handleMoreOptions(ticket.id)}
className="btn btn-ghost btn-sm"
title="More Options"
>
<MoreHorizontal className="w-4 h-4" />
</button>
{showMoreOptions === ticket.id && (
<MoreOptionsDropdown
itemType="ticket"
onViewPerformance={() => handleViewTicket(ticket)}
onDownloadReport={() => console.log('Download logs')}
onSendNotification={() => console.log('Send notification')}
onChangeTier={() => console.log('Change priority')}
onDeactivate={() => console.log('Close ticket')}
onDelete={() => console.log('Delete ticket')}
onSendMail={() => handleMailTicket(ticket)}
onClose={() => setShowMoreOptions(null)}
/>
)}
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Modals */}
<Modal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} title="Create New Ticket">
<AddTicketForm onSubmit={handleAddTicket} onCancel={() => setIsAddModalOpen(false)} />
</Modal>
<Modal isOpen={isDetailModalOpen} onClose={() => setIsDetailModalOpen(false)} title="Ticket Details">
{selectedTicket && (
<DetailView
type="ticket"
data={selectedTicket}
/>
)}
</Modal>
<Modal isOpen={isMailModalOpen} onClose={() => setIsMailModalOpen(false)} title="Send Email">
{selectedTicket && <MailComposeForm recipient={selectedTicket.customer} onSubmit={() => setIsMailModalOpen(false)} onCancel={() => setIsMailModalOpen(false)} />}
</Modal>
</div>
</div>
);
};
export default Support;

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

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

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

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

View File

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

View File

@ -0,0 +1,81 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface DashboardStats {
totalRevenue: number;
totalCustomers: number;
activeInstances: number;
pendingInvoices: number;
monthlyGrowth: number;
commissionEarned: number;
currency?: 'USD' | 'INR';
}
export interface RecentActivity {
id: string;
type: '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,
totalCustomers: 0,
activeInstances: 0,
pendingInvoices: 0,
monthlyGrowth: 0,
commissionEarned: 0,
},
recentActivities: [],
quickActions: [],
isLoading: false,
error: null,
};
const dashboardSlice = createSlice({
name: 'dashboard',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
setStats: (state, action: PayloadAction<DashboardStats>) => {
state.stats = action.payload;
},
setRecentActivities: (state, action: PayloadAction<RecentActivity[]>) => {
state.recentActivities = action.payload;
},
setQuickActions: (state, action: PayloadAction<QuickAction[]>) => {
state.quickActions = action.payload;
},
updateStats: (state, action: PayloadAction<Partial<DashboardStats>>) => {
state.stats = { ...state.stats, ...action.payload };
},
},
});
export const { setLoading, setError, setStats, setRecentActivities, setQuickActions, updateStats } = dashboardSlice.actions;
export default dashboardSlice.reducer;

View File

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

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

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

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

@ -0,0 +1,131 @@
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 => {
return new Intl.DateTimeFormat('en-IN', {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(new Date(date));
};
export const formatDateTime = (date: string | Date): string => {
return new Intl.DateTimeFormat('en-IN', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(date));
};
export const formatRelativeTime = (date: string | Date): string => {
const now = new Date();
const targetDate = new Date(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);
};
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)}%`;
};

123
tailwind.config.js Normal file
View File

@ -0,0 +1,123 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
// Modern Emerald Theme
primary: {
50: '#ecfdf5',
100: '#d1fae5',
200: '#a7f3d0',
300: '#6ee7b7',
400: '#34d399',
500: '#10b981',
600: '#059669',
700: '#047857',
800: '#065f46',
900: '#064e3b',
},
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'),
],
}