Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/css/udoit4-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,7 @@ input[type="color"] {
border-radius: 8px;
padding: .75rem 1rem;
border: 1px solid var(--border-color);
box-sizing: border-box;
}

.separator {
Expand Down
16 changes: 13 additions & 3 deletions assets/js/Components/Admin/AdminApp.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'
import AdminHeader from './AdminHeader'
import AdminDashboard from './AdminDashboard'
import CoursesPage from './CoursesPage'
import ReportsPage from './ReportsPage'
import UsersPage from './UsersPage'
Expand Down Expand Up @@ -33,7 +34,7 @@ export default function AdminApp(initialData) {
const [filters, setFilters] = useState({...initialFilters})
const [searchTerm, setSearchTerm] = useState('')
const [accountData, setAccountData] = useState([])
const [navigation, setNavigation] = useState('courses')
const [navigation, setNavigation] = useState('dashboard')
const [modal, setModal] = useState(null)
const [loadingCourses, setLoadingCourses] = useState(true)
const [trayOpen, setTrayOpen] = useState(false)
Expand Down Expand Up @@ -129,7 +130,7 @@ export default function AdminApp(initialData) {
/>

<main role="main" className="pt-2">
{ (navigation !== 'reports') &&
{ (navigation !== 'reports' && navigation !== 'dashboard') &&
<AdminFilters
t={t}
settings={settings}
Expand All @@ -153,8 +154,17 @@ export default function AdminApp(initialData) {
}

{ !loadingCourses && (
<div className="mt-3">
<div className="scrollable">

{('dashboard' === navigation) &&
<AdminDashboard
t={t}
settings={settings}
courses={courses}
handleNavigation={handleNavigation}
addMessage={addMessage}
/>
}
{('courses' === navigation) &&
<CoursesPage
t={t}
Expand Down
250 changes: 250 additions & 0 deletions assets/js/Components/Admin/AdminDashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import React, { useState, useEffect } from 'react'
import ProgressCircle from '../Widgets/ProgressCircle'
import '../HomePage.css'

export default function AdminDashboard({
t,
settings,
courses
}) {
const [dashboardStats, setDashboardStats] = useState({
loading: true,
totalCourses: 0,
scannedCourses: 0,
totalInstructors: 0,
uniqueInstructorsUsingUdoit: 0,
totalErrors: 0,
totalSuggestions: 0,
totalFixed: 0,
totalResolved: 0,
totalFilesReviewed: 0,
recentScans: 0,
accountBreakdown: {},
oldestScan: null,
newestScan: null
})

const progressMeterRadius = () => {
switch (settings?.user?.roles?.font_size) {
case 'font-small':
return 40;
case 'font-normal':
return 45;
case 'font-large':
return 50;
case 'font-xlarge':
return 60;
default:
return 40;
}
}

useEffect(() => {
calculateStats()
}, [courses])

const calculateStats = () => {
if (!courses || Object.keys(courses).length === 0) {
setDashboardStats(prev => ({ ...prev, loading: false }))
return
}

const stats = {
loading: false,
totalCourses: 0,
scannedCourses: 0,
totalInstructors: 0,
uniqueInstructorsUsingUdoit: 0,
totalErrors: 0,
totalSuggestions: 0,
totalFixed: 0,
totalResolved: 0,
totalFilesReviewed: 0,
recentScans: 0,
accountBreakdown: {},
oldestScan: null,
newestScan: null
}

const allInstructors = new Set()
const udoitInstructors = new Set()
const accountStats = {}
const scanDates = []
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)

Object.keys(courses).forEach(key => {
const course = courses[key]
stats.totalCourses++

// Track instructors
if (Array.isArray(course.instructors)) {
course.instructors.forEach(instructor => {
allInstructors.add(instructor)
if (course.hasReport) {
udoitInstructors.add(instructor)
}
})
}

// Account breakdown
const accountName = course.accountName || 'Unknown'
if (!accountStats[accountName]) {
accountStats[accountName] = { total: 0, scanned: 0, errors: 0 }
}
accountStats[accountName].total++

if (course.hasReport) {
stats.scannedCourses++
accountStats[accountName].scanned++

// Aggregate report data
if (course.report) {
stats.totalErrors += parseInt(course.report.errors) || 0
stats.totalSuggestions += parseInt(course.report.suggestions) || 0
stats.totalFixed += parseInt(course.report.contentFixed) || 0
stats.totalResolved += parseInt(course.report.contentResolved) || 0
stats.totalFilesReviewed += parseInt(course.report.filesReviewed) || 0
accountStats[accountName].errors += parseInt(course.report.errors) || 0
}

// Track scan dates
if (course.lastUpdated && course.lastUpdated !== '---') {
const scanDate = new Date(course.lastUpdated)
scanDates.push(scanDate)

if (scanDate > thirtyDaysAgo) {
stats.recentScans++
}
}
}
})

stats.totalInstructors = allInstructors.size
stats.uniqueInstructorsUsingUdoit = udoitInstructors.size
stats.accountBreakdown = accountStats
stats.accountNumber = Object.keys(accountStats).length

// Find oldest and newest scans
if (scanDates.length > 0) {
scanDates.sort((a, b) => a - b)
stats.oldestScan = scanDates[0]
stats.newestScan = scanDates[scanDates.length - 1]
}

setDashboardStats(stats)
}

if (dashboardStats.loading) {
return <div className="p-3">Loading dashboard...</div>
}

const scanPercentage = dashboardStats.totalCourses > 0
? ((dashboardStats.scannedCourses / dashboardStats.totalCourses) * 100)
: 0

const instructorAdoption = dashboardStats.totalInstructors > 0
? ((dashboardStats.uniqueInstructorsUsingUdoit / dashboardStats.totalInstructors) * 100)
: 0

return (
<>
<div className="flex-row gap-3 w-100 scrollable">

<div className="flex-column gap-3 w-100">
<section className="callout-container">
<div className="flex-column">
<div className="flex-row justify-content-evenly gap-3">
<div className="flex-column">
<h2 className="callout-heading align-self-center text-center mt-1">Course Usage</h2>
<div className="svg-container align-self-center">
<ProgressCircle
percent={scanPercentage}
radius={progressMeterRadius()}
circlePortion={75}
strokeWidth={10}/>
<div className="progress-text-container flex-column justify-content-center">
<div className="progress-text">{scanPercentage.toFixed(0)}%</div>
</div>
</div>
<div className="flex-row align-self-center count-summary">{dashboardStats.scannedCourses} of {dashboardStats.totalCourses} Courses Scanned</div>
</div>

<div className="flex-column">
<h2 className="callout-heading align-self-center text-center mt-1">Instructor Usage</h2>
<div className="svg-container align-self-center">
<ProgressCircle
percent={instructorAdoption}
radius={progressMeterRadius()}
circlePortion={75}
strokeWidth={10}/>
<div className="progress-text-container flex-column justify-content-center">
<div className="progress-text">{instructorAdoption.toFixed(0)}%</div>
</div>
</div>
<div className="flex-row align-self-center count-summary">{dashboardStats.uniqueInstructorsUsingUdoit} of {dashboardStats.totalInstructors} Instructors Using UDOIT</div>
</div>
</div>

<div className="separator mt-3 mb-3"></div>

<div className="flex-column w-100 mb-2 gap-1">
<p className="m-0"><span className="progress-text me-3">{dashboardStats.recentScans}</span> Courses Scanned in Last 30 Days</p>
{dashboardStats.oldestScan && (<p className="m-0"><span className="progress-text me-3">{dashboardStats.oldestScan.toLocaleDateString()}</span> Oldest Scan</p>)}
{dashboardStats.newestScan && (<p className="m-0"><span className="progress-text me-3">{dashboardStats.newestScan.toLocaleDateString()}</span> Most Recent Scan</p>)}
</div>
</div>

{/* <h2>Course Statistics</h2>
<p>Total Courses: {dashboardStats.totalCourses}</p>
<p>Scanned Courses: {dashboardStats.scannedCourses}</p>
<p>Unscanned Courses: {dashboardStats.totalCourses - dashboardStats.scannedCourses}</p>
<p>Scan Adoption Rate: {scanPercentage}%</p>
<p>Courses Scanned in Last 30 Days: {dashboardStats.recentScans}</p>

<h2>Instructor Statistics</h2>
<p>Total Instructors: {dashboardStats.totalInstructors}</p>
<p>Instructors Using UDOIT: {dashboardStats.uniqueInstructorsUsingUdoit}</p>
<p>Instructor Adoption Rate: {instructorAdoption}%</p>

<h2>Accessibility Statistics</h2>
<p>Total Accessibility Issues Found: {dashboardStats.totalErrors}</p>
<p>Total Suggestions Made: {dashboardStats.totalSuggestions}</p>
<p>Total Items Fixed: {dashboardStats.totalFixed}</p>
<p>Total Items Resolved: {dashboardStats.totalResolved}</p>
<p>Total Files Reviewed: {dashboardStats.totalFilesReviewed}</p> */}

</section>

<section className="callout-container">
<p>Total Accessibility Issues Found: {dashboardStats.totalErrors}</p>
<p>Total Suggestions Made: {dashboardStats.totalSuggestions}</p>
<p>Total Items Fixed: {dashboardStats.totalFixed}</p>
<p>Total Items Resolved: {dashboardStats.totalResolved}</p>
<p>Total Files Reviewed: {dashboardStats.totalFilesReviewed}</p>
{dashboardStats.scannedCourses > 0 && (
<>
<p>Average Issues per Scanned Course: {(dashboardStats.totalErrors / dashboardStats.scannedCourses).toFixed(1)}</p>
<p>Average Files Reviewed per Course: {(dashboardStats.totalFilesReviewed / dashboardStats.scannedCourses).toFixed(1)}</p>
</>
)}
</section>
</div>

{dashboardStats.accountNumber > 1 && (
<section className="callout-container">
<h2 className="callout-heading mt-1">Department/Account Breakdown</h2>
{Object.entries(dashboardStats.accountBreakdown).map(([account, stats]) => (
<div key={account}>
<p><strong>{account}:</strong></p>
<p style={{ marginLeft: '20px' }}>Total Courses: {stats.total}</p>
<p style={{ marginLeft: '20px' }}>Scanned: {stats.scanned} ({stats.total > 0 ? ((stats.scanned / stats.total) * 100).toFixed(1) : 0}%)</p>
<p style={{ marginLeft: '20px' }}>Total Issues: {stats.errors}</p>
</div>
))}
</section>
)}
</div>
</>
)
}
19 changes: 18 additions & 1 deletion assets/js/Components/Admin/AdminHeader.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import UDOITLogo from '../../../mediaAssets/udoit-logo.svg'
import HomeIcon from '../Icons/HomeIcon'
import ContentAssignmentIcon from '../Icons/ContentAssignmentIcon'
import UserIcon from '../Icons/UserIcon'
import '../Header.css'
Expand All @@ -13,7 +14,7 @@ export default function AdminHeader({
return (
<header role="banner">
<nav aria-label={t('menu.nav.label')}>
<div className="flex-row justify-content-start gap-2" onClick={() => handleNavigation('courses')}>
<div className="flex-row justify-content-start gap-2" onClick={() => handleNavigation('dashboard')}>
<div className="flex-column justify-content-center" style={{ width: 'min-content' }}>
<img alt={t('alt.UDOIT')} src={UDOITLogo}></img>
</div>
Expand All @@ -23,6 +24,22 @@ export default function AdminHeader({
</div>
<div>
<ul>
<li
className={`flex-row ${navigation === 'dashboard' ? ' active-link' : ''}`}
onClick={() => handleNavigation('dashboard')}
onKeyDown={(e) => {
if(e.key === 'Enter' || e.key === ' ') {
handleNavigation('dashboard')
}
}}
tabIndex="0">
<div className='flex-column justify-content-center'>
<HomeIcon className='icon-md pr-1'/>
</div>
<div className='flex-column justify-content-center'>
Dashboard
</div>
</li>
<li
className={`flex-row ${navigation === 'courses' ? ' active-link' : ''}`}
onClick={() => handleNavigation('courses')}
Expand Down
42 changes: 21 additions & 21 deletions assets/js/Components/HomePage.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,31 +71,31 @@
width: 30%;
min-width: 11em;
flex-shrink: 0;
}

.svg-container {
position: relative;
width: fit-content;

.progress-text-container {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
.svg-container {
position: relative;
width: fit-content;

.progress-text {
width: 100%;
text-align: center;
font-weight: 600;
font-size: 1.25em;
margin-top: -.5em;
}
}
.progress-text-container {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
}

.count-summary {
margin-top: -.5rem;
}
.progress-text {
width: 100%;
text-align: center;
font-weight: 600;
font-size: 1.25em;
margin-top: -.5em;
}

.count-summary {
margin-top: -.5rem;
}

.step-summary {
Expand Down