2920 lines
116 KiB
PHP
Executable File
2920 lines
116 KiB
PHP
Executable File
<?php
|
||
|
||
namespace App\Controllers\View;
|
||
use App\Controllers\BaseController;
|
||
use App\Models\UserModel;
|
||
use App\Models\RoleModel;
|
||
use App\Models\PermissionModel;
|
||
use App\Models\RolePermissionModel;
|
||
use App\Models\StudentModel;
|
||
use App\Models\LoginActivityModel;
|
||
use CodeIgniter\Controller;
|
||
use App\Models\InvoiceModel;
|
||
use App\Models\ClassSectionModel;
|
||
use App\Models\UserRoleModel;
|
||
use App\Models\ConfigurationModel;
|
||
use App\Models\RefundModel;
|
||
use App\Models\EnrollmentModel;
|
||
use App\Models\AdminNotificationSubjectModel;
|
||
use Doctrine\DBAL\Configuration;
|
||
use App\Services\FeeCalculationService;
|
||
use App\Models\StudentClassModel;
|
||
use App\Controllers\View\EmailController;
|
||
use App\Controllers\View\InvoiceController;
|
||
use App\Models\StaffAttendanceModel;
|
||
use App\Libraries\StaffTimeOffLinkService;
|
||
use App\Models\AttendanceDayModel;
|
||
use App\Models\ScoreCommentModel;
|
||
use App\Models\SemesterScoreModel;
|
||
use App\Models\TeacherClassModel;
|
||
use App\Models\TeacherSubmissionNotificationHistoryModel;
|
||
use App\Models\ExamDraftModel;
|
||
use App\Models\HomeworkModel;
|
||
use App\Services\SemesterRangeService;
|
||
|
||
use CodeIgniter\Events\Events;
|
||
|
||
class AdministratorController extends BaseController
|
||
{
|
||
protected $permissionModel;
|
||
protected $rolePermissionModel;
|
||
protected $roleModel;
|
||
protected $userModel;
|
||
protected $userRoleModel;
|
||
protected $db;
|
||
protected $configModel;
|
||
protected $semester;
|
||
protected $schoolYear;
|
||
protected $studentModel;
|
||
protected $loginActivityModel;
|
||
protected $invoiceModel;
|
||
protected $refundModel;
|
||
protected $enrollmentModel;
|
||
protected $classSectionModel;
|
||
protected $studentClassModel;
|
||
protected $staffAttendanceModel;
|
||
protected $adminNotificationSubjectModel;
|
||
|
||
public function __construct()
|
||
{
|
||
helper('auth');
|
||
// Load models
|
||
$this->rolePermissionModel = new RolePermissionModel();
|
||
$this->permissionModel = new PermissionModel();
|
||
$this->roleModel = new RoleModel();
|
||
$this->userModel = new UserModel();
|
||
$this->configModel = new ConfigurationModel();
|
||
$this->studentModel = new StudentModel();
|
||
$this->loginActivityModel = new LoginActivityModel();
|
||
$this->userRoleModel = new UserRoleModel();
|
||
$this->invoiceModel = new InvoiceModel();
|
||
$this->refundModel = new RefundModel();
|
||
$this->enrollmentModel = new EnrollmentModel();
|
||
$this->classSectionModel = new ClassSectionModel();
|
||
$this->adminNotificationSubjectModel = new AdminNotificationSubjectModel();
|
||
|
||
$this->semester = $this->configModel->getConfig('semester');
|
||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||
$this->studentClassModel = new StudentClassModel();
|
||
$this->staffAttendanceModel = new StaffAttendanceModel();
|
||
// Load the database service
|
||
$this->db = \Config\Database::connect();
|
||
// Check if the database connection is established
|
||
if (!$this->db->connect()) {
|
||
log_message('error', 'Database connection failed.');
|
||
throw new \Exception('Database connection failed.');
|
||
} else {
|
||
log_message('info', 'Database connection successful.');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Compute all allowed absence dates (future Sundays within Sep..May for the configured school year)
|
||
*/
|
||
private function allowedAbsenceDates(): array
|
||
{
|
||
$todayStr = local_date(utc_now(), 'Y-m-d');
|
||
try { $today = new \DateTimeImmutable($todayStr); } catch (\Throwable $e) { $today = new \DateTimeImmutable(); }
|
||
|
||
$sy = (string)($this->schoolYear ?? '');
|
||
$startYear = null; $endYear = null;
|
||
if (preg_match('/^(\d{4})\D+(\d{4})$/', $sy, $m)) {
|
||
$startYear = (int)$m[1];
|
||
$endYear = (int)$m[2];
|
||
} else {
|
||
$cy = (int)date('Y');
|
||
$cm = (int)date('n');
|
||
if ($cm >= 9) { // Sep-Dec
|
||
$startYear = $cy;
|
||
$endYear = $cy + 1;
|
||
} else {
|
||
$startYear = $cy - 1;
|
||
$endYear = $cy;
|
||
}
|
||
}
|
||
|
||
try {
|
||
$start = new \DateTimeImmutable(sprintf('%04d-09-01', $startYear));
|
||
$end = new \DateTimeImmutable(sprintf('%04d-05-31', $endYear));
|
||
} catch (\Throwable $e) {
|
||
return [];
|
||
}
|
||
|
||
if ($start < $today) {
|
||
$start = $today;
|
||
}
|
||
|
||
$dates = [];
|
||
for ($cursor = $start; $cursor <= $end; $cursor = $cursor->modify('+1 day')) {
|
||
if ((int)$cursor->format('w') === 0) { // Sunday
|
||
$dates[] = $cursor->format('Y-m-d');
|
||
}
|
||
}
|
||
return $dates;
|
||
}
|
||
|
||
private function getPreviousSchoolYear(string $schoolYear): string
|
||
{
|
||
$schoolYear = trim($schoolYear);
|
||
if ($schoolYear === '') {
|
||
return '';
|
||
}
|
||
|
||
if (preg_match('/^(\d{4})\s*-\s*(\d{4})$/', $schoolYear, $m)) {
|
||
return ((int)$m[1] - 1) . '-' . ((int)$m[2] - 1);
|
||
}
|
||
|
||
if (preg_match('/^(\d{4})\s*-\s*(\d{2})$/', $schoolYear, $m)) {
|
||
$start = (int)$m[1] - 1;
|
||
$end = (int)$m[2] - 1;
|
||
if ($end < 0) {
|
||
$end += 100;
|
||
}
|
||
return sprintf('%04d-%02d', $start, $end);
|
||
}
|
||
|
||
if (preg_match('/^\d{4}$/', $schoolYear)) {
|
||
return (string)((int)$schoolYear - 1);
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
private function getSchoolYearStartYear(string $schoolYear): ?int
|
||
{
|
||
$schoolYear = trim($schoolYear);
|
||
if ($schoolYear === '') {
|
||
return null;
|
||
}
|
||
|
||
if (preg_match('/^(\d{4})\s*-\s*(\d{4})$/', $schoolYear, $m)) {
|
||
return (int)$m[1];
|
||
}
|
||
|
||
if (preg_match('/^(\d{4})\s*-\s*(\d{2})$/', $schoolYear, $m)) {
|
||
return (int)$m[1];
|
||
}
|
||
|
||
if (preg_match('/^\d{4}$/', $schoolYear)) {
|
||
return (int)$schoolYear;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Admin self-service absence/vacation page (same features as teacher page)
|
||
*/
|
||
public function absenceFormAdmin()
|
||
{
|
||
$userId = (int)(session()->get('user_id') ?? 0);
|
||
if ($userId <= 0) {
|
||
return redirect()->to('login')->with('error', 'Please log in first.');
|
||
}
|
||
|
||
$admin = $this->userModel->find($userId);
|
||
$displayName = $admin ? trim(($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? '')) : 'Administrator';
|
||
|
||
$semester = (string)($this->semester ?? '');
|
||
$schoolYear = (string)($this->schoolYear ?? '');
|
||
|
||
$existing = $this->staffAttendanceModel
|
||
->where('user_id', $userId)
|
||
->where('semester', $semester)
|
||
->where('school_year', $schoolYear)
|
||
->orderBy('date', 'DESC')
|
||
->findAll();
|
||
|
||
return view('administrator/absence_vacation', [
|
||
'admin_name' => $displayName,
|
||
'semester' => $semester,
|
||
'schoolYear' => $schoolYear,
|
||
'existing' => $existing,
|
||
'availableDates' => $this->allowedAbsenceDates(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Handle admin absence submission (upserts staff_attendance like the teacher flow)
|
||
*/
|
||
public function submitAbsenceAdmin()
|
||
{
|
||
$userId = (int)(session()->get('user_id') ?? 0);
|
||
if ($userId <= 0) {
|
||
return redirect()->to('login')->with('error', 'Please log in first.');
|
||
}
|
||
|
||
$semester = (string)($this->semester ?? '');
|
||
$schoolYear = (string)($this->schoolYear ?? '');
|
||
if ($schoolYear === '') {
|
||
return redirect()->to('/administrator/absence')->with('status', 'error')->with('message', 'Semester or school year not configured.');
|
||
}
|
||
$semesterResolver = new SemesterRangeService($this->configModel);
|
||
|
||
$dates = (array)($this->request->getPost('dates') ?? []);
|
||
$reasonType = trim((string)($this->request->getPost('reason_type') ?? ''));
|
||
$reasonText = trim((string)($this->request->getPost('reason') ?? ''));
|
||
|
||
if ($reasonText === '') {
|
||
return redirect()->to('/administrator/absence')
|
||
->with('status', 'error')
|
||
->with('message', 'Reason is required.');
|
||
}
|
||
|
||
$reasonBase = ($reasonType !== '') ? strtolower($reasonType) : '';
|
||
$reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText;
|
||
|
||
$allowedDates = $this->allowedAbsenceDates();
|
||
$allowedSet = array_fill_keys($allowedDates, true);
|
||
|
||
$roleName = $this->userModel->getUserRole($userId) ?: null;
|
||
|
||
$saved = 0; $invalid = [];
|
||
$savedDates = [];
|
||
$dates = array_values(array_unique(array_map('strval', $dates)));
|
||
foreach ($dates as $d) {
|
||
$d = trim((string)$d);
|
||
if ($d === '') continue;
|
||
$dt = date_create_from_format('Y-m-d', $d);
|
||
if (!$dt || $dt->format('Y-m-d') !== $d || empty($allowedSet[$d])) {
|
||
$invalid[] = $d;
|
||
continue;
|
||
}
|
||
|
||
$semesterForDate = $semesterResolver->getSemesterForDate($d);
|
||
if ($semesterForDate === '') {
|
||
$semesterForDate = $semester;
|
||
}
|
||
|
||
$ok = $this->staffAttendanceModel->upsertOne(
|
||
userId: $userId,
|
||
roleName: $roleName,
|
||
date: $d,
|
||
semester: $semesterForDate,
|
||
schoolYear: $schoolYear,
|
||
status: StaffAttendanceModel::STATUS_ABSENT,
|
||
reason: $reason,
|
||
editorId: $userId
|
||
);
|
||
if ($ok) { $saved++; $savedDates[] = $d; }
|
||
}
|
||
|
||
if (!empty($invalid)) {
|
||
return redirect()->to('/administrator/absence')
|
||
->with('status', 'error')
|
||
->with('message', 'Invalid dates: ' . implode(', ', $invalid));
|
||
}
|
||
|
||
// Send notification email to principal with request details
|
||
try {
|
||
$user = $this->userModel->find($userId) ?: [];
|
||
$fullName = trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? '')) ?: 'Administrator';
|
||
$userEmail = $user['email'] ?? '';
|
||
$role = $roleName ?: 'admin';
|
||
$dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates);
|
||
|
||
$subject = sprintf('TimeOff Request: %s (%s) — %s', $fullName, ucfirst((string)$role), $dateList ?: 'No dates');
|
||
$submittedAt = utc_now();
|
||
|
||
$body = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;">'
|
||
. '<h3 style="margin:0 0 8px;">New TimeOff Request</h3>'
|
||
. '<p style="margin:0 0 12px;">A staff time-off request was submitted from the administrator portal.</p>'
|
||
. '<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;">'
|
||
. '<tr><td><strong>Name</strong></td><td>' . esc($fullName) . '</td></tr>'
|
||
. '<tr><td><strong>Email</strong></td><td>' . esc($userEmail) . '</td></tr>'
|
||
. '<tr><td><strong>Role</strong></td><td>' . esc((string)$role) . '</td></tr>'
|
||
. '<tr><td><strong>Semester</strong></td><td>' . esc($semester) . '</td></tr>'
|
||
. '<tr><td><strong>School Year</strong></td><td>' . esc($schoolYear) . '</td></tr>'
|
||
. '<tr><td><strong>Reason Type</strong></td><td>' . esc($reasonType ?: '-') . '</td></tr>'
|
||
. '<tr><td><strong>Reason</strong></td><td>' . esc($reasonText) . '</td></tr>'
|
||
. '<tr><td><strong>Dates</strong></td><td>' . esc($dateList ?: '-') . '</td></tr>'
|
||
. '<tr><td><strong>Submitted At</strong></td><td>' . esc($submittedAt) . '</td></tr>'
|
||
. '</table>'
|
||
. '</div>';
|
||
|
||
$linkService = new StaffTimeOffLinkService();
|
||
$token = $linkService->createToken([
|
||
'uid' => $userId,
|
||
'email' => $userEmail,
|
||
'name' => $fullName,
|
||
'role' => $role,
|
||
'dates' => $dateList ?: '-',
|
||
'reason' => $reasonText,
|
||
'reason_type' => $reasonType,
|
||
'submitted_at' => $submittedAt,
|
||
'origin' => 'administrator portal',
|
||
]);
|
||
$notifyUrl = base_url('timeoff/notify/' . rawurlencode($token));
|
||
$body .= '<p style="margin-top:16px;">Click <a href="' . esc($notifyUrl) . '">Send confirmation email to '
|
||
. esc($fullName) . '</a> so the staff member is notified automatically. '
|
||
. 'This link expires in 14 days.</p>';
|
||
|
||
$mailer = \Config\Services::emailService();
|
||
$principalEmail = env('PRINCIPAL_EMAIL') ?: 'principal@alrahmaisgl.org';
|
||
$mailer->send($principalEmail, $subject, $body, 'notifications');
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'Failed to send TimeOff email (admin): ' . $e->getMessage());
|
||
}
|
||
|
||
return redirect()->to('/administrator/absence')
|
||
->with('status', 'success')
|
||
->with('message', $saved . ' day(s) saved as absent.');
|
||
}
|
||
|
||
public function index()
|
||
{
|
||
$data = [
|
||
'users' => $this->userModel->findAll(),
|
||
'roles' => $this->roleModel->findAll(),
|
||
];
|
||
|
||
return view('administrator/dashboard', $data);
|
||
}
|
||
|
||
public function isadministrator()
|
||
{
|
||
$session = session();
|
||
$allowedRoles = [
|
||
'administrator',
|
||
'admin',
|
||
'principal',
|
||
//'vice_principal',
|
||
//'vice principal',
|
||
];
|
||
|
||
$role = strtolower((string) $session->get('role'));
|
||
if ($role !== '' && in_array($role, $allowedRoles, true)) {
|
||
return true;
|
||
}
|
||
|
||
$roles = $session->get('roles');
|
||
if (is_array($roles)) {
|
||
foreach ($roles as $candidate) {
|
||
$candidate = strtolower((string) $candidate);
|
||
if ($candidate !== '' && in_array($candidate, $allowedRoles, true)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public function administratorDashboard()
|
||
{
|
||
helper('url');
|
||
|
||
$searchData = $this->buildUserSearchData((string) $this->request->getGet('query'));
|
||
|
||
return view('administrator/administratordashboard', array_merge($searchData, [
|
||
'dashboardEndpoint' => site_url('api/administrator/dashboard'),
|
||
]));
|
||
}
|
||
|
||
public function dashboardMetrics()
|
||
{
|
||
return $this->response->setJSON($this->buildDashboardMetrics());
|
||
}
|
||
|
||
private function buildDashboardMetrics(): array
|
||
{
|
||
$recentActivities = $this->loginActivityModel->getLastActivities(4);
|
||
if (!is_array($recentActivities)) {
|
||
$recentActivities = [];
|
||
}
|
||
|
||
$totalAdmins = (int) ($this->userModel->countAdminsBySchoolYear($this->schoolYear) ?? 0);
|
||
|
||
$teachers = $this->userModel->getUsersByRoleAndSchoolYear('teacher', $this->schoolYear);
|
||
$totalTeachers = $this->countUniqueEntities($teachers);
|
||
|
||
$teacherAssistants = $this->userModel->getUsersByRoleAndSchoolYear('teacher_assistant', $this->schoolYear);
|
||
$totalTeacherAssistants = $this->countUniqueEntities($teacherAssistants);
|
||
|
||
$parents = $this->userModel->getUsersByRoleAndSchoolYear('parent', $this->schoolYear);
|
||
$totalParents = $this->countUniqueEntities($parents);
|
||
|
||
// Count only students that have a class assigned and exist in student_class for the current school year
|
||
$totalStudents = (int) (
|
||
$this->db->table('student_class')
|
||
->select('COUNT(DISTINCT student_class.student_id) AS cnt')
|
||
->join('students', 'students.id = student_class.student_id', 'inner')
|
||
->where('student_class.school_year', $this->schoolYear)
|
||
->where('student_class.class_section_id IS NOT NULL', null, false)
|
||
->where('students.is_active', 1)
|
||
->get()
|
||
->getRow('cnt')
|
||
?? 0
|
||
);
|
||
|
||
return [
|
||
'counts' => [
|
||
'students' => $totalStudents,
|
||
'teachers' => $totalTeachers,
|
||
'teacherAssistants' => $totalTeacherAssistants,
|
||
'admins' => $totalAdmins,
|
||
'parents' => $totalParents,
|
||
],
|
||
'recentActivities' => array_map(static function ($activity) {
|
||
if (!is_array($activity)) {
|
||
return [];
|
||
}
|
||
return [
|
||
'login_time' => $activity['login_time'] ?? null,
|
||
'email' => $activity['email'] ?? null,
|
||
];
|
||
}, $recentActivities),
|
||
'meta' => [
|
||
'schoolYear' => $this->schoolYear,
|
||
'semester' => $this->semester,
|
||
],
|
||
];
|
||
}
|
||
|
||
private function countUniqueEntities($rows): int
|
||
{
|
||
if (!is_array($rows) || $rows === []) {
|
||
return 0;
|
||
}
|
||
|
||
$ids = [];
|
||
foreach ($rows as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
if (isset($row['id'])) {
|
||
$ids[] = (int) $row['id'];
|
||
continue;
|
||
}
|
||
if (isset($row['user_id'])) {
|
||
$ids[] = (int) $row['user_id'];
|
||
}
|
||
}
|
||
|
||
return count(array_unique($ids));
|
||
}
|
||
|
||
public function userSearch()
|
||
{
|
||
$data = $this->buildUserSearchData((string) $this->request->getGet('query'));
|
||
|
||
return view('administrator/search_results', $data);
|
||
}
|
||
|
||
private function buildUserSearchData(string $query): array
|
||
{
|
||
$q = trim($query);
|
||
|
||
if ($q === '') {
|
||
return [
|
||
'query' => '',
|
||
'results' => [],
|
||
'scope_used' => 'unscoped-raw',
|
||
'scope_label' => 'all years/semesters (raw)',
|
||
'total_found' => 0,
|
||
];
|
||
}
|
||
|
||
$db = $this->db;
|
||
|
||
// 1) Tokenize input: split by whitespace and punctuation, keep meaningful pieces
|
||
$rawTokens = preg_split('/[,\s]+/u', $q, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||
$tokens = array_values(array_filter(array_map('trim', $rawTokens)));
|
||
|
||
// 2) Build phone variants for any token that looks numeric-ish
|
||
$phoneMap = []; // token => variants[]
|
||
foreach ($tokens as $t) {
|
||
$digits = preg_replace('/\D+/', '', $t);
|
||
if ($digits === '') {
|
||
continue;
|
||
}
|
||
|
||
$v = [];
|
||
if (strlen($digits) >= 7) {
|
||
// base forms
|
||
$v[] = $digits;
|
||
if (strlen($digits) === 10) {
|
||
$v[] = sprintf('(%s)-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||
$v[] = sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||
$v[] = sprintf('%s %s %s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||
// country-code forms
|
||
$v[] = '1' . $digits;
|
||
$v[] = '+1' . $digits;
|
||
$v[] = '+1 ' . sprintf('(%s) %s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||
$v[] = '+1-' . sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
|
||
} elseif (strlen($digits) === 11 && str_starts_with($digits, '1')) {
|
||
$ten = substr($digits, 1);
|
||
$v[] = $ten;
|
||
$v[] = sprintf('(%s)-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||
$v[] = sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||
$v[] = sprintf('%s %s %s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||
$v[] = '+1' . $ten;
|
||
$v[] = '+1 ' . sprintf('(%s) %s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||
$v[] = '+1-' . sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
|
||
} else {
|
||
// 7-9 digits: keep as-is (partial phone fragment)
|
||
$v[] = $digits;
|
||
}
|
||
}
|
||
if (!empty($v)) {
|
||
$phoneMap[$t] = array_values(array_unique($v));
|
||
}
|
||
}
|
||
|
||
// Helper: ( token1 AND token2 AND ... ), each token may match ANY of $columns,
|
||
// and phone variants are only applied to the provided $phoneCols for THIS table.
|
||
$applyMultiTokenLike = function ($qb, array $columns, array $tokens, array $phoneCols = []) use ($phoneMap) {
|
||
foreach ($tokens as $t) {
|
||
$qb->groupStart(); // OR across columns for this token
|
||
foreach ($columns as $i => $col) {
|
||
if ($i === 0) {
|
||
$qb->like($col, $t);
|
||
} else {
|
||
$qb->orLike($col, $t);
|
||
}
|
||
}
|
||
if (!empty($phoneMap[$t]) && !empty($phoneCols)) {
|
||
foreach ($phoneMap[$t] as $pv) {
|
||
foreach ($phoneCols as $pcol) {
|
||
$qb->orLike($pcol, $pv);
|
||
}
|
||
}
|
||
}
|
||
$qb->groupEnd();
|
||
}
|
||
return $qb;
|
||
};
|
||
|
||
// ===== RAW UNscoped searches (flat arrays) =====
|
||
|
||
// USERS (phone col: cellphone)
|
||
$uCols = ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state'];
|
||
$uQB = $db->table('users')
|
||
->select('id, firstname, lastname, email, cellphone, school_id, city, state, school_year, semester');
|
||
$applyMultiTokenLike($uQB, $uCols, $tokens, ['cellphone']);
|
||
$users = $uQB->limit(150)->get()->getResultArray();
|
||
|
||
// STUDENTS (no phone column to search)
|
||
$sCols = ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender'];
|
||
$sQB = $db->table('students')
|
||
->select('id, parent_id, school_id, firstname, lastname, dob, gender, school_year, semester, rfid_tag');
|
||
$applyMultiTokenLike($sQB, $sCols, $tokens, []);
|
||
$students = $sQB->limit(150)->get()->getResultArray();
|
||
|
||
// PARENTS (phone col: secondparent_phone)
|
||
$pCols = ['secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone'];
|
||
$pQB = $db->table('parents')
|
||
->select('id, firstparent_id, secondparent_firstname, secondparent_lastname, secondparent_email, secondparent_phone, school_year, semester');
|
||
$applyMultiTokenLike($pQB, $pCols, $tokens, ['secondparent_phone']);
|
||
foreach ($tokens as $t) {
|
||
if (ctype_digit($t)) {
|
||
$pQB->orWhere('firstparent_id', (int) $t)->orWhere('id', (int) $t);
|
||
}
|
||
}
|
||
$parents = $pQB->limit(150)->get()->getResultArray();
|
||
|
||
// STAFF (phone col: phone)
|
||
$stCols = ['firstname', 'lastname', 'email', 'role_name', 'phone'];
|
||
$stQB = $db->table('staff')
|
||
->select('id, user_id, firstname, lastname, email, phone, role_name, school_year, active_role');
|
||
$applyMultiTokenLike($stQB, $stCols, $tokens, ['phone']);
|
||
$staff = $stQB->limit(150)->get()->getResultArray();
|
||
|
||
// EMERGENCY CONTACTS (phone col: cellphone)
|
||
$ecCols = ['emergency_contact_name', 'relation', 'email', 'cellphone'];
|
||
$ecQB = $db->table('emergency_contacts')
|
||
->select('id, parent_id, emergency_contact_name, relation, cellphone, email, school_year, semester');
|
||
$applyMultiTokenLike($ecQB, $ecCols, $tokens, ['cellphone']);
|
||
$emergency = $ecQB->limit(150)->get()->getResultArray();
|
||
|
||
$raw = [
|
||
'users' => $users,
|
||
'students' => $students,
|
||
'parents' => $parents,
|
||
'staff' => $staff,
|
||
'emergency_contacts' => $emergency,
|
||
];
|
||
|
||
$total = count($users) + count($students) + count($parents) + count($staff) + count($emergency);
|
||
|
||
return [
|
||
'query' => $q,
|
||
'results' => $raw,
|
||
'scope_used' => 'unscoped-raw',
|
||
'scope_label' => 'all years/semesters (raw, tokenized)',
|
||
'total_found' => $total,
|
||
];
|
||
}
|
||
|
||
public function teachers()
|
||
{
|
||
return view('administrator/teachers'); // This is the correct view path
|
||
}
|
||
|
||
public function classes()
|
||
{
|
||
return view('administrator/classes');
|
||
}
|
||
|
||
public function subjects()
|
||
{
|
||
return view('administrator/subjects');
|
||
}
|
||
|
||
public function reports()
|
||
{
|
||
return view('administrator/reports');
|
||
}
|
||
|
||
public function teacherDashboard()
|
||
{
|
||
return view('teacher/dashboard');
|
||
}
|
||
|
||
public function parentDashboard()
|
||
{
|
||
return view('parent/dashboard');
|
||
}
|
||
|
||
public function CourseMaterials()
|
||
{
|
||
return view('administrator/course_materials');
|
||
}
|
||
|
||
public function administratorProfiles()
|
||
{
|
||
return view('administrator/administrator_profiles');
|
||
}
|
||
|
||
public function payrollManagement()
|
||
{
|
||
return view('administrator/payroll_management');
|
||
}
|
||
|
||
public function administratorAttendanceRecords()
|
||
{
|
||
return view('administrator/administrator_attendance_records');
|
||
}
|
||
|
||
public function performanceReviews()
|
||
{
|
||
return view('administrator/performance_reviews');
|
||
}
|
||
|
||
public function communicationLogs()
|
||
{
|
||
return view('administrator/communication_logs');
|
||
}
|
||
|
||
public function feePaymentRecords()
|
||
{
|
||
return view('administrator/fee_payment_records');
|
||
}
|
||
|
||
public function feedbackComplaints()
|
||
{
|
||
return view('administrator/feedback_complaints');
|
||
}
|
||
|
||
public function teacherSubmissionsReport()
|
||
{
|
||
$semester = (string)($this->configModel->getConfig('semester') ?? $this->semester ?? '');
|
||
$schoolYear = (string)($this->configModel->getConfig('school_year') ?? $this->schoolYear ?? '');
|
||
$semesterResolver = new SemesterRangeService($this->configModel);
|
||
$semesterNorm = $semesterResolver->normalizeSemester($semester);
|
||
$semesterFilter = $semesterNorm !== '' ? $semesterNorm : $semester;
|
||
$semesterCandidates = $this->buildSemesterCandidates($semesterFilter);
|
||
$lowProgressRaw = (string) $this->request->getGet('low_progress_sections');
|
||
$lowProgressSectionIds = array_values(array_unique(array_filter(array_map(
|
||
'intval',
|
||
preg_split('/\s*,\s*/', $lowProgressRaw, -1, PREG_SPLIT_NO_EMPTY)
|
||
))));
|
||
|
||
$scoreComments = new ScoreCommentModel();
|
||
$semesterScores = new SemesterScoreModel();
|
||
$attendanceDays = new AttendanceDayModel();
|
||
$examDrafts = new ExamDraftModel();
|
||
$homeworkModel = new HomeworkModel();
|
||
$historyModel = new TeacherSubmissionNotificationHistoryModel();
|
||
|
||
$assignmentQuery = $this->db->table('teacher_class tc')
|
||
->select([
|
||
'tc.class_section_id',
|
||
'cs.class_section_name',
|
||
'tc.teacher_id',
|
||
'u.firstname',
|
||
'u.lastname',
|
||
'tc.position',
|
||
])
|
||
->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left')
|
||
->join('users u', 'u.id = tc.teacher_id', 'left')
|
||
->orderBy('cs.class_section_name', 'ASC');
|
||
|
||
$filteredQuery = clone $assignmentQuery;
|
||
if ($schoolYear !== '') {
|
||
$filteredQuery = $filteredQuery->where('tc.school_year', $schoolYear);
|
||
}
|
||
if (!empty($semesterCandidates)) {
|
||
$filteredQuery = $filteredQuery->whereIn('tc.semester', $semesterCandidates);
|
||
}
|
||
|
||
$assignmentRows = $filteredQuery->get()->getResultArray();
|
||
if (empty($assignmentRows) && ($schoolYear !== '' || $semester !== '')) {
|
||
$assignmentRows = $assignmentQuery->get()->getResultArray();
|
||
}
|
||
|
||
$studentCounts = $this->studentClassModel->getStudentCountsBySection($schoolYear !== '' ? $schoolYear : null);
|
||
$sectionRows = $this->classSectionModel
|
||
->select('class_section_id, class_section_name')
|
||
->orderBy('class_section_name', 'ASC')
|
||
->findAll();
|
||
$sectionMap = [];
|
||
foreach ($sectionRows as $sectionRow) {
|
||
$sectionId = (int) ($sectionRow['class_section_id'] ?? 0);
|
||
if ($sectionId <= 0) {
|
||
continue;
|
||
}
|
||
if (empty($studentCounts[$sectionId])) {
|
||
continue;
|
||
}
|
||
$sectionMap[$sectionId] = $sectionRow['class_section_name'] ?? "Section {$sectionId}";
|
||
}
|
||
$sectionIds = array_keys($sectionMap);
|
||
|
||
[$progressExpectedWeeks, $progressSubmittedBySection] = $this->buildClassProgressStats($sectionIds);
|
||
$examDraftCounts = [];
|
||
$examDraftDeadline = $this->resolveTeacherDashboardExamDraftDeadline($semester, $schoolYear);
|
||
$examDraftDeadlineConfig = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? ''));
|
||
$examDraftDeadlineFormatted = '';
|
||
if ($examDraftDeadlineConfig !== '') {
|
||
$parsedUi = $this->parseExamDraftDeadlineConfigValue();
|
||
$examDraftDeadlineFormatted = $parsedUi !== null ? $parsedUi->format('M j, Y') : '';
|
||
}
|
||
$homeworkCounts = [];
|
||
if (! empty($sectionIds)) {
|
||
$draftBuilder = $examDrafts
|
||
->select('class_section_id')
|
||
->whereIn('class_section_id', $sectionIds);
|
||
if ($schoolYear !== '') {
|
||
$draftBuilder->where('school_year', $schoolYear);
|
||
}
|
||
if (!empty($semesterCandidates)) {
|
||
$draftBuilder->whereIn('semester', $semesterCandidates);
|
||
}
|
||
if ($this->db->fieldExists('is_legacy', 'exam_drafts')) {
|
||
$draftBuilder->where('is_legacy', 0);
|
||
}
|
||
$draftRows = $draftBuilder->findAll();
|
||
foreach ($draftRows as $draft) {
|
||
$sectionId = (int) ($draft['class_section_id'] ?? 0);
|
||
if ($sectionId <= 0) {
|
||
continue;
|
||
}
|
||
$examDraftCounts[$sectionId] = ($examDraftCounts[$sectionId] ?? 0) + 1;
|
||
}
|
||
|
||
$homeworkBuilder = $homeworkModel
|
||
->select('class_section_id, homework_index')
|
||
->whereIn('class_section_id', $sectionIds);
|
||
if ($schoolYear !== '') {
|
||
$homeworkBuilder->where('school_year', $schoolYear);
|
||
}
|
||
if (!empty($semesterCandidates)) {
|
||
$homeworkBuilder->whereIn('semester', $semesterCandidates);
|
||
}
|
||
$homeworkRows = $homeworkBuilder
|
||
->where('score IS NOT NULL', null, false)
|
||
->where('score !=', '')
|
||
->groupBy('class_section_id, homework_index')
|
||
->findAll();
|
||
foreach ($homeworkRows as $row) {
|
||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||
if ($sectionId <= 0) {
|
||
continue;
|
||
}
|
||
$homeworkCounts[$sectionId] = ($homeworkCounts[$sectionId] ?? 0) + 1;
|
||
}
|
||
}
|
||
|
||
if (empty($lowProgressSectionIds)) {
|
||
$lowProgressSectionIds = $this->resolveLowProgressSectionIds($sectionIds);
|
||
}
|
||
|
||
$teachersBySection = [];
|
||
foreach ($assignmentRows as $assignment) {
|
||
$sectionId = (int)($assignment['class_section_id'] ?? 0);
|
||
if ($sectionId <= 0) {
|
||
continue;
|
||
}
|
||
|
||
$positionKey = strtolower(trim((string)($assignment['position'] ?? '')));
|
||
$roleKey = $positionKey !== '' ? $positionKey : 'teacher';
|
||
$positionLabel = match ($roleKey) {
|
||
'ta' => 'TA',
|
||
'main' => 'Main',
|
||
default => $roleKey !== '' ? ucfirst($roleKey) : 'Teacher',
|
||
};
|
||
|
||
$teacherFullName = trim(($assignment['firstname'] ?? '') . ' ' . ($assignment['lastname'] ?? ''));
|
||
$teacherId = (int)($assignment['teacher_id'] ?? 0);
|
||
if ($teacherFullName === '' || $teacherId <= 0) {
|
||
continue;
|
||
}
|
||
|
||
$entry = &$teachersBySection[$sectionId];
|
||
if (!isset($entry)) {
|
||
$entry = [
|
||
'class_section' => $assignment['class_section_name'] ?? ($sectionMap[$sectionId] ?? "Section {$sectionId}"),
|
||
'teachers' => [],
|
||
];
|
||
}
|
||
|
||
$entry['teachers'][] = [
|
||
'id' => $teacherId,
|
||
'label' => "{$positionLabel}: {$teacherFullName}",
|
||
'role_key' => $roleKey,
|
||
];
|
||
unset($entry);
|
||
}
|
||
|
||
$today = (new \DateTimeImmutable('now', new \DateTimeZone(date_default_timezone_get() ?: 'UTC')))->format('Y-m-d');
|
||
|
||
$rows = [];
|
||
$totalStatuses = 0;
|
||
$missingItemCount = 0;
|
||
$allTeacherIds = [];
|
||
$allClassSectionIds = [];
|
||
$examTerm = $this->resolveExamTermLabel($semester);
|
||
$examScoreField = $examTerm === 'final' ? 'final_exam_score' : 'midterm_exam_score';
|
||
|
||
foreach ($sectionMap as $classSectionId => $sectionName) {
|
||
$classSectionId = (int)$classSectionId;
|
||
if ($classSectionId <= 0) {
|
||
continue;
|
||
}
|
||
|
||
$studentQuery = $this->studentClassModel
|
||
->select('student_id')
|
||
->where('class_section_id', $classSectionId)
|
||
->where('school_year', $schoolYear);
|
||
if (!empty($semesterCandidates)) {
|
||
$studentQuery->whereIn('semester', $semesterCandidates);
|
||
}
|
||
$studentEntries = $studentQuery->findAll();
|
||
if (empty($studentEntries)) {
|
||
$studentEntries = $this->studentClassModel
|
||
->select('student_id')
|
||
->where('class_section_id', $classSectionId)
|
||
->where('school_year', $schoolYear)
|
||
->findAll();
|
||
}
|
||
$studentIds = array_filter(array_map(static fn($entry) => (int)($entry['student_id'] ?? 0), $studentEntries));
|
||
$expected = count($studentIds);
|
||
|
||
$midtermStudents = [];
|
||
$participationStudents = [];
|
||
if ($classSectionId > 0) {
|
||
$scoreQuery = $semesterScores
|
||
->where('class_section_id', $classSectionId)
|
||
->where('school_year', $schoolYear);
|
||
if (!empty($semesterCandidates)) {
|
||
$scoreQuery->whereIn('semester', $semesterCandidates);
|
||
}
|
||
$scoreRecords = $scoreQuery->findAll();
|
||
foreach ($scoreRecords as $score) {
|
||
$sid = (int)($score['student_id'] ?? 0);
|
||
if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) {
|
||
continue;
|
||
}
|
||
$midtermValue = trim((string)($score[$examScoreField] ?? ''));
|
||
if ($midtermValue !== '') {
|
||
$midtermStudents[$sid] = true;
|
||
}
|
||
$participationValue = trim((string)($score['participation_score'] ?? ''));
|
||
if ($participationValue !== '') {
|
||
$participationStudents[$sid] = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
$midtermCommentStudents = [];
|
||
$ptapCommentStudents = [];
|
||
if (!empty($studentIds)) {
|
||
$commentQuery = $scoreComments
|
||
->select('student_id, score_type, comment')
|
||
->whereIn('student_id', $studentIds)
|
||
->where('school_year', $schoolYear)
|
||
->whereIn('score_type', [$examTerm, 'ptap']);
|
||
if (!empty($semesterCandidates)) {
|
||
$commentQuery->whereIn('semester', $semesterCandidates);
|
||
}
|
||
$comments = $commentQuery->findAll();
|
||
foreach ($comments as $comment) {
|
||
$sid = (int)($comment['student_id'] ?? 0);
|
||
if ($sid <= 0) {
|
||
continue;
|
||
}
|
||
$text = trim((string)($comment['comment'] ?? ''));
|
||
if ($text === '') {
|
||
continue;
|
||
}
|
||
$type = strtolower(trim((string)($comment['score_type'] ?? '')));
|
||
if ($type === $examTerm) {
|
||
$midtermCommentStudents[$sid] = true;
|
||
}
|
||
if ($type === 'ptap') {
|
||
$ptapCommentStudents[$sid] = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
$attendanceQuery = $attendanceDays
|
||
->where('class_section_id', $classSectionId)
|
||
->where('school_year', $schoolYear)
|
||
->where('date', $today);
|
||
if (!empty($semesterCandidates)) {
|
||
$attendanceQuery->whereIn('semester', $semesterCandidates);
|
||
}
|
||
$attendanceRow = $attendanceQuery->first();
|
||
$attendanceSubmitted = $attendanceRow && in_array(strtolower((string)($attendanceRow['status'] ?? '')), ['submitted', 'published', 'finalized'], true);
|
||
|
||
$section = $teachersBySection[$classSectionId] ?? ['teachers' => []];
|
||
$teacherList = $section['teachers'] ?? [];
|
||
if (!empty($teacherList)) {
|
||
usort($teacherList, function ($a, $b) {
|
||
return $this->teacherRolePriority($a['role_key'] ?? 'teacher') <=> $this->teacherRolePriority($b['role_key'] ?? 'teacher');
|
||
});
|
||
$teacherList = array_values($teacherList);
|
||
}
|
||
|
||
foreach ($teacherList as $teacherEntry) {
|
||
if (!empty($teacherEntry['id'])) {
|
||
$allTeacherIds[] = $teacherEntry['id'];
|
||
}
|
||
}
|
||
$allClassSectionIds[] = $classSectionId;
|
||
|
||
$midtermScoreStatus = $this->submissionStatus(count($midtermStudents), $expected);
|
||
$midtermCommentStatus = $this->submissionStatus(count($midtermCommentStudents), $expected);
|
||
$participationStatus = $this->submissionStatus(count($participationStudents), $expected);
|
||
$ptapCommentStatus = $this->submissionStatus(count($ptapCommentStudents), $expected);
|
||
$attendanceStatus = $this->attendanceStatus($attendanceSubmitted);
|
||
$progressSubmitted = (int) ($progressSubmittedBySection[$classSectionId] ?? 0);
|
||
$classProgressStatus = $this->progressStatus($progressSubmitted, $progressExpectedWeeks);
|
||
$draftSubmitted = (int) ($examDraftCounts[$classSectionId] ?? 0);
|
||
$examDraftStatus = $this->draftStatus($draftSubmitted, $examDraftDeadline);
|
||
$homeworkSubmitted = (int) ($homeworkCounts[$classSectionId] ?? 0);
|
||
$homeworkStatus = $this->homeworkStatus($homeworkSubmitted);
|
||
$statusDetails = [
|
||
'midterm_score_status' => $midtermScoreStatus,
|
||
'midterm_comment_status' => $midtermCommentStatus,
|
||
'participation_status' => $participationStatus,
|
||
'ptap_comment_status' => $ptapCommentStatus,
|
||
'class_progress_status' => $classProgressStatus,
|
||
'exam_draft_status' => $examDraftStatus,
|
||
'homework_status' => $homeworkStatus,
|
||
];
|
||
$missingItemsForSection = $this->buildMissingItems($statusDetails, $semester);
|
||
$missingItemCount += count($missingItemsForSection);
|
||
$totalStatuses += count($statusDetails);
|
||
|
||
$rows[] = [
|
||
'class_section' => $sectionMap[$classSectionId] ?? ($section['class_section'] ?? "Section {$classSectionId}"),
|
||
'class_section_id' => $classSectionId,
|
||
'teachers' => $teacherList,
|
||
'midterm_score_status' => $midtermScoreStatus,
|
||
'midterm_comment_status' => $midtermCommentStatus,
|
||
'participation_status' => $participationStatus,
|
||
'ptap_comment_status' => $ptapCommentStatus,
|
||
'attendance_status' => $attendanceStatus,
|
||
'class_progress_status' => $classProgressStatus,
|
||
'exam_draft_status' => $examDraftStatus,
|
||
'homework_status' => $homeworkStatus,
|
||
'missing_items' => $missingItemsForSection,
|
||
'student_count' => $expected,
|
||
];
|
||
}
|
||
|
||
$historyMap = [];
|
||
$teacherIds = array_values(array_unique($allTeacherIds));
|
||
$classSectionIds = array_values(array_unique($allClassSectionIds));
|
||
if (!empty($teacherIds) && !empty($classSectionIds)) {
|
||
$historyRecords = $historyModel
|
||
->select('teacher_submission_notification_history.*, u.firstname, u.lastname')
|
||
->join('users u', 'u.id = teacher_submission_notification_history.admin_id', 'left')
|
||
->where('notification_category', 'teacher_submissions')
|
||
->whereIn('teacher_submission_notification_history.teacher_id', $teacherIds)
|
||
->whereIn('teacher_submission_notification_history.class_section_id', $classSectionIds)
|
||
->orderBy('sent_at', 'DESC')
|
||
->findAll();
|
||
|
||
foreach ($historyRecords as $record) {
|
||
$sectionId = (int)($record['class_section_id'] ?? 0);
|
||
$teacherId = (int)($record['teacher_id'] ?? 0);
|
||
if ($sectionId <= 0 || $teacherId <= 0) {
|
||
continue;
|
||
}
|
||
$sentAt = $record['sent_at'] ?? null;
|
||
$sentAtText = $sentAt ? local_datetime($sentAt, 'M j, Y g:i A') : '';
|
||
$adminName = trim(($record['firstname'] ?? '') . ' ' . ($record['lastname'] ?? ''));
|
||
if ($adminName === '') {
|
||
$adminName = 'Administrator';
|
||
}
|
||
$historyMap[$sectionId][$teacherId][] = [
|
||
'sent_at_text' => $sentAtText,
|
||
'admin_name' => $adminName,
|
||
'status' => strtolower((string)($record['status'] ?? 'sent')),
|
||
];
|
||
}
|
||
|
||
foreach ($historyMap as &$teachersHistory) {
|
||
foreach ($teachersHistory as &$entries) {
|
||
$entries = array_slice($entries, 0, 3);
|
||
}
|
||
unset($entries);
|
||
}
|
||
unset($teachersHistory);
|
||
}
|
||
|
||
$summary = [
|
||
'total_items' => $totalStatuses,
|
||
'missing_items' => $missingItemCount,
|
||
'submitted_items' => max(0, $totalStatuses - $missingItemCount),
|
||
'submission_percentage' => $totalStatuses > 0
|
||
? (int)round((($totalStatuses - $missingItemCount) / $totalStatuses) * 100)
|
||
: 100,
|
||
];
|
||
|
||
return view('administrator/teacher_submissions', [
|
||
'rows' => $rows,
|
||
'semester' => $semester,
|
||
'schoolYear' => $schoolYear,
|
||
'notificationHistory' => $historyMap,
|
||
'summary' => $summary,
|
||
'lowProgressSectionIds' => $lowProgressSectionIds,
|
||
'examDraftDeadlineConfig' => $examDraftDeadlineConfig,
|
||
'examDraftDeadlineFormatted' => $examDraftDeadlineFormatted,
|
||
]);
|
||
}
|
||
|
||
private function resolveLowProgressSectionIds(array $sectionIds): array
|
||
{
|
||
[$expectedWeeks, $submittedBySection] = $this->buildClassProgressStats($sectionIds);
|
||
if ($expectedWeeks <= 0) {
|
||
return [];
|
||
}
|
||
|
||
$lowProgressSectionIds = [];
|
||
foreach ($sectionIds as $sectionId) {
|
||
$submitted = (int) ($submittedBySection[$sectionId] ?? 0);
|
||
$percent = ($submitted / $expectedWeeks) * 100;
|
||
if ($percent < 50) {
|
||
$lowProgressSectionIds[] = $sectionId;
|
||
}
|
||
}
|
||
|
||
return $lowProgressSectionIds;
|
||
}
|
||
|
||
private function buildClassProgressStats(array $sectionIds): array
|
||
{
|
||
$sectionIds = array_values(array_unique(array_filter(array_map('intval', $sectionIds))));
|
||
if (empty($sectionIds)) {
|
||
return [0, []];
|
||
}
|
||
|
||
$semesterResolver = new SemesterRangeService($this->configModel);
|
||
$schoolYear = (string)($this->configModel->getConfig('school_year') ?? '');
|
||
$semester = (string)($this->configModel->getConfig('semester') ?? '');
|
||
$schoolYearForRange = $schoolYear !== '' ? $schoolYear : (string)($this->configModel->getConfig('school_year') ?? '');
|
||
[$rangeStart, $rangeEnd] = $semesterResolver->getSchoolYearRange($schoolYearForRange);
|
||
$semesterNorm = $semesterResolver->normalizeSemester($semester);
|
||
if ($semesterNorm !== '' && $schoolYearForRange !== '') {
|
||
$semRange = $semesterResolver->getSemesterRange($schoolYearForRange, $semesterNorm);
|
||
if ($semRange) {
|
||
[$rangeStart, $rangeEnd] = $semRange;
|
||
}
|
||
}
|
||
|
||
$dateList = [];
|
||
try {
|
||
$start = new \DateTimeImmutable($rangeStart);
|
||
$end = new \DateTimeImmutable($rangeEnd);
|
||
$cursor = $start;
|
||
$w = (int) $cursor->format('w');
|
||
if ($w !== 0) {
|
||
$cursor = $cursor->modify('next sunday');
|
||
}
|
||
while ($cursor <= $end) {
|
||
$dateList[] = $cursor->format('Y-m-d');
|
||
$cursor = $cursor->modify('+7 days');
|
||
}
|
||
} catch (\Throwable $e) {
|
||
$dateList = [];
|
||
}
|
||
|
||
$noSchoolDays = [];
|
||
$events = [];
|
||
try {
|
||
$calendarModel = new \App\Models\CalendarModel();
|
||
$events = $calendarModel->getEvents();
|
||
} catch (\Throwable $e) {
|
||
$events = [];
|
||
}
|
||
foreach ($events as $event) {
|
||
$d = substr((string) ($event['date'] ?? ''), 0, 10);
|
||
if ($d === '' || empty($event['no_school'])) {
|
||
continue;
|
||
}
|
||
if ($d < $rangeStart || $d > $rangeEnd) {
|
||
continue;
|
||
}
|
||
$eventYear = trim((string) ($event['school_year'] ?? ''));
|
||
if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) {
|
||
continue;
|
||
}
|
||
$noSchoolDays[$d] = true;
|
||
}
|
||
|
||
$anchorSundayYmd = '';
|
||
try {
|
||
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
|
||
$tzObj = new \DateTimeZone($tzName ?: 'UTC');
|
||
} catch (\Throwable $e) {
|
||
try {
|
||
$tzObj = new \DateTimeZone(user_timezone() ?: 'UTC');
|
||
} catch (\Throwable $e2) {
|
||
$tzObj = new \DateTimeZone('UTC');
|
||
}
|
||
}
|
||
try {
|
||
$nowDate = new \DateTime('now', $tzObj);
|
||
} catch (\Throwable $e) {
|
||
$nowDate = new \DateTime('now');
|
||
}
|
||
$weekday = (int) $nowDate->format('w');
|
||
$anchorSundayYmd = $weekday === 0
|
||
? $nowDate->format('Y-m-d')
|
||
: $nowDate->modify('next sunday')->format('Y-m-d');
|
||
|
||
$activeDatesSet = [];
|
||
if (! empty($dateList) && $anchorSundayYmd !== '') {
|
||
foreach ($dateList as $d) {
|
||
if ($d <= $anchorSundayYmd && empty($noSchoolDays[$d])) {
|
||
$activeDatesSet[$d] = true;
|
||
}
|
||
}
|
||
}
|
||
$expectedWeeks = count($activeDatesSet);
|
||
if ($expectedWeeks === 0) {
|
||
return [0, []];
|
||
}
|
||
|
||
$builder = $this->db->table('class_progress_reports')
|
||
->select('class_section_id, week_start')
|
||
->whereIn('class_section_id', $sectionIds);
|
||
if (! empty($activeDatesSet)) {
|
||
$builder->whereIn('week_start', array_keys($activeDatesSet));
|
||
}
|
||
$rows = $builder->get()->getResultArray();
|
||
|
||
$submittedBySection = [];
|
||
foreach ($rows as $row) {
|
||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||
$weekStart = (string) ($row['week_start'] ?? '');
|
||
if ($sectionId === 0 || $weekStart === '' || empty($activeDatesSet[$weekStart])) {
|
||
continue;
|
||
}
|
||
$submittedBySection[$sectionId][$weekStart] = true;
|
||
}
|
||
|
||
$counts = [];
|
||
foreach ($sectionIds as $sectionId) {
|
||
$counts[$sectionId] = isset($submittedBySection[$sectionId])
|
||
? count($submittedBySection[$sectionId])
|
||
: 0;
|
||
}
|
||
|
||
return [$expectedWeeks, $counts];
|
||
}
|
||
|
||
public function sendTeacherSubmissionNotifications()
|
||
{$notify = $this->request->getPost('notify');
|
||
if (!is_array($notify)) {
|
||
return redirect()->back()->with('info', 'Select at least one teacher to notify.');
|
||
}
|
||
$semester = (string)($this->configModel->getConfig('semester') ?? $this->semester ?? '');
|
||
$missingItemsPayload = $this->request->getPost('missing_items') ?? [];
|
||
$homeworkNotifyAll = (bool) $this->request->getPost('homework_notify_all');
|
||
$examTerm = $this->resolveExamTermLabel($semester);
|
||
$examScoreLabel = $examTerm === 'final' ? 'final scores' : 'midterm scores';
|
||
$examCommentLabel = $examTerm === 'final' ? 'final comments' : 'midterm comments';
|
||
$forcedItems = [];
|
||
if ($this->request->getPost('notify_midterm_score')) {
|
||
$forcedItems[] = $examScoreLabel;
|
||
}
|
||
if ($this->request->getPost('notify_midterm_comment')) {
|
||
$forcedItems[] = $examCommentLabel;
|
||
}
|
||
if ($this->request->getPost('notify_participation')) {
|
||
$forcedItems[] = 'participation';
|
||
}
|
||
if ($this->request->getPost('notify_ptap_comment')) {
|
||
$forcedItems[] = 'PTAP comments';
|
||
}
|
||
if ($this->request->getPost('notify_class_progress')) {
|
||
$forcedItems[] = 'class progress';
|
||
}
|
||
if ($this->request->getPost('notify_exam_draft')) {
|
||
$forcedItems[] = 'exam draft';
|
||
}
|
||
|
||
$targets = [];
|
||
foreach ($notify as $sectionIdRaw => $teachers) {
|
||
$sectionId = (int)$sectionIdRaw;
|
||
if ($sectionId <= 0 || !is_array($teachers)) {
|
||
continue;
|
||
}
|
||
foreach ($teachers as $teacherIdRaw => $value) {
|
||
$teacherId = (int)$teacherIdRaw;
|
||
if ($teacherId <= 0 || $value === null || $value === '') {
|
||
continue;
|
||
}
|
||
$key = "{$sectionId}_{$teacherId}";
|
||
$targets[$key] = [
|
||
'class_section_id' => $sectionId,
|
||
'teacher_id' => $teacherId,
|
||
];
|
||
}
|
||
}
|
||
|
||
if (empty($targets)) {
|
||
return redirect()->back()->with('info', 'Select at least one teacher to notify.');
|
||
}
|
||
|
||
$targets = array_values($targets);
|
||
$teacherIds = array_values(array_unique(array_column($targets, 'teacher_id')));
|
||
$classSectionIds = array_values(array_unique(array_column($targets, 'class_section_id')));
|
||
|
||
$classSections = $this->classSectionModel
|
||
->select('class_section_id, class_section_name')
|
||
->whereIn('class_section_id', $classSectionIds)
|
||
->findAll();
|
||
$classSectionMap = [];
|
||
foreach ($classSections as $section) {
|
||
$classSectionMap[(int)($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? '';
|
||
}
|
||
|
||
$teachers = $this->userModel
|
||
->select('id, firstname, lastname, email')
|
||
->whereIn('id', $teacherIds)
|
||
->findAll();
|
||
$teacherLookup = [];
|
||
foreach ($teachers as $teacher) {
|
||
$teacherLookup[(int)$teacher['id']] = $teacher;
|
||
}
|
||
|
||
$mailer = new EmailController();
|
||
$adminId = (int)(session()->get('user_id') ?? 0);
|
||
if ($adminId <= 0) {
|
||
return redirect()->to('/login');
|
||
}
|
||
$adminUser = $this->userModel->find($adminId);
|
||
$adminName = trim(($adminUser['firstname'] ?? '') . ' ' . ($adminUser['lastname'] ?? '')) ?: 'Administrator';
|
||
|
||
$historyModel = new TeacherSubmissionNotificationHistoryModel();
|
||
$scoreUrl = site_url('/');
|
||
$progressUrl = site_url('teacher/progress/history');
|
||
$examDraftUrl = site_url('teacher/exam-drafts');
|
||
$homeworkUrl = site_url('teacher/addHomework');
|
||
$examDraftDeadlineEmailHtml = $this->buildExamDraftDeadlineEmailHtml();
|
||
$sentCount = 0;
|
||
$failCount = 0;
|
||
|
||
foreach ($targets as $target) {
|
||
$classSectionId = (int)$target['class_section_id'];
|
||
$teacherId = (int)$target['teacher_id'];
|
||
$teacher = $teacherLookup[$teacherId] ?? null;
|
||
$sectionName = $classSectionMap[$classSectionId] ?? "Section {$classSectionId}";
|
||
$teacherName = $teacher ? trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')) : '';
|
||
if ($teacherName === '') {
|
||
$teacherName = 'Teacher';
|
||
}
|
||
$subject = "Reminder: Complete submissions for {$sectionName}";
|
||
$missingPayload = $missingItemsPayload[$classSectionId][$teacherId] ?? '';
|
||
$missingItems = $this->parseMissingItemsPayload((string)$missingPayload);
|
||
$selectedItems = $forcedItems;
|
||
if ($homeworkNotifyAll && !in_array('homework', $selectedItems, true)) {
|
||
$selectedItems[] = 'homework';
|
||
}
|
||
if (!empty($selectedItems)) {
|
||
$missingItems = array_values(array_unique($selectedItems));
|
||
}
|
||
if (!empty($missingItems)) {
|
||
$missingText = htmlspecialchars(
|
||
$this->formatMissingItemsText($missingItems),
|
||
ENT_QUOTES,
|
||
'UTF-8'
|
||
);
|
||
$missingNote = "<p>Outstanding items: {$missingText}.</p>";
|
||
} else {
|
||
$missingNote = "<p>Our records show no outstanding submissions for this section, but please verify if anything still needs attention.</p>";
|
||
}
|
||
|
||
$subject = "Reminder: Complete submissions for {$sectionName}";
|
||
$progressNote = '';
|
||
if (in_array('class progress', $missingItems, true)) {
|
||
$progressNote = "<p>Class progress submissions can be updated at <a href=\"{$progressUrl}\">Teacher Progress History</a>.</p>";
|
||
}
|
||
$examDraftNote = '';
|
||
if (in_array('exam draft', $missingItems, true)) {
|
||
$semesterLabel = strtolower(trim((string) $semester));
|
||
if ($semesterLabel === 'fall') {
|
||
$draftLabel = 'midterm exam draft';
|
||
} elseif ($semesterLabel === 'spring') {
|
||
$draftLabel = 'final exam draft';
|
||
} else {
|
||
$draftLabel = 'exam draft';
|
||
}
|
||
$examDraftNote = "<p>" . ucfirst($draftLabel) . " submissions can be updated at <a href=\"{$examDraftUrl}\">Teacher Exam Drafts</a>.</p>"
|
||
. $examDraftDeadlineEmailHtml;
|
||
}
|
||
$homeworkNote = '';
|
||
if (in_array('homework', $missingItems, true)) {
|
||
$homeworkNote = "<p>Homework scores can be submitted at <a href=\"{$homeworkUrl}\">Teacher Homework</a>.</p>";
|
||
}
|
||
$hasScoreItems = (bool) array_intersect($missingItems, [
|
||
'midterm scores',
|
||
'midterm comments',
|
||
'final scores',
|
||
'final comments',
|
||
'participation',
|
||
'PTAP comments',
|
||
'homework',
|
||
]);
|
||
$nonScoreOnly = ! empty($missingItems) && ! $hasScoreItems;
|
||
$body = "<p>Dear {$teacherName},</p>"
|
||
. "<p>Administration is gently reminding you to wrap up any remaining "
|
||
. ($nonScoreOnly ? "submissions for {$sectionName}." : "score submissions, comments, and related items for {$sectionName}.")
|
||
. "</p>"
|
||
. $missingNote
|
||
. $progressNote
|
||
. $examDraftNote
|
||
. $homeworkNote
|
||
. ($nonScoreOnly ? '' : "<p>Visit <a href=\"{$scoreUrl}\">Teacher Score Submission</a> to address any remaining items.</p>")
|
||
. "<p>Thank you,<br>Al Rahma Administration</p>";
|
||
|
||
$email = $teacher['email'] ?? '';
|
||
$status = 'failed';
|
||
if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||
$ok = $mailer->sendEmail($email, $subject, $body, 'notifications');
|
||
$status = $ok ? 'sent' : 'failed';
|
||
}
|
||
|
||
if ($status === 'sent') {
|
||
$sentCount++;
|
||
} else {
|
||
$failCount++;
|
||
}
|
||
|
||
$historyModel->insert([
|
||
'teacher_id' => $teacherId,
|
||
'class_section_id' => $classSectionId,
|
||
'admin_id' => $adminId,
|
||
'notification_category' => 'teacher_submissions',
|
||
'message' => $this->truncateNotificationMessage($body),
|
||
'status' => $status,
|
||
'school_year' => $this->schoolYear,
|
||
'semester' => $this->semester,
|
||
'sent_at' => utc_now(),
|
||
]);
|
||
}
|
||
|
||
$statusParts = [];
|
||
if ($sentCount > 0) {
|
||
$statusParts[] = $sentCount . ' reminder' . ($sentCount === 1 ? '' : 's') . ' sent';
|
||
}
|
||
if ($failCount > 0) {
|
||
$statusParts[] = $failCount . ' reminder' . ($failCount === 1 ? '' : 's') . ' failed';
|
||
}
|
||
|
||
$message = !empty($statusParts) ? implode(' and ', $statusParts) : 'No notifications were sent.';
|
||
$flashType = $failCount === 0 ? 'success' : 'warning';
|
||
|
||
return redirect()->back()->with($flashType, $message);
|
||
}
|
||
|
||
private function submissionStatus(int $filled, int $expected): array
|
||
{
|
||
if ($expected <= 0) {
|
||
return [
|
||
'label' => 'No students',
|
||
'badge' => 'bg-secondary',
|
||
'detail' => '',
|
||
'completed' => true,
|
||
];
|
||
}
|
||
$completed = $filled >= $expected;
|
||
return [
|
||
'label' => $completed ? 'Submitted' : 'Missing',
|
||
'badge' => $completed ? 'bg-success' : 'bg-danger',
|
||
'detail' => "{$filled}/{$expected}",
|
||
'completed' => $completed,
|
||
];
|
||
}
|
||
|
||
private function progressStatus(int $submitted, int $expected): array
|
||
{
|
||
if ($expected <= 0) {
|
||
return [
|
||
'label' => 'N/A',
|
||
'badge' => 'bg-secondary',
|
||
'detail' => '',
|
||
'completed' => true,
|
||
];
|
||
}
|
||
$completed = $submitted >= $expected;
|
||
return [
|
||
'label' => $completed ? 'Submitted' : 'Missing',
|
||
'badge' => $completed ? 'bg-success' : 'bg-danger',
|
||
'detail' => "{$submitted}/{$expected}",
|
||
'completed' => $completed,
|
||
];
|
||
}
|
||
|
||
private function homeworkStatus(int $submitted): array
|
||
{
|
||
$completed = $submitted > 0;
|
||
return [
|
||
'label' => $completed ? 'Submitted' : 'Missing',
|
||
'badge' => $completed ? 'bg-success' : 'bg-danger',
|
||
'detail' => $completed ? (string) $submitted : '0',
|
||
'completed' => $completed,
|
||
];
|
||
}
|
||
|
||
private function draftStatus(int $submitted, ?\DateTimeImmutable $deadline): array
|
||
{
|
||
if ($deadline !== null) {
|
||
$today = new \DateTimeImmutable('today');
|
||
if ($today < $deadline) {
|
||
return [
|
||
'label' => 'Pending',
|
||
'badge' => 'bg-secondary',
|
||
'detail' => 'Not due',
|
||
'completed' => true,
|
||
];
|
||
}
|
||
}
|
||
$completed = $submitted > 0;
|
||
return [
|
||
'label' => $completed ? 'Submitted' : 'Missing',
|
||
'badge' => $completed ? 'bg-success' : 'bg-danger',
|
||
'detail' => $completed ? (string) $submitted : '0',
|
||
'completed' => $completed,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Exam draft due date for the teacher submissions dashboard: prefers the configuration key
|
||
* `exam_draft_deadline` (same as automated reminders); otherwise fall/spring exam deadlines.
|
||
*/
|
||
private function resolveTeacherDashboardExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable
|
||
{
|
||
$fromExamDraftKey = $this->parseExamDraftDeadlineConfigValue();
|
||
if ($fromExamDraftKey !== null) {
|
||
return $fromExamDraftKey;
|
||
}
|
||
|
||
return $this->resolveExamDraftDeadline($semester, $schoolYear);
|
||
}
|
||
|
||
/**
|
||
* Parses the `exam_draft_deadline` configuration value using the application timezone (midnight that calendar day).
|
||
*/
|
||
private function parseExamDraftDeadlineConfigValue(): ?\DateTimeImmutable
|
||
{
|
||
$raw = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? ''));
|
||
if ($raw === '') {
|
||
return null;
|
||
}
|
||
$tz = new \DateTimeZone(config('App')->appTimezone ?? 'UTC');
|
||
try {
|
||
$deadline = new \DateTimeImmutable($raw, $tz);
|
||
} catch (\Throwable $e) {
|
||
return null;
|
||
}
|
||
|
||
return $deadline->setTime(0, 0, 0);
|
||
}
|
||
|
||
/**
|
||
* HTML snippet for reminder emails when exam draft is included (deadline from exam_draft_deadline config).
|
||
*/
|
||
private function buildExamDraftDeadlineEmailHtml(): string
|
||
{
|
||
$raw = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? ''));
|
||
if ($raw === '') {
|
||
return '';
|
||
}
|
||
$parsed = $this->parseExamDraftDeadlineConfigValue();
|
||
$display = $parsed !== null
|
||
? htmlspecialchars($parsed->format('l, F j, Y'), ENT_QUOTES, 'UTF-8')
|
||
: htmlspecialchars($raw, ENT_QUOTES, 'UTF-8');
|
||
$rawEsc = htmlspecialchars($raw, ENT_QUOTES, 'UTF-8');
|
||
|
||
return '<p><strong>Exam draft submission deadline</strong> (<code>exam_draft_deadline</code>): '
|
||
. "<strong>{$display}</strong>"
|
||
. ($parsed !== null && $rawEsc !== $display ? " <span style=\"color:#555;\">(configured value: {$rawEsc})</span>" : '')
|
||
. '.</p>';
|
||
}
|
||
|
||
private function resolveExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable
|
||
{
|
||
$semesterKey = strtolower(trim($semester));
|
||
if ($semesterKey === 'fall') {
|
||
$deadlineValue = (string)($this->configModel->getConfig('fall_exam_deadline') ?? '');
|
||
} elseif ($semesterKey === 'spring') {
|
||
$deadlineValue = (string)($this->configModel->getConfig('spring_exam_deadline') ?? '');
|
||
} else {
|
||
return null;
|
||
}
|
||
$deadlineValue = trim($deadlineValue);
|
||
if ($deadlineValue === '') {
|
||
return null;
|
||
}
|
||
try {
|
||
$deadline = new \DateTimeImmutable($deadlineValue);
|
||
} catch (\Throwable $e) {
|
||
return null;
|
||
}
|
||
if ($schoolYear !== '' && preg_match('/^\d{4}-\d{4}$/', $schoolYear)) {
|
||
$deadlineYear = $deadline->format('Y');
|
||
if ($deadlineYear === '1970') {
|
||
return null;
|
||
}
|
||
}
|
||
return $deadline->setTime(0, 0, 0);
|
||
}
|
||
|
||
private function resolveExamTermLabel(string $semester): string
|
||
{
|
||
$semesterKey = strtolower(trim($semester));
|
||
if ($semesterKey === '') {
|
||
return 'midterm';
|
||
}
|
||
if (str_contains($semesterKey, 'spring')) {
|
||
return 'final';
|
||
}
|
||
if (str_contains($semesterKey, 'fall')) {
|
||
return 'midterm';
|
||
}
|
||
return 'midterm';
|
||
}
|
||
|
||
private function buildSemesterCandidates(string $semester): array
|
||
{
|
||
$semester = trim((string) $semester);
|
||
if ($semester === '') {
|
||
return [];
|
||
}
|
||
$candidates = [
|
||
$semester,
|
||
strtolower($semester),
|
||
strtoupper($semester),
|
||
ucfirst(strtolower($semester)),
|
||
];
|
||
$candidates = array_values(array_unique(array_filter($candidates, static fn ($v) => $v !== '')));
|
||
return $candidates;
|
||
}
|
||
private function attendanceStatus(bool $submitted): array
|
||
{
|
||
return [
|
||
'label' => $submitted ? 'Submitted' : 'Missing',
|
||
'badge' => $submitted ? 'bg-success' : 'bg-danger',
|
||
'completed' => $submitted,
|
||
];
|
||
}
|
||
|
||
private function buildMissingItems(array $statusMap, string $semester): array
|
||
{
|
||
$examTerm = $this->resolveExamTermLabel($semester);
|
||
$examScoreLabel = $examTerm === 'final' ? 'final scores' : 'midterm scores';
|
||
$examCommentLabel = $examTerm === 'final' ? 'final comments' : 'midterm comments';
|
||
$labels = [
|
||
'midterm_score_status' => $examScoreLabel,
|
||
'midterm_comment_status' => $examCommentLabel,
|
||
'participation_status' => 'participation',
|
||
'ptap_comment_status' => 'PTAP comments',
|
||
'attendance_status' => 'attendance',
|
||
'class_progress_status' => 'class progress',
|
||
'exam_draft_status' => 'exam draft',
|
||
'homework_status' => 'homework',
|
||
];
|
||
|
||
$items = [];
|
||
foreach ($statusMap as $key => $status) {
|
||
$completed = $status['completed'] ?? true;
|
||
if (!$completed && isset($labels[$key])) {
|
||
$items[] = $labels[$key];
|
||
}
|
||
}
|
||
|
||
return array_values($items);
|
||
}
|
||
|
||
private function formatMissingItemsText(array $items): string
|
||
{
|
||
$items = array_values(array_filter(array_map('trim', $items), static fn($v) => $v !== ''));
|
||
$count = count($items);
|
||
if ($count === 0) {
|
||
return '';
|
||
}
|
||
if ($count === 1) {
|
||
return $items[0];
|
||
}
|
||
if ($count === 2) {
|
||
return $items[0] . ' and ' . $items[1];
|
||
}
|
||
$last = array_pop($items);
|
||
return implode(', ', $items) . ' and ' . $last;
|
||
}
|
||
|
||
private function teacherRolePriority(string $roleKey): int
|
||
{
|
||
switch (strtolower($roleKey)) {
|
||
case 'main':
|
||
return 1;
|
||
case 'ta':
|
||
return 2;
|
||
default:
|
||
return 3;
|
||
}
|
||
}
|
||
|
||
private function truncateNotificationMessage(string $html, int $limit = 1000): string
|
||
{
|
||
$text = trim(strip_tags($html));
|
||
if ($text === '') {
|
||
return '';
|
||
}
|
||
if (mb_strlen($text) <= $limit) {
|
||
return $text;
|
||
}
|
||
return mb_substr($text, 0, $limit) . '…';
|
||
}
|
||
|
||
private function parseMissingItemsPayload(string $payload): array
|
||
{
|
||
if ($payload === '') {
|
||
return [];
|
||
}
|
||
$decoded = @json_decode(base64_decode($payload, true) ?: '', true);
|
||
if (!is_array($decoded)) {
|
||
return [];
|
||
}
|
||
|
||
$items = [];
|
||
foreach ($decoded as $item) {
|
||
$item = trim((string)$item);
|
||
if ($item === '') {
|
||
continue;
|
||
}
|
||
$items[] = $item;
|
||
}
|
||
|
||
return array_values(array_unique($items));
|
||
}
|
||
|
||
public function notificationsAlerts()
|
||
{
|
||
if (!$this->canManageAdminNotifications()) {
|
||
return redirect()->to('/login');
|
||
}
|
||
|
||
$admins = $this->fetchAdminNotificationUsers();
|
||
$subjects = $this->notificationSubjectOptions();
|
||
$assignedSubjects = [];
|
||
$tableReady = $this->db->tableExists('admin_notification_subjects');
|
||
|
||
if ($tableReady && !empty($admins)) {
|
||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||
$rows = $this->adminNotificationSubjectModel
|
||
->select('id, admin_id, subject')
|
||
->whereIn('admin_id', $adminIds)
|
||
->findAll();
|
||
|
||
foreach ($rows as $row) {
|
||
$adminId = (int) ($row['admin_id'] ?? 0);
|
||
$subject = (string) ($row['subject'] ?? '');
|
||
if ($adminId <= 0 || $subject === '') {
|
||
continue;
|
||
}
|
||
if (!isset($assignedSubjects[$adminId])) {
|
||
$assignedSubjects[$adminId] = [];
|
||
}
|
||
$assignedSubjects[$adminId][$subject] = true;
|
||
}
|
||
}
|
||
|
||
return view('administrator/notifications_alerts', [
|
||
'admins' => $admins,
|
||
'subjects' => $subjects,
|
||
'assignedSubjects' => $assignedSubjects,
|
||
'tableReady' => $tableReady,
|
||
]);
|
||
}
|
||
|
||
public function saveNotificationSubjects()
|
||
{
|
||
if (!$this->canManageAdminNotifications()) {
|
||
return redirect()->to('/login');
|
||
}
|
||
|
||
if (!$this->db->tableExists('admin_notification_subjects')) {
|
||
return redirect()->to('/administrator/notifications_alerts')
|
||
->with('error', 'Notification subject storage is missing. Run migrations first.');
|
||
}
|
||
|
||
$posted = $this->request->getPost('subjects');
|
||
if (!is_array($posted)) {
|
||
return redirect()->to('/administrator/notifications_alerts')
|
||
->with('error', 'No selections were submitted.');
|
||
}
|
||
|
||
$subjects = $this->notificationSubjectOptions();
|
||
$allowed = array_keys($subjects);
|
||
|
||
$admins = $this->fetchAdminNotificationUsers();
|
||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||
if (empty($adminIds)) {
|
||
return redirect()->to('/administrator/notifications_alerts')
|
||
->with('info', 'No admins found to update.');
|
||
}
|
||
|
||
$existing = $this->adminNotificationSubjectModel
|
||
->select('id, admin_id, subject')
|
||
->whereIn('admin_id', $adminIds)
|
||
->findAll();
|
||
|
||
$existingMap = [];
|
||
foreach ($existing as $row) {
|
||
$adminId = (int) ($row['admin_id'] ?? 0);
|
||
$subject = (string) ($row['subject'] ?? '');
|
||
if ($adminId <= 0 || $subject === '') {
|
||
continue;
|
||
}
|
||
if (!isset($existingMap[$adminId])) {
|
||
$existingMap[$adminId] = [];
|
||
}
|
||
$existingMap[$adminId][$subject] = (int) ($row['id'] ?? 0);
|
||
}
|
||
|
||
$updates = 0;
|
||
|
||
foreach ($adminIds as $adminId) {
|
||
$subjectRaw = $posted[$adminId] ?? [];
|
||
|
||
$selected = [];
|
||
if (is_array($subjectRaw)) {
|
||
foreach ($subjectRaw as $key => $value) {
|
||
$candidate = is_string($key) ? $key : $value;
|
||
if (!is_string($candidate)) {
|
||
continue;
|
||
}
|
||
$candidate = trim($candidate);
|
||
if ($candidate !== '') {
|
||
$selected[] = $candidate;
|
||
}
|
||
}
|
||
}
|
||
|
||
$selected = array_values(array_unique(array_filter($selected, function ($value) use ($allowed) {
|
||
return in_array($value, $allowed, true);
|
||
})));
|
||
|
||
$current = array_keys($existingMap[$adminId] ?? []);
|
||
$toDelete = array_values(array_diff($current, $selected));
|
||
$toInsert = array_values(array_diff($selected, $current));
|
||
|
||
if (!empty($toDelete)) {
|
||
$this->adminNotificationSubjectModel
|
||
->where('admin_id', $adminId)
|
||
->whereIn('subject', $toDelete)
|
||
->delete();
|
||
$updates += count($toDelete);
|
||
}
|
||
|
||
if (!empty($toInsert)) {
|
||
$batch = [];
|
||
foreach ($toInsert as $subject) {
|
||
$batch[] = [
|
||
'admin_id' => $adminId,
|
||
'subject' => $subject,
|
||
];
|
||
}
|
||
$this->adminNotificationSubjectModel->insertBatch($batch);
|
||
$updates += count($toInsert);
|
||
}
|
||
}
|
||
|
||
return redirect()->to('/administrator/notifications_alerts')
|
||
->with('success', $updates > 0 ? 'Notification subjects updated.' : 'No changes were made.');
|
||
}
|
||
|
||
public function printNotificationRecipients()
|
||
{
|
||
if (!$this->canManageAdminNotifications()) {
|
||
return redirect()->to('/login');
|
||
}
|
||
|
||
$admins = $this->fetchAdminNotificationUsers();
|
||
$tableReady = $this->db->tableExists('admin_notification_subjects');
|
||
$assigned = [];
|
||
|
||
if ($tableReady && !empty($admins)) {
|
||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||
$rows = $this->adminNotificationSubjectModel
|
||
->select('admin_id')
|
||
->where('subject', 'print_requests')
|
||
->whereIn('admin_id', $adminIds)
|
||
->findAll();
|
||
|
||
foreach ($rows as $row) {
|
||
$adminId = (int) ($row['admin_id'] ?? 0);
|
||
if ($adminId <= 0) {
|
||
continue;
|
||
}
|
||
$assigned[$adminId] = true;
|
||
}
|
||
}
|
||
|
||
return view('administrator/print_notification_admins', [
|
||
'admins' => $admins,
|
||
'assigned' => $assigned,
|
||
'tableReady' => $tableReady,
|
||
]);
|
||
}
|
||
|
||
public function savePrintNotificationRecipients()
|
||
{
|
||
if (!$this->canManageAdminNotifications()) {
|
||
return redirect()->to('/login');
|
||
}
|
||
|
||
if (!$this->db->tableExists('admin_notification_subjects')) {
|
||
return redirect()->to('/administrator/print-notifications')
|
||
->with('error', 'Notification subject storage is missing. Run migrations first.');
|
||
}
|
||
|
||
$admins = $this->fetchAdminNotificationUsers();
|
||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||
if (empty($adminIds)) {
|
||
return redirect()->to('/administrator/print-notifications')
|
||
->with('info', 'No admins found to update.');
|
||
}
|
||
|
||
$posted = (array) $this->request->getPost('notify');
|
||
$selected = [];
|
||
foreach ($posted as $key => $value) {
|
||
$adminId = (int) $key;
|
||
if ($adminId <= 0) {
|
||
continue;
|
||
}
|
||
if (!in_array($adminId, $adminIds, true)) {
|
||
continue;
|
||
}
|
||
$selected[] = $adminId;
|
||
}
|
||
$selected = array_values(array_unique($selected));
|
||
|
||
$existingRows = $this->adminNotificationSubjectModel
|
||
->select('admin_id')
|
||
->where('subject', 'print_requests')
|
||
->whereIn('admin_id', $adminIds)
|
||
->findAll();
|
||
|
||
$current = array_values(array_unique(array_map(
|
||
fn($row) => (int) ($row['admin_id'] ?? 0),
|
||
$existingRows
|
||
)));
|
||
|
||
$toDelete = array_values(array_diff($current, $selected));
|
||
$toInsert = array_values(array_diff($selected, $current));
|
||
|
||
if (!empty($toDelete)) {
|
||
$this->adminNotificationSubjectModel
|
||
->where('subject', 'print_requests')
|
||
->whereIn('admin_id', $toDelete)
|
||
->delete();
|
||
}
|
||
|
||
if (!empty($toInsert)) {
|
||
$batch = [];
|
||
foreach ($toInsert as $adminId) {
|
||
$batch[] = [
|
||
'admin_id' => $adminId,
|
||
'subject' => 'print_requests',
|
||
];
|
||
}
|
||
$this->adminNotificationSubjectModel->insertBatch($batch);
|
||
}
|
||
|
||
$changes = count($toDelete) + count($toInsert);
|
||
return redirect()->to('/administrator/print-notifications')
|
||
->with('success', $changes > 0 ? 'Print notification recipients updated.' : 'No changes were made.');
|
||
}
|
||
|
||
private function notificationSubjectOptions(): array
|
||
{
|
||
return [
|
||
'academics' => 'Academics',
|
||
'attendance' => 'Attendance',
|
||
'events' => 'Events',
|
||
'finance' => 'Finance',
|
||
'general' => 'General',
|
||
'print_requests' => 'Print Requests',
|
||
];
|
||
}
|
||
|
||
private function getAdminNotificationExcludedRoles(): array
|
||
{
|
||
return [
|
||
'parent',
|
||
'student',
|
||
'guest',
|
||
'teacher',
|
||
'assistant teacher',
|
||
'teacher assistant',
|
||
'teacher_assistant',
|
||
'assistant_teacher',
|
||
'ta',
|
||
'authorized_user',
|
||
];
|
||
}
|
||
|
||
private function canManageAdminNotifications(): bool
|
||
{
|
||
$session = session();
|
||
if (! $session->get('is_logged_in')) {
|
||
return false;
|
||
}
|
||
|
||
$role = trim((string) ($session->get('role') ?? ''));
|
||
if ($role === '') {
|
||
return false;
|
||
}
|
||
|
||
$excluded = array_map(
|
||
fn ($value) => strtolower(trim((string) $value)),
|
||
$this->getAdminNotificationExcludedRoles()
|
||
);
|
||
|
||
return !in_array(strtolower($role), $excluded, true);
|
||
}
|
||
|
||
private function fetchAdminNotificationUsers(): array
|
||
{
|
||
$excluded = $this->getAdminNotificationExcludedRoles();
|
||
$excludedList = "'" . implode("','", $excluded) . "'";
|
||
|
||
return $this->db->table('users u')
|
||
->select('u.id, u.firstname, u.lastname, u.email')
|
||
->join('user_roles ur', 'ur.user_id = u.id', 'inner')
|
||
->join('roles r', 'r.id = ur.role_id', 'inner')
|
||
->where('r.name IS NOT NULL', null, false)
|
||
->where('ur.deleted_at', null)
|
||
->where("LOWER(r.name) NOT IN ({$excludedList})", null, false)
|
||
->groupBy('u.id, u.firstname, u.lastname, u.email')
|
||
->orderBy('u.lastname', 'ASC')
|
||
->orderBy('u.firstname', 'ASC')
|
||
->get()
|
||
->getResultArray();
|
||
}
|
||
|
||
public function feeCollection()
|
||
{
|
||
return view('administrator/fee_collection');
|
||
}
|
||
|
||
public function expenseManagement()
|
||
{
|
||
return view('administrator/expense_management');
|
||
}
|
||
|
||
public function budgetReports()
|
||
{
|
||
return view('administrator/budget_reports');
|
||
}
|
||
|
||
public function scholarshipInformation()
|
||
{
|
||
return view('administrator/scholarship_information');
|
||
}
|
||
|
||
public function attendanceRecords()
|
||
{
|
||
return view('administrator/attendance_records');
|
||
}
|
||
|
||
public function academicPerformance()
|
||
{
|
||
return view('administrator/academic_performance');
|
||
}
|
||
|
||
public function behaviorReports()
|
||
{
|
||
return view('administrator/behavior_reports');
|
||
}
|
||
|
||
public function healthRecords()
|
||
{
|
||
return view('administrator/health_records');
|
||
}
|
||
|
||
public function contactInformation()
|
||
{
|
||
return view('administrator/contact_information');
|
||
}
|
||
|
||
public function studentProfiles()
|
||
{
|
||
$db = db_connect();
|
||
$isPg = ($db->getPlatform() === 'Postgre'); // 'MySQLi', 'Postgre', 'SQLSRV', ...
|
||
|
||
// In MySQL, avoid truncation of long lists
|
||
if (!$isPg) {
|
||
$db->query('SET SESSION group_concat_max_len = 8192');
|
||
}
|
||
|
||
$b = $db->table('students');
|
||
|
||
if ($isPg) {
|
||
// Postgres: use subqueries for aggregates to avoid GROUP BY on students.*
|
||
$students = $b->select([
|
||
'students.*',
|
||
'users.firstname AS parent_firstname',
|
||
'users.lastname AS parent_lastname',
|
||
'users.email AS parent_email',
|
||
'users.cellphone AS parent_phone',
|
||
|
||
// Pick one emergency contact (first by id)
|
||
"(SELECT ec.emergency_contact_name FROM emergency_contacts ec
|
||
WHERE ec.parent_id = students.parent_id
|
||
ORDER BY ec.id ASC LIMIT 1) AS emergency_name",
|
||
"(SELECT ec.relation FROM emergency_contacts ec
|
||
WHERE ec.parent_id = students.parent_id
|
||
ORDER BY ec.id ASC LIMIT 1) AS emergency_relationship",
|
||
"(SELECT ec.cellphone FROM emergency_contacts ec
|
||
WHERE ec.parent_id = students.parent_id
|
||
ORDER BY ec.id ASC LIMIT 1) AS emergency_phone",
|
||
"(SELECT ec.email FROM emergency_contacts ec
|
||
WHERE ec.parent_id = students.parent_id
|
||
ORDER BY ec.id ASC LIMIT 1) AS emergency_email",
|
||
|
||
// Aggregated lists
|
||
"(SELECT STRING_AGG(DISTINCT sa.allergy, ', ' ORDER BY sa.allergy)
|
||
FROM student_allergies sa WHERE sa.student_id = students.id) AS allergies",
|
||
"(SELECT STRING_AGG(DISTINCT smc.condition_name, ', ' ORDER BY smc.condition_name)
|
||
FROM student_medical_conditions smc WHERE smc.student_id = students.id) AS medical_conditions",
|
||
])
|
||
->join('users', 'users.id = students.parent_id', 'left')
|
||
->orderBy('students.lastname', 'ASC')
|
||
->orderBy('students.firstname', 'ASC')
|
||
->get()
|
||
->getResultArray();
|
||
} else {
|
||
// MySQL: GROUP_CONCAT + MIN() for emergency_* to satisfy ONLY_FULL_GROUP_BY
|
||
$students = $b->select([
|
||
'students.*',
|
||
'users.firstname AS parent_firstname',
|
||
'users.lastname AS parent_lastname',
|
||
'users.email AS parent_email',
|
||
'users.cellphone AS parent_phone',
|
||
|
||
'MIN(emergency_contacts.emergency_contact_name) AS emergency_name',
|
||
'MIN(emergency_contacts.relation) AS emergency_relationship',
|
||
'MIN(emergency_contacts.cellphone) AS emergency_phone',
|
||
'MIN(emergency_contacts.email) AS emergency_email',
|
||
|
||
"GROUP_CONCAT(DISTINCT student_allergies.allergy
|
||
ORDER BY student_allergies.allergy SEPARATOR ', ') AS allergies",
|
||
"GROUP_CONCAT(DISTINCT student_medical_conditions.condition_name
|
||
ORDER BY student_medical_conditions.condition_name SEPARATOR ', ') AS medical_conditions",
|
||
])
|
||
->join('users', 'users.id = students.parent_id', 'left')
|
||
->join('emergency_contacts', 'emergency_contacts.parent_id = students.parent_id', 'left')
|
||
->join('student_allergies', 'student_allergies.student_id = students.id', 'left')
|
||
->join('student_medical_conditions', 'student_medical_conditions.student_id = students.id', 'left')
|
||
->groupBy('students.id')
|
||
->orderBy('students.lastname', 'ASC')
|
||
->orderBy('students.firstname', 'ASC')
|
||
->get()
|
||
->getResultArray();
|
||
}
|
||
|
||
// === Inject class_section_name from student_class via your method and replace grade ===
|
||
foreach ($students as $i => $row) {
|
||
$sid = (int) ($row['id'] ?? 0);
|
||
if ($sid > 0) {
|
||
// Fetch class_section_name for this student
|
||
// Assumes your method signature: getClassSectionNameByStudentId(int $studentId): ?string
|
||
$classSectionName = (string) ($this->studentClassModel->getClassSectionNameByStudentId($sid) ?? '');
|
||
|
||
// Expose it explicitly and replace original grade if present
|
||
$students[$i]['class_section_name'] = $classSectionName;
|
||
} else {
|
||
// Keep keys consistent even if id missing
|
||
$students[$i]['class_section_name'] = '';
|
||
}
|
||
}
|
||
// === end injection ===
|
||
|
||
return view('administrator/student_profiles', [
|
||
'students' => $students,
|
||
'gradeOptions' => ['K', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Youth'],
|
||
'genderOptions' => ['Male', 'Female', 'Other'],
|
||
]);
|
||
}
|
||
|
||
|
||
|
||
public function parentProfiles()
|
||
{
|
||
// Fetch all users with their roles in one go
|
||
$allUsers = $this->userModel->findAll();
|
||
$parents = [];
|
||
|
||
// Fetch roles for users in one query to avoid looping over all users
|
||
foreach ($allUsers as $user) {
|
||
// Get the roles of the user directly
|
||
$roles = $this->userRoleModel->getRolesByUserId($user['id']);
|
||
$isParent = false;
|
||
|
||
// Check if $roles is iterable and contains the 'parent' role
|
||
if (is_array($roles)) {
|
||
foreach ($roles as $role) {
|
||
if (isset($role['role_name']) && $role['role_name'] === 'parent') {
|
||
$isParent = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($isParent) {
|
||
// Retrieve the latest paid amount and balance for this parent
|
||
$paidAmount = $this->invoiceModel->getLatestInvoicePaidAmount($user['id']) ?? 0;
|
||
$balance = $this->invoiceModel->getLatestInvoiceBalance($user['id']) ?? 0;
|
||
|
||
// Get students for this parent
|
||
$studentsData = $this->studentModel->where('parent_id', $user['id'])->findAll();
|
||
$students = [];
|
||
foreach ($studentsData as $student) {
|
||
$classSectionName = $this->studentClassModel->getClassSectionNameByStudentId($student['id']) ?? 'N/A';
|
||
$students[] = [
|
||
'name' => $student['firstname'] . ' ' . $student['lastname'],
|
||
'class_section' => $classSectionName
|
||
];
|
||
}
|
||
|
||
// Add parent data to the array
|
||
$parents[] = [
|
||
'id' => $user['id'],
|
||
'school_id' => $user['school_id'],
|
||
'firstname' => $user['firstname'],
|
||
'lastname' => $user['lastname'],
|
||
'email' => $user['email'],
|
||
'cellphone' => $user['cellphone'],
|
||
'gender' => $user['gender'],
|
||
'created_at' => $user['created_at'],
|
||
'school_year' => $user['school_year'],
|
||
'paid_amount' => $paidAmount,
|
||
'balance' => $balance,
|
||
'students' => $students,
|
||
];
|
||
}
|
||
}
|
||
|
||
return view('administrator/parent_profile', ['parents' => $parents]);
|
||
}
|
||
|
||
public function manageUsers()
|
||
{// Fetch all users
|
||
$users = $this->userModel->findAll();
|
||
|
||
// Fetch roles for each user
|
||
foreach ($users as &$user) {
|
||
$userRoles = ($this->userRoleModel)->where('user_id', $user['id'])->findAll();
|
||
$user['roles'] = [];
|
||
foreach ($userRoles as $userRole) {
|
||
$role = $this->roleModel->find($userRole['role_id']);
|
||
if ($role) {
|
||
$user['roles'][] = $role['name'];
|
||
}
|
||
}
|
||
}
|
||
|
||
$data['users'] = $users;
|
||
|
||
return view('administrator/manage_users', $data);
|
||
}
|
||
|
||
public function editUser($userId)
|
||
{$user = $this->userModel->find($userId);
|
||
|
||
// Fetch roles assigned to the user
|
||
$userRoles = $this->userRoleModel->where('user_id', $userId)->findAll();
|
||
$assignedRoles = [];
|
||
foreach ($userRoles as $userRole) {
|
||
$role = $this->roleModel->find($userRole['role_id']);
|
||
if ($role) {
|
||
$assignedRoles[] = $role['id'];
|
||
}
|
||
}
|
||
|
||
$roles = $this->roleModel->findAll();
|
||
|
||
$data = [
|
||
'user' => $user,
|
||
'roles' => $roles,
|
||
'assignedRoles' => $assignedRoles,
|
||
];
|
||
|
||
return view('administrator/edit_user', $data);
|
||
}
|
||
|
||
public function updateUser()
|
||
{$userId = $this->request->getPost('id');
|
||
$roleIds = $this->request->getPost('role_ids');
|
||
|
||
// Update user details
|
||
$data = [
|
||
'id' => $userId,
|
||
'firstname' => $this->request->getPost('firstname'),
|
||
'lastname' => $this->request->getPost('lastname'),
|
||
'email' => $this->request->getPost('email'),
|
||
];
|
||
$this->userModel->save($data);
|
||
|
||
// Update user roles
|
||
$this->userRoleModel->where('user_id', $userId)->delete();
|
||
foreach ($roleIds as $roleId) {
|
||
$this->userRoleModel->save([
|
||
'user_id' => $userId,
|
||
'role_id' => $roleId,
|
||
]);
|
||
}
|
||
|
||
return redirect()->to('/administrator/manage-users');
|
||
}
|
||
|
||
public function deleteUser($userId)
|
||
{// Delete user roles first
|
||
$this->userRoleModel->where('user_id', $userId)->delete();
|
||
|
||
// Delete the user
|
||
$this->userModel->delete($userId);
|
||
|
||
return redirect()->to('/administrator/manage-users');
|
||
}
|
||
|
||
public function showEnrollmentWithdrawalPage()
|
||
{
|
||
try {
|
||
$selectedYear = trim((string)($this->request->getGet('schoolYear') ?? ''));
|
||
if ($selectedYear === '') {
|
||
$selectedYear = (string)$this->schoolYear;
|
||
}
|
||
|
||
// Distinct school years from enrollments (for selector)
|
||
$yearsRows = $this->db->table('enrollments')
|
||
->distinct()
|
||
->select('school_year')
|
||
->orderBy('school_year', 'DESC')
|
||
->get()->getResultArray();
|
||
$schoolYears = array_values(array_filter(array_map(static function ($r) {
|
||
return isset($r['school_year']) ? (string)$r['school_year'] : null;
|
||
}, $yearsRows)));
|
||
|
||
$students = $this->studentModel->getStudentsWithClassAndEnrollment();
|
||
|
||
$selectedStartYear = $this->getSchoolYearStartYear((string)$selectedYear);
|
||
$removedPriorIds = [];
|
||
if ($selectedStartYear !== null) {
|
||
$removedRows = $this->db->table('enrollments')
|
||
->select('student_id, school_year')
|
||
->where('is_withdrawn', 1)
|
||
->get()->getResultArray();
|
||
foreach ($removedRows as $row) {
|
||
$rowYear = $this->getSchoolYearStartYear((string)($row['school_year'] ?? ''));
|
||
if ($rowYear !== null && $rowYear < $selectedStartYear) {
|
||
$removedPriorIds[(int)($row['student_id'] ?? 0)] = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($students as &$s) {
|
||
// ===== Ensure IDs needed by the modal =====
|
||
$s['student_id'] = (int)($s['id'] ?? 0);
|
||
$s['removed_previous_year'] = isset($removedPriorIds[$s['student_id']]) ? 'Yes' : 'No';
|
||
|
||
// Prefer parent_id; fallback to secondparent_user_id if present
|
||
if (empty($s['parent_id']) && !empty($s['secondparent_user_id'])) {
|
||
$s['parent_id'] = (int)$s['secondparent_user_id'];
|
||
} else {
|
||
$s['parent_id'] = (int)($s['parent_id'] ?? 0);
|
||
}
|
||
|
||
// ===== Parent display + sort keys (keep existing behavior) =====
|
||
$pf = trim((string)($s['parent_firstname'] ?? ''));
|
||
$pl = trim((string)($s['parent_lastname'] ?? ''));
|
||
|
||
// Fallback: if only a single full name exists
|
||
if ($pf === '' && $pl === '' && !empty($s['parent_fullname'])) {
|
||
$parts = preg_split('/\s+/', trim((string)$s['parent_fullname']), 2);
|
||
$pf = $parts[0] ?? '';
|
||
$pl = $parts[1] ?? '';
|
||
}
|
||
|
||
$s['parent_label'] = trim($pf . ' ' . $pl);
|
||
$s['parent_sort'] = trim(($pl !== '' ? $pl : $pf) . ' ' . $pf);
|
||
|
||
if ($s['parent_label'] === '') {
|
||
$s['parent_label'] = 'Unknown Parent';
|
||
$s['parent_sort'] = 'ZZZ Unknown Parent';
|
||
}
|
||
|
||
// ===== New-student flags =====
|
||
$s['is_new'] = (int) ($s['is_new'] ?? 0);
|
||
$s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No';
|
||
|
||
// ===== Admission override =====
|
||
// Enrollment status for selected year
|
||
$statusForYear = $this->enrollmentModel->getEnrollmentStatus((int)$s['student_id'], $selectedYear);
|
||
if (!empty($statusForYear)) {
|
||
$s['enrollment_status'] = $statusForYear;
|
||
} elseif (($s['admission_status'] ?? null) === 'denied') {
|
||
$s['enrollment_status'] = 'denied';
|
||
}
|
||
|
||
// ===== Class section name for the selected year =====
|
||
$name = $this->studentClassModel->getClassSectionsByStudentId((int)$s['student_id'], $selectedYear);
|
||
$s['class_section'] = $name ?: 'Class not Assigned';
|
||
|
||
// ===== Sortable registration date (for data-order in view) =====
|
||
$s['registration_date_order'] = !empty($s['registration_date'])
|
||
? date('Y-m-d', strtotime($s['registration_date']))
|
||
: '';
|
||
}
|
||
unset($s); // break reference
|
||
|
||
// ===== Sort by parent, then student (lastname, firstname) =====
|
||
usort($students, function (array $a, array $b) {
|
||
$pa = $a['parent_sort'] ?? '';
|
||
$pb = $b['parent_sort'] ?? '';
|
||
if (strcasecmp($pa, $pb) === 0) {
|
||
$la = $a['lastname'] ?? '';
|
||
$lb = $b['lastname'] ?? '';
|
||
$cmp = strcasecmp($la, $lb);
|
||
if ($cmp !== 0) return $cmp;
|
||
return strcasecmp($a['firstname'] ?? '', $b['firstname'] ?? '');
|
||
}
|
||
return strcasecmp($pa, $pb);
|
||
});
|
||
|
||
// ===== Provide classes for the modal select (like studentClassAssignment) =====
|
||
// Filter to current term; loosen if your table doesn’t store these fields.
|
||
$classes = $this->classSectionModel
|
||
->select('id, class_section_id, class_section_name, school_year, semester')
|
||
->where('school_year', (string)$selectedYear)
|
||
->where('semester', (string)$this->semester)
|
||
->orderBy('class_section_name', 'ASC')
|
||
->findAll();
|
||
|
||
// Fallback so the dropdown never shows empty if filters are too strict
|
||
if (empty($classes)) {
|
||
$classes = $this->classSectionModel
|
||
->select('id, class_section_id, class_section_name')
|
||
->orderBy('class_section_name', 'ASC')
|
||
->findAll();
|
||
}
|
||
|
||
return view('enroll_withdraw/enrollment_withdrawal', [
|
||
'students' => $students,
|
||
'classes' => $classes, // <-- used by the modal <select>
|
||
'schoolYears' => $schoolYears,
|
||
'selectedYear' => $selectedYear,
|
||
'currentYear' => (string)$this->schoolYear,
|
||
'isCurrentYear' => ((string)$selectedYear === (string)$this->schoolYear),
|
||
'missingYear' => empty($this->schoolYear),
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'Enrollment/Withdrawal page error: {msg}', ['msg' => $e->getMessage()]);
|
||
return view('errors/html/error_500');
|
||
}
|
||
}
|
||
|
||
// API: Enrollment/Withdrawal data for admin page
|
||
public function enrollmentWithdrawalData()
|
||
{
|
||
try {
|
||
$selectedYear = trim((string)($this->request->getGet('schoolYear') ?? ''));
|
||
if ($selectedYear === '') {
|
||
$selectedYear = (string)$this->schoolYear;
|
||
}
|
||
|
||
// Distinct school years
|
||
$yearsRows = $this->db->table('enrollments')
|
||
->distinct()
|
||
->select('school_year')
|
||
->orderBy('school_year', 'DESC')
|
||
->get()->getResultArray();
|
||
$schoolYears = array_values(array_filter(array_map(static function ($r) {
|
||
return isset($r['school_year']) ? (string)$r['school_year'] : null;
|
||
}, $yearsRows)));
|
||
|
||
$students = $this->studentModel->getStudentsWithClassAndEnrollment();
|
||
|
||
$selectedStartYear = $this->getSchoolYearStartYear((string)$selectedYear);
|
||
$removedPriorIds = [];
|
||
if ($selectedStartYear !== null) {
|
||
$removedRows = $this->db->table('enrollments')
|
||
->select('student_id, school_year')
|
||
->where('is_withdrawn', 1)
|
||
->get()->getResultArray();
|
||
foreach ($removedRows as $row) {
|
||
$rowYear = $this->getSchoolYearStartYear((string)($row['school_year'] ?? ''));
|
||
if ($rowYear !== null && $rowYear < $selectedStartYear) {
|
||
$removedPriorIds[(int)($row['student_id'] ?? 0)] = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($students as &$s) {
|
||
$s['student_id'] = (int)($s['id'] ?? 0);
|
||
$s['removed_previous_year'] = isset($removedPriorIds[$s['student_id']]) ? 'Yes' : 'No';
|
||
|
||
if (empty($s['parent_id']) && !empty($s['secondparent_user_id'])) {
|
||
$s['parent_id'] = (int)$s['secondparent_user_id'];
|
||
} else {
|
||
$s['parent_id'] = (int)($s['parent_id'] ?? 0);
|
||
}
|
||
|
||
$pf = trim((string)($s['parent_firstname'] ?? ''));
|
||
$pl = trim((string)($s['parent_lastname'] ?? ''));
|
||
if ($pf === '' && $pl === '' && !empty($s['parent_fullname'])) {
|
||
$parts = preg_split('/\s+/', trim((string)$s['parent_fullname']), 2);
|
||
$pf = $parts[0] ?? '';
|
||
$pl = $parts[1] ?? '';
|
||
}
|
||
$s['parent_label'] = trim($pf . ' ' . $pl) ?: 'Unknown Parent';
|
||
$s['parent_sort'] = trim(($pl !== '' ? $pl : $pf) . ' ' . $pf) ?: 'ZZZ Unknown Parent';
|
||
|
||
$s['is_new'] = (int) ($s['is_new'] ?? 0);
|
||
$s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No';
|
||
|
||
$statusForYear = $this->enrollmentModel->getEnrollmentStatus((int)$s['student_id'], $selectedYear);
|
||
if (!empty($statusForYear)) {
|
||
$s['enrollment_status'] = $statusForYear;
|
||
} elseif (($s['admission_status'] ?? null) === 'denied') {
|
||
$s['enrollment_status'] = 'denied';
|
||
}
|
||
|
||
$className = $this->studentClassModel->getClassSectionsByStudentId((int)$s['student_id'], $selectedYear);
|
||
$s['class_section'] = $className ?: 'Class not Assigned';
|
||
|
||
$s['registration_date_order'] = !empty($s['registration_date'])
|
||
? date('Y-m-d', strtotime($s['registration_date']))
|
||
: '';
|
||
}
|
||
unset($s);
|
||
|
||
usort($students, function (array $a, array $b) {
|
||
$pa = $a['parent_sort'] ?? '';
|
||
$pb = $b['parent_sort'] ?? '';
|
||
if (strcasecmp($pa, $pb) === 0) {
|
||
$la = $a['lastname'] ?? '';
|
||
$lb = $b['lastname'] ?? '';
|
||
$cmp = strcasecmp($la, $lb);
|
||
if ($cmp !== 0) return $cmp;
|
||
return strcasecmp($a['firstname'] ?? '', $b['firstname'] ?? '');
|
||
}
|
||
return strcasecmp($pa, $pb);
|
||
});
|
||
|
||
$classes = $this->classSectionModel
|
||
->select('id, class_section_id, class_section_name, school_year, semester')
|
||
->where('school_year', (string)$selectedYear)
|
||
->where('semester', (string)$this->semester)
|
||
->orderBy('class_section_name', 'ASC')
|
||
->findAll();
|
||
if (empty($classes)) {
|
||
$classes = $this->classSectionModel
|
||
->select('id, class_section_id, class_section_name')
|
||
->orderBy('class_section_name', 'ASC')
|
||
->findAll();
|
||
}
|
||
|
||
return $this->response->setJSON([
|
||
'students' => $students,
|
||
'classes' => $classes,
|
||
'csrfHash' => csrf_hash(),
|
||
'semester' => (string)$this->semester,
|
||
'school_year' => (string)$selectedYear,
|
||
'schoolYears' => $schoolYears,
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'enrollmentWithdrawalData error: {msg}', ['msg' => $e->getMessage()]);
|
||
return $this->response->setStatusCode(500)->setJSON(['error' => 'Server error']);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show only newly registered students, with contact modal support.
|
||
*
|
||
* Route example:
|
||
* $routes->get('admin/enrollment/new-students', 'EnrollmentController::showNewStudents', ['filter' => 'auth:view_new_students']);
|
||
*/
|
||
public function showNewStudents(): string
|
||
{
|
||
$rows = $this->studentModel->getStudentsWithParentsAndEmergency($this->schoolYear);
|
||
|
||
$newStudents = [];
|
||
foreach ($rows as $r) {
|
||
$r['new_student'] = 'Yes';
|
||
$classSection = $this->studentClassModel->getClassSectionsByStudentId($r['id'], $this->schoolYear);
|
||
$enrollmentstatus = $this->enrollmentModel->getEnrollmentStatus($r['id'], $this->schoolYear);
|
||
// robust default
|
||
$r['class_section'] = (isset($classSection) && trim((string)$classSection) !== '')
|
||
? $classSection
|
||
: 'Class not Assigned';
|
||
|
||
$r['is_new'] = (int) $r['is_new'];
|
||
$r['new_student'] = $r['is_new'] === 1 ? "Yes" : "No";
|
||
$r['modalIdContact'] = 'contact_' . (int)($r['id'] ?? 0);
|
||
$r['enrollment_status'] = $enrollmentstatus;
|
||
|
||
// ✅ Format registration_date for display
|
||
if (!empty($r['registration_date'])) {
|
||
try {
|
||
$r['registration_date'] = (new \DateTime($r['registration_date']))->format('Y-m-d');
|
||
} catch (\Throwable $e) {
|
||
$r['registration_date'] = '';
|
||
}
|
||
}
|
||
|
||
// Age comes directly from DB (already stored in students.age)
|
||
$newStudents[] = $r;
|
||
}
|
||
|
||
return view('enroll_withdraw/new-students', [
|
||
'new_students' => $newStudents,
|
||
'total_new' => count($newStudents),
|
||
]);
|
||
}
|
||
|
||
|
||
|
||
|
||
//update enrollment status
|
||
public function adminEnrollmentWithdrawalHandler()
|
||
{
|
||
$refundService = new FeeCalculationService();
|
||
$this->db->transStart();
|
||
|
||
try {
|
||
$enrollmentStatuses = $this->request->getPost('enrollment_status');
|
||
if (empty($enrollmentStatuses)) {
|
||
return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
|
||
->with('error', 'No enrollment statuses were submitted.');
|
||
}
|
||
|
||
$errors = [];
|
||
|
||
// For batching emails: parent -> status -> [students...]
|
||
$groupsByParentStatus = []; // [parent_id][status][] = ['student_id'=>, 'student_name'=>]
|
||
$parentInfo = []; // [parent_id] = ['user_id','email','firstname','lastname']
|
||
$refundParents = []; // parent_id => true (for refund calc)
|
||
$refundAmountByParent = []; // parent_id => amount
|
||
|
||
$validStatuses = [
|
||
'admission under review',
|
||
'payment pending',
|
||
'enrolled',
|
||
'withdraw under review',
|
||
'refund pending',
|
||
'withdrawn',
|
||
'denied',
|
||
'waitlist',
|
||
];
|
||
|
||
foreach ($enrollmentStatuses as $studentId => $newEnrollmentStatus) {
|
||
if (!in_array($newEnrollmentStatus, $validStatuses, true)) {
|
||
$errors[] = "Invalid enrollment status '$newEnrollmentStatus' for student ID $studentId.";
|
||
continue;
|
||
}
|
||
|
||
// Map admission_status based on the desired new status (needed for create-or-update)
|
||
if ($newEnrollmentStatus === 'denied') {
|
||
$admissionStatus = 'denied';
|
||
} elseif (in_array($newEnrollmentStatus, ['enrolled', 'payment pending'], true)) {
|
||
$admissionStatus = 'accepted';
|
||
} else {
|
||
$admissionStatus = 'pending';
|
||
}
|
||
|
||
// Current enrollment row
|
||
$enrollmentRow = $this->db->table('enrollments')
|
||
->where('student_id', $studentId)
|
||
->where('school_year', $this->schoolYear)
|
||
->get()
|
||
->getRowArray();
|
||
|
||
// If no enrollment found for this student/year, create one so invoice generation has data
|
||
if (!$enrollmentRow) {
|
||
$stu = $this->studentModel->find((int)$studentId) ?? [];
|
||
$parentId = (int)($stu['parent_id'] ?? ($stu['secondparent_user_id'] ?? 0));
|
||
if (!$parentId) {
|
||
$errors[] = "No parent ID found for student ID $studentId.";
|
||
continue;
|
||
}
|
||
|
||
$isWithdrawn = in_array($newEnrollmentStatus, ['withdrawn', 'refund pending', 'withdraw under review'], true) ? 1 : 0;
|
||
|
||
$ok = $this->db->table('enrollments')->insert([
|
||
'student_id' => (int)$studentId,
|
||
'parent_id' => $parentId,
|
||
'school_year' => (string)$this->schoolYear,
|
||
'semester' => (string)$this->semester,
|
||
'enrollment_date' => local_date(utc_now(), 'Y-m-d'),
|
||
'is_withdrawn' => $isWithdrawn,
|
||
'enrollment_status' => $newEnrollmentStatus,
|
||
'admission_status' => $admissionStatus,
|
||
'created_at' => utc_now(),
|
||
'updated_at' => utc_now(),
|
||
]);
|
||
|
||
if (!$ok) {
|
||
$errors[] = "Failed to create enrollment for student ID $studentId.";
|
||
continue;
|
||
}
|
||
|
||
// Group this newly-created change for notifications
|
||
$studentRow = $this->studentModel->find($studentId) ?? [];
|
||
$studentName = trim(($studentRow['firstname'] ?? '') . ' ' . ($studentRow['lastname'] ?? '')) ?: "Student #{$studentId}";
|
||
|
||
if (!isset($parentInfo[$parentId])) {
|
||
$p = $this->userModel->find($parentId) ?? [];
|
||
$parentInfo[$parentId] = [
|
||
'user_id' => $p['id'] ?? $parentId,
|
||
'email' => $p['email'] ?? null,
|
||
'firstname' => $p['firstname'] ?? '',
|
||
'lastname' => $p['lastname'] ?? '',
|
||
];
|
||
}
|
||
$groupsByParentStatus[$parentId][$newEnrollmentStatus][] = [
|
||
'student_id' => (int) $studentId,
|
||
'student_name' => $studentName,
|
||
];
|
||
|
||
if ($newEnrollmentStatus === 'refund pending') {
|
||
$refundParents[$parentId] = true;
|
||
}
|
||
|
||
log_message('info', "Created enrollment for student ID {$studentId} with status {$newEnrollmentStatus} and admission {$admissionStatus}.");
|
||
continue; // go to next student
|
||
}
|
||
|
||
$oldStatus = $enrollmentRow['enrollment_status'] ?? null;
|
||
$parentId = $enrollmentRow['parent_id'] ?? null;
|
||
|
||
if (!$parentId) {
|
||
$errors[] = "No parent ID found for student ID $studentId.";
|
||
continue;
|
||
}
|
||
|
||
// admissionStatus computed above
|
||
|
||
// Skip if no actual change
|
||
if ($oldStatus === $newEnrollmentStatus) {
|
||
log_message('debug', "No status change for student {$studentId} ({$oldStatus}) — skipping.");
|
||
continue;
|
||
}
|
||
|
||
// Update enrollment
|
||
$updated = $this->db->table('enrollments')
|
||
->where('student_id', $studentId)
|
||
->where('school_year', $this->schoolYear)
|
||
->update([
|
||
'enrollment_status' => $newEnrollmentStatus,
|
||
'admission_status' => $admissionStatus,
|
||
'updated_at' => utc_now(),
|
||
]);
|
||
|
||
if (!$updated) {
|
||
$errors[] = "Failed to update enrollment for student ID $studentId.";
|
||
continue;
|
||
}
|
||
|
||
log_message('info', "Updated enrollment for student ID $studentId: {$oldStatus} → {$newEnrollmentStatus} (admission: {$admissionStatus})");
|
||
|
||
// Student name
|
||
$studentRow = $this->studentModel->find($studentId);
|
||
$studentName = trim(($studentRow['firstname'] ?? '') . ' ' . ($studentRow['lastname'] ?? '')) ?: "Student #{$studentId}";
|
||
|
||
// Cache parent info once
|
||
if (!isset($parentInfo[$parentId])) {
|
||
$p = $this->userModel->find($parentId) ?? [];
|
||
$parentInfo[$parentId] = [
|
||
'user_id' => $p['id'] ?? $parentId, // assuming parent_id == user_id
|
||
'email' => $p['email'] ?? null,
|
||
'firstname' => $p['firstname'] ?? '',
|
||
'lastname' => $p['lastname'] ?? '',
|
||
];
|
||
}
|
||
|
||
// Group by parent & status for minimal emails
|
||
$groupsByParentStatus[$parentId][$newEnrollmentStatus][] = [
|
||
'student_id' => (int) $studentId,
|
||
'student_name' => $studentName,
|
||
];
|
||
|
||
// Mark for refund calc
|
||
if ($newEnrollmentStatus === 'refund pending') {
|
||
$refundParents[$parentId] = true;
|
||
}
|
||
}
|
||
|
||
// Compute refunds ONCE per parent needing it
|
||
foreach (array_keys($refundParents) as $pid) {
|
||
$students = $this->enrollmentModel
|
||
->where('parent_id', $pid)
|
||
->where('school_year', $this->schoolYear)
|
||
->findAll();
|
||
|
||
if (empty($students)) {
|
||
// If a parent is marked for refund but has no enrollments, just log and continue.
|
||
log_message('info', "No enrollments found for parent ID {$pid} (for refund calc); skipping refund.");
|
||
continue;
|
||
}
|
||
|
||
$invoice = $this->invoiceModel->where('parent_id', $pid)
|
||
->where('school_year', $this->schoolYear)
|
||
->orderBy('created_at', 'DESC')
|
||
->first();
|
||
|
||
if (!$invoice) {
|
||
$errors[] = "No invoice found for parent ID $pid (for refund calc).";
|
||
continue;
|
||
}
|
||
|
||
$refundAmount = $refundService->calculateRefund($students, $pid);
|
||
$refundAmountByParent[$pid] = $refundAmount;
|
||
|
||
$existingRefund = $this->refundModel->where('invoice_id', $invoice['id'])->first();
|
||
|
||
if ($existingRefund) {
|
||
$this->refundModel->update($existingRefund['id'], [
|
||
'refund_amount' => $refundAmount,
|
||
'status' => 'Pending',
|
||
'updated_by' => session()->get('user_id') ?? null,
|
||
]);
|
||
} else {
|
||
$this->refundModel->insert([
|
||
'parent_id' => $pid,
|
||
'school_year' => $invoice['school_year'],
|
||
'invoice_id' => $invoice['id'],
|
||
'refund_amount' => $refundAmount,
|
||
'refund_paid_amount' => 0.0,
|
||
'status' => 'Pending',
|
||
'requested_at' => utc_now(),
|
||
'updated_by' => session()->get('user_id') ?? null,
|
||
]);
|
||
}
|
||
|
||
log_message('info', "Refund of $refundAmount created/updated for invoice ID {$invoice['id']} (parent {$pid}).");
|
||
}
|
||
|
||
$this->db->transComplete();
|
||
|
||
if (!$this->db->transStatus()) {
|
||
return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
|
||
->with('error', 'A database error occurred. Changes were rolled back.');
|
||
}
|
||
|
||
// === AFTER COMMIT: fire specific events, batched per parent/status ===
|
||
$eventMap = [
|
||
'admission under review' => 'admissionUnderReview',
|
||
'payment pending' => 'paymentPending',
|
||
'enrolled' => 'studentEnrolled',
|
||
'withdraw under review' => 'withdrawUnderReview',
|
||
'refund pending' => 'refundPending',
|
||
'withdrawn' => 'withdrawn',
|
||
'denied' => 'denied',
|
||
'waitlist' => 'waitlist',
|
||
];
|
||
|
||
foreach ($groupsByParentStatus as $pid => $byStatus) {
|
||
// Common parent data
|
||
$p = $parentInfo[$pid] ?? ['user_id' => $pid, 'email' => null, 'firstname' => '', 'lastname' => ''];
|
||
|
||
// Fetch invoice once for this parent (for payment pending email data if needed)
|
||
$invoice = $this->invoiceModel->where('parent_id', $pid)
|
||
->where('school_year', $this->schoolYear)
|
||
->orderBy('created_at', 'DESC')
|
||
->first();
|
||
|
||
foreach ($byStatus as $status => $studentsArr) {
|
||
if (empty($eventMap[$status])) {
|
||
continue; // unknown mapping
|
||
}
|
||
|
||
// Build second arg: student list
|
||
$studentData = [];
|
||
foreach ($studentsArr as $s) {
|
||
$studentData[] = ['name' => $s['student_name'], 'student_id' => $s['student_id']];
|
||
}
|
||
|
||
// Parent payload (first arg)
|
||
$parentData = [
|
||
'user_id' => $p['user_id'],
|
||
'email' => $p['email'],
|
||
'firstname' => $p['firstname'],
|
||
'lastname' => $p['lastname'],
|
||
'school_year' => $this->schoolYear,
|
||
'portalLink' => base_url('/login'),
|
||
];
|
||
|
||
// Enrich with status-specific fields
|
||
if ($status === 'payment pending') {
|
||
if ($invoice) {
|
||
$parentData['amount'] = (float) ($invoice['balance'] ?? $invoice['amount_due'] ?? $invoice['total_amount'] ?? 0);
|
||
$parentData['due_date'] = $invoice['due_date'] ?? null;
|
||
}
|
||
} elseif ($status === 'refund pending') {
|
||
$parentData['amount'] = $refundAmountByParent[$pid] ?? null;
|
||
}
|
||
|
||
$eventName = $eventMap[$status];
|
||
|
||
log_message('info', "Triggering event '{$eventName}' for parent {$pid} with " . count($studentData) . " student(s).");
|
||
Events::trigger($eventName, $parentData, $studentData);
|
||
}
|
||
}
|
||
|
||
// === Server-side safety net: generate/update invoices for parents whose statuses require it ===
|
||
try {
|
||
$needsInvoiceFor = ['payment pending', 'enrolled', 'withdrawn', 'refund pending'];
|
||
$invCtl = new InvoiceController();
|
||
foreach ($groupsByParentStatus as $pid => $byStatus) {
|
||
$statuses = array_keys($byStatus);
|
||
$requires = array_intersect($statuses, $needsInvoiceFor);
|
||
if (!empty($requires)) {
|
||
// Best-effort; ignore response object
|
||
try {
|
||
$invCtl->generateInvoice((string)$pid);
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'Invoice fallback generation failed for parent {pid}: {err}', ['pid' => $pid, 'err' => $e->getMessage()]);
|
||
}
|
||
}
|
||
}
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'Invoice fallback block error: ' . $e->getMessage());
|
||
}
|
||
|
||
if (!empty($errors)) {
|
||
return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
|
||
->with('error', implode(' ', $errors));
|
||
}
|
||
|
||
return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
|
||
->with('success', 'Enrollment statuses updated and notifications sent.');
|
||
} catch (\Throwable $e) {
|
||
$this->db->transRollback();
|
||
log_message('error', 'Enrollment withdrawal error: ' . $e->getMessage());
|
||
return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
|
||
->with('error', 'An unexpected error occurred while processing enrollments.');
|
||
}
|
||
}
|
||
}
|