diff --git a/api/CourseRegistration.API/Controllers/CertificatesController.cs b/api/CourseRegistration.API/Controllers/CertificatesController.cs new file mode 100644 index 0000000..108eca7 --- /dev/null +++ b/api/CourseRegistration.API/Controllers/CertificatesController.cs @@ -0,0 +1,140 @@ +using Microsoft.AspNetCore.Mvc; +using CourseRegistration.Application.Services; +using CourseRegistration.Application.DTOs; + +namespace CourseRegistration.API.Controllers; + +/// +/// API controller for certificate operations +/// +[ApiController] +[Route("api/[controller]")] +public class CertificatesController : ControllerBase +{ + private readonly ICertificateService _certificateService; + private readonly ILogger _logger; + + public CertificatesController(ICertificateService certificateService, ILogger logger) + { + _certificateService = certificateService; + _logger = logger; + } + + /// + /// Get certificate by ID + /// + /// Certificate ID + /// Certificate details + [HttpGet("{id}")] + [ProducesResponseType(typeof(CertificateDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetCertificateById(Guid id) + { + try + { + _logger.LogInformation("Getting certificate with ID: {CertificateId}", id); + var certificate = await _certificateService.GetCertificateByIdAsync(id); + + if (certificate == null) + { + _logger.LogWarning("Certificate not found: {CertificateId}", id); + return NotFound(new { message = $"Certificate with ID {id} not found" }); + } + + return Ok(certificate); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving certificate {CertificateId}", id); + return StatusCode(StatusCodes.Status500InternalServerError, + new { message = "An error occurred while retrieving the certificate" }); + } + } + + /// + /// Get certificates by student ID + /// + /// Student ID + /// List of certificates for the student + [HttpGet("student/{studentId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetCertificatesByStudentId(Guid studentId) + { + try + { + _logger.LogInformation("Getting certificates for student: {StudentId}", studentId); + var certificates = await _certificateService.GetCertificatesByStudentIdAsync(studentId); + return Ok(certificates); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving certificates for student {StudentId}", studentId); + return StatusCode(StatusCodes.Status500InternalServerError, + new { message = "An error occurred while retrieving certificates" }); + } + } + + /// + /// Search certificates by student name + /// + /// Student name (partial match supported) + /// List of matching certificates + [HttpGet("search")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> SearchCertificates([FromQuery] string studentName) + { + try + { + if (string.IsNullOrWhiteSpace(studentName)) + { + return BadRequest(new { message = "Student name is required" }); + } + + _logger.LogInformation("Searching certificates for student name: {StudentName}", studentName); + var certificates = await _certificateService.GetCertificatesByStudentNameAsync(studentName); + return Ok(certificates); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error searching certificates for student name {StudentName}", studentName); + return StatusCode(StatusCodes.Status500InternalServerError, + new { message = "An error occurred while searching certificates" }); + } + } + + /// + /// Create a new certificate + /// + /// Certificate creation data + /// Created certificate + [HttpPost] + [ProducesResponseType(typeof(CertificateDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> CreateCertificate([FromBody] CreateCertificateDto createCertificateDto) + { + try + { + if (createCertificateDto == null) + { + return BadRequest(new { message = "Certificate data is required" }); + } + + _logger.LogInformation("Creating certificate for student {StudentId} and course {CourseId}", + createCertificateDto.StudentId, createCertificateDto.CourseId); + + var certificate = await _certificateService.CreateCertificateAsync(createCertificateDto); + + return CreatedAtAction( + nameof(GetCertificateById), + new { id = certificate.CertificateId }, + certificate + ); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating certificate"); + return StatusCode(StatusCodes.Status500InternalServerError, + new { message = "An error occurred while creating the certificate" }); + } + } +} diff --git a/api/CourseRegistration.API/Program.cs b/api/CourseRegistration.API/Program.cs index e7e52e4..dd57dc0 100644 --- a/api/CourseRegistration.API/Program.cs +++ b/api/CourseRegistration.API/Program.cs @@ -66,6 +66,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Register authorization services builder.Services.AddScoped(); @@ -299,6 +300,54 @@ static async Task SeedDatabase(CourseRegistrationDbContext context) context.Registrations.AddRange(registrations); await context.SaveChangesAsync(); - Log.Information("Database seeded successfully with {StudentCount} students, {CourseCount} courses, and {RegistrationCount} registrations.", - students.Length, courses.Length, registrations.Length); + // Sample certificates for completed courses + var certificates = new[] + { + new CourseRegistration.Domain.Entities.Certificate + { + StudentId = students[0].StudentId, + CourseId = courses[0].CourseId, + IssueDate = DateTime.UtcNow.AddDays(-30), + FinalGrade = CourseRegistration.Domain.Enums.Grade.A, + CertificateNumber = "CERT-2024-001", + Remarks = "Outstanding performance throughout the course", + DigitalSignature = "DS-" + Guid.NewGuid().ToString()[..8] + }, + new CourseRegistration.Domain.Entities.Certificate + { + StudentId = students[1].StudentId, + CourseId = courses[2].CourseId, + IssueDate = DateTime.UtcNow.AddDays(-20), + FinalGrade = CourseRegistration.Domain.Enums.Grade.B, + CertificateNumber = "CERT-2024-002", + Remarks = "Strong practical skills demonstrated", + DigitalSignature = "DS-" + Guid.NewGuid().ToString()[..8] + }, + new CourseRegistration.Domain.Entities.Certificate + { + StudentId = students[2].StudentId, + CourseId = courses[4].CourseId, + IssueDate = DateTime.UtcNow.AddDays(-10), + FinalGrade = CourseRegistration.Domain.Enums.Grade.A, + CertificateNumber = "CERT-2024-003", + Remarks = "Exceptional database design project", + DigitalSignature = "DS-" + Guid.NewGuid().ToString()[..8] + }, + new CourseRegistration.Domain.Entities.Certificate + { + StudentId = students[3].StudentId, + CourseId = courses[1].CourseId, + IssueDate = DateTime.UtcNow.AddDays(-5), + FinalGrade = CourseRegistration.Domain.Enums.Grade.B, + CertificateNumber = "CERT-2024-004", + Remarks = "Good analytical and problem-solving skills", + DigitalSignature = "DS-" + Guid.NewGuid().ToString()[..8] + } + }; + + context.Certificates.AddRange(certificates); + await context.SaveChangesAsync(); + + Log.Information("Database seeded successfully with {StudentCount} students, {CourseCount} courses, {RegistrationCount} registrations, and {CertificateCount} certificates.", + students.Length, courses.Length, registrations.Length, certificates.Length); } diff --git a/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContext.cs b/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContext.cs index 7491da8..6bfb967 100644 --- a/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContext.cs +++ b/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContext.cs @@ -32,6 +32,11 @@ public CourseRegistrationDbContext(DbContextOptions /// public DbSet Registrations { get; set; } = null!; + /// + /// Certificates DbSet + /// + public DbSet Certificates { get; set; } = null!; + /// /// Configures the model relationships and constraints /// @@ -108,6 +113,34 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(r => r.CourseId) .OnDelete(DeleteBehavior.Cascade); }); + + // Configure Certificate entity + modelBuilder.Entity(entity => + { + entity.HasKey(c => c.CertificateId); + entity.Property(c => c.StudentId).IsRequired(); + entity.Property(c => c.CourseId).IsRequired(); + entity.Property(c => c.IssueDate).IsRequired(); + entity.Property(c => c.FinalGrade).IsRequired() + .HasConversion(); + entity.Property(c => c.CertificateNumber).IsRequired().HasMaxLength(20); + entity.Property(c => c.Remarks).HasMaxLength(200); + entity.Property(c => c.DigitalSignature).HasMaxLength(100); + + // Create unique constraint for certificate number + entity.HasIndex(c => c.CertificateNumber).IsUnique(); + + // Configure relationships + entity.HasOne(c => c.Student) + .WithMany() + .HasForeignKey(c => c.StudentId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(c => c.Course) + .WithMany() + .HasForeignKey(c => c.CourseId) + .OnDelete(DeleteBehavior.Cascade); + }); } /// diff --git a/frontend/index.html b/frontend/index.html index 9773824..a78f037 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -34,6 +34,10 @@

Course Registration System

Registrations + + + Certificates + @@ -269,6 +273,43 @@

No registrations found

+ + + @@ -404,6 +445,22 @@

Registration Details

+ + + diff --git a/frontend/script.js b/frontend/script.js index 174617a..78ba4cc 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -903,4 +903,274 @@ window.showRegistrations = showRegistrations; window.showCreateRegistration = showCreateRegistration; window.showRegistrationDetails = showRegistrationDetails; window.refreshRegistrations = refreshRegistrations; -window.applyFilters = applyFilters; \ No newline at end of file +window.applyFilters = applyFilters; + +// ===== CERTIFICATES SECTION ===== + +// Show certificates section +function showCertificates(event) { + // Hide all sections + document.querySelectorAll('.section').forEach(section => { + section.classList.add('hidden'); + }); + + // Show certificates section + document.getElementById('certificates-section').classList.remove('hidden'); + + // Update navigation + updateNavigation(); + + // Set active nav link if event is provided + if (event) { + document.querySelectorAll('.nav-link').forEach(link => { + link.classList.remove('active'); + }); + event.target.closest('.nav-link').classList.add('active'); + } + + // Clear search results initially + document.getElementById('certificates-container').innerHTML = ''; + document.getElementById('certificates-empty').classList.add('hidden'); + document.getElementById('certificate-search-input').value = ''; +} + +// Search certificates +async function searchCertificates() { + const searchInput = document.getElementById('certificate-search-input'); + const searchTerm = searchInput.value.trim(); + + if (!searchTerm) { + showCertificateMessage('Please enter a student name or certificate ID to search'); + return; + } + + const loadingElement = document.getElementById('certificates-loading'); + const containerElement = document.getElementById('certificates-container'); + const emptyElement = document.getElementById('certificates-empty'); + + // Show loading + loadingElement.classList.remove('hidden'); + containerElement.innerHTML = ''; + emptyElement.classList.add('hidden'); + + try { + // Check if search term is a GUID (certificate ID) + const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + let certificates = []; + + if (guidPattern.test(searchTerm)) { + // Search by certificate ID + const response = await fetch(`${API_BASE_URL}/certificates/${searchTerm}`); + if (response.ok) { + const certificate = await response.json(); + certificates = [certificate]; + } + } else { + // Search by student name + const response = await fetch(`${API_BASE_URL}/certificates/search?studentName=${encodeURIComponent(searchTerm)}`); + if (response.ok) { + certificates = await response.json(); + } + } + + loadingElement.classList.add('hidden'); + + if (certificates && certificates.length > 0) { + displayCertificates(certificates); + } else { + containerElement.innerHTML = ''; + emptyElement.classList.remove('hidden'); + } + } catch (error) { + console.error('Error searching certificates:', error); + loadingElement.classList.add('hidden'); + showErrorModal('Failed to search certificates. Please try again.'); + } +} + +// Display certificates +function displayCertificates(certificates) { + const container = document.getElementById('certificates-container'); + + container.innerHTML = certificates.map(cert => ` +
+
+
+ +
+
+ ${escapeHtml(cert.certificateNumber)} +
+
+
+

${escapeHtml(cert.studentName)}

+

${escapeHtml(cert.courseName)}

+

${escapeHtml(cert.instructorName)}

+
+ + Grade: ${getGradeText(cert.finalGrade)} + + + + ${formatDate(cert.issueDate)} + +
+
+ +
+ `).join(''); +} + +// Show certificate details +async function showCertificateDetails(certificateId) { + try { + const response = await fetch(`${API_BASE_URL}/certificates/${certificateId}`); + + if (response.ok) { + const certificate = await response.json(); + const modal = document.getElementById('certificate-details-modal'); + const content = document.getElementById('certificate-details-content'); + + content.innerHTML = ` +
+
+ +

Certificate of Completion

+
+ +
+
+

This certifies that

+

${escapeHtml(certificate.studentName)}

+
+ +
+

Has successfully completed

+

${escapeHtml(certificate.courseName)}

+
+ +
+
+ +
+ Instructor +

${escapeHtml(certificate.instructorName)}

+
+
+ +
+ +
+ Final Grade +

+ ${getGradeText(certificate.finalGrade)} +

+
+
+ +
+ +
+ Course Duration +

${formatDate(certificate.courseStartDate)} - ${formatDate(certificate.courseEndDate)}

+
+
+ +
+ +
+ Issue Date +

${formatDate(certificate.issueDate)}

+
+
+ +
+ +
+ Certificate Number +

${escapeHtml(certificate.certificateNumber)}

+
+
+ +
+ +
+ Digital Signature +

${escapeHtml(certificate.digitalSignature || 'N/A')}

+
+
+
+ + ${certificate.remarks ? ` +
+ + Remarks: +

${escapeHtml(certificate.remarks)}

+
+ ` : ''} +
+ + +
+ `; + + modal.classList.remove('hidden'); + } else { + showErrorModal('Certificate not found'); + } + } catch (error) { + console.error('Error loading certificate details:', error); + showErrorModal('Failed to load certificate details'); + } +} + +// Helper function to show certificate message +function showCertificateMessage(message) { + const container = document.getElementById('certificates-container'); + const emptyElement = document.getElementById('certificates-empty'); + + container.innerHTML = ''; + emptyElement.classList.add('hidden'); + + container.innerHTML = ` +
+ +

${message}

+
+ `; +} + +// Helper function to get grade class +function getGradeClass(grade) { + switch (grade) { + case 0: return 'a'; // Grade A + case 1: return 'b'; // Grade B + case 2: return 'c'; // Grade C + case 3: return 'd'; // Grade D + case 4: return 'f'; // Grade F + default: return 'unknown'; + } +} + +// Helper function to get grade text +function getGradeText(grade) { + switch (grade) { + case 0: return 'A (Excellent)'; + case 1: return 'B (Good)'; + case 2: return 'C (Satisfactory)'; + case 3: return 'D (Poor)'; + case 4: return 'F (Fail)'; + default: return 'Unknown'; + } +} + +// Export certificate functions +window.showCertificates = showCertificates; +window.searchCertificates = searchCertificates; +window.showCertificateDetails = showCertificateDetails; \ No newline at end of file diff --git a/frontend/styles.css b/frontend/styles.css index 757453e..dc77187 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -785,6 +785,319 @@ body { } } +/* ===== CERTIFICATES SECTION ===== */ + +/* Certificates Grid */ +.certificates-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1.5rem; + margin-top: 2rem; +} + +/* Certificate Card */ +.certificate-card { + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: all 0.3s ease; + overflow: hidden; + border: 2px solid transparent; +} + +.certificate-card:hover { + box-shadow: 0 8px 20px rgba(102, 126, 234, 0.2); + transform: translateY(-4px); + border-color: #667eea; +} + +.certificate-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.certificate-icon { + font-size: 2rem; +} + +.certificate-number { + font-size: 0.875rem; + font-weight: 600; + background: rgba(255, 255, 255, 0.2); + padding: 0.5rem 1rem; + border-radius: 20px; +} + +.certificate-body { + padding: 1.5rem; +} + +.certificate-body h3 { + margin: 0 0 1rem 0; + color: #1e293b; + font-size: 1.25rem; +} + +.certificate-body .course-name, +.certificate-body .instructor-name { + color: #64748b; + font-size: 0.875rem; + margin: 0.5rem 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.certificate-body .course-name i, +.certificate-body .instructor-name i { + color: #667eea; +} + +.certificate-meta { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid #e2e8f0; +} + +.grade-badge { + padding: 0.375rem 0.75rem; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.grade-a { + background: #dcfce7; + color: #16a34a; +} + +.grade-b { + background: #dbeafe; + color: #2563eb; +} + +.grade-c { + background: #fef3c7; + color: #d97706; +} + +.grade-d { + background: #fed7aa; + color: #ea580c; +} + +.grade-f { + background: #fee2e2; + color: #dc2626; +} + +.issue-date { + color: #64748b; + font-size: 0.75rem; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.certificate-footer { + padding: 1rem 1.5rem; + background: #f8fafc; + border-top: 1px solid #e2e8f0; +} + +.btn-view-certificate { + width: 100%; + padding: 0.75rem; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + font-weight: 500; + cursor: pointer; + transition: background 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.btn-view-certificate:hover { + background: #5568d3; +} + +/* Certificate Display Modal */ +.certificate-modal .modal-content { + max-width: 800px; +} + +.certificate-display { + background: white; +} + +.certificate-banner { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 2rem; + text-align: center; + border-radius: 12px 12px 0 0; +} + +.certificate-banner i { + font-size: 3rem; + margin-bottom: 1rem; +} + +.certificate-banner h2 { + margin: 0; + font-size: 2rem; + font-weight: 700; +} + +.certificate-content { + padding: 2rem; +} + +.certificate-section { + text-align: center; + margin: 2rem 0; +} + +.certificate-section h3 { + color: #64748b; + font-size: 1rem; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.student-name-large { + font-size: 2rem; + font-weight: 700; + color: #1e293b; + margin: 0.5rem 0; +} + +.course-name-large { + font-size: 1.5rem; + font-weight: 600; + color: #667eea; + margin: 0.5rem 0; +} + +.certificate-details-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.5rem; + margin: 2rem 0; +} + +.detail-item { + display: flex; + gap: 1rem; + padding: 1rem; + background: #f8fafc; + border-radius: 8px; + border-left: 4px solid #667eea; +} + +.detail-item i { + font-size: 1.5rem; + color: #667eea; + margin-top: 0.25rem; +} + +.detail-item strong { + display: block; + color: #64748b; + font-size: 0.875rem; + margin-bottom: 0.25rem; +} + +.detail-item p { + margin: 0; + color: #1e293b; + font-weight: 500; +} + +.grade-display { + display: inline-block; + padding: 0.5rem 1rem; + border-radius: 20px; + font-weight: 600; +} + +.signature-code { + font-family: monospace; + font-size: 0.875rem; + color: #667eea; +} + +.certificate-remarks { + margin-top: 2rem; + padding: 1rem; + background: #fef3c7; + border-left: 4px solid #f59e0b; + border-radius: 8px; +} + +.certificate-remarks i { + color: #f59e0b; + margin-right: 0.5rem; +} + +.certificate-remarks strong { + color: #92400e; +} + +.certificate-remarks p { + margin: 0.5rem 0 0 0; + color: #78350f; +} + +.certificate-footer-info { + text-align: center; + padding: 1.5rem; + background: #f1f5f9; + border-radius: 0 0 12px 12px; +} + +.certificate-footer-info p { + margin: 0; + color: #64748b; + font-size: 0.875rem; +} + +.certificate-footer-info i { + color: #667eea; + margin-right: 0.5rem; +} + +/* Info Message */ +.info-message { + text-align: center; + padding: 3rem; + color: #64748b; +} + +.info-message i { + font-size: 3rem; + color: #667eea; + margin-bottom: 1rem; +} + +.info-message p { + font-size: 1.125rem; + margin: 0; +} + /* Utility Classes */ .hidden { display: none !important;