v1.0.1-alpha

This commit is contained in:
rohit 2025-08-11 18:04:34 +05:30
parent 8ac3c89b10
commit 7f8480a03d
26 changed files with 3072 additions and 393 deletions

View File

@ -323,7 +323,7 @@ function App() {
<Route path="/reseller-dashboard/billing" element={
<ProtectedRoute requiredRole="reseller_admin">
<ResellerLayout>
<PlaceholderPage title="Billing & Payments" description="Manage your billing, invoices, and payment methods" />
<ResellerBilling />
</ResellerLayout>
</ProtectedRoute>
} />
@ -505,7 +505,7 @@ function App() {
</Routes>
<CookieConsent />
<Toast />
<AuthDebug />
{/* <AuthDebug /> */}
<DeveloperFeedback />
</div>
</Router>

View File

@ -27,17 +27,13 @@ const AuthInitializer: React.FC<{ children: React.ReactNode }> = ({ children })
// Try to get current user
try {
await dispatch(getCurrentUser()).unwrap();
console.log('User loaded successfully');
} catch (error) {
console.log('Failed to get current user, trying to refresh token...', error);
// If getting user fails, try to refresh token
try {
await dispatch(refreshUserToken()).unwrap();
// Try to get user again after token refresh
await dispatch(getCurrentUser()).unwrap();
console.log('User loaded successfully after token refresh');
} catch (refreshError) {
console.log('Token refresh failed, clearing auth data...', refreshError);
// Clear invalid tokens and logout
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
@ -45,8 +41,6 @@ const AuthInitializer: React.FC<{ children: React.ReactNode }> = ({ children })
dispatch(logout());
}
}
} else {
console.log('No tokens found in localStorage');
}
} catch (error) {
console.error('Auth initialization error:', error);

View File

@ -0,0 +1,265 @@
import React, { useState, useEffect } from 'react';
import { TrendingUp, DollarSign, BarChart3, Calendar, LineChart } from 'lucide-react';
import { formatCurrency, formatPercentage } from '../utils/format';
import { cn } from '../utils/cn';
interface CommissionTrendsProps {
vendorId: string;
}
interface TrendData {
month: string;
revenue: number;
commission: number;
commissionRate: number;
}
interface SummaryData {
totalRevenue: number;
totalCommission: number;
averageCommissionRate: number;
}
const CommissionTrends: React.FC<CommissionTrendsProps> = ({ vendorId }) => {
const [trends, setTrends] = useState<TrendData[]>([]);
const [summary, setSummary] = useState<SummaryData>({
totalRevenue: 0,
totalCommission: 0,
averageCommissionRate: 0
});
const [period, setPeriod] = useState('6months');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchCommissionTrends();
}, [vendorId, period]);
const fetchCommissionTrends = async () => {
try {
setLoading(true);
setError(null);
const token = localStorage.getItem('accessToken');
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/vendors/dashboard/commission-trends?period=${period}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch commission trends data');
}
const data = await response.json();
if (data.success) {
setTrends(data.data.trends);
setSummary(data.data.summary);
} else {
throw new Error(data.message || 'Failed to fetch data');
}
} catch (error: any) {
console.error('Error fetching commission trends:', error);
setError(error.message);
} finally {
setLoading(false);
}
};
const getMonthLabel = (monthString: string) => {
const [year, month] = monthString.split('-');
const date = new Date(parseInt(year), parseInt(month) - 1);
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
};
const getTrendColor = (rate: number) => {
if (rate >= 20) return 'text-green-600';
if (rate >= 15) return 'text-blue-600';
if (rate >= 10) return 'text-yellow-600';
return 'text-slate-600';
};
if (loading) {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Commission Trends</h2>
<div className="animate-pulse bg-slate-200 dark:bg-slate-700 h-10 w-32 rounded-lg"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[1, 2, 3].map((i) => (
<div key={i} className="animate-pulse bg-slate-200 dark:bg-slate-700 h-24 rounded-lg"></div>
))}
</div>
<div className="animate-pulse bg-slate-200 dark:bg-slate-700 h-96 rounded-lg"></div>
</div>
);
}
if (error) {
return (
<div className="text-center py-12">
<div className="text-red-500 text-lg mb-2">Error loading commission trends</div>
<div className="text-slate-600 dark:text-slate-400 mb-4">{error}</div>
<button
onClick={fetchCommissionTrends}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Retry
</button>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Commission Trends</h2>
<p className="text-slate-600 dark:text-slate-400">Track revenue and commission trends over time</p>
</div>
<div className="flex items-center space-x-2">
<Calendar className="w-4 h-4 text-slate-500" />
<select
value={period}
onChange={(e) => setPeriod(e.target.value)}
className="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="3months">Last 3 Months</option>
<option value="6months">Last 6 Months</option>
<option value="12months">Last 12 Months</option>
</select>
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Revenue</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">
{formatCurrency(summary.totalRevenue)}
</p>
</div>
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center">
<DollarSign className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Commission</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">
{formatCurrency(summary.totalCommission)}
</p>
</div>
<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>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Avg Commission Rate</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">
{formatPercentage(summary.averageCommissionRate)}
</p>
</div>
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
<BarChart3 className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
</div>
{/* Trends Chart */}
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-6">
<div className="mb-6">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">Monthly Trends</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
Revenue and commission trends over the selected period
</p>
</div>
{trends.length > 0 ? (
<div className="space-y-6">
{/* Chart Visualization */}
<div className="h-80 bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4 flex items-center justify-center">
<div className="text-center">
<LineChart className="w-16 h-16 text-slate-400 mx-auto mb-4" />
<p className="text-slate-600 dark:text-slate-400">
Chart visualization will be implemented here
</p>
<p className="text-sm text-slate-500 dark:text-slate-500">
Using a charting library like Chart.js or Recharts
</p>
</div>
</div>
{/* Trends Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 dark:bg-slate-700/50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Month
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Revenue
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Commission
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Commission Rate
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-slate-800 divide-y divide-slate-200 dark:divide-slate-700">
{trends.map((trend, index) => (
<tr key={trend.month} className="hover:bg-slate-50 dark:hover:bg-slate-700/50">
<td className="px-4 py-4 whitespace-nowrap text-sm font-medium text-slate-900 dark:text-white">
{getMonthLabel(trend.month)}
</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-white">
{formatCurrency(trend.revenue)}
</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-white">
{formatCurrency(trend.commission)}
</td>
<td className="px-4 py-4 whitespace-nowrap">
<span className={cn(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
getTrendColor(trend.commissionRate)
)}>
{formatPercentage(trend.commissionRate)}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="text-center py-12">
<BarChart3 className="w-12 h-12 text-slate-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-slate-900 dark:text-white mb-2">No Trend Data</h3>
<p className="text-slate-600 dark:text-slate-400">
No commission trend data available for the selected period.
</p>
</div>
)}
</div>
</div>
);
};
export default CommissionTrends;

View File

@ -1,16 +1,16 @@
import React, { useState } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import {
MessageSquare,
Send,
X,
AlertCircle,
CheckCircle,
Clock,
MessageCircle,
Bug,
Lightbulb,
HelpCircle,
Star
AlertTriangle,
Star,
X,
Send,
Minimize2,
Maximize2
} from 'lucide-react';
import toast from 'react-hot-toast';
interface FeedbackTicket {
id: string;
@ -41,7 +41,7 @@ const DeveloperFeedback: React.FC = () => {
const ticketTypes = [
{ id: 'bug', label: 'Bug Report', icon: Bug, color: 'text-red-600' },
{ id: 'feature', label: 'Feature Request', icon: Lightbulb, color: 'text-blue-600' },
{ id: 'question', label: 'Question', icon: HelpCircle, color: 'text-green-600' },
{ id: 'question', label: 'Question', icon: AlertTriangle, color: 'text-green-600' },
{ id: 'general', label: 'General Feedback', icon: Star, color: 'text-yellow-600' }
];
@ -54,7 +54,7 @@ const DeveloperFeedback: React.FC = () => {
const handleSubmit = async () => {
if (!currentTicket.title || !currentTicket.description) {
alert('Please fill in all required fields');
toast.error('Please fill in all required fields');
return;
}
@ -107,9 +107,10 @@ const DeveloperFeedback: React.FC = () => {
setSubmitted(true);
setTimeout(() => setSubmitted(false), 3000);
toast.success('Feedback submitted successfully!');
} catch (error) {
console.error('Error submitting feedback:', error);
alert('Failed to submit feedback. Please try again.');
toast.error('Failed to submit feedback. Please try again.');
} finally {
setSubmitting(false);
}
@ -127,11 +128,11 @@ const DeveloperFeedback: React.FC = () => {
const getStatusIcon = (status: string) => {
switch (status) {
case 'open': return <Clock className="h-4 w-4" />;
case 'in-progress': return <AlertCircle className="h-4 w-4" />;
case 'resolved': return <CheckCircle className="h-4 w-4" />;
case 'open': return <Minimize2 className="h-4 w-4" />;
case 'in-progress': return <AlertTriangle className="h-4 w-4" />;
case 'resolved': return <Maximize2 className="h-4 w-4" />;
case 'closed': return <X className="h-4 w-4" />;
default: return <Clock className="h-4 w-4" />;
default: return <Minimize2 className="h-4 w-4" />;
}
};
@ -143,7 +144,7 @@ const DeveloperFeedback: React.FC = () => {
className="fixed bottom-20 right-4 bg-blue-600 hover:bg-blue-700 text-white rounded-full p-3 shadow-lg z-[9998] transition-all duration-200"
title="Developer Feedback"
>
<MessageSquare className="h-6 w-6" />
<MessageCircle className="h-6 w-6" />
</button>
{/* Modal */}
@ -180,7 +181,7 @@ const DeveloperFeedback: React.FC = () => {
{submitted && (
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-4">
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400 mr-2" />
<Maximize2 className="h-5 w-5 text-green-600 dark:text-green-400 mr-2" />
<span className="text-green-800 dark:text-green-200">
Feedback submitted successfully! Thank you for your input.
</span>

View File

@ -151,7 +151,6 @@ const DraggableFeedback: React.FC<DraggableFeedbackProps> = ({
try {
// Here you would typically send the feedback to your backend
console.log('Feedback submitted:', { feedback, email, rating });
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));

View File

@ -33,7 +33,7 @@ import { cn } from '../../utils/cn';
const resellerNavigation = [
{ name: 'Dashboard', href: '/reseller-dashboard', icon: Home },
{ name: 'Customers', href: '/reseller-dashboard/customers', icon: Users },
{ name: 'Cloud Instances', href: '/reseller-dashboard/instances', icon: Cloud },
{ name: 'Products', href: '/reseller-dashboard/products', icon: Package },
{ name: 'Billing', href: '/reseller-dashboard/billing', icon: CreditCard },
{ name: 'Support', href: '/reseller-dashboard/support', icon: Headphones },
{ name: 'Reports', href: '/reseller-dashboard/reports', icon: BarChart3 },

View File

@ -33,7 +33,7 @@ import { cn } from '../../utils/cn';
import toast from 'react-hot-toast';
const navigation = [
{ name: 'Dashboard', href: '/', icon: Home },
{ name: 'Dashboard', href: '/dashboard', icon: Home },
{ name: 'Product Management', href: '/product-management', icon: Package },
{ name: 'Reseller Requests', href: '/resellers', icon: Users },
{ name: 'Approved Resellers', href: '/approved-resellers', icon: Handshake },

View File

@ -1,13 +1,18 @@
import React, { useState, useEffect } from 'react';
import { Bell } from 'lucide-react';
import { Bell, BellOff, Check, X, Clock, AlertCircle, CheckCircle, Info, Zap } from 'lucide-react';
import { useAppSelector } from '../store/hooks';
import NotificationPanel from './NotificationPanel';
import { useSocket } from '../hooks/useSocket';
import toast from 'react-hot-toast';
import { cn } from '../utils/cn';
const NotificationBell: React.FC = () => {
const [unreadCount, setUnreadCount] = useState(0);
const [isPanelOpen, setIsPanelOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const { user } = useAppSelector((state) => state.auth);
const socket = useSocket();
useEffect(() => {
fetchUnreadCount();
@ -18,6 +23,66 @@ const NotificationBell: React.FC = () => {
return () => clearInterval(interval);
}, [user]);
useEffect(() => {
if (socket) {
// Socket connection events
socket.on('connect', () => {
setIsConnected(true);
console.log('Socket connected for notifications');
});
socket.on('disconnect', () => {
setIsConnected(false);
console.log('Socket disconnected from notifications');
});
// Real-time notification events
socket.on('NEW_CUSTOMER', (data: { customerName: string }) => {
toast.success(`New customer added: ${data.customerName}`);
fetchUnreadCount();
});
socket.on('NEW_SALE', (data: { productName: string; customerName: string }) => {
toast.success(`New sale: ${data.productName} to ${data.customerName}`, {
duration: 5000,
});
fetchUnreadCount();
});
socket.on('PAYMENT_RECEIVED', (data: { amount: number; customerName: string }) => {
toast.success(`Payment received: $${data.amount} from ${data.customerName}`, {
duration: 5000,
});
fetchUnreadCount();
});
socket.on('SUPPORT_TICKET', (data: { title: string }) => {
toast.error(`New support ticket: ${data.title}`, {
duration: 6000,
});
fetchUnreadCount();
});
socket.on('VENDOR_NOTIFICATION', (data: { message: string; type: 'success' | 'error' | 'info' }) => {
toast(data.message, {
icon: data.type === 'success' ? '✅' : data.type === 'error' ? '❌' : '',
duration: 5000,
});
fetchUnreadCount();
});
return () => {
socket.off('connect');
socket.off('disconnect');
socket.off('NEW_CUSTOMER');
socket.off('NEW_SALE');
socket.off('PAYMENT_RECEIVED');
socket.off('SUPPORT_TICKET');
socket.off('VENDOR_NOTIFICATION');
};
}
}, [socket]);
const fetchUnreadCount = async () => {
try {
setLoading(true);
@ -58,26 +123,78 @@ const NotificationBell: React.FC = () => {
fetchUnreadCount();
};
const getConnectionStatusIcon = () => {
if (!socket) return <BellOff className="w-5 h-5 text-gray-400" />;
if (isConnected) return <Zap className="w-5 h-5 text-green-500" />;
return <Clock className="w-5 h-5 text-yellow-500" />;
};
const getConnectionStatusColor = () => {
if (!socket) return 'text-gray-400';
if (isConnected) return 'text-green-500';
return 'text-yellow-500';
};
return (
<>
<div className="relative">
<div className="relative group">
<button
onClick={handleBellClick}
className="relative p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
className={cn(
"relative p-3 rounded-full transition-all duration-300 group-hover:bg-gray-100 dark:group-hover:bg-gray-800",
"flex items-center justify-center",
"text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
)}
disabled={loading}
title={isConnected ? 'Real-time notifications active' : 'Notifications (offline)'}
>
<Bell className="w-6 h-6" />
{/* Connection status indicator */}
<div className="absolute -top-1 -left-1">
{getConnectionStatusIcon()}
</div>
{/* Main bell icon */}
<Bell className={cn(
"w-6 h-6 transition-all duration-300",
unreadCount > 0 && "animate-bounce"
)} />
{/* Unread count badge */}
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center min-w-[20px]">
<span className={cn(
"absolute -top-2 -right-2",
"bg-gradient-to-r from-red-500 to-pink-500 text-white text-xs font-bold",
"rounded-full h-6 w-6 flex items-center justify-center min-w-[24px]",
"shadow-lg animate-pulse",
"border-2 border-white dark:border-gray-800"
)}>
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
{/* Loading indicator */}
{loading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
</div>
)}
</button>
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 text-white text-sm rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none whitespace-nowrap z-50">
<div className="flex items-center space-x-2">
{getConnectionStatusIcon()}
<span>
{isConnected ? 'Real-time notifications active' : 'Notifications (offline)'}
</span>
</div>
{unreadCount > 0 && (
<div className="mt-1 text-center">
{unreadCount} unread notification{unreadCount !== 1 ? 's' : ''}
</div>
)}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900"></div>
</div>
</div>
<NotificationPanel

View File

@ -23,7 +23,6 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
useEffect(() => {
// Check if user is authenticated but no user data
if (isAuthenticated && !user && !isLoading) {
console.log('ProtectedRoute: User authenticated but no user data, fetching user...');
dispatch(getCurrentUser());
}
}, [isAuthenticated, user, isLoading, dispatch]);
@ -63,12 +62,6 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
}
if (!hasRequiredRole) {
console.log('User does not have required role:', {
userRole: user.role,
requiredRole,
userRoles: user.roles,
primaryRole: user.roles?.[0]?.name
});
return <Navigate to="/unauthorized" replace />;
}
}

View File

@ -0,0 +1,314 @@
import React, { useState, useEffect } from 'react';
import { TrendingUp, Users, DollarSign, Package, Calendar, BarChart3 } from 'lucide-react';
import { formatCurrency, formatNumber, formatDate } from '../utils/format';
import { cn } from '../utils/cn';
interface ResellerPerformanceProps {
vendorId: string;
}
interface ResellerPerformanceData {
id: string;
name: string;
email: string;
company: string;
status: string;
totalSales: number;
totalCommission: number;
totalProducts: number;
orderCount: number;
averageOrderValue: number;
commissionRate: number;
lastOrder: string | null;
}
const ResellerPerformance: React.FC<ResellerPerformanceProps> = ({ vendorId }) => {
const [performanceData, setPerformanceData] = useState<ResellerPerformanceData[]>([]);
const [summary, setSummary] = useState({
totalResellers: 0,
totalSales: 0,
totalCommission: 0,
totalProducts: 0
});
const [period, setPeriod] = useState('month');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchResellerPerformance();
}, [vendorId, period]);
const fetchResellerPerformance = async () => {
try {
setLoading(true);
setError(null);
const token = localStorage.getItem('accessToken');
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/vendors/dashboard/reseller-performance?period=${period}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch reseller performance data');
}
const data = await response.json();
if (data.success) {
setPerformanceData(data.data.resellers);
setSummary(data.data.summary);
} else {
throw new Error(data.message || 'Failed to fetch data');
}
} catch (error: any) {
console.error('Error fetching reseller performance:', error);
setError(error.message);
} finally {
setLoading(false);
}
};
const getPerformanceColor = (sales: number) => {
if (sales >= 10000) return 'text-green-600 bg-green-100 dark:bg-green-900/30';
if (sales >= 5000) return 'text-blue-600 bg-blue-100 dark:bg-blue-900/30';
if (sales >= 1000) return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900/30';
return 'text-slate-600 bg-slate-100 dark:bg-slate-700/50';
};
const getCommissionColor = (rate: number) => {
if (rate >= 20) return 'text-green-600 bg-green-100 dark:bg-green-900/30';
if (rate >= 15) return 'text-blue-600 bg-blue-100 dark:bg-blue-900/30';
if (rate >= 10) return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900/30';
return 'text-slate-600 bg-slate-100 dark:bg-slate-700/50';
};
if (loading) {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Reseller Performance</h2>
<div className="animate-pulse bg-slate-200 dark:bg-slate-700 h-10 w-32 rounded-lg"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="animate-pulse bg-slate-200 dark:bg-slate-700 h-24 rounded-lg"></div>
))}
</div>
<div className="animate-pulse bg-slate-200 dark:bg-slate-700 h-96 rounded-lg"></div>
</div>
);
}
if (error) {
return (
<div className="text-center py-12">
<div className="text-red-500 text-lg mb-2">Error loading reseller performance</div>
<div className="text-slate-600 dark:text-slate-400 mb-4">{error}</div>
<button
onClick={fetchResellerPerformance}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Retry
</button>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Reseller Performance</h2>
<p className="text-slate-600 dark:text-slate-400">Track your resellers' sales and commission performance</p>
</div>
<div className="flex items-center space-x-2">
<Calendar className="w-4 h-4 text-slate-500" />
<select
value={period}
onChange={(e) => setPeriod(e.target.value)}
className="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="week">This Week</option>
<option value="month">This Month</option>
<option value="quarter">This Quarter</option>
<option value="year">This Year</option>
</select>
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Resellers</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{summary.totalResellers}</p>
</div>
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Sales</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">
{formatCurrency(summary.totalSales)}
</p>
</div>
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center">
<DollarSign className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Commission</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">
{formatCurrency(summary.totalCommission)}
</p>
</div>
<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>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Products Sold</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">
{formatNumber(summary.totalProducts)}
</p>
</div>
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900/50 rounded-lg flex items-center justify-center">
<Package className="w-6 h-6 text-orange-600 dark:text-orange-400" />
</div>
</div>
</div>
</div>
{/* Performance Table */}
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-200 dark:border-slate-700">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">Reseller Rankings</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
Sorted by total sales performance for {period === 'week' ? 'this week' :
period === 'month' ? 'this month' :
period === 'quarter' ? 'this quarter' : 'this year'}
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 dark:bg-slate-700/50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Reseller
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Sales
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Commission
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Products
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Orders
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Avg Order
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Commission Rate
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
Last Order
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-slate-800 divide-y divide-slate-200 dark:divide-slate-700">
{performanceData.map((reseller, index) => (
<tr key={reseller.id} className="hover:bg-slate-50 dark:hover:bg-slate-700/50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-8 h-8 bg-gradient-to-r from-blue-500 to-blue-600 rounded-full flex items-center justify-center text-white text-sm font-semibold mr-3">
{index + 1}
</div>
<div>
<div className="text-sm font-medium text-slate-900 dark:text-white">
{reseller.name}
</div>
<div className="text-sm text-slate-500 dark:text-slate-400">
{reseller.email}
</div>
{reseller.company && (
<div className="text-xs text-slate-400 dark:text-slate-500">
{reseller.company}
</div>
)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={cn(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
getPerformanceColor(reseller.totalSales)
)}>
{formatCurrency(reseller.totalSales)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-white">
{formatCurrency(reseller.totalCommission)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-white">
{formatNumber(reseller.totalProducts)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-white">
{reseller.orderCount}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-white">
{formatCurrency(reseller.averageOrderValue)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={cn(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
getCommissionColor(reseller.commissionRate)
)}>
{reseller.commissionRate.toFixed(1)}%
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
{reseller.lastOrder ? formatDate(reseller.lastOrder) : 'Never'}
</td>
</tr>
))}
</tbody>
</table>
</div>
{performanceData.length === 0 && (
<div className="text-center py-12">
<BarChart3 className="w-12 h-12 text-slate-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-slate-900 dark:text-white mb-2">No Performance Data</h3>
<p className="text-slate-600 dark:text-slate-400">
No reseller performance data available for the selected period.
</p>
</div>
)}
</div>
</div>
);
};
export default ResellerPerformance;

View File

@ -9,8 +9,6 @@ const VendorDetailsModal: React.FC<VendorModalProps> = ({
onApprove,
onReject
}) => {
console.log('VendorDetailsModal props:', { isOpen, vendor: vendor?.id, vendorName: vendor ? `${vendor.firstName} ${vendor.lastName}` : null });
if (!isOpen || !vendor) return null;
const formatRoleName = (role: string) => {

View File

@ -10,8 +10,6 @@ const VendorRejectionModal: React.FC<RejectionModalProps> = ({
}) => {
const [rejectionReason, setRejectionReason] = useState('');
console.log('VendorRejectionModal props:', { isOpen, vendor: vendor?.id, vendorName: vendor ? `${vendor.firstName} ${vendor.lastName}` : null });
if (!isOpen || !vendor) return null;
const handleReject = () => {

View File

@ -0,0 +1,295 @@
import React, { useState, useEffect } from 'react';
import { User, Package, DollarSign, Calendar, CheckCircle, X } from 'lucide-react';
import { cn } from '../../utils/cn';
import toast from 'react-hot-toast';
interface MarkProductSoldFormProps {
isOpen: boolean;
onClose: () => void;
onSuccess?: () => void;
products: any[];
customers: any[];
}
const MarkProductSoldForm: React.FC<MarkProductSoldFormProps> = ({
isOpen,
onClose,
onSuccess,
products,
customers
}) => {
const [formData, setFormData] = useState({
productId: '',
customerId: '',
quantity: 1,
customerDetails: {
name: '',
email: '',
company: '',
phone: ''
}
});
const [selectedProduct, setSelectedProduct] = useState<any>(null);
const [selectedCustomer, setSelectedCustomer] = useState<any>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (formData.productId) {
const product = products.find(p => p.id.toString() === formData.productId);
setSelectedProduct(product);
}
}, [formData.productId, products]);
useEffect(() => {
if (formData.customerId) {
const customer = customers.find(c => c.id.toString() === formData.customerId);
setSelectedCustomer(customer);
}
}, [formData.customerId, customers]);
const handleInputChange = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleCustomerDetailsChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
customerDetails: {
...prev.customerDetails,
[field]: value
}
}));
};
const calculateCommission = () => {
if (!selectedProduct) return 0;
const totalAmount = selectedProduct.price * formData.quantity;
return (totalAmount * selectedProduct.commissionRate) / 100;
};
const calculateTotalAmount = () => {
if (!selectedProduct) return 0;
return selectedProduct.price * formData.quantity;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.productId || !formData.customerId) {
toast.error('Please select both product and customer');
return;
}
if (formData.quantity < 1) {
toast.error('Quantity must be at least 1');
return;
}
setIsSubmitting(true);
try {
const token = localStorage.getItem('accessToken');
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/resellers/products/sold`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
productId: formData.productId,
customerId: formData.customerId,
quantity: formData.quantity,
customerDetails: formData.customerDetails
})
});
const data = await response.json();
if (data.success) {
toast.success('Product marked as sold successfully!');
onSuccess?.();
onClose();
// Reset form
setFormData({
productId: '',
customerId: '',
quantity: 1,
customerDetails: {
name: '',
email: '',
company: '',
phone: ''
}
});
} else {
toast.error(data.message || 'Failed to mark product as sold');
}
} catch (error: any) {
console.error('Error marking product as sold:', error);
toast.error('Failed to mark product as sold. Please try again.');
} finally {
setIsSubmitting(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-200 dark:border-slate-700">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center">
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<div>
<h3 className="text-xl font-bold text-slate-900 dark:text-white">Mark Product as Sold</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">Record a successful sale and track commission</p>
</div>
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Form */}
<div className="overflow-y-auto max-h-[calc(90vh-120px)]">
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Product Selection */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Product *
</label>
<select
value={formData.productId}
onChange={(e) => handleInputChange('productId', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-green-500"
required
>
<option value="">Select a product</option>
{products.map(product => (
<option key={product.id} value={product.id}>
{product.name} - ${product.price} ({product.commissionRate}% commission)
</option>
))}
</select>
</div>
{/* Customer Selection */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Customer *
</label>
<select
value={formData.customerId}
onChange={(e) => handleInputChange('customerId', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-green-500"
required
>
<option value="">Select a customer</option>
{customers.map(customer => (
<option key={customer.id} value={customer.id}>
{customer.name} - {customer.email}
</option>
))}
</select>
</div>
{/* Quantity */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Quantity *
</label>
<input
type="number"
min="1"
value={formData.quantity}
onChange={(e) => handleInputChange('quantity', parseInt(e.target.value) || 1)}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-green-500"
required
/>
</div>
{/* Sale Summary */}
{selectedProduct && (
<div className="bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4 space-y-3">
<h4 className="font-semibold text-slate-900 dark:text-white">Sale Summary</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-slate-600 dark:text-slate-400">Product:</span>
<p className="font-medium text-slate-900 dark:text-white">{selectedProduct.name}</p>
</div>
<div>
<span className="text-slate-600 dark:text-slate-400">Unit Price:</span>
<p className="font-medium text-slate-900 dark:text-white">${selectedProduct.price}</p>
</div>
<div>
<span className="text-slate-600 dark:text-slate-400">Total Amount:</span>
<p className="font-medium text-green-600 dark:text-green-400">${calculateTotalAmount()}</p>
</div>
<div>
<span className="text-slate-600 dark:text-slate-400">Commission ({selectedProduct.commissionRate}%):</span>
<p className="font-medium text-blue-600 dark:text-blue-400">${calculateCommission()}</p>
</div>
</div>
</div>
)}
{/* Customer Details */}
{selectedCustomer && (
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<h4 className="font-semibold text-slate-900 dark:text-white mb-3 flex items-center">
<User className="w-4 h-4 mr-2 text-blue-600" />
Customer Information
</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-slate-600 dark:text-slate-400">Name:</span>
<p className="font-medium text-slate-900 dark:text-white">{selectedCustomer.name}</p>
</div>
<div>
<span className="text-slate-600 dark:text-slate-400">Email:</span>
<p className="font-medium text-slate-900 dark:text-white">{selectedCustomer.email}</p>
</div>
<div>
<span className="text-slate-600 dark:text-slate-400">Company:</span>
<p className="font-medium text-slate-900 dark:text-white">{selectedCustomer.company || 'N/A'}</p>
</div>
<div>
<span className="text-slate-600 dark:text-slate-400">Phone:</span>
<p className="font-medium text-slate-900 dark:text-white">{selectedCustomer.phone || 'N/A'}</p>
</div>
</div>
</div>
)}
{/* Form Actions */}
<div className="flex justify-end space-x-3 pt-4 border-t border-slate-200 dark:border-slate-700">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-slate-600 dark:text-slate-400 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isSubmitting ? 'Recording Sale...' : 'Mark as Sold'}
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default MarkProductSoldForm;

View File

@ -78,6 +78,13 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
{ value: 'discontinued', label: 'Discontinued' },
];
const currencyOptions = [
{ value: 'USD', label: 'USD ($)' },
{ value: 'RS', label: 'RS (₹)' },
{ value: 'EUR', label: 'EUR (€)' },
{ value: 'GBP', label: 'GBP (£)' },
];
useEffect(() => {
if (product) {
setFormData({
@ -170,7 +177,8 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
formData.price > 0 &&
formData.sku.trim() !== '' &&
formData.commissionRate >= 0 &&
formData.commissionRate <= 100
formData.commissionRate <= 100 &&
formData.purchaseUrl.trim() !== ''
);
};
@ -197,6 +205,11 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
return;
}
if (!formData.purchaseUrl.trim()) {
toast.error('Purchase URL is required');
return;
}
try {
const productData = {
...formData,
@ -346,10 +359,10 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
type="number"
step="0.01"
min="0"
value={formData.price}
value={formData.price === 0 ? '' : formData.price}
onChange={(e) => handleInputChange('price', parseFloat(e.target.value) || 0)}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="0.00"
placeholder="Enter price"
required
/>
</div>
@ -358,14 +371,18 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Currency
</label>
<input
type="text"
<select
value={formData.currency}
onChange={(e) => handleInputChange('currency', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="USD"
maxLength={3}
/>
required
>
{currencyOptions.map(currency => (
<option key={currency.value} value={currency.value}>
{currency.label}
</option>
))}
</select>
</div>
<div>
@ -429,13 +446,30 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
type="number"
min="-1"
value={formData.stockQuantity}
onChange={(e) => handleInputChange('stockQuantity', parseInt(e.target.value) || -1)}
onChange={(e) => {
const value = e.target.value;
if (value === '') {
handleInputChange('stockQuantity', -1);
} else {
const numValue = parseInt(value);
if (!isNaN(numValue) && numValue >= -1) {
handleInputChange('stockQuantity', numValue);
}
}
}}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="-1"
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
Use -1 for unlimited stock
</p>
<div className="flex items-center justify-between mt-1">
<p className="text-xs text-slate-500 dark:text-slate-400">
Use -1 for unlimited stock, 0 for out of stock, or enter a positive number
</p>
<div className="text-xs text-slate-400">
{formData.stockQuantity === -1 ? 'Unlimited' :
formData.stockQuantity === 0 ? 'Out of Stock' :
`${formData.stockQuantity} units`}
</div>
</div>
</div>
</div>
@ -573,7 +607,7 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
{/* Purchase URL */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Purchase URL
Purchase URL *
</label>
<input
type="url"
@ -581,9 +615,10 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
onChange={(e) => handleInputChange('purchaseUrl', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="https://example.com/product"
required
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
Direct link where customers can purchase this product
Direct link where customers can purchase this product (required)
</p>
</div>

View File

@ -93,9 +93,9 @@ export const mockResellerRecentActivities: RecentActivity[] = [
},
{
id: '2',
type: 'instance_created',
title: 'Cloud Instance Created',
description: 'Created new cloud instance for DataFlow Inc with 8GB RAM configuration',
type: 'product_sold',
title: 'Product Sold',
description: 'Sold cloud hosting package to DataFlow Inc',
timestamp: '2025-01-15T09:15:00Z',
amount: 1200,
currency: 'USD',

62
src/hooks/useSocket.ts Normal file
View File

@ -0,0 +1,62 @@
import { useEffect, useRef, useState } from 'react';
import io from 'socket.io-client';
export const useSocket = () => {
const [isConnected, setIsConnected] = useState(false);
const socketRef = useRef<any>(null);
useEffect(() => {
// Initialize socket connection
const token = localStorage.getItem('accessToken');
if (!token) return;
const socket = io(process.env.REACT_APP_SOCKET_URL || 'http://localhost:5000', {
auth: {
token
},
transports: ['websocket', 'polling'],
timeout: 20000,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
});
socketRef.current = socket;
// Connection events
socket.on('connect', () => {
console.log('Socket connected');
setIsConnected(true);
});
socket.on('disconnect', () => {
console.log('Socket disconnected');
setIsConnected(false);
});
socket.on('connect_error', (error: Error) => {
console.error('Socket connection error:', error);
setIsConnected(false);
});
socket.on('reconnect', (attemptNumber: number) => {
console.log('Socket reconnected after', attemptNumber, 'attempts');
setIsConnected(true);
});
socket.on('reconnect_error', (error: Error) => {
console.error('Socket reconnection error:', error);
});
// Cleanup on unmount
return () => {
if (socket) {
socket.disconnect();
}
};
}, []);
return socketRef.current;
};
export default useSocket;

View File

@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react';
import { mockResellers } from '../../data/mockData';
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
import Modal from '../../components/Modal';
import AddPartnershipForm from '../../components/forms/AddPartnershipForm';
import DetailView from '../../components/DetailView';
import toast from 'react-hot-toast';
import {
Search,
Filter,
@ -23,28 +23,103 @@ import {
Download,
Mail,
MapPin,
Building2
Building2,
User
} from 'lucide-react';
import { cn } from '../../utils/cn';
interface Reseller {
id: string;
firstName: string;
lastName: string;
email: string;
phone?: string;
status: string;
tier?: string;
totalRevenue?: number;
lastActive?: string;
customers?: number;
commissionRate?: number;
region?: string;
avatar?: string;
company?: string;
createdAt?: string;
}
const ApprovedResellersPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('active');
const [tierFilter, setTierFilter] = useState('all');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [selectedReseller, setSelectedReseller] = useState<any>(null);
const [selectedReseller, setSelectedReseller] = useState<Reseller | null>(null);
const [resellers, setResellers] = useState<Reseller[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Set page title
useEffect(() => {
document.title = 'Approved Resellers - Cloudtopiaa';
}, []);
// Fetch approved resellers from API
useEffect(() => {
fetchApprovedResellers();
}, []);
const fetchApprovedResellers = async () => {
try {
setLoading(true);
setError(null);
const token = localStorage.getItem('accessToken');
if (!token) {
setError('Authentication required. Please log in again.');
return;
}
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/vendors/resellers?status=active`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch approved resellers');
}
const data = await response.json();
if (data.success) {
setResellers(data.data.resellers || []);
if (data.data.resellers && data.data.resellers.length > 0) {
toast.success(`Loaded ${data.data.resellers.length} approved resellers`);
} else {
toast('No approved resellers found');
}
} else {
const errorMsg = data.message || 'Failed to fetch approved resellers';
setError(errorMsg);
toast.error(errorMsg);
}
} catch (error: any) {
console.error('Error fetching approved resellers:', error);
const errorMsg = error.message || 'Failed to fetch approved resellers';
setError(errorMsg);
toast.error(errorMsg);
} finally {
setLoading(false);
}
};
// Filter only approved (active) resellers
const approvedResellers = mockResellers.filter(reseller => reseller.status === 'active');
const approvedResellers = resellers.filter(reseller => reseller.status === 'active');
const filteredResellers = approvedResellers.filter(reseller => {
const matchesSearch = reseller.name.toLowerCase().includes(searchTerm.toLowerCase());
const fullName = `${reseller.firstName} ${reseller.lastName}`.toLowerCase();
const matchesSearch = fullName.includes(searchTerm.toLowerCase()) ||
reseller.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
(reseller.company && reseller.company.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesStatus = statusFilter === 'all' || reseller.status === statusFilter;
const matchesTier = tierFilter === 'all' || reseller.tier === tierFilter;
@ -78,31 +153,28 @@ const ApprovedResellersPage: React.FC = () => {
};
const handleAddReseller = (data: any) => {
console.log('New reseller data:', data);
// Here you would typically make an API call to add the reseller
// For now, we'll just close the modal
setIsAddModalOpen(false);
// You could also show a success notification here
toast.success('Reseller partnership request submitted successfully!');
};
const handleViewReseller = (reseller: any) => {
const handleViewReseller = (reseller: Reseller) => {
setSelectedReseller(reseller);
setIsDetailModalOpen(true);
};
const handleEditReseller = (reseller: any) => {
console.log('Edit reseller:', reseller);
alert(`Edit functionality for ${reseller.name} - This would open an edit form`);
const handleEditReseller = (reseller: Reseller) => {
toast(`Edit functionality for ${reseller.firstName} ${reseller.lastName} - This would open an edit form`);
};
const handleMailReseller = (reseller: any) => {
console.log('Mail reseller:', reseller);
const handleMailReseller = (reseller: Reseller) => {
const mailtoLink = `mailto:${reseller.email}?subject=Cloudtopiaa Partnership Update`;
window.open(mailtoLink, '_blank');
toast.success(`Opening email client for ${reseller.firstName} ${reseller.lastName}`);
};
const handleMoreOptions = (reseller: any) => {
console.log('More options for reseller:', reseller);
const handleMoreOptions = (reseller: Reseller) => {
const options = [
'View Performance',
'Download Report',
@ -110,12 +182,31 @@ const ApprovedResellersPage: React.FC = () => {
'Change Terms',
'Suspend Partnership'
];
const selectedOption = prompt(`Select an option for ${reseller.name}:\n${options.join('\n')}`);
const selectedOption = prompt(`Select an option for ${reseller.firstName} ${reseller.lastName}:\n${options.join('\n')}`);
if (selectedOption) {
alert(`Selected: ${selectedOption} for ${reseller.name}`);
toast.success(`Selected: ${selectedOption} for ${reseller.firstName} ${reseller.lastName}`);
}
};
if (loading) {
return <div className="text-center py-8">Loading...</div>;
}
if (error) {
return <div className="text-center py-8 text-red-500">{error}</div>;
}
if (resellers.length === 0) {
return (
<div className="text-center py-8">
<p className="text-lg text-secondary-600 dark:text-secondary-400">No approved resellers found.</p>
<p className="text-sm text-secondary-500 dark:text-secondary-400">
Please ensure you have active partnerships with resellers.
</p>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
@ -183,9 +274,9 @@ const ApprovedResellersPage: React.FC = () => {
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Total Customers
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{approvedResellers.reduce((sum, r) => sum + r.customers, 0)}
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{approvedResellers.reduce((sum, r) => sum + (r.customers || 0), 0)}
</p>
</div>
<div className="w-12 h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-success-600 dark:text-success-400" />
@ -199,9 +290,9 @@ const ApprovedResellersPage: React.FC = () => {
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
Total Revenue
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{formatCurrency(approvedResellers.reduce((sum, r) => sum + r.totalRevenue, 0))}
</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{formatCurrency(approvedResellers.reduce((sum, r) => sum + (r.totalRevenue || 0), 0))}
</p>
</div>
<div className="w-12 h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center">
<DollarSign className="w-6 h-6 text-success-600 dark:text-success-400" />
@ -308,20 +399,39 @@ const ApprovedResellersPage: React.FC = () => {
<tr key={reseller.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-800">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<img
className="h-10 w-10 rounded-full mr-3"
src={reseller.avatar}
alt={reseller.name}
/>
<div className="relative mr-3">
{reseller.avatar ? (
<img
className="h-10 w-10 rounded-full object-cover"
src={reseller.avatar}
alt={`${reseller.firstName} ${reseller.lastName}`}
onError={(e) => {
// Hide the image if it fails to load and show fallback
e.currentTarget.style.display = 'none';
e.currentTarget.nextElementSibling?.classList.remove('hidden');
}}
/>
) : null}
<div className={cn(
"h-10 w-10 rounded-full flex items-center justify-center bg-gradient-to-r from-blue-500 to-blue-600 text-white font-semibold text-sm shadow-sm",
reseller.avatar ? 'hidden' : ''
)}>
{reseller.firstName && reseller.lastName ? (
`${reseller.firstName.charAt(0).toUpperCase()}${reseller.lastName.charAt(0).toUpperCase()}`
) : (
<User className="w-5 h-5" />
)}
</div>
</div>
<div>
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{reseller.name}
{reseller.firstName || ''} {reseller.lastName || ''}
</div>
<div className="text-sm text-secondary-500 dark:text-secondary-400">
{reseller.email}
{reseller.email || 'N/A'}
</div>
<div className="text-xs text-secondary-400 dark:text-secondary-500">
{reseller.phone}
{reseller.phone || 'N/A'}
</div>
</div>
</div>
@ -329,31 +439,31 @@ const ApprovedResellersPage: React.FC = () => {
<td className="px-6 py-4 whitespace-nowrap">
<span className={cn(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
getStatusColor(reseller.status)
getStatusColor(reseller.status || 'active')
)}>
{reseller.status.charAt(0).toUpperCase() + reseller.status.slice(1)}
{reseller.status ? reseller.status.charAt(0).toUpperCase() + reseller.status.slice(1) : 'Active'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={cn(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
getTierColor(reseller.tier)
)}>
{reseller.tier.charAt(0).toUpperCase() + reseller.tier.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{formatCurrency(reseller.totalRevenue)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{formatNumber(reseller.customers)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{reseller.commissionRate}%
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-400">
{formatDate(reseller.lastActive)}
<span className={cn(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
getTierColor(reseller.tier || 'silver')
)}>
{reseller.tier ? reseller.tier.charAt(0).toUpperCase() + reseller.tier.slice(1) : 'N/A'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{formatCurrency(reseller.totalRevenue || 0)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{formatNumber(reseller.customers || 0)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{reseller.commissionRate ? `${reseller.commissionRate}%` : 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-400">
{reseller.lastActive ? formatDate(reseller.lastActive) : 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center justify-end space-x-2">
<button

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { mockDeals } from '../../data/mockData';
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
import Modal from '../../components/Modal';
@ -11,18 +11,20 @@ import {
MoreVertical,
Eye,
Edit,
Trash2,
Mail,
TrendingUp,
DollarSign,
Calendar,
Target,
DollarSign,
Users,
Briefcase,
CheckCircle,
Clock,
AlertCircle,
Download,
Mail
Briefcase,
Target
} from 'lucide-react';
import toast from 'react-hot-toast';
import { cn } from '../../utils/cn';
const DealsPage: React.FC = () => {
@ -93,7 +95,7 @@ const DealsPage: React.FC = () => {
const handleEditDeal = (deal: any) => {
console.log('Edit deal:', deal);
alert(`Edit functionality for ${deal.title} - This would open an edit form`);
toast.success(`Edit functionality for ${deal.title} - This would open an edit form`);
};
const handleMailDeal = (deal: any) => {
@ -113,7 +115,7 @@ const DealsPage: React.FC = () => {
];
const selectedOption = prompt(`Select an option for ${deal.title}:\n${options.join('\n')}`);
if (selectedOption) {
alert(`Selected: ${selectedOption} for ${deal.title}`);
toast.success(`Selected: ${selectedOption} for ${deal.title}`);
}
};

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Plus, Search, Edit, Trash2, Eye, Pencil } from 'lucide-react';
import toast from 'react-hot-toast';
import { apiService, Product } from '../../services/api';
const Products: React.FC = () => {
@ -137,7 +138,7 @@ const Products: React.FC = () => {
category: formData.category || 'other'
});
alert('Product created successfully');
toast.success('Product created successfully');
setIsCreateModalOpen(false);
setFormData({
name: '',
@ -153,7 +154,7 @@ const Products: React.FC = () => {
});
fetchProducts();
} catch (error) {
alert('Error creating product');
toast.error('Error creating product');
}
};
@ -168,12 +169,12 @@ const Products: React.FC = () => {
category: formData.category || 'other'
});
alert('Product updated successfully');
toast.success('Product updated successfully');
setIsEditModalOpen(false);
setEditingProductId(null);
fetchProducts();
} catch (error) {
alert('Error updating product');
toast.error('Error updating product');
}
};
@ -187,12 +188,12 @@ const Products: React.FC = () => {
try {
await apiService.deleteProduct(deletingProductId);
alert('Product deleted successfully');
toast.success('Product deleted successfully');
fetchProducts();
setIsDeleteModalOpen(false);
setDeletingProductId(null);
} catch (error) {
alert('Error deleting product');
toast.error('Error deleting product');
}
};

View File

@ -16,13 +16,14 @@ import {
Calendar,
Link
} from 'lucide-react';
import toast from 'react-hot-toast';
interface Reseller {
id: string;
companyName: string;
contactEmail: string;
contactPhone: string;
website: string;
website: string | null;
tier: string;
status: string;
commissionRate: number;
@ -74,9 +75,12 @@ const Resellers: React.FC = () => {
if (data.success) {
setVendors(data.data);
} else {
toast.error('Failed to fetch vendors');
}
} catch (error) {
console.error('Error fetching vendors:', error);
toast.error('Failed to fetch vendors. Please try again.');
}
};
@ -97,9 +101,12 @@ const Resellers: React.FC = () => {
if (data.success) {
setResellers(data.data);
} else {
toast.error('Failed to fetch resellers');
}
} catch (error) {
console.error('Error fetching resellers:', error);
toast.error('Failed to fetch resellers. Please try again.');
} finally {
setLoading(false);
}
@ -129,12 +136,17 @@ const Resellers: React.FC = () => {
});
if (response.ok) {
toast.success('Reseller deleted successfully!');
fetchResellers();
setIsDeleteModalOpen(false);
setDeletingResellerId(null);
} else {
const errorData = await response.json();
toast.error(`Failed to delete reseller: ${errorData.message || 'Unknown error'}`);
}
} catch (error) {
console.error('Error deleting reseller:', error);
toast.error('Failed to delete reseller. Please try again.');
}
};
@ -150,11 +162,17 @@ const Resellers: React.FC = () => {
});
if (response.ok) {
// Show success message
toast.success('Reseller updated successfully!');
fetchResellers();
setEditingReseller(null);
} else {
const errorData = await response.json();
toast.error(`Failed to update reseller: ${errorData.message || 'Unknown error'}`);
}
} catch (error) {
console.error('Error updating reseller:', error);
toast.error('Failed to update reseller. Please try again.');
}
};
@ -196,6 +214,34 @@ const Resellers: React.FC = () => {
return matchesSearch && matchesStatus && matchesVendor;
});
const handleSearch = () => {
if (searchTerm.trim() === '') {
toast('Please enter a search term');
return;
}
fetchResellers();
toast.success(`Searching for: ${searchTerm}`);
};
const handleStatusFilterChange = (status: 'all' | 'active' | 'pending' | 'suspended') => {
setStatusFilter(status);
if (status !== 'all') {
toast.success(`Filtered by status: ${status}`);
} else {
toast.success('Showing all statuses');
}
};
const handleVendorFilterChange = (vendorId: string) => {
setVendorFilter(vendorId);
if (vendorId !== 'all') {
const vendor = vendors.find(v => v.id === vendorId);
toast.success(`Filtered by vendor: ${vendor?.company || 'Unknown'}`);
} else {
toast.success('Showing all vendors');
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
@ -228,9 +274,16 @@ const Resellers: React.FC = () => {
placeholder="Search resellers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
onClick={handleSearch}
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm"
>
Search
</button>
</div>
{/* Status Filter */}
@ -238,7 +291,7 @@ const Resellers: React.FC = () => {
<Filter className="w-4 h-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
onChange={(e) => handleStatusFilterChange(e.target.value as any)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Status</option>
@ -253,7 +306,7 @@ const Resellers: React.FC = () => {
<Building className="w-4 h-4 text-gray-400" />
<select
value={vendorFilter}
onChange={(e) => setVendorFilter(e.target.value)}
onChange={(e) => handleVendorFilterChange(e.target.value)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Vendors</option>
@ -485,6 +538,156 @@ const Resellers: React.FC = () => {
</div>
)}
{/* Edit Reseller Modal */}
{editingReseller && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Edit Reseller
</h3>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const website = formData.get('website') as string;
const updateData = {
companyName: formData.get('companyName') as string,
contactEmail: formData.get('contactEmail') as string,
contactPhone: formData.get('contactPhone') as string,
website: website && website.trim() !== '' ? website : null,
tier: formData.get('tier') as string,
status: formData.get('status') as string,
commissionRate: parseInt(formData.get('commissionRate') as string)
};
handleUpdate(editingReseller.id, updateData);
}}>
<div className="space-y-4">
<div>
<label htmlFor="companyName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Company Name *
</label>
<input
type="text"
id="companyName"
name="companyName"
defaultValue={editingReseller.companyName}
required
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="contactEmail" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Contact Email *
</label>
<input
type="email"
id="contactEmail"
name="contactEmail"
defaultValue={editingReseller.contactEmail}
required
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="contactPhone" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Contact Phone *
</label>
<input
type="tel"
id="contactPhone"
name="contactPhone"
defaultValue={editingReseller.contactPhone}
required
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="website" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Website
</label>
<input
type="url"
id="website"
name="website"
defaultValue={editingReseller.website || ''}
placeholder="https://example.com (optional)"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Leave empty if no website
</p>
</div>
<div>
<label htmlFor="tier" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tier *
</label>
<select
id="tier"
name="tier"
defaultValue={editingReseller.tier}
required
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="bronze">Bronze</option>
<option value="silver">Silver</option>
<option value="gold">Gold</option>
<option value="platinum">Platinum</option>
<option value="diamond">Diamond</option>
</select>
</div>
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status *
</label>
<select
id="status"
name="status"
defaultValue={editingReseller.status}
required
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="suspended">Suspended</option>
</select>
</div>
<div>
<label htmlFor="commissionRate" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Commission Rate (%) *
</label>
<input
type="number"
id="commissionRate"
name="commissionRate"
min="0"
max="100"
defaultValue={editingReseller.commissionRate}
required
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
onClick={() => setEditingReseller(null)}
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Update Reseller
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>

View File

@ -33,9 +33,7 @@ const VendorRequests: React.FC = () => {
// Get auth state from Redux
const { isAuthenticated, token, user } = useAppSelector((state) => state.auth);
// Debug: Log auth state
console.log('Auth state:', { isAuthenticated, hasToken: !!token, user });
console.log('Modal states:', { showModal, showRejectionModal, selectedVendor: selectedVendor?.id });
useEffect(() => {
fetchVendorRequests();
@ -53,11 +51,7 @@ const VendorRequests: React.FC = () => {
try {
setLoading(true);
// Debug: Check if token exists
const token = localStorage.getItem('accessToken');
console.log('Token exists:', !!token);
console.log('Token length:', token ? token.length : 0);
console.log('Redux token exists:', !!token);
if (!token) {
console.error('No token found in localStorage');
@ -72,12 +66,7 @@ const VendorRequests: React.FC = () => {
}
});
// Debug: Log response details
console.log('Response status:', response.status);
console.log('Response headers:', Object.fromEntries(response.headers.entries()));
const data = await response.json();
console.log('Response data:', data);
if (!response.ok) {
throw new Error(data.message || 'Failed to fetch vendor requests');
@ -321,10 +310,8 @@ const VendorRequests: React.FC = () => {
<div className="flex space-x-2">
<button
onClick={() => {
console.log('View button clicked for vendor:', vendor);
setSelectedVendor(vendor);
setShowModal(true);
console.log('Modal state set to true');
}}
className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
>

View File

@ -1,32 +1,494 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useAppSelector, useAppDispatch } from '../../store/hooks';
import {
fetchReceipts,
fetchPayments,
fetchSales
} from '../../store/reseller/resellerDashboardSlice';
import { formatNumber, formatDate, formatRelativeTime } from '../../utils/format';
import {
CreditCard,
Receipt,
DollarSign,
TrendingUp,
TrendingDown,
CheckCircle,
Clock,
AlertCircle,
Download,
Eye,
Filter,
Search,
Calendar,
Package,
Users,
Activity
} from 'lucide-react';
import toast from 'react-hot-toast';
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
const ResellerBilling: React.FC = () => {
const dispatch = useAppDispatch();
const { receipts, payments, sales, isLoading, error } = useAppSelector((state: any) => state.resellerDashboard);
const [activeTab, setActiveTab] = useState<'overview' | 'receipts' | 'payments' | 'sales'>('overview');
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [dateFilter, setDateFilter] = useState('all');
useEffect(() => {
const fetchData = async () => {
try {
await Promise.all([
dispatch(fetchReceipts()).unwrap(),
dispatch(fetchPayments()).unwrap(),
dispatch(fetchSales()).unwrap(),
]);
} catch (error) {
toast.error('Failed to load billing data');
console.error('Error fetching billing data:', error);
}
};
fetchData();
}, [dispatch]);
// Calculate billing statistics
const totalReceipts = receipts?.length || 0;
const totalPayments = payments?.length || 0;
const totalSales = sales?.length || 0;
const pendingReceipts = receipts?.filter((r: any) => r.status === 'pending').length || 0;
const overdueReceipts = receipts?.filter((r: any) => r.status === 'overdue').length || 0;
const paidReceipts = receipts?.filter((r: any) => r.status === 'paid').length || 0;
const totalRevenue = sales?.reduce((sum: number, sale: any) => sum + sale.amount, 0) || 0;
const totalCommission = sales?.reduce((sum: number, sale: any) => sum + sale.commission, 0) || 0;
const pendingAmount = receipts
?.filter((r: any) => r.status === 'pending' || r.status === 'overdue')
.reduce((sum: number, r: any) => sum + r.amount, 0) || 0;
const filteredReceipts = receipts?.filter((receipt: any) => {
const matchesSearch = receipt.customerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
receipt.invoiceNumber.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || receipt.status === statusFilter;
return matchesSearch && matchesStatus;
}) || [];
const filteredPayments = payments?.filter((payment: any) => {
const matchesSearch = payment.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
(payment.customerName && payment.customerName.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesStatus = statusFilter === 'all' || payment.status === statusFilter;
return matchesSearch && matchesStatus;
}) || [];
const filteredSales = sales?.filter((sale: any) => {
const matchesSearch = sale.customerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
sale.productName.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || sale.status === statusFilter;
return matchesSearch && matchesStatus;
}) || [];
const getStatusColor = (status: string) => {
switch (status) {
case 'paid':
case 'completed':
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
case 'pending':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';
case 'overdue':
case 'failed':
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'paid':
case 'completed':
return <CheckCircle className="w-4 h-4" />;
case 'pending':
return <Clock className="w-4 h-4" />;
case 'overdue':
case 'failed':
return <AlertCircle className="w-4 h-4" />;
default:
return <Clock className="w-4 h-4" />;
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (error) {
return (
<div className="text-center py-8">
<div className="text-red-500 text-lg mb-2">Error loading billing data</div>
<div className="text-gray-600">{error}</div>
</div>
);
}
const Billing: React.FC = () => {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white">
Billing & Payments
</h1>
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
Manage your billing, invoices, and payment methods
</p>
<div className="space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Billing & Payments</h1>
<p className="text-gray-600 dark:text-gray-400">Manage your invoices, payments, and sales records</p>
</div>
</div>
<div className="card p-12">
<div className="text-center">
<div className="w-16 h-16 bg-secondary-100 dark:bg-secondary-800 rounded-full flex items-center justify-center mx-auto mb-4">
<div className="w-8 h-8 bg-secondary-400 rounded"></div>
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Revenue</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
<DualCurrencyDisplay amount={totalRevenue} currency="USD" />
</p>
</div>
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-2">
Coming Soon
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Commission Earned</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
<DualCurrencyDisplay amount={totalCommission} currency="USD" />
</p>
</div>
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
<DollarSign className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Pending Amount</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
<DualCurrencyDisplay amount={pendingAmount} currency="USD" />
</p>
</div>
<div className="w-12 h-12 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center">
<Clock className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Sales</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">{totalSales}</p>
</div>
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center">
<Package className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="flex space-x-8 px-6">
{[
{ id: 'overview', label: 'Overview', icon: TrendingUp },
{ id: 'receipts', label: 'Receipts', icon: Receipt },
{ id: 'payments', label: 'Payments', icon: CreditCard },
{ id: 'sales', label: 'Sales Records', icon: Package },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<tab.icon className="w-4 h-4" />
<span>{tab.label}</span>
</button>
))}
</nav>
</div>
<div className="p-6">
{/* Search and Filters */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="overdue">Overdue</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
</select>
</div>
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Receipts Summary */}
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
<Receipt className="w-5 h-5 mr-2 text-blue-600" />
Receipts Summary
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400">Total Receipts</span>
<span className="font-semibold">{totalReceipts}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400">Paid</span>
<span className="font-semibold text-green-600">{paidReceipts}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400">Pending</span>
<span className="font-semibold text-yellow-600">{pendingReceipts}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400">Overdue</span>
<span className="font-semibold text-red-600">{overdueReceipts}</span>
</div>
</div>
</div>
{/* Recent Activity */}
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
<Activity className="w-5 h-5 mr-2 text-green-600" />
Recent Activity
</h3>
<p className="text-secondary-600 dark:text-secondary-400">
Billing and payment features will be available soon.
</p>
<div className="space-y-3">
{sales?.slice(0, 5).map((sale: any) => (
<div key={sale.id} className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">
{sale.productName} sold to {sale.customerName}
</span>
<span className="font-semibold">
<DualCurrencyDisplay amount={sale.amount} currency={sale.currency} />
</span>
</div>
))}
</div>
</div>
</div>
)}
{activeTab === 'receipts' && (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Invoice
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Customer
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Amount
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Due Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredReceipts.map((receipt: any) => (
<tr key={receipt.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{receipt.invoiceNumber}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{receipt.customerName}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
<DualCurrencyDisplay amount={receipt.amount} currency={receipt.currency} />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(receipt.status)}`}>
{getStatusIcon(receipt.status)}
<span className="ml-1">{receipt.status}</span>
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{formatDate(receipt.dueDate)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">
<Eye className="w-4 h-4" />
</button>
<button className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300">
<Download className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{activeTab === 'payments' && (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Description
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Amount
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredPayments.map((payment: any) => (
<tr key={payment.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{formatDate(payment.timestamp)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{payment.description}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
<DualCurrencyDisplay amount={payment.amount} currency={payment.currency} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
payment.type === 'received'
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'
}`}>
{payment.type === 'received' ? 'Received' : 'Sent'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(payment.status)}`}>
{getStatusIcon(payment.status)}
<span className="ml-1">{payment.status}</span>
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{activeTab === 'sales' && (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Customer
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Product
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Amount
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Commission
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredSales.map((sale: any) => (
<tr key={sale.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{formatDate(sale.timestamp)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{sale.customerName}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{sale.productName}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
<DualCurrencyDisplay amount={sale.amount} currency={sale.currency} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
<DualCurrencyDisplay amount={sale.commission} currency={sale.currency} />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(sale.status)}`}>
{getStatusIcon(sale.status)}
<span className="ml-1">{sale.status}</span>
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
);
};
export default Billing;
export default ResellerBilling;

View File

@ -1,91 +1,285 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useAppSelector, useAppDispatch } from '../../store/hooks';
import { useNavigate } from 'react-router-dom';
import { setStats, setRecentActivities, setQuickActions } from '../../store/slices/dashboardSlice';
import { mockDashboardStats, mockResellerQuickActions, mockResellerRecentActivities } from '../../data/mockData';
import {
fetchDashboardStats,
fetchCustomers,
fetchProducts,
fetchSales,
fetchRecentActivities,
addCustomer
} from '../../store/reseller/resellerDashboardSlice';
import { formatNumber, formatRelativeTime, formatPercentage } from '../../utils/format';
import RevenueChart from '../../components/charts/RevenueChart';
import ResellerPerformanceChart from '../../components/charts/ResellerPerformanceChart';
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
import MarkProductSoldForm from '../../components/forms/MarkProductSoldForm';
import {
TrendingUp,
Package,
Users,
Cloud,
DollarSign,
UserPlus,
CheckCircle,
Briefcase,
GraduationCap,
BarChart3,
CreditCard,
Headphones,
ShoppingBag,
Award,
HelpCircle,
Settings,
Wallet,
BookOpen,
Zap,
Target,
Star,
TrendingUp,
ArrowUpRight,
Activity
Star,
CheckCircle,
Activity,
Receipt,
FileText,
X,
Plus
} from 'lucide-react';
import { cn } from '../../utils/cn';
import toast from 'react-hot-toast';
const ResellerDashboard: React.FC = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard);
const { user } = useAppSelector((state) => state.auth);
const [isCustomerModalOpen, setIsCustomerModalOpen] = useState(false);
const [isMarkProductSoldModalOpen, setIsMarkProductSoldModalOpen] = useState(false);
const [customerForm, setCustomerForm] = useState({
firstName: '',
lastName: '',
email: '',
company: '',
phone: ''
});
const {
stats,
customers,
products,
sales,
recentActivities,
isLoading,
error
} = useAppSelector((state: any) => state.resellerDashboard);
const { user } = useAppSelector(state => state.auth);
useEffect(() => {
// Initialize dashboard data
dispatch(setStats(mockDashboardStats));
dispatch(setRecentActivities(mockResellerRecentActivities));
dispatch(setQuickActions(mockResellerQuickActions));
dispatch(fetchDashboardStats());
dispatch(fetchCustomers());
dispatch(fetchProducts());
dispatch(fetchSales());
dispatch(fetchRecentActivities());
}, [dispatch]);
const handleQuickAction = (action: any) => {
switch (action.id) {
case 'add-customer':
navigate('/reseller-dashboard/customers');
setIsCustomerModalOpen(true);
break;
case 'create-instance':
navigate('/reseller-dashboard/instances');
case 'manage-products':
navigate('/reseller-dashboard/products');
break;
case 'view-sales':
navigate('/reseller-dashboard/sales');
break;
case 'billing':
navigate('/reseller-dashboard/billing');
break;
case 'support':
navigate('/reseller-dashboard/support');
break;
case 'training':
navigate('/reseller-dashboard/training');
break;
case 'reports':
navigate('/reseller-dashboard/reports');
break;
case 'wallet':
navigate('/reseller-dashboard/wallet');
break;
case 'marketplace':
navigate('/reseller-dashboard/marketplace');
break;
case 'certifications':
navigate('/reseller-dashboard/certifications');
break;
case 'knowledge-base':
navigate('/reseller-dashboard/knowledge-base');
break;
case 'settings':
navigate('/reseller-dashboard/settings');
case 'mark-product-sold':
setIsMarkProductSoldModalOpen(true);
break;
default:
break;
toast('Feature coming soon!');
}
};
const handleCustomerSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!customerForm.firstName || !customerForm.lastName || !customerForm.email) {
toast.error('Please fill in all required fields');
return;
}
try {
await dispatch(addCustomer(customerForm)).unwrap();
toast.success('Customer added successfully!');
setIsCustomerModalOpen(false);
setCustomerForm({ firstName: '', lastName: '', email: '', company: '', phone: '' });
// Refresh customers list
dispatch(fetchCustomers());
} catch (error: any) {
toast.error(error.message || 'Failed to add customer');
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setCustomerForm(prev => ({
...prev,
[name]: value
}));
};
// Use real API data instead of mock data
const realCustomers = customers || [];
const realProducts = products || [];
const realSales = sales || [];
const realActivities = recentActivities || [];
const realStats = stats || {};
// Calculate metrics from real data
const activeCustomers = realCustomers.filter((c: any) => c.status === 'active').length;
const totalProducts = realProducts.filter((p: any) => p.status === 'active').length;
const monthlyRevenue = realSales
.filter((s: any) => {
const saleDate = new Date(s.timestamp);
const now = new Date();
return saleDate.getMonth() === now.getMonth() &&
saleDate.getFullYear() === now.getFullYear();
})
.reduce((sum: number, sale: any) => sum + sale.amount, 0);
const quickActions = [
{
id: 'add-customer',
title: 'Add Customer',
description: 'Add a new customer to your portfolio',
icon: Users,
color: 'bg-blue-500',
href: '#'
},
{
id: 'mark-product-sold',
title: 'Mark Product Sold',
description: 'Record a successful product sale',
icon: Package,
color: 'bg-green-500',
href: '#'
},
{
id: 'manage-products',
title: 'Manage Products',
description: 'View and manage your product catalog',
icon: Package,
color: 'bg-purple-500',
href: '/reseller-dashboard/products'
},
{
id: 'view-sales',
title: 'View Sales',
description: 'Track your sales performance',
icon: TrendingUp,
color: 'bg-orange-500',
href: '/reseller-dashboard/sales'
},
{
id: 'billing',
title: 'Billing',
description: 'Manage invoices and payments',
icon: Receipt,
color: 'bg-indigo-500',
href: '/reseller-dashboard/billing'
},
{
id: 'reports',
title: 'Reports',
description: 'Generate performance reports',
icon: FileText,
color: 'bg-teal-500',
href: '/reseller-dashboard/reports'
}
];
// Show loading state
if (isLoading) {
return (
<div className="space-y-8">
{/* Welcome Section Skeleton */}
<div className="bg-gradient-to-r from-emerald-600 via-teal-600 to-cyan-600 rounded-3xl p-8 text-white relative overflow-hidden">
<div className="animate-pulse">
<div className="h-8 bg-white/20 rounded w-1/3 mb-2"></div>
<div className="h-6 bg-white/20 rounded w-2/3"></div>
</div>
</div>
{/* Metrics Skeleton */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/2 mb-4"></div>
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
</div>
</div>
))}
</div>
{/* Loading Message */}
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<div className="text-gray-600 dark:text-gray-400">Loading dashboard data...</div>
</div>
</div>
);
}
// Show error state
if (error) {
return (
<div className="text-center py-8">
<div className="text-red-500 text-lg mb-2">Error loading dashboard</div>
<div className="text-gray-600">{error}</div>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Retry
</button>
</div>
);
}
// Show empty state if no data
if (!realCustomers.length && !realProducts.length && !realSales.length) {
return (
<div className="space-y-8">
{/* Welcome Section */}
<div className="bg-gradient-to-r from-emerald-600 via-teal-600 to-cyan-600 rounded-3xl p-8 text-white relative overflow-hidden">
<div className="absolute inset-0 bg-black/10"></div>
<div className="relative z-10">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2">Welcome back, {user ? `${user.firstName} ${user.lastName}` : 'User'}!</h1>
<p className="text-emerald-100 text-lg">Ready to grow your business? Let's get started!</p>
</div>
</div>
</div>
</div>
{/* Empty State */}
<div className="text-center py-16">
<div className="w-24 h-24 bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-6">
<Package className="w-12 h-12 text-gray-400" />
</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">No Data Available</h3>
<p className="text-gray-600 dark:text-gray-400 mb-6 max-w-md mx-auto">
Your dashboard is empty. Start by adding customers and recording your first sales.
Please ensure you have active partnerships with resellers.
</p>
<div className="flex justify-center space-x-4">
<button
onClick={() => setIsCustomerModalOpen(true)}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Add Customer
</button>
<button
onClick={() => setIsMarkProductSoldModalOpen(true)}
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Mark Product Sold
</button>
</div>
</div>
</div>
);
}
return (
<div className="space-y-8">
{/* Welcome Section */}
@ -94,18 +288,14 @@ const ResellerDashboard: React.FC = () => {
<div className="relative z-10">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2">Welcome back, {user ? `${user.firstName} ${user.lastName}` : 'User'}! </h1>
<p className="text-emerald-100 text-lg">Here's what's happening with your cloud services business today.</p>
<h1 className="text-3xl font-bold mb-2">Welcome back, {user ? `${user.firstName} ${user.lastName}` : 'User'}!</h1>
<p className="text-emerald-100 text-lg">Here's what's happening with your business today</p>
</div>
<div className="hidden lg:flex items-center space-x-4">
<div className="text-center">
<div className="text-2xl font-bold">{formatNumber(stats.totalResellers)}</div>
<div className="text-emerald-100 text-sm">Active Customers</div>
</div>
<div className="text-right">
<div className="text-center">
<DualCurrencyDisplay
amount={stats.commissionEarned}
currency={stats.currency}
amount={monthlyRevenue}
currency={realStats.currency || 'USD'}
className="text-2xl font-bold text-white"
/>
<div className="text-emerald-100 text-sm">Monthly Revenue</div>
@ -113,200 +303,280 @@ const ResellerDashboard: React.FC = () => {
</div>
</div>
</div>
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -translate-y-32 translate-x-32"></div>
<div className="absolute bottom-0 left-0 w-48 h-48 bg-white/5 rounded-full translate-y-24 -translate-x-24"></div>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300 group">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<DollarSign className="w-6 h-6 text-white" />
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
<DualCurrencyDisplay
amount={stats.totalRevenue}
currency={realStats.currency || 'USD'}
className="text-2xl"
/>
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Total Revenue</p>
<div className="flex items-center mt-2 text-green-600 text-sm">
<ArrowUpRight className="w-4 h-4 mr-1" />
+{realStats.monthlyGrowth || 0}% from last month
</div>
</div>
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<TrendingUp className="w-5 h-5 text-green-500" />
</div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
<DualCurrencyDisplay
amount={stats.totalRevenue}
currency={stats.currency}
className="text-2xl"
/>
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Total Revenue</p>
<div className="flex items-center mt-2 text-green-600 text-sm">
<ArrowUpRight className="w-4 h-4 mr-1" />
+{stats.monthlyGrowth}% from last month
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300 group">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Users className="w-6 h-6 text-white" />
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">{activeCustomers}</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Active Customers</p>
<div className="flex items-center mt-2 text-emerald-600 text-sm">
<ArrowUpRight className="w-4 h-4 mr-1" />
+{realCustomers.filter((c: any) => {
const customerDate = new Date(c.lastPurchase);
const now = new Date();
return customerDate.getMonth() === now.getMonth() &&
customerDate.getFullYear() === now.getFullYear();
}).length} this month
</div>
</div>
<div className="w-12 h-12 bg-emerald-100 dark:bg-emerald-900/50 rounded-lg flex items-center justify-center">
<Users className="w-6 h-6 text-emerald-600 dark:text-emerald-400" />
</div>
<Activity className="w-5 h-5 text-emerald-500" />
</div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
{formatNumber(stats.totalResellers)}
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Active Customers</p>
<div className="flex items-center mt-2 text-emerald-600 text-sm">
<ArrowUpRight className="w-4 h-4 mr-1" />
+5 new this month
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300 group">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-purple-500 to-purple-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Cloud className="w-6 h-6 text-white" />
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">{totalProducts}</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Active Products</p>
<div className="flex items-center mt-2 text-purple-600 text-sm">
<ArrowUpRight className="w-4 h-4 mr-1" />
+{realProducts.filter((p: any) => {
const productDate = new Date(p.timestamp || Date.now());
const now = new Date();
return productDate.getMonth() === now.getMonth() &&
productDate.getFullYear() === now.getFullYear();
}).length} this month
</div>
</div>
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center">
<Package className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<Zap className="w-5 h-5 text-purple-500" />
</div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
{formatNumber(stats.activePartnerships)}
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Cloud Instances</p>
<div className="flex items-center mt-2 text-purple-600 text-sm">
<ArrowUpRight className="w-4 h-4 mr-1" />
+12 new instances
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300 group">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-orange-500 to-orange-600 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Target className="w-6 h-6 text-white" />
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
{formatPercentage(realStats.commissionRate || 0)}
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Commission Rate</p>
<div className="flex items-center mt-2 text-orange-600 text-sm">
<CheckCircle className="w-4 h-4 mr-1" />
{(realStats.commissionRate || 0) >= 15 ? 'Premium tier' : 'Standard tier'}
</div>
</div>
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900/50 rounded-lg flex items-center justify-center">
<Star className="w-5 h-5 text-orange-600 dark:text-orange-400" />
</div>
<Star className="w-5 h-5 text-orange-500" />
</div>
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
{formatPercentage(15)}
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">Commission Rate</p>
<div className="flex items-center mt-2 text-orange-600 text-sm">
<CheckCircle className="w-4 h-4 mr-1" />
Premium tier
</div>
</div>
</div>
{/* Quick Actions & Recent Activity */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Quick Actions */}
<div className="lg:col-span-1">
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
<Zap className="w-5 h-5 mr-2 text-emerald-500" />
Quick Actions
</h3>
<div className="space-y-3">
{quickActions.slice(0, 6).map((action) => (
<button
key={action.id}
onClick={() => handleQuickAction(action)}
className="w-full flex items-center p-4 rounded-xl bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800 hover:from-emerald-50 hover:to-teal-50 dark:hover:from-emerald-900/20 dark:hover:to-teal-900/20 transition-all duration-300 group border border-slate-200 dark:border-slate-600"
>
<div className="w-10 h-10 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center mr-4 group-hover:scale-110 transition-transform duration-300">
{action.icon === 'UserPlus' && <UserPlus className="w-5 h-5 text-white" />}
{action.icon === 'Cloud' && <Cloud className="w-5 h-5 text-white" />}
{action.icon === 'CreditCard' && <CreditCard className="w-5 h-5 text-white" />}
{action.icon === 'Headphones' && <Headphones className="w-5 h-5 text-white" />}
{action.icon === 'GraduationCap' && <GraduationCap className="w-5 h-5 text-white" />}
{action.icon === 'BarChart3' && <BarChart3 className="w-5 h-5 text-white" />}
{action.icon === 'Wallet' && <Wallet className="w-5 h-5 text-white" />}
{action.icon === 'ShoppingBag' && <ShoppingBag className="w-5 h-5 text-white" />}
{action.icon === 'Award' && <Award className="w-5 h-5 text-white" />}
{action.icon === 'HelpCircle' && <HelpCircle className="w-5 h-5 text-white" />}
{action.icon === 'Settings' && <Settings className="w-5 h-5 text-white" />}
{action.icon === 'BookOpen' && <BookOpen className="w-5 h-5 text-white" />}
{/* Quick Actions */}
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Quick Actions</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{quickActions.map((action) => {
const IconComponent = action.icon;
return (
<button
key={action.id}
onClick={() => handleQuickAction(action)}
className="p-4 rounded-xl border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 transition-all duration-200 hover:shadow-md group"
>
<div className="flex items-center space-x-3">
<div className={cn("w-10 h-10 rounded-lg flex items-center justify-center", action.color)}>
<IconComponent className="w-5 h-5 text-white" />
</div>
<div className="text-left flex-1">
<p className="font-semibold text-slate-900 dark:text-white group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors duration-300">
<div className="text-left">
<h4 className="font-medium text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{action.title}
</p>
</h4>
<p className="text-sm text-slate-600 dark:text-slate-400">{action.description}</p>
</div>
<ArrowUpRight className="w-4 h-4 text-slate-400 group-hover:text-emerald-500 transition-colors duration-300" />
</button>
))}
</div>
</div>
</div>
{/* Recent Activity */}
<div className="lg:col-span-2">
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
<Activity className="w-5 h-5 mr-2 text-emerald-500" />
Recent Activity
</h3>
<div className="space-y-4">
{recentActivities.map((activity) => (
<div key={activity.id} className="flex items-start p-4 rounded-xl bg-slate-50 dark:bg-slate-700/50 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<div className="w-10 h-10 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center mr-4 flex-shrink-0">
{activity.type === 'customer_added' && <UserPlus className="w-5 h-5 text-white" />}
{activity.type === 'instance_created' && <Cloud className="w-5 h-5 text-white" />}
{activity.type === 'payment_received' && <CreditCard className="w-5 h-5 text-white" />}
{activity.type === 'support_ticket' && <Headphones className="w-5 h-5 text-white" />}
{activity.type === 'training_completed' && <GraduationCap className="w-5 h-5 text-white" />}
</div>
<div className="flex-1 min-w-0">
<p className="font-semibold text-slate-900 dark:text-white mb-1">
{activity.title}
</p>
<p className="text-sm text-slate-600 dark:text-slate-400 mb-2">
{activity.description}
</p>
{activity.amount && (
<div className="mb-2">
<DualCurrencyDisplay
amount={activity.amount}
currency={activity.currency}
className="text-sm font-semibold text-emerald-600"
/>
</div>
)}
<p className="text-xs text-slate-500 dark:text-slate-400">
{formatRelativeTime(activity.timestamp)}
</p>
</div>
</div>
))}
</button>
);
})}
</div>
</div>
{/* Recent Activity */}
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
Recent Activity
</h3>
<div className="space-y-4">
{realActivities.slice(0, 8).map((activity: any) => (
<div key={activity.id} className="flex items-start p-4 rounded-xl bg-slate-50 dark:bg-slate-700/50 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<div className="w-10 h-10 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center mr-4 flex-shrink-0">
{activity.type === 'customer_added' && <Users className="w-5 h-5 text-white" />}
{activity.type === 'product_sold' && <Package className="w-5 h-5 text-white" />}
{activity.type === 'payment_received' && <Receipt className="w-5 h-5 text-white" />}
{activity.type === 'commission_earned' && <TrendingUp className="w-5 h-5 text-white" />}
{!['customer_added', 'product_sold', 'payment_received', 'commission_earned'].includes(activity.type) && <Activity className="w-5 h-5 text-white" />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-slate-900 dark:text-white">
{activity.title}
</p>
<span className="text-xs text-slate-500 dark:text-slate-400">
{formatRelativeTime(activity.timestamp)}
</span>
</div>
<p className="text-sm text-slate-600 dark:text-slate-400 mt-1">
{activity.description}
</p>
{activity.amount && (
<div className="flex items-center mt-2">
<span className="text-sm font-medium text-emerald-600 dark:text-emerald-400">
{activity.currency === 'USD' ? '$' : '₹'}{activity.amount}
</span>
</div>
)}
</div>
</div>
<div className="mt-6 pt-4 border-t border-slate-200 dark:border-slate-700">
<button className="text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 font-medium flex items-center">
View all activities
<ArrowUpRight className="w-4 h-4 ml-1" />
))}
</div>
</div>
{/* Customer Modal */}
{isCustomerModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 w-full max-w-md mx-4">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold text-slate-900 dark:text-white">Add New Customer</h3>
<button
onClick={() => setIsCustomerModalOpen(false)}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
<X className="w-6 h-6" />
</button>
</div>
</div>
</div>
</div>
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
<BarChart3 className="w-5 h-5 mr-2 text-emerald-500" />
Revenue Overview
</h3>
<div className="h-64">
<RevenueChart />
</div>
</div>
<form onSubmit={handleCustomerSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
First Name *
</label>
<input
type="text"
name="firstName"
value={customerForm.firstName}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
required
/>
</div>
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
<Target className="w-5 h-5 mr-2 text-emerald-500" />
Customer Performance
</h3>
<div className="h-64">
<ResellerPerformanceChart />
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Last Name *
</label>
<input
type="text"
name="lastName"
value={customerForm.lastName}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Email *
</label>
<input
type="email"
name="email"
value={customerForm.email}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Company
</label>
<input
type="text"
name="company"
value={customerForm.company}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Phone
</label>
<input
type="tel"
name="phone"
value={customerForm.phone}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent dark:bg-slate-700 dark:text-white"
/>
</div>
<div className="flex space-x-3 pt-4">
<button
type="button"
onClick={() => setIsCustomerModalOpen(false)}
className="flex-1 px-4 py-2 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors"
>
Add Customer
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Mark Product Sold Modal */}
{isMarkProductSoldModalOpen && (
<MarkProductSoldForm
isOpen={isMarkProductSoldModalOpen}
onClose={() => setIsMarkProductSoldModalOpen(false)}
onSuccess={() => {
// Refresh data after marking product as sold
dispatch(fetchDashboardStats());
dispatch(fetchSales());
dispatch(fetchRecentActivities());
}}
products={realProducts}
customers={realCustomers}
/>
)}
</div>
);
};

View File

@ -0,0 +1,269 @@
import { apiService } from './api';
export interface ResellerDashboardStats {
totalRevenue: number;
activeCustomers: number;
totalProducts: number;
commissionRate: number;
monthlyGrowth: number;
currency: string;
commissionEarned: number;
averageDealSize: number;
conversionRate: number;
}
export interface ResellerCustomer {
id: string;
name: string;
email: string;
company: string;
status: 'active' | 'inactive' | 'pending';
totalSpent: number;
lastPurchase: string;
products: number;
region: string;
}
export interface ResellerProduct {
id: string;
name: string;
description: string;
price: number;
currency: string;
category: string;
status: 'active' | 'inactive';
salesCount: number;
revenue: number;
vendorId: string;
vendorName: string;
}
export interface ResellerSale {
id: string;
customerId: string;
customerName: string;
productId: string;
productName: string;
amount: number;
currency: string;
commission: number;
status: 'completed' | 'pending' | 'cancelled';
timestamp: string;
vendorId: string;
vendorName: string;
}
export interface ResellerReceipt {
id: string;
customerId: string;
customerName: string;
amount: number;
currency: string;
status: 'paid' | 'pending' | 'overdue';
dueDate: string;
paidDate?: string;
invoiceNumber: string;
description: string;
}
export interface ResellerPayment {
id: string;
amount: number;
currency: string;
type: 'received' | 'sent';
status: 'completed' | 'pending' | 'failed';
timestamp: string;
description: string;
customerId?: string;
customerName?: string;
}
export interface ResellerRecentActivity {
id: string;
type: 'customer_added' | 'product_sold' | 'payment_received' | 'support_ticket' | 'training_completed';
title: string;
description: string;
timestamp: string;
amount?: number;
currency?: string;
customerId?: string;
customerName?: string;
productId?: string;
productName?: string;
}
class ResellerDashboardService {
private baseUrl: string;
constructor() {
this.baseUrl = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';
}
private getHeaders() {
const token = localStorage.getItem('accessToken');
return {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
}
// Fetch dashboard statistics
async getDashboardStats(): Promise<ResellerDashboardStats> {
const response = await fetch(`${this.baseUrl}/resellers/dashboard/stats`, {
headers: this.getHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch dashboard stats');
}
const data = await response.json();
return data.data;
}
// Fetch customers
async getCustomers(): Promise<ResellerCustomer[]> {
const response = await fetch(`${this.baseUrl}/resellers/customers`, {
headers: this.getHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch customers');
}
const data = await response.json();
return data.data;
}
// Fetch products
async getProducts(): Promise<ResellerProduct[]> {
const response = await fetch(`${this.baseUrl}/resellers/products`, {
headers: this.getHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch products');
}
const data = await response.json();
return data.data;
}
// Fetch sales
async getSales(): Promise<ResellerSale[]> {
const response = await fetch(`${this.baseUrl}/resellers/sales`, {
headers: this.getHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch sales');
}
const data = await response.json();
return data.data;
}
// Fetch receipts
async getReceipts(): Promise<ResellerReceipt[]> {
const response = await fetch(`${this.baseUrl}/resellers/receipts`, {
headers: this.getHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch receipts');
}
const data = await response.json();
return data.data;
}
// Fetch payments
async getPayments(): Promise<ResellerPayment[]> {
const response = await fetch(`${this.baseUrl}/resellers/payments`, {
headers: this.getHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch payments');
}
const data = await response.json();
return data.data;
}
// Fetch recent activities
async getRecentActivities(): Promise<ResellerRecentActivity[]> {
const response = await fetch(`${this.baseUrl}/resellers/activities`, {
headers: this.getHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch recent activities');
}
const data = await response.json();
return data.data;
}
// Add new customer
async addCustomer(customerData: Partial<ResellerCustomer>): Promise<ResellerCustomer> {
const response = await fetch(`${this.baseUrl}/resellers/customers`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(customerData)
});
if (!response.ok) {
throw new Error('Failed to add customer');
}
const data = await response.json();
return data.data;
}
// Create sale
async createSale(saleData: Partial<ResellerSale>): Promise<ResellerSale> {
const response = await fetch(`${this.baseUrl}/resellers/sales`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(saleData)
});
if (!response.ok) {
throw new Error('Failed to create sale');
}
const data = await response.json();
return data.data;
}
// Update customer
async updateCustomer(customerId: string, customerData: Partial<ResellerCustomer>): Promise<ResellerCustomer> {
const response = await fetch(`${this.baseUrl}/resellers/customers/${customerId}`, {
method: 'PUT',
headers: this.getHeaders(),
body: JSON.stringify(customerData)
});
if (!response.ok) {
throw new Error('Failed to update customer');
}
const data = await response.json();
return data.data;
}
// Delete customer
async deleteCustomer(customerId: string): Promise<void> {
const response = await fetch(`${this.baseUrl}/resellers/customers/${customerId}`, {
method: 'DELETE',
headers: this.getHeaders()
});
if (!response.ok) {
throw new Error('Failed to delete customer');
}
}
}
export const resellerDashboardService = new ResellerDashboardService();

View File

@ -0,0 +1,304 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { resellerDashboardService, ResellerDashboardStats, ResellerCustomer, ResellerProduct, ResellerSale, ResellerReceipt, ResellerPayment, ResellerRecentActivity } from '../../services/resellerDashboardService';
export interface ResellerDashboardState {
stats: ResellerDashboardStats;
customers: ResellerCustomer[];
products: ResellerProduct[];
sales: ResellerSale[];
receipts: ResellerReceipt[];
payments: ResellerPayment[];
recentActivities: ResellerRecentActivity[];
isLoading: boolean;
error: string | null;
}
const initialState: ResellerDashboardState = {
stats: {
totalRevenue: 0,
activeCustomers: 0,
totalProducts: 0,
commissionRate: 0,
monthlyGrowth: 0,
currency: 'USD',
commissionEarned: 0,
averageDealSize: 0,
conversionRate: 0,
},
customers: [],
products: [],
sales: [],
receipts: [],
payments: [],
recentActivities: [],
isLoading: false,
error: null,
};
// Async thunks
export const fetchDashboardStats = createAsyncThunk(
'resellerDashboard/fetchStats',
async (_, { rejectWithValue }) => {
try {
return await resellerDashboardService.getDashboardStats();
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const fetchCustomers = createAsyncThunk(
'resellerDashboard/fetchCustomers',
async (_, { rejectWithValue }) => {
try {
return await resellerDashboardService.getCustomers();
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const fetchProducts = createAsyncThunk(
'resellerDashboard/fetchProducts',
async (_, { rejectWithValue }) => {
try {
return await resellerDashboardService.getProducts();
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const fetchSales = createAsyncThunk(
'resellerDashboard/fetchSales',
async (_, { rejectWithValue }) => {
try {
return await resellerDashboardService.getSales();
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const fetchReceipts = createAsyncThunk(
'resellerDashboard/fetchReceipts',
async (_, { rejectWithValue }) => {
try {
return await resellerDashboardService.getReceipts();
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const fetchPayments = createAsyncThunk(
'resellerDashboard/fetchPayments',
async (_, { rejectWithValue }) => {
try {
return await resellerDashboardService.getPayments();
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const fetchRecentActivities = createAsyncThunk(
'resellerDashboard/fetchRecentActivities',
async (_, { rejectWithValue }) => {
try {
return await resellerDashboardService.getRecentActivities();
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const addCustomer = createAsyncThunk(
'resellerDashboard/addCustomer',
async (customerData: Partial<ResellerCustomer>, { rejectWithValue }) => {
try {
return await resellerDashboardService.addCustomer(customerData);
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const createSale = createAsyncThunk(
'resellerDashboard/createSale',
async (saleData: Partial<ResellerSale>, { rejectWithValue }) => {
try {
return await resellerDashboardService.createSale(saleData);
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
const resellerDashboardSlice = createSlice({
name: 'resellerDashboard',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
updateStats: (state, action: PayloadAction<Partial<ResellerDashboardStats>>) => {
state.stats = { ...state.stats, ...action.payload };
},
addRecentActivity: (state, action: PayloadAction<ResellerRecentActivity>) => {
state.recentActivities.unshift(action.payload);
if (state.recentActivities.length > 20) {
state.recentActivities = state.recentActivities.slice(0, 20);
}
},
updateCustomer: (state, action: PayloadAction<ResellerCustomer>) => {
const index = state.customers.findIndex(c => c.id === action.payload.id);
if (index !== -1) {
state.customers[index] = action.payload;
}
},
removeCustomer: (state, action: PayloadAction<string>) => {
state.customers = state.customers.filter(c => c.id !== action.payload);
},
addSale: (state, action: PayloadAction<ResellerSale>) => {
state.sales.unshift(action.payload);
if (state.sales.length > 100) {
state.sales = state.sales.slice(0, 100);
}
},
addReceipt: (state, action: PayloadAction<ResellerReceipt>) => {
state.receipts.unshift(action.payload);
if (state.receipts.length > 100) {
state.receipts = state.receipts.slice(0, 100);
}
},
addPayment: (state, action: PayloadAction<ResellerPayment>) => {
state.payments.unshift(action.payload);
if (state.payments.length > 100) {
state.payments = state.payments.slice(0, 100);
}
},
},
extraReducers: (builder) => {
builder
// Dashboard Stats
.addCase(fetchDashboardStats.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchDashboardStats.fulfilled, (state, action) => {
state.isLoading = false;
state.stats = action.payload;
})
.addCase(fetchDashboardStats.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// Customers
.addCase(fetchCustomers.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchCustomers.fulfilled, (state, action) => {
state.isLoading = false;
state.customers = action.payload;
})
.addCase(fetchCustomers.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// Products
.addCase(fetchProducts.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.isLoading = false;
state.products = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// Sales
.addCase(fetchSales.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchSales.fulfilled, (state, action) => {
state.isLoading = false;
state.sales = action.payload;
})
.addCase(fetchSales.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// Receipts
.addCase(fetchReceipts.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchReceipts.fulfilled, (state, action) => {
state.isLoading = false;
state.receipts = action.payload;
})
.addCase(fetchReceipts.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// Payments
.addCase(fetchPayments.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchPayments.fulfilled, (state, action) => {
state.isLoading = false;
state.payments = action.payload;
})
.addCase(fetchPayments.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// Recent Activities
.addCase(fetchRecentActivities.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchRecentActivities.fulfilled, (state, action) => {
state.isLoading = false;
state.recentActivities = action.payload;
})
.addCase(fetchRecentActivities.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// Add Customer
.addCase(addCustomer.fulfilled, (state, action) => {
state.customers.unshift(action.payload);
state.stats.activeCustomers += 1;
})
// Create Sale
.addCase(createSale.fulfilled, (state, action) => {
state.sales.unshift(action.payload);
state.stats.totalRevenue += action.payload.amount;
state.stats.commissionEarned += action.payload.commission;
});
},
});
export const {
setLoading,
setError,
updateStats,
addRecentActivity,
updateCustomer,
removeCustomer,
addSale,
addReceipt,
addPayment,
} = resellerDashboardSlice.actions;
export default resellerDashboardSlice.reducer;

View File

@ -14,7 +14,7 @@ export interface DashboardStats {
export interface RecentActivity {
id: string;
type: 'reseller_added' | 'deal_closed' | 'commission_earned' | 'partnership_approved' | 'training_completed' | 'customer_added' | 'instance_created' | 'payment_received' | 'support_ticket';
type: 'reseller_added' | 'deal_closed' | 'commission_earned' | 'partnership_approved' | 'training_completed' | 'customer_added' | 'instance_created' | 'payment_received' | 'support_ticket' | 'product_sold';
title: string;
description: string;
timestamp: string;