AVP-86 Below 60 score
This commit is contained in:
@@ -499,6 +499,7 @@ $routes->get('grading/project/(:num)', 'View\ProjectController::showProjectMngt/
|
||||
$routes->post('grading/updateProject', 'View\ProjectController::updateProject');
|
||||
|
||||
$routes->get('grading/below-60', 'View\GradingController::belowSixty', ['filter' => 'auth:read']);
|
||||
$routes->get('grading/below-60/email/edit', 'View\GradingController::editBelowSixtyEmail', ['filter' => 'auth:read']);
|
||||
$routes->post('grading/below-60/email', 'View\GradingController::sendBelowSixtyEmail', ['filter' => 'auth:read']);
|
||||
$routes->post('grading/below-60/status', 'View\GradingController::updateBelowSixtyStatus', ['filter' => 'auth:read']);
|
||||
$routes->get('grading/below-60/schedule', 'View\GradingController::scheduleBelowSixty', ['filter' => 'auth:read']);
|
||||
|
||||
@@ -1087,11 +1087,11 @@ class GradingController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendBelowSixtyEmail()
|
||||
public function editBelowSixtyEmail()
|
||||
{
|
||||
$studentId = (int)$this->request->getPost('student_id');
|
||||
$semester = trim((string)$this->request->getPost('semester'));
|
||||
$schoolYear = trim((string)$this->request->getPost('school_year'));
|
||||
$studentId = (int)$this->request->getGet('student_id');
|
||||
$semester = trim((string)$this->request->getGet('semester'));
|
||||
$schoolYear = trim((string)$this->request->getGet('school_year'));
|
||||
|
||||
if ($studentId <= 0 || $semester === '' || $schoolYear === '') {
|
||||
return redirect()->back()->with('error', 'Missing student or term.');
|
||||
@@ -1103,6 +1103,65 @@ class GradingController extends Controller
|
||||
}
|
||||
|
||||
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
|
||||
$subject = $this->buildBelowSixtySubject($studentName, $semester, $schoolYear);
|
||||
$parentName = $this->fetchBelowSixtyParentName($studentId);
|
||||
|
||||
$scores = [
|
||||
'homework_avg' => $row['homework_avg'] ?? null,
|
||||
'project_avg' => $row['project_avg'] ?? null,
|
||||
'participation_score' => $row['participation_score'] ?? null,
|
||||
'test_avg' => $row['test_avg'] ?? null,
|
||||
'ptap_score' => $row['ptap_score'] ?? null,
|
||||
'attendance_score' => $row['attendance_score'] ?? null,
|
||||
'midterm_exam_score' => $row['midterm_exam_score'] ?? null,
|
||||
'semester_score' => $row['semester_score'] ?? null,
|
||||
];
|
||||
|
||||
$emailData = [
|
||||
'title' => $subject,
|
||||
'parent_name' => $parentName,
|
||||
'student_name' => $studentName !== '' ? $studentName : 'your student',
|
||||
'class_section_name' => $row['class_section_name'] ?? '',
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'scores' => $scores,
|
||||
'comment' => $row['comment'] ?? '',
|
||||
'sent_at' => utc_now(),
|
||||
];
|
||||
|
||||
$html = view('emails/below_sixty_performance', $emailData, ['saveData' => true]);
|
||||
|
||||
return view('grading/below_sixty_email_editor', [
|
||||
'studentId' => $studentId,
|
||||
'studentName' => $studentName,
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'subject' => $subject,
|
||||
'html' => $html,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendBelowSixtyEmail()
|
||||
{
|
||||
$studentId = (int)$this->request->getPost('student_id');
|
||||
$semester = trim((string)$this->request->getPost('semester'));
|
||||
$schoolYear = trim((string)$this->request->getPost('school_year'));
|
||||
$subjectInput = trim((string)$this->request->getPost('subject'));
|
||||
$htmlInput = (string)($this->request->getPost('html') ?? '');
|
||||
|
||||
if ($studentId <= 0 || $semester === '' || $schoolYear === '') {
|
||||
return redirect()->back()->with('error', 'Missing student or term.');
|
||||
}
|
||||
|
||||
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
|
||||
if (empty($row)) {
|
||||
return redirect()->back()->with('error', 'Student record not found for the selected term.');
|
||||
}
|
||||
|
||||
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
|
||||
$subject = $subjectInput !== ''
|
||||
? $subjectInput
|
||||
: $this->buildBelowSixtySubject($studentName, $semester, $schoolYear);
|
||||
$scores = [
|
||||
'homework_avg' => $row['homework_avg'] ?? null,
|
||||
'project_avg' => $row['project_avg'] ?? null,
|
||||
@@ -1122,8 +1181,13 @@ class GradingController extends Controller
|
||||
'school_year' => $schoolYear,
|
||||
'scores' => $scores,
|
||||
'comment' => $row['comment'] ?? '',
|
||||
'subject' => $subject,
|
||||
];
|
||||
|
||||
if (trim($htmlInput) !== '') {
|
||||
$payload['html'] = $htmlInput;
|
||||
}
|
||||
|
||||
Events::trigger('below60.email', $payload);
|
||||
|
||||
return redirect()->back()->with('status', 'Email sent to parent(s).');
|
||||
@@ -1799,6 +1863,43 @@ class GradingController extends Controller
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function fetchBelowSixtyParentName(int $studentId): string
|
||||
{
|
||||
$parentName = 'Parent/Guardian';
|
||||
try {
|
||||
$rows = $this->db->query(
|
||||
"SELECT u.firstname, u.lastname
|
||||
FROM family_students fs
|
||||
JOIN family_guardians fg ON fg.family_id = fs.family_id
|
||||
JOIN users u ON u.id = fg.user_id
|
||||
WHERE fs.student_id = ?
|
||||
ORDER BY fg.is_primary DESC, u.lastname, u.firstname
|
||||
LIMIT 1",
|
||||
[$studentId]
|
||||
)->getResultArray();
|
||||
if (!empty($rows[0])) {
|
||||
$candidate = trim((string)($rows[0]['firstname'] ?? '') . ' ' . (string)($rows[0]['lastname'] ?? ''));
|
||||
if ($candidate !== '') {
|
||||
$parentName = $candidate;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
return $parentName;
|
||||
}
|
||||
|
||||
private function buildBelowSixtySubject(string $studentName, string $semester, string $schoolYear): string
|
||||
{
|
||||
$subject = 'Student Performance Alert';
|
||||
if ($studentName !== '') {
|
||||
$subject .= ' — ' . $studentName;
|
||||
}
|
||||
if ($semester !== '' || $schoolYear !== '') {
|
||||
$subject .= ' (' . trim($semester . ' ' . $schoolYear) . ')';
|
||||
}
|
||||
return $subject;
|
||||
}
|
||||
|
||||
private function fetchBelowSixtyMeetingContext(int $studentId, string $schoolYear, string $semester): array
|
||||
{
|
||||
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddBelowSixtyNavItem extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
if (!$db->tableExists('nav_items')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parentColumn = null;
|
||||
if ($db->fieldExists('menu_parent_id', 'nav_items')) {
|
||||
$parentColumn = 'menu_parent_id';
|
||||
} elseif ($db->fieldExists('parent_id', 'nav_items')) {
|
||||
$parentColumn = 'parent_id';
|
||||
}
|
||||
|
||||
if ($parentColumn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$navBuilder = $db->table('nav_items');
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
$communication = $navBuilder
|
||||
->select('id')
|
||||
->where('label', 'Communication')
|
||||
->where($parentColumn, null)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($communication) {
|
||||
$communicationId = (int) $communication['id'];
|
||||
} else {
|
||||
$navBuilder->insert([
|
||||
$parentColumn => null,
|
||||
'label' => 'Communication',
|
||||
'url' => null,
|
||||
'sort_order' => 80,
|
||||
'is_enabled' => 1,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$communicationId = (int) $db->insertID();
|
||||
}
|
||||
|
||||
$belowSixtyUrl = 'grading/below-60';
|
||||
$existing = $navBuilder->select('id')
|
||||
->where('url', $belowSixtyUrl)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$existing) {
|
||||
$navBuilder->insert([
|
||||
$parentColumn => $communicationId ?: null,
|
||||
'label' => 'Below 60 Summary',
|
||||
'url' => $belowSixtyUrl,
|
||||
'sort_order' => 4,
|
||||
'is_enabled' => 1,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$belowSixtyId = (int) $db->insertID();
|
||||
} else {
|
||||
$belowSixtyId = (int) $existing['id'];
|
||||
}
|
||||
|
||||
if (!$db->tableExists('role_nav_items') || !$db->tableExists('roles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($belowSixtyId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$roleRows = $db->table('roles')
|
||||
->select('id, name')
|
||||
->whereIn('name', ['administrator', 'principal', 'vice_principal', 'admin'])
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$roleMap = $db->table('role_nav_items');
|
||||
foreach ($roleRows as $role) {
|
||||
$roleId = (int) ($role['id'] ?? 0);
|
||||
if ($roleId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = $roleMap
|
||||
->where('role_id', $roleId)
|
||||
->where('nav_item_id', $belowSixtyId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$roleMap->insert([
|
||||
'role_id' => $roleId,
|
||||
'nav_item_id' => $belowSixtyId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
if (!$db->tableExists('nav_items')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$belowSixtyUrl = 'grading/below-60';
|
||||
$row = $db->table('nav_items')
|
||||
->select('id')
|
||||
->where('url', $belowSixtyUrl)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($row && $db->tableExists('role_nav_items')) {
|
||||
$db->table('role_nav_items')
|
||||
->where('nav_item_id', (int) $row['id'])
|
||||
->delete();
|
||||
}
|
||||
|
||||
$db->table('nav_items')
|
||||
->where('url', $belowSixtyUrl)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,10 @@ class BelowSixtyEmailListener
|
||||
'sent_at' => utc_now(),
|
||||
];
|
||||
|
||||
$html = view('emails/below_sixty_performance', $emailData, ['saveData' => true]);
|
||||
$html = trim((string)($payload['html'] ?? ''));
|
||||
if ($html === '') {
|
||||
$html = view('emails/below_sixty_performance', $emailData, ['saveData' => true]);
|
||||
}
|
||||
|
||||
$okAny = false;
|
||||
foreach ($emails as $to) {
|
||||
|
||||
@@ -94,17 +94,14 @@
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<form method="post" action="<?= site_url('grading/below-60/email') ?>" class="d-inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="student_id" value="<?= esc((string)($row['student_id'] ?? '')) ?>">
|
||||
<input type="hidden" name="semester" value="<?= esc((string)($semester ?? '')) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc((string)($schoolYear ?? '')) ?>">
|
||||
<button type="submit"
|
||||
class="btn btn-sm <?= $isClosed ? 'btn-secondary' : 'btn-outline-primary' ?>"
|
||||
<?= $isClosed ? 'disabled' : '' ?>>
|
||||
<?php if ($isClosed): ?>
|
||||
<button type="button" class="btn btn-sm btn-secondary" disabled>Send Email</button>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-sm btn-outline-primary"
|
||||
href="<?= site_url('grading/below-60/email/edit?student_id=' . (int)($row['student_id'] ?? 0) . '&semester=' . rawurlencode((string)($semester ?? '')) . '&school_year=' . rawurlencode((string)($schoolYear ?? ''))) ?>">
|
||||
Send Email
|
||||
</button>
|
||||
</form>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php if ($isClosed): ?>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="wrapper">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h2 class="h4 mb-1">Edit Email</h2>
|
||||
<div class="text-muted">
|
||||
<?= esc($studentName !== '' ? $studentName : 'Student') ?> • <?= esc($semester ?? '') ?> • <?= esc($schoolYear ?? '') ?>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary btn-sm"
|
||||
href="<?= site_url('grading/below-60?semester=' . rawurlencode((string)($semester ?? '')) . '&school_year=' . rawurlencode((string)($schoolYear ?? ''))) ?>">
|
||||
Back to Below 60
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?= site_url('grading/below-60/email') ?>" class="card shadow-sm">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="student_id" value="<?= esc((string)($studentId ?? '')) ?>">
|
||||
<input type="hidden" name="semester" value="<?= esc((string)($semester ?? '')) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc((string)($schoolYear ?? '')) ?>">
|
||||
<input type="hidden" name="html" id="below60_html" value="<?= htmlspecialchars((string)($html ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">
|
||||
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="below60Subject">Subject</label>
|
||||
<input type="text" class="form-control" id="below60Subject" name="subject" value="<?= esc((string)($subject ?? '')) ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="below60Editor">Body (Rich Text)</label>
|
||||
<textarea class="form-control" id="below60Editor" rows="14"><?= (string)($html ?? '') ?></textarea>
|
||||
<div class="form-text">Use the toolbar to format the message before sending.</div>
|
||||
<noscript>
|
||||
<div class="alert alert-warning mt-2">
|
||||
JavaScript is disabled. The message will be sent from the hidden <code>html</code> field.
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a class="btn btn-outline-secondary"
|
||||
href="<?= site_url('grading/below-60?semester=' . rawurlencode((string)($semester ?? '')) . '&school_year=' . rawurlencode((string)($schoolYear ?? ''))) ?>">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">Send Email</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script src="<?= base_url('assets/tinymce/tinymce.min.js') ?>"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.querySelector('form[action$="grading/below-60/email"]');
|
||||
const hiddenHtml = document.getElementById('below60_html');
|
||||
|
||||
if (!window.tinymce) return;
|
||||
|
||||
tinymce.init({
|
||||
selector: '#below60Editor',
|
||||
base_url: '<?= base_url('assets/tinymce') ?>',
|
||||
suffix: '.min',
|
||||
license_key: 'gpl',
|
||||
height: 420,
|
||||
menubar: true,
|
||||
branding: false,
|
||||
promotion: false,
|
||||
plugins: 'advlist autolink lists link image charmap preview anchor ' +
|
||||
'searchreplace visualblocks code fullscreen insertdatetime media table ' +
|
||||
'help wordcount emoticons codesample',
|
||||
toolbar: 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | ' +
|
||||
'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'link image media table | emoticons codesample | removeformat | preview code',
|
||||
convert_urls: false,
|
||||
paste_data_images: true,
|
||||
image_caption: true,
|
||||
content_style: 'body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size: 14px; }',
|
||||
setup(editor) {
|
||||
editor.on('keyup change undo redo SetContent', function () {
|
||||
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
|
||||
});
|
||||
if (form) {
|
||||
form.addEventListener('submit', function () {
|
||||
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
+1
-1
@@ -1 +1 @@
|
||||
a:3:{s:4:"time";i:1772133029;s:3:"ttl";i:300;s:4:"data";a:3:{i:0;a:12:{s:2:"id";s:2:"83";s:14:"menu_parent_id";N;s:5:"label";s:15:"TimeOff Request";s:3:"url";s:22:"/administrator/absence";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-11-01 20:58:55";s:10:"updated_at";s:19:"2025-11-01 20:58:55";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"85";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:17:"Print for teacher";s:3:"url";s:20:"admin/print-requests";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-07 04:29:16";s:10:"updated_at";s:19:"2026-01-07 04:29:16";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"86";s:14:"menu_parent_id";N;s:5:"label";s:11:"Competition";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:40:32";s:10:"updated_at";s:19:"2026-01-11 07:40:32";s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"87";s:14:"menu_parent_id";s:2:"86";s:5:"label";s:17:"Competition Setup";s:3:"url";s:19:"competition-winners";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:41:19";s:10:"updated_at";s:19:"2026-01-11 07:41:19";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}}}
|
||||
a:3:{s:4:"time";i:1772401149;s:3:"ttl";i:300;s:4:"data";a:3:{i:0;a:12:{s:2:"id";s:2:"83";s:14:"menu_parent_id";N;s:5:"label";s:15:"TimeOff Request";s:3:"url";s:22:"/administrator/absence";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-11-01 20:58:55";s:10:"updated_at";s:19:"2025-11-01 20:58:55";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"85";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:17:"Print for teacher";s:3:"url";s:20:"admin/print-requests";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-07 04:29:16";s:10:"updated_at";s:19:"2026-01-07 04:29:16";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"86";s:14:"menu_parent_id";N;s:5:"label";s:11:"Competition";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:40:32";s:10:"updated_at";s:19:"2026-01-11 07:40:32";s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"87";s:14:"menu_parent_id";s:2:"86";s:5:"label";s:17:"Competition Setup";s:3:"url";s:19:"competition-winners";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:41:19";s:10:"updated_at";s:19:"2026-01-11 07:41:19";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}}}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user