diff --git a/src/App.tsx b/src/App.tsx index 5211152..d6864eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -323,7 +323,7 @@ function App() { - + } /> @@ -505,7 +505,7 @@ function App() { - + {/* */} diff --git a/src/components/AuthInitializer.tsx b/src/components/AuthInitializer.tsx index 3bf3985..c3d26cc 100644 --- a/src/components/AuthInitializer.tsx +++ b/src/components/AuthInitializer.tsx @@ -27,17 +27,13 @@ const AuthInitializer: React.FC<{ children: React.ReactNode }> = ({ children }) // Try to get current user try { await dispatch(getCurrentUser()).unwrap(); - console.log('User loaded successfully'); } catch (error) { - console.log('Failed to get current user, trying to refresh token...', error); // If getting user fails, try to refresh token try { await dispatch(refreshUserToken()).unwrap(); // Try to get user again after token refresh await dispatch(getCurrentUser()).unwrap(); - console.log('User loaded successfully after token refresh'); } catch (refreshError) { - console.log('Token refresh failed, clearing auth data...', refreshError); // Clear invalid tokens and logout localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); @@ -45,8 +41,6 @@ const AuthInitializer: React.FC<{ children: React.ReactNode }> = ({ children }) dispatch(logout()); } } - } else { - console.log('No tokens found in localStorage'); } } catch (error) { console.error('Auth initialization error:', error); diff --git a/src/components/CommissionTrends.tsx b/src/components/CommissionTrends.tsx new file mode 100644 index 0000000..fe2ff08 --- /dev/null +++ b/src/components/CommissionTrends.tsx @@ -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 = ({ vendorId }) => { + const [trends, setTrends] = useState([]); + const [summary, setSummary] = useState({ + totalRevenue: 0, + totalCommission: 0, + averageCommissionRate: 0 + }); + const [period, setPeriod] = useState('6months'); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
+

Commission Trends

+
+
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+
+ ); + } + + if (error) { + return ( +
+
Error loading commission trends
+
{error}
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Commission Trends

+

Track revenue and commission trends over time

+
+
+ + +
+
+ + {/* Summary Cards */} +
+
+
+
+

Total Revenue

+

+ {formatCurrency(summary.totalRevenue)} +

+
+
+ +
+
+
+ +
+
+
+

Total Commission

+

+ {formatCurrency(summary.totalCommission)} +

+
+
+ +
+
+
+ +
+
+
+

Avg Commission Rate

+

+ {formatPercentage(summary.averageCommissionRate)} +

+
+
+ +
+
+
+
+ + {/* Trends Chart */} +
+
+

Monthly Trends

+

+ Revenue and commission trends over the selected period +

+
+ + {trends.length > 0 ? ( +
+ {/* Chart Visualization */} +
+
+ +

+ Chart visualization will be implemented here +

+

+ Using a charting library like Chart.js or Recharts +

+
+
+ + {/* Trends Table */} +
+ + + + + + + + + + + {trends.map((trend, index) => ( + + + + + + + ))} + +
+ Month + + Revenue + + Commission + + Commission Rate +
+ {getMonthLabel(trend.month)} + + {formatCurrency(trend.revenue)} + + {formatCurrency(trend.commission)} + + + {formatPercentage(trend.commissionRate)} + +
+
+
+ ) : ( +
+ +

No Trend Data

+

+ No commission trend data available for the selected period. +

+
+ )} +
+
+ ); +}; + +export default CommissionTrends; \ No newline at end of file diff --git a/src/components/DeveloperFeedback.tsx b/src/components/DeveloperFeedback.tsx index af3f6f6..944a9d6 100644 --- a/src/components/DeveloperFeedback.tsx +++ b/src/components/DeveloperFeedback.tsx @@ -1,16 +1,16 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { - MessageSquare, - Send, + MessageCircle, + Bug, + Lightbulb, + AlertTriangle, + Star, X, - AlertCircle, - CheckCircle, - Clock, - Bug, - Lightbulb, - HelpCircle, - Star + Send, + Minimize2, + Maximize2 } from 'lucide-react'; +import toast from 'react-hot-toast'; interface FeedbackTicket { id: string; @@ -41,7 +41,7 @@ const DeveloperFeedback: React.FC = () => { const ticketTypes = [ { id: 'bug', label: 'Bug Report', icon: Bug, color: 'text-red-600' }, { id: 'feature', label: 'Feature Request', icon: Lightbulb, color: 'text-blue-600' }, - { id: 'question', label: 'Question', icon: HelpCircle, color: 'text-green-600' }, + { id: 'question', label: 'Question', icon: AlertTriangle, color: 'text-green-600' }, { id: 'general', label: 'General Feedback', icon: Star, color: 'text-yellow-600' } ]; @@ -54,7 +54,7 @@ const DeveloperFeedback: React.FC = () => { const handleSubmit = async () => { if (!currentTicket.title || !currentTicket.description) { - alert('Please fill in all required fields'); + toast.error('Please fill in all required fields'); return; } @@ -107,9 +107,10 @@ const DeveloperFeedback: React.FC = () => { setSubmitted(true); setTimeout(() => setSubmitted(false), 3000); + toast.success('Feedback submitted successfully!'); } catch (error) { console.error('Error submitting feedback:', error); - alert('Failed to submit feedback. Please try again.'); + toast.error('Failed to submit feedback. Please try again.'); } finally { setSubmitting(false); } @@ -127,11 +128,11 @@ const DeveloperFeedback: React.FC = () => { const getStatusIcon = (status: string) => { switch (status) { - case 'open': return ; - case 'in-progress': return ; - case 'resolved': return ; + case 'open': return ; + case 'in-progress': return ; + case 'resolved': return ; case 'closed': return ; - default: return ; + default: return ; } }; @@ -143,7 +144,7 @@ const DeveloperFeedback: React.FC = () => { className="fixed bottom-20 right-4 bg-blue-600 hover:bg-blue-700 text-white rounded-full p-3 shadow-lg z-[9998] transition-all duration-200" title="Developer Feedback" > - + {/* Modal */} @@ -180,7 +181,7 @@ const DeveloperFeedback: React.FC = () => { {submitted && (
- + Feedback submitted successfully! Thank you for your input. diff --git a/src/components/DraggableFeedback.tsx b/src/components/DraggableFeedback.tsx index 5c6d105..71921d5 100644 --- a/src/components/DraggableFeedback.tsx +++ b/src/components/DraggableFeedback.tsx @@ -151,7 +151,6 @@ const DraggableFeedback: React.FC = ({ try { // Here you would typically send the feedback to your backend - console.log('Feedback submitted:', { feedback, email, rating }); // Simulate API call await new Promise(resolve => setTimeout(resolve, 1000)); diff --git a/src/components/Layout/ResellerSidebar.tsx b/src/components/Layout/ResellerSidebar.tsx index b51c6e6..6ea96ad 100644 --- a/src/components/Layout/ResellerSidebar.tsx +++ b/src/components/Layout/ResellerSidebar.tsx @@ -33,7 +33,7 @@ import { cn } from '../../utils/cn'; const resellerNavigation = [ { name: 'Dashboard', href: '/reseller-dashboard', icon: Home }, { name: 'Customers', href: '/reseller-dashboard/customers', icon: Users }, - { name: 'Cloud Instances', href: '/reseller-dashboard/instances', icon: Cloud }, + { name: 'Products', href: '/reseller-dashboard/products', icon: Package }, { name: 'Billing', href: '/reseller-dashboard/billing', icon: CreditCard }, { name: 'Support', href: '/reseller-dashboard/support', icon: Headphones }, { name: 'Reports', href: '/reseller-dashboard/reports', icon: BarChart3 }, diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx index b902911..f171ec9 100644 --- a/src/components/Layout/Sidebar.tsx +++ b/src/components/Layout/Sidebar.tsx @@ -33,7 +33,7 @@ import { cn } from '../../utils/cn'; import toast from 'react-hot-toast'; const navigation = [ - { name: 'Dashboard', href: '/', icon: Home }, + { name: 'Dashboard', href: '/dashboard', icon: Home }, { name: 'Product Management', href: '/product-management', icon: Package }, { name: 'Reseller Requests', href: '/resellers', icon: Users }, { name: 'Approved Resellers', href: '/approved-resellers', icon: Handshake }, diff --git a/src/components/NotificationBell.tsx b/src/components/NotificationBell.tsx index d36b7f3..8bc673c 100644 --- a/src/components/NotificationBell.tsx +++ b/src/components/NotificationBell.tsx @@ -1,13 +1,18 @@ import React, { useState, useEffect } from 'react'; -import { Bell } from 'lucide-react'; +import { Bell, BellOff, Check, X, Clock, AlertCircle, CheckCircle, Info, Zap } from 'lucide-react'; import { useAppSelector } from '../store/hooks'; import NotificationPanel from './NotificationPanel'; +import { useSocket } from '../hooks/useSocket'; +import toast from 'react-hot-toast'; +import { cn } from '../utils/cn'; const NotificationBell: React.FC = () => { const [unreadCount, setUnreadCount] = useState(0); const [isPanelOpen, setIsPanelOpen] = useState(false); const [loading, setLoading] = useState(false); + const [isConnected, setIsConnected] = useState(false); const { user } = useAppSelector((state) => state.auth); + const socket = useSocket(); useEffect(() => { fetchUnreadCount(); @@ -18,6 +23,66 @@ const NotificationBell: React.FC = () => { return () => clearInterval(interval); }, [user]); + useEffect(() => { + if (socket) { + // Socket connection events + socket.on('connect', () => { + setIsConnected(true); + console.log('Socket connected for notifications'); + }); + + socket.on('disconnect', () => { + setIsConnected(false); + console.log('Socket disconnected from notifications'); + }); + + // Real-time notification events + socket.on('NEW_CUSTOMER', (data: { customerName: string }) => { + toast.success(`New customer added: ${data.customerName}`); + fetchUnreadCount(); + }); + + socket.on('NEW_SALE', (data: { productName: string; customerName: string }) => { + toast.success(`New sale: ${data.productName} to ${data.customerName}`, { + duration: 5000, + }); + fetchUnreadCount(); + }); + + socket.on('PAYMENT_RECEIVED', (data: { amount: number; customerName: string }) => { + toast.success(`Payment received: $${data.amount} from ${data.customerName}`, { + duration: 5000, + }); + fetchUnreadCount(); + }); + + socket.on('SUPPORT_TICKET', (data: { title: string }) => { + toast.error(`New support ticket: ${data.title}`, { + duration: 6000, + }); + fetchUnreadCount(); + }); + + socket.on('VENDOR_NOTIFICATION', (data: { message: string; type: 'success' | 'error' | 'info' }) => { + toast(data.message, { + icon: data.type === 'success' ? '✅' : data.type === 'error' ? '❌' : 'ℹ️', + duration: 5000, + }); + fetchUnreadCount(); + }); + + return () => { + socket.off('connect'); + socket.off('disconnect'); + socket.off('NEW_CUSTOMER'); + socket.off('NEW_SALE'); + socket.off('PAYMENT_RECEIVED'); + socket.off('SUPPORT_TICKET'); + socket.off('VENDOR_NOTIFICATION'); + }; + } + }, [socket]); + const fetchUnreadCount = async () => { try { setLoading(true); @@ -58,26 +123,78 @@ const NotificationBell: React.FC = () => { fetchUnreadCount(); }; + const getConnectionStatusIcon = () => { + if (!socket) return ; + if (isConnected) return ; + return ; + }; + + const getConnectionStatusColor = () => { + if (!socket) return 'text-gray-400'; + if (isConnected) return 'text-green-500'; + return 'text-yellow-500'; + }; + return ( <> -
+
+ + {/* Tooltip */} +
+
+ {getConnectionStatusIcon()} + + {isConnected ? 'Real-time notifications active' : 'Notifications (offline)'} + +
+ {unreadCount > 0 && ( +
+ {unreadCount} unread notification{unreadCount !== 1 ? 's' : ''} +
+ )} +
+
= ({ useEffect(() => { // Check if user is authenticated but no user data if (isAuthenticated && !user && !isLoading) { - console.log('ProtectedRoute: User authenticated but no user data, fetching user...'); dispatch(getCurrentUser()); } }, [isAuthenticated, user, isLoading, dispatch]); @@ -63,12 +62,6 @@ const ProtectedRoute: React.FC = ({ } if (!hasRequiredRole) { - console.log('User does not have required role:', { - userRole: user.role, - requiredRole, - userRoles: user.roles, - primaryRole: user.roles?.[0]?.name - }); return ; } } diff --git a/src/components/ResellerPerformance.tsx b/src/components/ResellerPerformance.tsx new file mode 100644 index 0000000..e4a8230 --- /dev/null +++ b/src/components/ResellerPerformance.tsx @@ -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 = ({ vendorId }) => { + const [performanceData, setPerformanceData] = useState([]); + 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(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 ( +
+
+

Reseller Performance

+
+
+
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+
+
+ ); + } + + if (error) { + return ( +
+
Error loading reseller performance
+
{error}
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Reseller Performance

+

Track your resellers' sales and commission performance

+
+
+ + +
+
+ + {/* Summary Cards */} +
+
+
+
+

Total Resellers

+

{summary.totalResellers}

+
+
+ +
+
+
+ +
+
+
+

Total Sales

+

+ {formatCurrency(summary.totalSales)} +

+
+
+ +
+
+
+ +
+
+
+

Total Commission

+

+ {formatCurrency(summary.totalCommission)} +

+
+
+ +
+
+
+ +
+
+
+

Products Sold

+

+ {formatNumber(summary.totalProducts)} +

+
+
+ +
+
+
+
+ + {/* Performance Table */} +
+
+

Reseller Rankings

+

+ Sorted by total sales performance for {period === 'week' ? 'this week' : + period === 'month' ? 'this month' : + period === 'quarter' ? 'this quarter' : 'this year'} +

+
+ +
+ + + + + + + + + + + + + + + {performanceData.map((reseller, index) => ( + + + + + + + + + + + ))} + +
+ Reseller + + Sales + + Commission + + Products + + Orders + + Avg Order + + Commission Rate + + Last Order +
+
+
+ {index + 1} +
+
+
+ {reseller.name} +
+
+ {reseller.email} +
+ {reseller.company && ( +
+ {reseller.company} +
+ )} +
+
+
+ + {formatCurrency(reseller.totalSales)} + + + {formatCurrency(reseller.totalCommission)} + + {formatNumber(reseller.totalProducts)} + + {reseller.orderCount} + + {formatCurrency(reseller.averageOrderValue)} + + + {reseller.commissionRate.toFixed(1)}% + + + {reseller.lastOrder ? formatDate(reseller.lastOrder) : 'Never'} +
+
+ + {performanceData.length === 0 && ( +
+ +

No Performance Data

+

+ No reseller performance data available for the selected period. +

+
+ )} +
+
+ ); +}; + +export default ResellerPerformance; \ No newline at end of file diff --git a/src/components/VendorDetailsModal.tsx b/src/components/VendorDetailsModal.tsx index 8be70f2..3905d62 100644 --- a/src/components/VendorDetailsModal.tsx +++ b/src/components/VendorDetailsModal.tsx @@ -9,8 +9,6 @@ const VendorDetailsModal: React.FC = ({ onApprove, onReject }) => { - console.log('VendorDetailsModal props:', { isOpen, vendor: vendor?.id, vendorName: vendor ? `${vendor.firstName} ${vendor.lastName}` : null }); - if (!isOpen || !vendor) return null; const formatRoleName = (role: string) => { diff --git a/src/components/VendorRejectionModal.tsx b/src/components/VendorRejectionModal.tsx index 4b66629..bc5604b 100644 --- a/src/components/VendorRejectionModal.tsx +++ b/src/components/VendorRejectionModal.tsx @@ -10,8 +10,6 @@ const VendorRejectionModal: React.FC = ({ }) => { const [rejectionReason, setRejectionReason] = useState(''); - console.log('VendorRejectionModal props:', { isOpen, vendor: vendor?.id, vendorName: vendor ? `${vendor.firstName} ${vendor.lastName}` : null }); - if (!isOpen || !vendor) return null; const handleReject = () => { diff --git a/src/components/forms/MarkProductSoldForm.tsx b/src/components/forms/MarkProductSoldForm.tsx new file mode 100644 index 0000000..7a54318 --- /dev/null +++ b/src/components/forms/MarkProductSoldForm.tsx @@ -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 = ({ + isOpen, + onClose, + onSuccess, + products, + customers +}) => { + const [formData, setFormData] = useState({ + productId: '', + customerId: '', + quantity: 1, + customerDetails: { + name: '', + email: '', + company: '', + phone: '' + } + }); + + const [selectedProduct, setSelectedProduct] = useState(null); + const [selectedCustomer, setSelectedCustomer] = useState(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 ( +
+
+ {/* Header */} +
+
+
+ +
+
+

Mark Product as Sold

+

Record a successful sale and track commission

+
+
+ +
+ + {/* Form */} +
+
+ {/* Product Selection */} +
+ + +
+ + {/* Customer Selection */} +
+ + +
+ + {/* Quantity */} +
+ + 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 + /> +
+ + {/* Sale Summary */} + {selectedProduct && ( +
+

Sale Summary

+
+
+ Product: +

{selectedProduct.name}

+
+
+ Unit Price: +

${selectedProduct.price}

+
+
+ Total Amount: +

${calculateTotalAmount()}

+
+
+ Commission ({selectedProduct.commissionRate}%): +

${calculateCommission()}

+
+
+
+ )} + + {/* Customer Details */} + {selectedCustomer && ( +
+

+ + Customer Information +

+
+
+ Name: +

{selectedCustomer.name}

+
+
+ Email: +

{selectedCustomer.email}

+
+
+ Company: +

{selectedCustomer.company || 'N/A'}

+
+
+ Phone: +

{selectedCustomer.phone || 'N/A'}

+
+
+
+ )} + + {/* Form Actions */} +
+ + +
+
+
+
+
+ ); +}; + +export default MarkProductSoldForm; \ No newline at end of file diff --git a/src/components/forms/ProductForm.tsx b/src/components/forms/ProductForm.tsx index 083e832..72e920c 100644 --- a/src/components/forms/ProductForm.tsx +++ b/src/components/forms/ProductForm.tsx @@ -78,6 +78,13 @@ const ProductForm: React.FC = ({ product, onClose, onSuccess } { value: 'discontinued', label: 'Discontinued' }, ]; + const currencyOptions = [ + { value: 'USD', label: 'USD ($)' }, + { value: 'RS', label: 'RS (₹)' }, + { value: 'EUR', label: 'EUR (€)' }, + { value: 'GBP', label: 'GBP (£)' }, + ]; + useEffect(() => { if (product) { setFormData({ @@ -170,7 +177,8 @@ const ProductForm: React.FC = ({ product, onClose, onSuccess } formData.price > 0 && formData.sku.trim() !== '' && formData.commissionRate >= 0 && - formData.commissionRate <= 100 + formData.commissionRate <= 100 && + formData.purchaseUrl.trim() !== '' ); }; @@ -197,6 +205,11 @@ const ProductForm: React.FC = ({ product, onClose, onSuccess } return; } + if (!formData.purchaseUrl.trim()) { + toast.error('Purchase URL is required'); + return; + } + try { const productData = { ...formData, @@ -346,10 +359,10 @@ const ProductForm: React.FC = ({ product, onClose, onSuccess } type="number" step="0.01" min="0" - value={formData.price} + value={formData.price === 0 ? '' : formData.price} onChange={(e) => handleInputChange('price', parseFloat(e.target.value) || 0)} className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="0.00" + placeholder="Enter price" required />
@@ -358,14 +371,18 @@ const ProductForm: React.FC = ({ product, onClose, onSuccess } - handleInputChange('currency', e.target.value)} className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="USD" - maxLength={3} - /> + required + > + {currencyOptions.map(currency => ( + + ))} +
@@ -429,13 +446,30 @@ const ProductForm: React.FC = ({ product, onClose, onSuccess } type="number" min="-1" value={formData.stockQuantity} - onChange={(e) => handleInputChange('stockQuantity', parseInt(e.target.value) || -1)} + onChange={(e) => { + const value = e.target.value; + if (value === '') { + handleInputChange('stockQuantity', -1); + } else { + const numValue = parseInt(value); + if (!isNaN(numValue) && numValue >= -1) { + handleInputChange('stockQuantity', numValue); + } + } + }} className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="-1" /> -

- Use -1 for unlimited stock -

+
+

+ Use -1 for unlimited stock, 0 for out of stock, or enter a positive number +

+
+ {formData.stockQuantity === -1 ? 'Unlimited' : + formData.stockQuantity === 0 ? 'Out of Stock' : + `${formData.stockQuantity} units`} +
+
@@ -573,7 +607,7 @@ const ProductForm: React.FC = ({ product, onClose, onSuccess } {/* Purchase URL */}
= ({ product, onClose, onSuccess } onChange={(e) => handleInputChange('purchaseUrl', e.target.value)} className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="https://example.com/product" + required />

- Direct link where customers can purchase this product + Direct link where customers can purchase this product (required)

diff --git a/src/data/mockData.ts b/src/data/mockData.ts index c1de02c..1c2979a 100644 --- a/src/data/mockData.ts +++ b/src/data/mockData.ts @@ -93,9 +93,9 @@ export const mockResellerRecentActivities: RecentActivity[] = [ }, { id: '2', - type: 'instance_created', - title: 'Cloud Instance Created', - description: 'Created new cloud instance for DataFlow Inc with 8GB RAM configuration', + type: 'product_sold', + title: 'Product Sold', + description: 'Sold cloud hosting package to DataFlow Inc', timestamp: '2025-01-15T09:15:00Z', amount: 1200, currency: 'USD', diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts new file mode 100644 index 0000000..fdd5f0b --- /dev/null +++ b/src/hooks/useSocket.ts @@ -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(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; \ No newline at end of file diff --git a/src/pages/ApprovedResellers/ApprovedResellers.tsx b/src/pages/ApprovedResellers/ApprovedResellers.tsx index 2b555a9..441c85e 100644 --- a/src/pages/ApprovedResellers/ApprovedResellers.tsx +++ b/src/pages/ApprovedResellers/ApprovedResellers.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { mockResellers } from '../../data/mockData'; import { formatCurrency, formatNumber, formatDate } from '../../utils/format'; import Modal from '../../components/Modal'; import AddPartnershipForm from '../../components/forms/AddPartnershipForm'; import DetailView from '../../components/DetailView'; +import toast from 'react-hot-toast'; import { Search, Filter, @@ -23,28 +23,103 @@ import { Download, Mail, MapPin, - Building2 + Building2, + User } from 'lucide-react'; import { cn } from '../../utils/cn'; +interface Reseller { + id: string; + firstName: string; + lastName: string; + email: string; + phone?: string; + status: string; + tier?: string; + totalRevenue?: number; + lastActive?: string; + customers?: number; + commissionRate?: number; + region?: string; + avatar?: string; + company?: string; + createdAt?: string; +} + const ApprovedResellersPage: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); - const [statusFilter, setStatusFilter] = useState('all'); + const [statusFilter, setStatusFilter] = useState('active'); const [tierFilter, setTierFilter] = useState('all'); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); - const [selectedReseller, setSelectedReseller] = useState(null); + const [selectedReseller, setSelectedReseller] = useState(null); + const [resellers, setResellers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); // Set page title useEffect(() => { document.title = 'Approved Resellers - Cloudtopiaa'; }, []); + // Fetch approved resellers from API + useEffect(() => { + fetchApprovedResellers(); + }, []); + + const fetchApprovedResellers = async () => { + try { + setLoading(true); + setError(null); + + const token = localStorage.getItem('accessToken'); + if (!token) { + setError('Authentication required. Please log in again.'); + return; + } + + const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/vendors/resellers?status=active`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Failed to fetch approved resellers'); + } + + const data = await response.json(); + if (data.success) { + setResellers(data.data.resellers || []); + if (data.data.resellers && data.data.resellers.length > 0) { + toast.success(`Loaded ${data.data.resellers.length} approved resellers`); + } else { + toast('No approved resellers found'); + } + } else { + const errorMsg = data.message || 'Failed to fetch approved resellers'; + setError(errorMsg); + toast.error(errorMsg); + } + } catch (error: any) { + console.error('Error fetching approved resellers:', error); + const errorMsg = error.message || 'Failed to fetch approved resellers'; + setError(errorMsg); + toast.error(errorMsg); + } finally { + setLoading(false); + } + }; + // Filter only approved (active) resellers - const approvedResellers = mockResellers.filter(reseller => reseller.status === 'active'); + const approvedResellers = resellers.filter(reseller => reseller.status === 'active'); const filteredResellers = approvedResellers.filter(reseller => { - const matchesSearch = reseller.name.toLowerCase().includes(searchTerm.toLowerCase()); + const fullName = `${reseller.firstName} ${reseller.lastName}`.toLowerCase(); + const matchesSearch = fullName.includes(searchTerm.toLowerCase()) || + reseller.email.toLowerCase().includes(searchTerm.toLowerCase()) || + (reseller.company && reseller.company.toLowerCase().includes(searchTerm.toLowerCase())); const matchesStatus = statusFilter === 'all' || reseller.status === statusFilter; const matchesTier = tierFilter === 'all' || reseller.tier === tierFilter; @@ -78,31 +153,28 @@ const ApprovedResellersPage: React.FC = () => { }; const handleAddReseller = (data: any) => { - console.log('New reseller data:', data); // Here you would typically make an API call to add the reseller // For now, we'll just close the modal setIsAddModalOpen(false); - // You could also show a success notification here + toast.success('Reseller partnership request submitted successfully!'); }; - const handleViewReseller = (reseller: any) => { + const handleViewReseller = (reseller: Reseller) => { setSelectedReseller(reseller); setIsDetailModalOpen(true); }; - const handleEditReseller = (reseller: any) => { - console.log('Edit reseller:', reseller); - alert(`Edit functionality for ${reseller.name} - This would open an edit form`); + const handleEditReseller = (reseller: Reseller) => { + toast(`Edit functionality for ${reseller.firstName} ${reseller.lastName} - This would open an edit form`); }; - const handleMailReseller = (reseller: any) => { - console.log('Mail reseller:', reseller); + const handleMailReseller = (reseller: Reseller) => { const mailtoLink = `mailto:${reseller.email}?subject=Cloudtopiaa Partnership Update`; window.open(mailtoLink, '_blank'); + toast.success(`Opening email client for ${reseller.firstName} ${reseller.lastName}`); }; - const handleMoreOptions = (reseller: any) => { - console.log('More options for reseller:', reseller); + const handleMoreOptions = (reseller: Reseller) => { const options = [ 'View Performance', 'Download Report', @@ -110,12 +182,31 @@ const ApprovedResellersPage: React.FC = () => { 'Change Terms', 'Suspend Partnership' ]; - const selectedOption = prompt(`Select an option for ${reseller.name}:\n${options.join('\n')}`); + const selectedOption = prompt(`Select an option for ${reseller.firstName} ${reseller.lastName}:\n${options.join('\n')}`); if (selectedOption) { - alert(`Selected: ${selectedOption} for ${reseller.name}`); + toast.success(`Selected: ${selectedOption} for ${reseller.firstName} ${reseller.lastName}`); } }; + if (loading) { + return
Loading...
; + } + + if (error) { + return
{error}
; + } + + if (resellers.length === 0) { + return ( +
+

No approved resellers found.

+

+ Please ensure you have active partnerships with resellers. +

+
+ ); + } + return (
{/* Header */} @@ -183,9 +274,9 @@ const ApprovedResellersPage: React.FC = () => {

Total Customers

-

- {approvedResellers.reduce((sum, r) => sum + r.customers, 0)} -

+

+ {approvedResellers.reduce((sum, r) => sum + (r.customers || 0), 0)} +

@@ -199,9 +290,9 @@ const ApprovedResellersPage: React.FC = () => {

Total Revenue

-

- {formatCurrency(approvedResellers.reduce((sum, r) => sum + r.totalRevenue, 0))} -

+

+ {formatCurrency(approvedResellers.reduce((sum, r) => sum + (r.totalRevenue || 0), 0))} +

@@ -308,20 +399,39 @@ const ApprovedResellersPage: React.FC = () => {
- {reseller.name} +
+ {reseller.avatar ? ( + {`${reseller.firstName} { + // Hide the image if it fails to load and show fallback + e.currentTarget.style.display = 'none'; + e.currentTarget.nextElementSibling?.classList.remove('hidden'); + }} + /> + ) : null} +
+ {reseller.firstName && reseller.lastName ? ( + `${reseller.firstName.charAt(0).toUpperCase()}${reseller.lastName.charAt(0).toUpperCase()}` + ) : ( + + )} +
+
- {reseller.name} + {reseller.firstName || ''} {reseller.lastName || ''}
- {reseller.email} + {reseller.email || 'N/A'}
- {reseller.phone} + {reseller.phone || 'N/A'}
@@ -329,31 +439,31 @@ const ApprovedResellersPage: React.FC = () => { - {reseller.status.charAt(0).toUpperCase() + reseller.status.slice(1)} + {reseller.status ? reseller.status.charAt(0).toUpperCase() + reseller.status.slice(1) : 'Active'} - - {reseller.tier.charAt(0).toUpperCase() + reseller.tier.slice(1)} - - - - {formatCurrency(reseller.totalRevenue)} - - - {formatNumber(reseller.customers)} - - - {reseller.commissionRate}% - - - {formatDate(reseller.lastActive)} + + {reseller.tier ? reseller.tier.charAt(0).toUpperCase() + reseller.tier.slice(1) : 'N/A'} + + + {formatCurrency(reseller.totalRevenue || 0)} + + + {formatNumber(reseller.customers || 0)} + + + {reseller.commissionRate ? `${reseller.commissionRate}%` : 'N/A'} + + + {reseller.lastActive ? formatDate(reseller.lastActive) : 'N/A'} +
{/* Status Filter */} @@ -238,7 +291,7 @@ const Resellers: React.FC = () => { 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" > @@ -485,6 +538,156 @@ const Resellers: React.FC = () => {
)} + {/* Edit Reseller Modal */} + {editingReseller && ( +
+
+
+

+ Edit Reseller +

+
{ + 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); + }}> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +

+ Leave empty if no website +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ )} + {/* Delete Confirmation Modal */} {isDeleteModalOpen && (
diff --git a/src/pages/admin/VendorRequests.tsx b/src/pages/admin/VendorRequests.tsx index 4b43823..7e5e2a2 100644 --- a/src/pages/admin/VendorRequests.tsx +++ b/src/pages/admin/VendorRequests.tsx @@ -33,9 +33,7 @@ const VendorRequests: React.FC = () => { // Get auth state from Redux const { isAuthenticated, token, user } = useAppSelector((state) => state.auth); - // Debug: Log auth state - console.log('Auth state:', { isAuthenticated, hasToken: !!token, user }); - console.log('Modal states:', { showModal, showRejectionModal, selectedVendor: selectedVendor?.id }); + useEffect(() => { fetchVendorRequests(); @@ -53,11 +51,7 @@ const VendorRequests: React.FC = () => { try { setLoading(true); - // Debug: Check if token exists const token = localStorage.getItem('accessToken'); - console.log('Token exists:', !!token); - console.log('Token length:', token ? token.length : 0); - console.log('Redux token exists:', !!token); if (!token) { console.error('No token found in localStorage'); @@ -72,12 +66,7 @@ const VendorRequests: React.FC = () => { } }); - // Debug: Log response details - console.log('Response status:', response.status); - console.log('Response headers:', Object.fromEntries(response.headers.entries())); - const data = await response.json(); - console.log('Response data:', data); if (!response.ok) { throw new Error(data.message || 'Failed to fetch vendor requests'); @@ -321,10 +310,8 @@ const VendorRequests: React.FC = () => {
+ ))} + +
+ +
+ {/* Search and Filters */} +
+
+
+ + 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" + /> +
+
+
+ +
-

- Coming Soon + + {/* Tab Content */} + {activeTab === 'overview' && ( +
+ {/* Receipts Summary */} +
+

+ + Receipts Summary +

+
+
+ Total Receipts + {totalReceipts} +
+
+ Paid + {paidReceipts} +
+
+ Pending + {pendingReceipts} +
+
+ Overdue + {overdueReceipts} +
+
+
+ + {/* Recent Activity */} +
+

+ + Recent Activity

-

- Billing and payment features will be available soon. -

+
+ {sales?.slice(0, 5).map((sale: any) => ( +
+ + {sale.productName} sold to {sale.customerName} + + + + +
+ ))} +
+
+
+ )} + + {activeTab === 'receipts' && ( +
+ + + + + + + + + + + + + {filteredReceipts.map((receipt: any) => ( + + + + + + + + + ))} + +
+ Invoice + + Customer + + Amount + + Status + + Due Date + + Actions +
+ {receipt.invoiceNumber} + + {receipt.customerName} + + + + + {getStatusIcon(receipt.status)} + {receipt.status} + + + {formatDate(receipt.dueDate)} + +
+ + +
+
+
+ )} + + {activeTab === 'payments' && ( +
+ + + + + + + + + + + + {filteredPayments.map((payment: any) => ( + + + + + + + + ))} + +
+ Date + + Description + + Amount + + Type + + Status +
+ {formatDate(payment.timestamp)} + + {payment.description} + + + + + {payment.type === 'received' ? 'Received' : 'Sent'} + + + + {getStatusIcon(payment.status)} + {payment.status} + +
+
+ )} + + {activeTab === 'sales' && ( +
+ + + + + + + + + + + + + {filteredSales.map((sale: any) => ( + + + + + + + + + ))} + +
+ Date + + Customer + + Product + + Amount + + Commission + + Status +
+ {formatDate(sale.timestamp)} + + {sale.customerName} + + {sale.productName} + + + + + + + {getStatusIcon(sale.status)} + {sale.status} + +
+
+ )}

); }; -export default Billing; \ No newline at end of file +export default ResellerBilling; \ No newline at end of file diff --git a/src/pages/reseller/Dashboard.tsx b/src/pages/reseller/Dashboard.tsx index 723545d..f7f19fa 100644 --- a/src/pages/reseller/Dashboard.tsx +++ b/src/pages/reseller/Dashboard.tsx @@ -1,91 +1,285 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useAppSelector, useAppDispatch } from '../../store/hooks'; import { useNavigate } from 'react-router-dom'; -import { setStats, setRecentActivities, setQuickActions } from '../../store/slices/dashboardSlice'; -import { mockDashboardStats, mockResellerQuickActions, mockResellerRecentActivities } from '../../data/mockData'; +import { + fetchDashboardStats, + fetchCustomers, + fetchProducts, + fetchSales, + fetchRecentActivities, + addCustomer +} from '../../store/reseller/resellerDashboardSlice'; import { formatNumber, formatRelativeTime, formatPercentage } from '../../utils/format'; import RevenueChart from '../../components/charts/RevenueChart'; -import ResellerPerformanceChart from '../../components/charts/ResellerPerformanceChart'; import DualCurrencyDisplay from '../../components/DualCurrencyDisplay'; +import MarkProductSoldForm from '../../components/forms/MarkProductSoldForm'; import { - TrendingUp, + Package, Users, - Cloud, - DollarSign, - UserPlus, + TrendingUp, + ArrowUpRight, + Star, CheckCircle, - Briefcase, - GraduationCap, - BarChart3, - CreditCard, - Headphones, - ShoppingBag, - Award, - HelpCircle, - Settings, - Wallet, - BookOpen, - Zap, - Target, - Star, - ArrowUpRight, - Activity + Activity, + Receipt, + FileText, + X, + Plus } from 'lucide-react'; import { cn } from '../../utils/cn'; +import toast from 'react-hot-toast'; const ResellerDashboard: React.FC = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); - const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard); - const { user } = useAppSelector((state) => state.auth); + const [isCustomerModalOpen, setIsCustomerModalOpen] = useState(false); + const [isMarkProductSoldModalOpen, setIsMarkProductSoldModalOpen] = useState(false); + const [customerForm, setCustomerForm] = useState({ + firstName: '', + lastName: '', + email: '', + company: '', + phone: '' + }); + + const { + stats, + customers, + products, + sales, + recentActivities, + isLoading, + error + } = useAppSelector((state: any) => state.resellerDashboard); + + const { user } = useAppSelector(state => state.auth); useEffect(() => { - // Initialize dashboard data - dispatch(setStats(mockDashboardStats)); - dispatch(setRecentActivities(mockResellerRecentActivities)); - dispatch(setQuickActions(mockResellerQuickActions)); + dispatch(fetchDashboardStats()); + dispatch(fetchCustomers()); + dispatch(fetchProducts()); + dispatch(fetchSales()); + dispatch(fetchRecentActivities()); }, [dispatch]); const handleQuickAction = (action: any) => { switch (action.id) { case 'add-customer': - navigate('/reseller-dashboard/customers'); + setIsCustomerModalOpen(true); break; - case 'create-instance': - navigate('/reseller-dashboard/instances'); + case 'manage-products': + navigate('/reseller-dashboard/products'); + break; + case 'view-sales': + navigate('/reseller-dashboard/sales'); break; case 'billing': navigate('/reseller-dashboard/billing'); break; - case 'support': - navigate('/reseller-dashboard/support'); - break; - case 'training': - navigate('/reseller-dashboard/training'); - break; case 'reports': navigate('/reseller-dashboard/reports'); break; - case 'wallet': - navigate('/reseller-dashboard/wallet'); - break; - case 'marketplace': - navigate('/reseller-dashboard/marketplace'); - break; - case 'certifications': - navigate('/reseller-dashboard/certifications'); - break; - case 'knowledge-base': - navigate('/reseller-dashboard/knowledge-base'); - break; - case 'settings': - navigate('/reseller-dashboard/settings'); + case 'mark-product-sold': + setIsMarkProductSoldModalOpen(true); break; default: - break; + toast('Feature coming soon!'); } }; + const handleCustomerSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!customerForm.firstName || !customerForm.lastName || !customerForm.email) { + toast.error('Please fill in all required fields'); + return; + } + + try { + await dispatch(addCustomer(customerForm)).unwrap(); + toast.success('Customer added successfully!'); + setIsCustomerModalOpen(false); + setCustomerForm({ firstName: '', lastName: '', email: '', company: '', phone: '' }); + // Refresh customers list + dispatch(fetchCustomers()); + } catch (error: any) { + toast.error(error.message || 'Failed to add customer'); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + 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 ( +
+ {/* Welcome Section Skeleton */} +
+
+
+
+
+
+ + {/* Metrics Skeleton */} +
+ {[1, 2, 3, 4].map((i) => ( +
+
+
+
+
+
+ ))} +
+ + {/* Loading Message */} +
+
+
Loading dashboard data...
+
+
+ ); + } + + // Show error state + if (error) { + return ( +
+
Error loading dashboard
+
{error}
+ +
+ ); + } + + // Show empty state if no data + if (!realCustomers.length && !realProducts.length && !realSales.length) { + return ( +
+ {/* Welcome Section */} +
+
+
+
+
+

Welcome back, {user ? `${user.firstName} ${user.lastName}` : 'User'}!

+

Ready to grow your business? Let's get started!

+
+
+
+
+ + {/* Empty State */} +
+
+ +
+

No Data Available

+

+ Your dashboard is empty. Start by adding customers and recording your first sales. + Please ensure you have active partnerships with resellers. +

+
+ + +
+
+
+ ); + } + return (
{/* Welcome Section */} @@ -94,18 +288,14 @@ const ResellerDashboard: React.FC = () => {
-

Welcome back, {user ? `${user.firstName} ${user.lastName}` : 'User'}!

-

Here's what's happening with your cloud services business today.

+

Welcome back, {user ? `${user.firstName} ${user.lastName}` : 'User'}!

+

Here's what's happening with your business today

-
-
-
{formatNumber(stats.totalResellers)}
-
Active Customers
-
+
Monthly Revenue
@@ -113,200 +303,280 @@ const ResellerDashboard: React.FC = () => {
-
-
{/* Key Metrics */}
-
-
-
- +
+
+
+

+ +

+

Total Revenue

+
+ + +{realStats.monthlyGrowth || 0}% from last month +
+
+
+
- -
-

- -

-

Total Revenue

-
- - +{stats.monthlyGrowth}% from last month
-
-
-
- +
+
+
+

{activeCustomers}

+

Active Customers

+
+ + +{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 +
+
+
+
- -
-

- {formatNumber(stats.totalResellers)} -

-

Active Customers

-
- - +5 new this month
-
-
-
- +
+
+
+

{totalProducts}

+

Active Products

+
+ + +{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 +
+
+
+
- -
-

- {formatNumber(stats.activePartnerships)} -

-

Cloud Instances

-
- - +12 new instances
-
-
-
- +
+
+
+

+ {formatPercentage(realStats.commissionRate || 0)} +

+

Commission Rate

+
+ + {(realStats.commissionRate || 0) >= 15 ? 'Premium tier' : 'Standard tier'} +
+
+
+
- -
-

- {formatPercentage(15)} -

-

Commission Rate

-
- - Premium tier
- {/* Quick Actions & Recent Activity */} -
- {/* Quick Actions */} -
-
-

- - Quick Actions -

-
- {quickActions.slice(0, 6).map((action) => ( - - ))} -
-
-
- - {/* Recent Activity */} -
-
-

- - Recent Activity -

-
- {recentActivities.map((activity) => ( -
-
- {activity.type === 'customer_added' && } - {activity.type === 'instance_created' && } - {activity.type === 'payment_received' && } - {activity.type === 'support_ticket' && } - {activity.type === 'training_completed' && } -
-
-

- {activity.title} -

-

- {activity.description} -

- {activity.amount && ( -
- -
- )} -

- {formatRelativeTime(activity.timestamp)} -

-
- ))} + + ); + })} +
+
+ + {/* Recent Activity */} +
+

+ Recent Activity +

+
+ {realActivities.slice(0, 8).map((activity: any) => ( +
+
+ {activity.type === 'customer_added' && } + {activity.type === 'product_sold' && } + {activity.type === 'payment_received' && } + {activity.type === 'commission_earned' && } + {!['customer_added', 'product_sold', 'payment_received', 'commission_earned'].includes(activity.type) && } +
+
+
+

+ {activity.title} +

+ + {formatRelativeTime(activity.timestamp)} + +
+

+ {activity.description} +

+ {activity.amount && ( +
+ + {activity.currency === 'USD' ? '$' : '₹'}{activity.amount} + +
+ )} +
-
-
+
+ + {/* Customer Modal */} + {isCustomerModalOpen && ( +
+
+
+

Add New Customer

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
-
+ )} - {/* Charts Section */} -
-
-

- - Revenue Overview -

-
- -
-
- -
-

- - Customer Performance -

-
- -
-
-
+ {/* Mark Product Sold Modal */} + {isMarkProductSoldModalOpen && ( + setIsMarkProductSoldModalOpen(false)} + onSuccess={() => { + // Refresh data after marking product as sold + dispatch(fetchDashboardStats()); + dispatch(fetchSales()); + dispatch(fetchRecentActivities()); + }} + products={realProducts} + customers={realCustomers} + /> + )}
); }; diff --git a/src/services/resellerDashboardService.ts b/src/services/resellerDashboardService.ts new file mode 100644 index 0000000..d799612 --- /dev/null +++ b/src/services/resellerDashboardService.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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): Promise { + 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): Promise { + 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): Promise { + 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 { + 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(); \ No newline at end of file diff --git a/src/store/reseller/resellerDashboardSlice.ts b/src/store/reseller/resellerDashboardSlice.ts new file mode 100644 index 0000000..0d1108d --- /dev/null +++ b/src/store/reseller/resellerDashboardSlice.ts @@ -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, { rejectWithValue }) => { + try { + return await resellerDashboardService.addCustomer(customerData); + } catch (error: any) { + return rejectWithValue(error.message); + } + } +); + +export const createSale = createAsyncThunk( + 'resellerDashboard/createSale', + async (saleData: Partial, { 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) => { + state.isLoading = action.payload; + }, + setError: (state, action: PayloadAction) => { + state.error = action.payload; + }, + updateStats: (state, action: PayloadAction>) => { + state.stats = { ...state.stats, ...action.payload }; + }, + addRecentActivity: (state, action: PayloadAction) => { + state.recentActivities.unshift(action.payload); + if (state.recentActivities.length > 20) { + state.recentActivities = state.recentActivities.slice(0, 20); + } + }, + updateCustomer: (state, action: PayloadAction) => { + const index = state.customers.findIndex(c => c.id === action.payload.id); + if (index !== -1) { + state.customers[index] = action.payload; + } + }, + removeCustomer: (state, action: PayloadAction) => { + state.customers = state.customers.filter(c => c.id !== action.payload); + }, + addSale: (state, action: PayloadAction) => { + state.sales.unshift(action.payload); + if (state.sales.length > 100) { + state.sales = state.sales.slice(0, 100); + } + }, + addReceipt: (state, action: PayloadAction) => { + state.receipts.unshift(action.payload); + if (state.receipts.length > 100) { + state.receipts = state.receipts.slice(0, 100); + } + }, + addPayment: (state, action: PayloadAction) => { + 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; \ No newline at end of file diff --git a/src/store/slices/dashboardSlice.ts b/src/store/slices/dashboardSlice.ts index e05f70d..a34d97c 100644 --- a/src/store/slices/dashboardSlice.ts +++ b/src/store/slices/dashboardSlice.ts @@ -14,7 +14,7 @@ export interface DashboardStats { export interface RecentActivity { id: string; - type: 'reseller_added' | 'deal_closed' | 'commission_earned' | 'partnership_approved' | 'training_completed' | 'customer_added' | 'instance_created' | 'payment_received' | 'support_ticket'; + type: 'reseller_added' | 'deal_closed' | 'commission_earned' | 'partnership_approved' | 'training_completed' | 'customer_added' | 'instance_created' | 'payment_received' | 'support_ticket' | 'product_sold'; title: string; description: string; timestamp: string;