diff --git a/FrontEnd/package.json b/FrontEnd/package.json index b188088..5e0ad3b 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -46,7 +46,8 @@ "styled-components": "^6.1.18", "sweetalert2": "^11.22.2", "swiper": "^11.2.8", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "xlsx": "^0.18.5" }, "scripts": { "start": "react-scripts start", diff --git a/FrontEnd/src/assets/AdminDashboard/AdminDashboard.css b/FrontEnd/src/assets/AdminDashboard/AdminDashboard.css index 7e5f2fd..2ec3e9c 100644 --- a/FrontEnd/src/assets/AdminDashboard/AdminDashboard.css +++ b/FrontEnd/src/assets/AdminDashboard/AdminDashboard.css @@ -17,6 +17,25 @@ body { padding: 24px; } +/* Dashboard Header */ +.dashboard-header { + margin-bottom: 2rem; +} + +.dashboard-greeting { + color: #6b7280; + font-size: 0.875rem; + margin: 0 0 0.5rem 0; + font-weight: 500; +} + +.dashboard-title { + font-size: 2rem; + font-weight: 700; + color: #111827; + margin: 0; +} + .ad-container .container { max-width: 1280px; margin: 0 auto; @@ -127,6 +146,14 @@ body { border-radius: 12px; overflow: hidden; margin-bottom: 24px; + transition: all 0.3s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); +} + +.ad-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + border-color: #d1d5db; } .ad-card-content { @@ -151,12 +178,13 @@ body { } .stat-icon { - width: 48px; - height: 48px; - border-radius: 12px; + width: 56px; + height: 56px; + border-radius: 14px; display: flex; align-items: center; justify-content: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .stat-icon.orange { @@ -190,15 +218,19 @@ body { } .stat-number { - font-size: 24px; + font-size: 28px; font-weight: 700; color: #1d2026; margin-bottom: 4px; + line-height: 1.2; } .stat-label { - color: #8c94a3; - font-size: 14px; + color: #6b7280; + font-size: 13px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; } /* Profile Section */ @@ -998,3 +1030,138 @@ body { width: 100%; } } + +/* NEW: Additional Stat Icons */ +.stat-icon.green { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); +} + +.stat-icon.blue { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); +} + +.stat-icon.cyan { + background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%); +} + +.stat-icon.indigo { + background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); +} + +.stat-icon.teal { + background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%); +} + +.stat-icon.pink { + background: linear-gradient(135deg, #ec4899 0%, #db2777 100%); +} + +.stat-icon.emerald { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); +} + +.stat-icon.violet { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); +} + +/* NEW: Two Column Charts Grid */ +.ad-charts-grid-two { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + margin-bottom: 24px; +} + +@media (max-width: 1024px) { + .ad-charts-grid-two { + grid-template-columns: 1fr; + } +} + +/* NEW: Top Instructors Table */ +.top-instructors-table { + overflow-x: auto; +} + +.top-instructors-table table { + width: 100%; + border-collapse: collapse; +} + +.top-instructors-table thead { + background-color: #f9fafb; + border-bottom: 2px solid #e5e7eb; +} + +.top-instructors-table th { + padding: 12px 16px; + text-align: left; + font-size: 0.875rem; + font-weight: 600; + color: #6b7280; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.top-instructors-table td { + padding: 16px; + border-bottom: 1px solid #e5e7eb; + font-size: 0.95rem; + color: #1d2026; +} + +.top-instructors-table tbody tr:hover { + background-color: #f9fafb; + transition: background-color 0.2s; +} + +.top-instructors-table .instructor-name { + font-weight: 600; + color: #7c3aed; +} + +.top-instructors-table .revenue-cell { + font-weight: 600; + color: #10b981; +} + +.top-instructors-table tbody tr:last-child td { + border-bottom: none; +} + +@media (max-width: 768px) { + .top-instructors-table { + font-size: 0.875rem; + } + + .top-instructors-table th, + .top-instructors-table td { + padding: 10px 12px; + } +} + +/* NEW: Chart Container Improvements */ +.chart-placeholder { + position: relative; + width: 100%; +} + +.chart-placeholder canvas { + max-height: 100%; +} + +/* Additional Responsive Adjustments */ +@media (max-width: 640px) { + .ad-stats-grid { + grid-template-columns: 1fr; + } + + .stat-card-content { + flex-direction: row; + } + + .stat-icon { + margin-bottom: 0; + margin-right: 16px; + } +} diff --git a/FrontEnd/src/assets/AdminDashboard/ModernAdminDashboard.css b/FrontEnd/src/assets/AdminDashboard/ModernAdminDashboard.css new file mode 100644 index 0000000..771b29e --- /dev/null +++ b/FrontEnd/src/assets/AdminDashboard/ModernAdminDashboard.css @@ -0,0 +1,536 @@ +/* Modern Financial Dashboard Style - Inspired by Trading Platforms */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.ad-dashboard-container { + padding: 1.5rem; + max-width: 1600px; + margin: 0 auto; + background: linear-gradient(135deg, #f0f4f8 0%, #e6ebf1 100%); + min-height: 100vh; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif; +} + +/* Dashboard Header */ +.ad-dashboard-header { + margin-bottom: 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.ad-dashboard-title { + font-size: 1.75rem; + font-weight: 700; + color: #1a202c; + margin-bottom: 0.25rem; + letter-spacing: -0.5px; +} + +.ad-dashboard-subtitle { + font-size: 0.9rem; + color: #718096; + font-weight: 400; +} + +/* Stats Grid - Horizontal Scroll Cards */ +.ad-stats-grid { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + overflow-x: auto; + padding-bottom: 0.5rem; + scrollbar-width: thin; + scrollbar-color: rgba(168, 85, 247, 0.3) transparent; +} + +.ad-stats-grid::-webkit-scrollbar { + height: 6px; +} + +.ad-stats-grid::-webkit-scrollbar-track { + background: transparent; +} + +.ad-stats-grid::-webkit-scrollbar-thumb { + background: rgba(168, 85, 247, 0.3); + border-radius: 10px; +} + +/* Stat Card with Gradient Background */ +.ad-stat-card { + min-width: 200px; + border-radius: 20px; + padding: 1.25rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.3); + flex-shrink: 0; +} + +/* Individual gradient colors for each card */ +.ad-stat-card:nth-child(1) { + background: linear-gradient(135deg, #76e4a6 0%, #4ade80 100%); + color: white; +} + +.ad-stat-card:nth-child(2) { + background: linear-gradient(135deg, #c084fc 0%, #a855f7 100%); + color: white; +} + +.ad-stat-card:nth-child(3) { + background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); + color: white; +} + +.ad-stat-card:nth-child(4) { + background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%); + color: white; +} + +.ad-stat-card:nth-child(5) { + background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); + color: white; +} + +.ad-stat-card:nth-child(6) { + background: linear-gradient(135deg, #ec4899 0%, #db2777 100%); + color: white; +} + +.ad-stat-card:nth-child(7) { + background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%); + color: white; +} + +.ad-stat-card:nth-child(8) { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); + color: white; +} + +.ad-stat-card:nth-child(9) { + background: linear-gradient(135deg, #f472b6 0%, #ec4899 100%); + color: white; +} + +.ad-stat-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15); +} + +/* Stat Card Content */ +.ad-stat-content { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.ad-stat-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.5rem; +} + +.ad-stat-info { + flex: 1; +} + +.ad-stat-label { + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.85); + margin-bottom: 0.5rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; +} + +.ad-stat-value { + font-size: 1.5rem; + font-weight: 700; + color: white; + margin-bottom: 0.25rem; + line-height: 1; +} + +.ad-stat-change { + font-size: 0.75rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.25rem; + color: rgba(255, 255, 255, 0.95); +} + +.ad-stat-icon { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.3s ease; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); +} + +.ad-stat-icon svg { + width: 20px; + height: 20px; + color: white; + opacity: 0.95; +} + +.ad-stat-card:hover .ad-stat-icon { + transform: scale(1.05); + background: rgba(255, 255, 255, 0.3); +} + +/* Mini Chart in Stat Card */ +.ad-stat-mini-chart { + height: 35px; + margin-top: 0.5rem; + opacity: 0.85; +} + +/* Main Content Grid */ +.ad-main-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +@media (max-width: 1200px) { + .ad-main-grid { + grid-template-columns: 1fr; + } +} + +/* Chart Cards - Clean White Background */ +.ad-card { + background: white; + border-radius: 20px; + padding: 1.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid rgba(226, 232, 240, 0.6); +} + +.ad-card:hover { + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); +} + +.ad-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.25rem; + padding-bottom: 1rem; + border-bottom: 1px solid rgba(226, 232, 240, 0.6); +} + +.ad-section-header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.ad-card-title { + font-size: 1rem; + font-weight: 600; + color: #1a202c; + margin: 0; + letter-spacing: -0.2px; +} + +.ad-card-content { + position: relative; +} + +/* Time Period Tabs */ +.ad-time-tabs { + display: flex; + gap: 0.5rem; + background: #f7fafc; + padding: 0.25rem; + border-radius: 10px; +} + +.ad-time-tab { + padding: 0.4rem 0.75rem; + font-size: 0.75rem; + font-weight: 500; + color: #718096; + background: transparent; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.ad-time-tab:hover { + color: #a855f7; + background: white; +} + +.ad-time-tab.active { + color: white; + background: #a855f7; + box-shadow: 0 2px 4px rgba(168, 85, 247, 0.3); +} + +/* Year Filter Select */ +.ad-select { + padding: 0.4rem 0.75rem; + border: 1.5px solid #e2e8f0; + border-radius: 10px; + font-size: 0.8rem; + font-weight: 500; + color: #4a5568; + background: white; + cursor: pointer; + transition: all 0.2s ease; +} + +.ad-select:hover { + border-color: #a855f7; + background: #faf5ff; +} + +.ad-select:focus { + outline: none; + border-color: #a855f7; + box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.1); +} + +/* Chart Placeholder */ +.chart-placeholder { + position: relative; + width: 100%; +} + +/* Stats Info Card (Snapshot) */ +.ad-stats-info { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.ad-info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid rgba(226, 232, 240, 0.6); +} + +.ad-info-item:last-child { + border-bottom: none; +} + +.ad-info-label { + font-size: 0.85rem; + color: #718096; + font-weight: 500; +} + +.ad-info-value { + font-size: 1.1rem; + font-weight: 600; + color: #1a202c; +} + +.ad-info-highlight { + font-size: 1.5rem; + font-weight: 700; + color: #a855f7; +} + +/* Bottom Grid for Additional Charts */ +.ad-bottom-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +@media (max-width: 768px) { + .ad-bottom-grid { + grid-template-columns: 1fr; + } +} + +/* Charts Grid */ +.ad-charts-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 1.5rem; +} + +/* Two Column Charts Grid */ +.ad-charts-grid-two { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +@media (max-width: 768px) { + .ad-charts-grid { + grid-template-columns: 1fr; + } + + .ad-charts-grid-two { + grid-template-columns: 1fr; + } +} + +/* Loading State */ +.ad-loading { + display: flex; + justify-content: center; + align-items: center; + min-height: 400px; + color: #718096; + font-size: 1rem; +} + +/* Error State */ +.ad-error { + background: #fee; + border: 1px solid #fcc; + border-radius: 12px; + padding: 1.5rem; + color: #c53030; + text-align: center; +} + +/* Top Courses/Instructors List */ +.ad-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.ad-list-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + background: #f7fafc; + border-radius: 10px; + transition: all 0.2s ease; +} + +.ad-list-item:hover { + background: #edf2f7; + transform: translateX(4px); +} + +.ad-list-item-info { + flex: 1; +} + +.ad-list-item-title { + font-size: 0.9rem; + font-weight: 600; + color: #1a202c; + margin-bottom: 0.25rem; +} + +.ad-list-item-subtitle { + font-size: 0.75rem; + color: #718096; +} + +.ad-list-item-value { + font-size: 1rem; + font-weight: 700; + color: #a855f7; +} + +/* Star Rating */ +.ad-star { + width: 14px; + height: 14px; + display: inline-block; +} + +.ad-star.filled { + color: #fbbf24; +} + +.ad-star.empty { + color: #e2e8f0; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .ad-dashboard-container { + padding: 1rem; + } + + .ad-stats-grid { + gap: 0.75rem; + } + + .ad-stat-card { + min-width: 180px; + padding: 1rem; + } + + .ad-stat-value { + font-size: 1.25rem; + } +} + +@media (max-width: 768px) { + .ad-dashboard-title { + font-size: 1.5rem; + } + + .ad-main-grid, + .ad-bottom-grid, + .ad-charts-grid { + grid-template-columns: 1fr; + } + + .ad-stat-card { + min-width: 160px; + } +} + +/* Animation */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.ad-card, +.ad-stat-card { + animation: fadeIn 0.4s ease-out; +} + +/* Tooltip Enhancement */ +.chartjs-tooltip { + background: rgba(0, 0, 0, 0.8) !important; + border-radius: 8px !important; + padding: 8px 12px !important; + font-size: 0.85rem !important; +} diff --git a/FrontEnd/src/assets/StudentDashboard/ModernStudentDashboard.css b/FrontEnd/src/assets/StudentDashboard/ModernStudentDashboard.css new file mode 100644 index 0000000..cef7466 --- /dev/null +++ b/FrontEnd/src/assets/StudentDashboard/ModernStudentDashboard.css @@ -0,0 +1,412 @@ +/* Modern Student Dashboard Styles */ +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + flex-wrap: wrap; + gap: 1rem; +} + +.dashboard-header h2 { + font-size: 28px; + font-weight: 700; + color: #1f2937; + margin: 0; +} + +.dashboard-header-actions { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +.export-btn { + padding: 10px 20px; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.3s ease; +} + +.export-btn.pdf { + background: linear-gradient(135deg, #e53e3e 0%, #fc8181 100%); + color: white; +} + +.export-btn.excel { + background: linear-gradient(135deg, #38a169 0%, #68d391 100%); + color: white; +} + +.export-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.export-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.learning-path-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 10px 20px; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all 0.3s ease; +} + +.learning-path-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +/* Chart Sections */ +.sd-chart-section { + background: white; + border-radius: 16px; + padding: 2rem; + margin-bottom: 2rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; +} + +.chart-header h3 { + font-size: 20px; + font-weight: 700; + color: #1f2937; + margin: 0; +} + +.filter-group { + display: flex; + gap: 10px; + align-items: center; +} + +.sd-year-select { + padding: 8px 16px; + border: 2px solid #e5e7eb; + border-radius: 8px; + background: white; + color: #374151; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + outline: none; +} + +.sd-year-select:hover { + border-color: #667eea; +} + +.sd-year-select:focus { + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.sd-chart-section h3 { + font-size: 20px; + font-weight: 700; + color: #1f2937; + margin: 0 0 1.5rem 0; +} + +.sd-chart-container { + height: 350px; + position: relative; +} + +/* Achievements Section */ +.sd-achievements-section { + background: white; + border-radius: 16px; + padding: 2rem; + margin-bottom: 2rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.sd-achievements-section h3 { + font-size: 20px; + font-weight: 700; + color: #1f2937; + margin: 0 0 1.5rem 0; +} + +.sd-achievements-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; +} + +.sd-achievement-card { + background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%); + border-radius: 12px; + padding: 1.5rem; + display: flex; + align-items: center; + gap: 1rem; + position: relative; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.sd-achievement-card.unlocked { + background: linear-gradient(135deg, #fef5e7 0%, #fdebd0 100%); + border-color: #f59e0b; +} + +.sd-achievement-card.locked { + opacity: 0.6; + filter: grayscale(0.5); +} + +.sd-achievement-card:hover { + transform: translateY(-3px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1); +} + +.achievement-icon { + font-size: 36px; + min-width: 50px; + text-align: center; +} + +.achievement-content h4 { + font-size: 16px; + font-weight: 600; + color: #1f2937; + margin: 0 0 0.25rem 0; +} + +.achievement-content p { + font-size: 13px; + color: #6b7280; + margin: 0; +} + +.achievement-badge { + position: absolute; + top: 10px; + right: 10px; + background: #22c55e; + color: white; + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: bold; +} + +/* Stats Grid */ +.sd-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.sd-stat-card { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.9) 100%); + border-radius: 16px; + padding: 1.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.sd-stat-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15); +} + +.sd-stat-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.sd-stat-icon { + width: 48px; + height: 48px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; +} + +.sd-stat-info h3 { + font-size: 32px; + font-weight: 700; + margin: 0; + color: #1f2937; +} + +.sd-stat-info p { + font-size: 14px; + color: #6b7280; + margin: 4px 0 0 0; +} + +.sd-stat-detail { + font-size: 12px; + color: #9ca3af; + margin-top: 8px; +} + +/* Charts Section */ +.sd-charts-section { + margin-bottom: 2rem; +} + +.sd-card { + background: rgba(255, 255, 255, 0.95); + border-radius: 16px; + padding: 1.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + margin-bottom: 1.5rem; +} + +.sd-card-title { + font-size: 18px; + font-weight: 700; + color: #1f2937; + margin: 0 0 1rem 0; +} + +/* Achievements Section */ +.sd-achievements-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.sd-achievement-card { + background: rgba(255, 255, 255, 0.8); + border-radius: 12px; + padding: 1rem; + text-align: center; + transition: transform 0.3s ease; + border: 2px solid transparent; +} + +.sd-achievement-card.unlocked { + border-color: #10b981; +} + +.sd-achievement-card:hover { + transform: scale(1.05); +} + +.sd-achievement-icon { + font-size: 48px; + margin-bottom: 8px; +} + +.sd-achievement-title { + font-size: 14px; + font-weight: 600; + color: #1f2937; + margin: 4px 0; +} + +.sd-achievement-desc { + font-size: 12px; + color: #6b7280; +} + +/* Courses Grid */ +.sd-courses-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.sd-course-card { + background: rgba(255, 255, 255, 0.95); + border-radius: 16px; + overflow: hidden; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; + cursor: pointer; +} + +.sd-course-card:hover { + transform: translateY(-5px); +} + +.sd-course-thumbnail { + width: 100%; + height: 180px; + object-fit: cover; +} + +.sd-course-info { + padding: 1.5rem; +} + +.sd-course-title { + font-size: 16px; + font-weight: 600; + color: #1f2937; + margin: 0 0 8px 0; +} + +.sd-progress-bar { + background: #e5e7eb; + border-radius: 8px; + height: 8px; + overflow: hidden; + margin: 8px 0; +} + +.sd-progress-fill { + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + height: 100%; + transition: width 0.3s ease; +} + +.sd-progress-text { + font-size: 12px; + color: #6b7280; +} + +/* Loading & Error States */ +.sd-loading { + text-align: center; + padding: 3rem; + color: white; + font-size: 18px; +} + +.sd-error { + background: rgba(239, 68, 68, 0.9); + color: white; + padding: 1rem 1.5rem; + border-radius: 12px; + margin-bottom: 1rem; +} diff --git a/FrontEnd/src/components/AdminDashboard/AdminDashboard.jsx b/FrontEnd/src/components/AdminDashboard/AdminDashboard.jsx index e7400b2..961aa80 100644 --- a/FrontEnd/src/components/AdminDashboard/AdminDashboard.jsx +++ b/FrontEnd/src/components/AdminDashboard/AdminDashboard.jsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from "react"; -import { Line } from "react-chartjs-2"; +import React, { useState, useEffect, useRef } from "react"; +import { Line, Bar, Doughnut, Pie } from "react-chartjs-2"; import { Chart as ChartJS, CategoryScale, @@ -7,13 +7,17 @@ import { PointElement, LineElement, BarElement, + ArcElement, Title, Tooltip, Legend, Filler, } from "chart.js"; import { getDashboardStats } from "../../services/dashboardService"; -import "../../assets/AdminDashboard/AdminDashboard.css"; +import jsPDF from "jspdf"; +import html2canvas from "html2canvas"; +import * as XLSX from "xlsx"; +import "../../assets/AdminDashboard/ModernAdminDashboard.css"; // Register Chart.js modules ChartJS.register( @@ -22,6 +26,7 @@ ChartJS.register( PointElement, LineElement, BarElement, + ArcElement, Title, Tooltip, Legend, @@ -32,6 +37,10 @@ const AdminDashboard = () => { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()); + const [availableYears, setAvailableYears] = useState([]); + const [exporting, setExporting] = useState(false); + const dashboardRef = useRef(null); // --- Icon Components --- const PlayIcon = () => ( @@ -89,12 +98,48 @@ const AdminDashboard = () => { /> ); + const TrendingUpIcon = () => ( + + + + ); + const AcademicCapIcon = () => ( + + + + + ); useEffect(() => { const fetchStats = async () => { try { - const response = await getDashboardStats(); + const response = await getDashboardStats(selectedYear); setStats(response.data); + console.log('Admin Dashboard Stats:', response.data); + console.log('Category Distribution:', response.data?.categoryDistribution); + + // Generate available years (from 2020 to current year) + const currentYear = new Date().getFullYear(); + const years = []; + for (let year = 2020; year <= currentYear; year++) { + years.push(year); + } + setAvailableYears(years); } catch (err) { setError( err.response?.data?.message || @@ -106,7 +151,145 @@ const AdminDashboard = () => { } }; fetchStats(); - }, []); + }, [selectedYear]); + + // Export Functions + const exportToPDF = async () => { + if (!dashboardRef.current) return; + + setExporting(true); + try { + const canvas = await html2canvas(dashboardRef.current, { + scale: 2, + useCORS: true, + logging: false, + }); + + const imgData = canvas.toDataURL('image/png'); + const pdf = new jsPDF({ + orientation: 'portrait', + unit: 'mm', + format: 'a4', + }); + + const imgWidth = 210; // A4 width in mm + const imgHeight = (canvas.height * imgWidth) / canvas.width; + + pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight); + pdf.save(`Admin_Dashboard_${selectedYear}_${new Date().toISOString().split('T')[0]}.pdf`); + } catch (error) { + console.error('Error exporting PDF:', error); + alert('Failed to export PDF. Please try again.'); + } finally { + setExporting(false); + } + }; + + const exportToExcel = () => { + if (!stats) return; + + setExporting(true); + try { + // Prepare data for Excel with styling + const wb = XLSX.utils.book_new(); + const worksheetData = []; + + // Title Section + worksheetData.push(['ADMIN DASHBOARD REPORT']); + worksheetData.push(['Generated:', new Date().toLocaleString('vi-VN')]); + worksheetData.push(['Year:', selectedYear]); + worksheetData.push([]); + + // Overview Statistics Section + worksheetData.push(['📊 OVERVIEW STATISTICS']); + worksheetData.push(['Metric', 'Value', 'Unit']); + worksheetData.push(['Total Users', stats.totalUsers || 0, 'users']); + worksheetData.push(['Total Courses', stats.totalCourses || 0, 'courses']); + worksheetData.push(['Total Enrollments', stats.totalEnrollments || 0, 'enrollments']); + worksheetData.push(['Total Revenue', stats.totalRevenue || 0, 'VND']); + worksheetData.push(['Revenue This Month', stats.revenueThisMonth || 0, 'VND']); + worksheetData.push(['New Users This Month', stats.newUsersThisMonth || 0, 'users']); + worksheetData.push([]); + + // Monthly Revenue Section + worksheetData.push(['💰 MONTHLY REVENUE BREAKDOWN']); + worksheetData.push(['Month', 'Revenue (VND)', 'Percentage']); + const totalYearRevenue = (stats.monthlySales || []).reduce((sum, item) => sum + item.revenue, 0); + (stats.monthlySales || []).forEach((item) => { + const monthName = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][item.month - 1]; + const percentage = totalYearRevenue > 0 ? ((item.revenue / totalYearRevenue) * 100).toFixed(2) + '%' : '0%'; + worksheetData.push([monthName, item.revenue, percentage]); + }); + worksheetData.push(['TOTAL', totalYearRevenue, '100%']); + worksheetData.push([]); + + // Category Distribution Section + worksheetData.push(['📚 CATEGORY DISTRIBUTION']); + worksheetData.push(['Category', 'Number of Courses', 'Percentage']); + const totalCourses = (stats.categoryDistribution || []).reduce((sum, cat) => sum + cat.count, 0); + (stats.categoryDistribution || []).forEach((cat) => { + const percentage = totalCourses > 0 ? ((cat.count / totalCourses) * 100).toFixed(2) + '%' : '0%'; + worksheetData.push([cat.name, cat.count, percentage]); + }); + worksheetData.push(['TOTAL', totalCourses, '100%']); + worksheetData.push([]); + + // Top Selling Courses Section + worksheetData.push(['🏆 TOP 5 SELLING COURSES']); + worksheetData.push(['Rank', 'Course Title', 'Enrollments', 'Price (VND)', 'Revenue (VND)']); + (stats.topSellingCourses || []).forEach((course, index) => { + const revenue = course.enrollmentCount * course.price; + worksheetData.push([ + `#${index + 1}`, + course.title, + course.enrollmentCount, + course.price, + revenue + ]); + }); + worksheetData.push([]); + + // Course Status Section + worksheetData.push(['📋 COURSE STATUS']); + worksheetData.push(['Status', 'Count', 'Percentage']); + const totalStatusCourses = (stats.courseStatus?.active || 0) + (stats.courseStatus?.pending || 0) + (stats.courseStatus?.draft || 0) + (stats.courseStatus?.rejected || 0); + worksheetData.push(['Active', stats.courseStatus?.active || 0, totalStatusCourses > 0 ? ((stats.courseStatus?.active / totalStatusCourses) * 100).toFixed(2) + '%' : '0%']); + worksheetData.push(['Pending', stats.courseStatus?.pending || 0, totalStatusCourses > 0 ? ((stats.courseStatus?.pending / totalStatusCourses) * 100).toFixed(2) + '%' : '0%']); + worksheetData.push(['Draft', stats.courseStatus?.draft || 0, totalStatusCourses > 0 ? ((stats.courseStatus?.draft / totalStatusCourses) * 100).toFixed(2) + '%' : '0%']); + worksheetData.push(['Rejected', stats.courseStatus?.rejected || 0, totalStatusCourses > 0 ? ((stats.courseStatus?.rejected / totalStatusCourses) * 100).toFixed(2) + '%' : '0%']); + worksheetData.push(['TOTAL', totalStatusCourses, '100%']); + worksheetData.push([]); + + // Instructor Statistics Section + worksheetData.push(['👨‍🏫 INSTRUCTOR STATISTICS']); + worksheetData.push(['Metric', 'Value', 'Note']); + worksheetData.push(['Total Instructors', stats.instructorStats?.total || 0, 'All registered instructors']); + worksheetData.push(['Active Instructors', stats.instructorStats?.active || 0, 'Instructors with published courses']); + worksheetData.push(['Total Students', stats.instructorStats?.students || 0, 'All registered students']); + const activeRate = stats.instructorStats?.total > 0 ? ((stats.instructorStats?.active / stats.instructorStats?.total) * 100).toFixed(2) + '%' : '0%'; + worksheetData.push(['Active Rate', activeRate, 'Percentage of active instructors']); + + // Create worksheet + const ws = XLSX.utils.aoa_to_sheet(worksheetData); + + // Set column widths + ws['!cols'] = [ + { wch: 35 }, // Column A - wider for titles + { wch: 22 }, // Column B + { wch: 18 }, // Column C + { wch: 18 }, // Column D + { wch: 18 }, // Column E + ]; + + XLSX.utils.book_append_sheet(wb, ws, 'Dashboard Report'); + XLSX.writeFile(wb, `Admin_Dashboard_${selectedYear}_${new Date().toISOString().split('T')[0]}.xlsx`); + } catch (error) { + console.error('Error exporting Excel:', error); + alert('Failed to export Excel. Please try again.'); + } finally { + setExporting(false); + } + }; // --- Chart Data & Options --- const monthLabels = [ @@ -125,11 +308,11 @@ const AdminDashboard = () => { ]; const revenueData = { - labels: stats?.monthlySales.map((s) => monthLabels[s.month - 1]) || [], + labels: stats?.monthlySales?.map((s) => monthLabels[s.month - 1]) || [], datasets: [ { label: "Revenue", - data: stats?.monthlySales.map((s) => s.revenue) || [], + data: stats?.monthlySales?.map((s) => s.revenue) || [], borderColor: "#7c3aed", borderWidth: 2, pointBackgroundColor: "#7c3aed", @@ -150,57 +333,437 @@ const AdminDashboard = () => { const revenueOptions = { responsive: true, maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false, + }, plugins: { legend: { display: false }, tooltip: { enabled: true, - backgroundColor: "#000", - padding: 10, - cornerRadius: 4, + backgroundColor: 'rgba(0, 0, 0, 0.9)', + padding: 12, + cornerRadius: 8, displayColors: false, + titleColor: '#fff', + bodyColor: '#fff', + titleFont: { + size: 13, + weight: '600', + }, + bodyFont: { + size: 14, + weight: '700', + }, callbacks: { + title: (context) => { + return context[0].label; + }, label: (context) => { const value = context.parsed.y .toString() .replace(/\B(?=(\d{3})+(?!\d))/g, "."); - return `${value} VND`; + return `${value} đ`; }, }, }, }, scales: { - x: { grid: { display: false }, ticks: { color: "#9ca3af" } }, + x: { + grid: { display: false }, + ticks: { + color: "#9ca3af", + font: { + size: 11, + weight: '500' + } + } + }, y: { - grid: { color: "#e5e7eb", borderDash: [5, 5] }, + grid: { + color: "#f3f4f6", + borderDash: [3, 3] + }, ticks: { color: "#9ca3af", + font: { + size: 11, + weight: '500' + }, callback: (value) => { - if (value >= 1000000) return `${value / 1000000}m`; - if (value >= 1000) return `${value / 1000}k`; + if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`; + if (value >= 1000) return `${(value / 1000).toFixed(0)}K`; return value; }, }, }, }, + animation: { + duration: 1000, + easing: 'easeInOutQuart', + }, + }; + + // NEW: User Growth Chart + const userGrowthData = { + labels: + stats?.userGrowth?.map((ug) => monthLabels[ug.month - 1]) || [], + datasets: [ + { + label: "New Users", + data: stats?.userGrowth?.map((ug) => ug.count) || [], + borderColor: "#10b981", + backgroundColor: "rgba(16, 185, 129, 0.1)", + borderWidth: 2, + tension: 0.4, + fill: true, + }, + ], + }; + + const userGrowthOptions = { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false, + }, + plugins: { + legend: { display: false }, + tooltip: { + backgroundColor: 'rgba(0, 0, 0, 0.9)', + padding: 12, + cornerRadius: 8, + displayColors: false, + titleColor: '#fff', + bodyColor: '#10b981', + titleFont: { + size: 13, + weight: '600', + }, + bodyFont: { + size: 14, + weight: '700', + }, + }, + }, + scales: { + x: { + grid: { display: false }, + ticks: { + color: "#9ca3af", + font: { size: 11, weight: '500' } + } + }, + y: { + grid: { + color: "#f3f4f6", + borderDash: [3, 3] + }, + ticks: { + color: "#9ca3af", + font: { size: 11, weight: '500' } + }, + }, + }, + animation: { + duration: 1000, + easing: 'easeInOutQuart', + }, + }; + + // NEW: Category Distribution Chart (Bar) + console.log('categoryDistribution from API:', stats?.categoryDistribution); + + const categoryData = { + labels: stats?.categoryDistribution?.length > 0 + ? stats.categoryDistribution.map((cat) => cat.name) + : ['No Data'], + datasets: [ + { + label: 'Number of Courses', + data: stats?.categoryDistribution?.length > 0 + ? stats.categoryDistribution.map((cat) => cat.count) + : [0], + backgroundColor: 'rgba(124, 58, 237, 0.8)', + borderColor: 'rgba(124, 58, 237, 1)', + borderWidth: 2, + borderRadius: 8, + barThickness: 40, + }, + ], + }; + + const categoryOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + tooltip: { + backgroundColor: "rgba(0, 0, 0, 0.8)", + padding: 12, + titleColor: "#fff", + bodyColor: "#fff", + borderColor: "rgba(124, 58, 237, 0.5)", + borderWidth: 1, + callbacks: { + label: (context) => { + return ` ${context.parsed.y} courses`; + }, + }, + }, + }, + scales: { + y: { + beginAtZero: true, + grid: { + color: "rgba(156, 163, 175, 0.1)", + drawBorder: false, + }, + ticks: { + color: "#9ca3af", + font: { size: 11, weight: '500' }, + stepSize: 1, + }, + title: { + display: true, + text: 'Number of Courses', + color: '#6b7280', + font: { size: 12, weight: '600' }, + }, + }, + x: { + grid: { + display: false, + }, + ticks: { + color: "#9ca3af", + font: { size: 11, weight: '500' }, + maxRotation: 45, + minRotation: 45, + }, + title: { + display: true, + text: 'Categories', + color: '#6b7280', + font: { size: 12, weight: '600' }, + }, + }, + }, + animation: { + duration: 1000, + easing: 'easeInOutQuart', + }, + }; + + // NEW: Top Selling Courses Chart (Horizontal Bar) + const topCoursesData = { + labels: stats?.topSellingCourses?.map((c) => c.title.slice(0, 30)) || [], + datasets: [ + { + label: "Enrollments", + data: stats?.topSellingCourses?.map((c) => c.enrollmentCount) || [], + backgroundColor: [ + 'rgba(249, 115, 22, 0.9)', + 'rgba(249, 115, 22, 0.8)', + 'rgba(249, 115, 22, 0.7)', + 'rgba(249, 115, 22, 0.6)', + 'rgba(249, 115, 22, 0.5)', + ], + borderColor: 'rgba(249, 115, 22, 1)', + borderWidth: 2, + borderRadius: 8, + barThickness: 35, + }, + ], + }; + + const topCoursesOptions = { + responsive: true, + maintainAspectRatio: false, + indexAxis: "y", + plugins: { + legend: { display: false }, + tooltip: { + backgroundColor: "rgba(0, 0, 0, 0.8)", + padding: 12, + titleColor: "#fff", + bodyColor: "#fff", + borderColor: "rgba(249, 115, 22, 0.5)", + borderWidth: 1, + displayColors: false, + callbacks: { + label: (context) => { + return ` ${context.parsed.x} enrollments`; + }, + }, + }, + }, + scales: { + x: { + beginAtZero: true, + grid: { + color: "rgba(156, 163, 175, 0.1)", + drawBorder: false, + }, + ticks: { + color: "#9ca3af", + font: { size: 11, weight: '500' }, + stepSize: 1, + }, + title: { + display: true, + text: 'Enrollments', + color: '#6b7280', + font: { size: 12, weight: '600' }, + }, + }, + y: { + grid: { display: false }, + ticks: { + color: "#9ca3af", + font: { size: 11, weight: '500' }, + }, + }, + }, + animation: { + duration: 1000, + easing: 'easeInOutQuart', + }, + }; + + // NEW: Course Status Chart (Pie) + const courseStatusData = { + labels: ["Active", "Pending", "Draft", "Rejected"], + datasets: [ + { + data: [ + stats?.courseStatus?.active || 0, + stats?.courseStatus?.pending || 0, + stats?.courseStatus?.draft || 0, + stats?.courseStatus?.rejected || 0, + ], + backgroundColor: ["#10b981", "#f59e0b", "#6b7280", "#ef4444"], + borderWidth: 0, + }, + ], + }; + + const courseStatusOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "bottom", + labels: { + padding: 15, + font: { size: 12 }, + usePointStyle: true, + }, + }, + tooltip: { + backgroundColor: "#000", + padding: 10, + }, + }, }; - // Static data for mini-chart, can be upgraded later - const ratingChartData = { - labels: ["1", "2", "3", "4", "5", "6", "7"], + // Course Rating Distribution Chart (Line with gradient) + const ratingData = { + labels: stats?.courseRating?.distribution?.map(d => d.rating.toFixed(1)) || [], datasets: [ { - data: [20, 40, 30, 50, 45, 60, 55], - borderColor: "#f97316", + label: 'Number of Courses', + data: stats?.courseRating?.distribution?.map(d => d.count) || [], + borderColor: 'rgb(124, 58, 237)', + backgroundColor: (context) => { + const ctx = context.chart.ctx; + const gradient = ctx.createLinearGradient(0, 0, 0, 300); + gradient.addColorStop(0, 'rgba(124, 58, 237, 0.4)'); + gradient.addColorStop(1, 'rgba(124, 58, 237, 0.0)'); + return gradient; + }, + borderWidth: 3, + fill: true, tension: 0.4, + pointRadius: 5, + pointHoverRadius: 7, + pointBackgroundColor: 'rgb(124, 58, 237)', + pointBorderColor: '#fff', + pointBorderWidth: 2, + pointHoverBackgroundColor: '#fff', + pointHoverBorderColor: 'rgb(124, 58, 237)', + pointHoverBorderWidth: 3, }, ], }; - const ratingChartOptions = { + + const ratingOptions = { responsive: true, maintainAspectRatio: false, - plugins: { legend: { display: false }, tooltip: { enabled: false } }, - scales: { x: { display: false }, y: { display: false } }, - elements: { point: { radius: 0 } }, + plugins: { + legend: { + display: false, + }, + tooltip: { + backgroundColor: "rgba(0, 0, 0, 0.8)", + padding: 12, + titleColor: "#fff", + bodyColor: "#fff", + borderColor: "rgba(124, 58, 237, 0.5)", + borderWidth: 1, + callbacks: { + title: (context) => `Rating: ${context[0].label} ⭐`, + label: (context) => { + const total = stats?.courseRating?.stats?.total || 0; + const percentage = total > 0 ? ((context.parsed.y / total) * 100).toFixed(1) : 0; + return ` ${context.parsed.y} courses (${percentage}%)`; + }, + }, + }, + }, + scales: { + x: { + grid: { + color: "rgba(156, 163, 175, 0.1)", + drawBorder: false, + }, + ticks: { + color: "#9ca3af", + font: { size: 11, weight: '500' }, + }, + title: { + display: true, + text: 'Rating ⭐', + color: '#6b7280', + font: { size: 12, weight: '600' }, + }, + }, + y: { + beginAtZero: true, + grid: { + color: "rgba(156, 163, 175, 0.1)", + drawBorder: false, + }, + ticks: { + color: "#9ca3af", + font: { size: 11, weight: '500' }, + stepSize: 1, + }, + title: { + display: true, + text: 'Number of Courses', + color: '#6b7280', + font: { size: 12, weight: '600' }, + }, + }, + }, + animation: { + duration: 1000, + easing: 'easeInOutQuart', + }, }; if (loading) { @@ -226,93 +789,309 @@ const AdminDashboard = () => { } return ( -
-
- {/* Stats Cards Row 1 */} -
-
-
-
-
- -
-
-

- {stats.totalEnrollments.toLocaleString()} -

-

Enrolled Courses

+
+ {/* Header */} +
+
+

Admin Dashboard

+

Welcome back! Here's your overview

+
+
+ + +
+
+ + {/* Stats Cards - Horizontal Scroll */} +
+ {/* Card 1: Total Enrollments */} +
+
+
+
+

Total Enrollments

+

{(stats?.totalEnrollments || 0).toLocaleString()}

+
+ +{((stats?.newUsersThisMonth || 0) / (stats?.totalEnrollments || 1) * 100).toFixed(1)}%
+
+ +
+
+
+ s.revenue) || [20, 35, 25, 45, 30, 50], + borderColor: 'rgba(255,255,255,0.8)', + borderWidth: 2, + fill: false, + tension: 0.4, + pointRadius: 0, + }] + }} + options={{ + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { x: { display: false }, y: { display: false } }, + }} + />
-
-
-
-
- -
-
-

- {stats.totalCourses.toLocaleString()} -

-

Total Courses

+
+ + {/* Card 2: Total Courses */} +
+
+
+
+

Total Courses

+

{(stats?.totalCourses || 0).toLocaleString()}

+
+ +{((stats?.courseStatus?.published || 0) / (stats?.totalCourses || 1) * 100).toFixed(1)}%
+
+ +
+
+
+
-
-
-
-
- -
-
-

- {stats.totalUsers.toLocaleString()} -

-

Total Users

+
+ + {/* Card 3: Total Revenue */} +
+
+
+
+

Total Revenue

+

+ {(stats?.totalRevenue || 0) + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ".")} đ +

+
+ +{((stats?.revenueThisMonth || 0) / (stats?.totalRevenue || 1) * 100).toFixed(1)}%
+
+ +
+
+
+ s.revenue) || [30, 45, 35, 55, 42, 60], + borderColor: 'rgba(255,255,255,0.8)', + borderWidth: 2, + fill: false, + tension: 0.4, + pointRadius: 0, + }] + }} + options={{ + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { x: { display: false }, y: { display: false } }, + }} + />
- {/* Stats Cards Row 2 */} -
-
-
-
-
- -
-
-

- {stats.newUsersThisMonth.toLocaleString()} -

-

New Users This Month

+ {/* Card 4: Total Students */} +
+
+
+
+

Total Students

+

{(stats?.instructorStats?.students || 0).toLocaleString()}

+
+ Online: {(stats?.instructorStats?.onlineStudents || 0).toLocaleString()}
+
+ +
+
+
+ u.count) || [40, 52, 48, 65, 58, 72], + borderColor: 'rgba(255,255,255,0.8)', + borderWidth: 2, + fill: false, + tension: 0.4, + pointRadius: 0, + }] + }} + options={{ + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { x: { display: false }, y: { display: false } }, + }} + />
-
-
-
-
- +
+ + {/* Card 5: Monthly Revenue */} +
+
+
+
+

Monthly Revenue

+

+ {(stats?.revenueThisMonth || 0) + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ".")} đ +

+
+ +12.5%
-
-

- {stats.totalRevenue - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ".")}{" "} - VND -

-

Total Earning

+
+
+ +
+
+
+ +
+
+
+ + {/* Card 6: Total Instructors */} +
+
+
+
+

Total Instructors

+

{(stats?.instructorStats?.total || 0).toLocaleString()}

+
+ Online: {(stats?.instructorStats?.online || 0).toLocaleString()}
+
+ +
+
+
+
+
{/* Revenue Chart */}
@@ -320,8 +1099,16 @@ const AdminDashboard = () => {

Revenue

- setSelectedYear(Number(e.target.value))} + > + {availableYears.map((year) => ( + + ))}
@@ -333,58 +1120,109 @@ const AdminDashboard = () => {
- {/* Rating Chart */} -
+ {/* NEW: User Growth Chart */} +
-

Overall Course Rating

- setSelectedYear(Number(e.target.value))} + > + {availableYears.map((year) => ( + + ))}
-
-
-
-

- {stats.courseRating?.averageRating.toFixed(1) || "N/A"} -

-

Overall Rating

-
-
- -
+
+ +
+
+
+
+ + {/* NEW: Two Column Charts Grid */} +
+ {/* Category Distribution */} +
+
+
+

Category Distribution

+
+
+
+ {stats?.categoryDistribution?.length > 0 ? ( +
+
-
- {stats.courseRating?.breakdown.map((rating) => ( -
-
- {[...Array(5)].map((_, i) => ( - - ))} -
-
-
-
- - {rating.percentage}% - -
- ))} + ) : ( +
+ No category data available
+ )} +
+
+ + {/* Course Rating Distribution */} +
+
+
+

Course Rating Distribution

+

+ Avg: {stats?.courseRating?.averageRating || '0.00'} ⭐ +

+
+
+
+ {stats?.courseRating?.distribution?.length > 0 ? ( +
+ +
+ ) : ( +
+ No rating data available +
+ )} +
+
+
+ + {/* NEW: Two Column Charts Grid - Course Status & Top Courses */} +
+ {/* Course Status */} +
+
+
+

Course Status

+
+
+
+
+ +
+
+
+ + {/* Top Selling Courses */} +
+
+
+

Top 5 Selling Courses

+
+
+
+
+
-
); }; diff --git a/FrontEnd/src/components/InstructorDashboard/InstructorDashboard.jsx b/FrontEnd/src/components/InstructorDashboard/InstructorDashboard.jsx index 572b20b..41af9d3 100644 --- a/FrontEnd/src/components/InstructorDashboard/InstructorDashboard.jsx +++ b/FrontEnd/src/components/InstructorDashboard/InstructorDashboard.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { Line, Doughnut } from "react-chartjs-2"; import { Chart as ChartJS, @@ -14,6 +14,9 @@ import { } from "chart.js"; import { getInstructorDashboardStats } from "../../services/dashboardService"; import { formatVND, formatVNDCompact } from "../../utils/formatCurrency"; +import jsPDF from "jspdf"; +import html2canvas from "html2canvas"; +import * as XLSX from "xlsx"; import { FaDollarSign, FaUsers, @@ -42,6 +45,8 @@ const InstructorDashboard = () => { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [exporting, setExporting] = useState(false); + const dashboardRef = useRef(null); // Revenue Chart specific state const [revenueData, setRevenueData] = useState(null); @@ -94,6 +99,161 @@ const InstructorDashboard = () => { fetchRevenueData(); }, [fetchRevenueData]); + // Export Functions + const exportToPDF = async () => { + if (!dashboardRef.current) return; + + setExporting(true); + try { + const canvas = await html2canvas(dashboardRef.current, { + scale: 2, + useCORS: true, + logging: false, + }); + + const imgData = canvas.toDataURL('image/png'); + const pdf = new jsPDF({ + orientation: 'portrait', + unit: 'mm', + format: 'a4', + }); + + const imgWidth = 210; + const imgHeight = (canvas.height * imgWidth) / canvas.width; + + pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight); + pdf.save(`Instructor_Dashboard_${selectedYear}_${new Date().toISOString().split('T')[0]}.pdf`); + } catch (error) { + console.error('Error exporting PDF:', error); + alert('Failed to export PDF. Please try again.'); + } finally { + setExporting(false); + } + }; + + const exportToExcel = () => { + if (!stats) return; + + setExporting(true); + try { + const wb = XLSX.utils.book_new(); + const worksheetData = []; + + // Title Section + worksheetData.push(['INSTRUCTOR DASHBOARD REPORT']); + worksheetData.push(['Generated:', new Date().toLocaleString('vi-VN')]); + worksheetData.push(['Year:', selectedYear]); + worksheetData.push([]); + + // Overview Statistics Section + worksheetData.push(['📊 OVERVIEW STATISTICS']); + worksheetData.push(['Metric', 'Value', 'Unit']); + worksheetData.push(['Total Revenue', stats.overview?.totalRevenue || 0, 'VND']); + worksheetData.push(['Period Revenue', stats.overview?.periodRevenue || 0, 'VND']); + worksheetData.push(['Total Students', stats.overview?.totalStudents || 0, 'students']); + worksheetData.push(['New Students This Month', stats.overview?.newStudentsThisMonth || 0, 'students']); + worksheetData.push(['Active Courses', stats.overview?.activeCourses || 0, 'courses']); + worksheetData.push(['Total Courses', stats.overview?.totalCourses || 0, 'courses']); + worksheetData.push(['Average Rating', (stats.overview?.averageRating || 0).toFixed(2), '⭐']); + worksheetData.push(['Total Reviews', stats.overview?.totalReviews || 0, 'reviews']); + worksheetData.push([]); + + // Monthly Revenue Section + if (revenueData?.monthlySales) { + worksheetData.push(['💰 MONTHLY REVENUE BREAKDOWN']); + worksheetData.push(['Month', 'Revenue (VND)', 'Transactions', 'Avg per Transaction']); + const totalRevenue = revenueData.monthlySales.reduce((sum, item) => sum + item.revenue, 0); + const totalTransactions = revenueData.monthlySales.reduce((sum, item) => sum + item.transactions, 0); + + revenueData.monthlySales.forEach((item) => { + const monthName = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][item.month - 1]; + const avgPerTransaction = item.transactions > 0 ? (item.revenue / item.transactions).toFixed(0) : 0; + worksheetData.push([monthName, item.revenue, item.transactions, avgPerTransaction]); + }); + worksheetData.push(['TOTAL', totalRevenue, totalTransactions, totalTransactions > 0 ? (totalRevenue / totalTransactions).toFixed(0) : 0]); + worksheetData.push([]); + } + + // Enrollment Status Section + if (stats.pieCharts?.enrollmentStatus) { + worksheetData.push(['📚 ENROLLMENT STATUS']); + worksheetData.push(['Status', 'Count', 'Percentage']); + const totalEnrollments = stats.pieCharts.enrollmentStatus.reduce((sum, item) => sum + item.count, 0); + stats.pieCharts.enrollmentStatus.forEach((item) => { + const percentage = totalEnrollments > 0 ? ((item.count / totalEnrollments) * 100).toFixed(2) + '%' : '0%'; + worksheetData.push([item.status, item.count, percentage]); + }); + worksheetData.push(['TOTAL', totalEnrollments, '100%']); + worksheetData.push([]); + } + + // Revenue by Category Section + if (stats.pieCharts?.revenueByCategory) { + worksheetData.push(['💼 REVENUE BY CATEGORY']); + worksheetData.push(['Category', 'Revenue (VND)', 'Enrollments', 'Avg Revenue per Enrollment']); + const totalCategoryRevenue = stats.pieCharts.revenueByCategory.reduce((sum, item) => sum + item.revenue, 0); + const totalCategoryEnrollments = stats.pieCharts.revenueByCategory.reduce((sum, item) => sum + item.enrollments, 0); + + stats.pieCharts.revenueByCategory.forEach((item) => { + const avgPerEnrollment = item.enrollments > 0 ? (item.revenue / item.enrollments).toFixed(0) : 0; + worksheetData.push([item.categoryName, item.revenue, item.enrollments, avgPerEnrollment]); + }); + worksheetData.push(['TOTAL', totalCategoryRevenue, totalCategoryEnrollments, totalCategoryEnrollments > 0 ? (totalCategoryRevenue / totalCategoryEnrollments).toFixed(0) : 0]); + worksheetData.push([]); + } + + // Course Completion Section + if (stats.pieCharts?.courseCompletion) { + worksheetData.push(['🎓 COURSE COMPLETION']); + worksheetData.push(['Course Title', 'Completion Rate', 'Completed', 'In Progress', 'Total Enrollments']); + stats.pieCharts.courseCompletion.forEach((item) => { + const completed = Math.round((item.completionRate / 100) * item.totalEnrollments); + const inProgress = item.totalEnrollments - completed; + worksheetData.push([ + item.title, + item.completionRate + '%', + completed, + inProgress, + item.totalEnrollments + ]); + }); + worksheetData.push([]); + } + + // Rating Distribution Section + if (stats.pieCharts?.courseRatingDistribution) { + worksheetData.push(['⭐ RATING DISTRIBUTION']); + worksheetData.push(['Rating', 'Count', 'Percentage']); + const totalRatings = stats.pieCharts.courseRatingDistribution.reduce((sum, item) => sum + item.count, 0); + stats.pieCharts.courseRatingDistribution.forEach((item) => { + const percentage = totalRatings > 0 ? ((item.count / totalRatings) * 100).toFixed(2) + '%' : '0%'; + worksheetData.push([`${item.stars} Stars`, item.count, percentage]); + }); + worksheetData.push(['TOTAL', totalRatings, '100%']); + } + + // Create worksheet + const ws = XLSX.utils.aoa_to_sheet(worksheetData); + + // Set column widths + ws['!cols'] = [ + { wch: 35 }, // Column A + { wch: 22 }, // Column B + { wch: 18 }, // Column C + { wch: 22 }, // Column D + { wch: 20 }, // Column E + ]; + + XLSX.utils.book_append_sheet(wb, ws, 'Dashboard Report'); + XLSX.writeFile(wb, `Instructor_Dashboard_${selectedYear}_${new Date().toISOString().split('T')[0]}.xlsx`); + } catch (error) { + console.error('Error exporting Excel:', error); + alert('Failed to export Excel. Please try again.'); + } finally { + setExporting(false); + } + }; + // Generate year options (current year and 5 years back) const getYearOptions = () => { const currentYear = new Date().getFullYear(); @@ -294,12 +454,60 @@ const InstructorDashboard = () => { } return ( -
+
{/* Header */}

Instructor Dashboard

+
+ + +
{/* 4 Key Metrics Cards */} diff --git a/FrontEnd/src/components/StudentDashboard/StudentDashboard.jsx b/FrontEnd/src/components/StudentDashboard/StudentDashboard.jsx index c5ce0ba..476a626 100644 --- a/FrontEnd/src/components/StudentDashboard/StudentDashboard.jsx +++ b/FrontEnd/src/components/StudentDashboard/StudentDashboard.jsx @@ -1,6 +1,18 @@ -import React, { useState, useEffect } from "react"; -import { FaPlay, FaBook, FaTrophy, FaUsers } from "react-icons/fa"; +import React, { useState, useEffect, useRef } from "react"; +import { FaPlay, FaBook, FaTrophy, FaUsers, FaClock, FaFire } from "react-icons/fa"; import { useLocation, useNavigate } from "react-router-dom"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + LineElement, + PointElement, + Title, + Tooltip, + Legend +} from 'chart.js'; +import { Line, Bar } from 'react-chartjs-2'; import ProfileSection from "../CourseList/ProfileSection"; import { getEnrolledCourses, @@ -12,7 +24,9 @@ import { updateCourseFeedback, getCourseAverageRating, } from "../../services/feedbackService"; +import { getStudentAnalytics } from "../../services/analyticsService"; import "../../assets/StudentDashboard/StudentDashboard.css"; +import "../../assets/StudentDashboard/ModernStudentDashboard.css"; import { useSelector, useDispatch } from "react-redux"; import ReviewModal from "../WatchCourse/ReviewModal"; import SurveyModal from "../LearningPath/SurveyModal"; @@ -23,6 +37,18 @@ import { openSurveyModal, } from "../../store/learningPathSlice"; +// Register Chart.js components +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + LineElement, + PointElement, + Title, + Tooltip, + Legend +); + const StatCard = ({ icon, count, label, color }) => (
{ const { surveyCompleted } = useSelector((state) => state.learningPath); const [enrolledCourses, setEnrolledCourses] = useState([]); const [coursesProgress, setCoursesProgress] = useState([]); + const [analytics, setAnalytics] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(0); + const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()); + const [availableYears, setAvailableYears] = useState([]); const coursesPerPage = 4; const [isReviewOpen, setIsReviewOpen] = useState(false); const [selectedCourseId, setSelectedCourseId] = useState(null); const [feedbackByCourseId, setFeedbackByCourseId] = useState({}); + const dashboardRef = useRef(null); useEffect(() => { fetchDashboardData(); // Check survey completion status dispatch(checkSurveyCompletion()); + + // Initialize available years (last 5 years) + const currentYear = new Date().getFullYear(); + const years = []; + for (let i = 0; i < 5; i++) { + years.push(currentYear - i); + } + setAvailableYears(years); }, [dispatch]); + useEffect(() => { + if (selectedYear) { + fetchDashboardData(); + } + }, [selectedYear]); + useEffect(() => { // Khi đã có danh sách courses, fetch feedback cho từng course if (enrolledCourses.length > 0 && currentUser) { @@ -158,14 +202,20 @@ const StudentDashboard = () => { try { setLoading(true); - // Fetch both enrolled courses and their progress data - const [enrolledResponse, progressResponse] = await Promise.all([ + console.log('Fetching analytics with:', { year: selectedYear }); + + // Fetch enrolled courses, progress, and analytics data + const [enrolledResponse, progressResponse, analyticsResponse] = await Promise.all([ getEnrolledCourses(), getAllCoursesProgress(), + getStudentAnalytics(selectedYear), ]); + console.log('Analytics API response:', analyticsResponse); + setEnrolledCourses(enrolledResponse.data.data); setCoursesProgress(progressResponse.data.data); + setAnalytics(analyticsResponse.data); } catch (err) { setError(err.response?.data?.message || "Failed to fetch dashboard data"); } finally { @@ -217,8 +267,93 @@ const StudentDashboard = () => { label: "Completed Courses", color: "#22c55e", }, + { + id: 4, + icon: , + count: analytics?.learningTime?.total || 0, + label: "Learning Hours", + color: "#f59e0b", + }, + { + id: 5, + icon: , + count: analytics?.streak?.current || 0, + label: "Day Streak", + color: "#ef4444", + }, ]; + // Debug: Log analytics data + console.log('Analytics data:', analytics); + console.log('Weekly activity:', analytics?.weeklyActivity); + if (analytics?.weeklyActivity) { + console.log('First activity item:', analytics.weeklyActivity[0]); + console.log('Activity array length:', analytics.weeklyActivity.length); + console.log('All activity data:', JSON.stringify(analytics.weeklyActivity, null, 2)); + } + + // Weekly activity chart data + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const weeklyChartData = { + labels: analytics?.weeklyActivity?.map(d => monthNames[d.month - 1]) || [], + datasets: [{ + label: 'Courses Completed', + data: analytics?.weeklyActivity?.map(d => d.courses) || [], + backgroundColor: 'rgba(102, 126, 234, 0.5)', + borderColor: 'rgb(102, 126, 234)', + borderWidth: 2, + borderRadius: 6, + }] + }; + + const weeklyChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'top', + }, + tooltip: { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + padding: 12, + titleFont: { size: 14, weight: 'bold' }, + bodyFont: { size: 13 }, + callbacks: { + title: (context) => `Day ${context[0].label}`, + label: (context) => `Courses: ${context.parsed.y}` + } + }, + }, + scales: { + x: { + title: { + display: true, + text: 'Day of Month', + font: { size: 14, weight: 'bold' } + }, + ticks: { + maxRotation: 0, + minRotation: 0, + autoSkip: true, + maxTicksLimit: 15 + } + }, + y: { + beginAtZero: true, + title: { + display: true, + text: 'Courses Completed', + font: { size: 14, weight: 'bold' } + }, + ticks: { + stepSize: 1, + precision: 0 + }, + }, + }, + }; + const handlePrevPage = () => { setCurrentPage((prev) => Math.max(0, prev - 1)); }; @@ -283,7 +418,7 @@ const StudentDashboard = () => { >
-

Dashboard

+

Learning Progress Dashboard

+ {/* Weekly Activity Chart */} + {analytics?.weeklyActivity && ( +
+
+

📅 Monthly Learning Activity

+
+ +
+
+
+ {analytics.weeklyActivity.length > 0 ? ( + + ) : ( +
+ No learning activity data for this period +
+ )} +
+
+ )} +

{`Let's start learning, ${displayName}`}

diff --git a/FrontEnd/src/services/analyticsService.js b/FrontEnd/src/services/analyticsService.js new file mode 100644 index 0000000..993775d --- /dev/null +++ b/FrontEnd/src/services/analyticsService.js @@ -0,0 +1,18 @@ +import apiClient from "./authService"; + +/** + * Get student learning analytics + * @param {number} year - Year to filter analytics (optional, defaults to current year) + */ +export const getStudentAnalytics = async (year) => { + try { + const params = []; + if (year) params.push(`year=${year}`); + const queryString = params.length > 0 ? `?${params.join('&')}` : ''; + const response = await apiClient.get(`/progress/analytics${queryString}`); + return response.data; + } catch (error) { + console.error("Error fetching student analytics:", error); + throw error; + } +}; diff --git a/FrontEnd/src/services/dashboardService.js b/FrontEnd/src/services/dashboardService.js index 9dfd0c2..abf230c 100644 --- a/FrontEnd/src/services/dashboardService.js +++ b/FrontEnd/src/services/dashboardService.js @@ -2,11 +2,12 @@ import apiClient from './authService'; /** * @desc Lấy dữ liệu thống kê cho dashboard (Admin) + * @param {number} year - Năm cần lấy dữ liệu * @returns {Promise} */ -export const getDashboardStats = () => { +export const getDashboardStats = (year) => { // apiClient sẽ tự động thêm token và gọi đến /api/admin/stats - return apiClient.get('/admin/stats'); + return apiClient.get('/admin/stats', { params: { year } }); }; /**