Files
alrahma_sunday_school/app/Controllers/View/AdministratorController.php
T
root 127098b87c
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 48s
Tests / PHPUnit (push) Successful in 1m17s
fix close year and ignore not active students
2026-08-20 17:27:28 -04:00

697 lines
23 KiB
PHP

<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\UserModel;
use App\Models\RoleModel;
use App\Models\UserRoleModel;
use App\Models\ConfigurationModel;
use App\Models\StaffAttendanceModel;
use App\Libraries\StaffTimeOffLinkService;
use App\Services\SemesterRangeService;
class AdministratorController extends BaseController
{
protected $roleModel;
protected $userModel;
protected $userRoleModel;
protected $configModel;
protected $semester;
protected $schoolYear;
protected $staffAttendanceModel;
public function __construct()
{
helper('auth');
$this->roleModel = new RoleModel();
$this->userModel = new UserModel();
$this->configModel = new ConfigurationModel();
$this->userRoleModel = new UserRoleModel();
$this->semester = getSemester();
$this->schoolYear = $this->configModel->getConfig('school_year');
$this->staffAttendanceModel = new StaffAttendanceModel();
}
/**
* 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;
}
/**
* 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 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 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');
}
private function redirectWithoutLegacyTermFilters(string $route): ?\CodeIgniter\HTTP\RedirectResponse
{
$legacyTermKeys = ['school_year', 'schoolYear', 'year', 'semester'];
$query = $this->request->getGet();
$hasLegacyTermFilter = false;
foreach ($legacyTermKeys as $key) {
if (array_key_exists($key, $query)) {
unset($query[$key]);
$hasLegacyTermFilter = true;
}
}
if (!$hasLegacyTermFilter) {
return null;
}
$target = site_url($route) . (!empty($query) ? '?' . http_build_query($query) : '');
return redirect()->to($target);
}
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,
]);
}
service('staffDirectorySync')->syncUser((int) $userId);
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');
}
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)),
service('adminNotificationSettings')->excludedRoles()
);
return !in_array(strtolower($role), $excluded, true);
}
public function administratorDashboard()
{
helper('url');
$searchData = service('administratorDashboard')->search((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(
service('administratorDashboard')->metrics((string) $this->schoolYear, (string) $this->semester)
);
}
public function userSearch()
{
$data = service('administratorDashboard')->search((string) $this->request->getGet('query'));
return view('administrator/search_results', $data);
}
public function teacherSubmissionsReport()
{
$semester = (string) ($this->semester !== '' ? $this->semester : (getSemester() ?? $this->semester ?? ''));
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? ''));
if ($schoolYear === '') {
$schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
}
$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)
))));
return view(
'administrator/teacher_submissions',
service('teacherSubmissionReport')->buildReport($semester, $schoolYear, $lowProgressSectionIds)
);
}
public function sendTeacherSubmissionNotifications()
{
$result = service('teacherSubmissionReport')->sendNotifications(
(array) $this->request->getPost(),
(string) ($this->semester !== '' ? $this->semester : (getSemester() ?? '')),
(int) (session()->get('user_id') ?? 0)
);
if (($result['redirect'] ?? '') === 'login') {
return redirect()->to('/login');
}
return redirect()->back()->with((string) ($result['type'] ?? 'info'), (string) ($result['message'] ?? ''));
}
public function notificationsAlerts()
{
if (!$this->canManageAdminNotifications()) {
return redirect()->to('/login');
}
return view('administrator/notifications_alerts', service('adminNotificationSettings')->alertsPage());
}
public function saveNotificationSubjects()
{
if (!$this->canManageAdminNotifications()) {
return redirect()->to('/login');
}
$result = service('adminNotificationSettings')->saveSubjects($this->request->getPost('subjects'));
return redirect()->to('/administrator/notifications_alerts')
->with((string) ($result['type'] ?? 'info'), (string) ($result['message'] ?? ''));
}
public function printNotificationRecipients()
{
if (!$this->canManageAdminNotifications()) {
return redirect()->to('/login');
}
return view('administrator/print_notification_admins', service('adminNotificationSettings')->printRecipientsPage());
}
public function savePrintNotificationRecipients()
{
if (!$this->canManageAdminNotifications()) {
return redirect()->to('/login');
}
$result = service('adminNotificationSettings')->savePrintRecipients((array) $this->request->getPost('notify'));
return redirect()->to('/administrator/print-notifications')
->with((string) ($result['type'] ?? 'info'), (string) ($result['message'] ?? ''));
}
public function studentProfiles()
{
$selectedYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
return view(
'administrator/student_profiles',
service('administratorDirectory')->studentProfiles($selectedYear)
);
}
public function parentProfiles()
{
if ($redirect = $this->redirectWithoutLegacyTermFilters('administrator/parent_profiles')) {
return $redirect;
}
return view('administrator/parent_profile', service('administratorDirectory')->parentProfiles());
}
public function showEnrollmentWithdrawalPage()
{
try {
$schoolYearContext = $this->resolveSchoolYearContext();
$selectedYear = $schoolYearContext->yearName();
$payload = service('enrollmentWithdrawal')->buildRoster($selectedYear, (string) $this->semester);
return view('enroll_withdraw/enrollment_withdrawal', [
'students' => $payload['students'],
'classes' => $payload['classes'],
'selectedYear' => $selectedYear,
'currentYear' => $selectedYear,
'isCurrentYear' => ! $schoolYearContext->isReadonly(),
'missingYear' => $selectedYear === '',
]);
} catch (\Throwable $e) {
log_message('error', 'Enrollment/Withdrawal page error: {msg}', ['msg' => $e->getMessage()]);
return view('errors/html/error_500');
}
}
public function enrollmentWithdrawalData()
{
try {
$selectedYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
$payload = service('enrollmentWithdrawal')->buildRoster($selectedYear, (string) $this->semester);
return $this->response->setJSON([
'students' => $payload['students'],
'classes' => $payload['classes'],
'csrfHash' => csrf_hash(),
'semester' => (string) $this->semester,
'school_year' => (string) $selectedYear,
]);
} catch (\Throwable $e) {
log_message('error', 'enrollmentWithdrawalData error: {msg}', ['msg' => $e->getMessage()]);
return $this->response->setStatusCode(500)->setJSON(['error' => 'Server error']);
}
}
public function showNewStudents(): string
{
return view(
'enroll_withdraw/new-students',
service('enrollmentWithdrawal')->newStudents((string) $this->schoolYear)
);
}
public function adminEnrollmentWithdrawalHandler()
{
$result = service('enrollmentWithdrawal')->updateStatuses(
$this->request->getPost('enrollment_status'),
(string) $this->schoolYear,
(string) $this->semester,
(int) (session()->get('user_id') ?? 0) ?: null
);
return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
->with(!empty($result['ok']) ? 'success' : 'error', (string) ($result['message'] ?? ''));
}
}