= ({ isOpen, onClose, title, children, size =
};
return (
-
+
+
{/* Backdrop */}
{/* Modal */}
@@ -65,7 +77,7 @@ const Modal: React.FC
= ({ isOpen, onClose, title, children, size =
{/* Content */}
-
diff --git a/src/components/NotificationBell.tsx b/src/components/NotificationBell.tsx
new file mode 100644
index 0000000..d36b7f3
--- /dev/null
+++ b/src/components/NotificationBell.tsx
@@ -0,0 +1,91 @@
+import React, { useState, useEffect } from 'react';
+import { Bell } from 'lucide-react';
+import { useAppSelector } from '../store/hooks';
+import NotificationPanel from './NotificationPanel';
+
+const NotificationBell: React.FC = () => {
+ const [unreadCount, setUnreadCount] = useState(0);
+ const [isPanelOpen, setIsPanelOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const { user } = useAppSelector((state) => state.auth);
+
+ useEffect(() => {
+ fetchUnreadCount();
+
+ // Set up polling for new notifications
+ const interval = setInterval(fetchUnreadCount, 30000); // Check every 30 seconds
+
+ return () => clearInterval(interval);
+ }, [user]);
+
+ const fetchUnreadCount = async () => {
+ try {
+ setLoading(true);
+
+ // Determine the appropriate endpoint based on user role
+ let endpoint = '/admin/notifications/stats';
+ if (user?.role && user.role.startsWith('channel_partner_')) {
+ endpoint = '/vendors/notifications/stats';
+ } else if (user?.role && user.role.startsWith('reseller_')) {
+ endpoint = '/resellers/notifications/stats';
+ }
+
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setUnreadCount(data.data?.unreadNotifications || 0);
+ }
+ } catch (error) {
+ console.error('Error fetching notification count:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleBellClick = () => {
+ setIsPanelOpen(true);
+ };
+
+ const handlePanelClose = () => {
+ setIsPanelOpen(false);
+ // Refresh count when panel closes
+ fetchUnreadCount();
+ };
+
+ return (
+ <>
+
+
+
+ {unreadCount > 0 && (
+
+ {unreadCount > 99 ? '99+' : unreadCount}
+
+ )}
+ {loading && (
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default NotificationBell;
\ No newline at end of file
diff --git a/src/components/NotificationPanel.tsx b/src/components/NotificationPanel.tsx
new file mode 100644
index 0000000..12ed98a
--- /dev/null
+++ b/src/components/NotificationPanel.tsx
@@ -0,0 +1,394 @@
+import React, { useState, useEffect } from 'react';
+import { Bell, X, Check, Trash2, Clock, AlertCircle, CheckCircle, XCircle, Users, UserPlus } from 'lucide-react';
+import { useAppSelector } from '../store/hooks';
+
+interface Notification {
+ id: string;
+ type: 'NEW_VENDOR_REQUEST' | 'NEW_RESELLER_REQUEST' | 'VENDOR_APPROVED' | 'VENDOR_REJECTED' | 'RESELLER_APPROVED' | 'RESELLER_REJECTED' | 'RESELLER_CREATED' | 'RESELLER_ACCOUNT_CREATED' | 'SYSTEM_ALERT' | 'GENERAL';
+ title: string;
+ message: string;
+ data?: any;
+ isRead: boolean;
+ priority: 'low' | 'medium' | 'high' | 'critical';
+ createdAt: string;
+ sender?: {
+ id: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+ };
+}
+
+interface NotificationPanelProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+const NotificationPanel: React.FC
= ({ isOpen, onClose }) => {
+ const [notifications, setNotifications] = useState([]);
+ const [unreadCount, setUnreadCount] = useState(0);
+ const [loading, setLoading] = useState(false);
+ const [activeTab, setActiveTab] = useState<'all' | 'unread'>('all');
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [deletingNotificationId, setDeletingNotificationId] = useState(null);
+ const { user } = useAppSelector((state) => state.auth);
+
+ useEffect(() => {
+ if (isOpen) {
+ fetchNotifications();
+ }
+ }, [isOpen, activeTab, user]);
+
+ const getApiEndpoint = () => {
+ if (user?.role && user.role.startsWith('channel_partner_')) {
+ return '/vendors/notifications';
+ } else if (user?.role && user.role.startsWith('reseller_')) {
+ return '/resellers/notifications';
+ } else {
+ return '/admin/notifications';
+ }
+ };
+
+ const getMarkAsReadEndpoint = (notificationId: string) => {
+ if (user?.role && user.role.startsWith('channel_partner_')) {
+ return `/vendors/notifications/${notificationId}/read`;
+ } else if (user?.role && user.role.startsWith('reseller_')) {
+ return `/resellers/notifications/${notificationId}/read`;
+ } else {
+ return `/admin/notifications/${notificationId}/read`;
+ }
+ };
+
+ const getMarkAllAsReadEndpoint = () => {
+ if (user?.role && user.role.startsWith('channel_partner_')) {
+ return '/vendors/notifications/read-all';
+ } else if (user?.role && user.role.startsWith('reseller_')) {
+ return '/resellers/notifications/read-all';
+ } else {
+ return '/admin/notifications/mark-all-read';
+ }
+ };
+
+ const fetchNotifications = async () => {
+ try {
+ setLoading(true);
+ const endpoint = getApiEndpoint();
+ const params = new URLSearchParams({
+ page: '1',
+ limit: '50',
+ ...(activeTab === 'unread' && { unreadOnly: 'true' })
+ });
+
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}?${params}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setNotifications(data.data.notifications);
+ setUnreadCount(data.data.notifications.filter((n: Notification) => !n.isRead).length);
+ }
+ } catch (error) {
+ console.error('Error fetching notifications:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const markAsRead = async (notificationId: string) => {
+ try {
+ const endpoint = getMarkAsReadEndpoint(notificationId);
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ setUnreadCount(prev => Math.max(0, prev - 1));
+ setNotifications(prev =>
+ prev.map(notif =>
+ notif.id === notificationId
+ ? { ...notif, isRead: true }
+ : notif
+ )
+ );
+ }
+ } catch (error) {
+ console.error('Error marking notification as read:', error);
+ }
+ };
+
+ const markAllAsRead = async () => {
+ try {
+ const endpoint = getMarkAllAsReadEndpoint();
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ setNotifications(prev => prev.map(notif => ({ ...notif, isRead: true })));
+ setUnreadCount(0);
+ }
+ } catch (error) {
+ console.error('Error marking all notifications as read:', error);
+ }
+ };
+
+ const deleteNotification = (notificationId: string) => {
+ setDeletingNotificationId(notificationId);
+ setIsDeleteModalOpen(true);
+ };
+
+ const confirmDelete = async () => {
+ if (!deletingNotificationId) return;
+
+ try {
+ const endpoint = getApiEndpoint();
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}/${deletingNotificationId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ setNotifications(prev => prev.filter(notif => notif.id !== deletingNotificationId));
+ setIsDeleteModalOpen(false);
+ setDeletingNotificationId(null);
+ }
+ } catch (error) {
+ console.error('Error deleting notification:', error);
+ }
+ };
+
+ const getNotificationIcon = (type: string) => {
+ switch (type) {
+ case 'NEW_VENDOR_REQUEST':
+ return ;
+ case 'NEW_RESELLER_REQUEST':
+ return ;
+ case 'VENDOR_APPROVED':
+ return ;
+ case 'VENDOR_REJECTED':
+ return ;
+ case 'RESELLER_APPROVED':
+ return ;
+ case 'RESELLER_REJECTED':
+ return ;
+ case 'RESELLER_CREATED':
+ return ;
+ case 'RESELLER_ACCOUNT_CREATED':
+ return ;
+ case 'SYSTEM_ALERT':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getPriorityColor = (priority: string) => {
+ switch (priority) {
+ case 'critical':
+ return 'border-red-500 bg-red-50';
+ case 'high':
+ return 'border-orange-500 bg-orange-50';
+ case 'medium':
+ return 'border-yellow-500 bg-yellow-50';
+ case 'low':
+ return 'border-gray-300 bg-gray-50';
+ default:
+ return 'border-gray-300 bg-gray-50';
+ }
+ };
+
+ const formatTime = (dateString: string) => {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
+
+ if (diffInMinutes < 1) return 'Just now';
+ if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
+ if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`;
+ return date.toLocaleDateString();
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ Notifications
+
+ {unreadCount > 0 && (
+
+ {unreadCount}
+
+ )}
+
+
+ {unreadCount > 0 && (
+
+ Mark all read
+
+ )}
+
+
+
+
+
+
+ {/* Tabs */}
+
+ setActiveTab('all')}
+ className={`flex-1 py-3 px-4 text-sm font-medium ${
+ activeTab === 'all'
+ ? 'text-blue-600 border-b-2 border-blue-600'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
+ }`}
+ >
+ All
+
+ setActiveTab('unread')}
+ className={`flex-1 py-3 px-4 text-sm font-medium ${
+ activeTab === 'unread'
+ ? 'text-blue-600 border-b-2 border-blue-600'
+ : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
+ }`}
+ >
+ Unread ({unreadCount})
+
+
+
+ {/* Notifications List */}
+
+ {loading ? (
+
+ ) : notifications.length === 0 ? (
+
+
+
No notifications
+
You're all caught up!
+
+ ) : (
+ notifications.map((notification) => (
+
+
+
+ {getNotificationIcon(notification.type)}
+
+
+
+
+
+ {notification.title}
+
+
+ {notification.message}
+
+
+
+
+ {formatTime(notification.createdAt)}
+
+ {notification.sender && (
+
+ From: {notification.sender.firstName} {notification.sender.lastName}
+
+ )}
+
+
+
+ {!notification.isRead && (
+ markAsRead(notification.id)}
+ className="p-1 text-gray-400 hover:text-green-600 dark:hover:text-green-400"
+ title="Mark as read"
+ >
+
+
+ )}
+ deleteNotification(notification.id)}
+ className="p-1 text-gray-400 hover:text-red-600 dark:hover:text-red-400"
+ title="Delete"
+ >
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+ {/* Delete Confirmation Modal */}
+ {isDeleteModalOpen && (
+
+
+
Confirm Delete
+
+ Are you sure you want to delete this notification? This action cannot be undone.
+
+
+ {
+ setIsDeleteModalOpen(false);
+ setDeletingNotificationId(null);
+ }}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
+ >
+ Cancel
+
+
+ Delete Notification
+
+
+
+
+ )}
+
+ );
+};
+
+export default NotificationPanel;
\ No newline at end of file
diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx
index b8af006..df68644 100644
--- a/src/components/ProtectedRoute.tsx
+++ b/src/components/ProtectedRoute.tsx
@@ -1,7 +1,9 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAppSelector, useAppDispatch } from '../store/hooks';
import { getCurrentUser } from '../store/slices/authThunks';
+import { logout } from '../store/slices/authSlice';
+import BlockedAccountModal from './BlockedAccountModal';
interface ProtectedRouteProps {
children: React.ReactNode;
@@ -20,10 +22,11 @@ const ProtectedRoute: React.FC = ({
useEffect(() => {
// Check if user is authenticated but no user data
- if (isAuthenticated && !user) {
+ if (isAuthenticated && !user && !isLoading) {
+ console.log('ProtectedRoute: User authenticated but no user data, fetching user...');
dispatch(getCurrentUser());
}
- }, [isAuthenticated, user, dispatch]);
+ }, [isAuthenticated, user, isLoading, dispatch]);
// Show loading while checking authentication
if (isLoading) {
@@ -51,10 +54,21 @@ const ProtectedRoute: React.FC = ({
hasRequiredRole = user.role === requiredRole;
} else {
console.warn('User roles not properly loaded:', user);
- return ;
+ // For system_admin, allow access even if roles are not loaded properly
+ if (requiredRole === 'system_admin' && user.role === 'system_admin') {
+ hasRequiredRole = true;
+ } else {
+ return ;
+ }
}
if (!hasRequiredRole) {
+ console.log('User does not have required role:', {
+ userRole: user.role,
+ requiredRole,
+ userRoles: user.roles,
+ primaryRole: user.roles?.[0]?.name
+ });
return ;
}
}
@@ -65,9 +79,20 @@ const ProtectedRoute: React.FC = ({
// return ;
// }
- // Check if account is active
- if (user && user.status !== 'active') {
- return ;
+ // Check if account is blocked (inactive, pending, or suspended)
+ if (user && ['inactive', 'pending', 'suspended'].includes(user.status)) {
+ // Show blocked account modal instead of redirecting
+ return (
+ {
+ // Force logout when modal is closed
+ dispatch(logout());
+ }}
+ userEmail={user.email}
+ userStatus={user.status}
+ />
+ );
}
return <>{children}>;
diff --git a/src/components/VendorDetailsModal.tsx b/src/components/VendorDetailsModal.tsx
new file mode 100644
index 0000000..8be70f2
--- /dev/null
+++ b/src/components/VendorDetailsModal.tsx
@@ -0,0 +1,358 @@
+import React from 'react';
+import { X, Building, Mail, Phone, MapPin, Globe, FileText, DollarSign, Users, Calendar } from 'lucide-react';
+import { VendorModalProps } from '../types/vendor';
+
+const VendorDetailsModal: React.FC = ({
+ vendor,
+ isOpen,
+ onClose,
+ 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) => {
+ if (role.startsWith('channel_partner_')) {
+ return 'Vendor';
+ } else if (role.startsWith('reseller_')) {
+ return 'Reseller';
+ } else if (role.startsWith('system_')) {
+ return 'System Admin';
+ }
+ return role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+ };
+
+ const formatUserType = (userType: string) => {
+ if (userType === 'channel_partner') {
+ return 'Vendor';
+ } else if (userType === 'reseller') {
+ return 'Reseller';
+ }
+ return userType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+ };
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('en-IN', {
+ style: 'currency',
+ currency: 'INR',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0
+ }).format(amount);
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ Vendor Details
+
+
+
+
+
+
+
+ {/* Content */}
+
+ {/* Basic Information */}
+
+
+ Basic Information
+
+
+
+
+ Full Name
+
+
+ {vendor.firstName} {vendor.lastName}
+
+
+
+
+ Email
+
+
+
+ {vendor.email}
+
+
+
+
+ Phone
+
+
+
+ {vendor.phone || 'N/A'}
+
+
+
+
+ Company
+
+
+
+ {vendor.company}
+
+
+
+
+ Role
+
+
+ {formatRoleName(vendor.role)}
+
+
+
+
+ User Type
+
+
+ {formatUserType(vendor.userType)}
+
+
+
+
+
+ {/* Business Information */}
+ {(vendor.companyType || vendor.registrationNumber || vendor.gstNumber || vendor.panNumber) && (
+
+
+ Business Information
+
+
+ {vendor.companyType && (
+
+
+ Company Type
+
+
+ {vendor.companyType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
+
+
+ )}
+ {vendor.registrationNumber && (
+
+
+ Registration Number
+
+
+ {vendor.registrationNumber}
+
+
+ )}
+ {vendor.gstNumber && (
+
+
+ GST Number
+
+
+ {vendor.gstNumber}
+
+
+ )}
+ {vendor.panNumber && (
+
+
+ PAN Number
+
+
+ {vendor.panNumber}
+
+
+ )}
+
+
+ )}
+
+ {/* Financial Information */}
+ {(vendor.annualRevenue || vendor.employeeCount || vendor.yearsInBusiness) && (
+
+
+ Financial Information
+
+
+ {vendor.annualRevenue && (
+
+
+ Annual Revenue
+
+
+
+ {formatCurrency(vendor.annualRevenue)}
+
+
+ )}
+ {vendor.employeeCount && (
+
+
+ Employee Count
+
+
+
+ {vendor.employeeCount} employees
+
+
+ )}
+ {vendor.yearsInBusiness && (
+
+
+ Years in Business
+
+
+
+ {vendor.yearsInBusiness} years
+
+
+ )}
+
+
+ )}
+
+ {/* Address Information */}
+ {vendor.address && (
+
+
+ Address Information
+
+
+
+ Address
+
+
+
+ {vendor.address}
+
+
+
+ )}
+
+ {/* Additional Information */}
+ {(vendor.website || vendor.businessLicense || vendor.taxId || vendor.industry) && (
+
+
+ Additional Information
+
+
+ {vendor.website && (
+
+ )}
+ {vendor.businessLicense && (
+
+
+ Business License
+
+
+
+ {vendor.businessLicense}
+
+
+ )}
+ {vendor.taxId && (
+
+
+ Tax ID
+
+
+ {vendor.taxId}
+
+
+ )}
+ {vendor.industry && (
+
+
+ Industry
+
+
+ {vendor.industry}
+
+
+ )}
+
+
+ )}
+
+ {/* Rejection Information */}
+ {vendor.rejectionReason && (
+
+
+ Rejection Information
+
+
+
+ Rejection Reason
+
+
+ {vendor.rejectionReason}
+
+
+
+ )}
+
+ {/* Timestamp */}
+
+
+ Timestamp
+
+
+
+ Created At
+
+
+
+ {new Date(vendor.createdAt).toLocaleString()}
+
+
+
+
+
+ {/* Footer */}
+
+
+ Close
+
+ {vendor.status === 'pending' && (
+ <>
+ onReject(vendor.id, '')}
+ className="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700"
+ >
+ Reject
+
+ onApprove(vendor.id)}
+ className="px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700"
+ >
+ Approve
+
+ >
+ )}
+
+
+
+ );
+};
+
+export default VendorDetailsModal;
\ No newline at end of file
diff --git a/src/components/VendorRejectionModal.tsx b/src/components/VendorRejectionModal.tsx
new file mode 100644
index 0000000..4b66629
--- /dev/null
+++ b/src/components/VendorRejectionModal.tsx
@@ -0,0 +1,96 @@
+import React, { useState } from 'react';
+import { X, AlertTriangle } from 'lucide-react';
+import { RejectionModalProps } from '../types/vendor';
+
+const VendorRejectionModal: React.FC = ({
+ vendor,
+ isOpen,
+ onClose,
+ onReject
+}) => {
+ 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 = () => {
+ if (rejectionReason.trim()) {
+ onReject(vendor.id, rejectionReason.trim());
+ setRejectionReason('');
+ onClose();
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ Reject Vendor Request
+
+
+
+
+
+
+
+ {/* Content */}
+
+
+
+ Are you sure you want to reject the vendor request for{' '}
+
+ {vendor.firstName} {vendor.lastName}
+
+ ?
+
+
+ Please provide a reason for rejection. This will be communicated to the vendor.
+
+
+
+
+
+ Rejection Reason *
+
+
+
+
+ {/* Footer */}
+
+
+ Cancel
+
+
+ Reject Request
+
+
+
+
+ );
+};
+
+export default VendorRejectionModal;
\ No newline at end of file
diff --git a/src/components/charts/CommissionTrendsChart.tsx b/src/components/charts/CommissionTrendsChart.tsx
index a631669..bcadc52 100644
--- a/src/components/charts/CommissionTrendsChart.tsx
+++ b/src/components/charts/CommissionTrendsChart.tsx
@@ -106,7 +106,7 @@ const CommissionTrendsChart: React.FC = () => {
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#6b7280' }}
- tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
+ tickFormatter={(value) => `${((Number(value) || 0) / 1000).toFixed(0)}k`}
/>
} />
= ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
- companyName: '',
- contactPerson: '',
+ firstName: '',
+ lastName: '',
email: '',
phone: '',
+ company: '',
+ userType: '' as 'reseller_admin' | 'reseller_sales' | 'reseller_support' | 'read_only',
region: '',
- commissionRate: '',
- tier: 'silver',
+ businessType: '',
address: '',
});
const [errors, setErrors] = useState>({});
const [isSubmitting, setIsSubmitting] = useState(false);
+ const [showUserTypeDropdown, setShowUserTypeDropdown] = useState(false);
+ const [showRegionDropdown, setShowRegionDropdown] = useState(false);
+ const [showBusinessTypeDropdown, setShowBusinessTypeDropdown] = useState(false);
+
+ const userTypes = [
+ { value: 'reseller_admin', label: 'Reseller Admin', description: 'Full access to reseller dashboard and management' },
+ { value: 'reseller_sales', label: 'Sales Representative', description: 'Access to sales tools and customer management' },
+ { value: 'reseller_support', label: 'Support Representative', description: 'Access to support tools and ticket management' },
+ { value: 'read_only', label: 'Read Only', description: 'View-only access to reports and data' }
+ ];
+
+ const regions = [
+ 'North America', 'South America', 'Europe', 'Asia Pacific',
+ 'Middle East', 'Africa', 'India', 'Australia'
+ ];
+
+ const businessTypes = [
+ 'Technology Services', 'IT Consulting', 'Cloud Services',
+ 'Software Development', 'Digital Marketing', 'E-commerce',
+ 'Healthcare IT', 'Financial Services', 'Education Technology',
+ 'Manufacturing', 'Retail', 'Other'
+ ];
const validateForm = () => {
const newErrors: Record = {};
- if (!formData.companyName.trim()) {
- newErrors.companyName = 'Company name is required';
+ if (!formData.firstName.trim()) {
+ newErrors.firstName = 'First name is required';
}
- if (!formData.contactPerson.trim()) {
- newErrors.contactPerson = 'Contact person is required';
+ if (!formData.lastName.trim()) {
+ newErrors.lastName = 'Last name is required';
}
if (!formData.email.trim()) {
@@ -45,17 +68,20 @@ const AddResellerForm: React.FC = ({ onSubmit, onCancel })
newErrors.phone = 'Please enter a valid phone number';
}
- if (!formData.region.trim()) {
+ if (!formData.company.trim()) {
+ newErrors.company = 'Company name is required';
+ }
+
+ if (!formData.userType) {
+ newErrors.userType = 'User type is required';
+ }
+
+ if (!formData.region) {
newErrors.region = 'Region is required';
}
- if (!formData.commissionRate) {
- newErrors.commissionRate = 'Commission rate is required';
- } else {
- const rate = parseFloat(formData.commissionRate);
- if (isNaN(rate) || rate < 5 || rate > 20) {
- newErrors.commissionRate = 'Commission rate must be between 5% and 20%';
- }
+ if (!formData.businessType) {
+ newErrors.businessType = 'Business type is required';
}
setErrors(newErrors);
@@ -77,7 +103,6 @@ const AddResellerForm: React.FC = ({ onSubmit, onCancel })
onSubmit({
...formData,
- commissionRate: parseFloat(formData.commissionRate),
id: Date.now().toString(),
status: 'pending',
createdAt: new Date().toISOString(),
@@ -99,49 +124,49 @@ const AddResellerForm: React.FC = ({ onSubmit, onCancel })
return (
- ${(reseller.revenue / 1000).toFixed(0)}K
+ ${((Number(reseller.revenue) || 0) / 1000).toFixed(0)}K
{reseller.deals}
@@ -312,7 +312,7 @@ const Analytics: React.FC = () => {
- ${(region.revenue / 1000).toFixed(0)}K
+ ${((Number(region.revenue) || 0) / 1000).toFixed(0)}K
Revenue
diff --git a/src/pages/Partnerships/index.tsx b/src/pages/ApprovedResellers/ApprovedResellers.tsx
similarity index 63%
rename from src/pages/Partnerships/index.tsx
rename to src/pages/ApprovedResellers/ApprovedResellers.tsx
index a043a81..2b555a9 100644
--- a/src/pages/Partnerships/index.tsx
+++ b/src/pages/ApprovedResellers/ApprovedResellers.tsx
@@ -1,5 +1,5 @@
-import React, { useState } from 'react';
-import { mockPartnerships } from '../../data/mockData';
+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';
@@ -22,22 +22,31 @@ import {
AlertCircle,
Download,
Mail,
- MapPin
+ MapPin,
+ Building2
} from 'lucide-react';
import { cn } from '../../utils/cn';
-const PartnershipsPage: React.FC = () => {
+const ApprovedResellersPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [tierFilter, setTierFilter] = useState('all');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
- const [selectedPartnership, setSelectedPartnership] = useState(null);
+ const [selectedReseller, setSelectedReseller] = useState(null);
- const filteredPartnerships = mockPartnerships.filter(partnership => {
- const matchesSearch = partnership.reseller.toLowerCase().includes(searchTerm.toLowerCase());
- const matchesStatus = statusFilter === 'all' || partnership.status === statusFilter;
- const matchesTier = tierFilter === 'all' || partnership.tier === tierFilter;
+ // Set page title
+ useEffect(() => {
+ document.title = 'Approved Resellers - Cloudtopiaa';
+ }, []);
+
+ // Filter only approved (active) resellers
+ const approvedResellers = mockResellers.filter(reseller => reseller.status === 'active');
+
+ const filteredResellers = approvedResellers.filter(reseller => {
+ const matchesSearch = reseller.name.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesStatus = statusFilter === 'all' || reseller.status === statusFilter;
+ const matchesTier = tierFilter === 'all' || reseller.tier === tierFilter;
return matchesSearch && matchesStatus && matchesTier;
});
@@ -68,42 +77,42 @@ const PartnershipsPage: React.FC = () => {
}
};
- const handleAddPartnership = (data: any) => {
- console.log('New partnership data:', data);
- // Here you would typically make an API call to add the partnership
+ 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
};
- const handleViewPartnership = (partnership: any) => {
- setSelectedPartnership(partnership);
+ const handleViewReseller = (reseller: any) => {
+ setSelectedReseller(reseller);
setIsDetailModalOpen(true);
};
- const handleEditPartnership = (partnership: any) => {
- console.log('Edit partnership:', partnership);
- alert(`Edit functionality for ${partnership.reseller} partnership - This would open an edit form`);
+ const handleEditReseller = (reseller: any) => {
+ console.log('Edit reseller:', reseller);
+ alert(`Edit functionality for ${reseller.name} - This would open an edit form`);
};
- const handleMailPartnership = (partnership: any) => {
- console.log('Mail partnership:', partnership);
- const mailtoLink = `mailto:${partnership.contactEmail}?subject=Cloudtopiaa Partnership Update`;
+ const handleMailReseller = (reseller: any) => {
+ console.log('Mail reseller:', reseller);
+ const mailtoLink = `mailto:${reseller.email}?subject=Cloudtopiaa Partnership Update`;
window.open(mailtoLink, '_blank');
};
- const handleMoreOptions = (partnership: any) => {
- console.log('More options for partnership:', partnership);
+ const handleMoreOptions = (reseller: any) => {
+ console.log('More options for reseller:', reseller);
const options = [
'View Performance',
'Download Report',
'Send Notification',
'Change Terms',
- 'Terminate Partnership'
+ 'Suspend Partnership'
];
- const selectedOption = prompt(`Select an option for ${partnership.reseller}:\n${options.join('\n')}`);
+ const selectedOption = prompt(`Select an option for ${reseller.name}:\n${options.join('\n')}`);
if (selectedOption) {
- alert(`Selected: ${selectedOption} for ${partnership.reseller}`);
+ alert(`Selected: ${selectedOption} for ${reseller.name}`);
}
};
@@ -113,10 +122,10 @@ const PartnershipsPage: React.FC = () => {
- Partnerships
+ Approved Resellers
- Manage reseller partnerships and approval workflows
+ Manage approved reseller partnerships and their performance
@@ -124,13 +133,13 @@ const PartnershipsPage: React.FC = () => {
Export
-
setIsAddModalOpen(true)}
- className="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:scale-105 shadow-lg hover:shadow-xl"
- >
-
- New Partnership
-
+
setIsAddModalOpen(true)}
+ className="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:scale-105 shadow-lg hover:shadow-xl"
+ >
+
+ Add Reseller
+
@@ -140,14 +149,14 @@ const PartnershipsPage: React.FC = () => {
- Total Partnerships
+ Total Approved Resellers
- {mockPartnerships.length}
+ {approvedResellers.length}
-
+
@@ -156,30 +165,30 @@ const PartnershipsPage: React.FC = () => {
- Active Partnerships
+ Platinum Tier
- {mockPartnerships.filter(p => p.status === 'active').length}
+ {approvedResellers.filter(r => r.tier === 'platinum').length}
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Customers
+
+
+ {approvedResellers.reduce((sum, r) => sum + r.customers, 0)}
-
-
-
-
-
-
-
-
-
- Pending Approvals
-
-
- {mockPartnerships.filter(p => p.status === 'pending').length}
-
-
-
-
+
@@ -191,7 +200,7 @@ const PartnershipsPage: React.FC = () => {
Total Revenue
- {formatCurrency(mockPartnerships.reduce((sum, p) => sum + p.totalRevenue, 0))}
+ {formatCurrency(approvedResellers.reduce((sum, r) => sum + r.totalRevenue, 0))}
@@ -201,61 +210,6 @@ const PartnershipsPage: React.FC = () => {
- {/* Pending Approvals Section */}
- {mockPartnerships.filter(p => p.status === 'pending').length > 0 && (
-
-
-
- Pending Approvals
-
-
- {mockPartnerships.filter(p => p.status === 'pending').length} pending
-
-
-
- {mockPartnerships.filter(p => p.status === 'pending').map((partnership) => (
-
-
-
- {partnership.reseller}
-
-
- {partnership.tier.charAt(0).toUpperCase() + partnership.tier.slice(1)}
-
-
-
-
-
- {partnership.customers} customers
-
-
-
- {partnership.commissionRate}% commission
-
-
-
- {partnership.region}
-
-
-
-
-
- Approve
-
-
-
- Reject
-
-
-
- ))}
-
-
- )}
-
{/* Filters and Search */}
- {/* Partnerships Table */}
+ {/* Resellers Table */}
- All Partnerships
+ All Approved Resellers
@@ -342,7 +296,7 @@ const PartnershipsPage: React.FC = () => {
Commission
- Start Date
+ Last Active
Actions
@@ -350,74 +304,81 @@ const PartnershipsPage: React.FC = () => {
- {filteredPartnerships.map((partnership) => (
-
+ {filteredResellers.map((reseller) => (
+
-
-
- {partnership.reseller}
-
-
- {partnership.contactPerson}
-
-
- {partnership.contactEmail}
+
+
+
+
+ {reseller.name}
+
+
+ {reseller.email}
+
+
+ {reseller.phone}
+
- {partnership.status.charAt(0).toUpperCase() + partnership.status.slice(1)}
+ {reseller.status.charAt(0).toUpperCase() + reseller.status.slice(1)}
- {partnership.tier.charAt(0).toUpperCase() + partnership.tier.slice(1)}
+ {reseller.tier.charAt(0).toUpperCase() + reseller.tier.slice(1)}
- {formatCurrency(partnership.totalRevenue)}
+ {formatCurrency(reseller.totalRevenue)}
- {formatNumber(partnership.customers)}
+ {formatNumber(reseller.customers)}
- {partnership.commissionRate}%
+ {reseller.commissionRate}%
- {formatDate(partnership.startDate)}
+ {formatDate(reseller.lastActive)}
handleViewPartnership(partnership)}
+ onClick={() => handleViewReseller(reseller)}
className="p-2 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-all duration-200 transform hover:scale-110"
title="View Details"
>
handleEditPartnership(partnership)}
+ onClick={() => handleEditReseller(reseller)}
className="p-2 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-all duration-200 transform hover:scale-110"
- title="Edit Partnership"
+ title="Edit Reseller"
>
handleMailPartnership(partnership)}
+ onClick={() => handleMailReseller(reseller)}
className="p-2 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-all duration-200 transform hover:scale-110"
title="Send Email"
>
handleMoreOptions(partnership)}
+ onClick={() => handleMoreOptions(reseller)}
className="p-2 rounded-md hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-all duration-200 transform hover:scale-110"
title="More Options"
>
@@ -432,30 +393,30 @@ const PartnershipsPage: React.FC = () => {
- {/* Add Partnership Modal */}
+ {/* Add Reseller Modal */}
setIsAddModalOpen(false)}
- title="Add New Partnership"
+ title="Add New Reseller"
size="lg"
>
setIsAddModalOpen(false)}
/>
- {/* Partnership Detail Modal */}
+ {/* Reseller Detail Modal */}
setIsDetailModalOpen(false)}
- title="Partnership Details"
+ title="Reseller Details"
size="lg"
>
- {selectedPartnership && (
+ {selectedReseller && (
)}
@@ -463,4 +424,4 @@ const PartnershipsPage: React.FC = () => {
);
};
-export default PartnershipsPage;
\ No newline at end of file
+export default ApprovedResellersPage;
\ No newline at end of file
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx
index 09a5618..8f3e34c 100644
--- a/src/pages/Dashboard.tsx
+++ b/src/pages/Dashboard.tsx
@@ -8,6 +8,8 @@ import { formatCurrency, formatCurrencyDual, formatNumber, formatRelativeTime, f
import RevenueChart from '../components/charts/RevenueChart';
import ResellerPerformanceChart from '../components/charts/ResellerPerformanceChart';
import DualCurrencyDisplay from '../components/DualCurrencyDisplay';
+import NotificationBell from '../components/NotificationBell';
+import DraggableFeedback from '../components/DraggableFeedback';
import {
TrendingUp,
Users,
@@ -22,7 +24,8 @@ import {
Target,
Briefcase as BriefcaseIcon,
FileText as FileTextIcon,
- Package
+ Package,
+ MessageCircle
} from 'lucide-react';
import { cn } from '../utils/cn';
@@ -31,6 +34,8 @@ const Dashboard: React.FC = () => {
const navigate = useNavigate();
const { stats, recentActivities, quickActions } = useAppSelector((state) => state.dashboard);
const { user } = useAppSelector((state) => state.auth);
+ const [showFeedback, setShowFeedback] = React.useState(false);
+ const [feedbackKey, setFeedbackKey] = React.useState(0);
useEffect(() => {
// Initialize dashboard data
@@ -114,6 +119,7 @@ const Dashboard: React.FC = () => {
+
Commission Earned
{
- Active Partnerships
+ Active Resellers
Approved business agreements
@@ -202,7 +208,7 @@ const Dashboard: React.FC = () => {
- +3 new partnerships
+ +3 new resellers
@@ -396,6 +402,31 @@ const Dashboard: React.FC = () => {
+
+ {/* Draggable Feedback Component */}
+ {showFeedback && (
+ {
+ setShowFeedback(false);
+ setFeedbackKey(prev => prev + 1);
+ }}
+ />
+ )}
+
+ {/* Feedback Trigger Button */}
+ {!showFeedback && (
+ {
+ setShowFeedback(true);
+ setFeedbackKey(prev => prev + 1);
+ }}
+ className="fixed bottom-6 right-6 bg-emerald-600 hover:bg-emerald-700 text-white p-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-200 z-50"
+ title="Send Feedback"
+ >
+
+
+ )}
);
};
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx
index 5738c1e..e183d4b 100644
--- a/src/pages/Login.tsx
+++ b/src/pages/Login.tsx
@@ -20,6 +20,8 @@ import {
import { RootState } from '../store';
import { toggleTheme } from '../store/slices/themeSlice';
import { cn } from '../utils/cn';
+import ApprovalStatusModal from '../components/ApprovalStatusModal';
+import BlockedAccountModal from '../components/BlockedAccountModal';
const Login: React.FC = () => {
const [email, setEmail] = useState('');
@@ -28,6 +30,12 @@ const Login: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [rememberMe, setRememberMe] = useState(false);
+ const [showApprovalModal, setShowApprovalModal] = useState(false);
+ const [pendingUserEmail, setPendingUserEmail] = useState('');
+ const [pendingUserCompany, setPendingUserCompany] = useState('');
+ const [showInactiveModal, setShowInactiveModal] = useState(false);
+ const [inactiveUserEmail, setInactiveUserEmail] = useState('');
+ const [inactiveUserStatus, setInactiveUserStatus] = useState('');
const navigate = useNavigate();
const location = useLocation();
@@ -80,8 +88,30 @@ const Login: React.FC = () => {
navigate(redirectPath, { replace: true });
} catch (err: any) {
const errorMessage = err.message || 'Invalid email or password. Please try again.';
- setError(errorMessage);
- toast.error(errorMessage);
+
+ // Check if the error is related to account status issues
+ if (errorMessage.includes('Account access blocked') || errorMessage.includes('Status: inactive') || errorMessage.includes('Status: suspended') || errorMessage.includes('Status: pending')) {
+ // Extract status from error message
+ let status = 'inactive';
+ if (errorMessage.includes('Status: suspended')) {
+ status = 'suspended';
+ } else if (errorMessage.includes('Status: pending')) {
+ status = 'pending';
+ }
+
+ setInactiveUserEmail(email);
+ setInactiveUserStatus(status);
+ setShowInactiveModal(true);
+ setError('');
+ } else if (errorMessage.includes('pending approval') || errorMessage.includes('under review')) {
+ setPendingUserEmail(email);
+ setPendingUserCompany(''); // You might want to extract this from the error response
+ setShowApprovalModal(true);
+ setError('');
+ } else {
+ setError(errorMessage);
+ toast.error(errorMessage);
+ }
} finally {
setIsLoading(false);
}
@@ -242,30 +272,7 @@ const Login: React.FC = () => {
- {/* Divider */}
-
-
-
-
- Or continue with
-
-
-
- {/* Social Login Buttons */}
-
-
-
-
-
-
-
-
- Continue with Google
-
-
{/* Sign Up Link */}
@@ -288,6 +295,22 @@ const Login: React.FC = () => {
+
+ {/* Approval Status Modal */}
+ setShowApprovalModal(false)}
+ userEmail={pendingUserEmail}
+ companyName={pendingUserCompany}
+ />
+
+ {/* Blocked Account Modal */}
+ setShowInactiveModal(false)}
+ userEmail={inactiveUserEmail}
+ userStatus={inactiveUserStatus}
+ />
);
};
diff --git a/src/pages/ProductManagement.tsx b/src/pages/ProductManagement.tsx
index 5cf4728..86dfcb4 100644
--- a/src/pages/ProductManagement.tsx
+++ b/src/pages/ProductManagement.tsx
@@ -1,4 +1,15 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
+import { useAppDispatch, useAppSelector } from '../store/hooks';
+import {
+ fetchProducts,
+ fetchProductCategories,
+ fetchProductStats,
+ deleteProductById
+} from '../store/slices/productThunks';
+import { setFilters } from '../store/slices/productSlice';
+import { Product } from '../services/api';
+import ProductForm from '../components/forms/ProductForm';
+import toast from 'react-hot-toast';
import {
Package,
DollarSign,
@@ -16,530 +27,492 @@ import {
Star,
Zap,
Shield,
- Globe
+ Globe,
+ Trash2,
+ MoreVertical,
+ Grid3X3,
+ List,
+ Database,
+ Cpu,
+ Lock,
+ BarChart3,
+ Brain,
+ Wifi,
+ Link
} from 'lucide-react';
-interface Product {
- id: string;
- name: string;
- category: string;
- basePrice: number;
- currentPrice: number;
- margin: number;
- marginType: 'percentage' | 'fixed';
- stock: number;
- status: 'active' | 'inactive';
- description: string;
- featured?: boolean;
-}
-
-const mockProducts: Product[] = [
- {
- id: '1',
- name: 'Cloud Hosting Basic',
- category: 'Hosting',
- basePrice: 29.99,
- currentPrice: 39.99,
- margin: 33.34,
- marginType: 'percentage',
- stock: 100,
- status: 'active',
- description: 'Basic cloud hosting package with 10GB storage and 99.9% uptime guarantee',
- featured: true
- },
- {
- id: '2',
- name: 'Cloud Hosting Pro',
- category: 'Hosting',
- basePrice: 59.99,
- currentPrice: 79.99,
- margin: 33.34,
- marginType: 'percentage',
- stock: 50,
- status: 'active',
- description: 'Professional cloud hosting with 50GB storage and advanced features',
- featured: true
- },
- {
- id: '3',
- name: 'SSL Certificate',
- category: 'Security',
- basePrice: 49.99,
- currentPrice: 69.99,
- margin: 40.01,
- marginType: 'percentage',
- stock: 200,
- status: 'active',
- description: 'Standard SSL certificate for website security and trust'
- },
- {
- id: '4',
- name: 'Domain Registration',
- category: 'Domains',
- basePrice: 12.99,
- currentPrice: 19.99,
- margin: 53.89,
- marginType: 'percentage',
- stock: 1000,
- status: 'active',
- description: 'Annual domain registration service with free privacy protection'
- },
- {
- id: '5',
- name: 'Backup Service',
- category: 'Storage',
- basePrice: 19.99,
- currentPrice: 29.99,
- margin: 50.03,
- marginType: 'percentage',
- stock: 75,
- status: 'active',
- description: 'Automated backup service with 100GB storage and encryption'
- }
-];
-
const ProductManagement: React.FC = () => {
- const [products, setProducts] = useState(mockProducts);
+ const dispatch = useAppDispatch();
+ const {
+ products,
+ pagination,
+ filters,
+ isLoading,
+ error
+ } = useAppSelector((state) => state.product);
+
+ const [showProductForm, setShowProductForm] = useState(false);
const [selectedProduct, setSelectedProduct] = useState(null);
- const [searchTerm, setSearchTerm] = useState('');
- const [categoryFilter, setCategoryFilter] = useState('all');
- const [sortBy, setSortBy] = useState<'name' | 'price' | 'margin'>('name');
- const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
- const [showPricingModal, setShowPricingModal] = useState(false);
+ const [showFilters, setShowFilters] = useState(false);
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [deletingProductId, setDeletingProductId] = useState(null);
- const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))];
+ useEffect(() => {
+ dispatch(fetchProducts({}));
+ dispatch(fetchProductCategories());
+ dispatch(fetchProductStats());
+ }, [dispatch]);
- const filteredAndSortedProducts = products
- .filter(product => {
- const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- product.description.toLowerCase().includes(searchTerm.toLowerCase());
- const matchesCategory = categoryFilter === 'all' || product.category === categoryFilter;
- return matchesSearch && matchesCategory;
- })
- .sort((a, b) => {
- let aValue: string | number;
- let bValue: string | number;
-
- switch (sortBy) {
- case 'name':
- aValue = a.name;
- bValue = b.name;
- break;
- case 'price':
- aValue = a.currentPrice;
- bValue = b.currentPrice;
- break;
- case 'margin':
- aValue = a.margin;
- bValue = b.margin;
- break;
- default:
- aValue = a.name;
- bValue = b.name;
- }
-
- if (sortOrder === 'asc') {
- return aValue > bValue ? 1 : -1;
- } else {
- return aValue < bValue ? 1 : -1;
- }
- });
-
- const handlePricingUpdate = (productId: string, newMargin: number, marginType: 'percentage' | 'fixed') => {
- setProducts(prev => prev.map(product => {
- if (product.id === productId) {
- const newPrice = marginType === 'percentage'
- ? product.basePrice * (1 + newMargin / 100)
- : product.basePrice + newMargin;
-
- return {
- ...product,
- currentPrice: Math.round(newPrice * 100) / 100,
- margin: newMargin,
- marginType
- };
- }
- return product;
- }));
+ const handleFilterChange = (key: string, value: string) => {
+ dispatch(setFilters({ [key]: value }));
+ dispatch(fetchProducts({ ...filters, [key]: value, page: 1 }));
};
- const getMarginColor = (margin: number) => {
- if (margin >= 50) return 'text-emerald-600 bg-emerald-50 dark:bg-emerald-900/20 dark:text-emerald-400 border-emerald-200 dark:border-emerald-800';
- if (margin >= 30) return 'text-blue-600 bg-blue-50 dark:bg-blue-900/20 dark:text-blue-400 border-blue-200 dark:border-blue-800';
- if (margin >= 20) return 'text-amber-600 bg-amber-50 dark:bg-amber-900/20 dark:text-amber-400 border-amber-200 dark:border-amber-800';
- return 'text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400 border-red-200 dark:border-red-800';
+ const handlePageChange = (page: number) => {
+ dispatch(fetchProducts({ ...filters, page }));
+ };
+
+ const handleSort = (sortBy: string) => {
+ const sortOrder = filters.sortBy === sortBy && filters.sortOrder === 'ASC' ? 'DESC' : 'ASC';
+ dispatch(setFilters({ sortBy, sortOrder }));
+ dispatch(fetchProducts({ ...filters, sortBy, sortOrder }));
+ };
+
+ const handleDeleteProduct = async (productId: number) => {
+ setDeletingProductId(productId);
+ setIsDeleteModalOpen(true);
+ };
+
+ const confirmDelete = async () => {
+ if (!deletingProductId) return;
+
+ try {
+ await dispatch(deleteProductById(deletingProductId)).unwrap();
+ toast.success('Product deleted successfully');
+ setIsDeleteModalOpen(false);
+ setDeletingProductId(null);
+ } catch (error: any) {
+ toast.error(error.message || 'Failed to delete product');
+ }
+ };
+
+ const handleEditProduct = (product: Product) => {
+ setSelectedProduct(product);
+ setShowProductForm(true);
+ };
+
+ const handleCreateProduct = () => {
+ setSelectedProduct(null);
+ setShowProductForm(true);
+ };
+
+ const handleProductFormSuccess = () => {
+ dispatch(fetchProducts(filters));
};
const getCategoryIcon = (category: string) => {
- switch (category.toLowerCase()) {
- case 'hosting':
- return ;
- case 'security':
- return ;
- case 'domains':
- return ;
- case 'storage':
- return ;
+ switch (category) {
+ case 'cloud_storage':
+ return ;
+ case 'cloud_computing':
+ return ;
+ case 'cybersecurity':
+ return ;
+ case 'data_analytics':
+ return ;
+ case 'ai_ml':
+ return ;
+ case 'iot':
+ return ;
+ case 'blockchain':
+ return ;
default:
- return ;
+ return ;
}
};
const getCategoryColor = (category: string) => {
- switch (category.toLowerCase()) {
- case 'hosting':
- return 'bg-gradient-to-r from-blue-500 to-cyan-500';
- case 'security':
- return 'bg-gradient-to-r from-amber-500 to-orange-500';
- case 'domains':
- return 'bg-gradient-to-r from-purple-500 to-pink-500';
- case 'storage':
- return 'bg-gradient-to-r from-red-500 to-pink-500';
+ switch (category) {
+ case 'cloud_storage':
+ return 'text-blue-600 bg-blue-100 dark:bg-blue-900';
+ case 'cloud_computing':
+ return 'text-purple-600 bg-purple-100 dark:bg-purple-900';
+ case 'cybersecurity':
+ return 'text-red-600 bg-red-100 dark:bg-red-900';
+ case 'data_analytics':
+ return 'text-green-600 bg-green-100 dark:bg-green-900';
+ case 'ai_ml':
+ return 'text-orange-600 bg-orange-100 dark:bg-orange-900';
+ case 'iot':
+ return 'text-indigo-600 bg-indigo-100 dark:bg-indigo-900';
+ case 'blockchain':
+ return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
default:
- return 'bg-gradient-to-r from-gray-500 to-gray-600';
+ return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
}
};
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'active':
+ return 'text-green-600 bg-green-100 dark:bg-green-900';
+ case 'inactive':
+ return 'text-red-600 bg-red-100 dark:bg-red-900';
+ case 'draft':
+ return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
+ case 'discontinued':
+ return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
+ default:
+ return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
+ }
+ };
+
+ if (error) {
+ return (
+
+ );
+ }
+
return (
{/* Header */}
-
+
-
- Product & Pricing Management
-
-
- Manage your product catalog and customize pricing strategies
-
+
Product Management
+
Manage your product catalog and pricing
-
-
- Add Product
-
-
-
- {/* Stats Cards */}
-
-
-
-
-
- Total Products
-
-
- Available in catalog
-
-
- {products.length}
-
-
-
-
-
-
-
-
-
-
- Avg. Margin
-
-
- Average profit margin
-
-
- {Math.round(products.reduce((acc, p) => acc + p.margin, 0) / products.length)}%
-
-
-
-
-
-
-
-
-
-
-
-
- Active Products
-
-
- Currently available
-
-
- {products.filter(p => p.status === 'active').length}
-
-
-
-
-
-
-
-
-
-
-
-
- Categories
-
-
- Product categories
-
-
- {categories.length - 1}
-
-
-
-
-
+
+ {/* View Mode Toggle */}
+
+ setViewMode('grid')}
+ className={`p-2 rounded-md transition-colors ${
+ viewMode === 'grid'
+ ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
+ : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
+ }`}
+ >
+
+
+ setViewMode('list')}
+ className={`p-2 rounded-md transition-colors ${
+ viewMode === 'list'
+ ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
+ : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
+ }`}
+ >
+
+
+
+
+ Add Product
+
{/* Filters and Search */}
-
+
+ {/* Search */}
-
+ {/* Category Filter */}
+
setCategoryFilter(e.target.value)}
- className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
+ value={filters.category}
+ onChange={(e) => handleFilterChange('category', 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"
>
- {categories.map(category => (
-
- {category === 'all' ? 'All Categories' : category}
-
- ))}
+ All Categories
+ Cloud Storage
+ Cloud Computing
+ Cybersecurity
+ Data Analytics
+ AI & ML
+ IoT
+ Blockchain
+ Other
+
+ {/* Status Filter */}
+
{
- const [field, order] = e.target.value.split('-');
- setSortBy(field as 'name' | 'price' | 'margin');
- setSortOrder(order as 'asc' | 'desc');
- }}
- className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
+ value={filters.status}
+ onChange={(e) => handleFilterChange('status', 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"
>
- Name A-Z
- Name Z-A
- Price Low-High
- Price High-Low
- Margin Low-High
- Margin High-Low
+ All Status
+ Active
+ Inactive
+ Draft
+ Discontinued
- {/* Product Grid */}
-
- {filteredAndSortedProducts.map((product) => (
-
- {/* Product Header */}
-
-
-
-
- {getCategoryIcon(product.category)}
-
- {product.featured && (
-
-
+ {/* Products Display */}
+
+ {isLoading ? (
+
+
+
Loading products...
+
+ ) : products.length === 0 ? (
+
+
+
No products found
+
Get started by creating your first product.
+
+ ) : viewMode === 'grid' ? (
+ /* Grid View */
+
+
+ {products.map((product) => (
+
+ {/* Product Header */}
+
+
+
+ {getCategoryIcon(product.category)}
+
+
+
{product.name}
+
{product.sku}
+
- )}
-
-
-
- {product.name}
-
-
-
- {product.category}
+
+ handleEditProduct(product)}
+ className="p-1 text-slate-400 hover:text-blue-600 dark:hover:text-blue-400"
+ >
+
+
+ handleDeleteProduct(product.id)}
+ className="p-1 text-slate-400 hover:text-red-600 dark:hover:text-red-400"
+ >
+
+
+
+
+
+ {/* Product Details */}
+
+
+ Price:
+
+ ${(Number(product.price) || 0).toFixed(2)}
+
+
+
+ Commission:
+
+ {product.commissionRate}%
+
+
+
+ Stock:
+
+ {product.stockQuantity === -1 ? 'Unlimited' : product.stockQuantity}
+
+
+
+
+ {/* Status Badge */}
+
+
+ {product.status}
-
-
- {product.status}
-
-
-
- {/* Product Description */}
-
- {product.description}
-
-
- {/* Pricing Information */}
-
-
- Base Price:
- ${product.basePrice}
-
-
-
- Your Price:
-
- ${product.currentPrice}
-
-
-
-
- Profit Margin:
-
- {product.marginType === 'percentage' ? `${product.margin}%` : `$${product.margin}`}
-
-
-
-
- Available Stock:
- {product.stock}
-
-
-
- {/* Action Buttons */}
-
- {
- setSelectedProduct(product);
- setShowPricingModal(true);
- }}
- className="flex-1 btn btn-outline btn-sm"
- >
-
- Edit Pricing
-
-
-
- View Details
-
+ ))}
- ))}
+ ) : (
+ /* List View */
+
+
+
+
+
+ Product
+
+
+ Category
+
+
+ Price
+
+
+ Commission
+
+
+ Status
+
+
+ Stock
+
+
+ Actions
+
+
+
+
+ {products.map((product) => (
+
+
+
+
+ {getCategoryIcon(product.category)}
+
+
+
+ {product.name}
+
+
+ {product.sku}
+
+
+
+
+
+
+ {getCategoryIcon(product.category)}
+ {product.category.replace('_', ' ')}
+
+
+
+
+ ${(Number(product.price) || 0).toFixed(2)}
+
+
+ {product.currency}
+
+
+
+
+ {product.commissionRate}%
+
+
+
+
+ {product.status.charAt(0).toUpperCase() + product.status.slice(1)}
+
+
+
+ {product.stockQuantity === -1 ? 'Unlimited' : product.stockQuantity}
+
+
+
+ handleEditProduct(product)}
+ className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
+ >
+
+
+ handleDeleteProduct(product.id)}
+ className="text-red-600 hover:text-red-900 dark:hover:text-red-400"
+ >
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Pagination */}
+ {pagination.totalPages > 1 && (
+
+
+
+ Showing {((pagination.currentPage - 1) * pagination.itemsPerPage) + 1} to{' '}
+ {Math.min(pagination.currentPage * pagination.itemsPerPage, pagination.totalItems)} of{' '}
+ {pagination.totalItems} results
+
+
+ handlePageChange(pagination.currentPage - 1)}
+ disabled={pagination.currentPage === 1}
+ className="px-3 py-1 text-sm border border-slate-300 dark:border-slate-600 rounded disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Previous
+
+
+ Page {pagination.currentPage} of {pagination.totalPages}
+
+ handlePageChange(pagination.currentPage + 1)}
+ disabled={pagination.currentPage === pagination.totalPages}
+ className="px-3 py-1 text-sm border border-slate-300 dark:border-slate-600 rounded disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Next
+
+
+
+
+ )}
- {/* Pricing Modal */}
- {showPricingModal && selectedProduct && (
-
-
-
-
-
- {getCategoryIcon(selectedProduct.category)}
-
-
-
-
- Edit Pricing
-
-
- {selectedProduct.name}
-
-
-
+ {/* Product Form Modal */}
+ {showProductForm && (
+
setShowProductForm(false)}
+ onSuccess={handleProductFormSuccess}
+ />
+ )}
-
-
-
- Margin Type
-
-
-
- setSelectedProduct({...selectedProduct, marginType: 'percentage'})}
- className="mr-3"
- />
-
-
Percentage
-
Add margin as %
-
-
-
- setSelectedProduct({...selectedProduct, marginType: 'fixed'})}
- className="mr-3"
- />
-
-
Fixed Amount
-
Add fixed $ amount
-
-
-
-
-
-
-
- Margin Value
-
-
- setSelectedProduct({...selectedProduct, margin: parseFloat(e.target.value) || 0})}
- className="w-full px-4 py-3 border border-secondary-300 dark:border-secondary-600 rounded-xl bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-300"
- step={selectedProduct.marginType === 'percentage' ? 0.01 : 0.01}
- min="0"
- />
-
- {selectedProduct.marginType === 'percentage' ? '%' : '$'}
-
-
-
-
-
-
- Base Price:
- ${selectedProduct.basePrice}
-
-
- New Price:
-
- ${selectedProduct.marginType === 'percentage'
- ? (selectedProduct.basePrice * (1 + selectedProduct.margin / 100)).toFixed(2)
- : (selectedProduct.basePrice + selectedProduct.margin).toFixed(2)
- }
-
-
-
-
-
-
+ {/* Delete Confirmation Modal */}
+ {isDeleteModalOpen && (
+
+
+
Confirm Delete
+
+ Are you sure you want to delete this product? This action cannot be undone.
+
+
setShowPricingModal(false)}
- className="flex-1 btn btn-outline btn-lg"
+ onClick={() => {
+ setIsDeleteModalOpen(false);
+ setDeletingProductId(null);
+ }}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
Cancel
{
- handlePricingUpdate(selectedProduct.id, selectedProduct.margin, selectedProduct.marginType);
- setShowPricingModal(false);
- }}
- className="flex-1 btn btn-primary btn-lg shadow-lg hover:shadow-xl transition-all duration-300"
+ onClick={confirmDelete}
+ className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
- Update Pricing
+ Delete Product
diff --git a/src/pages/Resellers/index.tsx b/src/pages/Resellers/index.tsx
index bfb2382..71fc6d4 100644
--- a/src/pages/Resellers/index.tsx
+++ b/src/pages/Resellers/index.tsx
@@ -1,5 +1,5 @@
-import React, { useState } from 'react';
-import { mockResellers } from '../../data/mockData';
+import React, { useState, useEffect } from 'react';
+import { useAppSelector } from '../../store/hooks';
import { formatCurrency, formatNumber, formatDate } from '../../utils/format';
import Modal from '../../components/Modal';
import AddResellerForm from '../../components/forms/AddResellerForm';
@@ -7,6 +7,8 @@ import EditResellerForm from '../../components/forms/EditResellerForm';
import MailComposeForm from '../../components/forms/MailComposeForm';
import MoreOptionsDropdown from '../../components/MoreOptionsDropdown';
import DetailView from '../../components/DetailView';
+import DraggableFeedback from '../../components/DraggableFeedback';
+import apiService from '../../services/api';
import {
Search,
Filter,
@@ -20,44 +22,259 @@ import {
TrendingUp,
Users,
DollarSign,
- Calendar
+ Calendar,
+ Grid3X3,
+ List,
+ CheckCircle,
+ XCircle,
+ Clock,
+ MessageCircle
} from 'lucide-react';
import { cn } from '../../utils/cn';
+import toast from 'react-hot-toast';
-const ResellersPage: React.FC = () => {
+interface Reseller {
+ id: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+ status: string;
+ tier?: string;
+ totalRevenue?: number;
+ customers?: number;
+ commissionRate?: number;
+ lastActive?: string;
+ region?: string;
+ company?: string;
+ createdAt: string;
+ approvedAt?: string;
+ rejectedAt?: string;
+ rejectionReason?: string;
+ phone?: string;
+ role?: string;
+ userType?: string;
+ permissions?: string[];
+ department?: string;
+ position?: string;
+ managerId?: number;
+ onboardingCompleted?: boolean;
+ channelPartnerId?: number;
+ resellerId?: number;
+ emailVerified?: boolean;
+}
+
+interface VendorDashboardStats {
+ totalResellers: number;
+ activeResellers: number;
+ pendingResellers: number;
+ totalRevenue: number;
+}
+
+const ResellerRequestsPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [tierFilter, setTierFilter] = useState('all');
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isMailModalOpen, setIsMailModalOpen] = useState(false);
- const [selectedReseller, setSelectedReseller] = useState
(null);
+ const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false);
+ const [newResellerData, setNewResellerData] = useState(null);
+ const [selectedReseller, setSelectedReseller] = useState(null);
const [showMoreOptions, setShowMoreOptions] = useState(null);
+ const [resellers, setResellers] = useState([]);
+ const [stats, setStats] = useState({
+ totalResellers: 0,
+ activeResellers: 0,
+ pendingResellers: 0,
+ totalRevenue: 0
+ });
+ const [loading, setLoading] = useState(true);
+ const [pagination, setPagination] = useState({
+ currentPage: 1,
+ totalPages: 1,
+ totalItems: 0,
+ itemsPerPage: 10
+ });
+ const [showFeedback, setShowFeedback] = useState(false);
+ const [feedbackKey, setFeedbackKey] = useState(0);
- const filteredResellers = mockResellers.filter(reseller => {
- const matchesSearch = reseller.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- reseller.email.toLowerCase().includes(searchTerm.toLowerCase());
+ const { user } = useAppSelector((state) => state.auth);
+
+ useEffect(() => {
+ fetchDashboardStats();
+ fetchResellers();
+ }, []);
+
+ useEffect(() => {
+ document.title = 'Reseller Requests - Cloudtopiaa';
+ }, []);
+
+ const fetchDashboardStats = async () => {
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/dashboard/stats`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ const data = await response.json();
+
+ if (data.success) {
+ setStats(data.data);
+ }
+ } catch (error) {
+ console.error('Error fetching dashboard stats:', error);
+ }
+ };
+
+ const fetchResellers = async (page = 1) => {
+ try {
+ setLoading(true);
+ const params = new URLSearchParams({
+ page: page.toString(),
+ limit: '10',
+ ...(statusFilter !== 'all' && { status: statusFilter }),
+ ...(searchTerm && { search: searchTerm })
+ });
+
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/resellers?${params}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ const data = await response.json();
+
+ if (data.success) {
+ // Map the API response to match our interface
+ const mappedResellers = data.data.map((reseller: any) => ({
+ id: reseller.id,
+ firstName: reseller.firstName || '',
+ lastName: reseller.lastName || '',
+ email: reseller.contactEmail || reseller.email || '',
+ status: reseller.status || 'pending',
+ tier: reseller.tier || 'silver',
+ totalRevenue: reseller.totalRevenue || 0,
+ customers: reseller.customers || 0,
+ commissionRate: reseller.commissionRate || 10,
+ lastActive: reseller.lastActive || reseller.updatedAt || reseller.createdAt,
+ region: reseller.region || 'Unknown',
+ company: reseller.companyName || reseller.company || '',
+ createdAt: reseller.createdAt,
+ approvedAt: reseller.approvedAt,
+ rejectedAt: reseller.rejectedAt,
+ rejectionReason: reseller.rejectionReason,
+ phone: reseller.contactPhone || reseller.phone || '',
+ role: reseller.role,
+ userType: reseller.userType,
+ permissions: reseller.permissions,
+ department: reseller.department,
+ position: reseller.position,
+ managerId: reseller.managerId,
+ onboardingCompleted: reseller.onboardingCompleted,
+ channelPartnerId: reseller.channelPartnerId,
+ resellerId: reseller.resellerId,
+ emailVerified: reseller.emailVerified
+ }));
+ setResellers(mappedResellers);
+ // Set default pagination since backend doesn't return pagination data
+ setPagination({
+ currentPage: page,
+ totalPages: 1,
+ totalItems: mappedResellers.length,
+ itemsPerPage: 10
+ });
+ }
+ } catch (error) {
+ console.error('Error fetching reseller applications:', error);
+ toast.error('Failed to fetch reseller applications');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleApproveReseller = async (resellerId: string) => {
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/resellers/${resellerId}/approve`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ toast.success('Application approved successfully');
+ fetchResellers();
+ fetchDashboardStats();
+ } else {
+ const errorData = await response.json();
+ toast.error(errorData.message || 'Failed to approve application');
+ }
+ } catch (error) {
+ console.error('Error approving application:', error);
+ toast.error('Failed to approve application');
+ }
+ };
+
+ const handleRejectReseller = async (resellerId: string, reason: string) => {
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/resellers/${resellerId}/reject`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ reason })
+ });
+
+ if (response.ok) {
+ toast.success('Application rejected successfully');
+ fetchResellers();
+ fetchDashboardStats();
+ } else {
+ const errorData = await response.json();
+ toast.error(errorData.message || 'Failed to reject application');
+ }
+ } catch (error) {
+ console.error('Error rejecting application:', error);
+ toast.error('Failed to reject application');
+ }
+ };
+
+ const filteredResellers = resellers.filter(reseller => {
+ // Only show pending and rejected applications, not approved ones
+ if (reseller.status === 'active' || reseller.status === 'approved') {
+ return false;
+ }
+
+ const matchesSearch = reseller.firstName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ reseller.lastName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ reseller.email?.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || reseller.status === statusFilter;
const matchesTier = tierFilter === 'all' || reseller.tier === tierFilter;
return matchesSearch && matchesStatus && matchesTier;
});
- const getStatusColor = (status: string) => {
+ const getStatusColor = (status?: string) => {
switch (status) {
case 'active':
return 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-300';
case 'pending':
return 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-300';
- case 'inactive':
+ case 'rejected':
return 'bg-danger-100 text-danger-800 dark:bg-danger-900 dark:text-danger-300';
+ case 'inactive':
+ return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
default:
return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
}
};
- const getTierColor = (tier: string) => {
+ const getTierColor = (tier?: string) => {
switch (tier) {
case 'platinum':
return 'bg-gradient-to-r from-yellow-400 to-yellow-600 text-white';
@@ -70,77 +287,118 @@ const ResellersPage: 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
+ const handleAddReseller = async (data: any) => {
+ try {
+ const response = await apiService.createReseller(data);
+
+ if (response.success) {
+ // Store the new reseller data and show success modal
+ setNewResellerData(response.data);
+ setIsSuccessModalOpen(true);
+ setIsAddModalOpen(false);
+
+ // Show success toast
+ toast.success('Reseller application added successfully! Check the details below.');
+
+ // Refresh the reseller applications list and stats
+ fetchResellers();
+ fetchDashboardStats();
+ } else {
+ toast.error(response.message || 'Failed to add reseller');
+ }
+ } catch (error) {
+ console.error('Error adding reseller:', error);
+ toast.error('Failed to add reseller. Please try again.');
+ }
};
- const handleViewReseller = (reseller: any) => {
+ const handleViewReseller = (reseller: Reseller) => {
setSelectedReseller(reseller);
setIsDetailModalOpen(true);
};
- const handleEditReseller = (reseller: any) => {
+ const handleEditReseller = (reseller: Reseller) => {
setSelectedReseller(reseller);
setIsEditModalOpen(true);
};
- const handleMailReseller = (reseller: any) => {
+ const handleMailReseller = (reseller: Reseller) => {
setSelectedReseller(reseller);
setIsMailModalOpen(true);
};
- const handleMoreOptions = (reseller: any) => {
+ const handleMoreOptions = (reseller: Reseller) => {
setShowMoreOptions(showMoreOptions === reseller.id ? null : reseller.id);
};
- const handleViewPerformance = (reseller: any) => {
- console.log('View performance for:', reseller.name);
- alert(`Viewing performance metrics for ${reseller.name}`);
+ const handleViewPerformance = (reseller: Reseller) => {
+ console.log('View performance for:', reseller);
+ setShowMoreOptions(null);
};
- const handleDownloadReport = (reseller: any) => {
- console.log('Download report for:', reseller.name);
- alert(`Downloading report for ${reseller.name}`);
+ const handleDownloadReport = (reseller: Reseller) => {
+ console.log('Download report for:', reseller);
+ setShowMoreOptions(null);
};
- const handleSendNotification = (reseller: any) => {
- console.log('Send notification to:', reseller.name);
- alert(`Sending notification to ${reseller.name}`);
+ const handleSendNotification = (reseller: Reseller) => {
+ console.log('Send notification to:', reseller);
+ setShowMoreOptions(null);
};
- const handleChangeTier = (reseller: any) => {
- console.log('Change tier for:', reseller.name);
- alert(`Changing tier for ${reseller.name}`);
+ const handleChangeTier = (reseller: Reseller) => {
+ console.log('Change tier for:', reseller);
+ setShowMoreOptions(null);
};
- const handleDeactivate = (reseller: any) => {
- console.log('Deactivate:', reseller.name);
- if (window.confirm(`Are you sure you want to deactivate ${reseller.name}?`)) {
- alert(`${reseller.name} has been deactivated`);
+ const handleDeactivate = async (reseller: Reseller) => {
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/resellers/${reseller.id}/deactivate`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ toast.success('Reseller deactivated successfully');
+ fetchResellers();
+ fetchDashboardStats();
+ } else {
+ const errorData = await response.json();
+ toast.error(errorData.message || 'Failed to deactivate reseller');
+ }
+ } catch (error) {
+ console.error('Error deactivating reseller:', error);
+ toast.error('Failed to deactivate reseller');
}
+ setShowMoreOptions(null);
};
- const handleDelete = (reseller: any) => {
- console.log('Delete:', reseller.name);
- if (window.confirm(`Are you sure you want to delete ${reseller.name}? This action cannot be undone.`)) {
- alert(`${reseller.name} has been deleted`);
- }
+ const handleDelete = (reseller: Reseller) => {
+ console.log('Delete:', reseller);
+ setShowMoreOptions(null);
};
const handleSendMail = (mailData: any) => {
- console.log('Sending mail:', mailData);
- alert('Email sent successfully!');
+ console.log('Send mail:', mailData);
setIsMailModalOpen(false);
+ toast.success('Email sent successfully');
};
const handleUpdateReseller = (updatedData: any) => {
console.log('Updating reseller:', updatedData);
- alert('Reseller updated successfully!');
setIsEditModalOpen(false);
+ toast.success('Reseller updated successfully');
+ };
+
+ const handleSearch = () => {
+ fetchResellers(1);
+ };
+
+ const handlePageChange = (page: number) => {
+ fetchResellers(page);
};
return (
@@ -149,34 +407,79 @@ const ResellersPage: React.FC = () => {
- Resellers
+ Reseller Requests
- Manage your reseller partnerships and performance
+ Review and manage pending and rejected reseller applications
-
setIsAddModalOpen(true)}
- className="btn btn-primary btn-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-300"
- >
-
- Add New Reseller
-
+
+ {/* View Mode Toggle */}
+
+ setViewMode('grid')}
+ className={`p-2 rounded-md transition-colors ${
+ viewMode === 'grid'
+ ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
+ }`}
+ >
+
+
+ setViewMode('list')}
+ className={`p-2 rounded-md transition-colors ${
+ viewMode === 'list'
+ ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
+ }`}
+ >
+
+
+
+
setIsAddModalOpen(true)}
+ className="btn btn-primary btn-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-300"
+ >
+
+ Add Reseller Request
+
+
+
{/* Stats Cards */}
-
+
- Total Resellers
+ Pending Requests
- {mockResellers.length}
+ {stats.pendingResellers}
- Currently active partner companies
+ Awaiting approval
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Requests
+
+
+ {stats.pendingResellers + (stats.totalResellers - stats.activeResellers - stats.pendingResellers)}
+
+
+ Pending + Rejected applications
@@ -189,55 +492,36 @@ const ResellersPage: React.FC = () => {
- Active Resellers
+ Rejected Applications
- {mockResellers.filter(r => r.status === 'active').length}
+ {stats.totalResellers - stats.activeResellers - stats.pendingResellers}
- Approved and active partners
+ Declined applications
+
+
+
+
+
+
+
+
+
+
+
+
+ Approved This Month
+
+
+ {stats.activeResellers}
+
+
+ Recently approved partners
-
-
-
-
-
-
-
-
-
- Total Revenue
-
-
- {formatCurrency(mockResellers.reduce((sum, r) => sum + r.totalRevenue, 0))}
-
-
- Total sales from all resellers
-
-
-
-
-
-
-
-
-
-
-
-
- Pending Approvals
-
-
- {mockResellers.filter(r => r.status === 'pending').length}
-
-
- Awaiting admin review
-
-
-
-
+
@@ -251,9 +535,10 @@ const ResellersPage: React.FC = () => {
setSearchTerm(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
className="input pl-10 w-full"
/>
@@ -266,8 +551,8 @@ const ResellersPage: React.FC = () => {
className="input min-w-[140px]"
>
All Status
-
Active
Pending
+
Rejected
Inactive
@@ -290,12 +575,12 @@ const ResellersPage: React.FC = () => {
- {/* Resellers Table */}
+ {/* Reseller Applications Display */}
- Reseller Partners
+ Pending & Rejected Applications
@@ -304,135 +589,332 @@ const ResellersPage: React.FC = () => {
-
-
-
-
- Reseller
- Status
- Tier
- Revenue
- Customers
- Commission
- Last Active
- Actions
-
-
-
+ {loading ? (
+
+
+
Loading applications...
+
+ ) : filteredResellers.length === 0 ? (
+
+
+
No applications found
+
Get started by adding your first reseller application.
+
+ ) : viewMode === 'grid' ? (
+ /* Grid View */
+
+
{filteredResellers.map((reseller) => (
-
-
-
-
{
- const target = e.target as HTMLImageElement;
- target.style.display = 'none';
- target.nextElementSibling?.classList.remove('hidden');
- }}
- />
-
- {reseller.name.split(' ').map(n => n[0]).join('')}
+
+ {/* Applicant Header */}
+
+
+
+ {reseller.firstName?.charAt(0)}{reseller.lastName?.charAt(0)}
-
-
- {reseller.name}
-
-
- {reseller.email}
-
-
-
- {reseller.region}
-
+
+
+ {reseller.firstName} {reseller.lastName}
+
+
{reseller.email}
-
-
-
- {reseller.status.charAt(0).toUpperCase() + reseller.status.slice(1)}
-
-
-
-
- {reseller.tier.charAt(0).toUpperCase() + reseller.tier.slice(1)}
-
-
-
- {formatCurrency(reseller.totalRevenue)}
-
-
- {formatNumber(reseller.customers)}
-
-
- {reseller.commissionRate}%
-
-
- {formatDate(reseller.lastActive)}
-
-
-
-
+ {reseller.status === 'pending' && (
+ <>
+ handleApproveReseller(reseller.id)}
+ className="p-1.5 bg-green-100 hover:bg-green-200 dark:bg-green-900 dark:hover:bg-green-800 text-green-700 dark:text-green-300 rounded-md transition-colors"
+ title="Approve Application"
+ >
+
+
+ handleRejectReseller(reseller.id, 'Rejected by vendor')}
+ className="p-1.5 bg-red-100 hover:bg-red-200 dark:bg-red-900 dark:hover:bg-red-800 text-red-700 dark:text-red-300 rounded-md transition-colors"
+ title="Reject Application"
+ >
+
+
+ >
+ )}
+ {reseller.status === 'active' && (
+ <>
+ handleRejectReseller(reseller.id, 'Deactivated by vendor')}
+ className="p-1.5 bg-orange-100 hover:bg-orange-200 dark:bg-orange-900 dark:hover:bg-orange-800 text-orange-700 dark:text-orange-300 rounded-md transition-colors"
+ title="Deactivate Reseller"
+ >
+
+
+ handleDeactivate(reseller)}
+ className="p-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md transition-colors"
+ title="Make Inactive"
+ >
+
+
+ >
+ )}
+ handleViewReseller(reseller)}
- className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
+ className="p-1.5 bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-300 rounded-md transition-colors"
title="View Details"
>
-
+
- handleEditReseller(reseller)}
- className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
- title="Edit Reseller"
- >
-
-
- handleMailReseller(reseller)}
- className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
- title="Send Email"
- >
-
-
-
- handleMoreOptions(reseller)}
- className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
- title="More Options"
- >
-
-
- {showMoreOptions === reseller.id && (
- setShowMoreOptions(null)}
- />
- )}
-
-
-
+
+
+ {/* Reseller Details */}
+
+
+ Status:
+
+ {reseller.status?.charAt(0).toUpperCase() + reseller.status?.slice(1) || 'Unknown'}
+
+
+ {reseller.tier && (
+
+ Tier:
+
+ {reseller.tier?.charAt(0).toUpperCase() + reseller.tier?.slice(1) || 'Unknown'}
+
+
+ )}
+ {reseller.totalRevenue && (
+
+ Revenue:
+
+ ₹{formatNumber(reseller.totalRevenue)}
+
+
+ )}
+ {reseller.customers && (
+
+ Customers:
+
+ {reseller.customers}
+
+
+ )}
+
+
+ {/* Actions */}
+
+
+ handleEditReseller(reseller)}
+ className="text-sm text-blue-600 hover:text-blue-800"
+ >
+ Edit
+
+ handleMailReseller(reseller)}
+ className="text-sm text-green-600 hover:text-green-800"
+ >
+ Email
+
+
+
+
))}
-
-
-
+
+
+ ) : (
+ /* List View */
+
+
+
+
+ Applicant
+ Status
+ Tier
+ Revenue
+ Customers
+ Commission
+ Last Active
+ Actions
+
+
+
+ {filteredResellers.map((reseller) => (
+
+
+
+
+ {reseller.firstName?.charAt(0)}{reseller.lastName?.charAt(0)}
+
+
+
+ {reseller.firstName} {reseller.lastName}
+
+
+ {reseller.email}
+
+ {reseller.region && (
+
+
+ {reseller.region}
+
+ )}
+
+
+
+
+
+ {reseller.status?.charAt(0).toUpperCase() + reseller.status?.slice(1) || 'Unknown'}
+
+
+
+ {reseller.tier && (
+
+ {reseller.tier?.charAt(0).toUpperCase() + reseller.tier?.slice(1) || 'Unknown'}
+
+ )}
+
+
+ {reseller.totalRevenue ? `₹${formatNumber(reseller.totalRevenue)}` : '-'}
+
+
+ {reseller.customers || '-'}
+
+
+ {reseller.commissionRate ? `${reseller.commissionRate}%` : '-'}
+
+
+ {reseller.lastActive ? formatDate(reseller.lastActive) : '-'}
+
+
+
+ {reseller.status === 'pending' && (
+ <>
+
handleApproveReseller(reseller.id)}
+ className="p-2 rounded-lg hover:bg-green-100 dark:hover:bg-green-900 transition-all duration-200 hover:scale-110"
+ title="Approve Application"
+ >
+
+
+
handleRejectReseller(reseller.id, 'Rejected by vendor')}
+ className="p-2 rounded-lg hover:bg-red-100 dark:hover:bg-red-900 transition-all duration-200 hover:scale-110"
+ title="Reject Application"
+ >
+
+
+ >
+ )}
+ {reseller.status === 'active' && (
+ <>
+
handleRejectReseller(reseller.id, 'Deactivated by vendor')}
+ className="p-2 rounded-lg hover:bg-orange-100 dark:hover:bg-orange-900 transition-all duration-200 hover:scale-110"
+ title="Deactivate Reseller"
+ >
+
+
+
handleDeactivate(reseller)}
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
+ title="Make Inactive"
+ >
+
+
+ >
+ )}
+
handleViewReseller(reseller)}
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
+ title="View Details"
+ >
+
+
+
handleEditReseller(reseller)}
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
+ title="Edit Reseller"
+ >
+
+
+
handleMailReseller(reseller)}
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
+ title="Send Email"
+ >
+
+
+
+ handleMoreOptions(reseller)}
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
+ title="More Options"
+ >
+
+
+ {showMoreOptions === reseller.id && (
+ setShowMoreOptions(null)}
+ />
+ )}
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Pagination */}
+ {pagination.totalPages > 1 && (
+
+
+
+ Showing {((pagination.currentPage - 1) * pagination.itemsPerPage) + 1} to{' '}
+ {Math.min(pagination.currentPage * pagination.itemsPerPage, pagination.totalItems)} of{' '}
+ {pagination.totalItems} results
+
+
+ handlePageChange(pagination.currentPage - 1)}
+ disabled={pagination.currentPage === 1}
+ className="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Previous
+
+
+ Page {pagination.currentPage} of {pagination.totalPages}
+
+ handlePageChange(pagination.currentPage + 1)}
+ disabled={pagination.currentPage === pagination.totalPages}
+ className="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Next
+
+
+
+
+ )}
{/* Add Reseller Modal */}
@@ -494,8 +976,140 @@ const ResellersPage: React.FC = () => {
/>
)}
+
+ {/* Success Modal - Show Reseller Details */}
+
setIsSuccessModalOpen(false)}
+ title="Reseller Created Successfully!"
+ size="md"
+ >
+ {newResellerData && (
+
+
+
+
+
+
+ Account Created Successfully
+
+
+
The reseller account has been created and is now active.
+
+
+
+
+
+
+
Reseller Details
+
+
+
Name:
+
+ {newResellerData.firstName} {newResellerData.lastName}
+
+
+
+
Email:
+
{newResellerData.email}
+
+
+
Phone:
+
{newResellerData.phone}
+
+
+
User Type:
+
+ {newResellerData.userType?.replace('_', ' ')}
+
+
+
+
Region:
+
{newResellerData.region}
+
+
+
Business Type:
+
{newResellerData.businessType}
+
+
+
+
+
+
+
+
+
+ Important: Temporary Password
+
+
+
The reseller will need this temporary password to log in for the first time:
+
+ {newResellerData.tempPassword}
+
+
+ Please share this password securely with the reseller. They should change it upon first login.
+
+
+
+
+
+
+
+ setIsSuccessModalOpen(false)}
+ className="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors"
+ >
+ Close
+
+ {
+ // Copy password to clipboard
+ navigator.clipboard.writeText(newResellerData.tempPassword);
+ toast.success('Password copied to clipboard!');
+ }}
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
+ >
+ Copy Password
+
+
+
+ )}
+
+
+ {/* Draggable Feedback Component */}
+ {showFeedback && (
+
{
+ setShowFeedback(false);
+ setFeedbackKey(prev => prev + 1);
+ }}
+ />
+ )}
+
+ {/* Feedback Trigger Button */}
+ {!showFeedback && (
+ {
+ setShowFeedback(true);
+ setFeedbackKey(prev => prev + 1);
+ }}
+ className="fixed bottom-6 right-6 bg-emerald-600 hover:bg-emerald-700 text-white p-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-200 z-50"
+ title="Send Feedback"
+ >
+
+
+ )}
);
};
-export default ResellersPage;
\ No newline at end of file
+export default ResellerRequestsPage;
\ No newline at end of file
diff --git a/src/pages/Signup.tsx b/src/pages/Signup.tsx
index ed6d27b..c4596cf 100644
--- a/src/pages/Signup.tsx
+++ b/src/pages/Signup.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../store/hooks';
import { registerUser } from '../store/slices/authThunks';
@@ -15,151 +15,341 @@ import {
Sun,
Moon,
ArrowRight,
+ ArrowLeft,
AlertCircle,
Cloud,
- Zap
+ Zap,
+ MapPin,
+ Globe,
+ FileText,
+ Briefcase,
+ Users,
+ TrendingUp,
+ Calendar,
+ Hash,
+ CheckCircle,
+ Circle
} from 'lucide-react';
import { useAppSelector } from '../store/hooks';
import { RootState } from '../store';
import { toggleTheme } from '../store/slices/themeSlice';
import { cn } from '../utils/cn';
+interface FormData {
+ firstName: string;
+ lastName: string;
+ email: string;
+ password: string;
+ confirmPassword: string;
+ phone: string;
+ company: string;
+ companyType: 'corporation' | 'llc' | 'partnership' | 'sole_proprietorship' | 'other' | '';
+ registrationNumber: string;
+ gstNumber: string;
+ panNumber: string;
+ address: string;
+ website: string;
+ businessLicense: string;
+ taxId: string;
+ industry: string;
+ yearsInBusiness: string;
+ annualRevenue: string;
+ employeeCount: string;
+}
+
const Signup: React.FC = () => {
- const [formData, setFormData] = useState({
+ const [currentStep, setCurrentStep] = useState(1);
+ const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
phone: '',
- company: ''
+ company: '',
+ companyType: '',
+ registrationNumber: '',
+ gstNumber: '',
+ panNumber: '',
+ address: '',
+ website: '',
+ businessLicense: '',
+ taxId: '',
+ industry: '',
+ yearsInBusiness: '',
+ annualRevenue: '',
+ employeeCount: ''
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [agreedToTerms, setAgreedToTerms] = useState(false);
+ const [showCompanyTypeDropdown, setShowCompanyTypeDropdown] = useState(false);
+ const [showIndustryDropdown, setShowIndustryDropdown] = useState(false);
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme);
+ const totalSteps = 4;
+
+ const companyTypes = [
+ { value: 'corporation', label: 'Corporation' },
+ { value: 'llc', label: 'LLC' },
+ { value: 'partnership', label: 'Partnership' },
+ { value: 'sole_proprietorship', label: 'Sole Proprietorship' },
+ { value: 'other', label: 'Other' }
+ ];
+
+ const industries = [
+ 'Technology Services', 'IT Consulting', 'Cloud Services',
+ 'Software Development', 'Digital Marketing', 'E-commerce',
+ 'Healthcare IT', 'Financial Services', 'Education Technology',
+ 'Manufacturing', 'Retail', 'Telecommunications',
+ 'Energy', 'Transportation', 'Real Estate',
+ 'Media & Entertainment', 'Professional Services', 'Other'
+ ];
+
+ // Close dropdowns when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ const target = event.target as Element;
+ if (!target.closest('.dropdown-container')) {
+ setShowCompanyTypeDropdown(false);
+ setShowIndustryDropdown(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
+ setError(''); // Clear error when user starts typing
+ };
+
+ const validateStep = (step: number): boolean => {
+ setError('');
+
+ switch (step) {
+ case 1:
+ if (!validateName(formData.firstName)) {
+ setError('First name must be between 2 and 50 characters');
+ return false;
+ }
+ if (!validateName(formData.lastName)) {
+ setError('Last name must be between 2 and 50 characters');
+ return false;
+ }
+ if (!validateEmail(formData.email)) {
+ setError('Please enter a valid email address');
+ return false;
+ }
+ if (!validatePhoneNumber(formData.phone)) {
+ setError('Please enter a valid phone number');
+ return false;
+ }
+ break;
+
+ case 2:
+ if (!validatePassword(formData.password)) {
+ setError('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
+ return false;
+ }
+ if (formData.password !== formData.confirmPassword) {
+ setError('Passwords do not match');
+ return false;
+ }
+ if (!formData.company || formData.company.trim().length === 0) {
+ setError('Company name is required');
+ return false;
+ }
+ break;
+
+ case 3:
+ if (!formData.companyType) {
+ setError('Company type is required');
+ return false;
+ }
+ if (!formData.registrationNumber || formData.registrationNumber.trim().length === 0) {
+ setError('Registration number is required');
+ return false;
+ }
+ if (!formData.address || formData.address.trim().length === 0) {
+ setError('Business address is mandatory for vendor registration');
+ return false;
+ }
+
+ if (formData.address.trim().length < 10) {
+ setError(`Business address must be at least 10 characters long. Current length: ${formData.address.trim().length} characters`);
+ return false;
+ }
+ break;
+
+ case 4:
+ if (!formData.industry || formData.industry.trim().length === 0) {
+ setError('Industry is required');
+ return false;
+ }
+ if (!formData.yearsInBusiness || formData.yearsInBusiness.trim().length === 0) {
+ setError('Years in business is required');
+ return false;
+ }
+ if (!formData.employeeCount || formData.employeeCount.trim().length === 0) {
+ setError('Number of employees is required');
+ return false;
+ }
+ if (isNaN(Number(formData.yearsInBusiness)) || Number(formData.yearsInBusiness) < 0) {
+ setError('Years in business must be a valid number');
+ return false;
+ }
+ if (isNaN(Number(formData.employeeCount)) || Number(formData.employeeCount) < 1) {
+ setError('Number of employees must be at least 1');
+ return false;
+ }
+ if (formData.annualRevenue && (isNaN(Number(formData.annualRevenue)) || Number(formData.annualRevenue) < 0)) {
+ setError('Annual revenue must be a valid number');
+ return false;
+ }
+ if (!agreedToTerms) {
+ setError('Please agree to the terms and conditions');
+ return false;
+ }
+ break;
+ }
+
+ return true;
+ };
+
+ const nextStep = () => {
+ if (validateStep(currentStep)) {
+ setCurrentStep(prev => Math.min(prev + 1, totalSteps));
+ }
+ };
+
+ const prevStep = () => {
+ setCurrentStep(prev => Math.max(prev - 1, 1));
+ setError('');
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- setError('');
-
- // Validation
- if (!validateName(formData.firstName)) {
- setError('First name must be between 2 and 50 characters');
- return;
- }
-
- if (!validateName(formData.lastName)) {
- setError('Last name must be between 2 and 50 characters');
- return;
- }
-
- if (!validateEmail(formData.email)) {
- setError('Please enter a valid email address');
- return;
- }
-
- if (!validatePassword(formData.password)) {
- setError('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
- return;
- }
-
- if (formData.password !== formData.confirmPassword) {
- setError('Passwords do not match');
- return;
- }
-
- if (formData.phone && !validatePhoneNumber(formData.phone)) {
- setError('Please enter a valid phone number (7-15 digits, optional + prefix)');
- return;
- }
-
- if (!agreedToTerms) {
- setError('Please agree to the terms and conditions');
+
+ if (!validateStep(currentStep)) {
return;
}
setIsLoading(true);
- try {
- await dispatch(registerUser({
- firstName: formData.firstName,
- lastName: formData.lastName,
- email: formData.email,
- password: formData.password,
- phone: formData.phone,
- company: formData.company,
- role: 'channel_partner_admin',
- userType: 'channel_partner'
- })).unwrap();
+ try {
+ // Prepare the registration data, only including fields with actual values
+ const registrationData: any = {
+ firstName: formData.firstName,
+ lastName: formData.lastName,
+ email: formData.email,
+ password: formData.password,
+ phone: formData.phone,
+ company: formData.company,
+ // Vendor-specific fields
+ companyType: formData.companyType || undefined,
+ registrationNumber: formData.registrationNumber,
+ address: formData.address,
+ industry: formData.industry,
+ yearsInBusiness: formData.yearsInBusiness,
+ employeeCount: formData.employeeCount,
+ role: 'channel_partner_admin',
+ userType: 'channel_partner'
+ };
- // Navigate to login page with success message
- navigate('/login', {
- state: {
- message: 'Registration successful! You can now login.'
- }
- });
- } catch (err: any) {
- const errorMessage = err.message || 'An error occurred during signup. Please try again.';
- setError(errorMessage);
- toast.error(errorMessage);
- } finally {
- setIsLoading(false);
+ // Only add optional fields if they have values
+ if (formData.gstNumber && formData.gstNumber.trim()) {
+ registrationData.gstNumber = formData.gstNumber;
}
+ if (formData.panNumber && formData.panNumber.trim()) {
+ registrationData.panNumber = formData.panNumber;
+ }
+ if (formData.website && formData.website.trim()) {
+ registrationData.website = formData.website;
+ }
+ if (formData.businessLicense && formData.businessLicense.trim()) {
+ registrationData.businessLicense = formData.businessLicense;
+ }
+ if (formData.taxId && formData.taxId.trim()) {
+ registrationData.taxId = formData.taxId;
+ }
+ if (formData.annualRevenue && formData.annualRevenue.trim()) {
+ registrationData.annualRevenue = formData.annualRevenue;
+ }
+
+ await dispatch(registerUser(registrationData)).unwrap();
+
+ navigate('/login', {
+ state: {
+ message: 'Registration successful! You can now login.'
+ }
+ });
+ } catch (err: any) {
+ const errorMessage = err.message || 'An error occurred during signup. Please try again.';
+ setError(errorMessage);
+ toast.error(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
};
const handleThemeToggle = () => {
dispatch(toggleTheme());
};
+ const renderStepIndicator = () => {
return (
-
- {/* Theme Toggle */}
-
- {theme === 'dark' ? (
-
- ) : (
-
- )}
-
-
-
- {/* Logo and Header */}
-
-
-
-
-
-
-
+
+ {Array.from({ length: totalSteps }, (_, index) => (
+
+
index + 1
+ ? "bg-green-500 border-green-500 text-white"
+ : currentStep === index + 1
+ ? "bg-blue-500 border-blue-500 text-white"
+ : "bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-500"
+ )}>
+ {currentStep > index + 1 ? (
+
+ ) : (
+ {index + 1}
+ )}
+ {index < totalSteps - 1 && (
+
index + 1 ? "bg-green-500" : "bg-slate-300 dark:bg-slate-600"
+ )} />
+ )}
+ ))}
-
- Join Cloudtopiaa Connect
-
+ );
+ };
+
+ const renderStepContent = () => {
+ switch (currentStep) {
+ case 1:
+ return (
+
+
+
+ Personal Information
+
- Partner with us to offer next-gen cloud services.
+ Let's start with your basic information
- {/* Signup Form */}
-
-
- {/* Name Fields */}
@@ -202,7 +392,6 @@ const Signup: React.FC = () => {
- {/* Email and Phone */}
@@ -244,29 +433,21 @@ const Signup: React.FC = () => {
+
+ );
- {/* Company */}
+ case 2:
+ return (
+
-
- Company Name
-
-
-
-
-
-
handleInputChange('company', e.target.value)}
- className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
- placeholder="Enter company name"
- required
- />
-
+
+ Account Security
+
+
+ Create a secure password and provide company information
+
- {/* Password Fields */}
@@ -331,6 +512,317 @@ const Signup: React.FC = () => {
+
+
+ Company Name
+
+
+
+
+
+
handleInputChange('company', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter company name"
+ required
+ />
+
+
+
+ );
+
+ case 3:
+ return (
+
+
+
+ Business Details
+
+
+ Provide your business registration and contact information
+
+
+
+
+
+
+ Company Type
+
+
+
setShowCompanyTypeDropdown(!showCompanyTypeDropdown)}
+ className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ >
+
+
+
+ {formData.companyType ? companyTypes.find(t => t.value === formData.companyType)?.label : 'Select company type'}
+
+
+
+
+
+
+
+ {showCompanyTypeDropdown && (
+
+ {companyTypes.map((type) => (
+ {
+ handleInputChange('companyType', type.value);
+ setShowCompanyTypeDropdown(false);
+ }}
+ className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+ {type.label}
+
+ ))}
+
+ )}
+
+
+
+
+
+ Registration Number
+
+
+
+
+
+
handleInputChange('registrationNumber', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., 1234567890"
+ required
+ />
+
+
+
+
+
+
+
+ GST Number (Optional)
+
+
+
+
+
+
handleInputChange('gstNumber', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., 24ABCDE1234F1Z9"
+ />
+
+
+
+
+
+ PAN Number (Optional)
+
+
+
+
+
+
handleInputChange('panNumber', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., ABCDE1234F"
+ />
+
+
+
+
+
+
+
+ Business Address
+
+
+
+
+
+
handleInputChange('address', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter complete business address (min. 10 characters)"
+ required
+ />
+
+
+
+ Minimum 10 characters required
+
+ = 10
+ ? "text-green-500"
+ : "text-slate-500 dark:text-slate-400"
+ )}>
+ {formData.address.length}/10
+
+
+
+
+
+
+ Website (Optional)
+
+
+
+
+
+
handleInputChange('website', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="https://example.com"
+ />
+
+
+
+
+ );
+
+ case 4:
+ return (
+
+
+
+ Business Profile
+
+
+ Complete your business profile and agree to terms
+
+
+
+
+
+
+ Industry
+
+
+
setShowIndustryDropdown(!showIndustryDropdown)}
+ className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ >
+
+
+
+ {formData.industry || 'Select industry'}
+
+
+
+
+
+
+
+ {showIndustryDropdown && (
+
+ {industries.map((industry) => (
+ {
+ handleInputChange('industry', industry);
+ setShowIndustryDropdown(false);
+ }}
+ className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+ {industry}
+
+ ))}
+
+ )}
+
+
+
+
+
+ Years in Business
+
+
+
+
+
+
handleInputChange('yearsInBusiness', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., 5"
+ required
+ />
+
+
+
+
+
+
+
+ Annual Revenue (Optional)
+
+
+
+
+
+
handleInputChange('annualRevenue', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., 1000000"
+ />
+
+
+
+
+
+ Number of Employees
+
+
+
+
+
+
handleInputChange('employeeCount', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., 50"
+ required
+ />
+
+
+
+
{/* Terms and Conditions */}
{
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+ {/* Theme Toggle */}
+
+ {theme === 'dark' ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Logo and Header */}
+
+
+
+ Join Cloudtopiaa Connect
+
+
+ Partner with us to offer next-gen cloud services.
+
+
+
+ {/* Step Indicator */}
+ {renderStepIndicator()}
+
+ {/* Signup Form */}
+
+
+ {renderStepContent()}
{/* Error Message */}
{error && (
-
+
)}
- {/* Submit Button */}
+ {/* Navigation Buttons */}
+
+
+
+ Previous
+
+
+ {currentStep < totalSteps ? (
+
+ Next
+
+
+ ) : (
{isLoading ? (
) : (
@@ -381,6 +947,8 @@ const Signup: React.FC = () => {
)}
+ )}
+
{/* Sign In Link */}
diff --git a/src/pages/SignupStepwise.tsx b/src/pages/SignupStepwise.tsx
new file mode 100644
index 0000000..c58711c
--- /dev/null
+++ b/src/pages/SignupStepwise.tsx
@@ -0,0 +1,989 @@
+import React, { useState, useEffect } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import { useAppDispatch } from '../store/hooks';
+import { registerUser } from '../store/slices/authThunks';
+import toast from 'react-hot-toast';
+import { validatePhoneNumber, validateEmail, validatePassword, validateName } from '../utils/validation';
+import {
+ Eye,
+ EyeOff,
+ Mail,
+ Lock,
+ User,
+ Building,
+ Phone,
+ Sun,
+ Moon,
+ ArrowRight,
+ ArrowLeft,
+ AlertCircle,
+ Cloud,
+ Zap,
+ MapPin,
+ Globe,
+ FileText,
+ Briefcase,
+ Users,
+ TrendingUp,
+ Calendar,
+ Hash,
+ CheckCircle
+} from 'lucide-react';
+import { useAppSelector } from '../store/hooks';
+import { RootState } from '../store';
+import { toggleTheme } from '../store/slices/themeSlice';
+import { cn } from '../utils/cn';
+
+interface FormData {
+ firstName: string;
+ lastName: string;
+ email: string;
+ password: string;
+ confirmPassword: string;
+ phone: string;
+ company: string;
+ companyType: 'corporation' | 'llc' | 'partnership' | 'sole_proprietorship' | 'other' | '';
+ registrationNumber: string;
+ gstNumber: string;
+ panNumber: string;
+ address: string;
+ website: string;
+ businessLicense: string;
+ taxId: string;
+ industry: string;
+ yearsInBusiness: string;
+ annualRevenue: string;
+ employeeCount: string;
+}
+
+const SignupStepwise: React.FC = () => {
+ const [currentStep, setCurrentStep] = useState(1);
+ const [formData, setFormData] = useState
({
+ firstName: '',
+ lastName: '',
+ email: '',
+ password: '',
+ confirmPassword: '',
+ phone: '',
+ company: '',
+ companyType: '',
+ registrationNumber: '',
+ gstNumber: '',
+ panNumber: '',
+ address: '',
+ website: '',
+ businessLicense: '',
+ taxId: '',
+ industry: '',
+ yearsInBusiness: '',
+ annualRevenue: '',
+ employeeCount: ''
+ });
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [agreedToTerms, setAgreedToTerms] = useState(false);
+ const [showCompanyTypeDropdown, setShowCompanyTypeDropdown] = useState(false);
+ const [showIndustryDropdown, setShowIndustryDropdown] = useState(false);
+
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const { theme } = useAppSelector((state: RootState) => state.theme);
+
+ const totalSteps = 4;
+
+ const companyTypes = [
+ { value: 'corporation', label: 'Corporation' },
+ { value: 'llc', label: 'LLC' },
+ { value: 'partnership', label: 'Partnership' },
+ { value: 'sole_proprietorship', label: 'Sole Proprietorship' },
+ { value: 'other', label: 'Other' }
+ ];
+
+ const industries = [
+ 'Technology Services', 'IT Consulting', 'Cloud Services',
+ 'Software Development', 'Digital Marketing', 'E-commerce',
+ 'Healthcare IT', 'Financial Services', 'Education Technology',
+ 'Manufacturing', 'Retail', 'Telecommunications',
+ 'Energy', 'Transportation', 'Real Estate',
+ 'Media & Entertainment', 'Professional Services', 'Other'
+ ];
+
+ // Close dropdowns when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ const target = event.target as Element;
+ if (!target.closest('.dropdown-container')) {
+ setShowCompanyTypeDropdown(false);
+ setShowIndustryDropdown(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ const handleInputChange = (field: string, value: string) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ setError(''); // Clear error when user starts typing
+ };
+
+ const validateStep = (step: number): boolean => {
+ setError('');
+
+ switch (step) {
+ case 1:
+ if (!validateName(formData.firstName)) {
+ setError('First name must be between 2 and 50 characters');
+ return false;
+ }
+ if (!validateName(formData.lastName)) {
+ setError('Last name must be between 2 and 50 characters');
+ return false;
+ }
+ if (!validateEmail(formData.email)) {
+ setError('Please enter a valid email address');
+ return false;
+ }
+ if (!validatePhoneNumber(formData.phone)) {
+ setError('Please enter a valid phone number');
+ return false;
+ }
+ break;
+
+ case 2:
+ if (!validatePassword(formData.password)) {
+ setError('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
+ return false;
+ }
+ if (formData.password !== formData.confirmPassword) {
+ setError('Passwords do not match');
+ return false;
+ }
+ if (!formData.company || formData.company.trim().length === 0) {
+ setError('Company name is required');
+ return false;
+ }
+ break;
+
+ case 3:
+ if (!formData.companyType) {
+ setError('Company type is required');
+ return false;
+ }
+ if (!formData.registrationNumber || formData.registrationNumber.trim().length === 0) {
+ setError('Registration number is required');
+ return false;
+ }
+ if (!formData.address || formData.address.trim().length === 0) {
+ setError('Business address is required');
+ return false;
+ }
+ if (formData.address.trim().length < 10) {
+ setError(`Business address must be at least 10 characters long. Current length: ${formData.address.trim().length} characters`);
+ return false;
+ }
+ break;
+
+ case 4:
+ if (!formData.industry || formData.industry.trim().length === 0) {
+ setError('Industry is required');
+ return false;
+ }
+ if (!formData.yearsInBusiness || formData.yearsInBusiness.trim().length === 0) {
+ setError('Years in business is required');
+ return false;
+ }
+ if (!formData.employeeCount || formData.employeeCount.trim().length === 0) {
+ setError('Number of employees is required');
+ return false;
+ }
+ if (isNaN(Number(formData.yearsInBusiness)) || Number(formData.yearsInBusiness) < 0) {
+ setError('Years in business must be a valid number');
+ return false;
+ }
+ if (isNaN(Number(formData.employeeCount)) || Number(formData.employeeCount) < 1) {
+ setError('Number of employees must be at least 1');
+ return false;
+ }
+ if (formData.annualRevenue && (isNaN(Number(formData.annualRevenue)) || Number(formData.annualRevenue) < 0)) {
+ setError('Annual revenue must be a valid number');
+ return false;
+ }
+ if (!agreedToTerms) {
+ setError('Please agree to the terms and conditions');
+ return false;
+ }
+ break;
+ }
+
+ return true;
+ };
+
+ const nextStep = () => {
+ if (validateStep(currentStep)) {
+ setCurrentStep(prev => Math.min(prev + 1, totalSteps));
+ }
+ };
+
+ const prevStep = () => {
+ setCurrentStep(prev => Math.max(prev - 1, 1));
+ setError('');
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!validateStep(currentStep)) {
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ // Prepare the registration data, only including fields with actual values
+ const registrationData: any = {
+ firstName: formData.firstName,
+ lastName: formData.lastName,
+ email: formData.email,
+ password: formData.password,
+ phone: formData.phone,
+ company: formData.company,
+ companyType: formData.companyType || undefined,
+ registrationNumber: formData.registrationNumber,
+ address: formData.address,
+ industry: formData.industry,
+ yearsInBusiness: formData.yearsInBusiness,
+ employeeCount: formData.employeeCount,
+ role: 'channel_partner_admin',
+ userType: 'channel_partner'
+ };
+
+ // Only add optional fields if they have values
+ if (formData.gstNumber && formData.gstNumber.trim()) {
+ registrationData.gstNumber = formData.gstNumber;
+ }
+ if (formData.panNumber && formData.panNumber.trim()) {
+ registrationData.panNumber = formData.panNumber;
+ }
+ if (formData.website && formData.website.trim()) {
+ registrationData.website = formData.website;
+ }
+ if (formData.businessLicense && formData.businessLicense.trim()) {
+ registrationData.businessLicense = formData.businessLicense;
+ }
+ if (formData.taxId && formData.taxId.trim()) {
+ registrationData.taxId = formData.taxId;
+ }
+ if (formData.annualRevenue && formData.annualRevenue.trim()) {
+ registrationData.annualRevenue = formData.annualRevenue;
+ }
+
+ await dispatch(registerUser(registrationData)).unwrap();
+
+ navigate('/login', {
+ state: {
+ message: 'Registration successful! You can now login.'
+ }
+ });
+ } catch (err: any) {
+ const errorMessage = err.message || 'An error occurred during signup. Please try again.';
+ setError(errorMessage);
+ toast.error(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleThemeToggle = () => {
+ dispatch(toggleTheme());
+ };
+
+ const renderStepIndicator = () => {
+ return (
+
+ {Array.from({ length: totalSteps }, (_, index) => (
+
+
index + 1
+ ? "bg-green-500 border-green-500 text-white"
+ : currentStep === index + 1
+ ? "bg-blue-500 border-blue-500 text-white"
+ : "bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-500"
+ )}>
+ {currentStep > index + 1 ? (
+
+ ) : (
+ {index + 1}
+ )}
+
+ {index < totalSteps - 1 && (
+
index + 1 ? "bg-green-500" : "bg-slate-300 dark:bg-slate-600"
+ )} />
+ )}
+
+ ))}
+
+ );
+ };
+
+ const renderStepContent = () => {
+ switch (currentStep) {
+ case 1:
+ return (
+
+
+
+ Personal Information
+
+
+ Let's start with your basic information
+
+
+
+
+
+
+ First Name
+
+
+
+
+
+
handleInputChange('firstName', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter first name"
+ required
+ />
+
+
+
+
+
+ Last Name
+
+
+
+
+
+
handleInputChange('lastName', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter last name"
+ required
+ />
+
+
+
+
+
+
+
+ Email Address
+
+
+
+
+
+
handleInputChange('email', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter email address"
+ required
+ />
+
+
+
+
+
+ Phone Number
+
+
+
+
handleInputChange('phone', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter phone number"
+ required
+ />
+
+
+
+
+ );
+
+ case 2:
+ return (
+
+
+
+ Account Security
+
+
+ Create a secure password and provide company information
+
+
+
+
+
+
+ Password
+
+
+
+
+
+
handleInputChange('password', e.target.value)}
+ className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="Create password"
+ required
+ />
+
setShowPassword(!showPassword)}
+ className="absolute inset-y-0 right-0 pr-3 flex items-center"
+ >
+ {showPassword ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ Confirm Password
+
+
+
+
+
+
handleInputChange('confirmPassword', e.target.value)}
+ className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="Confirm password"
+ required
+ />
+
setShowConfirmPassword(!showConfirmPassword)}
+ className="absolute inset-y-0 right-0 pr-3 flex items-center"
+ >
+ {showConfirmPassword ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ Company Name
+
+
+
+
+
+
handleInputChange('company', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter company name"
+ required
+ />
+
+
+
+ );
+
+ case 3:
+ return (
+
+
+
+ Business Details
+
+
+ Provide your business registration and contact information
+
+
+
+
+
+
+ Company Type
+
+
+
setShowCompanyTypeDropdown(!showCompanyTypeDropdown)}
+ className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ >
+
+
+
+ {formData.companyType ? companyTypes.find(t => t.value === formData.companyType)?.label : 'Select company type'}
+
+
+
+
+
+
+
+ {showCompanyTypeDropdown && (
+
+ {companyTypes.map((type) => (
+ {
+ handleInputChange('companyType', type.value);
+ setShowCompanyTypeDropdown(false);
+ }}
+ className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+ {type.label}
+
+ ))}
+
+ )}
+
+
+
+
+
+ Registration Number
+
+
+
+
+
+
handleInputChange('registrationNumber', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., 1234567890"
+ required
+ />
+
+
+
+
+
+
+
+ GST Number (Optional)
+
+
+
+
+
+
handleInputChange('gstNumber', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., 24ABCDE1234F1Z9"
+ />
+
+
+
+
+
+ PAN Number (Optional)
+
+
+
+
+
+
handleInputChange('panNumber', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., ABCDE1234F"
+ />
+
+
+
+
+
+
+
+ Business Address
+
+
+
+
+
+
handleInputChange('address', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter complete business address (min. 10 characters)"
+ required
+ />
+
+
+
+ Minimum 10 characters required
+
+ = 10
+ ? "text-green-500"
+ : "text-slate-500 dark:text-slate-400"
+ )}>
+ {formData.address.length}/10
+
+
+
+
+
+
+ Website (Optional)
+
+
+
+
+
+
handleInputChange('website', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="https://example.com"
+ />
+
+
+
+
+ );
+
+ case 4:
+ return (
+
+
+
+ Business Profile
+
+
+ Complete your business profile and agree to terms
+
+
+
+
+
+
+ Industry
+
+
+
setShowIndustryDropdown(!showIndustryDropdown)}
+ className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ >
+
+
+
+ {formData.industry || 'Select industry'}
+
+
+
+
+
+
+
+ {showIndustryDropdown && (
+
+ {industries.map((industry) => (
+ {
+ handleInputChange('industry', industry);
+ setShowIndustryDropdown(false);
+ }}
+ className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+ {industry}
+
+ ))}
+
+ )}
+
+
+
+
+
+ Years in Business
+
+
+
+
+
+
handleInputChange('yearsInBusiness', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., 5"
+ required
+ />
+
+
+
+
+
+
+
+ Annual Revenue (Optional)
+
+
+
+
+
+
handleInputChange('annualRevenue', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., 1000000"
+ />
+
+
+
+
+
+ Number of Employees
+
+
+
+
+
+
handleInputChange('employeeCount', e.target.value)}
+ className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
+ placeholder="e.g., 50"
+ required
+ />
+
+
+
+
+ {/* Terms and Conditions */}
+
+ setAgreedToTerms(e.target.checked)}
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-slate-300 dark:border-slate-600 rounded mt-1"
+ />
+
+ I agree to the{' '}
+
+ Terms and Conditions
+ {' '}
+ and{' '}
+
+ Privacy Policy
+
+
+
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+ {/* Theme Toggle */}
+
+ {theme === 'dark' ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Logo and Header */}
+
+
+
+ Join Cloudtopiaa Connect
+
+
+ Partner with us to offer next-gen cloud services.
+
+
+
+ {/* Step Indicator */}
+ {renderStepIndicator()}
+
+ {/* Signup Form */}
+
+
+ {renderStepContent()}
+
+ {/* Error Message */}
+ {error && (
+
+ )}
+
+ {/* Navigation Buttons */}
+
+
+
+ Previous
+
+
+ {currentStep < totalSteps ? (
+
+ Next
+
+
+ ) : (
+
+ {isLoading ? (
+
+
+ Creating account...
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+ {/* Sign In Link */}
+
+
+ Already have an account?{' '}
+
+ Sign in here
+
+
+
+
+ {/* Switch to Reseller */}
+
+
+ Are you a Reseller?{' '}
+
+ Sign up here
+
+
+
+
+
+ {/* Footer */}
+
+
+ © 2025 Cloudtopiaa. All rights reserved.
+
+
+
+
+ );
+};
+
+export default SignupStepwise;
\ No newline at end of file
diff --git a/src/pages/Training.tsx b/src/pages/Training.tsx
index d865c80..9d400ac 100644
--- a/src/pages/Training.tsx
+++ b/src/pages/Training.tsx
@@ -70,6 +70,8 @@ const Training: React.FC = () => {
const [selectedModuleForAdd, setSelectedModuleForAdd] = useState
('');
const [showAnalytics, setShowAnalytics] = useState(false);
const [currentView, setCurrentView] = useState<'training' | 'admin' | 'analytics'>('training');
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [deletingItem, setDeletingItem] = useState<{ type: 'module' | 'video' | 'material', id: string, moduleId?: string } | null>(null);
const trainingModules: TrainingModule[] = [
{
@@ -319,23 +321,43 @@ const Training: React.FC = () => {
};
const handleDeleteModule = (moduleId: string) => {
- if (window.confirm('Are you sure you want to delete this training module? This action cannot be undone.')) {
- // Handle module deletion
- console.log('Deleting module:', moduleId);
- }
+ setDeletingItem({ type: 'module', id: moduleId });
+ setIsDeleteModalOpen(true);
};
const handleDeleteVideo = (moduleId: string, videoId: string) => {
- if (window.confirm('Are you sure you want to delete this video?')) {
- // Handle video deletion
- console.log('Deleting video:', videoId, 'from module:', moduleId);
- }
+ setDeletingItem({ type: 'video', id: videoId, moduleId });
+ setIsDeleteModalOpen(true);
};
const handleDeleteMaterial = (moduleId: string, materialId: string) => {
- if (window.confirm('Are you sure you want to delete this material?')) {
- // Handle material deletion
- console.log('Deleting material:', materialId, 'from module:', moduleId);
+ setDeletingItem({ type: 'material', id: materialId, moduleId });
+ setIsDeleteModalOpen(true);
+ };
+
+ const confirmDelete = async () => {
+ if (!deletingItem) return;
+
+ try {
+ if (deletingItem.type === 'module') {
+ // Handle module deletion
+ console.log('Deleting module:', deletingItem.id);
+ // Here you would typically make an API call to delete the module
+ } else if (deletingItem.type === 'video') {
+ // Handle video deletion
+ console.log('Deleting video:', deletingItem.id, 'from module:', deletingItem.moduleId);
+ // Here you would typically make an API call to delete the video
+ } else if (deletingItem.type === 'material') {
+ // Handle material deletion
+ console.log('Deleting material:', deletingItem.id, 'from module:', deletingItem.moduleId);
+ // Here you would typically make an API call to delete the material
+ }
+
+ // Close modal and reset state
+ setIsDeleteModalOpen(false);
+ setDeletingItem(null);
+ } catch (error) {
+ console.error('Error deleting item:', error);
}
};
@@ -358,10 +380,8 @@ const Training: React.FC = () => {
};
const handleModuleDelete = (id: string) => {
- if (window.confirm('Are you sure you want to delete this module? This action cannot be undone.')) {
- console.log('Deleting module:', id);
- // Here you would typically update your state or make an API call
- }
+ setDeletingItem({ type: 'module', id });
+ setIsDeleteModalOpen(true);
};
const handleVideoAdd = (moduleId: string, video: Omit) => {
@@ -380,10 +400,8 @@ const Training: React.FC = () => {
};
const handleVideoDelete = (moduleId: string, videoId: string) => {
- if (window.confirm('Are you sure you want to delete this video?')) {
- console.log('Deleting video:', moduleId, videoId);
- // Here you would typically update your state or make an API call
- }
+ setDeletingItem({ type: 'video', id: videoId, moduleId });
+ setIsDeleteModalOpen(true);
};
const handleMaterialAdd = (moduleId: string, material: Omit) => {
@@ -402,10 +420,8 @@ const Training: React.FC = () => {
};
const handleMaterialDelete = (moduleId: string, materialId: string) => {
- if (window.confirm('Are you sure you want to delete this material?')) {
- console.log('Deleting material:', moduleId, materialId);
- // Here you would typically update your state or make an API call
- }
+ setDeletingItem({ type: 'material', id: materialId, moduleId });
+ setIsDeleteModalOpen(true);
};
const handleExportReport = (type: 'pdf' | 'excel' | 'csv') => {
@@ -1134,6 +1150,37 @@ const Training: React.FC = () => {
)}
+
+ {/* Delete Confirmation Modal */}
+ {isDeleteModalOpen && (
+
+
+
Confirm Delete
+
+ {deletingItem?.type === 'module' && 'Are you sure you want to delete this training module? This action cannot be undone.'}
+ {deletingItem?.type === 'video' && 'Are you sure you want to delete this video? This action cannot be undone.'}
+ {deletingItem?.type === 'material' && 'Are you sure you want to delete this material? This action cannot be undone.'}
+
+
+ {
+ setIsDeleteModalOpen(false);
+ setDeletingItem(null);
+ }}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
+ >
+ Cancel
+
+
+ Delete {deletingItem?.type === 'module' ? 'Module' : deletingItem?.type === 'video' ? 'Video' : 'Material'}
+
+
+
+
+ )}
);
};
diff --git a/src/pages/admin/Analytics.tsx b/src/pages/admin/Analytics.tsx
new file mode 100644
index 0000000..2ed0703
--- /dev/null
+++ b/src/pages/admin/Analytics.tsx
@@ -0,0 +1,289 @@
+import React, { useState, useEffect } from 'react';
+import {
+ BarChart3,
+ Users,
+ Building,
+ Package,
+ TrendingUp,
+ TrendingDown,
+ DollarSign,
+ Activity,
+ Calendar,
+ Target
+} from 'lucide-react';
+
+interface AnalyticsData {
+ userGrowth: {
+ total: number;
+ growth: number;
+ trend: 'up' | 'down';
+ };
+ revenue: {
+ total: number;
+ growth: number;
+ trend: 'up' | 'down';
+ };
+ vendorApprovals: {
+ total: number;
+ pending: number;
+ approved: number;
+ rejected: number;
+ };
+ productPerformance: {
+ totalProducts: number;
+ activeProducts: number;
+ topPerforming: Array<{
+ name: string;
+ revenue: number;
+ sales: number;
+ }>;
+ };
+ systemMetrics: {
+ uptime: number;
+ responseTime: number;
+ errorRate: number;
+ };
+}
+
+const AdminAnalytics: React.FC = () => {
+ const [analyticsData, setAnalyticsData] = useState
({
+ userGrowth: { total: 0, growth: 0, trend: 'up' },
+ revenue: { total: 0, growth: 0, trend: 'up' },
+ vendorApprovals: { total: 0, pending: 0, approved: 0, rejected: 0 },
+ productPerformance: { totalProducts: 0, activeProducts: 0, topPerforming: [] },
+ systemMetrics: { uptime: 0, responseTime: 0, errorRate: 0 }
+ });
+ const [loading, setLoading] = useState(true);
+ const [timeRange, setTimeRange] = useState('30d');
+
+ useEffect(() => {
+ fetchAnalyticsData();
+ }, [timeRange]);
+
+ const fetchAnalyticsData = async () => {
+ try {
+ setLoading(true);
+ // Mock data for now - replace with actual API call
+ const mockData: AnalyticsData = {
+ userGrowth: {
+ total: 1247,
+ growth: 12.5,
+ trend: 'up'
+ },
+ revenue: {
+ total: 284750,
+ growth: 8.3,
+ trend: 'up'
+ },
+ vendorApprovals: {
+ total: 156,
+ pending: 23,
+ approved: 128,
+ rejected: 5
+ },
+ productPerformance: {
+ totalProducts: 89,
+ activeProducts: 67,
+ topPerforming: [
+ { name: 'Cloud Storage Pro', revenue: 45000, sales: 156 },
+ { name: 'Database Hosting', revenue: 38000, sales: 89 },
+ { name: 'Load Balancer', revenue: 32000, sales: 67 }
+ ]
+ },
+ systemMetrics: {
+ uptime: 99.9,
+ responseTime: 245,
+ errorRate: 0.1
+ }
+ };
+
+ setAnalyticsData(mockData);
+ } catch (error) {
+ console.error('Error fetching analytics data:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD'
+ }).format(amount);
+ };
+
+ const getTrendIcon = (trend: 'up' | 'down') => {
+ return trend === 'up' ? (
+
+ ) : (
+
+ );
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Analytics Dashboard
+
System-wide performance and insights
+
+
+ setTimeRange(e.target.value)}
+ className="px-3 py-2 border border-gray-300 rounded-md bg-white dark:bg-gray-800 dark:border-gray-600"
+ >
+ Last 7 days
+ Last 30 days
+ Last 90 days
+ Last year
+
+
+
+
+ {/* Key Metrics */}
+
+
+
+
Total Users
+
+
+
+
{(analyticsData.userGrowth.total || 0).toLocaleString()}
+
+ {getTrendIcon(analyticsData.userGrowth.trend)}
+ {analyticsData.userGrowth.growth}% from last month
+
+
+
+
+
+
+
Total Revenue
+
+
+
+
{formatCurrency(analyticsData.revenue.total)}
+
+ {getTrendIcon(analyticsData.revenue.trend)}
+ {analyticsData.revenue.growth}% from last month
+
+
+
+
+
+
+
+
{analyticsData.productPerformance.activeProducts}
+
+ of {analyticsData.productPerformance.totalProducts} total products
+
+
+
+
+
+
+
+
{analyticsData.systemMetrics.uptime}%
+
+ Avg response: {analyticsData.systemMetrics.responseTime}ms
+
+
+
+
+
+ {/* Vendor Approvals */}
+
+
+
+
+
+ Vendor Approval Status
+
+
+
+
+ Total Requests
+ {analyticsData.vendorApprovals.total}
+
+
+ Pending
+ {analyticsData.vendorApprovals.pending}
+
+
+ Approved
+ {analyticsData.vendorApprovals.approved}
+
+
+ Rejected
+ {analyticsData.vendorApprovals.rejected}
+
+
+
+
+
+
+
+
+ Top Performing Products
+
+
+
+ {analyticsData.productPerformance.topPerforming.map((product, index) => (
+
+
+
{product.name}
+
{product.sales} sales
+
+
+
{formatCurrency(product.revenue)}
+
+
+ ))}
+
+
+
+
+ {/* System Health */}
+
+
+
+
+ System Health Metrics
+
+
+
+
+
{analyticsData.systemMetrics.uptime}%
+
Uptime
+
+
+
{analyticsData.systemMetrics.responseTime}ms
+
Avg Response Time
+
+
+
{analyticsData.systemMetrics.errorRate}%
+
Error Rate
+
+
+
+
+ );
+};
+
+export default AdminAnalytics;
\ No newline at end of file
diff --git a/src/pages/admin/ChannelPartners.tsx b/src/pages/admin/ChannelPartners.tsx
index 82398d8..1db366c 100644
--- a/src/pages/admin/ChannelPartners.tsx
+++ b/src/pages/admin/ChannelPartners.tsx
@@ -40,6 +40,8 @@ const ChannelPartners: React.FC = () => {
const [selectedPartner, setSelectedPartner] = useState(null);
const [showModal, setShowModal] = useState(false);
const [editingPartner, setEditingPartner] = useState(null);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [deletingPartnerId, setDeletingPartnerId] = useState(null);
useEffect(() => {
fetchChannelPartners();
@@ -47,9 +49,10 @@ const ChannelPartners: React.FC = () => {
const fetchChannelPartners = async () => {
try {
- const response = await fetch('/api/admin/channel-partners', {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/channel-partners`, {
headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
}
});
const data = await response.json();
@@ -65,31 +68,38 @@ const ChannelPartners: React.FC = () => {
};
const handleDelete = async (partnerId: string) => {
- if (window.confirm('Are you sure you want to delete this channel partner?')) {
- try {
- const response = await fetch(`/api/admin/channel-partners/${partnerId}`, {
- method: 'DELETE',
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
- }
- });
+ setDeletingPartnerId(partnerId);
+ setIsDeleteModalOpen(true);
+ };
- if (response.ok) {
- fetchChannelPartners();
+ const confirmDelete = async () => {
+ if (!deletingPartnerId) return;
+
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/channel-partners/${deletingPartnerId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
}
- } catch (error) {
- console.error('Error deleting channel partner:', error);
+ });
+
+ if (response.ok) {
+ fetchChannelPartners();
+ setIsDeleteModalOpen(false);
+ setDeletingPartnerId(null);
}
+ } catch (error) {
+ console.error('Error deleting channel partner:', error);
}
};
const handleUpdate = async (partnerId: string, updateData: Partial) => {
try {
- const response = await fetch(`/api/admin/channel-partners/${partnerId}`, {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/channel-partners/${partnerId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
},
body: JSON.stringify(updateData)
});
@@ -318,7 +328,7 @@ const ChannelPartners: React.FC = () => {
{/* Partner Details Modal */}
{selectedPartner && (
-
+
@@ -426,6 +436,35 @@ const ChannelPartners: React.FC = () => {
)}
+
+ {/* Delete Confirmation Modal */}
+ {isDeleteModalOpen && (
+
+
+
Confirm Delete
+
+ Are you sure you want to delete this channel partner? This action cannot be undone.
+
+
+ {
+ setIsDeleteModalOpen(false);
+ setDeletingPartnerId(null);
+ }}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
+ >
+ Cancel
+
+
+ Delete Partner
+
+
+
+
+ )}
);
diff --git a/src/pages/admin/Dashboard.tsx b/src/pages/admin/Dashboard.tsx
index f0e7778..0d44fbf 100644
--- a/src/pages/admin/Dashboard.tsx
+++ b/src/pages/admin/Dashboard.tsx
@@ -15,41 +15,69 @@ import {
Bell
} from 'lucide-react';
import { useAppSelector } from '../../store/hooks';
+import { VendorRequest } from '../../types/vendor';
+import VendorDetailsModal from '../../components/VendorDetailsModal';
+import VendorRejectionModal from '../../components/VendorRejectionModal';
interface DashboardStats {
totalUsers: number;
pendingVendors: number;
- totalChannelPartners: number;
+ totalRegisteredVendors: number;
totalResellers: number;
recentRequests: number;
approvedToday: number;
rejectedToday: number;
revenue: number;
+ userBreakdown: {
+ systemAdmins: number;
+ vendors: number;
+ resellers: number;
+ inactiveUsers: number;
+ };
+ todayStats: {
+ approvedToday: number;
+ rejectedToday: number;
+ };
+ recentActivity: Array<{
+ id: number;
+ type: string;
+ title: string;
+ message: string;
+ createdAt: string;
+ priority: string;
+ }>;
}
-interface PendingVendor {
- id: string;
- firstName: string;
- lastName: string;
- email: string;
- company: string;
- createdAt: string;
- status: 'pending' | 'approved' | 'rejected';
-}
+// Use the shared VendorRequest interface instead of PendingVendor
const AdminDashboard: React.FC = () => {
const [stats, setStats] = useState({
totalUsers: 0,
pendingVendors: 0,
- totalChannelPartners: 0,
+ totalRegisteredVendors: 0,
totalResellers: 0,
recentRequests: 0,
approvedToday: 0,
rejectedToday: 0,
- revenue: 0
+ revenue: 0,
+ userBreakdown: {
+ systemAdmins: 0,
+ vendors: 0,
+ resellers: 0,
+ inactiveUsers: 0
+ },
+ todayStats: {
+ approvedToday: 0,
+ rejectedToday: 0
+ },
+ recentActivity: []
});
- const [pendingVendors, setPendingVendors] = useState([]);
+ const [pendingVendors, setPendingVendors] = useState([]);
const [loading, setLoading] = useState(true);
+ const [selectedVendor, setSelectedVendor] = useState(null);
+ const [showVendorModal, setShowVendorModal] = useState(false);
+ const [rejectionReason, setRejectionReason] = useState('');
+ const [showRejectionModal, setShowRejectionModal] = useState(false);
const { user } = useAppSelector((state) => state.auth);
useEffect(() => {
@@ -59,27 +87,28 @@ const AdminDashboard: React.FC = () => {
const fetchDashboardData = async () => {
try {
// Fetch dashboard stats
- const statsResponse = await fetch('/api/admin/dashboard', {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/dashboard`, {
headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
}
});
- const statsData = await statsResponse.json();
+ const statsData = await response.json();
if (statsData.success) {
- setStats(statsData.data);
+ setStats(statsData.data.stats);
}
// Fetch pending vendors
- const vendorsResponse = await fetch('/api/admin/pending-vendors', {
+ const vendorsResponse = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/pending-vendors`, {
headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
}
});
const vendorsData = await vendorsResponse.json();
if (vendorsData.success) {
- setPendingVendors(vendorsData.data.slice(0, 5)); // Show only first 5
+ setPendingVendors(vendorsData.data.pendingRequests?.slice(0, 5) || []); // Show only first 5
}
} catch (error) {
console.error('Error fetching dashboard data:', error);
@@ -90,11 +119,11 @@ const AdminDashboard: React.FC = () => {
const handleApproveVendor = async (vendorId: string) => {
try {
- const response = await fetch(`/api/admin/vendors/${vendorId}/approve`, {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/vendors/${vendorId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
},
body: JSON.stringify({ reason: 'Approved by admin' })
});
@@ -107,25 +136,40 @@ const AdminDashboard: React.FC = () => {
}
};
- const handleRejectVendor = async (vendorId: string) => {
+ const handleRejectVendor = async (vendorId: string, reason: string = 'Rejected by admin') => {
try {
- const response = await fetch(`/api/admin/vendors/${vendorId}/reject`, {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/vendors/${vendorId}/reject`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
},
- body: JSON.stringify({ reason: 'Rejected by admin' })
+ body: JSON.stringify({ reason })
});
if (response.ok) {
fetchDashboardData(); // Refresh data
+ setShowRejectionModal(false);
+ setRejectionReason('');
}
} catch (error) {
console.error('Error rejecting vendor:', error);
}
};
+ const handleViewVendorDetails = (vendor: VendorRequest) => {
+ setSelectedVendor(vendor);
+ setShowVendorModal(true);
+ };
+
+ const handleRejectWithReason = (vendorId: string) => {
+ const vendor = pendingVendors.find(v => v.id === vendorId);
+ if (vendor) {
+ setSelectedVendor(vendor);
+ setShowRejectionModal(true);
+ }
+ };
+
if (loading) {
return (
@@ -151,12 +195,30 @@ const AdminDashboard: React.FC = () => {
{/* Total Users */}
-
-
-
Total Users
-
{stats.totalUsers}
+
+
+
Total Users
+
{stats.totalUsers || 0}
+
+
+ System Admins
+ {stats.userBreakdown?.systemAdmins || 0}
+
+
+ Vendors
+ {stats.userBreakdown?.vendors || 0}
+
+
+ Resellers
+ {stats.userBreakdown?.resellers || 0}
+
+
+ Inactive Users
+ {stats.userBreakdown?.inactiveUsers || 0}
+
+
-
@@ -167,7 +229,7 @@ const AdminDashboard: React.FC = () => {
Pending Vendors
-
{stats.pendingVendors}
+
{stats.pendingVendors || 0}
@@ -179,8 +241,8 @@ const AdminDashboard: React.FC = () => {
-
Channel Partners
-
{stats.totalChannelPartners}
+
Registered Vendors
+
{stats.totalRegisteredVendors || 0}
@@ -193,7 +255,7 @@ const AdminDashboard: React.FC = () => {
Monthly Revenue
-
${stats.revenue.toLocaleString()}
+
${(stats.revenue || 0).toLocaleString()}
@@ -241,13 +303,14 @@ const AdminDashboard: React.FC = () => {
handleRejectVendor(vendor.id)}
+ onClick={() => handleRejectWithReason(vendor.id)}
className="p-2 bg-red-100 dark:bg-red-900 rounded-lg hover:bg-red-200 dark:hover:bg-red-800 transition-colors"
title="Reject"
>
handleViewVendorDetails(vendor)}
className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
title="View Details"
>
@@ -265,35 +328,71 @@ const AdminDashboard: React.FC = () => {
Recent Activity
-
-
-
+ {stats.recentActivity && stats.recentActivity.length > 0 ? (
+ stats.recentActivity.slice(0, 5).map((activity) => {
+ const getActivityIcon = (type: string) => {
+ switch (type) {
+ case 'NEW_VENDOR_REQUEST':
+ return
;
+ case 'VENDOR_APPROVED':
+ return
;
+ case 'VENDOR_REJECTED':
+ return
;
+ case 'NEW_RESELLER_REQUEST':
+ return
;
+ default:
+ return
;
+ }
+ };
+
+ const getActivityColor = (type: string) => {
+ switch (type) {
+ case 'NEW_VENDOR_REQUEST':
+ return 'bg-blue-100 dark:bg-blue-900';
+ case 'VENDOR_APPROVED':
+ return 'bg-green-100 dark:bg-green-900';
+ case 'VENDOR_REJECTED':
+ return 'bg-red-100 dark:bg-red-900';
+ case 'NEW_RESELLER_REQUEST':
+ return 'bg-purple-100 dark:bg-purple-900';
+ default:
+ return 'bg-gray-100 dark:bg-gray-900';
+ }
+ };
+
+ const formatTimeAgo = (dateString: string) => {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
+
+ if (diffInMinutes < 1) return 'Just now';
+ if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
+ if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)} hours ago`;
+ return `${Math.floor(diffInMinutes / 1440)} days ago`;
+ };
+
+ return (
+
+
+ {getActivityIcon(activity.type)}
+
+
+
+ {activity.title}
+
+
+ {formatTimeAgo(activity.createdAt)}
+
+
+
+ );
+ })
+ ) : (
+
-
-
Vendor Approved
-
2 minutes ago
-
-
-
-
-
-
-
-
-
Vendor Rejected
-
15 minutes ago
-
-
-
-
-
-
-
New Registration
-
1 hour ago
-
-
+ )}
@@ -307,7 +406,7 @@ const AdminDashboard: React.FC = () => {
Approved Today
-
{stats.approvedToday}
+
{stats.todayStats?.approvedToday || 0}
@@ -319,7 +418,7 @@ const AdminDashboard: React.FC = () => {
Rejected Today
-
{stats.rejectedToday}
+
{stats.todayStats?.rejectedToday || 0}
@@ -337,6 +436,246 @@ const AdminDashboard: React.FC = () => {
+
+ {/* Vendor Details Modal */}
+ {showVendorModal && selectedVendor && (
+
+
+
+
Vendor Details
+ setShowVendorModal(false)}
+ className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
+ >
+
+
+
+
+ {/* Basic Information */}
+
+
+
Name
+
{selectedVendor.firstName} {selectedVendor.lastName}
+
+
+
Email
+
{selectedVendor.email}
+
+
+
+
+
+
Phone
+
{selectedVendor.phone || 'Not provided'}
+
+
+
Company
+
{selectedVendor.company}
+
+
+
+
+
+
Role
+
{selectedVendor.role ? selectedVendor.role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) : 'Vendor'}
+
+
+
Registration Date
+
{new Date(selectedVendor.createdAt).toLocaleDateString()}
+
+
+
+ {/* Business Information */}
+ {(selectedVendor.companyType || selectedVendor.industry || selectedVendor.yearsInBusiness) && (
+
+
Business Information
+
+ {selectedVendor.companyType && (
+
+
Company Type
+
{selectedVendor.companyType}
+
+ )}
+ {selectedVendor.industry && (
+
+
Industry
+
{selectedVendor.industry}
+
+ )}
+ {selectedVendor.yearsInBusiness && (
+
+
Years in Business
+
{selectedVendor.yearsInBusiness} years
+
+ )}
+ {selectedVendor.employeeCount && (
+
+
Employee Count
+
{selectedVendor.employeeCount}
+
+ )}
+
+
+ )}
+
+ {/* Financial Information */}
+ {(selectedVendor.annualRevenue || selectedVendor.taxId) && (
+
+
Financial Information
+
+ {selectedVendor.annualRevenue && (
+
+
Annual Revenue
+
${selectedVendor.annualRevenue.toLocaleString()}
+
+ )}
+ {selectedVendor.taxId && (
+
+
Tax ID
+
{selectedVendor.taxId}
+
+ )}
+
+
+ )}
+
+ {/* Legal Information */}
+ {(selectedVendor.registrationNumber || selectedVendor.gstNumber || selectedVendor.panNumber || selectedVendor.businessLicense) && (
+
+
Legal Information
+
+ {selectedVendor.registrationNumber && (
+
+
Registration Number
+
{selectedVendor.registrationNumber}
+
+ )}
+ {selectedVendor.gstNumber && (
+
+
GST Number
+
{selectedVendor.gstNumber}
+
+ )}
+ {selectedVendor.panNumber && (
+
+
PAN Number
+
{selectedVendor.panNumber}
+
+ )}
+ {selectedVendor.businessLicense && (
+
+
Business License
+
{selectedVendor.businessLicense}
+
+ )}
+
+
+ )}
+
+ {/* Address Information */}
+ {selectedVendor.address && (
+
+
Address Information
+
+
Address
+
{selectedVendor.address}
+
+
+ )}
+
+ {/* Rejection Reason */}
+ {selectedVendor.rejectionReason && (
+
+
Rejection Information
+
+
Rejection Reason
+
{selectedVendor.rejectionReason}
+
+
+ )}
+
+
+ handleApproveVendor(selectedVendor.id)}
+ className="flex-1 bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 transition-colors"
+ >
+ Approve
+
+ {
+ setShowVendorModal(false);
+ handleRejectWithReason(selectedVendor.id);
+ }}
+ className="flex-1 bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 transition-colors"
+ >
+ Reject
+
+
+
+
+ )}
+
+ {/* Rejection Reason Modal */}
+ {showRejectionModal && selectedVendor && (
+
+
+
+
Reject Vendor
+ setShowRejectionModal(false)}
+ className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
+ >
+
+
+
+
+
+ Rejecting: {selectedVendor.firstName} {selectedVendor.lastName} ({selectedVendor.company})
+
+
+ Reason for rejection
+
+
setRejectionReason(e.target.value)}
+ className="w-full p-3 border border-slate-300 dark:border-slate-600 rounded-md bg-white dark:bg-slate-700 text-slate-900 dark:text-white"
+ rows={3}
+ placeholder="Enter reason for rejection..."
+ />
+
+
+ setShowRejectionModal(false)}
+ className="flex-1 bg-slate-300 dark:bg-slate-600 text-slate-700 dark:text-slate-300 py-2 px-4 rounded-md hover:bg-slate-400 dark:hover:bg-slate-500 transition-colors"
+ >
+ Cancel
+
+ handleRejectVendor(selectedVendor.id, rejectionReason || 'Rejected by admin')}
+ className="flex-1 bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 transition-colors"
+ >
+ Reject
+
+
+
+
+ )}
+
+ {/* Shared Vendor Modals */}
+
setShowVendorModal(false)}
+ onApprove={handleApproveVendor}
+ onReject={handleRejectVendor}
+ />
+
+ setShowRejectionModal(false)}
+ onReject={handleRejectVendor}
+ />
);
};
diff --git a/src/pages/admin/Feedback.tsx b/src/pages/admin/Feedback.tsx
new file mode 100644
index 0000000..2f7383d
--- /dev/null
+++ b/src/pages/admin/Feedback.tsx
@@ -0,0 +1,433 @@
+import React, { useState, useEffect } from 'react';
+import {
+ MessageSquare,
+ AlertCircle,
+ CheckCircle,
+ Clock,
+ Filter,
+ Search,
+ Eye,
+ XCircle,
+ Star,
+ Bug,
+ Lightbulb,
+ HelpCircle
+} from 'lucide-react';
+
+interface Feedback {
+ id: number;
+ type: 'bug' | 'feature' | 'question' | 'general';
+ priority: 'low' | 'medium' | 'high' | 'critical';
+ status: 'open' | 'in_progress' | 'resolved' | 'closed';
+ title: string;
+ description: string;
+ submittedBy: string;
+ submittedAt: string;
+ assignedTo?: string;
+ resolvedAt?: string;
+ resolution?: string;
+ systemInfo?: any;
+}
+
+const AdminFeedback: React.FC = () => {
+ const [feedbacks, setFeedbacks] = useState
([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedFeedback, setSelectedFeedback] = useState(null);
+ const [showModal, setShowModal] = useState(false);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState<'all' | 'open' | 'in_progress' | 'resolved' | 'closed'>('all');
+ const [typeFilter, setTypeFilter] = useState<'all' | 'bug' | 'feature' | 'question' | 'general'>('all');
+ const [priorityFilter, setPriorityFilter] = useState<'all' | 'low' | 'medium' | 'high' | 'critical'>('all');
+
+ useEffect(() => {
+ fetchFeedbacks();
+ }, []);
+
+ const fetchFeedbacks = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/developer-feedback`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setFeedbacks(data.data || []);
+ }
+ } catch (error) {
+ console.error('Error fetching feedbacks:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const updateFeedbackStatus = async (feedbackId: number, status: string, resolution?: string) => {
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/developer-feedback/${feedbackId}/status`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ status, resolution })
+ });
+
+ if (response.ok) {
+ fetchFeedbacks();
+ setShowModal(false);
+ }
+ } catch (error) {
+ console.error('Error updating feedback status:', error);
+ }
+ };
+
+ const getTypeIcon = (type: string) => {
+ switch (type) {
+ case 'bug':
+ return ;
+ case 'feature':
+ return ;
+ case 'question':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getPriorityColor = (priority: string) => {
+ switch (priority) {
+ case 'critical':
+ return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
+ case 'high':
+ return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200';
+ case 'medium':
+ return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
+ case 'low':
+ return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
+ default:
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'open':
+ return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
+ case 'in_progress':
+ return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
+ case 'resolved':
+ return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
+ case 'closed':
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
+ default:
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
+ }
+ };
+
+ const filteredFeedbacks = feedbacks.filter(feedback => {
+ const matchesSearch = feedback.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ feedback.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ feedback.submittedBy.toLowerCase().includes(searchTerm.toLowerCase());
+
+ const matchesStatus = statusFilter === 'all' || feedback.status === statusFilter;
+ const matchesType = typeFilter === 'all' || feedback.type === typeFilter;
+ const matchesPriority = priorityFilter === 'all' || feedback.priority === priorityFilter;
+
+ return matchesSearch && matchesStatus && matchesType && matchesPriority;
+ });
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ Developer Feedback & Issues
+
+
+ Manage feedback, bug reports, and feature requests from developers
+
+
+
+ {/* Filters */}
+
+
+ {/* Search */}
+
+
+
+ setSearchTerm(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
+ />
+
+
+
+ {/* Status Filter */}
+
+ setStatusFilter(e.target.value as any)}
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ All Status
+ Open
+ In Progress
+ Resolved
+ Closed
+
+
+
+ {/* Type Filter */}
+
+ setTypeFilter(e.target.value as any)}
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ All Types
+ Bug
+ Feature
+ Question
+ General
+
+
+
+ {/* Priority Filter */}
+
+ setPriorityFilter(e.target.value as any)}
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ All Priorities
+ Critical
+ High
+ Medium
+ Low
+
+
+
+
+
+ {/* Feedback List */}
+
+
+
+ Feedback ({filteredFeedbacks.length})
+
+
+
+
+
+
+
+
+ Type
+
+
+ Title
+
+
+ Priority
+
+
+ Status
+
+
+ Submitted By
+
+
+ Date
+
+
+ Actions
+
+
+
+
+ {filteredFeedbacks.map((feedback) => (
+
+
+
+ {getTypeIcon(feedback.type)}
+
+ {feedback.type}
+
+
+
+
+
+ {feedback.title}
+
+
+ {feedback.description}
+
+
+
+
+ {feedback.priority}
+
+
+
+
+ {feedback.status === 'in_progress' ? 'In Progress' : feedback.status.replace('_', ' ')}
+
+
+
+ {feedback.submittedBy}
+
+
+ {new Date(feedback.submittedAt).toLocaleDateString()}
+
+
+
+ {
+ setSelectedFeedback(feedback);
+ setShowModal(true);
+ }}
+ className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
+ title="View Details"
+ >
+
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* Feedback Details Modal */}
+ {showModal && selectedFeedback && (
+
+
+
+
+
+ Feedback Details
+
+ setShowModal(false)}
+ className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
+ >
+
+
+
+
+
+
+
+
+ Type
+
+
+ {getTypeIcon(selectedFeedback.type)}
+
+ {selectedFeedback.type}
+
+
+
+
+
+ Priority
+
+
+ {selectedFeedback.priority}
+
+
+
+
+
+
+ Title
+
+
+ {selectedFeedback.title}
+
+
+
+
+
+ Description
+
+
+ {selectedFeedback.description}
+
+
+
+
+
+
+ Submitted By
+
+
+ {selectedFeedback.submittedBy}
+
+
+
+
+ Submitted At
+
+
+ {new Date(selectedFeedback.submittedAt).toLocaleString()}
+
+
+
+
+ {selectedFeedback.systemInfo && (
+
+
+ System Information
+
+
+ {JSON.stringify(selectedFeedback.systemInfo, null, 2)}
+
+
+ )}
+
+
+
+ Update Status
+
+
+ {
+ updateFeedbackStatus(selectedFeedback.id, e.target.value);
+ }}
+ >
+ Open
+ In Progress
+ Resolved
+ Closed
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default AdminFeedback;
\ No newline at end of file
diff --git a/src/pages/admin/Products.tsx b/src/pages/admin/Products.tsx
new file mode 100644
index 0000000..319fd31
--- /dev/null
+++ b/src/pages/admin/Products.tsx
@@ -0,0 +1,920 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { Plus, Search, Edit, Trash2, Eye, Pencil } from 'lucide-react';
+import { apiService, Product } from '../../services/api';
+
+const Products: React.FC = () => {
+ const [products, setProducts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [categoryFilter, setCategoryFilter] = useState('all');
+ const [statusFilter, setStatusFilter] = useState('all');
+ const [vendorFilter, setVendorFilter] = useState('all');
+ const [vendors, setVendors] = useState>([]);
+ const [vendorMap, setVendorMap] = useState>({});
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('list');
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
+ const [isEditModalOpen, setIsEditModalOpen] = useState(false);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [deletingProductId, setDeletingProductId] = useState(null);
+ const [editingProductId, setEditingProductId] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(10);
+ const [totalItems, setTotalItems] = useState(0);
+ const [totalPages, setTotalPages] = useState(0);
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ category: '' as Product['category'],
+ price: '',
+ commissionRate: '',
+ status: 'draft' as Product['status'],
+ availability: 'available' as Product['availability'],
+ features: [] as string[],
+ specifications: {},
+ purchaseUrl: ''
+ });
+
+ const categories = [
+ { value: 'cloud_storage', label: 'Cloud Storage' },
+ { value: 'cloud_computing', label: 'Cloud Computing' },
+ { value: 'cybersecurity', label: 'Cybersecurity' },
+ { value: 'data_analytics', label: 'Data Analytics' },
+ { value: 'ai_ml', label: 'AI & Machine Learning' },
+ { value: 'iot', label: 'Internet of Things' },
+ { value: 'blockchain', label: 'Blockchain' },
+ { value: 'other', label: 'Other' }
+ ];
+
+ const statuses = [
+ { value: 'draft', label: 'Draft' },
+ { value: 'active', label: 'Active' },
+ { value: 'inactive', label: 'Inactive' },
+ { value: 'discontinued', label: 'Discontinued' }
+ ];
+
+ const availabilities = [
+ { value: 'available', label: 'Available' },
+ { value: 'out_of_stock', label: 'Out of Stock' },
+ { value: 'coming_soon', label: 'Coming Soon' },
+ { value: 'discontinued', label: 'Discontinued' }
+ ];
+
+ const fetchProducts = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const response = await apiService.getAllProducts({
+ page: currentPage,
+ limit: itemsPerPage,
+ category: categoryFilter === 'all' ? undefined : categoryFilter,
+ status: statusFilter === 'all' ? undefined : statusFilter,
+ vendor: vendorFilter === 'all' ? undefined : vendorFilter,
+ search: searchTerm || undefined,
+ sortBy: 'createdAt',
+ sortOrder: 'desc'
+ });
+
+ if (response.success) {
+ setProducts(response.data.products);
+ setTotalItems(response.data.pagination.totalItems);
+ setTotalPages(response.data.pagination.totalPages);
+ } else {
+ setError('Failed to fetch products');
+ }
+ } catch (error) {
+ console.error('Error fetching products:', error);
+ setError(error instanceof Error ? error.message : 'Failed to fetch products');
+ } finally {
+ setLoading(false);
+ }
+ }, [currentPage, itemsPerPage, categoryFilter, statusFilter, vendorFilter, searchTerm]);
+
+ const fetchVendors = useCallback(async () => {
+ try {
+ const response = await apiService.getActiveVendors();
+ if (response.success) {
+ setVendors(response.data);
+
+ // Build vendor map for quick lookup
+ const vendorMapData: Record = {};
+ response.data.forEach(vendor => {
+ vendorMapData[vendor.id] = {
+ firstName: vendor.firstName,
+ lastName: vendor.lastName,
+ company: vendor.company
+ };
+ });
+ setVendorMap(vendorMapData);
+ }
+ } catch (error) {
+ console.error('Error fetching vendors:', error);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchProducts();
+ }, [fetchProducts]);
+
+
+
+ useEffect(() => {
+ fetchVendors();
+ }, [fetchVendors]);
+
+ useEffect(() => {
+ if (categoryFilter !== '' || statusFilter !== '' || searchTerm !== '' || vendorFilter !== 'all') {
+ fetchProducts();
+ }
+ }, [categoryFilter, statusFilter, searchTerm, vendorFilter, fetchProducts]);
+
+ const handleCreateProduct = async () => {
+ try {
+ await apiService.createProduct({
+ ...formData,
+ price: parseFloat(formData.price),
+ commissionRate: parseFloat(formData.commissionRate),
+ category: formData.category || 'other'
+ });
+
+ alert('Product created successfully');
+ setIsCreateModalOpen(false);
+ setFormData({
+ name: '',
+ description: '',
+ category: 'other',
+ price: '',
+ commissionRate: '',
+ status: 'draft',
+ availability: 'available',
+ features: [],
+ specifications: {},
+ purchaseUrl: ''
+ });
+ fetchProducts();
+ } catch (error) {
+ alert('Error creating product');
+ }
+ };
+
+ const handleUpdateProduct = async () => {
+ if (!editingProductId) return;
+
+ try {
+ await apiService.updateProduct(editingProductId, {
+ ...formData,
+ price: parseFloat(formData.price),
+ commissionRate: parseFloat(formData.commissionRate),
+ category: formData.category || 'other'
+ });
+
+ alert('Product updated successfully');
+ setIsEditModalOpen(false);
+ setEditingProductId(null);
+ fetchProducts();
+ } catch (error) {
+ alert('Error updating product');
+ }
+ };
+
+ const handleDeleteProduct = async (productId: number) => {
+ setDeletingProductId(productId);
+ setIsDeleteModalOpen(true);
+ };
+
+ const confirmDelete = async () => {
+ if (!deletingProductId) return;
+
+ try {
+ await apiService.deleteProduct(deletingProductId);
+ alert('Product deleted successfully');
+ fetchProducts();
+ setIsDeleteModalOpen(false);
+ setDeletingProductId(null);
+ } catch (error) {
+ alert('Error deleting product');
+ }
+ };
+
+ const filteredProducts = products.filter(product => {
+ const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ (product.sku && product.sku.toLowerCase().includes(searchTerm.toLowerCase()));
+ const matchesCategory = !categoryFilter || product.category === categoryFilter;
+ const matchesStatus = !statusFilter || product.status === statusFilter;
+
+ return matchesSearch && matchesCategory && matchesStatus;
+ });
+
+ const getStatusBadge = (status: string) => {
+ const statusConfig = {
+ draft: { color: 'bg-gray-100 text-gray-800', label: 'Draft' },
+ active: { color: 'bg-green-100 text-green-800', label: 'Active' },
+ inactive: { color: 'bg-yellow-100 text-yellow-800', label: 'Inactive' },
+ discontinued: { color: 'bg-red-100 text-red-800', label: 'Discontinued' }
+ };
+
+ const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.draft;
+ return {config.label} ;
+ };
+
+ const getAvailabilityBadge = (availability: string) => {
+ const availabilityConfig = {
+ available: { color: 'bg-green-100 text-green-800', label: 'Available' },
+ out_of_stock: { color: 'bg-red-100 text-red-800', label: 'Out of Stock' },
+ coming_soon: { color: 'bg-blue-100 text-blue-800', label: 'Coming Soon' },
+ discontinued: { color: 'bg-gray-100 text-gray-800', label: 'Discontinued' }
+ };
+
+ const config = availabilityConfig[availability as keyof typeof availabilityConfig] || availabilityConfig.available;
+ return {config.label} ;
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
Products
+
setIsCreateModalOpen(true)}
+ className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
+ >
+
+ Add Product
+
+
+
+ {/* Filters */}
+
+
+ setSearchTerm(e.target.value)}
+ className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+ setCategoryFilter(e.target.value)}
+ className="min-w-[150px] px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ All Categories
+ {categories.map((category) => (
+
+ {category.label}
+
+ ))}
+
+ setStatusFilter(e.target.value)}
+ className="min-w-[150px] px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ All Statuses
+ Draft
+ Active
+ Inactive
+ Discontinued
+
+
+
+ {/* Vendor filter on separate line */}
+
+ setVendorFilter(e.target.value)}
+ className="min-w-[200px] px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ All Vendors
+ {vendors.map((vendor) => (
+
+ {vendor.company ? `${vendor.company} (${vendor.firstName} ${vendor.lastName})` : `${vendor.firstName} ${vendor.lastName}`}
+
+ ))}
+
+
+
+ {/* View Toggle */}
+
+
+ setViewMode('list')}
+ className={`px-3 py-2 text-sm font-medium ${
+ viewMode === 'list'
+ ? 'bg-blue-600 text-white'
+ : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600'
+ }`}
+ >
+ List
+
+ setViewMode('grid')}
+ className={`px-3 py-2 text-sm font-medium ${
+ viewMode === 'grid'
+ ? 'bg-blue-600 text-white'
+ : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600'
+ }`}
+ >
+ Grid
+
+
+
+
+
+ {/* Loading State */}
+ {loading && (
+
+ )}
+
+ {/* Error Display */}
+ {error && (
+
+ )}
+
+ {/* Products Display */}
+ {!loading && products.length === 0 ? (
+
+ ) : viewMode === 'grid' ? (
+
+ {products.map((product) => (
+
+
+
+
+ {product.name}
+
+
+ {(product.isAdminCreated || product.source === 'admin') && (
+
+ Admin
+
+ )}
+
+ {product.status}
+
+
+
+
+
+ {product.description}
+
+
+
+
+ Category:
+ {product.category}
+
+
+ Price:
+ ${product.price}
+
+
+ Vendor:
+
+ {product.isAdminCreated || product.source === 'admin' ? (
+ From Cloudtopiaa
+ ) : product.creator ? (
+ product.creator.company ?
+ `${product.creator.company} (${product.creator.firstName} ${product.creator.lastName})` :
+ `${product.creator.firstName} ${product.creator.lastName}`
+ ) : (
+ 'Unknown Vendor'
+ )}
+
+
+
+
+
+
{
+ setFormData({
+ name: product.name,
+ description: product.description || '',
+ category: product.category,
+ price: product.price.toString(),
+ commissionRate: product.commissionRate.toString(),
+ status: product.status,
+ availability: product.availability,
+ features: product.features || [],
+ specifications: product.specifications || {},
+ purchaseUrl: product.purchaseUrl || ''
+ });
+ setEditingProductId(product.id);
+ setIsEditModalOpen(true);
+ }}
+ className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
+ >
+
+
+ {(product.isAdminCreated || product.source === 'admin') ? (
+
+
+
+ ) : (
+
handleDeleteProduct(product.id)}
+ className="p-2 text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
+ >
+
+
+ )}
+
+
+
+ ))}
+
+ ) : (
+
+
+
+
+
+
+ Product
+
+
+ Vendor
+
+
+ Category
+
+
+ Price
+
+
+ Commission
+
+
+ Status
+
+
+ Actions
+
+
+
+
+ {products.map((product) => (
+
+
+
+
+
+
+
{product.name}
+ {(product.isAdminCreated || product.source === 'admin') && (
+
+ Admin
+
+ )}
+
+
{product.sku || 'N/A'}
+
+
+
+
+
+ {product.isAdminCreated || product.source === 'admin' ? (
+ 'From Cloudtopiaa'
+ ) : product.creator ? (
+ product.creator.company ?
+ `${product.creator.company} (${product.creator.firstName} ${product.creator.lastName})` :
+ `${product.creator.firstName} ${product.creator.lastName}`
+ ) : (
+ 'Unknown Vendor'
+ )}
+
+
+
+
+ {product.category.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
+
+
+
+ ${product.price}
+
+
+ {product.commissionRate}%
+
+
+ {getStatusBadge(product.status)}
+
+
+
+
{
+ setFormData({
+ name: product.name,
+ description: product.description || '',
+ category: product.category,
+ price: product.price.toString(),
+ commissionRate: product.commissionRate.toString(),
+ status: product.status,
+ availability: product.availability,
+ features: product.features || [],
+ specifications: product.specifications || {},
+ purchaseUrl: product.purchaseUrl || ''
+ });
+ setEditingProductId(product.id);
+ setIsEditModalOpen(true);
+ }}
+ className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
+ >
+
+
+ {(product.isAdminCreated || product.source === 'admin') ? (
+
+
+
+ ) : (
+
handleDeleteProduct(product.id)}
+ className="p-2 text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
+ >
+
+
+ )}
+
+
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Pagination */}
+ {totalItems > itemsPerPage && (
+
+
+ setCurrentPage(prev => Math.max(prev - 1, 1))}
+ disabled={currentPage === 1}
+ className="px-3 py-2 text-sm font-medium text-gray-500 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Previous
+
+
+ {Array.from({ length: Math.ceil(totalItems / itemsPerPage) }, (_, i) => i + 1).map((page) => (
+ setCurrentPage(page)}
+ className={`px-3 py-2 text-sm font-medium rounded-lg ${
+ currentPage === page
+ ? 'bg-blue-600 text-white'
+ : 'text-gray-500 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
+ }`}
+ >
+ {page}
+
+ ))}
+
+ setCurrentPage(prev => Math.min(prev + 1, Math.ceil(totalItems / itemsPerPage)))}
+ disabled={currentPage === Math.ceil(totalItems / itemsPerPage)}
+ className="px-3 py-2 text-sm font-medium text-gray-500 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Next
+
+
+
+ )}
+
+ {/* Create Product Modal */}
+ {isCreateModalOpen && (
+
+
+
Create New Product
+
+{`Note: Products created by admin will be:
+• Available to ALL vendors
+• Non-deletable by vendors
+• Displayed as "From Cloudtopiaa" in vendor portal`}
+
+
+
+ Product Name
+ setFormData({ ...formData, name: e.target.value })}
+ placeholder="Enter product name"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+
+
+ Description
+ setFormData({ ...formData, description: e.target.value })}
+ placeholder="Enter product description"
+ rows={3}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+
+
+
+ Category
+ setFormData({ ...formData, category: e.target.value as Product['category'] })}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ Select category
+ {categories.map((category) => (
+
+ {category.label}
+
+ ))}
+
+
+
+ Price
+ setFormData({ ...formData, price: e.target.value })}
+ placeholder="0.00"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+
+
+
+
+ Commission Rate (%)
+ setFormData({ ...formData, commissionRate: e.target.value })}
+ placeholder="10.0"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+
+
+ Status
+ setFormData({ ...formData, status: e.target.value as Product['status'] })}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ {statuses.map((status) => (
+
+ {status.label}
+
+ ))}
+
+
+
+
+ Availability
+ setFormData({ ...formData, availability: e.target.value as Product['availability'] })}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ {availabilities.map((availability) => (
+
+ {availability.label}
+
+ ))}
+
+
+
+ Purchase URL
+ setFormData({ ...formData, purchaseUrl: e.target.value })}
+ placeholder="https://example.com/product"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+
+
+ setIsCreateModalOpen(false)}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 dark:text-white"
+ >
+ Cancel
+
+
+ Create Product
+
+
+
+
+
+ )}
+
+ {/* Edit Product Modal */}
+ {isEditModalOpen && (
+
+
+
Edit Product
+
+
+ Product Name
+ setFormData({ ...formData, name: e.target.value })}
+ placeholder="Enter product name"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+
+
+ Description
+ setFormData({ ...formData, description: e.target.value })}
+ placeholder="Enter product description"
+ rows={3}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+
+
+
+ Category
+ setFormData({ ...formData, category: e.target.value as Product['category'] })}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ {categories.map((category) => (
+
+ {category.label}
+
+ ))}
+
+
+
+ Price
+ setFormData({ ...formData, price: e.target.value })}
+ placeholder="0.00"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+
+
+
+
+ Commission Rate (%)
+ setFormData({ ...formData, commissionRate: e.target.value })}
+ placeholder="10.0"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+
+
+ Status
+ setFormData({ ...formData, status: e.target.value as Product['status'] })}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ {statuses.map((status) => (
+
+ {status.label}
+
+ ))}
+
+
+
+
+ Availability
+ setFormData({ ...formData, availability: e.target.value as Product['availability'] })}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ {availabilities.map((availability) => (
+
+ {availability.label}
+
+ ))}
+
+
+
+ Purchase URL
+ setFormData({ ...formData, purchaseUrl: e.target.value })}
+ placeholder="https://example.com/product"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ />
+
+
+ setIsEditModalOpen(false)}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 dark:text-white"
+ >
+ Cancel
+
+
+ Update Product
+
+
+
+
+
+ )}
+
+ {/* Delete Confirmation Modal */}
+ {isDeleteModalOpen && (
+
+
+
Confirm Delete
+
+ Are you sure you want to delete this product? This action cannot be undone.
+
+
+ {
+ setIsDeleteModalOpen(false);
+ setDeletingProductId(null);
+ }}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
+ >
+ Cancel
+
+
+ Delete Product
+
+
+
+
+ )}
+
+ );
+};
+
+export default Products;
\ No newline at end of file
diff --git a/src/pages/admin/RegisteredVendors.tsx b/src/pages/admin/RegisteredVendors.tsx
new file mode 100644
index 0000000..680bb8c
--- /dev/null
+++ b/src/pages/admin/RegisteredVendors.tsx
@@ -0,0 +1,568 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Building,
+ Users,
+ Plus,
+ Edit,
+ Trash2,
+ Eye,
+ Search,
+ Filter,
+ MapPin,
+ Mail,
+ Phone,
+ Globe,
+ TrendingUp,
+ Calendar
+} from 'lucide-react';
+
+interface RegisteredVendor {
+ id: string;
+ companyName: string;
+ companyType: string;
+ contactEmail: string;
+ contactPhone: string;
+ website: string;
+ tier: string;
+ status: string;
+ commissionRate: number;
+ territory: string;
+ specializations: string[];
+ createdAt: string;
+ approvedAt?: string;
+}
+
+const RegisteredVendors: React.FC = () => {
+ const [vendors, setVendors] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'pending' | 'suspended'>('all');
+ const [selectedVendor, setSelectedVendor] = useState(null);
+ const [showModal, setShowModal] = useState(false);
+ const [editingVendor, setEditingVendor] = useState(null);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [deletingVendorId, setDeletingVendorId] = useState(null);
+
+ useEffect(() => {
+ fetchRegisteredVendors();
+ }, []);
+
+ const fetchRegisteredVendors = async () => {
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/registered-vendors`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ const data = await response.json();
+
+ if (data.success) {
+ setVendors(data.data);
+ }
+ } catch (error) {
+ console.error('Error fetching registered vendors:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDelete = async (vendorId: string) => {
+ setDeletingVendorId(vendorId);
+ setIsDeleteModalOpen(true);
+ };
+
+ const confirmDelete = async () => {
+ if (!deletingVendorId) return;
+
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/registered-vendors/${deletingVendorId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
+ }
+ });
+
+ if (response.ok) {
+ fetchRegisteredVendors();
+ setIsDeleteModalOpen(false);
+ setDeletingVendorId(null);
+ }
+ } catch (error) {
+ console.error('Error deleting registered vendor:', error);
+ }
+ };
+
+ const handleUpdate = async (vendorId: string, updateData: Partial) => {
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/registered-vendors/${vendorId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
+ },
+ body: JSON.stringify(updateData)
+ });
+
+ if (response.ok) {
+ fetchRegisteredVendors();
+ setEditingVendor(null);
+ }
+ } catch (error) {
+ console.error('Error updating registered vendor:', error);
+ }
+ };
+
+ const getStatusColor = (status?: string) => {
+ switch (status) {
+ case 'active':
+ return 'text-green-600 bg-green-100 dark:bg-green-900';
+ case 'pending':
+ return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
+ case 'suspended':
+ return 'text-red-600 bg-red-100 dark:bg-red-900';
+ default:
+ return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
+ }
+ };
+
+ const getTierColor = (tier?: string) => {
+ switch (tier) {
+ case 'diamond':
+ return 'text-purple-600 bg-purple-100 dark:bg-purple-900';
+ case 'platinum':
+ return 'text-blue-600 bg-blue-100 dark:bg-blue-900';
+ case 'gold':
+ return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
+ case 'silver':
+ return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
+ case 'bronze':
+ return 'text-orange-600 bg-orange-100 dark:bg-orange-900';
+ default:
+ return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
+ }
+ };
+
+ const filteredVendors = vendors.filter(vendor => {
+ const matchesSearch = vendor.companyName.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ vendor.contactEmail.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesStatus = statusFilter === 'all' || vendor.status === statusFilter;
+ return matchesSearch && matchesStatus;
+ });
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ Registered Vendors
+
+
+ Manage vendors who have partnered with us
+
+
+
+ {/* Filters */}
+
+
+ {/* Search */}
+
+
+
+ setSearchTerm(e.target.value)}
+ 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"
+ />
+
+
+
+ {/* Status Filter */}
+
+
+ setStatusFilter(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"
+ >
+ All Status
+ Active
+ Pending
+ Suspended
+
+
+
+
+
+ {/* Vendors Table */}
+
+
+
+
+
+
+ Vendor
+
+
+ Contact
+
+
+ Tier
+
+
+ Status
+
+
+ Commission
+
+
+ Joined
+
+
+ Actions
+
+
+
+
+ {filteredVendors.map((vendor) => (
+
+
+
+
+
+
+
+
+ {vendor.companyName}
+
+
+ {vendor.companyType}
+
+
+
+
+
+
+
+
+ {vendor.contactEmail}
+
+
+
+ {vendor.contactPhone}
+
+
+
+
+
+ {vendor.tier?.charAt(0).toUpperCase() + vendor.tier?.slice(1) || 'Unknown'}
+
+
+
+
+ {vendor.status?.charAt(0).toUpperCase() + vendor.status?.slice(1) || 'Unknown'}
+
+
+
+ {vendor.commissionRate}%
+
+
+ {new Date(vendor.createdAt).toLocaleDateString()}
+
+
+
+ {
+ setSelectedVendor(vendor);
+ setShowModal(true);
+ }}
+ className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
+ >
+
+
+ setEditingVendor(vendor)}
+ className="text-green-600 hover:text-green-900 dark:hover:text-green-400"
+ >
+
+
+ handleDelete(vendor.id)}
+ className="text-red-600 hover:text-red-900 dark:hover:text-red-400"
+ >
+
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* Vendor Details Modal */}
+ {showModal && selectedVendor && (
+
+
+
+
+ Vendor Details
+
+
+
+
+ Company Name
+
+
{selectedVendor.companyName}
+
+
+
+ Company Type
+
+
{selectedVendor.companyType}
+
+
+
+ Contact Email
+
+
{selectedVendor.contactEmail}
+
+
+
+ Contact Phone
+
+
{selectedVendor.contactPhone}
+
+
+
+ Website
+
+
{selectedVendor.website || 'N/A'}
+
+
+
+ Tier
+
+
+ {selectedVendor.tier?.charAt(0).toUpperCase() + selectedVendor.tier?.slice(1) || 'Unknown'}
+
+
+
+
+ Status
+
+
+ {selectedVendor.status?.charAt(0).toUpperCase() + selectedVendor.status?.slice(1) || 'Unknown'}
+
+
+
+
+ Commission Rate
+
+
{selectedVendor.commissionRate}%
+
+
+
+ Created At
+
+
{new Date(selectedVendor.createdAt).toLocaleString()}
+
+
+
+
+ setShowModal(false)}
+ 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"
+ >
+ Close
+
+
+
+
+ )}
+
+ {/* Edit Vendor Modal */}
+ {editingVendor && (
+
+
+
+
+ Edit Vendor
+
+
{
+ e.preventDefault();
+ const formData = new FormData(e.currentTarget);
+ const updateData = {
+ companyName: formData.get('companyName') as string,
+ contactEmail: formData.get('contactEmail') as string,
+ contactPhone: formData.get('contactPhone') as string,
+ website: (formData.get('website') as string) || undefined,
+ tier: formData.get('tier') as string,
+ status: formData.get('status') as string,
+ commissionRate: parseFloat(formData.get('commissionRate') as string)
+ };
+ handleUpdate(editingVendor.id, updateData);
+ }}>
+
+
+
+ Company Name
+
+
+
+
+
+ Contact Email
+
+
+
+
+
+ Contact Phone
+
+
+
+
+
+ Website
+
+
+
+
+
+ Tier
+
+
+ Bronze
+ Silver
+ Gold
+ Platinum
+ Diamond
+
+
+
+
+ Status
+
+
+ Active
+ Pending
+ Suspended
+ Inactive
+
+
+
+
+ Commission Rate (%)
+
+
+
+
+
+ setEditingVendor(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
+
+
+ Update Vendor
+
+
+
+
+
+
+ )}
+
+ {/* Delete Confirmation Modal */}
+ {isDeleteModalOpen && (
+
+
+
Confirm Delete
+
+ Are you sure you want to delete this registered vendor? This action cannot be undone.
+
+
+ {
+ setIsDeleteModalOpen(false);
+ setDeletingVendorId(null);
+ }}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
+ >
+ Cancel
+
+
+ Delete Vendor
+
+
+
+
+ )}
+
+ );
+};
+
+export default RegisteredVendors;
\ No newline at end of file
diff --git a/src/pages/admin/Reports.tsx b/src/pages/admin/Reports.tsx
new file mode 100644
index 0000000..4f78bc3
--- /dev/null
+++ b/src/pages/admin/Reports.tsx
@@ -0,0 +1,379 @@
+import React, { useState, useEffect } from 'react';
+import {
+ FileText,
+ Download,
+ Calendar,
+ Filter,
+ Users,
+ Building,
+ Package,
+ DollarSign,
+ TrendingUp,
+ TrendingDown,
+ BarChart3,
+ PieChart,
+ Activity
+} from 'lucide-react';
+
+interface ReportData {
+ userReports: {
+ totalUsers: number;
+ newUsers: number;
+ activeUsers: number;
+ userGrowth: number;
+ };
+ vendorReports: {
+ totalVendors: number;
+ pendingVendors: number;
+ approvedVendors: number;
+ rejectedVendors: number;
+ };
+ revenueReports: {
+ totalRevenue: number;
+ monthlyRevenue: number;
+ revenueGrowth: number;
+ topProducts: Array<{
+ name: string;
+ revenue: number;
+ sales: number;
+ }>;
+ };
+ systemReports: {
+ uptime: number;
+ responseTime: number;
+ errorRate: number;
+ activeSessions: number;
+ };
+}
+
+const AdminReports: React.FC = () => {
+ const [reportData, setReportData] = useState({
+ userReports: { totalUsers: 0, newUsers: 0, activeUsers: 0, userGrowth: 0 },
+ vendorReports: { totalVendors: 0, pendingVendors: 0, approvedVendors: 0, rejectedVendors: 0 },
+ revenueReports: { totalRevenue: 0, monthlyRevenue: 0, revenueGrowth: 0, topProducts: [] },
+ systemReports: { uptime: 0, responseTime: 0, errorRate: 0, activeSessions: 0 }
+ });
+ const [loading, setLoading] = useState(true);
+ const [reportType, setReportType] = useState('overview');
+ const [dateRange, setDateRange] = useState('30d');
+
+ useEffect(() => {
+ fetchReportData();
+ }, [reportType, dateRange]);
+
+ const fetchReportData = async () => {
+ try {
+ setLoading(true);
+ // Mock data for now - replace with actual API call
+ const mockData: ReportData = {
+ userReports: {
+ totalUsers: 1247,
+ newUsers: 89,
+ activeUsers: 892,
+ userGrowth: 12.5
+ },
+ vendorReports: {
+ totalVendors: 156,
+ pendingVendors: 23,
+ approvedVendors: 128,
+ rejectedVendors: 5
+ },
+ revenueReports: {
+ totalRevenue: 284750,
+ monthlyRevenue: 45600,
+ revenueGrowth: 8.3,
+ topProducts: [
+ { name: 'Cloud Storage Pro', revenue: 45000, sales: 156 },
+ { name: 'Database Hosting', revenue: 38000, sales: 89 },
+ { name: 'Load Balancer', revenue: 32000, sales: 67 },
+ { name: 'CDN Service', revenue: 28000, sales: 45 },
+ { name: 'Backup Solution', revenue: 25000, sales: 34 }
+ ]
+ },
+ systemReports: {
+ uptime: 99.9,
+ responseTime: 245,
+ errorRate: 0.1,
+ activeSessions: 456
+ }
+ };
+
+ setReportData(mockData);
+ } catch (error) {
+ console.error('Error fetching report data:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD'
+ }).format(amount);
+ };
+
+ const getGrowthIcon = (growth: number) => {
+ return growth >= 0 ? (
+
+ ) : (
+
+ );
+ };
+
+ const handleExportReport = (type: string) => {
+ // Mock export functionality
+ console.log(`Exporting ${type} report for ${dateRange}`);
+ // In real implementation, this would trigger a download
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Reports & Analytics
+
Comprehensive system reports and insights
+
+
+ setReportType(e.target.value)}
+ className="px-3 py-2 border border-gray-300 rounded-md bg-white dark:bg-gray-800 dark:border-gray-600"
+ >
+ Overview
+ User Reports
+ Vendor Reports
+ Revenue Reports
+ System Reports
+
+ setDateRange(e.target.value)}
+ className="px-3 py-2 border border-gray-300 rounded-md bg-white dark:bg-gray-800 dark:border-gray-600"
+ >
+ Last 7 days
+ Last 30 days
+ Last 90 days
+ Last year
+
+ handleExportReport(reportType)}
+ className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center space-x-2"
+ >
+
+ Export
+
+
+
+
+ {/* Overview Report */}
+ {reportType === 'overview' && (
+
+ {/* Key Metrics */}
+
+
+
+
+
Total Users
+
{(reportData.userReports.totalUsers || 0).toLocaleString()}
+
+ {getGrowthIcon(reportData.userReports.userGrowth)}
+ {reportData.userReports.userGrowth}% growth
+
+
+
+
+
+
+
+
+
+
Total Revenue
+
{formatCurrency(reportData.revenueReports.totalRevenue)}
+
+ {getGrowthIcon(reportData.revenueReports.revenueGrowth)}
+ {reportData.revenueReports.revenueGrowth}% growth
+
+
+
+
+
+
+
+
+
+
Active Vendors
+
{reportData.vendorReports.approvedVendors}
+
{reportData.vendorReports.pendingVendors} pending
+
+
+
+
+
+
+
+
+
System Uptime
+
{reportData.systemReports.uptime}%
+
{reportData.systemReports.activeSessions} active sessions
+
+
+
+
+
+
+ {/* Top Products */}
+
+
+
+
+ Top Performing Products
+
+
+
+ {reportData.revenueReports.topProducts.map((product, index) => (
+
+
+
+ {index + 1}
+
+
+
{product.name}
+
{product.sales} sales
+
+
+
+
{formatCurrency(product.revenue)}
+
+
+ ))}
+
+
+
+ )}
+
+ {/* User Reports */}
+ {reportType === 'users' && (
+
+
+
+
+ User Statistics
+
+
+
+
{reportData.userReports.totalUsers}
+
Total Users
+
+
+
{reportData.userReports.newUsers}
+
New This Month
+
+
+
{reportData.userReports.activeUsers}
+
Active Users
+
+
+
+
+ )}
+
+ {/* Vendor Reports */}
+ {reportType === 'vendors' && (
+
+
+
+
+ Vendor Statistics
+
+
+
+
{reportData.vendorReports.totalVendors}
+
Total Vendors
+
+
+
{reportData.vendorReports.pendingVendors}
+
Pending
+
+
+
{reportData.vendorReports.approvedVendors}
+
Approved
+
+
+
{reportData.vendorReports.rejectedVendors}
+
Rejected
+
+
+
+
+ )}
+
+ {/* Revenue Reports */}
+ {reportType === 'revenue' && (
+
+
+
+
+ Revenue Statistics
+
+
+
+
{formatCurrency(reportData.revenueReports.totalRevenue)}
+
Total Revenue
+
+
+
{formatCurrency(reportData.revenueReports.monthlyRevenue)}
+
This Month
+
+
+
{reportData.revenueReports.revenueGrowth}%
+
Growth Rate
+
+
+
+
+ )}
+
+ {/* System Reports */}
+ {reportType === 'system' && (
+
+
+
+
+ System Health
+
+
+
+
{reportData.systemReports.uptime}%
+
Uptime
+
+
+
{reportData.systemReports.responseTime}ms
+
Response Time
+
+
+
{reportData.systemReports.errorRate}%
+
Error Rate
+
+
+
{reportData.systemReports.activeSessions}
+
Active Sessions
+
+
+
+
+ )}
+
+ );
+};
+
+export default AdminReports;
\ No newline at end of file
diff --git a/src/pages/admin/Resellers.tsx b/src/pages/admin/Resellers.tsx
new file mode 100644
index 0000000..55837e9
--- /dev/null
+++ b/src/pages/admin/Resellers.tsx
@@ -0,0 +1,520 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Users,
+ Building,
+ Plus,
+ Edit,
+ Trash2,
+ Eye,
+ Search,
+ Filter,
+ MapPin,
+ Mail,
+ Phone,
+ Globe,
+ TrendingUp,
+ Calendar,
+ Link
+} from 'lucide-react';
+
+interface Reseller {
+ id: string;
+ companyName: string;
+ contactEmail: string;
+ contactPhone: string;
+ website: string;
+ tier: string;
+ status: string;
+ commissionRate: number;
+ channelPartnerId: string;
+ channelPartner?: {
+ id: string;
+ companyName: string;
+ status: string;
+ };
+ users?: Array<{
+ id: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+ role: string;
+ status: string;
+ }>;
+ createdAt: string;
+ approvedAt?: string;
+}
+
+const Resellers: React.FC = () => {
+ const [resellers, setResellers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'pending' | 'suspended'>('all');
+ const [vendorFilter, setVendorFilter] = useState('all');
+ const [vendors, setVendors] = useState>([]);
+ const [selectedReseller, setSelectedReseller] = useState(null);
+ const [showModal, setShowModal] = useState(false);
+ const [editingReseller, setEditingReseller] = useState(null);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [deletingResellerId, setDeletingResellerId] = useState(null);
+
+ useEffect(() => {
+ fetchResellers();
+ fetchVendors();
+ }, []);
+
+ const fetchVendors = async () => {
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/active-vendors`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ const data = await response.json();
+
+ if (data.success) {
+ setVendors(data.data);
+ }
+ } catch (error) {
+ console.error('Error fetching vendors:', error);
+ }
+ };
+
+ const fetchResellers = async () => {
+ try {
+ const params = new URLSearchParams();
+ if (vendorFilter !== 'all') {
+ params.append('vendor', vendorFilter);
+ }
+
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/resellers?${params}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+ const data = await response.json();
+
+ if (data.success) {
+ setResellers(data.data);
+ }
+ } catch (error) {
+ console.error('Error fetching resellers:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Refetch resellers when vendor filter changes
+ useEffect(() => {
+ if (vendors.length > 0) {
+ fetchResellers();
+ }
+ }, [vendorFilter]);
+
+ const handleDelete = async (resellerId: string) => {
+ setDeletingResellerId(resellerId);
+ setIsDeleteModalOpen(true);
+ };
+
+ const confirmDelete = async () => {
+ if (!deletingResellerId) return;
+
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/resellers/${deletingResellerId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
+ }
+ });
+
+ if (response.ok) {
+ fetchResellers();
+ setIsDeleteModalOpen(false);
+ setDeletingResellerId(null);
+ }
+ } catch (error) {
+ console.error('Error deleting reseller:', error);
+ }
+ };
+
+ const handleUpdate = async (resellerId: string, updateData: Partial) => {
+ try {
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/resellers/${resellerId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
+ },
+ body: JSON.stringify(updateData)
+ });
+
+ if (response.ok) {
+ fetchResellers();
+ setEditingReseller(null);
+ }
+ } catch (error) {
+ console.error('Error updating reseller:', error);
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'active':
+ return 'text-green-600 bg-green-100 dark:bg-green-900';
+ case 'pending':
+ return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
+ case 'suspended':
+ return 'text-red-600 bg-red-100 dark:bg-red-900';
+ default:
+ return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
+ }
+ };
+
+ const getTierColor = (tier: string) => {
+ switch (tier) {
+ case 'diamond':
+ return 'text-purple-600 bg-purple-100 dark:bg-purple-900';
+ case 'platinum':
+ return 'text-blue-600 bg-blue-100 dark:bg-blue-900';
+ case 'gold':
+ return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
+ case 'silver':
+ return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
+ case 'bronze':
+ return 'text-orange-600 bg-orange-100 dark:bg-orange-900';
+ default:
+ return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
+ }
+ };
+
+ const filteredResellers = resellers.filter(reseller => {
+ const matchesSearch = reseller.companyName.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ reseller.contactEmail.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesStatus = statusFilter === 'all' || reseller.status === statusFilter;
+ const matchesVendor = vendorFilter === 'all' || reseller.channelPartnerId === vendorFilter;
+ return matchesSearch && matchesStatus && matchesVendor;
+ });
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ Resellers
+
+
+ Manage resellers who register with our vendors
+
+
+
+ {/* Filters */}
+
+
+ {/* Search */}
+
+
+
+ setSearchTerm(e.target.value)}
+ 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"
+ />
+
+
+
+ {/* Status Filter */}
+
+
+ setStatusFilter(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"
+ >
+ All Status
+ Active
+ Pending
+ Suspended
+
+
+
+ {/* Vendor Filter */}
+
+
+ setVendorFilter(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"
+ >
+ All Vendors
+ {vendors.map((vendor) => (
+
+ {vendor.company}
+
+ ))}
+
+
+
+
+
+ {/* Resellers Table */}
+
+
+
+
+
+
+ Reseller
+
+
+ Contact
+
+
+ Vendor
+
+
+ Tier
+
+
+ Status
+
+
+ Commission
+
+
+ Joined
+
+
+ Actions
+
+
+
+
+ {filteredResellers.map((reseller) => (
+
+
+
+
+
+
+
+
+ {reseller.companyName}
+
+
+ Reseller
+
+
+
+
+
+
+
+
+ {reseller.contactEmail}
+
+
+
+ {reseller.contactPhone}
+
+
+
+
+
+
+
+ {reseller.channelPartner?.companyName || 'Unknown Vendor'}
+
+
+
+
+
+ {reseller.tier?.charAt(0).toUpperCase() + reseller.tier?.slice(1) || 'Unknown'}
+
+
+
+
+ {reseller.status?.charAt(0).toUpperCase() + reseller.status?.slice(1) || 'Unknown'}
+
+
+
+ {reseller.commissionRate}%
+
+
+ {new Date(reseller.createdAt).toLocaleDateString()}
+
+
+
+ {
+ setSelectedReseller(reseller);
+ setShowModal(true);
+ }}
+ className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
+ >
+
+
+ setEditingReseller(reseller)}
+ className="text-green-600 hover:text-green-900 dark:hover:text-green-400"
+ >
+
+
+ handleDelete(reseller.id)}
+ className="text-red-600 hover:text-red-900 dark:hover:text-red-400"
+ >
+
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* Reseller Details Modal */}
+ {showModal && selectedReseller && (
+
+
+
+
+ Reseller Details
+
+
+
+
+ Company Name
+
+
{selectedReseller.companyName}
+
+
+
+ Contact Email
+
+
{selectedReseller.contactEmail}
+
+
+
+ Contact Phone
+
+
{selectedReseller.contactPhone}
+
+
+
+ Website
+
+
{selectedReseller.website || 'N/A'}
+
+
+
+ Associated Vendor
+
+
{selectedReseller.channelPartner?.companyName || 'Unknown Vendor'}
+
+
+
+ Tier
+
+
+ {selectedReseller.tier?.charAt(0).toUpperCase() + selectedReseller.tier?.slice(1) || 'Unknown'}
+
+
+
+
+ Status
+
+
+ {selectedReseller.status?.charAt(0).toUpperCase() + selectedReseller.status?.slice(1) || 'Unknown'}
+
+
+
+
+ Commission Rate
+
+
{selectedReseller.commissionRate}%
+
+
+
+ Created At
+
+
{new Date(selectedReseller.createdAt).toLocaleString()}
+
+ {selectedReseller.users && selectedReseller.users.length > 0 && (
+
+
+ Associated Users
+
+
+ {selectedReseller.users.map((user) => (
+
+
+ {user.firstName} {user.lastName} ({user.email})
+
+
+ {user.status}
+
+
+ ))}
+
+
+ )}
+
+
+
+ setShowModal(false)}
+ 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"
+ >
+ Close
+
+
+
+
+ )}
+
+ {/* Delete Confirmation Modal */}
+ {isDeleteModalOpen && (
+
+
+
Confirm Delete
+
+ Are you sure you want to delete this reseller? This action cannot be undone.
+
+
+ {
+ setIsDeleteModalOpen(false);
+ setDeletingResellerId(null);
+ }}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
+ >
+ Cancel
+
+
+ Delete Reseller
+
+
+
+
+ )}
+
+ );
+};
+
+export default Resellers;
\ No newline at end of file
diff --git a/src/pages/admin/Settings.tsx b/src/pages/admin/Settings.tsx
new file mode 100644
index 0000000..768bd6e
--- /dev/null
+++ b/src/pages/admin/Settings.tsx
@@ -0,0 +1,617 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Settings,
+ Save,
+ Shield,
+ Bell,
+ Database,
+ Globe,
+ Users,
+ Key,
+ Eye,
+ EyeOff,
+ CheckCircle,
+ AlertCircle
+} from 'lucide-react';
+
+interface SystemSettings {
+ general: {
+ siteName: string;
+ siteDescription: string;
+ maintenanceMode: boolean;
+ defaultLanguage: string;
+ timezone: string;
+ };
+ security: {
+ sessionTimeout: number;
+ maxLoginAttempts: number;
+ requireTwoFactor: boolean;
+ passwordMinLength: number;
+ enableAuditLog: boolean;
+ };
+ email: {
+ smtpHost: string;
+ smtpPort: number;
+ smtpUser: string;
+ smtpPassword: string;
+ fromEmail: string;
+ fromName: string;
+ };
+ notifications: {
+ emailNotifications: boolean;
+ pushNotifications: boolean;
+ vendorApprovalAlerts: boolean;
+ systemAlerts: boolean;
+ reportAlerts: boolean;
+ };
+ integrations: {
+ enableAnalytics: boolean;
+ enableLogging: boolean;
+ enableBackup: boolean;
+ backupFrequency: string;
+ };
+}
+
+const AdminSettings: React.FC = () => {
+ const [settings, setSettings] = useState({
+ general: {
+ siteName: 'Cloutopiaa Reseller Portal',
+ siteDescription: 'Cloud services reseller management platform',
+ maintenanceMode: false,
+ defaultLanguage: 'en',
+ timezone: 'UTC'
+ },
+ security: {
+ sessionTimeout: 30,
+ maxLoginAttempts: 5,
+ requireTwoFactor: false,
+ passwordMinLength: 8,
+ enableAuditLog: true
+ },
+ email: {
+ smtpHost: 'smtp.gmail.com',
+ smtpPort: 587,
+ smtpUser: '',
+ smtpPassword: '',
+ fromEmail: 'noreply@cloutopiaa.com',
+ fromName: 'Cloutopiaa Admin'
+ },
+ notifications: {
+ emailNotifications: true,
+ pushNotifications: true,
+ vendorApprovalAlerts: true,
+ systemAlerts: true,
+ reportAlerts: false
+ },
+ integrations: {
+ enableAnalytics: true,
+ enableLogging: true,
+ enableBackup: true,
+ backupFrequency: 'daily'
+ }
+ });
+
+ const [activeTab, setActiveTab] = useState('general');
+ const [loading, setLoading] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
+
+ useEffect(() => {
+ fetchSettings();
+ }, []);
+
+ const fetchSettings = async () => {
+ try {
+ setLoading(true);
+ // Mock API call - replace with actual implementation
+ // const response = await fetch('/api/admin/settings');
+ // const data = await response.json();
+ // setSettings(data);
+ } catch (error) {
+ console.error('Error fetching settings:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSave = async () => {
+ try {
+ setSaveStatus('saving');
+ // Mock API call - replace with actual implementation
+ // const response = await fetch('/api/admin/settings', {
+ // method: 'PUT',
+ // headers: { 'Content-Type': 'application/json' },
+ // body: JSON.stringify(settings)
+ // });
+
+ // Simulate API delay
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ setSaveStatus('success');
+ setTimeout(() => setSaveStatus('idle'), 3000);
+ } catch (error) {
+ console.error('Error saving settings:', error);
+ setSaveStatus('error');
+ setTimeout(() => setSaveStatus('idle'), 3000);
+ }
+ };
+
+ const updateSetting = (section: keyof SystemSettings, key: string, value: any) => {
+ setSettings(prev => ({
+ ...prev,
+ [section]: {
+ ...prev[section],
+ [key]: value
+ }
+ }));
+ };
+
+ const tabs = [
+ { id: 'general', name: 'General', icon: Settings },
+ { id: 'security', name: 'Security', icon: Shield },
+ { id: 'email', name: 'Email', icon: Bell },
+ { id: 'notifications', name: 'Notifications', icon: Bell },
+ { id: 'integrations', name: 'Integrations', icon: Database }
+ ];
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
System Settings
+
Configure system-wide settings and preferences
+
+
+ {saveStatus === 'saving' ? (
+
+ ) : saveStatus === 'success' ? (
+
+ ) : saveStatus === 'error' ? (
+
+ ) : (
+
+ )}
+
+ {saveStatus === 'saving' ? 'Saving...' :
+ saveStatus === 'success' ? 'Saved!' :
+ saveStatus === 'error' ? 'Error' : 'Save Changes'}
+
+
+
+
+ {/* Tabs */}
+
+
+ {tabs.map((tab) => {
+ const Icon = tab.icon;
+ return (
+ setActiveTab(tab.id)}
+ className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 whitespace-nowrap ${
+ activeTab === tab.id
+ ? 'border-blue-500 text-blue-600'
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
+ }`}
+ >
+
+ {tab.name}
+
+ );
+ })}
+
+
+
+ {/* Tab Content */}
+
+ {/* General Settings */}
+ {activeTab === 'general' && (
+
+
+
+ General Settings
+
+
+
+
+
+ Site Name
+
+ updateSetting('general', 'siteName', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+ Site Description
+
+ updateSetting('general', 'siteDescription', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+ Default Language
+
+ updateSetting('general', 'defaultLanguage', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ English
+ Spanish
+ French
+ German
+
+
+
+
+
+ Timezone
+
+ updateSetting('general', 'timezone', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ UTC
+ Eastern Time
+ Pacific Time
+ GMT
+
+
+
+
+
+ updateSetting('general', 'maintenanceMode', e.target.checked)}
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
+ />
+
+ Enable Maintenance Mode
+
+
+
+ )}
+
+ {/* Security Settings */}
+ {activeTab === 'security' && (
+
+
+
+ Security Settings
+
+
+
+
+
+
+ )}
+
+ {/* Email Settings */}
+ {activeTab === 'email' && (
+
+
+
+ Email Configuration
+
+
+
+
+
+ SMTP Host
+
+ updateSetting('email', 'smtpHost', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+ SMTP Port
+
+ updateSetting('email', 'smtpPort', parseInt(e.target.value))}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+ SMTP Username
+
+ updateSetting('email', 'smtpUser', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+ SMTP Password
+
+
+ updateSetting('email', 'smtpPassword', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10"
+ />
+ setShowPassword(!showPassword)}
+ className="absolute inset-y-0 right-0 pr-3 flex items-center"
+ >
+ {showPassword ? : }
+
+
+
+
+
+
+ From Email
+
+ updateSetting('email', 'fromEmail', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+ From Name
+
+ updateSetting('email', 'fromName', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+ )}
+
+ {/* Notification Settings */}
+ {activeTab === 'notifications' && (
+
+
+
+ Notification Preferences
+
+
+
+
+ updateSetting('notifications', 'emailNotifications', e.target.checked)}
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
+ />
+
+ Enable Email Notifications
+
+
+
+
+ updateSetting('notifications', 'pushNotifications', e.target.checked)}
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
+ />
+
+ Enable Push Notifications
+
+
+
+
+ updateSetting('notifications', 'vendorApprovalAlerts', e.target.checked)}
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
+ />
+
+ Vendor Approval Alerts
+
+
+
+
+ updateSetting('notifications', 'systemAlerts', e.target.checked)}
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
+ />
+
+ System Alerts
+
+
+
+
+ updateSetting('notifications', 'reportAlerts', e.target.checked)}
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
+ />
+
+ Report Generation Alerts
+
+
+
+
+ )}
+
+ {/* Integration Settings */}
+ {activeTab === 'integrations' && (
+
+
+
+ System Integrations
+
+
+
+
+ updateSetting('integrations', 'enableAnalytics', e.target.checked)}
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
+ />
+
+ Enable Analytics Tracking
+
+
+
+
+ updateSetting('integrations', 'enableLogging', e.target.checked)}
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
+ />
+
+ Enable System Logging
+
+
+
+
+ updateSetting('integrations', 'enableBackup', e.target.checked)}
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
+ />
+
+ Enable Automated Backups
+
+
+
+
+
+ Backup Frequency
+
+ updateSetting('integrations', 'backupFrequency', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ Daily
+ Weekly
+ Monthly
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default AdminSettings;
\ No newline at end of file
diff --git a/src/pages/admin/VendorRequests.tsx b/src/pages/admin/VendorRequests.tsx
index 4fc2e0c..4b43823 100644
--- a/src/pages/admin/VendorRequests.tsx
+++ b/src/pages/admin/VendorRequests.tsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
+import { useAppSelector } from '../../store/hooks';
import {
Users,
Clock,
@@ -11,94 +12,171 @@ import {
Building,
Mail,
Phone,
- MapPin
+ MapPin,
+ AlertCircle
} from 'lucide-react';
-
-interface VendorRequest {
- id: string;
- firstName: string;
- lastName: string;
- email: string;
- phone: string;
- company: string;
- role: string;
- userType: string;
- status: 'pending' | 'approved' | 'rejected';
- createdAt: string;
- rejectionReason?: string;
-}
+import { VendorRequest } from '../../types/vendor';
+import VendorDetailsModal from '../../components/VendorDetailsModal';
+import VendorRejectionModal from '../../components/VendorRejectionModal';
const VendorRequests: React.FC = () => {
- const [vendors, setVendors] = useState([]);
+ const [vendorRequests, setVendorRequests] = useState([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all');
const [selectedVendor, setSelectedVendor] = useState(null);
const [showModal, setShowModal] = useState(false);
+ const [rejectionReason, setRejectionReason] = useState('');
+ const [showRejectionModal, setShowRejectionModal] = useState(false);
+ const [error, setError] = useState(null);
+
+ // 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();
}, []);
+ // Check authentication on component mount
+ useEffect(() => {
+ if (!isAuthenticated || !token) {
+ console.error('User not authenticated');
+ setError('Please log in to access this page');
+ }
+ }, [isAuthenticated, token]);
+
const fetchVendorRequests = async () => {
try {
- const response = await fetch('/api/admin/pending-vendors', {
+ 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');
+ setError('Authentication required. Please log in again.');
+ return;
+ }
+
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/pending-vendors`, {
headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
}
});
+
+ // 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');
+ }
if (data.success) {
- setVendors(data.data);
+ setVendorRequests(data.data.pendingRequests || []);
+ } else {
+ setError(data.message || 'Failed to fetch vendor requests');
}
- } catch (error) {
+ } catch (error: any) {
console.error('Error fetching vendor requests:', error);
+ setError(error.message || 'Failed to fetch vendor requests');
} finally {
setLoading(false);
}
};
- const handleApprove = async (vendorId: string) => {
+ const approveVendorRequest = async (userId: string) => {
try {
- const response = await fetch(`/api/admin/vendors/${vendorId}/approve`, {
+ const token = localStorage.getItem('accessToken');
+ if (!token) {
+ console.error('No token found');
+ return;
+ }
+
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/vendors/${userId}/approve`, {
method: 'POST',
headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
- },
- body: JSON.stringify({ reason: 'Approved by admin' })
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
});
if (response.ok) {
fetchVendorRequests();
+ } else {
+ const errorData = await response.json();
+ console.error('Failed to approve vendor request:', errorData);
}
} catch (error) {
console.error('Error approving vendor:', error);
}
};
- const handleReject = async (vendorId: string, reason: string) => {
+ const rejectVendorRequest = async (userId: string, reason: string) => {
try {
- const response = await fetch(`/api/admin/vendors/${vendorId}/reject`, {
+ const token = localStorage.getItem('accessToken');
+ if (!token) {
+ console.error('No token found');
+ return;
+ }
+
+ const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/admin/vendors/${userId}/reject`, {
method: 'POST',
headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
},
body: JSON.stringify({ reason })
});
if (response.ok) {
fetchVendorRequests();
- setShowModal(false);
+ setShowRejectionModal(false);
+ } else {
+ const errorData = await response.json();
+ console.error('Failed to reject vendor request:', errorData);
}
} catch (error) {
console.error('Error rejecting vendor:', error);
}
};
- const filteredVendors = vendors.filter(vendor => {
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'pending':
+ return 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900';
+ case 'approved':
+ return 'text-green-600 bg-green-100 dark:bg-green-900';
+ case 'rejected':
+ return 'text-red-600 bg-red-100 dark:bg-red-900';
+ default:
+ return 'text-gray-600 bg-gray-100 dark:bg-gray-900';
+ }
+ };
+
+ const formatRoleName = (role: string) => {
+ if (role.startsWith('channel_partner_')) {
+ return 'Vendor';
+ } else if (role.startsWith('reseller_')) {
+ return 'Reseller';
+ } else if (role.startsWith('system_')) {
+ return 'System Admin';
+ }
+ return role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+ };
+
+ const filteredVendors = vendorRequests.filter(vendor => {
const matchesSearch = vendor.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
vendor.lastName.toLowerCase().includes(searchTerm.toLowerCase()) ||
vendor.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -109,325 +187,192 @@ const VendorRequests: React.FC = () => {
return matchesSearch && matchesStatus;
});
- const getStatusColor = (status: string) => {
- switch (status) {
- case 'pending': return 'text-amber-600 bg-amber-100 dark:text-amber-400 dark:bg-amber-900';
- case 'approved': return 'text-green-600 bg-green-100 dark:text-green-400 dark:bg-green-900';
- case 'rejected': return 'text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-900';
- default: return 'text-slate-600 bg-slate-100 dark:text-slate-400 dark:bg-slate-700';
- }
- };
-
if (loading) {
return (
-
-
+
);
}
return (
-
-
- {/* Header */}
-
-
- Vendor Requests
-
-
- Review and manage vendor registration requests
-
+
+ {/* Header */}
+
+
+ Vendor Requests
+
+
+ Review and manage vendor registration requests
+
+
+
+ {/* Error Display */}
+ {error && (
+
+ )}
- {/* Filters */}
-
-
- {/* Search */}
-
-
-
- setSearchTerm(e.target.value)}
- className="w-full pl-10 pr-4 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"
- />
-
-
-
- {/* Status Filter */}
-
-
-
setStatusFilter(e.target.value as any)}
- className="px-4 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"
- >
- All Status
- Pending
- Approved
- Rejected
-
+ {/* Filters */}
+
+
- {/* Vendor List */}
-
-
-
- Vendor Requests ({filteredVendors.length})
-
-
-
-
-
-
-
-
- Vendor
-
-
- Company
-
-
- Role
-
-
- Status
-
-
- Date
-
-
- Actions
-
-
-
-
- {filteredVendors.map((vendor) => (
-
-
-
-
-
-
-
-
- {vendor.firstName} {vendor.lastName}
-
-
- {vendor.email}
-
-
-
-
-
-
-
-
- {vendor.company}
-
-
-
-
-
- {vendor.role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
-
-
-
-
- {vendor.status.charAt(0).toUpperCase() + vendor.status.slice(1)}
-
-
-
-
-
- {new Date(vendor.createdAt).toLocaleDateString()}
-
-
-
-
- setSelectedVendor(vendor)}
- className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
- title="View Details"
- >
-
-
- {vendor.status === 'pending' && (
- <>
- handleApprove(vendor.id)}
- className="p-2 bg-green-100 dark:bg-green-900 rounded-lg hover:bg-green-200 dark:hover:bg-green-800 transition-colors"
- title="Approve"
- >
-
-
- {
- setSelectedVendor(vendor);
- setShowModal(true);
- }}
- className="p-2 bg-red-100 dark:bg-red-900 rounded-lg hover:bg-red-200 dark:hover:bg-red-800 transition-colors"
- title="Reject"
- >
-
-
- >
- )}
-
-
-
- ))}
-
-
+ {/* Status Filter */}
+
+
+ setStatusFilter(e.target.value as any)}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
+ >
+ All Status
+ Pending
+ Approved
+ Rejected
+
+
- {/* Vendor Details Modal */}
- {selectedVendor && (
-
-
-
-
- Vendor Details
-
- setSelectedVendor(null)}
- className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
- >
- ✕
-
-
+ {/* Vendor List */}
+
+
+
+ Vendor Requests ({filteredVendors.length})
+
+
-
-
-
-
- Full Name
-
-
- {selectedVendor.firstName} {selectedVendor.lastName}
-
-
-
-
- Email
-
+
+
+
+
+
+ Vendor
+
+
+ Company
+
+
+ Role
+
+
+ Status
+
+
+ Date
+
+
+ Actions
+
+
+
+
+ {filteredVendors.map((vendor) => (
+
+
-
-
{selectedVendor.email}
+
+
+
+
+
+ {vendor.firstName} {vendor.lastName}
+
+
+ {vendor.email}
+
+
-
-
-
- Phone
-
-
-
-
{selectedVendor.phone}
+
+
+ {vendor.company}
+
+
+
+ {formatRoleName(vendor.role)}
-
-
-
- Company
-
-
-
-
{selectedVendor.company}
-
-
-
-
- Role
-
-
- {selectedVendor.role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
-
-
-
-
- User Type
-
-
- {selectedVendor.userType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
-
-
-
-
- {selectedVendor.rejectionReason && (
-
-
- Rejection Reason
-
-
{selectedVendor.rejectionReason}
-
- )}
-
-
-
- Registered on {new Date(selectedVendor.createdAt).toLocaleDateString()}
-
- {selectedVendor.status === 'pending' && (
+
+
+
+ {vendor.status.charAt(0).toUpperCase() + vendor.status.slice(1)}
+
+
+
+ {new Date(vendor.createdAt).toLocaleDateString()}
+
+
handleApprove(selectedVendor.id)}
- className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
+ onClick={() => {
+ console.log('View button clicked for vendor:', vendor);
+ setSelectedVendor(vendor);
+ setShowModal(true);
+ console.log('Modal state set to true');
+ }}
+ className="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
>
- Approve
-
- setShowModal(true)}
- className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
- >
- Reject
+
+ {vendor.status === 'pending' && (
+ <>
+ approveVendorRequest(vendor.id)}
+ className="text-green-600 hover:text-green-900 dark:hover:text-green-400"
+ >
+
+
+ {
+ setSelectedVendor(vendor);
+ setShowRejectionModal(true);
+ }}
+ className="text-red-600 hover:text-red-900 dark:hover:text-red-400"
+ >
+
+
+ >
+ )}
- )}
-
-
-
-
- )}
-
- {/* Rejection Modal */}
- {showModal && selectedVendor && (
-
-
-
- Reject Vendor Request
-
-
-
- {
- const reason = (document.getElementById('rejectionReason') as HTMLTextAreaElement).value;
- if (reason.trim()) {
- handleReject(selectedVendor.id, reason);
- }
- }}
- className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
- >
- Reject
-
- setShowModal(false)}
- className="flex-1 px-4 py-2 bg-slate-300 dark:bg-slate-600 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-400 dark:hover:bg-slate-500 transition-colors"
- >
- Cancel
-
-
-
-
- )}
+
+
+ ))}
+
+
+
+
+ {/* Shared Vendor Modals */}
+
setShowModal(false)}
+ onApprove={approveVendorRequest}
+ onReject={rejectVendorRequest}
+ />
+
+ setShowRejectionModal(false)}
+ onReject={rejectVendorRequest}
+ />
);
};
diff --git a/src/pages/reseller/Instances.tsx b/src/pages/reseller/Instances.tsx
index 44ad31b..34a4329 100644
--- a/src/pages/reseller/Instances.tsx
+++ b/src/pages/reseller/Instances.tsx
@@ -1,6 +1,6 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import {
- Cloud,
+ Package,
Plus,
Search,
Filter,
@@ -13,160 +13,248 @@ import {
Clock,
CheckCircle,
XCircle,
- AlertTriangle
+ AlertTriangle,
+ Eye,
+ ShoppingCart,
+ Star,
+ Tag
} from 'lucide-react';
+import { apiService, Product } from '../../services/api';
-interface Instance {
- id: string;
- name: string;
- type: string;
- status: 'running' | 'stopped' | 'starting' | 'stopping' | 'error';
- region: string;
- cpu: string;
- memory: string;
- storage: string;
- ipAddress: string;
- customer: string;
- monthlyCost: number;
- createdAt: string;
- lastStarted: string;
+interface AvailableProduct extends Product {
+ vendorName?: string;
+ vendorCompany?: string;
+ stockAvailable?: number;
+ commissionEarned?: number;
}
-const mockInstances: Instance[] = [
- {
- id: '1',
- name: 'web-server-01',
- type: 't3.medium',
- status: 'running',
- region: 'us-east-1',
- cpu: '2 vCPU',
- memory: '4 GB',
- storage: '20 GB SSD',
- ipAddress: '192.168.1.100',
- customer: 'TechCorp Solutions',
- monthlyCost: 35.50,
- createdAt: '2024-12-01T00:00:00Z',
- lastStarted: '2025-01-15T08:00:00Z'
- },
- {
- id: '2',
- name: 'db-server-01',
- type: 't3.large',
- status: 'running',
- region: 'us-east-1',
- cpu: '2 vCPU',
- memory: '8 GB',
- storage: '100 GB SSD',
- ipAddress: '192.168.1.101',
- customer: 'DataFlow Inc',
- monthlyCost: 70.25,
- createdAt: '2024-11-15T00:00:00Z',
- lastStarted: '2025-01-14T06:30:00Z'
- },
- {
- id: '3',
- name: 'app-server-01',
- type: 't3.small',
- status: 'stopped',
- region: 'us-west-2',
- cpu: '2 vCPU',
- memory: '2 GB',
- storage: '20 GB SSD',
- ipAddress: '192.168.1.102',
- customer: 'CloudTech Ltd',
- monthlyCost: 17.75,
- createdAt: '2024-10-20T00:00:00Z',
- lastStarted: '2025-01-10T14:20:00Z'
- },
- {
- id: '4',
- name: 'cache-server-01',
- type: 't3.micro',
- status: 'running',
- region: 'us-east-1',
- cpu: '2 vCPU',
- memory: '1 GB',
- storage: '8 GB SSD',
- ipAddress: '192.168.1.103',
- customer: 'InnovateSoft',
- monthlyCost: 8.90,
- createdAt: '2024-12-10T00:00:00Z',
- lastStarted: '2025-01-15T09:15:00Z'
- },
- {
- id: '5',
- name: 'backup-server-01',
- type: 't3.medium',
- status: 'error',
- region: 'us-west-2',
- cpu: '2 vCPU',
- memory: '4 GB',
- storage: '500 GB SSD',
- ipAddress: '192.168.1.104',
- customer: 'NetSolutions',
- monthlyCost: 45.00,
- createdAt: '2024-09-05T00:00:00Z',
- lastStarted: '2025-01-12T22:45:00Z'
- }
-];
-
-const Instances: React.FC = () => {
- const [instances, setInstances] = useState
(mockInstances);
+const AvailableProducts: React.FC = () => {
+ const [products, setProducts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
+ const [categoryFilter, setCategoryFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
- const [regionFilter, setRegionFilter] = useState('all');
+ const [priceFilter, setPriceFilter] = useState('all');
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(12);
+ const [totalItems, setTotalItems] = useState(0);
- const filteredInstances = instances.filter(instance => {
- const matchesSearch = instance.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- instance.customer.toLowerCase().includes(searchTerm.toLowerCase()) ||
- instance.ipAddress.includes(searchTerm);
- const matchesStatus = statusFilter === 'all' || instance.status === statusFilter;
- const matchesRegion = regionFilter === 'all' || instance.region === regionFilter;
- return matchesSearch && matchesStatus && matchesRegion;
+ // Mock data for demonstration - in real app this would come from API
+ const mockProducts: AvailableProduct[] = [
+ {
+ id: 1,
+ name: 'AWS EC2 t3.medium Instance',
+ description: 'High-performance cloud computing instance with 2 vCPUs and 4GB RAM',
+ category: 'cloud_computing',
+ price: 35.50,
+ currency: 'USD',
+ commissionRate: 15,
+ status: 'active',
+ availability: 'available',
+ stockQuantity: 100,
+ sku: 'AWS-EC2-T3M',
+ vendorName: 'John Smith',
+ vendorCompany: 'CloudTech Solutions',
+ stockAvailable: 100,
+ commissionEarned: 5.33,
+ createdBy: 1,
+ createdAt: '2024-12-01T00:00:00Z',
+ updatedAt: '2024-12-01T00:00:00Z'
+ },
+ {
+ id: 2,
+ name: 'Google Cloud Storage 100GB',
+ description: 'Scalable cloud storage solution with high availability and redundancy',
+ category: 'cloud_storage',
+ price: 20.00,
+ currency: 'USD',
+ commissionRate: 12,
+ status: 'active',
+ availability: 'available',
+ stockQuantity: 500,
+ sku: 'GCS-100GB',
+ vendorName: 'Sarah Johnson',
+ vendorCompany: 'DataFlow Inc',
+ stockAvailable: 500,
+ commissionEarned: 2.40,
+ createdBy: 1,
+ createdAt: '2024-11-15T00:00:00Z',
+ updatedAt: '2024-11-15T00:00:00Z'
+ },
+ {
+ id: 3,
+ name: 'Microsoft Azure Security Center',
+ description: 'Advanced threat protection and security management for cloud workloads',
+ category: 'cybersecurity',
+ price: 150.00,
+ currency: 'USD',
+ commissionRate: 18,
+ status: 'active',
+ availability: 'available',
+ stockQuantity: 50,
+ sku: 'AZURE-SEC',
+ vendorName: 'Mike Chen',
+ vendorCompany: 'SecureNet Pro',
+ stockAvailable: 50,
+ commissionEarned: 27.00,
+ createdBy: 1,
+ createdAt: '2024-10-20T00:00:00Z',
+ updatedAt: '2024-10-20T00:00:00Z'
+ },
+ {
+ id: 4,
+ name: 'IBM Watson AI Platform',
+ description: 'Enterprise AI and machine learning platform with cognitive services',
+ category: 'ai_ml',
+ price: 500.00,
+ currency: 'USD',
+ commissionRate: 20,
+ status: 'active',
+ availability: 'available',
+ stockQuantity: 25,
+ sku: 'IBM-WATSON',
+ vendorName: 'Lisa Wang',
+ vendorCompany: 'AI Innovations',
+ stockAvailable: 25,
+ commissionEarned: 100.00,
+ createdBy: 1,
+ createdAt: '2024-12-10T00:00:00Z',
+ updatedAt: '2024-12-10T00:00:00Z'
+ },
+ {
+ id: 5,
+ name: 'Oracle Database Cloud Service',
+ description: 'Fully managed Oracle database service with automated backups',
+ category: 'data_analytics',
+ price: 300.00,
+ currency: 'USD',
+ commissionRate: 16,
+ status: 'active',
+ availability: 'available',
+ stockQuantity: 30,
+ sku: 'ORACLE-DB',
+ vendorName: 'David Brown',
+ vendorCompany: 'Database Experts',
+ stockAvailable: 30,
+ commissionEarned: 48.00,
+ createdBy: 1,
+ createdAt: '2024-09-05T00:00:00Z',
+ updatedAt: '2024-09-05T00:00:00Z'
+ },
+ {
+ id: 6,
+ name: 'Cisco IoT Gateway',
+ description: 'Industrial IoT gateway for edge computing and device management',
+ category: 'iot',
+ price: 250.00,
+ currency: 'USD',
+ commissionRate: 14,
+ status: 'active',
+ availability: 'available',
+ stockQuantity: 40,
+ sku: 'CISCO-IOT',
+ vendorName: 'Alex Rodriguez',
+ vendorCompany: 'IoT Solutions',
+ stockAvailable: 40,
+ commissionEarned: 35.00,
+ createdBy: 1,
+ createdAt: '2024-11-20T00:00:00Z',
+ updatedAt: '2024-11-20T00:00:00Z'
+ }
+ ];
+
+ useEffect(() => {
+ // In real app, fetch products from API
+ // fetchProducts();
+ setProducts(mockProducts);
+ setTotalItems(mockProducts.length);
+ setLoading(false);
+ }, []);
+
+ const categories = [
+ { value: 'cloud_storage', label: 'Cloud Storage' },
+ { value: 'cloud_computing', label: 'Cloud Computing' },
+ { value: 'cybersecurity', label: 'Cybersecurity' },
+ { value: 'data_analytics', label: 'Data Analytics' },
+ { value: 'ai_ml', label: 'AI & Machine Learning' },
+ { value: 'iot', label: 'Internet of Things' },
+ { value: 'blockchain', label: 'Blockchain' },
+ { value: 'other', label: 'Other' }
+ ];
+
+ const priceRanges = [
+ { value: 'all', label: 'All Prices' },
+ { value: '0-50', label: '$0 - $50' },
+ { value: '51-100', label: '$51 - $100' },
+ { value: '101-200', label: '$101 - $200' },
+ { value: '201-500', label: '$201 - $500' },
+ { value: '500+', label: '$500+' }
+ ];
+
+ const filteredProducts = products.filter(product => {
+ const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ product.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ product.sku.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesCategory = categoryFilter === 'all' || product.category === categoryFilter;
+ const matchesStatus = statusFilter === 'all' || product.status === statusFilter;
+
+ let matchesPrice = true;
+ if (priceFilter !== 'all') {
+ const [min, max] = priceFilter.split('-').map(Number);
+ if (max) {
+ matchesPrice = product.price >= min && product.price <= max;
+ } else {
+ matchesPrice = product.price >= min;
+ }
+ }
+
+ return matchesSearch && matchesCategory && matchesStatus && matchesPrice;
});
- const getStatusColor = (status: string) => {
- switch (status) {
- case 'running':
- return 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-300';
- case 'stopped':
- return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
- case 'starting':
- case 'stopping':
- return 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-300';
- case 'error':
- return 'bg-danger-100 text-danger-800 dark:bg-danger-900 dark:text-danger-300';
+ const getCategoryColor = (category: string) => {
+ switch (category) {
+ case 'cloud_storage':
+ return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
+ case 'cloud_computing':
+ return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
+ case 'cybersecurity':
+ return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
+ case 'data_analytics':
+ return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300';
+ case 'ai_ml':
+ return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300';
+ case 'iot':
+ return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300';
+ case 'blockchain':
+ return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';
default:
- return 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-300';
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
}
};
- const getStatusIcon = (status: string) => {
- switch (status) {
- case 'running':
- return ;
- case 'stopped':
- return ;
- case 'starting':
- case 'stopping':
- return ;
- case 'error':
- return ;
+ const getCategoryIcon = (category: string) => {
+ switch (category) {
+ case 'cloud_storage':
+ return ;
+ case 'cloud_computing':
+ return ;
+ case 'cybersecurity':
+ return ;
+ case 'data_analytics':
+ return ;
+ case 'ai_ml':
+ return ;
+ case 'iot':
+ return ;
+ case 'blockchain':
+ return ;
default:
- return ;
+ return ;
}
};
- const formatDate = (dateString: string) => {
- return new Date(dateString).toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- });
- };
-
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
@@ -176,7 +264,51 @@ const Instances: React.FC = () => {
}).format(amount);
};
- const totalMonthlyCost = instances.reduce((sum, instance) => sum + instance.monthlyCost, 0);
+ const totalProducts = products.length;
+ const totalValue = products.reduce((sum, product) => sum + product.price, 0);
+ const totalCommission = products.reduce((sum, product) => sum + (product.price * product.commissionRate / 100), 0);
+ const availableCategories = new Set(products.map(p => p.category)).size;
+
+ const handleViewProduct = (product: AvailableProduct) => {
+ // Handle viewing product details
+ console.log('Viewing product:', product);
+ };
+
+ const handleAddToCart = (product: AvailableProduct) => {
+ // Handle adding product to cart
+ console.log('Adding to cart:', product);
+ };
+
+ const handleContactVendor = (product: AvailableProduct) => {
+ // Handle contacting vendor
+ console.log('Contacting vendor for:', product);
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ Error loading products
+
+
{error}
+
window.location.reload()}
+ className="btn btn-primary"
+ >
+ Try Again
+
+
+ );
+ }
return (
@@ -184,16 +316,34 @@ const Instances: React.FC = () => {
- Cloud Instances
+ Available Products
- Manage your cloud infrastructure and instances
+ Browse and manage products from your assigned vendor
-
-
- Create Instance
-
+
+
setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
+ className="btn btn-outline btn-sm"
+ >
+ {viewMode === 'grid' ? (
+ <>
+
+ Grid View
+ >
+ ) : (
+ <>
+
+ List View
+ >
+ )}
+
+
+
+ Request Product
+
+
{/* Stats Cards */}
@@ -202,14 +352,14 @@ const Instances: React.FC = () => {
- Total Instances
+ Total Products
- {instances.length}
+ {totalProducts}
@@ -218,14 +368,14 @@ const Instances: React.FC = () => {
- Running Instances
+ Total Value
- {instances.filter(i => i.status === 'running').length}
+ {formatCurrency(totalValue)}
-
+
@@ -234,14 +384,14 @@ const Instances: React.FC = () => {
- Monthly Cost
+ Commission Potential
- {formatCurrency(totalMonthlyCost)}
+ {formatCurrency(totalCommission)}
-
+
@@ -250,14 +400,14 @@ const Instances: React.FC = () => {
- Active Customers
+ Categories
- {new Set(instances.map(i => i.customer)).size}
+ {availableCategories}
-
+
@@ -271,7 +421,7 @@ const Instances: React.FC = () => {
setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
@@ -280,132 +430,246 @@ const Instances: React.FC = () => {
+ setCategoryFilter(e.target.value)}
+ className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
+ >
+ All Categories
+ {categories.map(category => (
+
+ {category.label}
+
+ ))}
+
+
setStatusFilter(e.target.value)}
className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
>
All Status
- Running
- Stopped
- Starting
- Stopping
- Error
+ Active
+ Inactive
+ Draft
-
+
setRegionFilter(e.target.value)}
+ value={priceFilter}
+ onChange={(e) => setPriceFilter(e.target.value)}
className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
>
- All Regions
- US East (N. Virginia)
- US West (Oregon)
- Europe (Ireland)
- Asia Pacific (Singapore)
+ {priceRanges.map(range => (
+
+ {range.label}
+
+ ))}
- {/* Instances Grid */}
-
- {filteredInstances.map((instance) => (
-
-
-
-
- {instance.name}
-
-
- {instance.customer}
-
-
-
-
-
-
-
-
-
- Status
-
- {getStatusIcon(instance.status)}
- {instance.status}
-
-
-
-
- Type
- {instance.type}
-
-
-
- Region
- {instance.region}
-
-
-
- IP Address
- {instance.ipAddress}
-
-
-
-
-
{instance.cpu}
-
CPU
+ {/* Products Display */}
+ {viewMode === 'grid' ? (
+
+ {filteredProducts.map((product) => (
+
+
+
+
+
+ {getCategoryIcon(product.category)}
+
+ {categories.find(c => c.value === product.category)?.label}
+
+
+
+
+ {product.name}
+
+
+ {product.description}
+
+
+ SKU: {product.sku}
+ Stock: {product.stockAvailable}
+
-
-
{instance.memory}
-
Memory
+
+
+
+
+
+
+
+ Vendor
+
+ {product.vendorCompany}
+
-
-
{instance.storage}
-
Storage
+
+
+ Commission Rate
+
+ {product.commissionRate}%
+
+
+
+
+ Monthly Cost
+
+ {formatCurrency(product.price)}
+
+
+
+
+ Commission Earned
+
+ {formatCurrency(product.commissionEarned || 0)}
+
-
- Monthly Cost
-
- {formatCurrency(instance.monthlyCost)}
-
-
-
-
-
Created: {formatDate(instance.createdAt)}
-
Last: {formatDate(instance.lastStarted)}
+
+ handleViewProduct(product)}
+ className="flex-1 px-3 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors duration-200"
+ >
+
+ View Details
+
+ handleAddToCart(product)}
+ className="px-3 py-2 text-sm font-medium text-secondary-700 dark:text-secondary-300 border border-secondary-300 dark:border-secondary-600 hover:bg-secondary-50 dark:hover:bg-secondary-800 rounded-lg transition-colors duration-200"
+ >
+
+
-
-
-
- Manage
-
-
-
-
-
+ ))}
+
+ ) : (
+
+
+
+
+
+
+ Product
+
+
+ Category
+
+
+ Vendor
+
+
+ Price
+
+
+ Commission
+
+
+ Stock
+
+
+ Actions
+
+
+
+
+ {filteredProducts.map((product) => (
+
+
+
+
+ {product.name}
+
+
+ {product.sku}
+
+
+
+
+
+ {getCategoryIcon(product.category)}
+
+ {categories.find(c => c.value === product.category)?.label}
+
+
+
+
+
+ {product.vendorCompany}
+
+
+ {product.vendorName}
+
+
+
+
+ {formatCurrency(product.price)}
+
+
+
+
+ {product.commissionRate}%
+
+
+ {formatCurrency(product.commissionEarned || 0)}
+
+
+
+
+ {product.stockAvailable}
+
+
+
+
+ handleViewProduct(product)}
+ className="text-primary-600 hover:text-primary-900 dark:hover:text-primary-400"
+ >
+
+
+ handleAddToCart(product)}
+ className="text-secondary-600 hover:text-secondary-900 dark:hover:text-secondary-400"
+ >
+
+
+ handleContactVendor(product)}
+ className="text-warning-600 hover:text-warning-900 dark:hover:text-warning-400"
+ >
+
+
+
+
+
+ ))}
+
+
- ))}
-
+
+ )}
{/* Empty State */}
- {filteredInstances.length === 0 && (
+ {filteredProducts.length === 0 && (
-
+
- No instances found
+ No products found
- {searchTerm || statusFilter !== 'all' || regionFilter !== 'all'
+ {searchTerm || categoryFilter !== 'all' || statusFilter !== 'all' || priceFilter !== 'all'
? 'Try adjusting your filters or search terms.'
- : 'Get started by creating your first cloud instance.'
+ : 'No products are currently available from your assigned vendor.'
}
-
+
- Create Instance
+ Request Product
)}
@@ -413,4 +677,4 @@ const Instances: React.FC = () => {
);
};
-export default Instances;
\ No newline at end of file
+export default AvailableProducts;
\ No newline at end of file
diff --git a/src/pages/reseller/Login.tsx b/src/pages/reseller/Login.tsx
index 90902ca..365a8f6 100644
--- a/src/pages/reseller/Login.tsx
+++ b/src/pages/reseller/Login.tsx
@@ -18,6 +18,7 @@ import { useAppSelector } from '../../store/hooks';
import { RootState } from '../../store';
import { toggleTheme } from '../../store/slices/themeSlice';
import { cn } from '../../utils/cn';
+import BlockedAccountModal from '../../components/BlockedAccountModal';
const ResellerLogin: React.FC = () => {
const [email, setEmail] = useState('');
@@ -26,6 +27,9 @@ const ResellerLogin: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [rememberMe, setRememberMe] = useState(false);
+ const [showInactiveModal, setShowInactiveModal] = useState(false);
+ const [inactiveUserEmail, setInactiveUserEmail] = useState('');
+ const [inactiveUserStatus, setInactiveUserStatus] = useState('');
const navigate = useNavigate();
const location = useLocation();
@@ -75,8 +79,25 @@ const ResellerLogin: React.FC = () => {
navigate(redirectPath, { replace: true });
} catch (err: any) {
const errorMessage = err.message || 'Invalid email or password. Please try again.';
- setError(errorMessage);
- toast.error(errorMessage);
+
+ // Check if the error is related to account status issues
+ if (errorMessage.includes('Account access blocked') || errorMessage.includes('Status: inactive') || errorMessage.includes('Status: suspended') || errorMessage.includes('Status: pending')) {
+ // Extract status from error message
+ let status = 'inactive';
+ if (errorMessage.includes('Status: suspended')) {
+ status = 'suspended';
+ } else if (errorMessage.includes('Status: pending')) {
+ status = 'pending';
+ }
+
+ setInactiveUserEmail(email);
+ setInactiveUserStatus(status);
+ setShowInactiveModal(true);
+ setError('');
+ } else {
+ setError(errorMessage);
+ toast.error(errorMessage);
+ }
} finally {
setIsLoading(false);
}
@@ -223,30 +244,7 @@ const ResellerLogin: React.FC = () => {
- {/* Divider */}
-
-
-
-
- Or continue with
-
-
-
- {/* Social Login Buttons */}
-
-
-
-
-
-
-
-
- Continue with Google
-
-
{/* Sign Up Link */}
@@ -282,6 +280,14 @@ const ResellerLogin: React.FC = () => {
+
+ {/* Blocked Account Modal */}
+
setShowInactiveModal(false)}
+ userEmail={inactiveUserEmail}
+ userStatus={inactiveUserStatus}
+ />
);
};
diff --git a/src/pages/reseller/Receipts.tsx b/src/pages/reseller/Receipts.tsx
new file mode 100644
index 0000000..1677c2b
--- /dev/null
+++ b/src/pages/reseller/Receipts.tsx
@@ -0,0 +1,827 @@
+import React, { useState, useEffect } from 'react';
+import { useAppDispatch, useAppSelector } from '../../store/hooks';
+import {
+ FileText,
+ Upload,
+ Plus,
+ Search,
+ Filter,
+ Eye,
+ Download,
+ Trash2,
+ Calendar,
+ DollarSign,
+ User,
+ CheckCircle,
+ XCircle,
+ Clock,
+ AlertCircle,
+ MoreVertical,
+ RefreshCw
+} from 'lucide-react';
+import { Receipt, ReceiptUploadData, ReceiptFilters } from '../../types/receipt';
+import {
+ fetchReceipts,
+ uploadReceipt,
+ deleteReceipt,
+ downloadReceipt,
+ setFilters,
+ clearFilters,
+ setSelectedReceipt,
+ clearError,
+ selectReceipts,
+ selectReceiptPagination,
+ selectReceiptFilters,
+ selectReceiptsLoading,
+ selectReceiptsUploading,
+ selectReceiptsError,
+ selectReceiptStats
+} from '../../store/slices/receiptSlice';
+import toast from 'react-hot-toast';
+import { cn } from '../../utils/cn';
+
+const Receipts: React.FC = () => {
+ const dispatch = useAppDispatch();
+ const { user } = useAppSelector((state) => state.auth);
+
+ // Redux selectors
+ const receipts = useAppSelector(selectReceipts);
+ const pagination = useAppSelector(selectReceiptPagination);
+ const filters = useAppSelector(selectReceiptFilters);
+ const isLoading = useAppSelector(selectReceiptsLoading);
+ const isUploading = useAppSelector(selectReceiptsUploading);
+ const error = useAppSelector(selectReceiptsError);
+ const stats = useAppSelector(selectReceiptStats);
+
+ const [showUploadModal, setShowUploadModal] = useState(false);
+ const [selectedReceipt, setSelectedReceipt] = useState
(null);
+ const [uploadForm, setUploadForm] = useState({
+ orderId: 0,
+ clientName: '',
+ clientEmail: '',
+ clientPhone: '',
+ clientAddress: '',
+ saleAmount: 0,
+ currency: 'USD',
+ saleDate: new Date().toISOString().split('T')[0],
+ paymentMethod: '',
+ description: '',
+ receiptFile: null
+ });
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [deletingReceiptId, setDeletingReceiptId] = useState(null);
+
+ useEffect(() => {
+ dispatch(fetchReceipts({}));
+ }, [dispatch]);
+
+ // Clear error when component unmounts or error changes
+ useEffect(() => {
+ if (error) {
+ toast.error(error);
+ dispatch(clearError());
+ }
+ }, [error, dispatch]);
+
+ const handleUpload = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!uploadForm.receiptFile) {
+ toast.error('Please select a receipt file');
+ return;
+ }
+
+ try {
+ const formData = new FormData();
+
+ // Append all form fields
+ Object.entries(uploadForm).forEach(([key, value]) => {
+ if (value !== null && value !== undefined) {
+ formData.append(key, value);
+ }
+ });
+
+ const result = await dispatch(uploadReceipt(formData)).unwrap();
+
+ if (result.success) {
+ toast.success('Receipt uploaded successfully');
+ setShowUploadModal(false);
+ resetUploadForm();
+ dispatch(fetchReceipts({}));
+ }
+ } catch (error) {
+ console.error('Error uploading receipt:', error);
+ toast.error('Failed to upload receipt');
+ }
+ };
+
+ const resetUploadForm = () => {
+ setUploadForm({
+ orderId: 0,
+ clientName: '',
+ clientEmail: '',
+ clientPhone: '',
+ clientAddress: '',
+ saleAmount: 0,
+ currency: 'USD',
+ saleDate: new Date().toISOString().split('T')[0],
+ paymentMethod: '',
+ description: '',
+ receiptFile: null
+ });
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ setUploadForm(prev => ({ ...prev, receiptFile: file }));
+ }
+ };
+
+ const handleSearch = (e: React.ChangeEvent) => {
+ const searchTerm = e.target.value;
+ dispatch(setFilters({ search: searchTerm, page: 1 }));
+ };
+
+ const handleDeleteReceipt = async (receiptId: number) => {
+ setDeletingReceiptId(receiptId);
+ setIsDeleteModalOpen(true);
+ };
+
+ const confirmDelete = async () => {
+ if (!deletingReceiptId) return;
+
+ try {
+ const result = await dispatch(deleteReceipt(deletingReceiptId)).unwrap();
+ if (result.success) {
+ toast.success('Receipt deleted successfully');
+ dispatch(fetchReceipts({}));
+ setIsDeleteModalOpen(false);
+ setDeletingReceiptId(null);
+ }
+ } catch (error) {
+ console.error('Error deleting receipt:', error);
+ toast.error('Failed to delete receipt');
+ }
+ };
+
+ const handleDownloadReceipt = async (receiptId: number) => {
+ try {
+ const result = await dispatch(downloadReceipt(receiptId)).unwrap();
+ const url = window.URL.createObjectURL(result.blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `receipt-${receiptId}.pdf`;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ } catch (error) {
+ console.error('Error downloading receipt:', error);
+ toast.error('Failed to download receipt');
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
+ case 'rejected':
+ return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
+ case 'pending':
+ return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
+ case 'disputed':
+ return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200';
+ default:
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
+ }
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return ;
+ case 'rejected':
+ return ;
+ case 'pending':
+ return ;
+ case 'disputed':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+ Receipt Management
+
+
+ Upload and manage your sales receipts for commission tracking
+
+
+
+ {/* Stats Cards */}
+
+
+
+
+
Total Receipts
+
{stats.totalReceipts}
+
+
+
+
+
+
+
+
+
+
+
Pending Approval
+
+ {stats.pendingApproval}
+
+
+
+
+
+
+
+
+
+
+
+
Approved
+
+ {stats.approved}
+
+
+
+
+
+
+
+
+
+
+
+
Total Sales
+
+ ${stats.totalSales.toLocaleString()}
+
+
+
+
+
+
+
+
+
+ {/* Actions Bar */}
+
+
+
+
+
+
+
+
+
dispatch(setFilters({ status: e.target.value, page: 1 }))}
+ className="px-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-emerald-500 focus:border-transparent"
+ >
+ All Status
+ Pending
+ Approved
+ Rejected
+ Disputed
+
+
+
+
setShowUploadModal(true)}
+ disabled={isUploading}
+ className="inline-flex items-center gap-2 px-6 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {isUploading ? (
+ <>
+
+ Uploading...
+ >
+ ) : (
+ <>
+
+ Upload Receipt
+ >
+ )}
+
+
+
+
+ {/* Receipts Table */}
+
+ {isLoading ? (
+
+
+
Loading receipts...
+
+ ) : receipts.length === 0 ? (
+
+
+
No receipts found
+
+ Get started by uploading your first receipt
+
+
setShowUploadModal(true)}
+ className="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors"
+ >
+
+ Upload Receipt
+
+
+ ) : (
+
+
+
+
+
+ Receipt
+
+
+ Client
+
+
+ Amount
+
+
+ Date
+
+
+ Status
+
+
+ Actions
+
+
+
+
+ {receipts.map((receipt) => (
+
+
+
+
+
+
+ {receipt.receiptNumber}
+
+
+ Order #{receipt.orderId}
+
+
+
+
+
+
+
+ {receipt.clientName}
+
+
+ {receipt.clientEmail}
+
+
+
+
+
+ ${receipt.saleAmount.toLocaleString()}
+
+
+ {receipt.currency}
+
+
+
+
+ {new Date(receipt.saleDate).toLocaleDateString()}
+
+
+
+
+ {getStatusIcon(receipt.status)}
+ {receipt.status.charAt(0).toUpperCase() + receipt.status.slice(1)}
+
+
+
+
+ setSelectedReceipt(receipt)}
+ className="text-emerald-600 hover:text-emerald-900 dark:text-emerald-400 dark:hover:text-emerald-300"
+ >
+
+
+ handleDownloadReceipt(receipt.id)}
+ className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
+ >
+
+
+ handleDeleteReceipt(receipt.id)}
+ className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
+ >
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+ {/* Pagination */}
+ {pagination && pagination.totalPages > 1 && (
+
+
+ Showing {((pagination.currentPage - 1) * pagination.itemsPerPage) + 1} to{' '}
+ {Math.min(pagination.currentPage * pagination.itemsPerPage, pagination.totalItems)} of{' '}
+ {pagination.totalItems} results
+
+
+ dispatch(setFilters({ page: pagination.currentPage - 1 }))}
+ disabled={pagination.currentPage === 1}
+ className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Previous
+
+ dispatch(setFilters({ page: pagination.currentPage + 1 }))}
+ disabled={pagination.currentPage === pagination.totalPages}
+ className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:cursor-not-allowed"
+ >
+ Next
+
+
+
+ )}
+
+
+ {/* Upload Modal */}
+ {showUploadModal && (
+
+
+
+
+ Upload New Receipt
+
+ setShowUploadModal(false)}
+ className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
+ >
+
+
+
+
+
+
+
+
+ Order ID
+
+ setUploadForm(prev => ({ ...prev, orderId: parseInt(e.target.value) || 0 }))}
+ className="w-full 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-emerald-500 focus:border-transparent"
+ required
+ />
+
+
+
+
+ Client Name
+
+ setUploadForm(prev => ({ ...prev, clientName: e.target.value }))}
+ className="w-full 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-emerald-500 focus:border-transparent"
+ required
+ />
+
+
+
+
+ Client Email
+
+ setUploadForm(prev => ({ ...prev, clientEmail: e.target.value }))}
+ className="w-full 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-emerald-500 focus:border-transparent"
+ />
+
+
+
+
+ Client Phone
+
+ setUploadForm(prev => ({ ...prev, clientPhone: e.target.value }))}
+ className="w-full 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-emerald-500 focus:border-transparent"
+ />
+
+
+
+
+ Sale Amount
+
+ setUploadForm(prev => ({ ...prev, saleAmount: parseFloat(e.target.value) || 0 }))}
+ className="w-full 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-emerald-500 focus:border-transparent"
+ required
+ />
+
+
+
+
+ Currency
+
+ setUploadForm(prev => ({ ...prev, currency: e.target.value }))}
+ className="w-full 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-emerald-500 focus:border-transparent"
+ >
+ USD
+ EUR
+ GBP
+ INR
+
+
+
+
+
+ Sale Date
+
+ setUploadForm(prev => ({ ...prev, saleDate: e.target.value }))}
+ className="w-full 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-emerald-500 focus:border-transparent"
+ required
+ />
+
+
+
+
+ Payment Method
+
+ setUploadForm(prev => ({ ...prev, paymentMethod: e.target.value }))}
+ className="w-full 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-emerald-500 focus:border-transparent"
+ placeholder="e.g., Credit Card, Bank Transfer"
+ />
+
+
+
+
+
+ Client Address
+
+ setUploadForm(prev => ({ ...prev, clientAddress: e.target.value }))}
+ rows={3}
+ className="w-full 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-emerald-500 focus:border-transparent"
+ />
+
+
+
+
+ Description
+
+ setUploadForm(prev => ({ ...prev, description: e.target.value }))}
+ rows={3}
+ className="w-full 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-emerald-500 focus:border-transparent"
+ placeholder="Additional details about the sale..."
+ />
+
+
+
+
+ Receipt File
+
+
+
+
+
+
+ {uploadForm.receiptFile ? uploadForm.receiptFile.name : 'Click to upload or drag and drop'}
+
+
+ PDF, JPG, PNG, DOC, DOCX (Max 10MB)
+
+
+
+
+
+
+ setShowUploadModal(false)}
+ className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
+ >
+ Cancel
+
+
+ {isUploading ? 'Uploading...' : 'Upload Receipt'}
+
+
+
+
+
+ )}
+
+ {/* Receipt Detail Modal */}
+ {selectedReceipt && (
+
+
+
+
+ Receipt Details
+
+ setSelectedReceipt(null)}
+ className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
+ >
+
+
+
+
+
+
+
+
Receipt Number
+
{selectedReceipt.receiptNumber}
+
+
+
Order ID
+
{selectedReceipt.orderId}
+
+
+
Client Name
+
{selectedReceipt.clientName}
+
+
+
Sale Amount
+
+ ${selectedReceipt.saleAmount.toLocaleString()} {selectedReceipt.currency}
+
+
+
+
Sale Date
+
+ {new Date(selectedReceipt.saleDate).toLocaleDateString()}
+
+
+
+ Status
+
+ {getStatusIcon(selectedReceipt.status)}
+ {selectedReceipt.status.charAt(0).toUpperCase() + selectedReceipt.status.slice(1)}
+
+
+
+
+ {selectedReceipt.clientEmail && (
+
+
Client Email
+
{selectedReceipt.clientEmail}
+
+ )}
+
+ {selectedReceipt.clientPhone && (
+
+
Client Phone
+
{selectedReceipt.clientPhone}
+
+ )}
+
+ {selectedReceipt.clientAddress && (
+
+
Client Address
+
{selectedReceipt.clientAddress}
+
+ )}
+
+ {selectedReceipt.description && (
+
+
Description
+
{selectedReceipt.description}
+
+ )}
+
+ {selectedReceipt.rejectionReason && (
+
+
Rejection Reason
+
{selectedReceipt.rejectionReason}
+
+ )}
+
+
+ handleDownloadReceipt(selectedReceipt.id)}
+ className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
+ >
+
+ Download
+
+ setSelectedReceipt(null)}
+ className="flex-1 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
+ >
+ Close
+
+
+
+
+
+ )}
+
+ {/* Delete Confirmation Modal */}
+ {isDeleteModalOpen && (
+
+
+
Confirm Delete
+
+ Are you sure you want to delete this receipt? This action cannot be undone.
+
+
+ {
+ setIsDeleteModalOpen(false);
+ setDeletingReceiptId(null);
+ }}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
+ >
+ Cancel
+
+
+ Delete Receipt
+
+
+
+
+ )}
+
+ );
+};
+
+export default Receipts;
\ No newline at end of file
diff --git a/src/pages/reseller/Signup.tsx b/src/pages/reseller/Signup.tsx
index 825b5a4..c608d39 100644
--- a/src/pages/reseller/Signup.tsx
+++ b/src/pages/reseller/Signup.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../../store/hooks';
import { registerUser } from '../../store/slices/authThunks';
@@ -15,18 +15,24 @@ import {
Sun,
Moon,
ArrowRight,
+ ArrowLeft,
AlertCircle,
ChevronDown,
Globe,
Users,
- Briefcase
+ Briefcase,
+ CheckCircle,
+ Shield,
+ FileText
} from 'lucide-react';
import { useAppSelector } from '../../store/hooks';
import { RootState } from '../../store';
import { toggleTheme } from '../../store/slices/themeSlice';
import { cn } from '../../utils/cn';
+import { apiService } from '../../services/api';
const ResellerSignup: React.FC = () => {
+ const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
@@ -35,9 +41,10 @@ const ResellerSignup: React.FC = () => {
confirmPassword: '',
phone: '',
company: '',
- userType: '' as 'reseller_admin' | 'sales_agent' | 'support_agent' | 'read_only' | '',
+ userType: '' as 'reseller_admin' | 'reseller_sales' | 'reseller_support' | 'read_only',
region: '',
- businessType: ''
+ businessType: '',
+ terms: false
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
@@ -46,18 +53,18 @@ const ResellerSignup: React.FC = () => {
const [showUserTypeDropdown, setShowUserTypeDropdown] = useState(false);
const [showRegionDropdown, setShowRegionDropdown] = useState(false);
const [showBusinessTypeDropdown, setShowBusinessTypeDropdown] = useState(false);
+ const [showVendorDropdown, setShowVendorDropdown] = useState(false);
const [agreedToTerms, setAgreedToTerms] = useState(false);
+ const [availableVendors, setAvailableVendors] = useState>([]);
+ const [isLoadingVendors, setIsLoadingVendors] = useState(false);
+ const [userTypes, setUserTypes] = useState>([]);
+ const [isLoadingUserTypes, setIsLoadingUserTypes] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { theme } = useAppSelector((state: RootState) => state.theme);
- const userTypes = [
- { value: 'reseller_admin', label: 'Reseller Admin', icon: Users, description: 'Sell cloud services to customers' },
- { value: 'sales_agent', label: 'Sales Agent', icon: Building, description: 'Manage sales and customer relationships' },
- { value: 'support_agent', label: 'Support Agent', icon: Briefcase, description: 'Provide customer support services' }
- ];
-
const regions = [
'North America', 'South America', 'Europe', 'Asia Pacific',
'Middle East', 'Africa', 'India', 'Australia'
@@ -70,81 +77,175 @@ const ResellerSignup: React.FC = () => {
'Manufacturing', 'Retail', 'Other'
];
+ // Load available vendors and user types on component mount
+ useEffect(() => {
+ const loadData = async () => {
+ // Load vendors
+ setIsLoadingVendors(true);
+ try {
+ const vendorsResponse = await apiService.getAvailableVendorCompanies();
+ if (vendorsResponse.success) {
+ setAvailableVendors(vendorsResponse.data);
+ }
+ } catch (error) {
+ console.error('Failed to load vendors:', error);
+ toast.error('Failed to load available vendors');
+ } finally {
+ setIsLoadingVendors(false);
+ }
+
+ // Load user types
+ setIsLoadingUserTypes(true);
+ try {
+ const userTypesResponse = await apiService.getResellerUserTypes();
+ if (userTypesResponse.success) {
+ setUserTypes(userTypesResponse.data);
+ }
+ } catch (error) {
+ console.error('Failed to load user types:', error);
+ toast.error('Failed to load user types');
+ } finally {
+ setIsLoadingUserTypes(false);
+ }
+ };
+
+ loadData();
+ }, []);
+
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
+ const validateStep1 = () => {
+ if (!formData.firstName.trim()) {
+ setError('First name is required');
+ return false;
+ }
+ if (!formData.lastName.trim()) {
+ setError('Last name is required');
+ return false;
+ }
+ if (!formData.email.trim()) {
+ setError('Email is required');
+ return false;
+ }
+ if (!validateEmail(formData.email)) {
+ setError('Please enter a valid email address');
+ return false;
+ }
+ return true;
+ };
+
+ const validateStep2 = () => {
+ if (!formData.password) {
+ setError('Password is required');
+ return false;
+ }
+ if (!validatePassword(formData.password)) {
+ setError('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
+ return false;
+ }
+ if (formData.password !== formData.confirmPassword) {
+ setError('Passwords do not match');
+ return false;
+ }
+ return true;
+ };
+
+ const validateStep3 = () => {
+ if (!formData.phone.trim()) {
+ setError('Phone number is required');
+ return false;
+ }
+ if (!validatePhoneNumber(formData.phone)) {
+ setError('Please enter a valid phone number');
+ return false;
+ }
+ if (!formData.company.trim()) {
+ setError('Company name is required');
+ return false;
+ }
+ return true;
+ };
+
+ const validateStep4 = () => {
+ if (!formData.userType) {
+ setError('Please select a user type');
+ return false;
+ }
+ if (!formData.region) {
+ setError('Please select a region');
+ return false;
+ }
+ if (!formData.businessType) {
+ setError('Please select a business type');
+ return false;
+ }
+ if (!formData.terms) {
+ setError('You must agree to the terms and conditions');
+ return false;
+ }
+ return true;
+ };
+
+ const handleNextStep = () => {
+ setError('');
+ let isValid = false;
+
+ switch (currentStep) {
+ case 1:
+ isValid = validateStep1();
+ break;
+ case 2:
+ isValid = validateStep2();
+ break;
+ case 3:
+ isValid = validateStep3();
+ break;
+ default:
+ isValid = true;
+ }
+
+ if (isValid) {
+ setCurrentStep(currentStep + 1);
+ }
+ };
+
+ const handlePrevStep = () => {
+ setCurrentStep(currentStep - 1);
+ setError('');
+ };
+
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
-
- // Validation
- if (!validateName(formData.firstName)) {
- setError('First name must be between 2 and 50 characters');
+
+ if (!validateStep4()) {
return;
}
- if (!validateName(formData.lastName)) {
- setError('Last name must be between 2 and 50 characters');
- return;
- }
-
- if (!validateEmail(formData.email)) {
- setError('Please enter a valid email address');
- return;
- }
-
- if (!validatePassword(formData.password)) {
- setError('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
- return;
- }
-
- if (formData.password !== formData.confirmPassword) {
- setError('Passwords do not match');
- return;
- }
-
- if (formData.phone && !validatePhoneNumber(formData.phone)) {
- setError('Please enter a valid phone number (7-15 digits, optional + prefix)');
- return;
- }
-
- if (!agreedToTerms) {
- setError('Please agree to the terms and conditions');
- return;
- }
-
- if (!formData.userType) {
- setError('Please select a user type');
- return;
- }
-
- setIsLoading(true);
-
+ setIsSubmitting(true);
try {
- await dispatch(registerUser({
- firstName: formData.firstName,
- lastName: formData.lastName,
+ const result = await dispatch(registerUser({
email: formData.email,
password: formData.password,
+ firstName: formData.firstName,
+ lastName: formData.lastName,
phone: formData.phone,
company: formData.company,
- role: 'reseller_admin',
- userType: 'reseller'
+ role: formData.userType,
+ userType: 'reseller'
})).unwrap();
- // Navigate to common login page with success message
- navigate('/login', {
- state: {
- message: 'Registration successful! You can now login.'
- }
- });
- } catch (err: any) {
- const errorMessage = err.message || 'An error occurred during signup. Please try again.';
- setError(errorMessage);
- toast.error(errorMessage);
+ if (result.success) {
+ toast.success('Account created successfully! Please check your email for verification.');
+ navigate('/reseller/login');
+ }
+ } catch (error: any) {
+ console.error('Registration error:', error);
+ setError(error.message || 'Registration failed. Please try again.');
} finally {
- setIsLoading(false);
+ setIsSubmitting(false);
}
};
@@ -152,158 +253,493 @@ const ResellerSignup: React.FC = () => {
dispatch(toggleTheme());
};
- return (
-
- {/* Theme Toggle */}
-
- {theme === 'dark' ? (
-
- ) : (
-
- )}
-
+ const getStepContent = () => {
+ switch (currentStep) {
+ case 1:
+ return (
+
+
+
+ Personal & Contact Information
+
+
+ Let's start with your basic details and contact information
+
+
-
- {/* Logo and Header */}
-
-
-
-
-
- Join Cloudtopiaa Connect
-
-
- Create your reseller account and start your journey with us.
-
-
-
- {/* Signup Form */}
-
-
- {/* Name Fields */}
+ {/* First Name */}
- First Name
+ First Name *
-
-
-
+
handleInputChange('firstName', e.target.value)}
- className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
- placeholder="Enter first name"
- required
+ className="w-full pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter your first name"
/>
+ {/* Last Name */}
- Last Name
+ Last Name *
-
-
-
+
handleInputChange('lastName', e.target.value)}
- className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
- placeholder="Enter last name"
- required
+ className="w-full pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter your last name"
/>
- {/* Email and Phone */}
-
-
-
- Email Address
-
-
-
-
-
-
handleInputChange('email', e.target.value)}
- className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
- placeholder="Enter email address"
- required
- />
-
+ {/* Email */}
+
+
+ Email Address *
+
+
+
+ handleInputChange('email', e.target.value)}
+ className="w-full pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter your email address"
+ />
+
+
+ );
-
-
- Phone Number
-
-
-
-
handleInputChange('phone', e.target.value)}
- className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
- placeholder="Enter phone number"
- required
- />
-
+ case 2:
+ return (
+
+
+
+ Security Setup
+
+
+ Create a strong password for your account
+
+
+
+ {/* Password */}
+
+
+ Password *
+
+
+
+ handleInputChange('password', e.target.value)}
+ className="w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ placeholder="Create a strong password"
+ />
+ setShowPassword(!showPassword)}
+ className="absolute right-3 top-1/2 transform -translate-y-1/2"
+ >
+ {showPassword ? (
+
+ ) : (
+
+ )}
+
- {/* Company and User Type */}
-
-
-
- Company Name
-
-
-
-
-
-
handleInputChange('company', e.target.value)}
- className="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
- placeholder="Enter company name"
- required
- />
-
+ {/* Confirm Password */}
+
+
+ Confirm Password *
+
+
+
+ handleInputChange('confirmPassword', e.target.value)}
+ className="w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ placeholder="Confirm your password"
+ />
+ setShowConfirmPassword(!showConfirmPassword)}
+ className="absolute right-3 top-1/2 transform -translate-y-1/2"
+ >
+ {showConfirmPassword ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
-
-
- I want to become a
-
-
-
+
+
+ Contact & Company
+
+
+ Tell us about your contact and company details
+
+
+
+ {/* Phone Number */}
+
+
+ Phone Number *
+
+
+
+
handleInputChange('phone', e.target.value)}
+ className="w-full pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter your phone number"
+ />
+
+
+
+ {/* Company Selection */}
+
+
+ Select Vendor Company *
+
+
+
+
handleInputChange('company', e.target.value)}
+ onFocus={() => setShowVendorDropdown(true)}
+ className="w-full pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ placeholder={isLoadingVendors ? "Loading vendors..." : "Select a vendor company"}
+ disabled={isLoadingVendors}
+ />
+
+
+ {showVendorDropdown && !isLoadingVendors && (
+
+ {availableVendors.length === 0 ? (
+
+ No vendors available
+
+ ) : (
+ availableVendors.map((vendor) => (
+
{
+ handleInputChange('company', vendor.company);
+ setShowVendorDropdown(false);
+ }}
+ className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+ {vendor.company}
+
+ {vendor.firstName} {vendor.lastName}
+
+
+ ))
+ )}
+
+ )}
+
+
+
+ );
+
+ case 4:
+ return (
+
+
+
+ Business Details
+
+
+ Finalize your business information
+
+
+
+ {/* Phone Number */}
+
+
+ Phone Number *
+
+
+
+
handleInputChange('phone', e.target.value)}
+ className="w-full pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ placeholder="Enter your phone number"
+ />
+
+
+
+ {/* Company Selection */}
+
+
+ Select Vendor Company *
+
+
+
+
handleInputChange('company', e.target.value)}
+ onFocus={() => setShowVendorDropdown(true)}
+ className="w-full pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ placeholder={isLoadingVendors ? "Loading vendors..." : "Select a vendor company"}
+ disabled={isLoadingVendors}
+ />
+
+
+ {showVendorDropdown && !isLoadingVendors && (
+
+ {availableVendors.length === 0 ? (
+
+ No vendors available
+
+ ) : (
+ availableVendors.map((vendor) => (
+
{
+ handleInputChange('company', vendor.company);
+ setShowVendorDropdown(false);
+ }}
+ className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+ {vendor.company}
+
+ {vendor.firstName} {vendor.lastName}
+
+
+ ))
+ )}
+
+ )}
+
+
+
+ {/* User Type */}
+
+
+ User Type *
+
+
+
setShowUserTypeDropdown(!showUserTypeDropdown)}
+ disabled={isLoadingUserTypes}
+ className="w-full flex items-center justify-between px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+
+ {isLoadingUserTypes ? 'Loading user types...' : (formData.userType ? userTypes.find(t => t.value === formData.userType)?.label : 'Select user type')}
+
+
+
+
+ {showUserTypeDropdown && (
+
+ {userTypes.map((type) => (
+
{
+ handleInputChange('userType', type.value);
+ setShowUserTypeDropdown(false);
+ }}
+ className="w-full text-left px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+
+
+
+
{type.label}
+
{type.description}
+
+
+
+ ))}
+
+ )}
+
+
+
+ {/* Region */}
+
+
+ Region *
+
+
+
+
setShowRegionDropdown(!showRegionDropdown)}
+ className="w-full flex items-center justify-between pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ >
+
+ {formData.region || 'Select region'}
+
+
+
+
+ {showRegionDropdown && (
+
+ {regions.map((region) => (
+ {
+ handleInputChange('region', region);
+ setShowRegionDropdown(false);
+ }}
+ className="w-full text-left px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+ {region}
+
+ ))}
+
+ )}
+
+
+
+ {/* Business Type */}
+
+
+ Business Type *
+
+
+
+
setShowBusinessTypeDropdown(!showBusinessTypeDropdown)}
+ className="w-full flex items-center justify-between pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ >
+
+ {formData.businessType || 'Select business type'}
+
+
+
+
+ {showBusinessTypeDropdown && (
+
+ {businessTypes.map((type) => (
+ {
+ handleInputChange('businessType', type);
+ setShowBusinessTypeDropdown(false);
+ }}
+ className="w-full text-left px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+ {type}
+
+ ))}
+
+ )}
+
+
+
+ {/* Terms and Conditions */}
+
+
handleInputChange('terms', e.target.checked.toString())}
+ className="mt-1 h-4 w-4 text-emerald-600 focus:ring-emerald-500 border-slate-300 dark:border-slate-600 rounded"
+ />
+
+ I agree to the{' '}
+
+ Terms and Conditions
+ {' '}
+ and{' '}
+
+ Privacy Policy
+
+
+
+
+ {/* Submit Button */}
+
+ {isSubmitting ? (
+
+
+ Creating Account...
+
+ ) : (
+ 'Create Account'
+ )}
+
+
+ );
+
+ case 5:
+ return (
+
+
+
+ Business Details
+
+
+ Finalize your business information
+
+
+
+ {/* User Type */}
+
+
+ User Type *
+
+
+
setShowUserTypeDropdown(!showUserTypeDropdown)}
- className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ disabled={isLoadingUserTypes}
+ className="w-full flex items-center justify-between px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
- {formData.userType ? userTypes.find(t => t.value === formData.userType)?.label : 'Select user type'}
+ {isLoadingUserTypes ? 'Loading user types...' : (formData.userType ? userTypes.find(t => t.value === formData.userType)?.label : 'Select user type')}
@@ -318,248 +754,263 @@ const ResellerSignup: React.FC = () => {
handleInputChange('userType', type.value);
setShowUserTypeDropdown(false);
}}
- className="w-full flex items-center p-3 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ className="w-full text-left px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
>
-
-
-
{type.label}
-
{type.description}
+
+
+
+
{type.label}
+
{type.description}
+
))}
)}
-
- {/* Region and Business Type */}
-
-
-
- Region
-
-
-
setShowRegionDropdown(!showRegionDropdown)}
- className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
- >
-
-
-
- {formData.region || 'Select region'}
-
-
-
-
-
- {showRegionDropdown && (
-
- {regions.map((region) => (
- {
- handleInputChange('region', region);
- setShowRegionDropdown(false);
- }}
- className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
- >
- {region}
-
- ))}
-
- )}
-
-
-
-
-
- Business Type
-
-
-
setShowBusinessTypeDropdown(!showBusinessTypeDropdown)}
- className="w-full flex items-center justify-between px-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
- >
-
-
-
- {formData.businessType || 'Select business type'}
-
-
-
-
-
- {showBusinessTypeDropdown && (
-
- {businessTypes.map((type) => (
- {
- handleInputChange('businessType', type);
- setShowBusinessTypeDropdown(false);
- }}
- className="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
- >
- {type}
-
- ))}
-
- )}
-
+ {/* Region */}
+
+
+ Region *
+
+
+
+
setShowRegionDropdown(!showRegionDropdown)}
+ className="w-full flex items-center justify-between pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ >
+
+ {formData.region || 'Select region'}
+
+
+
+
+ {showRegionDropdown && (
+
+ {regions.map((region) => (
+ {
+ handleInputChange('region', region);
+ setShowRegionDropdown(false);
+ }}
+ className="w-full text-left px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+ {region}
+
+ ))}
+
+ )}
- {/* Password Fields */}
-
-
-
- Password
-
-
-
-
+ {/* Business Type */}
+
+
+ Business Type *
+
+
+
+
setShowBusinessTypeDropdown(!showBusinessTypeDropdown)}
+ className="w-full flex items-center justify-between pl-10 pr-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
+ >
+
+ {formData.businessType || 'Select business type'}
+
+
+
+
+ {showBusinessTypeDropdown && (
+
+ {businessTypes.map((type) => (
+ {
+ handleInputChange('businessType', type);
+ setShowBusinessTypeDropdown(false);
+ }}
+ className="w-full text-left px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
+ >
+ {type}
+
+ ))}
-
handleInputChange('password', e.target.value)}
- className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
- placeholder="Create password"
- required
- />
-
setShowPassword(!showPassword)}
- className="absolute inset-y-0 right-0 pr-3 flex items-center"
- >
- {showPassword ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
- Confirm Password
-
-
-
-
-
-
handleInputChange('confirmPassword', e.target.value)}
- className="block w-full pl-10 pr-12 py-3 border border-slate-300 dark:border-slate-600 rounded-xl bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all duration-200"
- placeholder="Confirm password"
- required
- />
-
setShowConfirmPassword(!showConfirmPassword)}
- className="absolute inset-y-0 right-0 pr-3 flex items-center"
- >
- {showConfirmPassword ? (
-
- ) : (
-
- )}
-
-
+ )}
{/* Terms and Conditions */}
-
+
-
- {/* Error Message */}
- {error && (
-
- )}
-
- {/* Submit Button */}
-
- {isLoading ? (
-
-
- Creating account...
-
- ) : (
-
- )}
-
+ );
- {/* Sign In Link */}
-
-
- Already have an account?{' '}
-
- Sign in here
-
-
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ CloudTopiaa Reseller Portal
+
+
+ Join our network of successful resellers
+
+
- {/* Switch to Channel Partner */}
-
-
- Are you a Channel Partner?{' '}
-
- Sign up here
-
-
+ {/* Progress Steps */}
+
+
+ {[1, 2, 3, 4].map((step) => (
+
+ = step
+ ? "bg-emerald-600 border-emerald-600 text-white"
+ : "bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 text-slate-500"
+ )}>
+ {currentStep > step ? : step}
+
+ {step < 4 && (
+ step ? "bg-emerald-600" : "bg-slate-300 dark:bg-slate-600"
+ )}>
+ )}
+
+ ))}
+ {/* Step Labels */}
+
+ = 1 ? "text-emerald-600" : "text-slate-500")}>
+ Personal & Contact
+
+ = 2 ? "text-emerald-600" : "text-slate-500")}>
+ Security
+
+ = 3 ? "text-emerald-600" : "text-slate-500")}>
+ Company
+
+ = 4 ? "text-emerald-600" : "text-slate-500")}>
+ Business
+
+
+
+ {/* Form Container */}
+
+ {/* Theme Toggle */}
+
+
+ {theme === 'dark' ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Step Content */}
+ {getStepContent()}
+
+ {/* Error Message */}
+ {error && (
+
+ )}
+
+ {/* Navigation Buttons - Only show for steps 1-3 */}
+ {currentStep < 4 && (
+
+ {currentStep > 1 && (
+
+
+ Previous
+
+ )}
+
+
+ Next Step
+
+
+
+ )}
+
+
+ {/* Sign In Link */}
+
+
+ Already have an account?{' '}
+
+ Sign in here
+
+
+
+
+ {/* Switch to Vendor */}
+
+
+ Are you a Vendor?{' '}
+
+ Sign up here
+
+
+
+
{/* Footer */}
-
+
© 2024 CloudTopiaa Reseller Portal. All rights reserved.
diff --git a/src/services/api.ts b/src/services/api.ts
index 377c38d..8e28e12 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -13,6 +13,19 @@ export interface RegisterRequest {
lastName: string;
phone?: string;
company?: string;
+ // Vendor-specific fields
+ companyType?: 'corporation' | 'llc' | 'partnership' | 'sole_proprietorship' | 'other';
+ registrationNumber?: string;
+ gstNumber?: string;
+ panNumber?: string;
+ address?: string;
+ website?: string;
+ businessLicense?: string;
+ taxId?: string;
+ industry?: string;
+ yearsInBusiness?: string | number;
+ annualRevenue?: string | number;
+ employeeCount?: string | number;
role?: 'channel_partner_admin' | 'channel_partner_manager' | 'channel_partner_sales' | 'channel_partner_support' | 'channel_partner_finance' | 'channel_partner_analyst' | 'reseller_admin' | 'reseller_manager' | 'reseller_sales' | 'reseller_support' | 'reseller_finance' | 'reseller_analyst' | 'system_admin' | 'system_support' | 'system_analyst' | 'read_only';
userType?: 'channel_partner' | 'reseller' | 'system';
}
@@ -66,6 +79,127 @@ export interface User {
}>;
}
+export interface Product {
+ id: number;
+ name: string;
+ description?: string;
+ category: 'cloud_storage' | 'cloud_computing' | 'cybersecurity' | 'data_analytics' | 'ai_ml' | 'iot' | 'blockchain' | 'other';
+ subcategory?: string;
+ price: number;
+ currency: string;
+ commissionRate: number;
+ features?: string[];
+ specifications?: Record
;
+ images?: string[];
+ documents?: string[];
+ status: 'draft' | 'active' | 'inactive' | 'discontinued';
+ availability: 'available' | 'out_of_stock' | 'coming_soon' | 'discontinued';
+ stockQuantity: number;
+ sku: string;
+ tags?: string[];
+ metadata?: Record;
+ createdBy: number;
+ updatedBy?: number;
+ createdAt: string;
+ updatedAt: string;
+ creator?: {
+ id: number;
+ firstName: string;
+ lastName: string;
+ email: string;
+ company?: string;
+ };
+ updater?: {
+ id: number;
+ firstName: string;
+ lastName: string;
+ email: string;
+ };
+ vendor?: {
+ id: number;
+ firstName: string;
+ lastName: string;
+ email: string;
+ company?: string;
+ };
+ isAdminCreated?: boolean;
+ source?: 'admin' | 'vendor';
+ purchaseUrl?: string;
+}
+
+export interface TrainingCategory {
+ id: number;
+ name: string;
+ description?: string;
+ icon?: string;
+ color?: string;
+ sortOrder: number;
+ isActive: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface TrainingVideo {
+ id: number;
+ moduleId: number;
+ title: string;
+ description?: string;
+ youtubeUrl?: string;
+ duration?: string;
+ thumbnail?: string;
+ sortOrder: number;
+ isActive: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface TrainingMaterial {
+ id: number;
+ moduleId: number;
+ title: string;
+ description?: string;
+ type: 'PDF' | 'PPT' | 'DOC' | 'VIDEO';
+ downloadUrl?: string;
+ fileSize?: string;
+ sortOrder: number;
+ isActive: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface TrainingModule {
+ id: number;
+ title: string;
+ description?: string;
+ duration?: string;
+ level: 'Beginner' | 'Intermediate' | 'Advanced';
+ categoryId?: number;
+ thumbnailUrl?: string;
+ isActive: boolean;
+ sortOrder: number;
+ createdBy?: number;
+ createdAt: string;
+ updatedAt: string;
+ category?: TrainingCategory;
+ videos?: TrainingVideo[];
+ materials?: TrainingMaterial[];
+ userProgress?: {
+ status: 'not_started' | 'in_progress' | 'completed';
+ progressPercentage: number;
+ timeSpent: number;
+ completedAt?: string;
+ };
+}
+
+export interface TrainingProgress {
+ moduleId: number;
+ videoId?: number;
+ materialId?: number;
+ status: 'not_started' | 'in_progress' | 'completed';
+ progressPercentage: number;
+ timeSpent: number;
+}
+
class ApiService {
private baseURL: string;
@@ -101,6 +235,14 @@ class ApiService {
const data = await response.json();
if (!response.ok) {
+ // Handle account status errors
+ if (response.status === 403 && data.errorCode) {
+ const error = new Error(data.message || 'Account is not active');
+ (error as any).errorCode = data.errorCode;
+ (error as any).status = data.status;
+ throw error;
+ }
+
throw new Error(data.message || 'API request failed');
}
@@ -207,6 +349,271 @@ class ApiService {
body: JSON.stringify({ currentPassword, newPassword }),
});
}
+
+ // Vendor operations
+ async getAvailableVendorCompanies(): Promise<{ success: boolean; data: Array<{ id: number; company: string; firstName: string; lastName: string; email: string }> }> {
+ return this.request<{ success: boolean; data: Array<{ id: number; company: string; firstName: string; lastName: string; email: string }> }>('/public/vendors/available-companies');
+ }
+
+ // Reseller operations
+ async getResellerUserTypes(): Promise<{ success: boolean; data: Array<{ value: string; label: string; description: string; permissions: string[] }> }> {
+ return this.request<{ success: boolean; data: Array<{ value: string; label: string; description: string; permissions: string[] }> }>('/public/reseller/user-types');
+ }
+
+ async getPendingResellerRequests(): Promise<{ success: boolean; data: User[] }> {
+ return this.request<{ success: boolean; data: User[] }>('/vendors/pending-resellers');
+ }
+
+ async getVendorResellers(): Promise<{ success: boolean; data: User[] }> {
+ return this.request<{ success: boolean; data: User[] }>('/vendors/resellers');
+ }
+
+ async createReseller(resellerData: {
+ firstName: string;
+ lastName: string;
+ email: string;
+ phone: string;
+ company: string;
+ userType: 'reseller_admin' | 'reseller_sales' | 'reseller_support' | 'read_only';
+ region: string;
+ businessType: string;
+ address?: string;
+ }): Promise<{ success: boolean; message: string; data: any }> {
+ return this.request<{ success: boolean; message: string; data: any }>('/vendors/resellers', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(resellerData),
+ });
+ }
+
+ async getVendorDashboardStats(): Promise<{ success: boolean; data: any }> {
+ return this.request<{ success: boolean; data: any }>('/vendors/dashboard/stats');
+ }
+
+ async getVendorProducts(params?: {
+ page?: number;
+ limit?: number;
+ category?: string;
+ status?: string;
+ search?: string;
+ }): Promise<{ success: boolean; data: { products: Product[]; pagination: any } }> {
+ const queryParams = new URLSearchParams();
+ if (params) {
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined) queryParams.append(key, value.toString());
+ });
+ }
+ return this.request<{ success: boolean; data: { products: Product[]; pagination: any } }>(`/vendors/products?${queryParams}`);
+ }
+
+ async getVendorCommissions(params?: {
+ page?: number;
+ limit?: number;
+ status?: string;
+ dateRange?: string;
+ }): Promise<{ success: boolean; data: { commissions: any[]; pagination: any } }> {
+ const queryParams = new URLSearchParams();
+ if (params) {
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined) queryParams.append(key, value.toString());
+ });
+ }
+ return this.request<{ success: boolean; data: { commissions: any[]; pagination: any } }>(`/vendors/commissions?${queryParams}`);
+ }
+
+ async approveResellerRequest(userId: number): Promise<{ success: boolean; message: string }> {
+ return this.request<{ success: boolean; message: string }>(`/vendors/resellers/${userId}/approve`, {
+ method: 'POST'
+ });
+ }
+
+ async rejectResellerRequest(userId: number, reason: string): Promise<{ success: boolean; message: string }> {
+ return this.request<{ success: boolean; message: string }>(`/vendors/resellers/${userId}/reject`, {
+ method: 'POST',
+ body: JSON.stringify({ reason })
+ });
+ }
+
+ // Product management
+ async getAllProducts(params?: {
+ page?: number;
+ limit?: number;
+ category?: string;
+ status?: string;
+ search?: string;
+ vendor?: string;
+ sortBy?: string;
+ sortOrder?: string;
+ }): Promise<{ success: boolean; data: { products: Product[]; pagination: any } }> {
+ const queryParams = new URLSearchParams();
+ if (params) {
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined) queryParams.append(key, value.toString());
+ });
+ }
+ return this.request<{ success: boolean; data: { products: Product[]; pagination: any } }>(`/products?${queryParams}`);
+ }
+
+ async getProductById(id: number): Promise<{ success: boolean; data: Product }> {
+ return this.request<{ success: boolean; data: Product }>(`/products/${id}`);
+ }
+
+ async createProduct(productData: Partial): Promise<{ success: boolean; data: Product }> {
+ return this.request<{ success: boolean; data: Product }>('/products', {
+ method: 'POST',
+ body: JSON.stringify(productData),
+ });
+ }
+
+ async updateProduct(id: number, productData: Partial): Promise<{ success: boolean; data: Product }> {
+ return this.request<{ success: boolean; data: Product }>(`/products/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(productData),
+ });
+ }
+
+ async deleteProduct(id: number): Promise<{ success: boolean; message: string }> {
+ return this.request<{ success: boolean; message: string }>(`/products/${id}`, {
+ method: 'DELETE',
+ });
+ }
+
+ async getProductCategories(): Promise<{ success: boolean; data: string[] }> {
+ return this.request<{ success: boolean; data: string[] }>('/products/categories');
+ }
+
+ async getProductStats(): Promise<{ success: boolean; data: any }> {
+ return this.request<{ success: boolean; data: any }>('/products/stats');
+ }
+
+ async getActiveVendors(): Promise<{ success: boolean; data: Array<{ id: number; firstName: string; lastName: string; company?: string }> }> {
+ return this.request<{ success: boolean; data: Array<{ id: number; firstName: string; lastName: string; company?: string }> }>('/products/vendors');
+ }
+
+ async getVendorById(id: number): Promise<{ success: boolean; data: { id: number; firstName: string; lastName: string; email: string; company?: string } }> {
+ return this.request<{ success: boolean; data: { id: number; firstName: string; lastName: string; email: string; company?: string } }>(`/vendors/${id}`);
+ }
+
+ // Receipt management
+ async uploadReceipt(data: FormData): Promise<{ success: boolean; data: any; message: string }> {
+ return this.request<{ success: boolean; data: any; message: string }>('/receipts/upload', {
+ method: 'POST',
+ body: data,
+ });
+ }
+
+ async getResellerReceipts(params?: {
+ page?: number;
+ limit?: number;
+ status?: string;
+ startDate?: string;
+ endDate?: string;
+ }): Promise<{ success: boolean; data: any[]; pagination?: any }> {
+ const queryParams = new URLSearchParams();
+ if (params) {
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined) queryParams.append(key, value.toString());
+ });
+ }
+ return this.request<{ success: boolean; data: any[]; pagination?: any }>(`/receipts/reseller?${queryParams}`);
+ }
+
+ async getVendorReceipts(params?: {
+ page?: number;
+ limit?: number;
+ status?: string;
+ resellerId?: number;
+ startDate?: string;
+ endDate?: string;
+ }): Promise<{ success: boolean; data: any[]; pagination?: any }> {
+ const queryParams = new URLSearchParams();
+ if (params) {
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined) queryParams.append(key, value.toString());
+ });
+ }
+ return this.request<{ success: boolean; data: any[]; pagination?: any }>(`/receipts/vendor?${queryParams}`);
+ }
+
+ async getReceiptById(id: number): Promise<{ success: boolean; data: any }> {
+ return this.request<{ success: boolean; data: any }>(`/receipts/reseller/${id}`);
+ }
+
+ async getVendorReceiptById(id: number): Promise<{ success: boolean; data: any }> {
+ return this.request<{ success: boolean; data: any }>(`/receipts/vendor/${id}`);
+ }
+
+ async updateReceiptStatus(id: number, statusUpdate: { status: string; rejectionReason?: string }): Promise<{ success: boolean; data: any; message: string }> {
+ return this.request<{ success: boolean; data: any; message: string }>(`/receipts/vendor/${id}/status`, {
+ method: 'PUT',
+ body: JSON.stringify(statusUpdate),
+ });
+ }
+
+ async downloadReceipt(id: number): Promise {
+ const response = await fetch(`${this.baseURL}/receipts/${id}/download`, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return response.blob();
+ }
+
+ async deleteReceipt(id: number): Promise<{ success: boolean; message: string }> {
+ return this.request<{ success: boolean; message: string }>(`/receipts/reseller/${id}`, {
+ method: 'DELETE',
+ });
+ }
+
+ // Get vendor products with current stock quantities
+ async getVendorProductsWithStock(params?: {
+ page?: number;
+ limit?: number;
+ category?: string;
+ status?: string;
+ search?: string;
+ }): Promise<{ success: boolean; data: { products: any[]; pagination: any } }> {
+ const queryParams = new URLSearchParams();
+ if (params) {
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined) queryParams.append(key, value.toString());
+ });
+ }
+ return this.request<{ success: boolean; data: { products: any[]; pagination: any } }>(`/receipts/vendor/products/stock?${queryParams}`);
+ }
+
+ // Get products shared by vendor for reseller with current stock
+ async getResellerVendorProducts(params: {
+ vendorId: number;
+ page?: number;
+ limit?: number;
+ category?: string;
+ status?: string;
+ search?: string;
+ }): Promise<{ success: boolean; data: { products: any[]; pagination: any } }> {
+ const queryParams = new URLSearchParams();
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined) queryParams.append(key, value.toString());
+ });
+ return this.request<{ success: boolean; data: { products: any[]; pagination: any } }>(`/receipts/reseller/vendor/products?${queryParams}`);
+ }
+
+ // Manually update product stock quantity
+ async updateProductStock(productId: number, stockQuantity: number): Promise<{ success: boolean; data: any; message: string }> {
+ return this.request<{ success: boolean; data: any; message: string }>(`/receipts/vendor/products/${productId}/stock`, {
+ method: 'PUT',
+ body: JSON.stringify({ stockQuantity }),
+ });
+ }
}
export const apiService = new ApiService();
diff --git a/src/services/socketService.ts b/src/services/socketService.ts
new file mode 100644
index 0000000..ccf00c6
--- /dev/null
+++ b/src/services/socketService.ts
@@ -0,0 +1,187 @@
+import io from 'socket.io-client';
+
+class SocketService {
+ private socket: any = null;
+ private isConnected = false;
+ private reconnectAttempts = 0;
+ private maxReconnectAttempts = 5;
+ private reconnectDelay = 1000;
+
+ // Event listeners
+ private listeners: Map = new Map();
+
+ connect(token: string) {
+ if (this.socket && this.isConnected) {
+ return;
+ }
+
+ try {
+ this.socket = io(process.env.REACT_APP_SOCKET_URL || 'http://localhost:5000', {
+ auth: {
+ token
+ },
+ transports: ['websocket', 'polling'],
+ timeout: 20000,
+ reconnection: true,
+ reconnectionAttempts: this.maxReconnectAttempts,
+ reconnectionDelay: this.reconnectDelay
+ });
+
+ this.setupEventListeners();
+ } catch (error) {
+ console.error('Socket connection error:', error);
+ }
+ }
+
+ private setupEventListeners() {
+ if (!this.socket) return;
+
+ this.socket.on('connect', () => {
+ console.log('Socket connected');
+ this.isConnected = true;
+ this.reconnectAttempts = 0;
+ this.emit('socket_connected', { timestamp: new Date().toISOString() });
+ });
+
+ this.socket.on('disconnect', (reason: string) => {
+ console.log('Socket disconnected:', reason);
+ this.isConnected = false;
+
+ if (reason === 'io server disconnect') {
+ // Server disconnected us, try to reconnect
+ this.socket?.connect();
+ }
+ });
+
+ this.socket.on('connect_error', (error: Error) => {
+ console.error('Socket connection error:', error);
+ this.reconnectAttempts++;
+
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ console.error('Max reconnection attempts reached');
+ }
+ });
+
+ this.socket.on('reconnect', (attemptNumber: number) => {
+ console.log('Socket reconnected after', attemptNumber, 'attempts');
+ this.isConnected = true;
+ this.reconnectAttempts = 0;
+ });
+
+ this.socket.on('reconnect_error', (error: Error) => {
+ console.error('Socket reconnection error:', error);
+ });
+
+ // Handle notification events
+ this.socket.on('NEW_VENDOR_REQUEST', (data: any) => {
+ this.triggerListeners('NEW_VENDOR_REQUEST', data);
+ });
+
+ this.socket.on('NEW_RESELLER_REQUEST', (data: any) => {
+ this.triggerListeners('NEW_RESELLER_REQUEST', data);
+ });
+
+ this.socket.on('VENDOR_APPROVED', (data: any) => {
+ this.triggerListeners('VENDOR_APPROVED', data);
+ });
+
+ this.socket.on('VENDOR_REJECTED', (data: any) => {
+ this.triggerListeners('VENDOR_REJECTED', data);
+ });
+
+ this.socket.on('RESELLER_CREATED', (data: any) => {
+ this.triggerListeners('RESELLER_CREATED', data);
+ });
+
+ this.socket.on('RESELLER_ACCOUNT_CREATED', (data: any) => {
+ this.triggerListeners('RESELLER_ACCOUNT_CREATED', data);
+ });
+
+ this.socket.on('SYSTEM_ALERT', (data: any) => {
+ this.triggerListeners('SYSTEM_ALERT', data);
+ });
+
+ // Handle general notifications
+ this.socket.on('notification', (data: any) => {
+ this.triggerListeners('notification', data);
+ });
+ }
+
+ disconnect() {
+ if (this.socket) {
+ this.socket.disconnect();
+ this.socket = null;
+ this.isConnected = false;
+ this.listeners.clear();
+ }
+ }
+
+ emit(event: string, data: any) {
+ if (this.socket && this.isConnected) {
+ this.socket.emit(event, data);
+ } else {
+ console.warn('Socket not connected, cannot emit event:', event);
+ }
+ }
+
+ on(event: string, callback: Function) {
+ if (!this.listeners.has(event)) {
+ this.listeners.set(event, []);
+ }
+ this.listeners.get(event)?.push(callback);
+ }
+
+ off(event: string, callback?: Function) {
+ if (!callback) {
+ this.listeners.delete(event);
+ } else {
+ const callbacks = this.listeners.get(event);
+ if (callbacks) {
+ const index = callbacks.indexOf(callback);
+ if (index > -1) {
+ callbacks.splice(index, 1);
+ }
+ }
+ }
+ }
+
+ private triggerListeners(event: string, data: any) {
+ const callbacks = this.listeners.get(event);
+ if (callbacks) {
+ callbacks.forEach(callback => {
+ try {
+ callback(data);
+ } catch (error) {
+ console.error('Error in socket event listener:', error);
+ }
+ });
+ }
+ }
+
+ isSocketConnected(): boolean {
+ return this.isConnected;
+ }
+
+ // Join specific rooms
+ joinRoom(room: string) {
+ this.emit('join-room', room);
+ }
+
+ leaveRoom(room: string) {
+ this.emit('leave-room', room);
+ }
+
+ // Get connection status
+ getConnectionStatus() {
+ return {
+ isConnected: this.isConnected,
+ reconnectAttempts: this.reconnectAttempts,
+ maxReconnectAttempts: this.maxReconnectAttempts
+ };
+ }
+}
+
+// Create singleton instance
+const socketService = new SocketService();
+
+export default socketService;
\ No newline at end of file
diff --git a/src/store/index.ts b/src/store/index.ts
index 32c6f50..859b1a9 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -3,6 +3,8 @@ import themeReducer from './slices/themeSlice';
import authReducer from './slices/authSlice';
import dashboardReducer from './slices/dashboardSlice';
import resellerDashboardReducer from './reseller/dashboardSlice';
+import productReducer from './slices/productSlice';
+import receiptReducer from './slices/receiptSlice';
export const store = configureStore({
reducer: {
@@ -10,6 +12,8 @@ export const store = configureStore({
auth: authReducer,
dashboard: dashboardReducer,
resellerDashboard: resellerDashboardReducer,
+ product: productReducer,
+ receipts: receiptReducer,
},
});
diff --git a/src/store/slices/authSlice.ts b/src/store/slices/authSlice.ts
index 95e1200..059521e 100644
--- a/src/store/slices/authSlice.ts
+++ b/src/store/slices/authSlice.ts
@@ -13,12 +13,12 @@ interface AuthState {
const initialState: AuthState = {
user: null,
- isAuthenticated: false,
+ isAuthenticated: !!localStorage.getItem('accessToken'),
isLoading: false,
error: null,
- token: null,
- refreshToken: null,
- sessionId: null,
+ token: localStorage.getItem('accessToken'),
+ refreshToken: localStorage.getItem('refreshToken'),
+ sessionId: localStorage.getItem('sessionId'),
};
const authSlice = createSlice({
@@ -38,6 +38,11 @@ const authSlice = createSlice({
state.sessionId = action.payload.sessionId;
state.isAuthenticated = true;
state.error = null;
+ state.isLoading = false;
+ // Store tokens in localStorage
+ localStorage.setItem('accessToken', action.payload.token);
+ localStorage.setItem('refreshToken', action.payload.refreshToken);
+ localStorage.setItem('sessionId', action.payload.sessionId);
},
logout: (state) => {
state.user = null;
@@ -46,6 +51,11 @@ const authSlice = createSlice({
state.sessionId = null;
state.isAuthenticated = false;
state.error = null;
+ state.isLoading = false;
+ // Clear localStorage
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ localStorage.removeItem('sessionId');
},
updateUser: (state, action: PayloadAction>) => {
if (state.user) {
@@ -56,10 +66,21 @@ const authSlice = createSlice({
state.token = action.payload.token;
state.refreshToken = action.payload.refreshToken;
state.sessionId = action.payload.sessionId;
+ // Set authenticated to true when tokens are set
+ state.isAuthenticated = true;
+ // Store tokens in localStorage
+ localStorage.setItem('accessToken', action.payload.token);
+ localStorage.setItem('refreshToken', action.payload.refreshToken);
+ localStorage.setItem('sessionId', action.payload.sessionId);
+ },
+ setUser: (state, action: PayloadAction) => {
+ state.user = action.payload;
+ state.isAuthenticated = true;
+ state.error = null;
},
},
});
-export const { setLoading, setError, loginSuccess, logout, updateUser, setTokens } = authSlice.actions;
+export const { setLoading, setError, loginSuccess, logout, updateUser, setTokens, setUser } = authSlice.actions;
export type { User };
export default authSlice.reducer;
\ No newline at end of file
diff --git a/src/store/slices/authThunks.ts b/src/store/slices/authThunks.ts
index bf18498..3636ff8 100644
--- a/src/store/slices/authThunks.ts
+++ b/src/store/slices/authThunks.ts
@@ -1,6 +1,6 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { apiService, LoginRequest, RegisterRequest } from '../../services/api';
-import { setLoading, setError, loginSuccess, logout, setTokens } from './authSlice';
+import { setLoading, setError, loginSuccess, logout, setTokens, setUser } from './authSlice';
export const loginUser = createAsyncThunk(
'auth/login',
@@ -12,12 +12,13 @@ export const loginUser = createAsyncThunk(
const response = await apiService.login(credentials);
if (response.success && response.data) {
- // Store tokens in localStorage
- localStorage.setItem('accessToken', response.data.accessToken);
- localStorage.setItem('refreshToken', response.data.refreshToken);
- localStorage.setItem('sessionId', response.data.sessionId);
+ // Check if user account is blocked
+ const userStatus = response.data.user.status;
+ if (['inactive', 'pending', 'suspended'].includes(userStatus)) {
+ throw new Error(`Account access blocked. Status: ${userStatus}. Please contact your vendor administrator.`);
+ }
- // Dispatch login success
+ // Dispatch login success (tokens will be stored in the slice)
dispatch(loginSuccess({
user: response.data.user,
token: response.data.accessToken,
@@ -122,19 +123,11 @@ export const logoutUser = createAsyncThunk(
await apiService.logout(sessionId);
}
- // Clear localStorage
- localStorage.removeItem('accessToken');
- localStorage.removeItem('refreshToken');
- localStorage.removeItem('sessionId');
-
- // Dispatch logout
+ // Dispatch logout (localStorage will be cleared in the slice)
dispatch(logout());
} catch (error) {
console.error('Logout error:', error);
// Still logout locally even if API call fails
- localStorage.removeItem('accessToken');
- localStorage.removeItem('refreshToken');
- localStorage.removeItem('sessionId');
dispatch(logout());
}
}
@@ -150,6 +143,15 @@ export const getCurrentUser = createAsyncThunk(
const response = await apiService.getCurrentUser();
if (response.success) {
+ // Check if user account is blocked
+ const userStatus = response.data.status;
+ if (['inactive', 'pending', 'suspended'].includes(userStatus)) {
+ // Force logout if account is blocked
+ dispatch(logout());
+ throw new Error(`Account access blocked. Status: ${userStatus}. Please contact your vendor administrator.`);
+ }
+
+ dispatch(setUser(response.data));
return response.data;
} else {
throw new Error('Failed to get current user');
@@ -178,12 +180,17 @@ export const refreshUserToken = createAsyncThunk(
const response = await apiService.refreshToken(refreshToken);
if (response.success && response.data) {
- // Update tokens in localStorage
- localStorage.setItem('accessToken', response.data.accessToken);
- localStorage.setItem('refreshToken', response.data.refreshToken);
- localStorage.setItem('sessionId', response.data.sessionId);
+ // Check if user account is blocked (if user data is included in refresh response)
+ if (response.data.user) {
+ const userStatus = response.data.user.status;
+ if (['inactive', 'pending', 'suspended'].includes(userStatus)) {
+ // Force logout if account is blocked
+ dispatch(logout());
+ throw new Error(`Account access blocked. Status: ${userStatus}. Please contact your vendor administrator.`);
+ }
+ }
- // Update tokens in store
+ // Update tokens in store (localStorage will be updated in the slice)
dispatch(setTokens({
token: response.data.accessToken,
refreshToken: response.data.refreshToken,
diff --git a/src/store/slices/productSlice.ts b/src/store/slices/productSlice.ts
new file mode 100644
index 0000000..d2dcf3f
--- /dev/null
+++ b/src/store/slices/productSlice.ts
@@ -0,0 +1,117 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { Product } from '../../services/api';
+
+interface ProductState {
+ products: Product[];
+ currentProduct: Product | null;
+ categories: string[];
+ stats: any;
+ pagination: {
+ currentPage: number;
+ totalPages: number;
+ totalItems: number;
+ itemsPerPage: number;
+ };
+ filters: {
+ category: string;
+ status: string;
+ search: string;
+ sortBy: string;
+ sortOrder: string;
+ };
+ isLoading: boolean;
+ error: string | null;
+}
+
+const initialState: ProductState = {
+ products: [],
+ currentProduct: null,
+ categories: [],
+ stats: null,
+ pagination: {
+ currentPage: 1,
+ totalPages: 1,
+ totalItems: 0,
+ itemsPerPage: 10,
+ },
+ filters: {
+ category: '',
+ status: '',
+ search: '',
+ sortBy: 'createdAt',
+ sortOrder: 'DESC',
+ },
+ isLoading: false,
+ error: null,
+};
+
+const productSlice = createSlice({
+ name: 'product',
+ initialState,
+ reducers: {
+ setLoading: (state, action: PayloadAction) => {
+ state.isLoading = action.payload;
+ },
+ setError: (state, action: PayloadAction) => {
+ state.error = action.payload;
+ },
+ setProducts: (state, action: PayloadAction) => {
+ state.products = action.payload;
+ },
+ setCurrentProduct: (state, action: PayloadAction) => {
+ state.currentProduct = action.payload;
+ },
+ setCategories: (state, action: PayloadAction) => {
+ state.categories = action.payload;
+ },
+ setStats: (state, action: PayloadAction) => {
+ state.stats = action.payload;
+ },
+ setPagination: (state, action: PayloadAction) => {
+ state.pagination = action.payload;
+ },
+ setFilters: (state, action: PayloadAction>) => {
+ state.filters = { ...state.filters, ...action.payload };
+ },
+ addProduct: (state, action: PayloadAction) => {
+ state.products.unshift(action.payload);
+ },
+ updateProduct: (state, action: PayloadAction) => {
+ const index = state.products.findIndex(p => p.id === action.payload.id);
+ if (index !== -1) {
+ state.products[index] = action.payload;
+ }
+ if (state.currentProduct?.id === action.payload.id) {
+ state.currentProduct = action.payload;
+ }
+ },
+ removeProduct: (state, action: PayloadAction) => {
+ state.products = state.products.filter(p => p.id !== action.payload);
+ if (state.currentProduct?.id === action.payload) {
+ state.currentProduct = null;
+ }
+ },
+ clearProducts: (state) => {
+ state.products = [];
+ state.currentProduct = null;
+ state.pagination = initialState.pagination;
+ },
+ },
+});
+
+export const {
+ setLoading,
+ setError,
+ setProducts,
+ setCurrentProduct,
+ setCategories,
+ setStats,
+ setPagination,
+ setFilters,
+ addProduct,
+ updateProduct,
+ removeProduct,
+ clearProducts,
+} = productSlice.actions;
+
+export default productSlice.reducer;
\ No newline at end of file
diff --git a/src/store/slices/productThunks.ts b/src/store/slices/productThunks.ts
new file mode 100644
index 0000000..9afeac0
--- /dev/null
+++ b/src/store/slices/productThunks.ts
@@ -0,0 +1,192 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { apiService, Product } from '../../services/api';
+import {
+ setLoading,
+ setError,
+ setProducts,
+ setCurrentProduct,
+ setCategories,
+ setStats,
+ setPagination,
+ addProduct,
+ updateProduct,
+ removeProduct,
+} from './productSlice';
+
+export const fetchProducts = createAsyncThunk(
+ 'product/fetchProducts',
+ async (params: {
+ page?: number;
+ limit?: number;
+ category?: string;
+ status?: string;
+ search?: string;
+ sortBy?: string;
+ sortOrder?: string;
+ } = {}, { dispatch }) => {
+ try {
+ dispatch(setLoading(true));
+ dispatch(setError(null));
+
+ const response = await apiService.getAllProducts(params);
+
+ if (response.success) {
+ dispatch(setProducts(response.data.products));
+ dispatch(setPagination(response.data.pagination));
+ return response.data;
+ } else {
+ throw new Error('Failed to fetch products');
+ }
+ } catch (error: any) {
+ const errorMessage = error.message || 'Failed to fetch products';
+ dispatch(setError(errorMessage));
+ throw error;
+ } finally {
+ dispatch(setLoading(false));
+ }
+ }
+);
+
+export const fetchProductById = createAsyncThunk(
+ 'product/fetchProductById',
+ async (id: number, { dispatch }) => {
+ try {
+ dispatch(setLoading(true));
+ dispatch(setError(null));
+
+ const response = await apiService.getProductById(id);
+
+ if (response.success) {
+ dispatch(setCurrentProduct(response.data));
+ return response.data;
+ } else {
+ throw new Error('Failed to fetch product');
+ }
+ } catch (error: any) {
+ const errorMessage = error.message || 'Failed to fetch product';
+ dispatch(setError(errorMessage));
+ throw error;
+ } finally {
+ dispatch(setLoading(false));
+ }
+ }
+);
+
+export const createProduct = createAsyncThunk(
+ 'product/createProduct',
+ async (productData: Partial, { dispatch }) => {
+ try {
+ dispatch(setLoading(true));
+ dispatch(setError(null));
+
+ const response = await apiService.createProduct(productData);
+
+ if (response.success) {
+ dispatch(addProduct(response.data));
+ return response.data;
+ } else {
+ throw new Error('Failed to create product');
+ }
+ } catch (error: any) {
+ const errorMessage = error.message || 'Failed to create product';
+ dispatch(setError(errorMessage));
+ throw error;
+ } finally {
+ dispatch(setLoading(false));
+ }
+ }
+);
+
+export const updateProductById = createAsyncThunk(
+ 'product/updateProductById',
+ async ({ id, productData }: { id: number; productData: Partial }, { dispatch }) => {
+ try {
+ dispatch(setLoading(true));
+ dispatch(setError(null));
+
+ const response = await apiService.updateProduct(id, productData);
+
+ if (response.success) {
+ dispatch(updateProduct(response.data));
+ return response.data;
+ } else {
+ throw new Error('Failed to update product');
+ }
+ } catch (error: any) {
+ const errorMessage = error.message || 'Failed to update product';
+ dispatch(setError(errorMessage));
+ throw error;
+ } finally {
+ dispatch(setLoading(false));
+ }
+ }
+);
+
+export const deleteProductById = createAsyncThunk(
+ 'product/deleteProductById',
+ async (id: number, { dispatch }) => {
+ try {
+ dispatch(setLoading(true));
+ dispatch(setError(null));
+
+ const response = await apiService.deleteProduct(id);
+
+ if (response.success) {
+ dispatch(removeProduct(id));
+ return response;
+ } else {
+ throw new Error('Failed to delete product');
+ }
+ } catch (error: any) {
+ const errorMessage = error.message || 'Failed to delete product';
+ dispatch(setError(errorMessage));
+ throw error;
+ } finally {
+ dispatch(setLoading(false));
+ }
+ }
+);
+
+export const fetchProductCategories = createAsyncThunk(
+ 'product/fetchProductCategories',
+ async (_, { dispatch }) => {
+ try {
+ dispatch(setError(null));
+
+ const response = await apiService.getProductCategories();
+
+ if (response.success) {
+ dispatch(setCategories(response.data));
+ return response.data;
+ } else {
+ throw new Error('Failed to fetch product categories');
+ }
+ } catch (error: any) {
+ const errorMessage = error.message || 'Failed to fetch product categories';
+ dispatch(setError(errorMessage));
+ throw error;
+ }
+ }
+);
+
+export const fetchProductStats = createAsyncThunk(
+ 'product/fetchProductStats',
+ async (_, { dispatch }) => {
+ try {
+ dispatch(setError(null));
+
+ const response = await apiService.getProductStats();
+
+ if (response.success) {
+ dispatch(setStats(response.data));
+ return response.data;
+ } else {
+ throw new Error('Failed to fetch product stats');
+ }
+ } catch (error: any) {
+ const errorMessage = error.message || 'Failed to fetch product stats';
+ dispatch(setError(errorMessage));
+ throw error;
+ }
+ }
+);
\ No newline at end of file
diff --git a/src/store/slices/receiptSlice.ts b/src/store/slices/receiptSlice.ts
new file mode 100644
index 0000000..421c853
--- /dev/null
+++ b/src/store/slices/receiptSlice.ts
@@ -0,0 +1,232 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import { Receipt, ReceiptUploadData, ReceiptFilters, ReceiptResponse } from '../../types/receipt';
+import apiService from '../../services/api';
+
+// Async thunks
+export const fetchReceipts = createAsyncThunk(
+ 'receipts/fetchReceipts',
+ async (filters: ReceiptFilters = {}) => {
+ const response = await apiService.getResellerReceipts(filters);
+ if (!response.success) {
+ throw new Error('Failed to fetch receipts');
+ }
+ return response;
+ }
+);
+
+export const uploadReceipt = createAsyncThunk(
+ 'receipts/uploadReceipt',
+ async (data: FormData) => {
+ const response = await apiService.uploadReceipt(data);
+ if (!response.success) {
+ throw new Error(response.message || 'Failed to upload receipt');
+ }
+ return response;
+ }
+);
+
+export const deleteReceipt = createAsyncThunk(
+ 'receipts/deleteReceipt',
+ async (receiptId: number) => {
+ const response = await apiService.deleteReceipt(receiptId);
+ if (!response.success) {
+ throw new Error(response.message || 'Failed to delete receipt');
+ }
+ return { receiptId, ...response };
+ }
+);
+
+export const downloadReceipt = createAsyncThunk(
+ 'receipts/downloadReceipt',
+ async (receiptId: number) => {
+ const blob = await apiService.downloadReceipt(receiptId);
+ return { receiptId, blob };
+ }
+);
+
+// State interface
+interface ReceiptState {
+ receipts: Receipt[];
+ pagination: {
+ currentPage: number;
+ totalPages: number;
+ totalItems: number;
+ itemsPerPage: number;
+ } | null;
+ filters: ReceiptFilters;
+ isLoading: boolean;
+ isUploading: boolean;
+ error: string | null;
+ selectedReceipt: Receipt | null;
+ stats: {
+ totalReceipts: number;
+ pendingApproval: number;
+ approved: number;
+ totalSales: number;
+ };
+}
+
+// Initial state
+const initialState: ReceiptState = {
+ receipts: [],
+ pagination: null,
+ filters: {
+ status: '',
+ startDate: '',
+ endDate: '',
+ page: 1,
+ limit: 10
+ },
+ isLoading: false,
+ isUploading: false,
+ error: null,
+ selectedReceipt: null,
+ stats: {
+ totalReceipts: 0,
+ pendingApproval: 0,
+ approved: 0,
+ totalSales: 0
+ }
+};
+
+// Slice
+const receiptSlice = createSlice({
+ name: 'receipts',
+ initialState,
+ reducers: {
+ setFilters: (state, action: PayloadAction>) => {
+ state.filters = { ...state.filters, ...action.payload };
+ // Reset to first page when filters change
+ if (action.payload.page === undefined) {
+ state.filters.page = 1;
+ }
+ },
+ clearFilters: (state) => {
+ state.filters = {
+ status: '',
+ startDate: '',
+ endDate: '',
+ page: 1,
+ limit: 10
+ };
+ },
+ setSelectedReceipt: (state, action: PayloadAction) => {
+ state.selectedReceipt = action.payload;
+ },
+ clearError: (state) => {
+ state.error = null;
+ },
+ updateReceiptStatus: (state, action: PayloadAction<{ receiptId: number; status: string; rejectionReason?: string }>) => {
+ const receipt = state.receipts.find(r => r.id === action.payload.receiptId);
+ if (receipt) {
+ receipt.status = action.payload.status as any;
+ if (action.payload.rejectionReason) {
+ receipt.rejectionReason = action.payload.rejectionReason;
+ }
+ }
+ },
+ calculateStats: (state) => {
+ const receipts = state.receipts;
+ state.stats = {
+ totalReceipts: receipts.length,
+ pendingApproval: receipts.filter(r => r.status === 'pending').length,
+ approved: receipts.filter(r => r.status === 'approved').length,
+ totalSales: receipts.reduce((sum, r) => sum + r.saleAmount, 0)
+ };
+ }
+ },
+ extraReducers: (builder) => {
+ // Fetch receipts
+ builder
+ .addCase(fetchReceipts.pending, (state) => {
+ state.isLoading = true;
+ state.error = null;
+ })
+ .addCase(fetchReceipts.fulfilled, (state, action) => {
+ state.isLoading = false;
+ state.receipts = action.payload.data || [];
+ state.pagination = action.payload.pagination || null;
+ // Calculate stats
+ const receipts = action.payload.data || [];
+ state.stats = {
+ totalReceipts: receipts.length,
+ pendingApproval: receipts.filter(r => r.status === 'pending').length,
+ approved: receipts.filter(r => r.status === 'approved').length,
+ totalSales: receipts.reduce((sum, r) => sum + r.saleAmount, 0)
+ };
+ })
+ .addCase(fetchReceipts.rejected, (state, action) => {
+ state.isLoading = false;
+ state.error = action.error.message || 'Failed to fetch receipts';
+ });
+
+ // Upload receipt
+ builder
+ .addCase(uploadReceipt.pending, (state) => {
+ state.isUploading = true;
+ state.error = null;
+ })
+ .addCase(uploadReceipt.fulfilled, (state, action) => {
+ state.isUploading = false;
+ // Add the new receipt to the list
+ if (action.payload.data) {
+ state.receipts.unshift(action.payload.data);
+ // Recalculate stats
+ const receipts = state.receipts;
+ state.stats = {
+ totalReceipts: receipts.length,
+ pendingApproval: receipts.filter(r => r.status === 'pending').length,
+ approved: receipts.filter(r => r.status === 'approved').length,
+ totalSales: receipts.reduce((sum, r) => sum + r.saleAmount, 0)
+ };
+ }
+ })
+ .addCase(uploadReceipt.rejected, (state, action) => {
+ state.isUploading = false;
+ state.error = action.error.message || 'Failed to upload receipt';
+ });
+
+ // Delete receipt
+ builder
+ .addCase(deleteReceipt.fulfilled, (state, action) => {
+ state.receipts = state.receipts.filter(r => r.id !== action.payload.receiptId);
+ // Recalculate stats
+ const receipts = state.receipts;
+ state.stats = {
+ totalReceipts: receipts.length,
+ pendingApproval: receipts.filter(r => r.status === 'pending').length,
+ approved: receipts.filter(r => r.status === 'approved').length,
+ totalSales: receipts.reduce((sum, r) => sum + r.saleAmount, 0)
+ };
+ });
+
+ // Download receipt
+ builder
+ .addCase(downloadReceipt.fulfilled, (state, action) => {
+ // Handle download success (usually no state changes needed)
+ });
+ }
+});
+
+// Export actions
+export const {
+ setFilters,
+ clearFilters,
+ setSelectedReceipt,
+ clearError,
+ updateReceiptStatus,
+ calculateStats
+} = receiptSlice.actions;
+
+// Export selectors
+export const selectReceipts = (state: { receipts: ReceiptState }) => state.receipts.receipts;
+export const selectReceiptPagination = (state: { receipts: ReceiptState }) => state.receipts.pagination;
+export const selectReceiptFilters = (state: { receipts: ReceiptState }) => state.receipts.filters;
+export const selectReceiptsLoading = (state: { receipts: ReceiptState }) => state.receipts.isLoading;
+export const selectReceiptsUploading = (state: { receipts: ReceiptState }) => state.receipts.isUploading;
+export const selectReceiptsError = (state: { receipts: ReceiptState }) => state.receipts.error;
+export const selectSelectedReceipt = (state: { receipts: ReceiptState }) => state.receipts.selectedReceipt;
+export const selectReceiptStats = (state: { receipts: ReceiptState }) => state.receipts.stats;
+
+// Export reducer
+export default receiptSlice.reducer;
\ No newline at end of file
diff --git a/src/store/slices/themeSlice.ts b/src/store/slices/themeSlice.ts
index 218bb24..07770bd 100644
--- a/src/store/slices/themeSlice.ts
+++ b/src/store/slices/themeSlice.ts
@@ -12,10 +12,10 @@ const getInitialTheme = (): Theme => {
const savedTheme = localStorage.getItem('theme') as Theme;
if (savedTheme) return savedTheme;
- const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
- return systemTheme;
+ // Default to dark theme instead of system preference
+ return 'dark';
}
- return 'light';
+ return 'dark';
};
const initialState: ThemeState = {
diff --git a/src/types/receipt.ts b/src/types/receipt.ts
new file mode 100644
index 0000000..71137c8
--- /dev/null
+++ b/src/types/receipt.ts
@@ -0,0 +1,104 @@
+export interface Receipt {
+ id: number;
+ receiptNumber: string;
+ orderId: number;
+ resellerId: number;
+ vendorId: number;
+ channelPartnerId: number;
+ clientName: string;
+ clientEmail?: string;
+ clientPhone?: string;
+ clientAddress?: string;
+ saleAmount: number;
+ currency: string;
+ saleDate: string;
+ paymentMethod?: string;
+ paymentStatus: 'pending' | 'paid' | 'partial' | 'failed';
+ receiptFile: string;
+ receiptFileType?: string;
+ receiptFileSize?: number;
+ description?: string;
+ status: 'pending' | 'approved' | 'rejected' | 'disputed';
+ approvedBy?: number;
+ approvedAt?: string;
+ rejectionReason?: string;
+ metadata?: Record;
+ createdAt: string;
+ updatedAt: string;
+
+ // Related data
+ order?: {
+ id: number;
+ orderNumber: string;
+ items?: Array<{
+ id: number;
+ product: {
+ id: number;
+ name: string;
+ sku: string;
+ price: number;
+ };
+ quantity: number;
+ unitPrice: number;
+ }>;
+ };
+ vendor?: {
+ id: number;
+ firstName: string;
+ lastName: string;
+ email: string;
+ company?: string;
+ };
+ reseller?: {
+ id: number;
+ companyName: string;
+ contactEmail: string;
+ contactPhone: string;
+ };
+ approver?: {
+ id: number;
+ firstName: string;
+ lastName: string;
+ email: string;
+ };
+}
+
+export interface ReceiptUploadData {
+ orderId: number;
+ clientName: string;
+ clientEmail?: string;
+ clientPhone?: string;
+ clientAddress?: string;
+ saleAmount: number;
+ currency?: string;
+ saleDate?: string;
+ paymentMethod?: string;
+ description?: string;
+ receiptFile?: File | null;
+}
+
+export interface ReceiptFilters {
+ status?: string;
+ startDate?: string;
+ endDate?: string;
+ resellerId?: number;
+ search?: string;
+ page?: number;
+ limit?: number;
+}
+
+export interface ReceiptResponse {
+ success: boolean;
+ data: Receipt[];
+ pagination?: {
+ currentPage: number;
+ totalPages: number;
+ totalItems: number;
+ itemsPerPage: number;
+ };
+}
+
+export interface ReceiptStatusUpdate {
+ status: 'approved' | 'rejected';
+ rejectionReason?: string;
+}
\ No newline at end of file
diff --git a/src/types/vendor.ts b/src/types/vendor.ts
new file mode 100644
index 0000000..4637076
--- /dev/null
+++ b/src/types/vendor.ts
@@ -0,0 +1,41 @@
+export interface VendorRequest {
+ id: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+ phone: string;
+ company: string;
+ role: string;
+ userType: string;
+ status: 'pending' | 'approved' | 'rejected';
+ createdAt: string;
+ rejectionReason?: string;
+ // Vendor-specific fields from User model
+ companyType?: string;
+ registrationNumber?: string;
+ gstNumber?: string;
+ panNumber?: string;
+ address?: string;
+ website?: string;
+ businessLicense?: string;
+ taxId?: string;
+ industry?: string;
+ yearsInBusiness?: number;
+ annualRevenue?: number;
+ employeeCount?: number;
+}
+
+export interface VendorModalProps {
+ vendor: VendorRequest | null;
+ isOpen: boolean;
+ onClose: () => void;
+ onApprove: (vendorId: string) => void;
+ onReject: (vendorId: string, reason: string) => void;
+}
+
+export interface RejectionModalProps {
+ vendor: VendorRequest | null;
+ isOpen: boolean;
+ onClose: () => void;
+ onReject: (vendorId: string, reason: string) => void;
+}
\ No newline at end of file
diff --git a/src/utils/authTest.ts b/src/utils/authTest.ts
new file mode 100644
index 0000000..1cc3c7b
--- /dev/null
+++ b/src/utils/authTest.ts
@@ -0,0 +1,38 @@
+// Utility to test authentication persistence
+export const testAuthPersistence = () => {
+ console.log('=== Testing Authentication Persistence ===');
+
+ // Check localStorage
+ const accessToken = localStorage.getItem('accessToken');
+ const refreshToken = localStorage.getItem('refreshToken');
+ const sessionId = localStorage.getItem('sessionId');
+
+ console.log('localStorage tokens:');
+ console.log('- accessToken:', accessToken ? 'Present' : 'Missing');
+ console.log('- refreshToken:', refreshToken ? 'Present' : 'Missing');
+ console.log('- sessionId:', sessionId ? 'Present' : 'Missing');
+
+ // Check if tokens are valid (not expired)
+ if (accessToken) {
+ try {
+ const payload = JSON.parse(atob(accessToken.split('.')[1]));
+ const expiry = new Date(payload.exp * 1000);
+ const now = new Date();
+
+ console.log('Token expiry:', expiry.toISOString());
+ console.log('Current time:', now.toISOString());
+ console.log('Token expired:', expiry < now);
+ } catch (error) {
+ console.log('Error parsing token:', error);
+ }
+ }
+
+ console.log('==========================================');
+};
+
+// Function to simulate page reload
+export const simulatePageReload = () => {
+ console.log('Simulating page reload...');
+ // Clear any in-memory state but keep localStorage
+ window.location.reload();
+};
\ No newline at end of file
diff --git a/src/utils/validation.ts b/src/utils/validation.ts
index 7587db8..5da910c 100644
--- a/src/utils/validation.ts
+++ b/src/utils/validation.ts
@@ -1,7 +1,7 @@
// Phone number validation
export const validatePhoneNumber = (phone: string): boolean => {
// Allow international format with optional +, digits only, 7-15 digits total
- const phoneRegex = /^[\+]?[1-9][\d]{6,14}$/;
+ const phoneRegex = /^[+]?[1-9][\d]{6,14}$/;
return phoneRegex.test(phone);
};