fix decisions and rank
This commit is contained in:
@@ -6,7 +6,6 @@ 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,9 +26,9 @@ class CertificateController extends BaseController
|
||||
|
||||
public function index()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$selectedCsid = $this->request->getGet('class_section_id');
|
||||
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||||
$db = \Config\Database::connect();
|
||||
$selectedCsid = $this->request->getGet('class_section_id');
|
||||
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||||
|
||||
// ── All enrolled students across every class section ───────────────────
|
||||
$allEnrolled = $db->table('student_class sc')
|
||||
@@ -40,96 +39,126 @@ class CertificateController extends BaseController
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->get()->getResultArray();
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$allIds = array_unique(array_column($allEnrolled, 'student_id'));
|
||||
$allIds = array_values(array_unique(array_map('intval', array_column($allEnrolled, 'student_id'))));
|
||||
|
||||
// ── Saved generated YEAR decisions from student_decisions ──────────────
|
||||
//
|
||||
// Source of truth:
|
||||
// student_decisions.year_score
|
||||
// student_decisions.decision
|
||||
//
|
||||
// Do NOT recalculate certificate decisions here from semester_scores.
|
||||
$decisionsByStudent = [];
|
||||
|
||||
// ── Semester scores ────────────────────────────────────────────────────
|
||||
$allScoreMap = [];
|
||||
if (!empty($allIds)) {
|
||||
foreach ($db->table('semester_scores')
|
||||
->select('student_id, semester, semester_score')
|
||||
$decisionRows = $db->table('student_decisions')
|
||||
->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;
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($decisionRows as $d) {
|
||||
$sid = (int)($d['student_id'] ?? 0);
|
||||
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$decision = trim((string)($d['decision'] ?? ''));
|
||||
$source = trim((string)($d['source'] ?? ''));
|
||||
|
||||
if ($source === '') {
|
||||
$source = $decision === '' ? 'pending' : 'manual';
|
||||
}
|
||||
|
||||
$decisionsByStudent[$sid]['Year'] = [
|
||||
'decision' => $decision,
|
||||
'source' => $source,
|
||||
'notes' => (string)($d['notes'] ?? ''),
|
||||
'year_score' => $d['year_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) ──────────────────────
|
||||
// ── 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) {
|
||||
->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}]
|
||||
// ── Build per-class buckets and stats ──────────────────────────────────
|
||||
$studentsByClass = [];
|
||||
$statsPerClass = [];
|
||||
|
||||
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];
|
||||
if (!isset($statsPerClass[$csid])) {
|
||||
$statsPerClass[$csid] = [
|
||||
'name' => $row['class_section_name'],
|
||||
'total' => 0,
|
||||
'pass' => 0,
|
||||
'cert' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// 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 no generated decision exists yet, explicitly mark pending.
|
||||
if (!isset($decisionsByStudent[$sid])) {
|
||||
$decisionsByStudent[$sid]['Decision'] = [
|
||||
'decision' => '',
|
||||
'source' => 'pending',
|
||||
'notes' => '',
|
||||
'year_score' => null,
|
||||
];
|
||||
}
|
||||
|
||||
// Certificate eligibility:
|
||||
// A student is eligible only if the saved final generated decision is Pass.
|
||||
$studentDecisions = $decisionsByStudent[$sid] ?? [];
|
||||
$isPass = !empty($studentDecisions);
|
||||
|
||||
foreach ($studentDecisions as $decisionRow) {
|
||||
$decision = trim((string)($decisionRow['decision'] ?? ''));
|
||||
|
||||
if (strcasecmp($decision, 'Pass') !== 0) {
|
||||
$isPass = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isPass) {
|
||||
$statsPerClass[$csid]['pass']++;
|
||||
}
|
||||
|
||||
if (isset($certsByStudent[$sid])) {
|
||||
$statsPerClass[$csid]['cert']++;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -181,9 +210,12 @@ class CertificateController extends BaseController
|
||||
->where('cr.verification_token', $certNumber)
|
||||
->orWhere('cr.certificate_number', strtoupper($certNumber))
|
||||
->groupEnd()
|
||||
->get()->getRowArray();
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
return view('certificates/verify', ['record' => $record ?: null]);
|
||||
return view('certificates/verify', [
|
||||
'record' => $record ?: null,
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Reprint an existing certificate ──────────────────────────────────────
|
||||
@@ -193,7 +225,8 @@ class CertificateController extends BaseController
|
||||
$record = \Config\Database::connect()
|
||||
->table('certificate_records')
|
||||
->where('certificate_number', strtoupper($certNumber))
|
||||
->get()->getRowArray();
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$record) {
|
||||
return redirect()->to('administrator/certificates/log')
|
||||
@@ -201,23 +234,26 @@ class CertificateController extends BaseController
|
||||
}
|
||||
|
||||
$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'] ?? ''),
|
||||
'firstname' => (string)($record['student_name'] ?? ''),
|
||||
'lastname' => '',
|
||||
'grade' => (string)($record['grade'] ?? ''),
|
||||
'cert_number' => (string)($record['certificate_number'] ?? ''),
|
||||
'verify_token' => $this->ensureVerificationTokenForRecord($record),
|
||||
];
|
||||
|
||||
// student_name is stored as "Firstname Lastname" — split for the PDF builder
|
||||
// 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];
|
||||
@@ -246,13 +282,15 @@ class CertificateController extends BaseController
|
||||
return $this->response
|
||||
->setStatusCode(422)
|
||||
->setJSON([
|
||||
'ok' => false,
|
||||
'error' => 'Please select at least one student.',
|
||||
'ok' => false,
|
||||
'error' => 'Please select at least one student.',
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
return redirect()->to('administrator/certificates')->with('error', 'Please select at least one student.');
|
||||
|
||||
return redirect()->to('administrator/certificates')
|
||||
->with('error', 'Please select at least one student.');
|
||||
}
|
||||
|
||||
$studentIds = array_filter(array_map('intval', $studentIds));
|
||||
@@ -263,13 +301,15 @@ class CertificateController extends BaseController
|
||||
return $this->response
|
||||
->setStatusCode(422)
|
||||
->setJSON([
|
||||
'ok' => false,
|
||||
'error' => 'Invalid student selection.',
|
||||
'ok' => false,
|
||||
'error' => 'Invalid student selection.',
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
return redirect()->to('administrator/certificates')->with('error', 'Invalid student selection.');
|
||||
|
||||
return redirect()->to('administrator/certificates')
|
||||
->with('error', 'Invalid student selection.');
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
@@ -277,21 +317,26 @@ class CertificateController extends BaseController
|
||||
|
||||
foreach ($studentIds as $id) {
|
||||
$row = null;
|
||||
|
||||
if ($classSectionId) {
|
||||
$row = $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('sc.class_section_id', (int)$classSectionId)
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->where('s.id', $id)
|
||||
->get()->getRowArray();
|
||||
->get()
|
||||
->getRowArray();
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
$s = $db->table('students')
|
||||
->select('id, firstname, lastname, registration_grade AS grade')
|
||||
->where('id', $id)
|
||||
->get()->getRowArray();
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$row = $s ?: null;
|
||||
}
|
||||
|
||||
@@ -305,57 +350,62 @@ class CertificateController extends BaseController
|
||||
return $this->response
|
||||
->setStatusCode(422)
|
||||
->setJSON([
|
||||
'ok' => false,
|
||||
'error' => 'No valid students found.',
|
||||
'ok' => false,
|
||||
'error' => 'No valid students found.',
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
return redirect()->to('administrator/certificates')->with('error', 'No valid students found.');
|
||||
|
||||
return redirect()->to('administrator/certificates')
|
||||
->with('error', 'No valid students found.');
|
||||
}
|
||||
|
||||
$issuedBy = session()->get('user_id');
|
||||
$issuedAt = date('Y-m-d H:i:s');
|
||||
$certDateDb = $this->parseCertDate($certDate);
|
||||
$issuedAt = date('Y-m-d H:i:s');
|
||||
|
||||
// Load any existing certificates for these students this school year
|
||||
$db = \Config\Database::connect();
|
||||
// Load existing certificates for these students this school year.
|
||||
$existingCerts = $db->table('certificate_records')
|
||||
->select('id, student_id, certificate_number, verification_token')
|
||||
->whereIn('student_id', array_column($students, 'id'))
|
||||
->where('school_year', $schoolYear)
|
||||
->get()->getResultArray();
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$existingCertMap = [];
|
||||
|
||||
foreach ($existingCerts as $ec) {
|
||||
$existingCertMap[(int)$ec['student_id']] = $ec;
|
||||
}
|
||||
|
||||
foreach ($students as &$student) {
|
||||
$sid = (int)$student['id'];
|
||||
|
||||
if (isset($existingCertMap[$sid])) {
|
||||
// Reuse existing certificate number — do not create a new record
|
||||
// Reuse existing certificate number — do not create a new record.
|
||||
$existing = $existingCertMap[$sid];
|
||||
$student['cert_number'] = (string)($existing['certificate_number'] ?? '');
|
||||
|
||||
$student['cert_number'] = (string)($existing['certificate_number'] ?? '');
|
||||
$student['verify_token'] = $this->ensureVerificationTokenForRecord($existing);
|
||||
} else {
|
||||
$certNumber = $this->certRecordModel->nextNumber($schoolYear);
|
||||
$certNumber = $this->certRecordModel->nextNumber($schoolYear);
|
||||
$verifyToken = $this->certRecordModel->generateVerificationToken();
|
||||
|
||||
$this->certRecordModel->insert([
|
||||
'certificate_number' => $certNumber,
|
||||
'verification_token' => $verifyToken,
|
||||
'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;
|
||||
|
||||
$student['cert_number'] = $certNumber;
|
||||
$student['verify_token'] = $verifyToken;
|
||||
}
|
||||
}
|
||||
|
||||
unset($student);
|
||||
|
||||
$pdfData = $this->buildPdf($students, $certDate);
|
||||
@@ -376,9 +426,11 @@ class CertificateController extends BaseController
|
||||
if (preg_match('#^(\d{2})/(\d{2})/(\d{4})$#', $certDate, $m)) {
|
||||
return $m[3] . '-' . $m[1] . '-' . $m[2];
|
||||
}
|
||||
|
||||
if (preg_match('#^\d{4}-\d{2}-\d{2}$#', $certDate)) {
|
||||
return $certDate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -387,17 +439,20 @@ class CertificateController extends BaseController
|
||||
$clean = trim($raw);
|
||||
$lower = strtolower($clean);
|
||||
|
||||
// Strip section suffix: "Grade 1-A" → "Grade 1", "Grade 2-B" → "Grade 2"
|
||||
// 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' || $lower === 'kindergarten') {
|
||||
return 'Kindergarten';
|
||||
}
|
||||
// Raw number or number-section (e.g. "1", "1-A", "2-B") → keep number only
|
||||
|
||||
// Raw number or number-section: "1", "1-A", "2-B" → keep number only.
|
||||
if (preg_match('/^(\d+)([- ][A-Za-z0-9]+)?$/', $clean, $m)) {
|
||||
return 'Grade ' . (int)$m[1];
|
||||
}
|
||||
@@ -408,17 +463,22 @@ class CertificateController extends BaseController
|
||||
private function ensureVerificationTokenForRecord(array $record): string
|
||||
{
|
||||
$token = trim((string)($record['verification_token'] ?? ''));
|
||||
|
||||
if ($token !== '') {
|
||||
return $token;
|
||||
}
|
||||
|
||||
$recordId = (int)($record['id'] ?? 0);
|
||||
|
||||
if ($recordId <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$token = $this->certRecordModel->generateVerificationToken();
|
||||
$this->certRecordModel->update($recordId, ['verification_token' => $token]);
|
||||
|
||||
$this->certRecordModel->update($recordId, [
|
||||
'verification_token' => $token,
|
||||
]);
|
||||
|
||||
return $token;
|
||||
}
|
||||
@@ -431,10 +491,11 @@ class CertificateController extends BaseController
|
||||
$imgDir = FCPATH . 'assets' . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR;
|
||||
|
||||
$edwardianFont = \TCPDF_FONTS::addTTFfont($fontDir . 'Edwardian Script ITC Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$garamondBold = \TCPDF_FONTS::addTTFfont($fontDir . 'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$ebGaramond = \TCPDF_FONTS::addTTFfont($fontDir . 'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$garamondBold = \TCPDF_FONTS::addTTFfont($fontDir . 'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32);
|
||||
$ebGaramond = \TCPDF_FONTS::addTTFfont($fontDir . 'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||
|
||||
$pdf = new \TCPDF('L', 'pt', 'A4', true, 'UTF-8', false);
|
||||
|
||||
$pdf->SetCreator('Al Rahma Sunday School');
|
||||
$pdf->SetTitle('Student Certificates');
|
||||
$pdf->SetMargins(0, 0, 0, true);
|
||||
@@ -450,9 +511,9 @@ class CertificateController extends BaseController
|
||||
foreach ($students as $student) {
|
||||
$pdf->AddPage();
|
||||
|
||||
$name = $student['firstname'] . ' ' . $student['lastname'];
|
||||
$grade = $this->formatGrade($student['grade'] ?? '');
|
||||
$certNumber = $student['cert_number'] ?? '';
|
||||
$name = trim((string)($student['firstname'] ?? '') . ' ' . (string)($student['lastname'] ?? ''));
|
||||
$grade = $this->formatGrade($student['grade'] ?? '');
|
||||
$certNumber = $student['cert_number'] ?? '';
|
||||
$verifyToken = $student['verify_token'] ?? '';
|
||||
|
||||
$verifyUrl = $verifyToken !== ''
|
||||
@@ -460,10 +521,18 @@ class CertificateController extends BaseController
|
||||
: null;
|
||||
|
||||
$this->drawCertificate(
|
||||
$pdf, $W, $H,
|
||||
$name, $grade, $certDate, $certNumber,
|
||||
$pdf,
|
||||
$W,
|
||||
$H,
|
||||
$name,
|
||||
$grade,
|
||||
$certDate,
|
||||
$certNumber,
|
||||
$verifyUrl,
|
||||
$imgDir, $edwardianFont, $garamondBold, $ebGaramond
|
||||
$imgDir,
|
||||
$edwardianFont,
|
||||
$garamondBold,
|
||||
$ebGaramond
|
||||
);
|
||||
}
|
||||
|
||||
@@ -476,8 +545,8 @@ class CertificateController extends BaseController
|
||||
*/
|
||||
private function drawCertificate(
|
||||
\TCPDF $pdf,
|
||||
float $W,
|
||||
float $H,
|
||||
float $W,
|
||||
float $H,
|
||||
string $name,
|
||||
string $grade,
|
||||
string $certDate,
|
||||
@@ -489,9 +558,9 @@ class CertificateController extends BaseController
|
||||
string $ebGaramond
|
||||
): void {
|
||||
// ── Images
|
||||
$pdf->Image($imgDir . 'title.png', 126, 0, 600);
|
||||
$pdf->Image($imgDir . 'title.png', 126, 0, 600);
|
||||
$pdf->Image($imgDir . 'background.png', 280, 176, 291, 340);
|
||||
$pdf->Image($imgDir . 'signature.png', 140, 410, 90, 80);
|
||||
$pdf->Image($imgDir . 'signature.png', 140, 410, 90, 80);
|
||||
|
||||
// ── "Presented to:"
|
||||
$pdf->SetFont('times', 'B', 24);
|
||||
@@ -499,25 +568,28 @@ class CertificateController extends BaseController
|
||||
$pdf->SetXY(0, 151);
|
||||
$pdf->Cell($W, 24, 'Presented to:', 0, 0, 'C');
|
||||
|
||||
// ── QR code — left-aligned with "Presented to:", vertically centred on that line
|
||||
$qrSize = 42; // pt
|
||||
// ── QR code — left-aligned with "Presented to:", vertically centred on that line.
|
||||
$qrSize = 42;
|
||||
|
||||
if (!empty($verifyUrl)) {
|
||||
$qrX = 120; // ~1 cm from left edge
|
||||
$qrY = 171 + (24 - $qrSize) / 2; // vertically centred on "Presented to:" row
|
||||
$qrX = 120;
|
||||
$qrY = 171 + (24 - $qrSize) / 2;
|
||||
|
||||
$style = [
|
||||
'border' => false,
|
||||
'border' => false,
|
||||
'padding' => 0,
|
||||
'fgcolor' => [0, 0, 0],
|
||||
'bgcolor' => false,
|
||||
];
|
||||
|
||||
$pdf->write2DBarcode($verifyUrl, 'QRCODE,L', $qrX, $qrY, $qrSize, $qrSize, $style, 'N');
|
||||
}
|
||||
|
||||
// ── Student name — center based on actual string width
|
||||
// ── Student name — center based on actual string width.
|
||||
$pdf->SetFont($edwardianFont, '', 38);
|
||||
$nameX = ($W - $pdf->GetStringWidth($name)) / 2;
|
||||
$this->drawGradientText($pdf, $edwardianFont, 38, $name, $nameX, 221.5);
|
||||
|
||||
|
||||
// ── Line under name
|
||||
$pdf->SetFont('times', '', 20);
|
||||
$pdf->SetXY(0, 236);
|
||||
@@ -543,7 +615,7 @@ class CertificateController extends BaseController
|
||||
$pdf->SetXY(0, 375);
|
||||
$pdf->Cell($W, 20, 'Al Rahma Sunday School', 0, 0, 'C');
|
||||
|
||||
// ── Date (gradient script)
|
||||
// ── Date
|
||||
$this->drawGradientText($pdf, $edwardianFont, 26, $certDate, 586, 456);
|
||||
|
||||
// ── Date underline + label
|
||||
@@ -559,7 +631,7 @@ class CertificateController extends BaseController
|
||||
$pdf->SetXY(106, 492);
|
||||
$pdf->Cell(168, 20, 'Signature', 0, 0, 'C');
|
||||
|
||||
// ── Certificate number — 1.4 cm from bottom, 2.5 cm from left
|
||||
// ── Certificate number — 1.4 cm from bottom, 2.5 cm from left.
|
||||
if ($certNumber !== '') {
|
||||
$pdf->SetFont('helvetica', '', 8);
|
||||
$pdf->SetTextColor(150, 150, 150);
|
||||
@@ -569,8 +641,14 @@ class CertificateController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
private function drawGradientText(\TCPDF $pdf, string $fontName, float $fontSize, string $text, float $x, float $y): void
|
||||
{
|
||||
private function drawGradientText(
|
||||
\TCPDF $pdf,
|
||||
string $fontName,
|
||||
float $fontSize,
|
||||
string $text,
|
||||
float $x,
|
||||
float $y
|
||||
): void {
|
||||
$pdf->SetFont($fontName, '', $fontSize);
|
||||
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
@@ -583,4 +661,4 @@ class CertificateController extends BaseController
|
||||
$pdf->SetTextColor(0, 0, 0);
|
||||
$pdf->Text($x, $y, $text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2298,95 +2298,124 @@ class GradingController extends Controller
|
||||
return 50000;
|
||||
}
|
||||
|
||||
|
||||
public function belowSixtyDecisions()
|
||||
{
|
||||
$configuredSemester = (string) $this->semester;
|
||||
$configuredYear = (string) $this->schoolYear;
|
||||
{
|
||||
$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;
|
||||
$semester = trim((string)($this->request->getGet('semester') ?? ''));
|
||||
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
||||
|
||||
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
||||
$rows = $this->fetchBelowSixtyRows($schoolYear, $semester);
|
||||
if ($semester === '') {
|
||||
$semester = $configuredSemester !== '' ? $configuredSemester : 'Fall';
|
||||
}
|
||||
|
||||
$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
|
||||
)));
|
||||
if ($schoolYear === '') {
|
||||
$schoolYear = $configuredYear;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
||||
$rows = $this->fetchBelowSixtyRows($schoolYear, $semester);
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$sid = (int)($row['student_id'] ?? 0);
|
||||
$row['decision'] = $decisionMap[$sid]['decision'] ?? '';
|
||||
$row['decision_notes'] = $decisionMap[$sid]['notes'] ?? '';
|
||||
}
|
||||
unset($row);
|
||||
$studentIds = array_values(array_unique(array_filter(
|
||||
array_map(static fn($r) => (int)($r['student_id'] ?? 0), $rows),
|
||||
static fn($id) => $id > 0
|
||||
)));
|
||||
|
||||
// Load consolidated decisions from student_decisions for this term
|
||||
$sdModel = new StudentDecisionModel();
|
||||
$sdRows = $sdModel
|
||||
// ── Load manual below-60 semester decisions ─────────────────────────────
|
||||
//
|
||||
// This table still uses semester, because below-60 decisions are tied to
|
||||
// the selected below-60 screen/term.
|
||||
$decisionModel = new BelowSixtyDecisionModel();
|
||||
|
||||
$decisionMap = [];
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
$dRows = $decisionModel
|
||||
->whereIn('student_id', $studentIds)
|
||||
->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 ($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 YEAR decisions from student_decisions ─────────────
|
||||
//
|
||||
// IMPORTANT:
|
||||
// student_decisions no longer has semester or semester_score.
|
||||
// It is now one row per student per school_year using year_score.
|
||||
$sdMap = [];
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
$sdRows = $this->db->table('student_decisions')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($sdRows as $sd) {
|
||||
$sid = (int)($sd['student_id'] ?? 0);
|
||||
|
||||
if ($sid > 0) {
|
||||
$sdMap[$sid] = $sd;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Load the most recent certificate per student for this school year ───
|
||||
$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['year_score'] = $sdMap[$sid]['year_score'] ?? 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');
|
||||
@@ -2643,278 +2672,343 @@ class GradingController extends Controller
|
||||
->with('status', 'Decision email sent to parent(s).');
|
||||
}
|
||||
|
||||
public function allDecisions()
|
||||
{
|
||||
$configuredYear = (string)$this->schoolYear;
|
||||
public function allDecisions()
|
||||
{
|
||||
$configuredYear = (string)$this->schoolYear;
|
||||
|
||||
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
||||
if ($schoolYear === '') $schoolYear = $configuredYear;
|
||||
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
||||
if ($schoolYear === '') {
|
||||
$schoolYear = $configuredYear;
|
||||
}
|
||||
|
||||
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
||||
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
||||
|
||||
// Load saved year decisions (semester='year') for this school year
|
||||
$decModel = new StudentDecisionModel();
|
||||
$saved = $decModel
|
||||
->where('semester', 'year')
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
$savedMap = [];
|
||||
foreach ($saved as $s) {
|
||||
$savedMap[(int)$s['student_id']] = $s;
|
||||
// Load saved YEAR decisions for this school year.
|
||||
// New structure:
|
||||
// one row per student per school_year
|
||||
// uses year_score, not semester_score
|
||||
// does not use semester = 'year'
|
||||
$decModel = new StudentDecisionModel();
|
||||
|
||||
$saved = $decModel
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
|
||||
$savedMap = [];
|
||||
foreach ($saved as $s) {
|
||||
$sid = (int)($s['student_id'] ?? 0);
|
||||
if ($sid > 0) {
|
||||
$savedMap[$sid] = $s;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Fall and Spring semester scores per student.
|
||||
// These raw semester scores are only used to calculate the final year_score.
|
||||
$allScoreRows = $this->db->table('semester_scores ss')
|
||||
->select([
|
||||
's.id AS student_id',
|
||||
's.school_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
'LOWER(TRIM(ss.semester)) AS sem_key',
|
||||
'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)
|
||||
->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring'])
|
||||
->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();
|
||||
|
||||
// Group Fall/Spring scores by student.
|
||||
$studentMap = [];
|
||||
|
||||
foreach ($allScoreRows as $sr) {
|
||||
$sid = (int)($sr['student_id'] ?? 0);
|
||||
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch Fall and Spring semester_scores per student for this school year
|
||||
$allScoreRows = $this->db->table('semester_scores ss')
|
||||
->select([
|
||||
's.id AS student_id',
|
||||
's.school_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
'LOWER(TRIM(ss.semester)) AS sem_key',
|
||||
'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)
|
||||
->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring'])
|
||||
->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();
|
||||
|
||||
// Group by student: keep one base info row + fall/spring scores
|
||||
$studentMap = [];
|
||||
foreach ($allScoreRows as $sr) {
|
||||
$sid = (int)$sr['student_id'];
|
||||
if (!isset($studentMap[$sid])) {
|
||||
$studentMap[$sid] = [
|
||||
'student_id' => $sid,
|
||||
'school_id' => $sr['school_id'] ?? '',
|
||||
'firstname' => $sr['firstname'] ?? '',
|
||||
'lastname' => $sr['lastname'] ?? '',
|
||||
'class_section_name' => $sr['class_section_name'] ?? '',
|
||||
'fall_score' => null,
|
||||
'spring_score' => null,
|
||||
];
|
||||
}
|
||||
$semKey = strtolower(trim((string)($sr['sem_key'] ?? '')));
|
||||
$val = is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null;
|
||||
if ($semKey === 'fall') {
|
||||
$studentMap[$sid]['fall_score'] = $val;
|
||||
} elseif ($semKey === 'spring') {
|
||||
$studentMap[$sid]['spring_score'] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
// Pull below-60 decisions for either semester (use worst available)
|
||||
$belowDecModel = new BelowSixtyDecisionModel();
|
||||
$belowRows = $belowDecModel
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
$belowMap = [];
|
||||
foreach ($belowRows as $b) {
|
||||
$sid = (int)$b['student_id'];
|
||||
// prefer a non-empty decision over empty
|
||||
if (!isset($belowMap[$sid]) || (string)($belowMap[$sid]['decision'] ?? '') === '') {
|
||||
$belowMap[$sid] = $b;
|
||||
}
|
||||
}
|
||||
|
||||
// Build final rows with year_score = (fall + spring) / 2
|
||||
$rows = [];
|
||||
foreach ($studentMap as $sid => $info) {
|
||||
$fall = $info['fall_score'];
|
||||
$spring = $info['spring_score'];
|
||||
|
||||
if ($fall !== null && $spring !== null) {
|
||||
$yearScore = round(($fall + $spring) / 2, 2);
|
||||
} elseif ($fall !== null) {
|
||||
$yearScore = $fall;
|
||||
} elseif ($spring !== null) {
|
||||
$yearScore = $spring;
|
||||
} else {
|
||||
$yearScore = null;
|
||||
}
|
||||
|
||||
if (isset($savedMap[$sid])) {
|
||||
$decision = (string)($savedMap[$sid]['decision'] ?? '');
|
||||
$source = (string)($savedMap[$sid]['source'] ?? 'auto');
|
||||
$notes = (string)($savedMap[$sid]['notes'] ?? '');
|
||||
} elseif ($yearScore !== null && $yearScore >= 60) {
|
||||
$decision = 'Pass';
|
||||
$source = 'auto';
|
||||
$notes = '';
|
||||
} elseif ($yearScore !== null && isset($belowMap[$sid])) {
|
||||
$decision = (string)($belowMap[$sid]['decision'] ?? '');
|
||||
$source = $decision !== '' ? 'manual' : 'pending';
|
||||
$notes = (string)($belowMap[$sid]['notes'] ?? '');
|
||||
} else {
|
||||
$decision = '';
|
||||
$source = 'pending';
|
||||
$notes = '';
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'student_id' => $sid,
|
||||
'school_id' => $info['school_id'],
|
||||
'firstname' => $info['firstname'],
|
||||
'lastname' => $info['lastname'],
|
||||
'class_section_name' => $info['class_section_name'],
|
||||
'fall_score' => $fall,
|
||||
'spring_score' => $spring,
|
||||
'year_score' => $yearScore,
|
||||
'decision' => $decision,
|
||||
'source' => $source,
|
||||
'notes' => $notes,
|
||||
'saved' => isset($savedMap[$sid]),
|
||||
if (!isset($studentMap[$sid])) {
|
||||
$studentMap[$sid] = [
|
||||
'school_id' => $sr['school_id'] ?? '',
|
||||
'firstname' => $sr['firstname'] ?? '',
|
||||
'lastname' => $sr['lastname'] ?? '',
|
||||
'class_section_name' => $sr['class_section_name'] ?? '',
|
||||
'fall_score' => null,
|
||||
'spring_score' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$generated = !empty($saved);
|
||||
$semKey = strtolower(trim((string)($sr['sem_key'] ?? '')));
|
||||
$val = is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null;
|
||||
|
||||
return view('grading/all_decisions', [
|
||||
'rows' => $rows,
|
||||
'schoolYear' => $schoolYear,
|
||||
'schoolYears' => $schoolYears,
|
||||
'generated' => $generated,
|
||||
]);
|
||||
if ($semKey === 'fall') {
|
||||
$studentMap[$sid]['fall_score'] = $val;
|
||||
} elseif ($semKey === 'spring') {
|
||||
$studentMap[$sid]['spring_score'] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
public function generateAllDecisions()
|
||||
{
|
||||
$schoolYear = trim((string)$this->request->getPost('school_year'));
|
||||
// Pull below-60 manual decisions for this school year.
|
||||
// Used only when the calculated year_score is below 60.
|
||||
$belowDecModel = new BelowSixtyDecisionModel();
|
||||
|
||||
if ($schoolYear === '') {
|
||||
return redirect()->back()->with('error', 'Missing school year.');
|
||||
$belowRows = $belowDecModel
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
|
||||
$belowMap = [];
|
||||
|
||||
foreach ($belowRows as $b) {
|
||||
$sid = (int)($b['student_id'] ?? 0);
|
||||
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch Fall and Spring scores per student
|
||||
$allScoreRows = $this->db->table('semester_scores ss')
|
||||
->select([
|
||||
's.id AS student_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
'LOWER(TRIM(ss.semester)) AS sem_key',
|
||||
'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)
|
||||
->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring'])
|
||||
->where('ss.semester_score IS NOT NULL', null, false)
|
||||
->get()->getResultArray();
|
||||
// Keep the first non-empty decision found for this student.
|
||||
if (!isset($belowMap[$sid]) || trim((string)($belowMap[$sid]['decision'] ?? '')) === '') {
|
||||
$belowMap[$sid] = $b;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($allScoreRows)) {
|
||||
return redirect()->back()->with('error', 'No semester scores found for this school year.');
|
||||
// Build final display rows.
|
||||
$rows = [];
|
||||
|
||||
foreach ($studentMap as $sid => $info) {
|
||||
$fall = $info['fall_score'];
|
||||
$spring = $info['spring_score'];
|
||||
|
||||
if ($fall !== null && $spring !== null) {
|
||||
$yearScore = round(($fall + $spring) / 2, 2);
|
||||
} elseif ($fall !== null) {
|
||||
$yearScore = round((float)$fall, 2);
|
||||
} elseif ($spring !== null) {
|
||||
$yearScore = round((float)$spring, 2);
|
||||
} else {
|
||||
$yearScore = null;
|
||||
}
|
||||
|
||||
// Group by student
|
||||
$studentMap = [];
|
||||
foreach ($allScoreRows as $sr) {
|
||||
$sid = (int)$sr['student_id'];
|
||||
if (!isset($studentMap[$sid])) {
|
||||
$studentMap[$sid] = [
|
||||
'firstname' => $sr['firstname'] ?? '',
|
||||
'lastname' => $sr['lastname'] ?? '',
|
||||
'class_section_name' => $sr['class_section_name'] ?? '',
|
||||
'fall_score' => null,
|
||||
'spring_score' => null,
|
||||
];
|
||||
}
|
||||
$semKey = strtolower(trim((string)($sr['sem_key'] ?? '')));
|
||||
$val = is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null;
|
||||
if ($semKey === 'fall') {
|
||||
$studentMap[$sid]['fall_score'] = $val;
|
||||
} elseif ($semKey === 'spring') {
|
||||
$studentMap[$sid]['spring_score'] = $val;
|
||||
if (isset($savedMap[$sid])) {
|
||||
$savedRow = $savedMap[$sid];
|
||||
|
||||
$decision = trim((string)($savedRow['decision'] ?? ''));
|
||||
$source = trim((string)($savedRow['source'] ?? 'pending'));
|
||||
$notes = (string)($savedRow['notes'] ?? '');
|
||||
|
||||
if (isset($savedRow['year_score']) && $savedRow['year_score'] !== '' && is_numeric($savedRow['year_score'])) {
|
||||
$yearScore = round((float)$savedRow['year_score'], 2);
|
||||
}
|
||||
} elseif ($yearScore !== null && $yearScore >= 60) {
|
||||
$decision = 'Pass';
|
||||
$source = 'auto';
|
||||
$notes = '';
|
||||
} elseif ($yearScore !== null && isset($belowMap[$sid])) {
|
||||
$decision = trim((string)($belowMap[$sid]['decision'] ?? ''));
|
||||
$source = $decision !== '' ? 'manual' : 'pending';
|
||||
$notes = (string)($belowMap[$sid]['notes'] ?? '');
|
||||
} else {
|
||||
$decision = '';
|
||||
$source = 'pending';
|
||||
$notes = '';
|
||||
}
|
||||
|
||||
// Pull below-60 decisions for any semester of this year
|
||||
$belowDecModel = new BelowSixtyDecisionModel();
|
||||
$belowRows = $belowDecModel
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
$belowMap = [];
|
||||
foreach ($belowRows as $b) {
|
||||
$sid = (int)$b['student_id'];
|
||||
if (!isset($belowMap[$sid]) || (string)($belowMap[$sid]['decision'] ?? '') === '') {
|
||||
$belowMap[$sid] = $b;
|
||||
}
|
||||
$rows[] = [
|
||||
'student_id' => $sid,
|
||||
'school_id' => $info['school_id'],
|
||||
'firstname' => $info['firstname'],
|
||||
'lastname' => $info['lastname'],
|
||||
'class_section_name' => $info['class_section_name'],
|
||||
'fall_score' => $fall,
|
||||
'spring_score' => $spring,
|
||||
'year_score' => $yearScore,
|
||||
'decision' => $decision,
|
||||
'source' => $source,
|
||||
'notes' => $notes,
|
||||
'saved' => isset($savedMap[$sid]),
|
||||
];
|
||||
}
|
||||
|
||||
$generated = !empty($saved);
|
||||
|
||||
return view('grading/all_decisions', [
|
||||
'rows' => $rows,
|
||||
'schoolYear' => $schoolYear,
|
||||
'schoolYears' => $schoolYears,
|
||||
'generated' => $generated,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function generateAllDecisions()
|
||||
{
|
||||
$schoolYear = trim((string)$this->request->getPost('school_year'));
|
||||
|
||||
if ($schoolYear === '') {
|
||||
return redirect()->back()->with('error', 'Missing school year.');
|
||||
}
|
||||
|
||||
// Fetch Fall and Spring scores per student.
|
||||
$allScoreRows = $this->db->table('semester_scores ss')
|
||||
->select([
|
||||
's.id AS student_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
'LOWER(TRIM(ss.semester)) AS sem_key',
|
||||
'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)
|
||||
->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring'])
|
||||
->where('ss.semester_score IS NOT NULL', null, false)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($allScoreRows)) {
|
||||
return redirect()->back()->with('error', 'No semester scores found for this school year.');
|
||||
}
|
||||
|
||||
// Group Fall/Spring scores by student.
|
||||
$studentMap = [];
|
||||
|
||||
foreach ($allScoreRows as $sr) {
|
||||
$sid = (int)($sr['student_id'] ?? 0);
|
||||
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load existing year decisions to upsert
|
||||
$decModel = new StudentDecisionModel();
|
||||
$existing = $decModel
|
||||
->where('semester', 'year')
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
$existingMap = [];
|
||||
foreach ($existing as $e) {
|
||||
$existingMap[(int)$e['student_id']] = $e;
|
||||
}
|
||||
|
||||
$userId = (int)(session()->get('user_id') ?? 0) ?: null;
|
||||
$savedCount = 0;
|
||||
|
||||
foreach ($studentMap as $sid => $info) {
|
||||
$fall = $info['fall_score'];
|
||||
$spring = $info['spring_score'];
|
||||
|
||||
if ($fall !== null && $spring !== null) {
|
||||
$yearScore = round(($fall + $spring) / 2, 2);
|
||||
} elseif ($fall !== null) {
|
||||
$yearScore = $fall;
|
||||
} elseif ($spring !== null) {
|
||||
$yearScore = $spring;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($yearScore >= 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' => 'year',
|
||||
'school_year' => $schoolYear,
|
||||
'class_section_name' => $info['class_section_name'] ?? null,
|
||||
'semester_score' => $yearScore,
|
||||
'decision' => $decision,
|
||||
'source' => $source,
|
||||
'notes' => $notes,
|
||||
'generated_by' => $userId,
|
||||
if (!isset($studentMap[$sid])) {
|
||||
$studentMap[$sid] = [
|
||||
'firstname' => $sr['firstname'] ?? '',
|
||||
'lastname' => $sr['lastname'] ?? '',
|
||||
'class_section_name' => $sr['class_section_name'] ?? '',
|
||||
'fall_score' => null,
|
||||
'spring_score' => null,
|
||||
];
|
||||
|
||||
if (isset($existingMap[$sid])) {
|
||||
$decModel->update($existingMap[$sid]['id'], $payload);
|
||||
} else {
|
||||
$decModel->insert($payload);
|
||||
}
|
||||
$savedCount++;
|
||||
}
|
||||
|
||||
$query = http_build_query(['school_year' => $schoolYear]);
|
||||
return redirect()->to(base_url('grading/decisions') . '?' . $query)
|
||||
->with('status', "Decisions generated for {$savedCount} students.");
|
||||
$semKey = strtolower(trim((string)($sr['sem_key'] ?? '')));
|
||||
$val = is_numeric($sr['semester_score']) ? (float)$sr['semester_score'] : null;
|
||||
|
||||
if ($semKey === 'fall') {
|
||||
$studentMap[$sid]['fall_score'] = $val;
|
||||
} elseif ($semKey === 'spring') {
|
||||
$studentMap[$sid]['spring_score'] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
// Pull below-60 manual decisions for this school year.
|
||||
$belowDecModel = new BelowSixtyDecisionModel();
|
||||
|
||||
$belowRows = $belowDecModel
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
|
||||
$belowMap = [];
|
||||
|
||||
foreach ($belowRows as $b) {
|
||||
$sid = (int)($b['student_id'] ?? 0);
|
||||
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($belowMap[$sid]) || trim((string)($belowMap[$sid]['decision'] ?? '')) === '') {
|
||||
$belowMap[$sid] = $b;
|
||||
}
|
||||
}
|
||||
|
||||
// Load existing year decisions so we update instead of duplicating.
|
||||
// New structure: one row per student per school_year.
|
||||
$decModel = new StudentDecisionModel();
|
||||
|
||||
$existing = $decModel
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
|
||||
$existingMap = [];
|
||||
|
||||
foreach ($existing as $e) {
|
||||
$sid = (int)($e['student_id'] ?? 0);
|
||||
|
||||
if ($sid > 0) {
|
||||
$existingMap[$sid] = $e;
|
||||
}
|
||||
}
|
||||
|
||||
$userId = (int)(session()->get('user_id') ?? 0) ?: null;
|
||||
$savedCount = 0;
|
||||
|
||||
foreach ($studentMap as $sid => $info) {
|
||||
$fall = $info['fall_score'];
|
||||
$spring = $info['spring_score'];
|
||||
|
||||
if ($fall !== null && $spring !== null) {
|
||||
$yearScore = round(($fall + $spring) / 2, 2);
|
||||
} elseif ($fall !== null) {
|
||||
$yearScore = round((float)$fall, 2);
|
||||
} elseif ($spring !== null) {
|
||||
$yearScore = round((float)$spring, 2);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($yearScore >= 60) {
|
||||
$decision = 'Pass';
|
||||
$source = 'auto';
|
||||
$notes = null;
|
||||
} elseif (isset($belowMap[$sid]) && trim((string)($belowMap[$sid]['decision'] ?? '')) !== '') {
|
||||
$decision = trim((string)$belowMap[$sid]['decision']);
|
||||
$source = 'manual';
|
||||
$notes = trim((string)($belowMap[$sid]['notes'] ?? ''));
|
||||
$notes = $notes !== '' ? $notes : null;
|
||||
} else {
|
||||
$decision = null;
|
||||
$source = 'pending';
|
||||
$notes = null;
|
||||
}
|
||||
|
||||
// Important fix:
|
||||
// use year_score, not semester_score.
|
||||
// do not save semester = 'year'.
|
||||
$payload = [
|
||||
'student_id' => $sid,
|
||||
'school_year' => $schoolYear,
|
||||
'class_section_name' => $info['class_section_name'] ?? null,
|
||||
'year_score' => $yearScore,
|
||||
'decision' => $decision,
|
||||
'source' => $source,
|
||||
'notes' => $notes,
|
||||
'generated_by' => $userId,
|
||||
];
|
||||
|
||||
if (isset($existingMap[$sid])) {
|
||||
$decModel->update((int)$existingMap[$sid]['id'], $payload);
|
||||
} else {
|
||||
$decModel->insert($payload);
|
||||
}
|
||||
|
||||
$savedCount++;
|
||||
}
|
||||
|
||||
$query = http_build_query(['school_year' => $schoolYear]);
|
||||
|
||||
return redirect()->to(base_url('grading/decisions') . '?' . $query)
|
||||
->with('status', "Decisions generated for {$savedCount} students.");
|
||||
}
|
||||
|
||||
public function getScoreComment()
|
||||
{
|
||||
// Get all students for the current semester and school year
|
||||
|
||||
@@ -1014,9 +1014,9 @@ $drawRankCell = static function (
|
||||
$pdf->Rect($x, $y, $w, $h);
|
||||
$pdf->SetXY($x + $pad, $y + 3);
|
||||
$pdf->SetFont('Helvetica', 'B', 11);
|
||||
$pdf->Write(5, ' Ranking: ');
|
||||
$pdf->Write(5, ' Rank: ');
|
||||
|
||||
$labelWidth = $pdf->GetStringWidth(' Ranking: ');
|
||||
$labelWidth = $pdf->GetStringWidth(' Rank: ');
|
||||
$pdf->SetFont('Helvetica', '', 12);
|
||||
$pdf->SetXY($x + 2 + $labelWidth, $y + 3);
|
||||
$pdf->Write(5, $rankValue);
|
||||
@@ -1879,7 +1879,7 @@ $scoresEndY = $pdf->GetY();
|
||||
return [
|
||||
'position' => $position,
|
||||
'total' => $total,
|
||||
'display' => $this->formatOrdinal($position) . ' of ' . $total,
|
||||
'display' => $this->formatOrdinal($position) . ' out of ' . $total,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user