Passed
Pull Request — master (#7150)
by
unknown
08:54
created

build_current_courses_rows()   F

Complexity

Conditions 40
Paths > 20000

Size

Total Lines 317
Code Lines 177

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 40
eloc 177
nc 29381
nop 2
dl 0
loc 317
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
/**
6
 * Report for current courses followed by the user.
7
 */
8
9
use Chamilo\CoreBundle\Enums\ActionIcon;
10
11
$cidReset = true;
12
require_once __DIR__.'/../inc/global.inc.php';
13
$this_section = SECTION_TRACKING;
14
$filename = 'reporting';
15
16
if (!api_is_allowed_to_create_course()) {
17
    api_not_allowed(true);
18
}
19
20
$userId = api_get_user_id();
21
$sessionId = 0; // Only base courses (no session) for this report.
22
23
// ------------------------------------------------------------------------
24
// Common headers/columns (light, can be defined once).
25
// ------------------------------------------------------------------------
26
$headers = [
27
    get_lang('Learning paths'),
28
    get_lang('Trainers'),
29
    get_lang('Courses'),
30
    get_lang('Number of learners'),
31
    get_lang('Number of learners accessing the course'),
32
    get_lang('Percentage of learners accessing the course'),
33
    get_lang('Number of learners who completed all activities (100% progress)'),
34
    get_lang('Percentage of learners who completed all activities (100% progress)'),
35
    get_lang('Average number of activities completed per learner'),
36
    get_lang('Total time spent in the course'),
37
    get_lang('Average time spent per learner in the course'),
38
    get_lang('Number of documents in learning path'),
39
    get_lang('Number of exercises in learning path'),
40
    get_lang('Number of links in learning path'),
41
    get_lang('Number of forums in learning path'),
42
    get_lang('Number of assignments in learning path'),
43
    get_lang('Number of announcements in course'),
44
];
45
46
$columns = [
47
    'lp',
48
    'teachers',
49
    'course_name',
50
    'count_students',
51
    'count_students_accessing',
52
    'count_students_accessing_percentage',
53
    'count_students_complete_all_activities_at_50',
54
    'count_students_complete_all_activities',
55
    'average_percentage_activities_completed_per_student',
56
    'total_time_spent',
57
    'average_time_spent_per_student',
58
    'learnpath_docs',
59
    'learnpath_exercises',
60
    'learnpath_links',
61
    'learnpath_forums',
62
    'learnpath_assignments',
63
    'total_announcements',
64
];
65
66
/**
67
 * Build report rows for current courses.
68
 *
69
 * @param int $userId
70
 * @param int $sessionId
71
 *
72
 * @return array<int, array<string, mixed>>
73
 */
74
function build_current_courses_rows(int $userId, int $sessionId = 0): array
75
{
76
    $rows = [];
77
    $rowIndex = 0;
78
79
    $myCourses = CourseManager::get_course_list_of_user_as_course_admin($userId);
80
81
    if (empty($myCourses)) {
82
        return $rows;
83
    }
84
85
    foreach ($myCourses as $course) {
86
        $courseId = (int) ($course['id'] ?? 0);
87
88
        if (0 === $courseId) {
89
            continue;
90
        }
91
92
        $courseInfo = api_get_course_info_by_id($courseId);
93
        if (empty($courseInfo)) {
94
            // Safety check: if course info is not found, skip.
95
            continue;
96
        }
97
98
        $courseCode = $courseInfo['code'] ?? $course['code'];
99
100
        // Only show open courses.
101
        if (0 === (int) $courseInfo['visibility']) {
102
            continue;
103
        }
104
105
        // ------------------------------------------------------------
106
        // 1) Teachers list (for display).
107
        // ------------------------------------------------------------
108
        $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
109
        $teacherNames = [];
110
111
        if (!empty($teachers)) {
112
            foreach ($teachers as $teacher) {
113
                $teacherNames[] = $teacher['firstname'].' '.$teacher['lastname'];
114
            }
115
        }
116
117
        // ------------------------------------------------------------
118
        // 2) Students list (only "real" students, status = STUDENT).
119
        // ------------------------------------------------------------
120
        $tmpStudents = CourseManager::get_student_list_from_course_code($courseCode, false);
121
        $students = [];
122
123
        foreach ($tmpStudents as $student) {
124
            $userInfo = api_get_user_info($student['user_id']);
125
            if (STUDENT !== (int) $userInfo['status']) {
126
                continue;
127
            }
128
129
            $students[] = (int) $student['user_id'];
130
        }
131
132
        $studentCount = count($students);
133
        $studentsIdList = $studentCount > 0 ? implode(',', $students) : '';
134
135
        // ------------------------------------------------------------
136
        // 3) Course tables and main tables.
137
        // ------------------------------------------------------------
138
        $tLp = Database::get_course_table(TABLE_LP_MAIN);                // c_lp
139
        $tLpItem = Database::get_course_table(TABLE_LP_ITEM);            // c_lp_item
140
        $tNews = Database::get_course_table(TABLE_ANNOUNCEMENT);         // c_announcement
141
        $tLpView = Database::get_course_table(TABLE_LP_VIEW);            // c_lp_view
142
        $tLpItemView = Database::get_course_table(TABLE_LP_ITEM_VIEW);   // c_lp_item_view
143
        $tResourceLink = 'resource_link'; // Main table name for resource links.
144
145
        // ------------------------------------------------------------
146
        // 4) Get LP list for this course (base course, no session).
147
        //    FIRST TRY: through resource_link (original behaviour).
148
        // ------------------------------------------------------------
149
        $sqlLp = "SELECT lp.iid, lp.title
150
                  FROM $tLp lp
151
                  INNER JOIN $tResourceLink li
152
                      ON lp.resource_node_id = li.id
153
                  WHERE li.c_id = $courseId
154
                    AND li.session_id = $sessionId";
155
156
        $resLp = Database::query($sqlLp);
157
158
        if (0 === Database::num_rows($resLp)) {
159
            // SECOND TRY (fallback): LPs that have tracking entries in c_lp_view.
160
            $sqlLpFallback = "SELECT DISTINCT lp.iid, lp.title
161
                              FROM $tLp lp
162
                              INNER JOIN $tLpView v
163
                                  ON v.lp_id = lp.iid
164
                              WHERE v.c_id = $courseId
165
                                AND (v.session_id = $sessionId OR v.session_id IS NULL)";
166
            $resLp = Database::query($sqlLpFallback);
167
        }
168
169
        if (0 === Database::num_rows($resLp)) {
170
            // Course without learning paths: still add one row so the course
171
            // is visible in the report (all stats = 0).
172
            $rows[$rowIndex] = [
173
                'lp' => get_lang('No learning paths'),
174
                'teachers' => !empty($teacherNames) ? implode(', ', $teacherNames) : '',
175
                'course_name' => $course['title'],
176
                'count_students' => $studentCount,
177
                'count_students_accessing' => 0,
178
                'count_students_accessing_percentage' => 0,
179
                // Legacy naming: "_at_50" actually stores the count of 100% completed.
180
                'count_students_complete_all_activities_at_50' => 0,
181
                // Legacy: this stores the percentage of 100% completed.
182
                'count_students_complete_all_activities' => 0,
183
                'average_percentage_activities_completed_per_student' => 0,
184
                'total_time_spent' => 0,
185
                'average_time_spent_per_student' => 0,
186
                'learnpath_docs' => 0,
187
                'learnpath_exercises' => 0,
188
                'learnpath_links' => 0,
189
                'learnpath_forums' => 0,
190
                'learnpath_assignments' => 0,
191
                'total_announcements' => 0,
192
            ];
193
            $rowIndex++;
194
195
            continue;
196
        }
197
198
        // Store LPs in a PHP array to be able to iterate twice.
199
        $lpRows = [];
200
        $lpIds = [];
201
202
        while ($lpRow = Database::fetch_array($resLp)) {
203
            $lpId = (int) $lpRow['iid'];
204
            $lpRows[] = $lpRow;
205
            $lpIds[] = $lpId;
206
        }
207
208
        if (empty($lpRows)) {
209
            continue;
210
        }
211
212
        // ------------------------------------------------------------
213
        // 5) Pre-compute stats per (lp, user) using c_lp_view + c_lp_item_view.
214
        //    This avoids looping over each student in PHP.
215
        // ------------------------------------------------------------
216
        $lpStats = []; // [lp_id => ['count_accessing', 'count_100', 'sum_progress', 'sum_time']]
217
218
        if ($studentCount > 0 && !empty($lpIds) && '' !== $studentsIdList) {
219
            $lpIdList = implode(',', $lpIds);
220
221
            // This query returns ONE row per (lp_id, user_id) with:
222
            // - progress (0..100)
223
            // - total_time (sum of all item views in that LP for that user)
224
            $sqlStats = "
225
                SELECT
226
                    v.lp_id,
227
                    v.user_id,
228
                    COALESCE(MAX(v.progress), 0) AS progress,
229
                    COALESCE(SUM(iv.total_time), 0) AS total_time
230
                FROM $tLpView v
231
                LEFT JOIN $tLpItemView iv
232
                    ON iv.lp_view_id = v.iid
233
                WHERE v.c_id = $courseId
234
                  AND (v.session_id = $sessionId OR v.session_id IS NULL)
235
                  AND v.lp_id IN ($lpIdList)
236
                  AND v.user_id IN ($studentsIdList)
237
                GROUP BY v.lp_id, v.user_id
238
            ";
239
240
            $resStats = Database::query($sqlStats);
241
242
            while ($row = Database::fetch_array($resStats)) {
243
                $lpId = (int) $row['lp_id'];
244
                $progress = (int) $row['progress'];
245
                $totalTime = (int) $row['total_time'];
246
247
                if (!isset($lpStats[$lpId])) {
248
                    $lpStats[$lpId] = [
249
                        'count_accessing' => 0,
250
                        'count_100' => 0,
251
                        'sum_progress' => 0,
252
                        'sum_time' => 0,
253
                    ];
254
                }
255
256
                // Student is considered "accessing" if there is progress or time.
257
                if ($progress > 0 || $totalTime > 0) {
258
                    $lpStats[$lpId]['count_accessing']++;
259
                }
260
261
                // Student completed 100% of the LP.
262
                if (100 === $progress) {
263
                    $lpStats[$lpId]['count_100']++;
264
                }
265
266
                $lpStats[$lpId]['sum_progress'] += $progress;
267
                $lpStats[$lpId]['sum_time'] += $totalTime;
268
            }
269
        }
270
271
        // ------------------------------------------------------------
272
        // 6) Build one row per LP with all stats.
273
        // ------------------------------------------------------------
274
        foreach ($lpRows as $lpRow) {
275
            $lpId = (int) $lpRow['iid'];
276
            $lpTitle = $lpRow['title'];
277
278
            // Base row with default values.
279
            $rows[$rowIndex] = [
280
                'lp' => '<a href="'.
281
                    api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?cidReq='.
282
                    $courseCode.'&amp;action=view&amp;lp_id='.$lpId.
283
                    '" target="_blank">'.
284
                    Security::remove_XSS($lpTitle).
285
                    '</a>',
286
                'teachers' => !empty($teacherNames) ? implode(', ', $teacherNames) : '',
287
                'course_name' => $course['title'],
288
                'count_students' => $studentCount,
289
                'count_students_accessing' => 0,
290
                'count_students_accessing_percentage' => 0,
291
                // Legacy naming kept for compatibility.
292
                'count_students_complete_all_activities_at_50' => 0,
293
                'count_students_complete_all_activities' => 0,
294
                'average_percentage_activities_completed_per_student' => 0,
295
                'total_time_spent' => 0,
296
                'average_time_spent_per_student' => 0,
297
                'learnpath_docs' => 0,
298
                'learnpath_exercises' => 0,
299
                'learnpath_links' => 0,
300
                'learnpath_forums' => 0,
301
                'learnpath_assignments' => 0,
302
                'total_announcements' => 0,
303
            ];
304
305
            // 6.a) Apply pre-computed stats for this LP (if any).
306
            if ($studentCount > 0 && isset($lpStats[$lpId])) {
307
                $stats = $lpStats[$lpId];
308
309
                $countAccessing = (int) $stats['count_accessing'];
310
                $countComplete100 = (int) $stats['count_100'];
311
                $sumProgress = (int) $stats['sum_progress'];
312
                $sumTime = (int) $stats['sum_time'];
313
314
                $rows[$rowIndex]['count_students_accessing'] = $countAccessing;
315
316
                if ($studentCount > 0) {
317
                    $rows[$rowIndex]['count_students_accessing_percentage'] =
318
                        $countAccessing > 0
319
                            ? round($countAccessing / $studentCount * 100, 0)
320
                            : 0;
321
322
                    // Keep legacy semantics:
323
                    // - "_at_50" column stores the absolute number of students at 100%.
324
                    // - "count_students_complete_all_activities" stores the percentage.
325
                    $rows[$rowIndex]['count_students_complete_all_activities_at_50'] =
326
                        $countComplete100;
327
328
                    $rows[$rowIndex]['count_students_complete_all_activities'] =
329
                        $countComplete100 > 0
330
                            ? round($countComplete100 / $studentCount * 100, 0)
331
                            : 0;
332
333
                    // Average progress across all enrolled students (0..100).
334
                    $rows[$rowIndex]['average_percentage_activities_completed_per_student'] =
335
                        $sumProgress > 0
336
                            ? round($sumProgress / $studentCount, 2)
337
                            : 0;
338
                }
339
340
                if ($sumTime > 0) {
341
                    $rows[$rowIndex]['total_time_spent'] = api_time_to_hms($sumTime);
342
                    $rows[$rowIndex]['average_time_spent_per_student'] =
343
                        api_time_to_hms($sumTime / max($studentCount, 1));
344
                }
345
            }
346
347
            // 6.b) Count LP items (documents, quizzes, links, forums, assignments).
348
            $sqlLpItems = "SELECT lpi.item_type
349
                           FROM $tLpItem lpi
350
                           WHERE lpi.lp_id = $lpId
351
                           ORDER BY lpi.item_type";
352
            $resLpItems = Database::query($sqlLpItems);
353
354
            while ($rowItem = Database::fetch_array($resLpItems)) {
355
                switch ($rowItem['item_type']) {
356
                    case 'document':
357
                        $rows[$rowIndex]['learnpath_docs']++;
358
                        break;
359
                    case 'quiz':
360
                        $rows[$rowIndex]['learnpath_exercises']++;
361
                        break;
362
                    case 'link':
363
                        $rows[$rowIndex]['learnpath_links']++;
364
                        break;
365
                    case 'forum':
366
                    case 'thread':
367
                        $rows[$rowIndex]['learnpath_forums']++;
368
                        break;
369
                    case 'student_publication':
370
                        $rows[$rowIndex]['learnpath_assignments']++;
371
                        break;
372
                }
373
            }
374
375
            // 6.c) Announcements count (optional – commented out for perf).
376
            /*
377
            $sqlNews = "SELECT COUNT(n.iid) AS total
378
                        FROM $tNews n
379
                        WHERE n.c_id = $courseId";
380
            $resNews = Database::query($sqlNews);
381
            if ($rowNews = Database::fetch_array($resNews)) {
382
                $rows[$rowIndex]['total_announcements'] = (int) $rowNews['total'];
383
            }
384
            */
385
386
            $rowIndex++;
387
        }
388
    }
389
390
    return $rows;
391
}
392
393
// ------------------------------------------------------------------------
394
// AJAX endpoint: heavy work happens here, returns JSON.
395
// ------------------------------------------------------------------------
396
if (isset($_GET['ajax'])) {
397
    $rows = build_current_courses_rows($userId, $sessionId);
398
399
    $dataRows = [];
400
    foreach ($rows as $rowData) {
401
        $row = [];
402
        foreach ($columns as $key) {
403
            $row[] = $rowData[$key] ?? '';
404
        }
405
        $dataRows[] = $row;
406
    }
407
408
    header('Content-Type: application/json');
409
    echo json_encode(
410
        [
411
            'rows' => $dataRows,
412
            'message' => empty($dataRows) ? get_lang('No data available') : '',
413
        ],
414
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
415
    );
416
    exit;
417
}
418
419
// ------------------------------------------------------------------------
420
// Export to XLS: still uses the heavy builder, but only on demand.
421
// ------------------------------------------------------------------------
422
if (isset($_GET['export'])) {
423
    $rows = build_current_courses_rows($userId, $sessionId);
424
425
    if (!empty($rows[0])) {
426
        $firstRow = [];
427
        foreach ($columns as $key) {
428
            $firstRow[] = $rows[0][$key] ?? '';
429
        }
430
431
        $list = [
432
            0 => $headers,
433
            1 => $firstRow,
434
        ];
435
        Export::arrayToXls($list, $filename);
436
        exit;
437
    }
438
}
439
440
// ------------------------------------------------------------------------
441
// Normal HTML request: renders the layout and delegates heavy work to JS.
442
// ------------------------------------------------------------------------
443
$pageTitle = get_lang('Current courses report');
444
$pageSubtitle = get_lang('This table summarizes learner progress and activity per learning path in your current courses.');
445
446
$interbreadcrumb[] = ['url' => 'index.php', 'name' => get_lang('Reporting')];
447
Display::display_header($pageTitle);
448
449
// ------------------------------------------------------------------------
450
// Toolbar (MySpace main menu + Back / Print / Export actions).
451
// ------------------------------------------------------------------------
452
$actionsLeft = Display::mySpaceMenu('current_courses');
453
454
$actionsRight = Display::url(
455
    Display::getMdiIcon(
456
        ActionIcon::BACK,
457
        'ch-tool-icon',
458
        null,
459
        ICON_SIZE_MEDIUM,
460
        get_lang('Back')
461
    ),
462
    api_get_path(WEB_CODE_PATH).'my_space/index.php'
463
);
464
465
$actionsRight .= Display::url(
466
    Display::getMdiIcon(
467
        ActionIcon::PRINT,
468
        'ch-tool-icon',
469
        null,
470
        ICON_SIZE_MEDIUM,
471
        get_lang('Print')
472
    ),
473
    'javascript: void(0);',
474
    ['onclick' => 'javascript: window.print();']
475
);
476
477
$actionsRight .= Display::url(
478
    Display::getMdiIcon(
479
        ActionIcon::EXPORT_SPREADSHEET,
480
        'ch-tool-icon',
481
        null,
482
        ICON_SIZE_MEDIUM,
483
        get_lang('Current courses report')
484
    ),
485
    api_get_path(WEB_CODE_PATH).'my_space/current_courses.php?export=1'
486
);
487
488
echo Display::toolbarAction('toolbar', [$actionsLeft, $actionsRight]);
489
490
// ------------------------------------------------------------------------
491
// Page heading (title + subtitle).
492
// ------------------------------------------------------------------------
493
echo '<div class="mt-4 mb-3">';
494
echo '  <h2 class="h4 mb-1">'.$pageTitle.'</h2>';
495
echo '  <p class="text-muted mb-0">'.$pageSubtitle.'</p>';
496
echo '</div>';
497
498
// Summary KPI cards.
499
echo '<div id="current-courses-summary" class="row mb-3">';
500
echo '  <div class="col-6 col-md-3 mb-2">';
501
echo '      <div class="card shadow-sm h-100 border-0">';
502
echo '          <div class="card-body text-center p-2">';
503
echo '              <div class="small text-muted">'.get_lang('Number of courses').'</div>';
504
echo '              <div id="current-courses-kpi-courses" class="h4 mb-0">-</div>';
505
echo '          </div>';
506
echo '      </div>';
507
echo '  </div>';
508
echo '  <div class="col-6 col-md-3 mb-2">';
509
echo '      <div class="card shadow-sm h-100 border-0">';
510
echo '          <div class="card-body text-center p-2">';
511
echo '              <div class="small text-muted">'.get_lang('Total learners').'</div>';
512
echo '              <div id="current-courses-kpi-learners" class="h4 mb-0">-</div>';
513
echo '          </div>';
514
echo '      </div>';
515
echo '  </div>';
516
echo '  <div class="col-6 col-md-3 mb-2">';
517
echo '      <div class="card shadow-sm h-100 border-0">';
518
echo '          <div class="card-body text-center p-2">';
519
echo '              <div class="small text-muted">'.get_lang('Average access rate').'</div>';
520
echo '              <div id="current-courses-kpi-access" class="h4 mb-0">-</div>';
521
echo '          </div>';
522
echo '      </div>';
523
echo '  </div>';
524
echo '  <div class="col-6 col-md-3 mb-2">';
525
echo '      <div class="card shadow-sm h-100 border-0">';
526
echo '          <div class="card-body text-center p-2">';
527
echo '              <div class="small text-muted">'.get_lang('Average completion rate').'</div>';
528
echo '              <div id="current-courses-kpi-completion" class="h4 mb-0">-</div>';
529
echo '          </div>';
530
echo '      </div>';
531
echo '  </div>';
532
echo '</div>';
533
534
// Table with a centered loading row.
535
$colspan = count($headers);
536
537
echo '<div class="table-responsive">';
538
echo '<table id="current-courses-table" class="table table-hover table-striped data_table">';
539
echo '<thead><tr>';
540
foreach ($headers as $header) {
541
    echo '<th>'.$header.'</th>';
542
}
543
echo '</tr></thead>';
544
echo '<tbody id="current-courses-tbody">';
545
echo '<tr id="current-courses-loading-row">';
546
echo '<td colspan="'.$colspan.'" class="text-center">';
547
echo '<div class="spinner-border" role="status" aria-hidden="true"></div>';
548
echo '<span class="ms-2">'.get_lang('Loading').'...</span>';
549
echo '</td>';
550
echo '</tr>';
551
echo '</tbody>';
552
echo '</table>';
553
echo '</div>';
554
555
// Simple JS loader that calls the AJAX endpoint, fills the table and updates KPIs.
556
$ajaxUrl = api_get_path(WEB_CODE_PATH).'my_space/current_courses.php?ajax=1';
557
$noDataText = addslashes(get_lang('No data available'));
558
$errorText = addslashes(get_lang('Error while loading data'));
559
560
echo '<script>
561
document.addEventListener("DOMContentLoaded", function () {
562
    var table = document.getElementById("current-courses-table");
563
    var tbody = document.getElementById("current-courses-tbody");
564
    var loadingRow = document.getElementById("current-courses-loading-row");
565
566
    var kpiCourses = document.getElementById("current-courses-kpi-courses");
567
    var kpiLearners = document.getElementById("current-courses-kpi-learners");
568
    var kpiAccess = document.getElementById("current-courses-kpi-access");
569
    var kpiCompletion = document.getElementById("current-courses-kpi-completion");
570
571
    if (!table || !tbody || !loadingRow) {
572
        return;
573
    }
574
575
    // Helper: update loading row text and style.
576
    function setLoadingText(text, isError) {
577
        var td = loadingRow.querySelector("td");
578
        if (!td) {
579
            return;
580
        }
581
        td.textContent = text;
582
        td.classList.toggle("text-danger", !!isError);
583
    }
584
585
    // Helper: update KPI cards based on loaded rows.
586
    function updateKpis(rows) {
587
        if (!rows || !rows.length) {
588
            if (kpiCourses) { kpiCourses.textContent = "0"; }
589
            if (kpiLearners) { kpiLearners.textContent = "0"; }
590
            if (kpiAccess) { kpiAccess.textContent = "0%"; }
591
            if (kpiCompletion) { kpiCompletion.textContent = "0%"; }
592
            return;
593
        }
594
595
        var indexCourseName = 2; // course_name
596
        var indexStudents = 3;   // count_students
597
        var indexAccessPct = 5;  // count_students_accessing_percentage
598
        var indexCompletePct = 7; // count_students_complete_all_activities
599
600
        var courseSet = {};
601
        var totalLearners = 0;
602
        var sumAccess = 0;
603
        var sumCompletion = 0;
604
        var countRows = 0;
605
606
        rows.forEach(function (row) {
607
            var courseName = row[indexCourseName] || "";
608
            if (courseName) {
609
                courseSet[courseName] = true;
610
            }
611
612
            var learners = parseFloat(row[indexStudents]) || 0;
613
            totalLearners += learners;
614
615
            var accessPct = parseFloat(row[indexAccessPct]) || 0;
616
            var completionPct = parseFloat(row[indexCompletePct]) || 0;
617
618
            sumAccess += accessPct;
619
            sumCompletion += completionPct;
620
            countRows++;
621
        });
622
623
        var totalCourses = Object.keys(courseSet).length;
624
        var avgAccess = countRows ? (sumAccess / countRows) : 0;
625
        var avgCompletion = countRows ? (sumCompletion / countRows) : 0;
626
627
        if (kpiCourses) {
628
            kpiCourses.textContent = String(totalCourses);
629
        }
630
        if (kpiLearners) {
631
            kpiLearners.textContent = String(totalLearners);
632
        }
633
        if (kpiAccess) {
634
            kpiAccess.textContent = avgAccess.toFixed(1) + "%";
635
        }
636
        if (kpiCompletion) {
637
            kpiCompletion.textContent = avgCompletion.toFixed(1) + "%";
638
        }
639
    }
640
641
    fetch("'.$ajaxUrl.'")
642
        .then(function (response) {
643
            if (!response.ok) {
644
                throw new Error("HTTP " + response.status);
645
            }
646
            return response.json();
647
        })
648
        .then(function (data) {
649
            if (!data || !Array.isArray(data.rows) || data.rows.length === 0) {
650
                setLoadingText(data && data.message ? data.message : "'.$noDataText.'", false);
651
                updateKpis([]);
652
                return;
653
            }
654
655
            // Remove loading row before appending data rows.
656
            if (loadingRow && loadingRow.parentNode) {
657
                loadingRow.parentNode.removeChild(loadingRow);
658
            }
659
660
            data.rows.forEach(function (row) {
661
                var tr = document.createElement("tr");
662
                row.forEach(function (cell) {
663
                    var td = document.createElement("td");
664
                    td.innerHTML = cell;
665
                    tr.appendChild(td);
666
                });
667
                tbody.appendChild(tr);
668
            });
669
670
            updateKpis(data.rows);
671
        })
672
        .catch(function (e) {
673
            setLoadingText("'.$errorText.'", true);
674
            updateKpis([]);
675
            if (window.console && console.error) {
676
                console.error(e);
677
            }
678
        });
679
});
680
</script>';
681
682
Display::display_footer();
683