v1.0.1-alpha
This commit is contained in:
parent
8ac3c89b10
commit
7f8480a03d
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
265
src/components/CommissionTrends.tsx
Normal file
265
src/components/CommissionTrends.tsx
Normal 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;
|
||||
@ -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>
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 />;
|
||||
}
|
||||
}
|
||||
|
||||
314
src/components/ResellerPerformance.tsx
Normal file
314
src/components/ResellerPerformance.tsx
Normal 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;
|
||||
@ -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) => {
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
295
src/components/forms/MarkProductSoldForm.tsx
Normal file
295
src/components/forms/MarkProductSoldForm.tsx
Normal 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;
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
62
src/hooks/useSocket.ts
Normal 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;
|
||||
@ -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
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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)' }}>
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
269
src/services/resellerDashboardService.ts
Normal file
269
src/services/resellerDashboardService.ts
Normal 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();
|
||||
304
src/store/reseller/resellerDashboardSlice.ts
Normal file
304
src/store/reseller/resellerDashboardSlice.ts
Normal 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;
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user