Cloudtopiaa_Reseller_Frontend/src/components/VendorCertificates.tsx

436 lines
18 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Award, Search, Filter, Download, Eye, Calendar, User, BookOpen } from 'lucide-react';
import toast from 'react-hot-toast';
interface Certificate {
id: number;
certificateNumber: string;
issuedAt: string;
completionDate: string;
grade: string;
score: number;
course: {
id: number;
title: string;
description: string;
level: string;
category: string;
};
user: {
id: number;
firstName: string;
lastName: string;
email: string;
avatar?: string;
};
}
interface CertificateStats {
totalCertificates: number;
thisMonthCertificates: number;
thisYearCertificates: number;
averageScore: number;
}
const VendorCertificates: React.FC = () => {
const [certificates, setCertificates] = useState<Certificate[]>([]);
const [stats, setStats] = useState<CertificateStats | null>(null);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [selectedCourse, setSelectedCourse] = useState<string>('all');
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [courses, setCourses] = useState<Array<{ id: number; title: string }>>([]);
useEffect(() => {
fetchCertificates();
fetchCertificateStats();
fetchCourses();
}, [currentPage, search, selectedCourse]);
const fetchCertificates = async () => {
try {
setLoading(true);
const token = localStorage.getItem('accessToken');
const params = new URLSearchParams({
page: currentPage.toString(),
limit: '10'
});
if (search) params.append('search', search);
if (selectedCourse !== 'all') params.append('courseId', selectedCourse);
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/training-new/vendor/certificates?${params}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setCertificates(data.data.certificates || []);
setTotalPages(data.data.totalPages || 1);
} else {
toast.error('Failed to fetch certificates');
}
} catch (error) {
console.error('Error fetching certificates:', error);
toast.error('Failed to fetch certificates');
} finally {
setLoading(false);
}
};
const fetchCertificateStats = async () => {
try {
const token = localStorage.getItem('accessToken');
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/training-new/vendor/certificates/stats`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setStats(data.data);
}
} catch (error) {
console.error('Error fetching certificate stats:', error);
}
};
const fetchCourses = async () => {
try {
const token = localStorage.getItem('accessToken');
const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/training-new/courses`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setCourses(data.data.courses || []);
}
} catch (error) {
console.error('Error fetching courses:', error);
}
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setCurrentPage(1);
};
const handleCourseFilter = (courseId: string) => {
setSelectedCourse(courseId);
setCurrentPage(1);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
const getGradeColor = (grade: string) => {
switch (grade) {
case 'Pass': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
case 'Merit': return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
case 'Distinction': return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200';
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
}
};
const getLevelColor = (level: string) => {
switch (level) {
case 'Beginner': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
case 'Intermediate': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
case 'Advanced': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
}
};
if (loading && certificates.length === 0) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-secondary-900 dark:text-white mb-3">
Certificates Issued
</h1>
<p className="text-secondary-600 dark:text-secondary-400 text-lg">
Track all certificates issued to your resellers
</p>
</div>
{/* Stats Cards */}
{stats && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
Total Certificates
</p>
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
{stats.totalCertificates}
</p>
</div>
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<Award className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
This Month
</p>
<p className="text-3xl font-bold text-green-600 dark:text-green-400">
{stats.thisMonthCertificates}
</p>
</div>
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center">
<Calendar className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
This Year
</p>
<p className="text-3xl font-bold text-yellow-600 dark:text-yellow-400">
{stats.thisYearCertificates}
</p>
</div>
<div className="w-12 h-12 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center">
<Calendar className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
Average Score
</p>
<p className="text-3xl font-bold text-purple-600 dark:text-purple-400">
{stats.averageScore}%
</p>
</div>
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center">
<BookOpen className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
</div>
</div>
</div>
)}
{/* Filters and Search */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex flex-col lg:flex-row gap-4 items-center justify-between">
<div className="flex flex-col sm:flex-row gap-4 flex-1">
<form onSubmit={handleSearch} className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Search by reseller name, email, or course..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
</div>
</form>
<div className="flex items-center space-x-2">
<Filter className="w-4 h-4 text-gray-400" />
<select
value={selectedCourse}
onChange={(e) => handleCourseFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="all">All Courses</option>
{courses.map((course) => (
<option key={course.id} value={course.id}>
{course.title}
</option>
))}
</select>
</div>
</div>
</div>
</div>
{/* Certificates Table */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Reseller
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Course
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Certificate
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Grade & Score
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Issued Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{certificates.map((certificate) => (
<tr key={certificate.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-10 h-10 bg-gray-200 dark:bg-gray-600 rounded-full flex items-center justify-center mr-3">
{certificate.user.avatar ? (
<img src={certificate.user.avatar} alt={`${certificate.user.firstName} ${certificate.user.lastName}`} className="w-10 h-10 rounded-full" />
) : (
<span className="text-gray-600 dark:text-gray-400 font-medium">
{certificate.user.firstName.charAt(0)}{certificate.user.lastName.charAt(0)}
</span>
)}
</div>
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{certificate.user.firstName} {certificate.user.lastName}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{certificate.user.email}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{certificate.course.title}
</div>
<div className="flex items-center space-x-2 mt-1">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getLevelColor(certificate.course.level)}`}>
{certificate.course.level}
</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
{certificate.course.category}
</span>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white font-mono">
{certificate.certificateNumber}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center space-x-2">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getGradeColor(certificate.grade)}`}>
{certificate.grade}
</span>
<span className="text-sm text-gray-900 dark:text-white">
{certificate.score}%
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{formatDate(certificate.issuedAt)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
title="View Certificate"
>
<Eye className="w-4 h-4" />
</button>
<button
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
title="Download Certificate"
>
<Download className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{certificates.length === 0 && !loading && (
<div className="text-center py-12">
<div className="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-4">
<Award className="w-8 h-8 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
No certificates issued yet
</h3>
<p className="text-gray-500 dark:text-gray-400">
Certificates will appear here once resellers complete your courses
</p>
</div>
)}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between">
<div className="text-sm text-gray-700 dark:text-gray-300">
Page {currentPage} of {totalPages}
</div>
<div className="flex space-x-2">
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)}
</div>
);
};
export default VendorCertificates;