fix certificates
This commit is contained in:
@@ -6,6 +6,7 @@ use App\Controllers\BaseController;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\CertificateRecordModel;
|
||||
use App\Models\StudentDecisionModel;
|
||||
|
||||
class CertificateController extends BaseController
|
||||
{
|
||||
@@ -27,40 +28,120 @@ class CertificateController extends BaseController
|
||||
public function index()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$classSectionId = $this->request->getGet('class_section_id');
|
||||
$selectedCsid = $this->request->getGet('class_section_id');
|
||||
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||||
|
||||
$classSections = $db->table('classSection cs')
|
||||
->select('cs.class_section_id, cs.class_section_name')
|
||||
->join('student_class sc', 'sc.class_section_id = cs.class_section_id')
|
||||
// ── All enrolled students across every class section ───────────────────
|
||||
$allEnrolled = $db->table('student_class sc')
|
||||
->select('s.id AS student_id, s.firstname, s.lastname, sc.class_section_id, cs.class_section_name')
|
||||
->join('students s', 's.id = sc.student_id')
|
||||
->join('classSection cs', 'cs.class_section_id = sc.class_section_id')
|
||||
->where('s.is_active', 1)
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->groupBy('cs.class_section_id, cs.class_section_name')
|
||||
->having('COUNT(s.id) >', 1)
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->get()->getResultArray();
|
||||
|
||||
$students = [];
|
||||
if ($classSectionId) {
|
||||
$students = $db->table('student_class sc')
|
||||
->select('s.id, s.firstname, s.lastname, cs.class_section_name AS grade')
|
||||
->join('students s', 's.id = sc.student_id')
|
||||
->join('classSection cs', 'cs.class_section_id = sc.class_section_id')
|
||||
->where('sc.class_section_id', (int) $classSectionId)
|
||||
->where('s.is_active', 1)
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->get()->getResultArray();
|
||||
$allIds = array_unique(array_column($allEnrolled, 'student_id'));
|
||||
|
||||
// ── Semester scores ────────────────────────────────────────────────────
|
||||
$allScoreMap = [];
|
||||
if (!empty($allIds)) {
|
||||
foreach ($db->table('semester_scores')
|
||||
->select('student_id, semester, semester_score')
|
||||
->whereIn('student_id', $allIds)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester_score IS NOT NULL', null, false)
|
||||
->get()->getResultArray() as $sr) {
|
||||
$allScoreMap[(int)$sr['student_id']][ucfirst(strtolower($sr['semester']))] =
|
||||
is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Below-60 manual decisions ──────────────────────────────────────────
|
||||
$allBelowMap = [];
|
||||
if (!empty($allIds)) {
|
||||
foreach ($db->table('below_sixty_decisions')
|
||||
->whereIn('student_id', $allIds)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()->getResultArray() as $b) {
|
||||
$allBelowMap[(int)$b['student_id']][ucfirst(strtolower($b['semester']))] = (string)$b['decision'];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Certificate records (most recent per student) ──────────────────────
|
||||
$certsByStudent = [];
|
||||
if (!empty($allIds)) {
|
||||
foreach ($db->table('certificate_records')
|
||||
->select('student_id, certificate_number, issued_at')
|
||||
->whereIn('student_id', $allIds)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('issued_at', 'DESC')
|
||||
->get()->getResultArray() as $c) {
|
||||
$sid = (int)$c['student_id'];
|
||||
if (!isset($certsByStudent[$sid])) {
|
||||
$certsByStudent[$sid] = $c['certificate_number'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Build per-student decisions + per-class buckets ────────────────────
|
||||
$decisionsByStudent = [];
|
||||
$studentsByClass = []; // [csid => [student rows...]]
|
||||
$statsPerClass = []; // [csid => {name, total, pass, cert}]
|
||||
|
||||
foreach ($allEnrolled as $row) {
|
||||
$sid = (int)$row['student_id'];
|
||||
$csid = (int)$row['class_section_id'];
|
||||
|
||||
// Decisions per semester
|
||||
foreach ($allScoreMap[$sid] ?? [] as $sem => $score) {
|
||||
if ($score === null) continue;
|
||||
if ($score >= 60) {
|
||||
$dec = 'Pass'; $src = 'auto';
|
||||
} elseif (!empty($allBelowMap[$sid][$sem])) {
|
||||
$dec = $allBelowMap[$sid][$sem]; $src = 'manual';
|
||||
} else {
|
||||
$dec = ''; $src = 'pending';
|
||||
}
|
||||
$decisionsByStudent[$sid][$sem] = ['decision' => $dec, 'source' => $src];
|
||||
}
|
||||
|
||||
// Per-class stats
|
||||
if (!isset($statsPerClass[$csid])) {
|
||||
$statsPerClass[$csid] = ['name' => $row['class_section_name'], 'total' => 0, 'pass' => 0, 'cert' => 0];
|
||||
}
|
||||
$statsPerClass[$csid]['total']++;
|
||||
|
||||
$sems = $allScoreMap[$sid] ?? [];
|
||||
$isPass = !empty($sems);
|
||||
foreach ($sems as $sem => $score) {
|
||||
if ($score === null) { $isPass = false; break; }
|
||||
if ($score >= 60) continue;
|
||||
$md = $allBelowMap[$sid][$sem] ?? '';
|
||||
if ($md === '' || $md !== 'Pass') { $isPass = false; break; }
|
||||
}
|
||||
if ($isPass) $statsPerClass[$csid]['pass']++;
|
||||
if (isset($certsByStudent[$sid])) $statsPerClass[$csid]['cert']++;
|
||||
|
||||
// Group students by class
|
||||
$studentsByClass[$csid][] = $row;
|
||||
}
|
||||
|
||||
// ── Determine default active tab ───────────────────────────────────────
|
||||
$firstCsid = !empty($allEnrolled) ? (int)$allEnrolled[0]['class_section_id'] : null;
|
||||
if ($selectedCsid === null && $firstCsid !== null) {
|
||||
$selectedCsid = (string)$firstCsid;
|
||||
}
|
||||
|
||||
return view('admin/certificates/index', [
|
||||
'classSections' => $classSections,
|
||||
'students' => $students,
|
||||
'selectedClassId' => $classSectionId,
|
||||
'schoolYear' => $schoolYear,
|
||||
'certDate' => date('m/d/Y'),
|
||||
'studentsByClass' => $studentsByClass,
|
||||
'selectedClassId' => $selectedCsid,
|
||||
'schoolYear' => $schoolYear,
|
||||
'certDate' => date('m/d/Y'),
|
||||
'decisionsByStudent' => $decisionsByStudent,
|
||||
'certsByStudent' => $certsByStudent,
|
||||
'statsPerClass' => $statsPerClass,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -102,6 +183,51 @@ class CertificateController extends BaseController
|
||||
return view('certificates/verify', ['record' => $record ?: null]);
|
||||
}
|
||||
|
||||
// ─── Reprint an existing certificate ──────────────────────────────────────
|
||||
|
||||
public function reprint(string $certNumber)
|
||||
{
|
||||
$record = \Config\Database::connect()
|
||||
->table('certificate_records')
|
||||
->where('certificate_number', strtoupper($certNumber))
|
||||
->get()->getRowArray();
|
||||
|
||||
if (!$record) {
|
||||
return redirect()->to('administrator/certificates/log')
|
||||
->with('error', 'Certificate not found: ' . $certNumber);
|
||||
}
|
||||
|
||||
$certDateFormatted = '';
|
||||
if (!empty($record['cert_date'])) {
|
||||
$ts = strtotime((string)$record['cert_date']);
|
||||
if ($ts) {
|
||||
$certDateFormatted = date('m/d/Y', $ts);
|
||||
}
|
||||
}
|
||||
|
||||
$student = [
|
||||
'firstname' => (string)($record['student_name'] ?? ''),
|
||||
'lastname' => '',
|
||||
'grade' => (string)($record['grade'] ?? ''),
|
||||
'cert_number' => (string)($record['certificate_number'] ?? ''),
|
||||
];
|
||||
|
||||
// student_name is stored as "Firstname Lastname" — split for the PDF builder
|
||||
$parts = explode(' ', trim($student['firstname']), 2);
|
||||
if (count($parts) === 2) {
|
||||
$student['firstname'] = $parts[0];
|
||||
$student['lastname'] = $parts[1];
|
||||
}
|
||||
|
||||
$pdfData = $this->buildPdf([$student], $certDateFormatted ?: date('m/d/Y'));
|
||||
$filename = 'Certificate_' . strtoupper($certNumber) . '.pdf';
|
||||
|
||||
return $this->response
|
||||
->setHeader('Content-Type', 'application/pdf')
|
||||
->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"')
|
||||
->setBody($pdfData);
|
||||
}
|
||||
|
||||
// ─── PDF generation ────────────────────────────────────────────────────────
|
||||
|
||||
public function generate()
|
||||
@@ -188,20 +314,38 @@ class CertificateController extends BaseController
|
||||
$issuedAt = date('Y-m-d H:i:s');
|
||||
$certDateDb = $this->parseCertDate($certDate);
|
||||
|
||||
// Load any existing certificates for these students this school year
|
||||
$db = \Config\Database::connect();
|
||||
$existingCerts = $db->table('certificate_records')
|
||||
->select('student_id, certificate_number')
|
||||
->whereIn('student_id', array_column($students, 'id'))
|
||||
->where('school_year', $schoolYear)
|
||||
->get()->getResultArray();
|
||||
$existingCertMap = [];
|
||||
foreach ($existingCerts as $ec) {
|
||||
$existingCertMap[(int)$ec['student_id']] = $ec['certificate_number'];
|
||||
}
|
||||
|
||||
foreach ($students as &$student) {
|
||||
$certNumber = $this->certRecordModel->nextNumber($schoolYear);
|
||||
$this->certRecordModel->insert([
|
||||
'certificate_number' => $certNumber,
|
||||
'student_id' => $student['id'],
|
||||
'student_name' => $student['firstname'] . ' ' . $student['lastname'],
|
||||
'grade' => $this->formatGrade($student['grade'] ?? ''),
|
||||
'cert_date' => $certDateDb,
|
||||
'school_year' => $schoolYear,
|
||||
'class_section_id' => $classSectionId ?: null,
|
||||
'issued_by' => $issuedBy,
|
||||
'issued_at' => $issuedAt,
|
||||
]);
|
||||
$student['cert_number'] = $certNumber;
|
||||
$sid = (int)$student['id'];
|
||||
if (isset($existingCertMap[$sid])) {
|
||||
// Reuse existing certificate number — do not create a new record
|
||||
$student['cert_number'] = $existingCertMap[$sid];
|
||||
} else {
|
||||
$certNumber = $this->certRecordModel->nextNumber($schoolYear);
|
||||
$this->certRecordModel->insert([
|
||||
'certificate_number' => $certNumber,
|
||||
'student_id' => $sid,
|
||||
'student_name' => $student['firstname'] . ' ' . $student['lastname'],
|
||||
'grade' => $this->formatGrade($student['grade'] ?? ''),
|
||||
'cert_date' => $certDateDb,
|
||||
'school_year' => $schoolYear,
|
||||
'class_section_id' => $classSectionId ?: null,
|
||||
'issued_by' => $issuedBy,
|
||||
'issued_at' => $issuedAt,
|
||||
]);
|
||||
$student['cert_number'] = $certNumber;
|
||||
}
|
||||
}
|
||||
unset($student);
|
||||
|
||||
@@ -234,15 +378,20 @@ class CertificateController extends BaseController
|
||||
$clean = trim($raw);
|
||||
$lower = strtolower($clean);
|
||||
|
||||
if (preg_match('/^\d+$/', $clean) && (int)$clean >= 1 && (int)$clean <= 9) {
|
||||
return 'Grade ' . $clean;
|
||||
// Strip section suffix: "Grade 1-A" → "Grade 1", "Grade 2-B" → "Grade 2"
|
||||
if (preg_match('/^grade\s*(\d+)/i', $clean, $m)) {
|
||||
return 'Grade ' . (int)$m[1];
|
||||
}
|
||||
if ($lower === 'youth') {
|
||||
return 'Youth';
|
||||
}
|
||||
if ($lower === 'kg') {
|
||||
if ($lower === 'kg' || $lower === 'kindergarten') {
|
||||
return 'Kindergarten';
|
||||
}
|
||||
// Raw number or number-section (e.g. "1", "1-A", "2-B") → keep number only
|
||||
if (preg_match('/^(\d+)([- ][A-Za-z0-9]+)?$/', $clean, $m)) {
|
||||
return 'Grade ' . (int)$m[1];
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ use App\Models\PlacementLevelModel;
|
||||
use App\Models\PlacementBatchModel;
|
||||
use App\Models\PlacementScoreModel;
|
||||
use App\Models\GradingLockModel;
|
||||
use App\Models\BelowSixtyDecisionModel;
|
||||
use App\Models\StudentDecisionModel;
|
||||
use App\Services\NavbarService;
|
||||
|
||||
//use App\Models\ScoreModel;
|
||||
@@ -1954,6 +1956,73 @@ class GradingController extends Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
private function fetchAllSemestersForStudent(int $studentId, string $schoolYear): array
|
||||
{
|
||||
$rows = $this->db->table('semester_scores ss')
|
||||
->select([
|
||||
'ss.semester',
|
||||
'cs.class_section_name',
|
||||
'ss.homework_avg',
|
||||
'ss.project_avg',
|
||||
'ss.participation_score',
|
||||
'COALESCE(ss.test_avg, ss.quiz_avg) AS test_avg',
|
||||
'ss.ptap_score',
|
||||
'ss.attendance_score',
|
||||
'ss.midterm_exam_score',
|
||||
'ss.final_exam_score',
|
||||
'ss.semester_score',
|
||||
])
|
||||
->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left')
|
||||
->where('ss.student_id', $studentId)
|
||||
->where('ss.school_year', $schoolYear)
|
||||
->orderBy('ss.semester', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$semesters = [];
|
||||
foreach ($rows as $sr) {
|
||||
$sem = ucfirst(strtolower(trim((string)($sr['semester'] ?? ''))));
|
||||
$semesters[$sem] = [
|
||||
'semester' => $sem,
|
||||
'class_section_name' => $sr['class_section_name'] ?? '',
|
||||
'homework_avg' => $sr['homework_avg'] ?? null,
|
||||
'project_avg' => $sr['project_avg'] ?? null,
|
||||
'participation_score' => $sr['participation_score'] ?? null,
|
||||
'test_avg' => $sr['test_avg'] ?? null,
|
||||
'ptap_score' => $sr['ptap_score'] ?? null,
|
||||
'attendance_score' => $sr['attendance_score'] ?? null,
|
||||
'midterm_exam_score' => $sr['midterm_exam_score'] ?? null,
|
||||
'final_exam_score' => $sr['final_exam_score'] ?? null,
|
||||
'semester_score' => $sr['semester_score'] ?? null,
|
||||
'comments' => [],
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($semesters)) {
|
||||
$commentRows = $this->db->table('score_comments')
|
||||
->select('semester, score_type, comment, created_at')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('comment IS NOT NULL', null, false)
|
||||
->where('comment !=', '')
|
||||
->orderBy('semester', 'ASC')
|
||||
->orderBy('score_type', 'ASC')
|
||||
->orderBy('created_at', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($commentRows as $c) {
|
||||
$sem = ucfirst(strtolower(trim((string)($c['semester'] ?? ''))));
|
||||
$type = strtolower(trim((string)($c['score_type'] ?? 'general')));
|
||||
if (isset($semesters[$sem]) && !isset($semesters[$sem]['comments'][$type])) {
|
||||
$semesters[$sem]['comments'][$type] = (string)($c['comment'] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($semesters);
|
||||
}
|
||||
|
||||
private function fetchBelowSixtyParentName(int $studentId): string
|
||||
{
|
||||
$parentName = 'Parent/Guardian';
|
||||
@@ -2124,6 +2193,556 @@ class GradingController extends Controller
|
||||
}
|
||||
|
||||
|
||||
public function belowSixtyDecisions()
|
||||
{
|
||||
$configuredSemester = (string) $this->semester;
|
||||
$configuredYear = (string) $this->schoolYear;
|
||||
|
||||
$semester = trim((string)($this->request->getGet('semester') ?? ''));
|
||||
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
||||
if ($semester === '') $semester = $configuredSemester !== '' ? $configuredSemester : 'Fall';
|
||||
if ($schoolYear === '') $schoolYear = $configuredYear;
|
||||
|
||||
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
||||
$rows = $this->fetchBelowSixtyRows($schoolYear, $semester);
|
||||
|
||||
$decisionModel = new BelowSixtyDecisionModel();
|
||||
$studentIds = array_values(array_unique(array_filter(
|
||||
array_map(static fn($r) => (int)($r['student_id'] ?? 0), $rows),
|
||||
static fn($id) => $id > 0
|
||||
)));
|
||||
|
||||
$decisionMap = [];
|
||||
if (!empty($studentIds)) {
|
||||
$dRows = $decisionModel
|
||||
->whereIn('student_id', $studentIds)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
foreach ($dRows as $d) {
|
||||
$decisionMap[(int)$d['student_id']] = $d;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$sid = (int)($row['student_id'] ?? 0);
|
||||
$row['decision'] = $decisionMap[$sid]['decision'] ?? '';
|
||||
$row['decision_notes'] = $decisionMap[$sid]['notes'] ?? '';
|
||||
}
|
||||
unset($row);
|
||||
|
||||
// Load consolidated decisions from student_decisions for this term
|
||||
$sdModel = new StudentDecisionModel();
|
||||
$sdRows = $sdModel
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
$sdMap = [];
|
||||
foreach ($sdRows as $sd) {
|
||||
$sdMap[(int)$sd['student_id']] = $sd;
|
||||
}
|
||||
|
||||
// Load the most recent certificate per student for this school_year
|
||||
$studentIds = array_values(array_unique(array_filter(
|
||||
array_map(static fn($r) => (int)($r['student_id'] ?? 0), $rows),
|
||||
static fn($id) => $id > 0
|
||||
)));
|
||||
$certMap = [];
|
||||
if (!empty($studentIds)) {
|
||||
$certRows = $this->db->table('certificate_records')
|
||||
->select('student_id, certificate_number, issued_at')
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('student_id', $studentIds)
|
||||
->orderBy('issued_at', 'DESC')
|
||||
->get()->getResultArray();
|
||||
foreach ($certRows as $cr) {
|
||||
$sid = (int)($cr['student_id'] ?? 0);
|
||||
if ($sid > 0 && !isset($certMap[$sid])) {
|
||||
$certMap[$sid] = (string)($cr['certificate_number'] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$sid = (int)($row['student_id'] ?? 0);
|
||||
$row['consolidated_decision'] = $sdMap[$sid]['decision'] ?? null;
|
||||
$row['certificate_number'] = $certMap[$sid] ?? '';
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$canViewGrading = $this->userHasMenuUrl('grading');
|
||||
|
||||
return view('grading/below_sixty_decisions', [
|
||||
'rows' => $rows,
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'schoolYears' => $schoolYears,
|
||||
'canViewGrading' => $canViewGrading,
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveBelowSixtyDecision()
|
||||
{
|
||||
$studentId = (int)$this->request->getPost('student_id');
|
||||
$semester = trim((string)$this->request->getPost('semester'));
|
||||
$schoolYear = trim((string)$this->request->getPost('school_year'));
|
||||
$decision = trim((string)$this->request->getPost('decision'));
|
||||
$notes = trim((string)$this->request->getPost('notes'));
|
||||
|
||||
if ($studentId <= 0 || $semester === '' || $schoolYear === '') {
|
||||
return redirect()->back()->with('error', 'Missing required data.');
|
||||
}
|
||||
|
||||
$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.');
|
||||
}
|
||||
|
||||
public function studentDecisionDetails()
|
||||
{
|
||||
$studentId = (int)$this->request->getGet('student_id');
|
||||
$schoolYear = trim((string)$this->request->getGet('school_year'));
|
||||
|
||||
if ($studentId <= 0 || $schoolYear === '') {
|
||||
return $this->response->setJSON(['error' => 'Missing student or school year.'])->setStatusCode(400);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'semesters' => $this->fetchAllSemestersForStudent($studentId, $schoolYear),
|
||||
]);
|
||||
}
|
||||
|
||||
public function previewDecisionEmail()
|
||||
{
|
||||
$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 $this->response->setJSON(['error' => 'Missing student or term.'])->setStatusCode(400);
|
||||
}
|
||||
|
||||
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
|
||||
if (empty($row)) {
|
||||
return $this->response->setJSON(['error' => 'Student not found.'])->setStatusCode(404);
|
||||
}
|
||||
|
||||
$decisionModel = new BelowSixtyDecisionModel();
|
||||
$decisionRow = $decisionModel
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
$decision = (string)($decisionRow['decision'] ?? '');
|
||||
$notes = (string)($decisionRow['notes'] ?? '');
|
||||
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
|
||||
$parentName = $this->fetchBelowSixtyParentName($studentId);
|
||||
|
||||
$subject = 'Academic Decision';
|
||||
if ($studentName !== '') $subject .= ' — ' . $studentName;
|
||||
if ($semester !== '' || $schoolYear !== '') {
|
||||
$subject .= ' (' . trim($semester . ' ' . $schoolYear) . ')';
|
||||
}
|
||||
|
||||
$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,
|
||||
];
|
||||
|
||||
// Fetch all semesters' scores + comments for the email
|
||||
$allSemesters = $this->fetchAllSemestersForStudent($studentId, $schoolYear);
|
||||
|
||||
$html = view('emails/below_sixty_decision', [
|
||||
'title' => $subject,
|
||||
'parent_name' => $parentName,
|
||||
'student_name' => $studentName !== '' ? $studentName : 'your student',
|
||||
'class_section_name' => $row['class_section_name'] ?? '',
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'decision' => $decision,
|
||||
'notes' => $notes,
|
||||
'scores' => $scores,
|
||||
'all_semesters' => array_values($allSemesters),
|
||||
], ['saveData' => true]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'subject' => $subject,
|
||||
'html' => $html,
|
||||
'student_id' => $studentId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function editDecisionEmail()
|
||||
{
|
||||
$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.');
|
||||
}
|
||||
|
||||
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
|
||||
if (empty($row)) {
|
||||
return redirect()->back()->with('error', 'Student record not found for the selected term.');
|
||||
}
|
||||
|
||||
$decisionModel = new BelowSixtyDecisionModel();
|
||||
$decisionRow = $decisionModel
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
$decision = (string)($decisionRow['decision'] ?? '');
|
||||
$notes = (string)($decisionRow['notes'] ?? '');
|
||||
|
||||
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
|
||||
$parentName = $this->fetchBelowSixtyParentName($studentId);
|
||||
|
||||
$subject = 'Academic Decision';
|
||||
if ($studentName !== '') $subject .= ' — ' . $studentName;
|
||||
if ($semester !== '' || $schoolYear !== '') {
|
||||
$subject .= ' (' . trim($semester . ' ' . $schoolYear) . ')';
|
||||
}
|
||||
|
||||
$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,
|
||||
];
|
||||
|
||||
$html = view('emails/below_sixty_decision', [
|
||||
'title' => $subject,
|
||||
'parent_name' => $parentName,
|
||||
'student_name' => $studentName !== '' ? $studentName : 'your student',
|
||||
'class_section_name' => $row['class_section_name'] ?? '',
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'decision' => $decision,
|
||||
'notes' => $notes,
|
||||
'scores' => $scores,
|
||||
], ['saveData' => true]);
|
||||
|
||||
return view('grading/below_sixty_decision_email_editor', [
|
||||
'studentId' => $studentId,
|
||||
'studentName' => $studentName,
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'subject' => $subject,
|
||||
'html' => $html,
|
||||
'decision' => $decision,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendDecisionEmail()
|
||||
{
|
||||
$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.');
|
||||
}
|
||||
|
||||
$decisionModel = new BelowSixtyDecisionModel();
|
||||
$decisionRow = $decisionModel
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
|
||||
$subject = $subjectInput !== '' ? $subjectInput : ('Academic Decision — ' . $studentName . ' (' . trim($semester . ' ' . $schoolYear) . ')');
|
||||
|
||||
$allSemesters = $this->fetchAllSemestersForStudent($studentId, $schoolYear);
|
||||
|
||||
$payload = [
|
||||
'student_id' => $studentId,
|
||||
'student_name' => $studentName,
|
||||
'class_section_name' => $row['class_section_name'] ?? '',
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'decision' => (string)($decisionRow['decision'] ?? ''),
|
||||
'notes' => (string)($decisionRow['notes'] ?? ''),
|
||||
'subject' => $subject,
|
||||
'all_semesters' => $allSemesters,
|
||||
'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,
|
||||
],
|
||||
];
|
||||
|
||||
if (trim($htmlInput) !== '') {
|
||||
$payload['html'] = $htmlInput;
|
||||
}
|
||||
|
||||
\CodeIgniter\Events\Events::trigger('below60.decision_email', $payload);
|
||||
|
||||
$query = http_build_query(['semester' => $semester, 'school_year' => $schoolYear]);
|
||||
return redirect()->to(base_url('grading/below-60/decisions') . ($query ? '?' . $query : ''))
|
||||
->with('status', 'Decision email sent to parent(s).');
|
||||
}
|
||||
|
||||
public function allDecisions()
|
||||
{
|
||||
$configuredSemester = (string)$this->semester;
|
||||
$configuredYear = (string)$this->schoolYear;
|
||||
|
||||
$semester = trim((string)($this->request->getGet('semester') ?? ''));
|
||||
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
||||
if ($semester === '') $semester = $configuredSemester !== '' ? $configuredSemester : 'Fall';
|
||||
if ($schoolYear === '') $schoolYear = $configuredYear;
|
||||
|
||||
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
||||
|
||||
// Load saved decisions for this term
|
||||
$decModel = new StudentDecisionModel();
|
||||
$saved = $decModel
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
$savedMap = [];
|
||||
foreach ($saved as $s) {
|
||||
$savedMap[(int)$s['student_id']] = $s;
|
||||
}
|
||||
|
||||
// All semester_scores rows for this term (one per student)
|
||||
$semKey = strtolower(trim($semester));
|
||||
$scoreRows = $this->db->table('semester_scores ss')
|
||||
->select([
|
||||
's.id AS student_id',
|
||||
's.school_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
'ss.semester_score',
|
||||
])
|
||||
->join('students s', 's.id = ss.student_id', 'inner')
|
||||
->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left')
|
||||
->where('s.is_active', 1)
|
||||
->where('ss.school_year', $schoolYear)
|
||||
->where("LOWER(TRIM(ss.semester))", $semKey)
|
||||
->where('ss.semester_score IS NOT NULL', null, false)
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->get()->getResultArray();
|
||||
|
||||
// Pull below-60 decisions
|
||||
$belowDecModel = new BelowSixtyDecisionModel();
|
||||
$belowRows = $belowDecModel
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
$belowMap = [];
|
||||
foreach ($belowRows as $b) {
|
||||
$belowMap[(int)$b['student_id']] = $b;
|
||||
}
|
||||
|
||||
// Build rows combining live scores with saved/computed decisions
|
||||
$rows = [];
|
||||
foreach ($scoreRows as $sr) {
|
||||
$sid = (int)$sr['student_id'];
|
||||
$score = is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null;
|
||||
|
||||
if (isset($savedMap[$sid])) {
|
||||
$decision = (string)($savedMap[$sid]['decision'] ?? '');
|
||||
$source = (string)($savedMap[$sid]['source'] ?? 'auto');
|
||||
$notes = (string)($savedMap[$sid]['notes'] ?? '');
|
||||
} elseif ($score !== null && $score >= 60) {
|
||||
$decision = 'Pass';
|
||||
$source = 'auto';
|
||||
$notes = '';
|
||||
} elseif ($score !== null && isset($belowMap[$sid])) {
|
||||
$decision = (string)($belowMap[$sid]['decision'] ?? '');
|
||||
$source = $decision !== '' ? 'manual' : 'pending';
|
||||
$notes = (string)($belowMap[$sid]['notes'] ?? '');
|
||||
} else {
|
||||
$decision = $score !== null ? '' : '';
|
||||
$source = 'pending';
|
||||
$notes = '';
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'student_id' => $sid,
|
||||
'school_id' => $sr['school_id'] ?? '',
|
||||
'firstname' => $sr['firstname'] ?? '',
|
||||
'lastname' => $sr['lastname'] ?? '',
|
||||
'class_section_name' => $sr['class_section_name'] ?? '',
|
||||
'semester_score' => $score,
|
||||
'decision' => $decision,
|
||||
'source' => $source,
|
||||
'notes' => $notes,
|
||||
'saved' => isset($savedMap[$sid]),
|
||||
];
|
||||
}
|
||||
|
||||
$generated = !empty($saved);
|
||||
|
||||
return view('grading/all_decisions', [
|
||||
'rows' => $rows,
|
||||
'semester' => $semester,
|
||||
'schoolYear' => $schoolYear,
|
||||
'schoolYears' => $schoolYears,
|
||||
'generated' => $generated,
|
||||
]);
|
||||
}
|
||||
|
||||
public function generateAllDecisions()
|
||||
{
|
||||
$semester = trim((string)$this->request->getPost('semester'));
|
||||
$schoolYear = trim((string)$this->request->getPost('school_year'));
|
||||
|
||||
if ($semester === '' || $schoolYear === '') {
|
||||
return redirect()->back()->with('error', 'Missing semester or school year.');
|
||||
}
|
||||
|
||||
$semKey = strtolower(trim($semester));
|
||||
$scoreRows = $this->db->table('semester_scores ss')
|
||||
->select([
|
||||
's.id AS student_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
'ss.semester_score',
|
||||
])
|
||||
->join('students s', 's.id = ss.student_id', 'inner')
|
||||
->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left')
|
||||
->where('s.is_active', 1)
|
||||
->where('ss.school_year', $schoolYear)
|
||||
->where("LOWER(TRIM(ss.semester))", $semKey)
|
||||
->where('ss.semester_score IS NOT NULL', null, false)
|
||||
->get()->getResultArray();
|
||||
|
||||
if (empty($scoreRows)) {
|
||||
return redirect()->back()->with('error', 'No semester scores found for this selection.');
|
||||
}
|
||||
|
||||
// Pull all below-60 decisions for this term
|
||||
$belowDecModel = new BelowSixtyDecisionModel();
|
||||
$belowRows = $belowDecModel
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
$belowMap = [];
|
||||
foreach ($belowRows as $b) {
|
||||
$belowMap[(int)$b['student_id']] = $b;
|
||||
}
|
||||
|
||||
// Load existing saved decisions to upsert
|
||||
$decModel = new StudentDecisionModel();
|
||||
$existing = $decModel
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
$existingMap = [];
|
||||
foreach ($existing as $e) {
|
||||
$existingMap[(int)$e['student_id']] = $e;
|
||||
}
|
||||
|
||||
$userId = (int)(session()->get('user_id') ?? 0) ?: null;
|
||||
$now = utc_now();
|
||||
$saved = 0;
|
||||
|
||||
foreach ($scoreRows as $sr) {
|
||||
$sid = (int)$sr['student_id'];
|
||||
$score = is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null;
|
||||
|
||||
if ($score === null) continue;
|
||||
|
||||
if ($score >= 60) {
|
||||
$decision = 'Pass';
|
||||
$source = 'auto';
|
||||
$notes = null;
|
||||
} elseif (isset($belowMap[$sid]) && (string)($belowMap[$sid]['decision'] ?? '') !== '') {
|
||||
$decision = (string)$belowMap[$sid]['decision'];
|
||||
$source = 'manual';
|
||||
$notes = ($belowMap[$sid]['notes'] ?? '') !== '' ? (string)$belowMap[$sid]['notes'] : null;
|
||||
} else {
|
||||
$decision = null;
|
||||
$source = 'pending';
|
||||
$notes = null;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'student_id' => $sid,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'class_section_name' => $sr['class_section_name'] ?? null,
|
||||
'semester_score' => $score,
|
||||
'decision' => $decision,
|
||||
'source' => $source,
|
||||
'notes' => $notes,
|
||||
'generated_by' => $userId,
|
||||
];
|
||||
|
||||
if (isset($existingMap[$sid])) {
|
||||
$decModel->update($existingMap[$sid]['id'], $payload);
|
||||
} else {
|
||||
$decModel->insert($payload);
|
||||
}
|
||||
$saved++;
|
||||
}
|
||||
|
||||
$query = http_build_query(['semester' => $semester, 'school_year' => $schoolYear]);
|
||||
return redirect()->to(base_url('grading/decisions') . '?' . $query)
|
||||
->with('status', "Decisions generated for {$saved} students.");
|
||||
}
|
||||
|
||||
public function getScoreComment()
|
||||
{
|
||||
// Get all students for the current semester and school year
|
||||
|
||||
Reference in New Issue
Block a user