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={
|
<Route path="/reseller-dashboard/billing" element={
|
||||||
<ProtectedRoute requiredRole="reseller_admin">
|
<ProtectedRoute requiredRole="reseller_admin">
|
||||||
<ResellerLayout>
|
<ResellerLayout>
|
||||||
<PlaceholderPage title="Billing & Payments" description="Manage your billing, invoices, and payment methods" />
|
<ResellerBilling />
|
||||||
</ResellerLayout>
|
</ResellerLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
@ -505,7 +505,7 @@ function App() {
|
|||||||
</Routes>
|
</Routes>
|
||||||
<CookieConsent />
|
<CookieConsent />
|
||||||
<Toast />
|
<Toast />
|
||||||
<AuthDebug />
|
{/* <AuthDebug /> */}
|
||||||
<DeveloperFeedback />
|
<DeveloperFeedback />
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@ -27,17 +27,13 @@ const AuthInitializer: React.FC<{ children: React.ReactNode }> = ({ children })
|
|||||||
// Try to get current user
|
// Try to get current user
|
||||||
try {
|
try {
|
||||||
await dispatch(getCurrentUser()).unwrap();
|
await dispatch(getCurrentUser()).unwrap();
|
||||||
console.log('User loaded successfully');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Failed to get current user, trying to refresh token...', error);
|
|
||||||
// If getting user fails, try to refresh token
|
// If getting user fails, try to refresh token
|
||||||
try {
|
try {
|
||||||
await dispatch(refreshUserToken()).unwrap();
|
await dispatch(refreshUserToken()).unwrap();
|
||||||
// Try to get user again after token refresh
|
// Try to get user again after token refresh
|
||||||
await dispatch(getCurrentUser()).unwrap();
|
await dispatch(getCurrentUser()).unwrap();
|
||||||
console.log('User loaded successfully after token refresh');
|
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
console.log('Token refresh failed, clearing auth data...', refreshError);
|
|
||||||
// Clear invalid tokens and logout
|
// Clear invalid tokens and logout
|
||||||
localStorage.removeItem('accessToken');
|
localStorage.removeItem('accessToken');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
@ -45,8 +41,6 @@ const AuthInitializer: React.FC<{ children: React.ReactNode }> = ({ children })
|
|||||||
dispatch(logout());
|
dispatch(logout());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log('No tokens found in localStorage');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth initialization error:', 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 {
|
import {
|
||||||
MessageSquare,
|
MessageCircle,
|
||||||
Send,
|
Bug,
|
||||||
|
Lightbulb,
|
||||||
|
AlertTriangle,
|
||||||
|
Star,
|
||||||
X,
|
X,
|
||||||
AlertCircle,
|
Send,
|
||||||
CheckCircle,
|
Minimize2,
|
||||||
Clock,
|
Maximize2
|
||||||
Bug,
|
|
||||||
Lightbulb,
|
|
||||||
HelpCircle,
|
|
||||||
Star
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
interface FeedbackTicket {
|
interface FeedbackTicket {
|
||||||
id: string;
|
id: string;
|
||||||
@ -41,7 +41,7 @@ const DeveloperFeedback: React.FC = () => {
|
|||||||
const ticketTypes = [
|
const ticketTypes = [
|
||||||
{ id: 'bug', label: 'Bug Report', icon: Bug, color: 'text-red-600' },
|
{ id: 'bug', label: 'Bug Report', icon: Bug, color: 'text-red-600' },
|
||||||
{ id: 'feature', label: 'Feature Request', icon: Lightbulb, color: 'text-blue-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' }
|
{ id: 'general', label: 'General Feedback', icon: Star, color: 'text-yellow-600' }
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ const DeveloperFeedback: React.FC = () => {
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!currentTicket.title || !currentTicket.description) {
|
if (!currentTicket.title || !currentTicket.description) {
|
||||||
alert('Please fill in all required fields');
|
toast.error('Please fill in all required fields');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,9 +107,10 @@ const DeveloperFeedback: React.FC = () => {
|
|||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
|
|
||||||
setTimeout(() => setSubmitted(false), 3000);
|
setTimeout(() => setSubmitted(false), 3000);
|
||||||
|
toast.success('Feedback submitted successfully!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error submitting feedback:', 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 {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
@ -127,11 +128,11 @@ const DeveloperFeedback: React.FC = () => {
|
|||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
const getStatusIcon = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'open': return <Clock className="h-4 w-4" />;
|
case 'open': return <Minimize2 className="h-4 w-4" />;
|
||||||
case 'in-progress': return <AlertCircle className="h-4 w-4" />;
|
case 'in-progress': return <AlertTriangle className="h-4 w-4" />;
|
||||||
case 'resolved': return <CheckCircle className="h-4 w-4" />;
|
case 'resolved': return <Maximize2 className="h-4 w-4" />;
|
||||||
case 'closed': return <X 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"
|
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"
|
title="Developer Feedback"
|
||||||
>
|
>
|
||||||
<MessageSquare className="h-6 w-6" />
|
<MessageCircle className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Modal */}
|
||||||
@ -180,7 +181,7 @@ const DeveloperFeedback: React.FC = () => {
|
|||||||
{submitted && (
|
{submitted && (
|
||||||
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-4">
|
<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">
|
<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">
|
<span className="text-green-800 dark:text-green-200">
|
||||||
Feedback submitted successfully! Thank you for your input.
|
Feedback submitted successfully! Thank you for your input.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -151,7 +151,6 @@ const DraggableFeedback: React.FC<DraggableFeedbackProps> = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Here you would typically send the feedback to your backend
|
// Here you would typically send the feedback to your backend
|
||||||
console.log('Feedback submitted:', { feedback, email, rating });
|
|
||||||
|
|
||||||
// Simulate API call
|
// Simulate API call
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import { cn } from '../../utils/cn';
|
|||||||
const resellerNavigation = [
|
const resellerNavigation = [
|
||||||
{ name: 'Dashboard', href: '/reseller-dashboard', icon: Home },
|
{ name: 'Dashboard', href: '/reseller-dashboard', icon: Home },
|
||||||
{ name: 'Customers', href: '/reseller-dashboard/customers', icon: Users },
|
{ 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: 'Billing', href: '/reseller-dashboard/billing', icon: CreditCard },
|
||||||
{ name: 'Support', href: '/reseller-dashboard/support', icon: Headphones },
|
{ name: 'Support', href: '/reseller-dashboard/support', icon: Headphones },
|
||||||
{ name: 'Reports', href: '/reseller-dashboard/reports', icon: BarChart3 },
|
{ name: 'Reports', href: '/reseller-dashboard/reports', icon: BarChart3 },
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import { cn } from '../../utils/cn';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Dashboard', href: '/', icon: Home },
|
{ name: 'Dashboard', href: '/dashboard', icon: Home },
|
||||||
{ name: 'Product Management', href: '/product-management', icon: Package },
|
{ name: 'Product Management', href: '/product-management', icon: Package },
|
||||||
{ name: 'Reseller Requests', href: '/resellers', icon: Users },
|
{ name: 'Reseller Requests', href: '/resellers', icon: Users },
|
||||||
{ name: 'Approved Resellers', href: '/approved-resellers', icon: Handshake },
|
{ name: 'Approved Resellers', href: '/approved-resellers', icon: Handshake },
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { useAppSelector } from '../store/hooks';
|
||||||
import NotificationPanel from './NotificationPanel';
|
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 NotificationBell: React.FC = () => {
|
||||||
const [unreadCount, setUnreadCount] = useState(0);
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const { user } = useAppSelector((state) => state.auth);
|
const { user } = useAppSelector((state) => state.auth);
|
||||||
|
const socket = useSocket();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUnreadCount();
|
fetchUnreadCount();
|
||||||
@ -18,6 +23,66 @@ const NotificationBell: React.FC = () => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [user]);
|
}, [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 () => {
|
const fetchUnreadCount = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -58,26 +123,78 @@ const NotificationBell: React.FC = () => {
|
|||||||
fetchUnreadCount();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative">
|
<div className="relative group">
|
||||||
<button
|
<button
|
||||||
onClick={handleBellClick}
|
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}
|
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 && (
|
{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}
|
{unreadCount > 99 ? '99+' : unreadCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Loading indicator */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<NotificationPanel
|
<NotificationPanel
|
||||||
|
|||||||
@ -23,7 +23,6 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if user is authenticated but no user data
|
// Check if user is authenticated but no user data
|
||||||
if (isAuthenticated && !user && !isLoading) {
|
if (isAuthenticated && !user && !isLoading) {
|
||||||
console.log('ProtectedRoute: User authenticated but no user data, fetching user...');
|
|
||||||
dispatch(getCurrentUser());
|
dispatch(getCurrentUser());
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, user, isLoading, dispatch]);
|
}, [isAuthenticated, user, isLoading, dispatch]);
|
||||||
@ -63,12 +62,6 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasRequiredRole) {
|
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 />;
|
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,
|
onApprove,
|
||||||
onReject
|
onReject
|
||||||
}) => {
|
}) => {
|
||||||
console.log('VendorDetailsModal props:', { isOpen, vendor: vendor?.id, vendorName: vendor ? `${vendor.firstName} ${vendor.lastName}` : null });
|
|
||||||
|
|
||||||
if (!isOpen || !vendor) return null;
|
if (!isOpen || !vendor) return null;
|
||||||
|
|
||||||
const formatRoleName = (role: string) => {
|
const formatRoleName = (role: string) => {
|
||||||
|
|||||||
@ -10,8 +10,6 @@ const VendorRejectionModal: React.FC<RejectionModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [rejectionReason, setRejectionReason] = useState('');
|
const [rejectionReason, setRejectionReason] = useState('');
|
||||||
|
|
||||||
console.log('VendorRejectionModal props:', { isOpen, vendor: vendor?.id, vendorName: vendor ? `${vendor.firstName} ${vendor.lastName}` : null });
|
|
||||||
|
|
||||||
if (!isOpen || !vendor) return null;
|
if (!isOpen || !vendor) return null;
|
||||||
|
|
||||||
const handleReject = () => {
|
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' },
|
{ value: 'discontinued', label: 'Discontinued' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const currencyOptions = [
|
||||||
|
{ value: 'USD', label: 'USD ($)' },
|
||||||
|
{ value: 'RS', label: 'RS (₹)' },
|
||||||
|
{ value: 'EUR', label: 'EUR (€)' },
|
||||||
|
{ value: 'GBP', label: 'GBP (£)' },
|
||||||
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (product) {
|
if (product) {
|
||||||
setFormData({
|
setFormData({
|
||||||
@ -170,7 +177,8 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
|
|||||||
formData.price > 0 &&
|
formData.price > 0 &&
|
||||||
formData.sku.trim() !== '' &&
|
formData.sku.trim() !== '' &&
|
||||||
formData.commissionRate >= 0 &&
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!formData.purchaseUrl.trim()) {
|
||||||
|
toast.error('Purchase URL is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const productData = {
|
const productData = {
|
||||||
...formData,
|
...formData,
|
||||||
@ -346,10 +359,10 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
|
|||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
value={formData.price}
|
value={formData.price === 0 ? '' : formData.price}
|
||||||
onChange={(e) => handleInputChange('price', parseFloat(e.target.value) || 0)}
|
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"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||||
Currency
|
Currency
|
||||||
</label>
|
</label>
|
||||||
<input
|
<select
|
||||||
type="text"
|
|
||||||
value={formData.currency}
|
value={formData.currency}
|
||||||
onChange={(e) => handleInputChange('currency', e.target.value)}
|
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"
|
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"
|
required
|
||||||
maxLength={3}
|
>
|
||||||
/>
|
{currencyOptions.map(currency => (
|
||||||
|
<option key={currency.value} value={currency.value}>
|
||||||
|
{currency.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -429,13 +446,30 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
|
|||||||
type="number"
|
type="number"
|
||||||
min="-1"
|
min="-1"
|
||||||
value={formData.stockQuantity}
|
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"
|
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"
|
placeholder="-1"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
<div className="flex items-center justify-between mt-1">
|
||||||
Use -1 for unlimited stock
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -573,7 +607,7 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
|
|||||||
{/* Purchase URL */}
|
{/* Purchase URL */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||||
Purchase URL
|
Purchase URL *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
@ -581,9 +615,10 @@ const ProductForm: React.FC<ProductFormProps> = ({ product, onClose, onSuccess }
|
|||||||
onChange={(e) => handleInputChange('purchaseUrl', e.target.value)}
|
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"
|
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"
|
placeholder="https://example.com/product"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -93,9 +93,9 @@ export const mockResellerRecentActivities: RecentActivity[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
type: 'instance_created',
|
type: 'product_sold',
|
||||||
title: 'Cloud Instance Created',
|
title: 'Product Sold',
|
||||||
description: 'Created new cloud instance for DataFlow Inc with 8GB RAM configuration',
|
description: 'Sold cloud hosting package to DataFlow Inc',
|
||||||
timestamp: '2025-01-15T09:15:00Z',
|
timestamp: '2025-01-15T09:15:00Z',
|
||||||
amount: 1200,
|
amount: 1200,
|
||||||
currency: 'USD',
|
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 React, { useState, useEffect } from 'react';
|
||||||
import { mockResellers } from '../../data/mockData';
|
|
||||||
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
|
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
|
||||||
import Modal from '../../components/Modal';
|
import Modal from '../../components/Modal';
|
||||||
import AddPartnershipForm from '../../components/forms/AddPartnershipForm';
|
import AddPartnershipForm from '../../components/forms/AddPartnershipForm';
|
||||||
import DetailView from '../../components/DetailView';
|
import DetailView from '../../components/DetailView';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Filter,
|
Filter,
|
||||||
@ -23,28 +23,103 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Mail,
|
Mail,
|
||||||
MapPin,
|
MapPin,
|
||||||
Building2
|
Building2,
|
||||||
|
User
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '../../utils/cn';
|
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 ApprovedResellersPage: React.FC = () => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('active');
|
||||||
const [tierFilter, setTierFilter] = useState('all');
|
const [tierFilter, setTierFilter] = useState('all');
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const [isDetailModalOpen, setIsDetailModalOpen] = 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
|
// Set page title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Approved Resellers - Cloudtopiaa';
|
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
|
// 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 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 matchesStatus = statusFilter === 'all' || reseller.status === statusFilter;
|
||||||
const matchesTier = tierFilter === 'all' || reseller.tier === tierFilter;
|
const matchesTier = tierFilter === 'all' || reseller.tier === tierFilter;
|
||||||
|
|
||||||
@ -78,31 +153,28 @@ const ApprovedResellersPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddReseller = (data: any) => {
|
const handleAddReseller = (data: any) => {
|
||||||
console.log('New reseller data:', data);
|
|
||||||
// Here you would typically make an API call to add the reseller
|
// Here you would typically make an API call to add the reseller
|
||||||
// For now, we'll just close the modal
|
// For now, we'll just close the modal
|
||||||
setIsAddModalOpen(false);
|
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);
|
setSelectedReseller(reseller);
|
||||||
setIsDetailModalOpen(true);
|
setIsDetailModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditReseller = (reseller: any) => {
|
const handleEditReseller = (reseller: Reseller) => {
|
||||||
console.log('Edit reseller:', reseller);
|
toast(`Edit functionality for ${reseller.firstName} ${reseller.lastName} - This would open an edit form`);
|
||||||
alert(`Edit functionality for ${reseller.name} - This would open an edit form`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMailReseller = (reseller: any) => {
|
const handleMailReseller = (reseller: Reseller) => {
|
||||||
console.log('Mail reseller:', reseller);
|
|
||||||
const mailtoLink = `mailto:${reseller.email}?subject=Cloudtopiaa Partnership Update`;
|
const mailtoLink = `mailto:${reseller.email}?subject=Cloudtopiaa Partnership Update`;
|
||||||
window.open(mailtoLink, '_blank');
|
window.open(mailtoLink, '_blank');
|
||||||
|
toast.success(`Opening email client for ${reseller.firstName} ${reseller.lastName}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMoreOptions = (reseller: any) => {
|
const handleMoreOptions = (reseller: Reseller) => {
|
||||||
console.log('More options for reseller:', reseller);
|
|
||||||
const options = [
|
const options = [
|
||||||
'View Performance',
|
'View Performance',
|
||||||
'Download Report',
|
'Download Report',
|
||||||
@ -110,12 +182,31 @@ const ApprovedResellersPage: React.FC = () => {
|
|||||||
'Change Terms',
|
'Change Terms',
|
||||||
'Suspend Partnership'
|
'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) {
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -183,9 +274,9 @@ const ApprovedResellersPage: React.FC = () => {
|
|||||||
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
|
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
|
||||||
Total Customers
|
Total Customers
|
||||||
</p>
|
</p>
|
||||||
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
|
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
|
||||||
{approvedResellers.reduce((sum, r) => sum + r.customers, 0)}
|
{approvedResellers.reduce((sum, r) => sum + (r.customers || 0), 0)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-12 h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center">
|
<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" />
|
<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">
|
<p className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
|
||||||
Total Revenue
|
Total Revenue
|
||||||
</p>
|
</p>
|
||||||
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
|
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
|
||||||
{formatCurrency(approvedResellers.reduce((sum, r) => sum + r.totalRevenue, 0))}
|
{formatCurrency(approvedResellers.reduce((sum, r) => sum + (r.totalRevenue || 0), 0))}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-12 h-12 bg-success-100 dark:bg-success-900 rounded-full flex items-center justify-center">
|
<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" />
|
<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">
|
<tr key={reseller.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-800">
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img
|
<div className="relative mr-3">
|
||||||
className="h-10 w-10 rounded-full mr-3"
|
{reseller.avatar ? (
|
||||||
src={reseller.avatar}
|
<img
|
||||||
alt={reseller.name}
|
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>
|
||||||
<div className="text-sm font-medium text-secondary-900 dark:text-white">
|
<div className="text-sm font-medium text-secondary-900 dark:text-white">
|
||||||
{reseller.name}
|
{reseller.firstName || ''} {reseller.lastName || ''}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-secondary-500 dark:text-secondary-400">
|
<div className="text-sm text-secondary-500 dark:text-secondary-400">
|
||||||
{reseller.email}
|
{reseller.email || 'N/A'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-secondary-400 dark:text-secondary-500">
|
<div className="text-xs text-secondary-400 dark:text-secondary-500">
|
||||||
{reseller.phone}
|
{reseller.phone || 'N/A'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -329,31 +439,31 @@ const ApprovedResellersPage: React.FC = () => {
|
|||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
"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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
||||||
getTierColor(reseller.tier)
|
getTierColor(reseller.tier || 'silver')
|
||||||
)}>
|
)}>
|
||||||
{reseller.tier.charAt(0).toUpperCase() + reseller.tier.slice(1)}
|
{reseller.tier ? reseller.tier.charAt(0).toUpperCase() + reseller.tier.slice(1) : 'N/A'}
|
||||||
</span>
|
</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)}
|
|
||||||
</td>
|
</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">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<div className="flex items-center justify-end space-x-2">
|
<div className="flex items-center justify-end space-x-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { mockDeals } from '../../data/mockData';
|
import { mockDeals } from '../../data/mockData';
|
||||||
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
|
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
|
||||||
import Modal from '../../components/Modal';
|
import Modal from '../../components/Modal';
|
||||||
@ -11,18 +11,20 @@ import {
|
|||||||
MoreVertical,
|
MoreVertical,
|
||||||
Eye,
|
Eye,
|
||||||
Edit,
|
Edit,
|
||||||
TrendingUp,
|
Trash2,
|
||||||
DollarSign,
|
Mail,
|
||||||
Calendar,
|
TrendingUp,
|
||||||
Target,
|
Calendar,
|
||||||
|
DollarSign,
|
||||||
Users,
|
Users,
|
||||||
Briefcase,
|
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Clock,
|
Clock,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Download,
|
Download,
|
||||||
Mail
|
Briefcase,
|
||||||
|
Target
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { cn } from '../../utils/cn';
|
import { cn } from '../../utils/cn';
|
||||||
|
|
||||||
const DealsPage: React.FC = () => {
|
const DealsPage: React.FC = () => {
|
||||||
@ -93,7 +95,7 @@ const DealsPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleEditDeal = (deal: any) => {
|
const handleEditDeal = (deal: any) => {
|
||||||
console.log('Edit deal:', deal);
|
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) => {
|
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')}`);
|
const selectedOption = prompt(`Select an option for ${deal.title}:\n${options.join('\n')}`);
|
||||||
if (selectedOption) {
|
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 React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Plus, Search, Edit, Trash2, Eye, Pencil } from 'lucide-react';
|
import { Plus, Search, Edit, Trash2, Eye, Pencil } from 'lucide-react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { apiService, Product } from '../../services/api';
|
import { apiService, Product } from '../../services/api';
|
||||||
|
|
||||||
const Products: React.FC = () => {
|
const Products: React.FC = () => {
|
||||||
@ -137,7 +138,7 @@ const Products: React.FC = () => {
|
|||||||
category: formData.category || 'other'
|
category: formData.category || 'other'
|
||||||
});
|
});
|
||||||
|
|
||||||
alert('Product created successfully');
|
toast.success('Product created successfully');
|
||||||
setIsCreateModalOpen(false);
|
setIsCreateModalOpen(false);
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
@ -153,7 +154,7 @@ const Products: React.FC = () => {
|
|||||||
});
|
});
|
||||||
fetchProducts();
|
fetchProducts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error creating product');
|
toast.error('Error creating product');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -168,12 +169,12 @@ const Products: React.FC = () => {
|
|||||||
category: formData.category || 'other'
|
category: formData.category || 'other'
|
||||||
});
|
});
|
||||||
|
|
||||||
alert('Product updated successfully');
|
toast.success('Product updated successfully');
|
||||||
setIsEditModalOpen(false);
|
setIsEditModalOpen(false);
|
||||||
setEditingProductId(null);
|
setEditingProductId(null);
|
||||||
fetchProducts();
|
fetchProducts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error updating product');
|
toast.error('Error updating product');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -187,12 +188,12 @@ const Products: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await apiService.deleteProduct(deletingProductId);
|
await apiService.deleteProduct(deletingProductId);
|
||||||
alert('Product deleted successfully');
|
toast.success('Product deleted successfully');
|
||||||
fetchProducts();
|
fetchProducts();
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setDeletingProductId(null);
|
setDeletingProductId(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error deleting product');
|
toast.error('Error deleting product');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -16,13 +16,14 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
Link
|
Link
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
interface Reseller {
|
interface Reseller {
|
||||||
id: string;
|
id: string;
|
||||||
companyName: string;
|
companyName: string;
|
||||||
contactEmail: string;
|
contactEmail: string;
|
||||||
contactPhone: string;
|
contactPhone: string;
|
||||||
website: string;
|
website: string | null;
|
||||||
tier: string;
|
tier: string;
|
||||||
status: string;
|
status: string;
|
||||||
commissionRate: number;
|
commissionRate: number;
|
||||||
@ -74,9 +75,12 @@ const Resellers: React.FC = () => {
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setVendors(data.data);
|
setVendors(data.data);
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to fetch vendors');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching vendors:', 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) {
|
if (data.success) {
|
||||||
setResellers(data.data);
|
setResellers(data.data);
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to fetch resellers');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching resellers:', error);
|
console.error('Error fetching resellers:', error);
|
||||||
|
toast.error('Failed to fetch resellers. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -129,12 +136,17 @@ const Resellers: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
toast.success('Reseller deleted successfully!');
|
||||||
fetchResellers();
|
fetchResellers();
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setDeletingResellerId(null);
|
setDeletingResellerId(null);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
toast.error(`Failed to delete reseller: ${errorData.message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting reseller:', 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) {
|
if (response.ok) {
|
||||||
|
// Show success message
|
||||||
|
toast.success('Reseller updated successfully!');
|
||||||
fetchResellers();
|
fetchResellers();
|
||||||
setEditingReseller(null);
|
setEditingReseller(null);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
toast.error(`Failed to update reseller: ${errorData.message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating reseller:', 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;
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
@ -228,9 +274,16 @@ const Resellers: React.FC = () => {
|
|||||||
placeholder="Search resellers..."
|
placeholder="Search resellers..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
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"
|
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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Status Filter */}
|
{/* Status Filter */}
|
||||||
@ -238,7 +291,7 @@ const Resellers: React.FC = () => {
|
|||||||
<Filter className="w-4 h-4 text-gray-400" />
|
<Filter className="w-4 h-4 text-gray-400" />
|
||||||
<select
|
<select
|
||||||
value={statusFilter}
|
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"
|
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>
|
<option value="all">All Status</option>
|
||||||
@ -253,7 +306,7 @@ const Resellers: React.FC = () => {
|
|||||||
<Building className="w-4 h-4 text-gray-400" />
|
<Building className="w-4 h-4 text-gray-400" />
|
||||||
<select
|
<select
|
||||||
value={vendorFilter}
|
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"
|
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>
|
<option value="all">All Vendors</option>
|
||||||
@ -485,6 +538,156 @@ const Resellers: React.FC = () => {
|
|||||||
</div>
|
</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 */}
|
{/* Delete Confirmation Modal */}
|
||||||
{isDeleteModalOpen && (
|
{isDeleteModalOpen && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4" style={{ backdropFilter: 'blur(4px)' }}>
|
<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
|
// Get auth state from Redux
|
||||||
const { isAuthenticated, token, user } = useAppSelector((state) => state.auth);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchVendorRequests();
|
fetchVendorRequests();
|
||||||
@ -53,11 +51,7 @@ const VendorRequests: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Debug: Check if token exists
|
|
||||||
const token = localStorage.getItem('accessToken');
|
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) {
|
if (!token) {
|
||||||
console.error('No token found in localStorage');
|
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();
|
const data = await response.json();
|
||||||
console.log('Response data:', data);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(data.message || 'Failed to fetch vendor requests');
|
throw new Error(data.message || 'Failed to fetch vendor requests');
|
||||||
@ -321,10 +310,8 @@ const VendorRequests: React.FC = () => {
|
|||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('View button clicked for vendor:', vendor);
|
|
||||||
setSelectedVendor(vendor);
|
setSelectedVendor(vendor);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
console.log('Modal state set to true');
|
|
||||||
}}
|
}}
|
||||||
className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</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>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white">
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Pending Amount</p>
|
||||||
Billing & Payments
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
</h1>
|
<DualCurrencyDisplay amount={pendingAmount} currency="USD" />
|
||||||
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
|
</p>
|
||||||
Manage your billing, invoices, and payment methods
|
</div>
|
||||||
</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="card p-12">
|
{/* Tabs */}
|
||||||
<div className="text-center">
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700">
|
||||||
<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="border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="w-8 h-8 bg-secondary-400 rounded"></div>
|
<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>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-2">
|
|
||||||
Coming Soon
|
{/* 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>
|
</h3>
|
||||||
<p className="text-secondary-600 dark:text-secondary-400">
|
<div className="space-y-3">
|
||||||
Billing and payment features will be available soon.
|
{sales?.slice(0, 5).map((sale: any) => (
|
||||||
</p>
|
<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>
|
</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 { useAppSelector, useAppDispatch } from '../../store/hooks';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { setStats, setRecentActivities, setQuickActions } from '../../store/slices/dashboardSlice';
|
import {
|
||||||
import { mockDashboardStats, mockResellerQuickActions, mockResellerRecentActivities } from '../../data/mockData';
|
fetchDashboardStats,
|
||||||
|
fetchCustomers,
|
||||||
|
fetchProducts,
|
||||||
|
fetchSales,
|
||||||
|
fetchRecentActivities,
|
||||||
|
addCustomer
|
||||||
|
} from '../../store/reseller/resellerDashboardSlice';
|
||||||
import { formatNumber, formatRelativeTime, formatPercentage } from '../../utils/format';
|
import { formatNumber, formatRelativeTime, formatPercentage } from '../../utils/format';
|
||||||
import RevenueChart from '../../components/charts/RevenueChart';
|
import RevenueChart from '../../components/charts/RevenueChart';
|
||||||
import ResellerPerformanceChart from '../../components/charts/ResellerPerformanceChart';
|
|
||||||
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
|
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
|
||||||
|
import MarkProductSoldForm from '../../components/forms/MarkProductSoldForm';
|
||||||
import {
|
import {
|
||||||
TrendingUp,
|
Package,
|
||||||
Users,
|
Users,
|
||||||
Cloud,
|
TrendingUp,
|
||||||
DollarSign,
|
ArrowUpRight,
|
||||||
UserPlus,
|
Star,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Briefcase,
|
Activity,
|
||||||
GraduationCap,
|
Receipt,
|
||||||
BarChart3,
|
FileText,
|
||||||
CreditCard,
|
X,
|
||||||
Headphones,
|
Plus
|
||||||
ShoppingBag,
|
|
||||||
Award,
|
|
||||||
HelpCircle,
|
|
||||||
Settings,
|
|
||||||
Wallet,
|
|
||||||
BookOpen,
|
|
||||||
Zap,
|
|
||||||
Target,
|
|
||||||
Star,
|
|
||||||
ArrowUpRight,
|
|
||||||
Activity
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '../../utils/cn';
|
import { cn } from '../../utils/cn';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const ResellerDashboard: React.FC = () => {
|
const ResellerDashboard: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard);
|
const [isCustomerModalOpen, setIsCustomerModalOpen] = useState(false);
|
||||||
const { user } = useAppSelector((state) => state.auth);
|
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(() => {
|
useEffect(() => {
|
||||||
// Initialize dashboard data
|
dispatch(fetchDashboardStats());
|
||||||
dispatch(setStats(mockDashboardStats));
|
dispatch(fetchCustomers());
|
||||||
dispatch(setRecentActivities(mockResellerRecentActivities));
|
dispatch(fetchProducts());
|
||||||
dispatch(setQuickActions(mockResellerQuickActions));
|
dispatch(fetchSales());
|
||||||
|
dispatch(fetchRecentActivities());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleQuickAction = (action: any) => {
|
const handleQuickAction = (action: any) => {
|
||||||
switch (action.id) {
|
switch (action.id) {
|
||||||
case 'add-customer':
|
case 'add-customer':
|
||||||
navigate('/reseller-dashboard/customers');
|
setIsCustomerModalOpen(true);
|
||||||
break;
|
break;
|
||||||
case 'create-instance':
|
case 'manage-products':
|
||||||
navigate('/reseller-dashboard/instances');
|
navigate('/reseller-dashboard/products');
|
||||||
|
break;
|
||||||
|
case 'view-sales':
|
||||||
|
navigate('/reseller-dashboard/sales');
|
||||||
break;
|
break;
|
||||||
case 'billing':
|
case 'billing':
|
||||||
navigate('/reseller-dashboard/billing');
|
navigate('/reseller-dashboard/billing');
|
||||||
break;
|
break;
|
||||||
case 'support':
|
|
||||||
navigate('/reseller-dashboard/support');
|
|
||||||
break;
|
|
||||||
case 'training':
|
|
||||||
navigate('/reseller-dashboard/training');
|
|
||||||
break;
|
|
||||||
case 'reports':
|
case 'reports':
|
||||||
navigate('/reseller-dashboard/reports');
|
navigate('/reseller-dashboard/reports');
|
||||||
break;
|
break;
|
||||||
case 'wallet':
|
case 'mark-product-sold':
|
||||||
navigate('/reseller-dashboard/wallet');
|
setIsMarkProductSoldModalOpen(true);
|
||||||
break;
|
|
||||||
case 'marketplace':
|
|
||||||
navigate('/reseller-dashboard/marketplace');
|
|
||||||
break;
|
|
||||||
case 'certifications':
|
|
||||||
navigate('/reseller-dashboard/certifications');
|
|
||||||
break;
|
|
||||||
case 'knowledge-base':
|
|
||||||
navigate('/reseller-dashboard/knowledge-base');
|
|
||||||
break;
|
|
||||||
case 'settings':
|
|
||||||
navigate('/reseller-dashboard/settings');
|
|
||||||
break;
|
break;
|
||||||
default:
|
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 (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Welcome Section */}
|
{/* Welcome Section */}
|
||||||
@ -94,18 +288,14 @@ const ResellerDashboard: React.FC = () => {
|
|||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold mb-2">Welcome back, {user ? `${user.firstName} ${user.lastName}` : 'User'}! </h1>
|
<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>
|
<p className="text-emerald-100 text-lg">Here's what's happening with your business today</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden lg:flex items-center space-x-4">
|
<div className="text-right">
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-2xl font-bold">{formatNumber(stats.totalResellers)}</div>
|
|
||||||
<div className="text-emerald-100 text-sm">Active Customers</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<DualCurrencyDisplay
|
<DualCurrencyDisplay
|
||||||
amount={stats.commissionEarned}
|
amount={monthlyRevenue}
|
||||||
currency={stats.currency}
|
currency={realStats.currency || 'USD'}
|
||||||
className="text-2xl font-bold text-white"
|
className="text-2xl font-bold text-white"
|
||||||
/>
|
/>
|
||||||
<div className="text-emerald-100 text-sm">Monthly Revenue</div>
|
<div className="text-emerald-100 text-sm">Monthly Revenue</div>
|
||||||
@ -113,200 +303,280 @@ const ResellerDashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -translate-y-32 translate-x-32"></div>
|
|
||||||
<div className="absolute bottom-0 left-0 w-48 h-48 bg-white/5 rounded-full translate-y-24 -translate-x-24"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Key Metrics */}
|
{/* Key Metrics */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<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="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 mb-4">
|
<div className="flex items-center justify-between">
|
||||||
<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">
|
<div>
|
||||||
<DollarSign className="w-6 h-6 text-white" />
|
<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>
|
</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>
|
</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="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 mb-4">
|
<div className="flex items-center justify-between">
|
||||||
<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">
|
<div>
|
||||||
<Users className="w-6 h-6 text-white" />
|
<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>
|
</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>
|
</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="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 mb-4">
|
<div className="flex items-center justify-between">
|
||||||
<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">
|
<div>
|
||||||
<Cloud className="w-6 h-6 text-white" />
|
<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>
|
</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>
|
</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="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 mb-4">
|
<div className="flex items-center justify-between">
|
||||||
<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">
|
<div>
|
||||||
<Target className="w-6 h-6 text-white" />
|
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions & Recent Activity */}
|
{/* Quick Actions */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
|
||||||
{/* Quick Actions */}
|
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Quick Actions</h3>
|
||||||
<div className="lg:col-span-1">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
|
{quickActions.map((action) => {
|
||||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
|
const IconComponent = action.icon;
|
||||||
<Zap className="w-5 h-5 mr-2 text-emerald-500" />
|
return (
|
||||||
Quick Actions
|
<button
|
||||||
</h3>
|
key={action.id}
|
||||||
<div className="space-y-3">
|
onClick={() => handleQuickAction(action)}
|
||||||
{quickActions.slice(0, 6).map((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"
|
||||||
<button
|
>
|
||||||
key={action.id}
|
<div className="flex items-center space-x-3">
|
||||||
onClick={() => handleQuickAction(action)}
|
<div className={cn("w-10 h-10 rounded-lg flex items-center justify-center", action.color)}>
|
||||||
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"
|
<IconComponent className="w-5 h-5 text-white" />
|
||||||
>
|
|
||||||
<div className="w-10 h-10 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center mr-4 group-hover:scale-110 transition-transform duration-300">
|
|
||||||
{action.icon === 'UserPlus' && <UserPlus className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'Cloud' && <Cloud className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'CreditCard' && <CreditCard className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'Headphones' && <Headphones className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'GraduationCap' && <GraduationCap className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'BarChart3' && <BarChart3 className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'Wallet' && <Wallet className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'ShoppingBag' && <ShoppingBag className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'Award' && <Award className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'HelpCircle' && <HelpCircle className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'Settings' && <Settings className="w-5 h-5 text-white" />}
|
|
||||||
{action.icon === 'BookOpen' && <BookOpen className="w-5 h-5 text-white" />}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left flex-1">
|
<div className="text-left">
|
||||||
<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">
|
<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}
|
{action.title}
|
||||||
</p>
|
</h4>
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-400">{action.description}</p>
|
<p className="text-sm text-slate-600 dark:text-slate-400">{action.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<ArrowUpRight className="w-4 h-4 text-slate-400 group-hover:text-emerald-500 transition-colors duration-300" />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recent Activity */}
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
|
|
||||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
|
|
||||||
<Activity className="w-5 h-5 mr-2 text-emerald-500" />
|
|
||||||
Recent Activity
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{recentActivities.map((activity) => (
|
|
||||||
<div key={activity.id} className="flex items-start p-4 rounded-xl bg-slate-50 dark:bg-slate-700/50 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
|
|
||||||
<div className="w-10 h-10 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center mr-4 flex-shrink-0">
|
|
||||||
{activity.type === 'customer_added' && <UserPlus className="w-5 h-5 text-white" />}
|
|
||||||
{activity.type === 'instance_created' && <Cloud className="w-5 h-5 text-white" />}
|
|
||||||
{activity.type === 'payment_received' && <CreditCard className="w-5 h-5 text-white" />}
|
|
||||||
{activity.type === 'support_ticket' && <Headphones className="w-5 h-5 text-white" />}
|
|
||||||
{activity.type === 'training_completed' && <GraduationCap className="w-5 h-5 text-white" />}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="font-semibold text-slate-900 dark:text-white mb-1">
|
|
||||||
{activity.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-400 mb-2">
|
|
||||||
{activity.description}
|
|
||||||
</p>
|
|
||||||
{activity.amount && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<DualCurrencyDisplay
|
|
||||||
amount={activity.amount}
|
|
||||||
currency={activity.currency}
|
|
||||||
className="text-sm font-semibold text-emerald-600"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
|
||||||
{formatRelativeTime(activity.timestamp)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</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>
|
||||||
<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">
|
</div>
|
||||||
View all activities
|
</div>
|
||||||
<ArrowUpRight className="w-4 h-4 ml-1" />
|
|
||||||
|
{/* 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>
|
</button>
|
||||||
</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>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Charts Section */}
|
{/* Mark Product Sold Modal */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
{isMarkProductSoldModalOpen && (
|
||||||
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
|
<MarkProductSoldForm
|
||||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
|
isOpen={isMarkProductSoldModalOpen}
|
||||||
<BarChart3 className="w-5 h-5 mr-2 text-emerald-500" />
|
onClose={() => setIsMarkProductSoldModalOpen(false)}
|
||||||
Revenue Overview
|
onSuccess={() => {
|
||||||
</h3>
|
// Refresh data after marking product as sold
|
||||||
<div className="h-64">
|
dispatch(fetchDashboardStats());
|
||||||
<RevenueChart />
|
dispatch(fetchSales());
|
||||||
</div>
|
dispatch(fetchRecentActivities());
|
||||||
</div>
|
}}
|
||||||
|
products={realProducts}
|
||||||
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-lg border border-slate-200 dark:border-slate-700">
|
customers={realCustomers}
|
||||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6 flex items-center">
|
/>
|
||||||
<Target className="w-5 h-5 mr-2 text-emerald-500" />
|
)}
|
||||||
Customer Performance
|
|
||||||
</h3>
|
|
||||||
<div className="h-64">
|
|
||||||
<ResellerPerformanceChart />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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 {
|
export interface RecentActivity {
|
||||||
id: string;
|
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;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user