Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 090cb88573 | |||
| 95bcefc3a9 |
+10
-3
@@ -528,10 +528,17 @@ $routes->post('grading/decisions/generate', 'View\GradingController::generateAll
|
|||||||
$routes->get('grading/below-60/decisions', 'View\GradingController::belowSixtyDecisions', ['filter' => 'auth:read']);
|
$routes->get('grading/below-60/decisions', 'View\GradingController::belowSixtyDecisions', ['filter' => 'auth:read']);
|
||||||
$routes->post('grading/below-60/decisions/save', 'View\GradingController::saveBelowSixtyDecision', ['filter' => 'auth:read']);
|
$routes->post('grading/below-60/decisions/save', 'View\GradingController::saveBelowSixtyDecision', ['filter' => 'auth:read']);
|
||||||
$routes->get('grading/below-60/decisions/student-details', 'View\GradingController::studentDecisionDetails', ['filter' => 'auth:read']);
|
$routes->get('grading/below-60/decisions/student-details', 'View\GradingController::studentDecisionDetails', ['filter' => 'auth:read']);
|
||||||
$routes->get('grading/below-60/decisions/email/preview', 'View\GradingController::previewDecisionEmail', ['filter' => 'auth:read']);
|
$routes->get(
|
||||||
$routes->get('grading/below-60/decisions/email/edit', 'View\GradingController::editDecisionEmail', ['filter' => 'auth:read']);
|
'grading/below-60/decisions/email/preview',
|
||||||
$routes->post('grading/below-60/decisions/email', 'View\GradingController::sendDecisionEmail', ['filter' => 'auth:read']);
|
'View\GradingController::previewBelowSixtyDecisionEmail',
|
||||||
|
['filter' => 'auth:read']
|
||||||
|
);
|
||||||
|
|
||||||
|
$routes->post(
|
||||||
|
'grading/below-60/decisions/email',
|
||||||
|
'View\GradingController::sendBelowSixtyDecisionEmail',
|
||||||
|
['filter' => 'auth:read']
|
||||||
|
);
|
||||||
|
|
||||||
// Final part
|
// Final part
|
||||||
$routes->get('grading/(:segment)/(:num)/(:num)', 'View\GradingController::show/$1/$2/$3');
|
$routes->get('grading/(:segment)/(:num)/(:num)', 'View\GradingController::show/$1/$2/$3');
|
||||||
|
|||||||
@@ -2543,51 +2543,148 @@ public function belowSixty()
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveBelowSixtyDecision()
|
public function saveBelowSixtyDecision()
|
||||||
{
|
{
|
||||||
$studentId = (int)$this->request->getPost('student_id');
|
$studentId = (int)($this->request->getPost('student_id') ?? 0);
|
||||||
$semester = trim((string)$this->request->getPost('semester'));
|
$semester = strtolower(trim((string)($this->request->getPost('semester') ?? 'year')));
|
||||||
$schoolYear = trim((string)$this->request->getPost('school_year'));
|
$schoolYear = trim((string)($this->request->getPost('school_year') ?? ''));
|
||||||
$decision = trim((string)$this->request->getPost('decision'));
|
$decision = trim((string)($this->request->getPost('decision') ?? ''));
|
||||||
$notes = trim((string)$this->request->getPost('notes'));
|
$notes = trim((string)($this->request->getPost('notes') ?? ''));
|
||||||
|
|
||||||
if ($studentId <= 0 || $semester === '' || $schoolYear === '') {
|
if ($studentId <= 0 || $schoolYear === '') {
|
||||||
return redirect()->back()->with('error', 'Missing required data.');
|
return redirect()->back()->with('error', 'Missing student or school year.');
|
||||||
}
|
|
||||||
|
|
||||||
$allowed = ['', 'Pass', 'Repeat Class', 'Make-up exam in fall', 'Deferred decision', 'Expel', 'Withdrawn'];
|
|
||||||
if (!in_array($decision, $allowed, true)) {
|
|
||||||
return redirect()->back()->with('error', 'Invalid decision value.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$decisionModel = new BelowSixtyDecisionModel();
|
|
||||||
$existing = $decisionModel
|
|
||||||
->where('student_id', $studentId)
|
|
||||||
->where('semester', $semester)
|
|
||||||
->where('school_year', $schoolYear)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$userId = (int)(session()->get('user_id') ?? 0) ?: null;
|
|
||||||
$payload = [
|
|
||||||
'decision' => $decision !== '' ? $decision : null,
|
|
||||||
'notes' => $notes !== '' ? $notes : null,
|
|
||||||
'decided_by' => $userId,
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
$decisionModel->update((int)$existing['id'], $payload);
|
|
||||||
} else {
|
|
||||||
$payload['student_id'] = $studentId;
|
|
||||||
$payload['semester'] = $semester;
|
|
||||||
$payload['school_year'] = $schoolYear;
|
|
||||||
$decisionModel->insert($payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = http_build_query(['semester' => $semester, 'school_year' => $schoolYear]);
|
|
||||||
return redirect()->to(base_url('grading/below-60/decisions') . ($query ? '?' . $query : ''))
|
|
||||||
->with('status', 'Decision saved.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This decision page should feed certificate decisions as whole-year decisions.
|
||||||
|
// Force year mode here so certificate logic receives final year decision.
|
||||||
|
$semester = 'year';
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 1. Save/update the manual decision in below_sixty_decisions.
|
||||||
|
* This keeps your below-60 page history working.
|
||||||
|
*/
|
||||||
|
$belowModel = new \App\Models\BelowSixtyDecisionModel();
|
||||||
|
|
||||||
|
$existingBelow = $belowModel
|
||||||
|
->where('student_id', $studentId)
|
||||||
|
->where('semester', $semester)
|
||||||
|
->where('school_year', $schoolYear)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$belowPayload = [
|
||||||
|
'student_id' => $studentId,
|
||||||
|
'semester' => $semester,
|
||||||
|
'school_year' => $schoolYear,
|
||||||
|
'decision' => $decision,
|
||||||
|
'notes' => $notes,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($existingBelow) {
|
||||||
|
$belowModel->update((int)$existingBelow['id'], $belowPayload);
|
||||||
|
} else {
|
||||||
|
$belowModel->insert($belowPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 2. Calculate the student's whole-year score.
|
||||||
|
* Certificate page uses student_decisions.year_score.
|
||||||
|
*/
|
||||||
|
$scoreRows = $db->table('semester_scores ss')
|
||||||
|
->select([
|
||||||
|
'LOWER(TRIM(ss.semester)) AS sem_key',
|
||||||
|
'ss.semester_score',
|
||||||
|
'cs.class_section_name',
|
||||||
|
])
|
||||||
|
->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left')
|
||||||
|
->where('ss.student_id', $studentId)
|
||||||
|
->where('ss.school_year', $schoolYear)
|
||||||
|
->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring'])
|
||||||
|
->where('ss.semester_score IS NOT NULL', null, false)
|
||||||
|
->orderBy('ss.updated_at', 'DESC')
|
||||||
|
->orderBy('ss.id', 'DESC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
$fallScore = null;
|
||||||
|
$springScore = null;
|
||||||
|
$classSectionName = null;
|
||||||
|
|
||||||
|
foreach ($scoreRows as $sr) {
|
||||||
|
$semKey = strtolower(trim((string)($sr['sem_key'] ?? '')));
|
||||||
|
$score = is_numeric($sr['semester_score'] ?? null) ? (float)$sr['semester_score'] : null;
|
||||||
|
|
||||||
|
if ($classSectionName === null && !empty($sr['class_section_name'])) {
|
||||||
|
$classSectionName = (string)$sr['class_section_name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($semKey === 'fall' && $fallScore === null) {
|
||||||
|
$fallScore = $score;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($semKey === 'spring' && $springScore === null) {
|
||||||
|
$springScore = $score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fallScore !== null && $springScore !== null) {
|
||||||
|
$yearScore = round(($fallScore + $springScore) / 2, 2);
|
||||||
|
} elseif ($fallScore !== null) {
|
||||||
|
$yearScore = round($fallScore, 2);
|
||||||
|
} elseif ($springScore !== null) {
|
||||||
|
$yearScore = round($springScore, 2);
|
||||||
|
} else {
|
||||||
|
$yearScore = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 3. Sync into student_decisions.
|
||||||
|
* This is the part your certificate page needs.
|
||||||
|
*
|
||||||
|
* student_decisions is now year-based:
|
||||||
|
* - no semester
|
||||||
|
* - no semester_score
|
||||||
|
* - uses year_score
|
||||||
|
*/
|
||||||
|
$source = $decision === '' ? 'pending' : 'manual';
|
||||||
|
|
||||||
|
$studentDecisionPayload = [
|
||||||
|
'student_id' => $studentId,
|
||||||
|
'school_year' => $schoolYear,
|
||||||
|
'class_section_name' => $classSectionName,
|
||||||
|
'year_score' => $yearScore,
|
||||||
|
'decision' => $decision !== '' ? $decision : null,
|
||||||
|
'source' => $source,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'generated_by' => (int)(session()->get('user_id') ?? 0) ?: null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$existingStudentDecision = $db->table('student_decisions')
|
||||||
|
->where('student_id', $studentId)
|
||||||
|
->where('school_year', $schoolYear)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if ($existingStudentDecision) {
|
||||||
|
$db->table('student_decisions')
|
||||||
|
->where('id', (int)$existingStudentDecision['id'])
|
||||||
|
->update($studentDecisionPayload);
|
||||||
|
} else {
|
||||||
|
$db->table('student_decisions')
|
||||||
|
->insert($studentDecisionPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = http_build_query([
|
||||||
|
'semester' => 'year',
|
||||||
|
'school_year' => $schoolYear,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->to(base_url('grading/below-60/decisions') . '?' . $query)
|
||||||
|
->with('status', 'Decision saved and certificate decision updated.');
|
||||||
|
}
|
||||||
|
|
||||||
public function studentDecisionDetails()
|
public function studentDecisionDetails()
|
||||||
{
|
{
|
||||||
$studentId = (int)$this->request->getGet('student_id');
|
$studentId = (int)$this->request->getGet('student_id');
|
||||||
@@ -2602,6 +2699,469 @@ public function belowSixty()
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function previewBelowSixtyDecisionEmail()
|
||||||
|
{
|
||||||
|
$studentId = (int)($this->request->getGet('student_id') ?? 0);
|
||||||
|
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
||||||
|
|
||||||
|
if ($studentId <= 0 || $schoolYear === '') {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'error' => 'Missing student or school year.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Whole-year decision email.
|
||||||
|
* Do NOT query semester_scores.semester = "year".
|
||||||
|
* The Details button uses fetchAllSemestersForStudent(), so this email does too.
|
||||||
|
*/
|
||||||
|
$context = $this->getBelowSixtyDecisionEmailContext($studentId, $schoolYear);
|
||||||
|
|
||||||
|
if (empty($context['student'])) {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'error' => 'Student not found.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($context['decision_row']) || trim((string)($context['decision_row']['decision'] ?? '')) === '') {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'error' => 'No saved decision found for this student. Save a decision first.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$studentName = (string)($context['student_name'] ?? 'Student');
|
||||||
|
$classSectionName = (string)($context['class_section_name'] ?? '');
|
||||||
|
$decisionRow = $context['decision_row'];
|
||||||
|
|
||||||
|
$decision = trim((string)($decisionRow['decision'] ?? ''));
|
||||||
|
$notes = trim((string)($decisionRow['notes'] ?? ''));
|
||||||
|
|
||||||
|
$fallScore = $context['fall_score'] ?? null;
|
||||||
|
$springScore = $context['spring_score'] ?? null;
|
||||||
|
$yearScore = $context['year_score'] ?? null;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Same data used by the Details modal.
|
||||||
|
*/
|
||||||
|
$allSemesters = $context['all_semesters'] ?? [];
|
||||||
|
|
||||||
|
if (empty($allSemesters)) {
|
||||||
|
$allSemesters = $this->fetchAllSemestersForStudent($studentId, $schoolYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallText = $fallScore !== null
|
||||||
|
? number_format((float)$fallScore, 2)
|
||||||
|
: 'N/A';
|
||||||
|
|
||||||
|
$springText = $springScore !== null
|
||||||
|
? number_format((float)$springScore, 2)
|
||||||
|
: 'N/A';
|
||||||
|
|
||||||
|
$yearText = $yearScore !== null
|
||||||
|
? number_format((float)$yearScore, 2)
|
||||||
|
: 'N/A';
|
||||||
|
|
||||||
|
$subject = 'Whole Year Academic Decision — ' . $studentName . ' (' . $schoolYear . ')';
|
||||||
|
|
||||||
|
$html = '
|
||||||
|
<p>Dear Parent/Guardian,</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This message is regarding <strong>' . esc($studentName) . '</strong>
|
||||||
|
for the <strong>' . esc($schoolYear) . '</strong> school year.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Class: <strong>' . esc($classSectionName !== '' ? $classSectionName : 'N/A') . '</strong><br>
|
||||||
|
Fall Score: <strong>' . esc($fallText) . '</strong><br>
|
||||||
|
Spring Score: <strong>' . esc($springText) . '</strong><br>
|
||||||
|
Whole-Year Score: <strong>' . esc($yearText) . '</strong><br>
|
||||||
|
Decision: <strong>' . esc($decision) . '</strong>
|
||||||
|
</p>
|
||||||
|
';
|
||||||
|
|
||||||
|
if ($notes !== '') {
|
||||||
|
$html .= '
|
||||||
|
<p>
|
||||||
|
<strong>Decision Notes:</strong><br>
|
||||||
|
' . nl2br(esc($notes)) . '
|
||||||
|
</p>
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the exact Details-button score/comment content into the email.
|
||||||
|
*/
|
||||||
|
$html .= $this->buildDecisionEmailDetailsHtml($allSemesters);
|
||||||
|
|
||||||
|
$html .= '
|
||||||
|
<p>
|
||||||
|
Please contact the school administration if you have any questions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Regards,<br>
|
||||||
|
Al Rahma Sunday School
|
||||||
|
</p>
|
||||||
|
';
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'ok' => true,
|
||||||
|
'subject' => $subject,
|
||||||
|
'html' => $html,
|
||||||
|
'decision' => $decision,
|
||||||
|
'score' => $yearScore,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildDecisionEmailDetailsHtml(array $semesters): string
|
||||||
|
{
|
||||||
|
if (empty($semesters)) {
|
||||||
|
return '
|
||||||
|
<p><strong>Detailed Scores and Comments</strong></p>
|
||||||
|
<p>No detailed score data found.</p>
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
$scoreLabels = [
|
||||||
|
'homework_avg' => 'Homework Avg',
|
||||||
|
'project_avg' => 'Project Avg',
|
||||||
|
'participation_score' => 'Participation',
|
||||||
|
'test_avg' => 'Test Avg',
|
||||||
|
'ptap_score' => 'PTAP Score',
|
||||||
|
'attendance_score' => 'Attendance',
|
||||||
|
'midterm_exam_score' => 'Midterm Score',
|
||||||
|
'final_exam_score' => 'Final Exam',
|
||||||
|
'semester_score' => 'Semester Score',
|
||||||
|
];
|
||||||
|
|
||||||
|
$commentTypeLabels = [
|
||||||
|
'general' => 'General',
|
||||||
|
'attendance' => 'Attendance',
|
||||||
|
'attendance_comment' => 'Attendance',
|
||||||
|
'midterm' => 'Midterm',
|
||||||
|
'final' => 'Final Exam',
|
||||||
|
'ptap' => 'PTAP',
|
||||||
|
];
|
||||||
|
|
||||||
|
$html = '
|
||||||
|
<hr>
|
||||||
|
<p><strong>Detailed Scores and Comments</strong></p>
|
||||||
|
';
|
||||||
|
|
||||||
|
foreach ($semesters as $sem) {
|
||||||
|
$semesterName = trim((string)($sem['semester'] ?? ''));
|
||||||
|
|
||||||
|
if ($semesterName === '') {
|
||||||
|
$semesterName = 'Semester';
|
||||||
|
}
|
||||||
|
|
||||||
|
$classSectionName = trim((string)($sem['class_section_name'] ?? ''));
|
||||||
|
|
||||||
|
$html .= '
|
||||||
|
<p style="margin-top:16px;margin-bottom:6px;">
|
||||||
|
<strong>' . esc($semesterName) . ' Semester</strong>';
|
||||||
|
|
||||||
|
if ($classSectionName !== '') {
|
||||||
|
$html .= ' — ' . esc($classSectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table border="1"
|
||||||
|
cellpadding="6"
|
||||||
|
cellspacing="0"
|
||||||
|
style="border-collapse:collapse;width:100%;margin-bottom:10px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" style="background:#f2f2f2;">Item</th>
|
||||||
|
<th align="right" style="background:#f2f2f2;">Score</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
';
|
||||||
|
|
||||||
|
$hasScoreRow = false;
|
||||||
|
|
||||||
|
foreach ($scoreLabels as $key => $label) {
|
||||||
|
if (!array_key_exists($key, $sem)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $sem[$key];
|
||||||
|
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scoreText = is_numeric($value)
|
||||||
|
? number_format((float)$value, 2)
|
||||||
|
: (string)$value;
|
||||||
|
|
||||||
|
$fontWeight = $key === 'semester_score' ? 'font-weight:bold;' : '';
|
||||||
|
|
||||||
|
$html .= '
|
||||||
|
<tr>
|
||||||
|
<td>' . esc($label) . '</td>
|
||||||
|
<td align="right" style="' . $fontWeight . '">' . esc($scoreText) . '</td>
|
||||||
|
</tr>
|
||||||
|
';
|
||||||
|
|
||||||
|
$hasScoreRow = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasScoreRow) {
|
||||||
|
$html .= '
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">No scores recorded.</td>
|
||||||
|
</tr>
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
';
|
||||||
|
|
||||||
|
$comments = $sem['comments'] ?? [];
|
||||||
|
|
||||||
|
if (is_array($comments) && !empty($comments)) {
|
||||||
|
$deduped = [];
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
|
foreach ($comments as $type => $text) {
|
||||||
|
$text = trim((string)$text);
|
||||||
|
|
||||||
|
if ($text === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = $commentTypeLabels[$type] ?? (string)$type;
|
||||||
|
$key = $label . '|' . $text;
|
||||||
|
|
||||||
|
if (isset($seen[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen[$key] = true;
|
||||||
|
$deduped[] = [
|
||||||
|
'label' => $label,
|
||||||
|
'text' => $text,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($deduped)) {
|
||||||
|
$html .= '
|
||||||
|
<p style="margin:8px 0 4px;"><strong>Comments</strong></p>
|
||||||
|
';
|
||||||
|
|
||||||
|
foreach ($deduped as $comment) {
|
||||||
|
$html .= '
|
||||||
|
<p style="margin:4px 0 8px;padding:8px;background:#f8f9fa;border-left:4px solid #999;">
|
||||||
|
<strong>' . esc($comment['label']) . ':</strong><br>
|
||||||
|
' . nl2br(esc($comment['text'])) . '
|
||||||
|
</p>
|
||||||
|
';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendBelowSixtyDecisionEmail()
|
||||||
|
{
|
||||||
|
$studentId = (int)($this->request->getPost('student_id') ?? 0);
|
||||||
|
$schoolYear = trim((string)($this->request->getPost('school_year') ?? ''));
|
||||||
|
$subjectInput = trim((string)($this->request->getPost('subject') ?? ''));
|
||||||
|
$htmlInput = (string)($this->request->getPost('html') ?? '');
|
||||||
|
|
||||||
|
if ($studentId <= 0 || $schoolYear === '') {
|
||||||
|
return redirect()->back()->with('error', 'Missing student or school year.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Whole-year decision email.
|
||||||
|
* Do NOT use sendBelowSixtyEmail(), because that one is semester-score based.
|
||||||
|
*/
|
||||||
|
$semester = 'year';
|
||||||
|
|
||||||
|
$context = $this->getBelowSixtyDecisionEmailContext($studentId, $schoolYear);
|
||||||
|
|
||||||
|
if (empty($context['student'])) {
|
||||||
|
return redirect()->back()->with('error', 'Student not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($context['decision_row']) || trim((string)($context['decision_row']['decision'] ?? '')) === '') {
|
||||||
|
return redirect()->back()->with('error', 'No saved decision found for this student.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$studentName = $context['student_name'];
|
||||||
|
$classSectionName = $context['class_section_name'];
|
||||||
|
$decisionRow = $context['decision_row'];
|
||||||
|
|
||||||
|
$subject = $subjectInput !== ''
|
||||||
|
? $subjectInput
|
||||||
|
: 'Whole Year Academic Decision — ' . $studentName . ' (' . $schoolYear . ')';
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'student_id' => $studentId,
|
||||||
|
'student_name' => $studentName,
|
||||||
|
'class_section_name' => $classSectionName,
|
||||||
|
'semester' => 'year',
|
||||||
|
'school_year' => $schoolYear,
|
||||||
|
'decision' => (string)($decisionRow['decision'] ?? ''),
|
||||||
|
'notes' => (string)($decisionRow['notes'] ?? ''),
|
||||||
|
'subject' => $subject,
|
||||||
|
'scores' => [
|
||||||
|
'fall_score' => $context['fall_score'],
|
||||||
|
'spring_score' => $context['spring_score'],
|
||||||
|
'year_score' => $context['year_score'],
|
||||||
|
],
|
||||||
|
'all_semesters' => $context['all_semesters'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (trim($htmlInput) !== '') {
|
||||||
|
$payload['html'] = $htmlInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
Events::trigger('below60.decision_email', $payload);
|
||||||
|
|
||||||
|
$query = http_build_query([
|
||||||
|
'semester' => 'year',
|
||||||
|
'school_year' => $schoolYear,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->to(base_url('grading/below-60/decisions') . '?' . $query)
|
||||||
|
->with('status', 'Decision email sent to parent(s).');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function getBelowSixtyDecisionEmailContext(int $studentId, string $schoolYear): array
|
||||||
|
{
|
||||||
|
$student = $this->db->table('students s')
|
||||||
|
->select([
|
||||||
|
's.id',
|
||||||
|
's.firstname',
|
||||||
|
's.lastname',
|
||||||
|
's.is_active',
|
||||||
|
])
|
||||||
|
->where('s.id', $studentId)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Do not require is_active = 1 here.
|
||||||
|
* You were getting "Student not found" even though the student ID exists.
|
||||||
|
* If the record exists, let the email preview work.
|
||||||
|
*/
|
||||||
|
if (!$student) {
|
||||||
|
return [
|
||||||
|
'student' => null,
|
||||||
|
'decision_row' => null,
|
||||||
|
'student_name' => '',
|
||||||
|
'class_section_name' => '',
|
||||||
|
'fall_score' => null,
|
||||||
|
'spring_score' => null,
|
||||||
|
'year_score' => null,
|
||||||
|
'all_semesters' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$studentName = trim((string)($student['firstname'] ?? '') . ' ' . (string)($student['lastname'] ?? ''));
|
||||||
|
|
||||||
|
if ($studentName === '') {
|
||||||
|
$studentName = 'Student';
|
||||||
|
}
|
||||||
|
|
||||||
|
$decisionRow = $this->db->table('below_sixty_decisions')
|
||||||
|
->where('student_id', $studentId)
|
||||||
|
->where('school_year', $schoolYear)
|
||||||
|
->where('semester', 'year')
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
$scoreRows = $this->db->table('semester_scores ss')
|
||||||
|
->select([
|
||||||
|
'LOWER(TRIM(ss.semester)) AS sem_key',
|
||||||
|
'ss.semester',
|
||||||
|
'ss.semester_score',
|
||||||
|
'ss.class_section_id',
|
||||||
|
'cs.class_section_name',
|
||||||
|
])
|
||||||
|
->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left')
|
||||||
|
->where('ss.student_id', $studentId)
|
||||||
|
->where('ss.school_year', $schoolYear)
|
||||||
|
->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring'])
|
||||||
|
->where('ss.semester_score IS NOT NULL', null, false)
|
||||||
|
->orderBy('ss.updated_at', 'DESC')
|
||||||
|
->orderBy('ss.id', 'DESC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
$fallScore = null;
|
||||||
|
$springScore = null;
|
||||||
|
$classSectionName = '';
|
||||||
|
|
||||||
|
foreach ($scoreRows as $sr) {
|
||||||
|
$semKey = strtolower(trim((string)($sr['sem_key'] ?? '')));
|
||||||
|
$score = is_numeric($sr['semester_score'] ?? null) ? (float)$sr['semester_score'] : null;
|
||||||
|
|
||||||
|
if ($classSectionName === '' && !empty($sr['class_section_name'])) {
|
||||||
|
$classSectionName = (string)$sr['class_section_name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($score === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($semKey === 'fall' && $fallScore === null) {
|
||||||
|
$fallScore = $score;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($semKey === 'spring' && $springScore === null) {
|
||||||
|
$springScore = $score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($classSectionName === '') {
|
||||||
|
$enrollment = $this->db->table('student_class sc')
|
||||||
|
->select('cs.class_section_name')
|
||||||
|
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left')
|
||||||
|
->where('sc.student_id', $studentId)
|
||||||
|
->where('sc.school_year', $schoolYear)
|
||||||
|
->orderBy('sc.id', 'DESC')
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
$classSectionName = (string)($enrollment['class_section_name'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fallScore !== null && $springScore !== null) {
|
||||||
|
$yearScore = round(($fallScore + $springScore) / 2, 2);
|
||||||
|
} elseif ($fallScore !== null) {
|
||||||
|
$yearScore = round($fallScore, 2);
|
||||||
|
} elseif ($springScore !== null) {
|
||||||
|
$yearScore = round($springScore, 2);
|
||||||
|
} else {
|
||||||
|
$yearScore = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'student' => $student,
|
||||||
|
'decision_row' => $decisionRow,
|
||||||
|
'student_name' => $studentName,
|
||||||
|
'class_section_name' => $classSectionName,
|
||||||
|
'fall_score' => $fallScore,
|
||||||
|
'spring_score' => $springScore,
|
||||||
|
'year_score' => $yearScore,
|
||||||
|
'all_semesters' => $this->fetchAllSemestersForStudent($studentId, $schoolYear),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function previewDecisionEmail()
|
public function previewDecisionEmail()
|
||||||
{
|
{
|
||||||
$studentId = (int)$this->request->getGet('student_id');
|
$studentId = (int)$this->request->getGet('student_id');
|
||||||
|
|||||||
@@ -317,18 +317,27 @@ class ReportCardsController extends PrintablesBaseController
|
|||||||
$examCommentTypes = $isSecond
|
$examCommentTypes = $isSecond
|
||||||
? ['final']
|
? ['final']
|
||||||
: ($isFirst ? ['midterm'] : ['midterm', 'final']);
|
: ($isFirst ? ['midterm'] : ['midterm', 'final']);
|
||||||
$commentTypes = array_merge($examCommentTypes, ['ptap', 'attendance', 'attendance_comment']);
|
$wantedCommentTypes = array_merge($examCommentTypes, ['ptap', 'attendance']);
|
||||||
$hasCommentReview = $this->db->fieldExists('comment_review', 'score_comments');
|
$hasCommentReview = $this->db->fieldExists('comment_review', 'score_comments');
|
||||||
$commentSelect = $hasCommentReview
|
$hasCommentSemester = $this->db->fieldExists('semester', 'score_comments');
|
||||||
? 'student_id, score_type, comment, comment_review'
|
$hasCommentUpdatedAt = $this->db->fieldExists('updated_at', 'score_comments');
|
||||||
: 'student_id, score_type, comment';
|
$commentSelectParts = ['student_id', 'score_type', 'comment'];
|
||||||
|
if ($hasCommentReview) {
|
||||||
|
$commentSelectParts[] = 'comment_review';
|
||||||
|
}
|
||||||
|
if ($hasCommentSemester) {
|
||||||
|
$commentSelectParts[] = 'semester';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do NOT filter comments by score_type or semester here.
|
||||||
|
// A single bad row like "Attendance Comment", "attendance-comment", or a blank semester
|
||||||
|
// should not make one student look incomplete while the PDF can still print the comment.
|
||||||
$commentBuilder = $this->scoreCommentModel
|
$commentBuilder = $this->scoreCommentModel
|
||||||
->select($commentSelect)
|
->select(implode(', ', $commentSelectParts))
|
||||||
->whereIn('student_id', $studentIds)
|
->whereIn('student_id', $studentIds)
|
||||||
->where('school_year', $year)
|
->where('school_year', $year);
|
||||||
->whereIn('score_type', $commentTypes);
|
if ($hasCommentUpdatedAt) {
|
||||||
if ($sem !== '') {
|
$commentBuilder->orderBy('updated_at', 'DESC');
|
||||||
$this->applySemesterFilter($commentBuilder, $sem, 'semester');
|
|
||||||
}
|
}
|
||||||
$commentRows = [];
|
$commentRows = [];
|
||||||
try {
|
try {
|
||||||
@@ -336,25 +345,92 @@ class ReportCardsController extends PrintablesBaseController
|
|||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cleanComment = static function ($value): string {
|
||||||
|
$text = (string)($value ?? '');
|
||||||
|
$text = str_replace("\xc2\xa0", ' ', $text); // normalize non-breaking spaces
|
||||||
|
return trim($text);
|
||||||
|
};
|
||||||
|
|
||||||
|
$normalizeCommentType = static function ($value) use ($cleanComment): string {
|
||||||
|
$type = strtolower($cleanComment($value));
|
||||||
|
$type = preg_replace('/[^a-z0-9]+/', '_', $type);
|
||||||
|
$type = trim((string)$type, '_');
|
||||||
|
|
||||||
|
$map = [
|
||||||
|
'attendance_comment' => 'attendance',
|
||||||
|
'attendance_comments' => 'attendance',
|
||||||
|
'attendance' => 'attendance',
|
||||||
|
'attendence' => 'attendance',
|
||||||
|
'attendence_comment' => 'attendance',
|
||||||
|
'attendence_comments' => 'attendance',
|
||||||
|
'ptap_comment' => 'ptap',
|
||||||
|
'ptap_comments' => 'ptap',
|
||||||
|
'ptap' => 'ptap',
|
||||||
|
'midterm_comment' => 'midterm',
|
||||||
|
'midterm_comments' => 'midterm',
|
||||||
|
'midterm' => 'midterm',
|
||||||
|
'final_comment' => 'final',
|
||||||
|
'final_comments' => 'final',
|
||||||
|
'final' => 'final',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $map[$type] ?? $type;
|
||||||
|
};
|
||||||
|
|
||||||
|
$normalizeSemester = static function ($value) use ($cleanComment): string {
|
||||||
|
$v = strtolower($cleanComment($value));
|
||||||
|
$v = preg_replace('/[^a-z0-9]+/', ' ', $v);
|
||||||
|
$v = trim((string)$v);
|
||||||
|
if (in_array($v, ['fall', 'first', 'first semester', 'semester 1', '1'], true)) {
|
||||||
|
return 'fall';
|
||||||
|
}
|
||||||
|
if (in_array($v, ['spring', 'second', 'second semester', 'semester 2', '2'], true)) {
|
||||||
|
return 'spring';
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
};
|
||||||
|
$wantedSemester = $normalizeSemester($sem);
|
||||||
|
|
||||||
$commentsByStudent = [];
|
$commentsByStudent = [];
|
||||||
|
$commentPriorityByStudent = [];
|
||||||
foreach ($commentRows as $row) {
|
foreach ($commentRows as $row) {
|
||||||
$sid = (int)($row['student_id'] ?? 0);
|
$sid = (int)($row['student_id'] ?? 0);
|
||||||
if ($sid <= 0) {
|
if ($sid <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$typeRaw = strtolower(trim((string)($row['score_type'] ?? '')));
|
|
||||||
if ($typeRaw === '') {
|
$typeRaw = $normalizeCommentType($row['score_type'] ?? '');
|
||||||
|
if ($typeRaw === '' || !in_array($typeRaw, $wantedCommentTypes, true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ($typeRaw === 'attendance_comment') {
|
|
||||||
$typeRaw = 'attendance';
|
$rawVal = $cleanComment($row['comment'] ?? '');
|
||||||
|
if ($typeRaw === 'attendance') {
|
||||||
|
// Attendance comments are stored in score_comments.comment.
|
||||||
|
// Do not use comment_review here, because it can be blank while the PDF comment exists.
|
||||||
|
$commentVal = $rawVal;
|
||||||
|
} else {
|
||||||
|
// For non-attendance comments, prefer reviewed text but fall back to the saved comment.
|
||||||
|
$reviewVal = $hasCommentReview ? $cleanComment($row['comment_review'] ?? '') : '';
|
||||||
|
$commentVal = $reviewVal !== '' ? $reviewVal : $rawVal;
|
||||||
}
|
}
|
||||||
$reviewVal = $hasCommentReview ? trim((string)($row['comment_review'] ?? '')) : '';
|
|
||||||
$commentVal = $hasCommentReview ? $reviewVal : trim((string)($row['comment'] ?? ''));
|
|
||||||
if ($commentVal === '') {
|
if ($commentVal === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$commentsByStudent[$sid][$typeRaw] = $commentVal;
|
|
||||||
|
$rowSemester = $hasCommentSemester ? $normalizeSemester($row['semester'] ?? '') : '';
|
||||||
|
$priority = 1;
|
||||||
|
if ($wantedSemester !== '' && $rowSemester === $wantedSemester) {
|
||||||
|
$priority = 3;
|
||||||
|
} elseif ($rowSemester === '') {
|
||||||
|
$priority = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingPriority = $commentPriorityByStudent[$sid][$typeRaw] ?? 0;
|
||||||
|
if (!isset($commentsByStudent[$sid][$typeRaw]) || $priority >= $existingPriority) {
|
||||||
|
$commentsByStudent[$sid][$typeRaw] = $commentVal;
|
||||||
|
$commentPriorityByStudent[$sid][$typeRaw] = $priority;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$isNumeric = static fn($v) => $v !== null && $v !== '' && is_numeric($v);
|
$isNumeric = static fn($v) => $v !== null && $v !== '' && is_numeric($v);
|
||||||
@@ -467,6 +543,20 @@ class ReportCardsController extends PrintablesBaseController
|
|||||||
if (trim((string)($commentSet['ptap'] ?? '')) === '') {
|
if (trim((string)($commentSet['ptap'] ?? '')) === '') {
|
||||||
$missing[] = 'PTAP comment';
|
$missing[] = 'PTAP comment';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep completeness consistent with the PDF report generation:
|
||||||
|
// the report can auto-generate an attendance comment from attendance_score.
|
||||||
|
if (trim((string)($commentSet['attendance'] ?? '')) === '' && $isNumeric($attendanceScore)) {
|
||||||
|
$autoAttendance = attendance_comment_from_score(
|
||||||
|
(float)$attendanceScore,
|
||||||
|
trim((string)($student['firstname'] ?? ''))
|
||||||
|
);
|
||||||
|
if ($autoAttendance !== null && trim((string)$autoAttendance) !== '') {
|
||||||
|
$commentSet['attendance'] = $autoAttendance;
|
||||||
|
$warnings[] = 'Attendance comment computed from attendance score';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (trim((string)($commentSet['attendance'] ?? '')) === '') {
|
if (trim((string)($commentSet['attendance'] ?? '')) === '') {
|
||||||
$missing[] = 'Attendance comment';
|
$missing[] = 'Attendance comment';
|
||||||
}
|
}
|
||||||
@@ -1531,10 +1621,16 @@ $scoresEndY = $pdf->GetY();
|
|||||||
if ($typeRaw === 'attendance_comment') {
|
if ($typeRaw === 'attendance_comment') {
|
||||||
$typeRaw = 'attendance';
|
$typeRaw = 'attendance';
|
||||||
}
|
}
|
||||||
$reviewVal = trim((string)($row['comment_review'] ?? ''));
|
$rawComment = trim((string)($row['comment'] ?? ''));
|
||||||
$commentVal = $this->db->fieldExists('comment_review', 'score_comments')
|
if ($typeRaw === 'attendance') {
|
||||||
? $reviewVal
|
// Attendance comments must come from score_comments.comment.
|
||||||
: trim((string)($row['comment'] ?? ''));
|
$commentVal = $rawComment;
|
||||||
|
} else {
|
||||||
|
$reviewVal = trim((string)($row['comment_review'] ?? ''));
|
||||||
|
$commentVal = $this->db->fieldExists('comment_review', 'score_comments') && $reviewVal !== ''
|
||||||
|
? $reviewVal
|
||||||
|
: $rawComment;
|
||||||
|
}
|
||||||
if ($commentVal === '') {
|
if ($commentVal === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user