Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f5925e10a | |||
| 3cc5546733 | |||
| fe7b99581d | |||
| a6880ea14f |
@@ -26,7 +26,7 @@ SMTP_HOST = smtp.gmail.com
|
||||
SMTP_USER = alrahma.sunday.school@gmail.com
|
||||
SMTP_PASS = "psnp emdq dykw ypul"
|
||||
SMTP_PORT = 587
|
||||
SMTP_ENCRYPTION = tls # was SSL; set to ssl to match 587
|
||||
SMTP_ENCRYPTION = tls # was ssl; use tls for port 587
|
||||
|
||||
# Reply-To for all mail profiles
|
||||
MAIL_DEFAULT_REPLY_TO=alrahma.isgl@gmail.com
|
||||
@@ -35,7 +35,6 @@ MAIL_COMMUNICATION_REPLY_TO=alrahma.isgl@gmail.com
|
||||
MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications"
|
||||
MAIL_PAYMENT_REPLY_TO=alrahma.isgl@gmail.com
|
||||
MAIL_PAYMENT_REPLY_TO_NAME="School Payments"
|
||||
MAIL_DEFAULT_REPLY_TO="alrahma.isgl@gmail.com"
|
||||
|
||||
# Optional sender directory you already use elsewhere
|
||||
MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma No-Reply\"},\
|
||||
@@ -44,8 +43,11 @@ MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al R
|
||||
\"finance\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Finance Office\"}}"
|
||||
|
||||
# Principal notification recipient for TimeOff requests (comma-separated allowed)
|
||||
PRINCIPAL_EMAIL="principal@alrahmaisgl.org"
|
||||
PRINCIPAL_EMAIL="melabidi@alrahmaisgl.org"
|
||||
MAIL_PARENT_REPORT_TO="alrahma.isgl@gmail.com"
|
||||
MAIL_FROM_ADDRESS = "alrahma.sunday.school@gmail.com"
|
||||
|
||||
session.expiration = 43200
|
||||
#--------------------------------------------------------------------
|
||||
# APP
|
||||
#--------------------------------------------------------------------
|
||||
@@ -61,7 +63,7 @@ PRINCIPAL_EMAIL="principal@alrahmaisgl.org"
|
||||
database.default.hostname = 127.0.0.1
|
||||
database.default.database = school
|
||||
database.default.username = root
|
||||
database.default.password = root
|
||||
database.default.password =
|
||||
database.default.DBDriver = MySQLi
|
||||
database.default.DBPrefix =
|
||||
database.default.port = 3306
|
||||
@@ -155,7 +157,9 @@ database.default.port = 3306
|
||||
# LOGGER
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# logger.threshold = 4
|
||||
logger.threshold = 4
|
||||
MAIL_DEBUG = true
|
||||
MAIL_TIMEOUT = 15
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# CURLRequest
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.log
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -1,197 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Controllers\View\EmailController;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\TeacherSubmissionNotificationHistoryModel;
|
||||
use App\Models\UserModel;
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use Config\Database;
|
||||
|
||||
class SendExamDraftDeadlineReminders extends BaseCommand
|
||||
{
|
||||
protected $group = 'Exam Drafts';
|
||||
protected $name = 'exam-drafts:deadline-reminders';
|
||||
protected $description = 'Sends exam draft deadline reminders to teachers who have not submitted drafts.';
|
||||
|
||||
public function run(array $params)
|
||||
{
|
||||
helper('date');
|
||||
|
||||
$configModel = new ConfigurationModel();
|
||||
$schoolYear = (string) ($configModel->getConfig('school_year') ?? '');
|
||||
$semester = (string) ($configModel->getConfig('semester') ?? '');
|
||||
$deadlineValue = trim((string) ($configModel->getConfig('exam_draft_deadline') ?? ''));
|
||||
if ($deadlineValue === '') {
|
||||
CLI::write('exam_draft_deadline is not configured.', 'yellow');
|
||||
return;
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone(config('App')->appTimezone ?? 'UTC');
|
||||
try {
|
||||
$deadline = new \DateTimeImmutable($deadlineValue, $tz);
|
||||
} catch (\Throwable $e) {
|
||||
CLI::write('Invalid exam_draft_deadline value.', 'red');
|
||||
return;
|
||||
}
|
||||
$deadline = $deadline->setTime(0, 0, 0);
|
||||
$today = new \DateTimeImmutable('now', $tz);
|
||||
$today = $today->setTime(0, 0, 0);
|
||||
|
||||
if ($today > $deadline) {
|
||||
CLI::write('Deadline has passed. No reminders sent.', 'yellow');
|
||||
return;
|
||||
}
|
||||
|
||||
$daysToDeadline = (int) $today->diff($deadline)->format('%r%a');
|
||||
if (!$this->shouldSendOnDay($daysToDeadline)) {
|
||||
CLI::write('No reminder scheduled for today.', 'yellow');
|
||||
return;
|
||||
}
|
||||
|
||||
$db = Database::connect();
|
||||
$teacherClassRows = $db->table('teacher_class')
|
||||
->select('teacher_id, class_section_id, school_year, semester')
|
||||
->when($schoolYear !== '', static function ($builder) use ($schoolYear) {
|
||||
return $builder->where('school_year', $schoolYear);
|
||||
})
|
||||
->when($semester !== '', static function ($builder) use ($semester) {
|
||||
return $builder->where('semester', $semester);
|
||||
})
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($teacherClassRows)) {
|
||||
CLI::write('No teacher assignments found.', 'yellow');
|
||||
return;
|
||||
}
|
||||
|
||||
$draftTable = 'exam_drafts';
|
||||
$fields = $db->getFieldNames($draftTable);
|
||||
$teacherColumn = in_array('teacher_id', $fields, true) ? 'teacher_id' : 'author_id';
|
||||
$hasStatusColumn = in_array('status', $fields, true);
|
||||
$hasIsLegacyColumn = in_array('is_legacy', $fields, true);
|
||||
|
||||
$draftsQuery = $db->table($draftTable)
|
||||
->select("{$teacherColumn} AS teacher_id, class_section_id")
|
||||
->when($schoolYear !== '', static function ($builder) use ($schoolYear) {
|
||||
return $builder->where('school_year', $schoolYear);
|
||||
})
|
||||
->when($semester !== '', static function ($builder) use ($semester) {
|
||||
return $builder->where('semester', $semester);
|
||||
});
|
||||
|
||||
if ($hasStatusColumn) {
|
||||
$draftsQuery = $draftsQuery->where('status !=', 'legacy');
|
||||
}
|
||||
if ($hasIsLegacyColumn) {
|
||||
$draftsQuery = $draftsQuery->where('is_legacy', 0);
|
||||
}
|
||||
|
||||
$draftRows = $draftsQuery->get()->getResultArray();
|
||||
$submittedMap = [];
|
||||
foreach ($draftRows as $row) {
|
||||
$key = (int) ($row['teacher_id'] ?? 0) . '|' . (int) ($row['class_section_id'] ?? 0);
|
||||
$submittedMap[$key] = true;
|
||||
}
|
||||
|
||||
$missingByTeacher = [];
|
||||
foreach ($teacherClassRows as $row) {
|
||||
$teacherId = (int) ($row['teacher_id'] ?? 0);
|
||||
$classSectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
if ($teacherId <= 0 || $classSectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$key = $teacherId . '|' . $classSectionId;
|
||||
if (!isset($submittedMap[$key])) {
|
||||
$missingByTeacher[$teacherId][] = $classSectionId;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($missingByTeacher)) {
|
||||
CLI::write('All teachers have submitted drafts.', 'green');
|
||||
return;
|
||||
}
|
||||
|
||||
$userModel = new UserModel();
|
||||
$classSectionModel = new ClassSectionModel();
|
||||
$historyModel = new TeacherSubmissionNotificationHistoryModel();
|
||||
$mailer = new EmailController();
|
||||
|
||||
$classSections = $classSectionModel
|
||||
->select('class_section_id, class_section_name')
|
||||
->findAll();
|
||||
$classLookup = [];
|
||||
foreach ($classSections as $section) {
|
||||
$classLookup[(int) ($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? '';
|
||||
}
|
||||
|
||||
$todayStamp = $today->format('Y-m-d');
|
||||
foreach ($missingByTeacher as $teacherId => $sectionIds) {
|
||||
$teacher = $userModel->find($teacherId);
|
||||
$email = $teacher['email'] ?? '';
|
||||
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$alreadySent = $historyModel
|
||||
->where('teacher_id', $teacherId)
|
||||
->where('notification_category', 'exam_draft_deadline')
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->like('sent_at', $todayStamp)
|
||||
->countAllResults() > 0;
|
||||
if ($alreadySent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classNames = array_map(static function ($id) use ($classLookup) {
|
||||
return $classLookup[(int) $id] ?? "Class {$id}";
|
||||
}, $sectionIds);
|
||||
$classList = implode(', ', $classNames);
|
||||
$teacherName = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
|
||||
if ($teacherName === '') {
|
||||
$teacherName = 'Teacher';
|
||||
}
|
||||
|
||||
$subject = 'Reminder: Exam draft submission deadline';
|
||||
$body = '<p>Dear ' . esc($teacherName) . ',</p>'
|
||||
. '<p>This is a reminder to submit your exam draft(s) for: ' . esc($classList) . '.</p>'
|
||||
. '<p>Deadline: <strong>' . esc($deadline->format('Y-m-d')) . '</strong></p>'
|
||||
. '<p>Please submit your draft at <a href="' . esc(base_url('teacher/exam-drafts')) . '">Teacher Exam Drafts</a>.</p>'
|
||||
. '<p>Thank you.</p>';
|
||||
|
||||
$status = 'failed';
|
||||
if ($mailer->sendEmail($email, $subject, $body, 'notifications')) {
|
||||
$status = 'sent';
|
||||
CLI::write("Sent reminder to {$email}", 'green');
|
||||
}
|
||||
|
||||
$historyModel->insert([
|
||||
'teacher_id' => $teacherId,
|
||||
'class_section_id' => 0,
|
||||
'admin_id' => null,
|
||||
'notification_category' => 'exam_draft_deadline',
|
||||
'message' => strip_tags($body),
|
||||
'status' => $status,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'sent_at' => utc_now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldSendOnDay(int $daysToDeadline): bool
|
||||
{
|
||||
if ($daysToDeadline < 0) {
|
||||
return false;
|
||||
}
|
||||
if ($daysToDeadline <= 4) {
|
||||
return true;
|
||||
}
|
||||
return in_array($daysToDeadline, [28, 21, 14, 7], true);
|
||||
}
|
||||
}
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+1
-1
@@ -82,7 +82,7 @@ class App extends BaseConfig
|
||||
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
||||
|
|
||||
*/
|
||||
public string $permittedURIChars = 'a-z 0-9~%.:_\-,';
|
||||
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+15
-23
@@ -9,20 +9,13 @@ class Database extends Config
|
||||
public string $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR;
|
||||
public string $defaultGroup = 'default';
|
||||
|
||||
public array $default = [];
|
||||
public array $tests = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->default = [
|
||||
public array $default = [
|
||||
'DSN' => '',
|
||||
'hostname' => env('database.default.hostname'),
|
||||
'username' => env('database.default.username'),
|
||||
'password' => env('database.default.password'),
|
||||
'database' => env('database.default.database'),
|
||||
'DBDriver' => env('database.default.DBDriver', 'MySQLi'),
|
||||
'hostname' => 'localhost',
|
||||
'username' => 'u280815660_melabidi',
|
||||
'password' => '>tNxlRzP/W8',
|
||||
'database' => 'u280815660_school',
|
||||
'DBDriver' => 'MySQLi',
|
||||
'DBPrefix' => '',
|
||||
'pConnect' => false,
|
||||
'DBDebug' => (ENVIRONMENT !== 'development'),
|
||||
@@ -33,17 +26,17 @@ class Database extends Config
|
||||
'compress' => false,
|
||||
'strictOn' => false,
|
||||
'failover' => [],
|
||||
'port' => (int) env('database.default.port', 3306),
|
||||
'port' => 3306,
|
||||
];
|
||||
|
||||
$this->tests = [
|
||||
public array $tests = [
|
||||
'DSN' => '',
|
||||
'hostname' => env('database.tests.hostname', env('database.default.hostname')),
|
||||
'username' => env('database.tests.username', env('database.default.username')),
|
||||
'password' => env('database.tests.password', env('database.default.password')),
|
||||
'database' => env('database.tests.database', env('database.default.database')),
|
||||
'DBDriver' => env('database.tests.DBDriver', env('database.default.DBDriver', 'MySQLi')),
|
||||
'DBPrefix' => env('database.tests.DBPrefix', 'db_'),
|
||||
'hostname' => 'localhost',
|
||||
'username' => 'u280815660_melabidi',
|
||||
'password' => '>tNxlRzP/W8',
|
||||
'database' => 'u280815660_school',
|
||||
'DBDriver' => 'MySQLi',
|
||||
'DBPrefix' => 'db_',
|
||||
'pConnect' => false,
|
||||
'DBDebug' => true,
|
||||
'charset' => 'utf8',
|
||||
@@ -53,7 +46,6 @@ class Database extends Config
|
||||
'compress' => false,
|
||||
'strictOn' => false,
|
||||
'failover' => [],
|
||||
'port' => (int) env('database.tests.port', env('database.default.port', 3306)),
|
||||
'port' => 3306,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+3
-19
@@ -230,10 +230,6 @@ $routes->get('reset_password', 'View\UserController::resetPassword');
|
||||
|
||||
//$routes->get('/blocked', 'View\UserController::blocked');
|
||||
|
||||
$routes->get('confirm_authorized_user', 'View\AuthorizedUsersController::confirm');
|
||||
$routes->get('set_authorized_user_password/(:num)', 'View\AuthorizedUsersController::setPassword/$1');
|
||||
$routes->post('set_authorized_user_password/(:num)', 'View\AuthorizedUsersController::savePassword/$1');
|
||||
|
||||
$routes->post('assign_class_student', 'View\StudentController::assignClassStudent');
|
||||
$routes->post('remove_class_student', 'View\StudentController::removeClassStudent');
|
||||
$routes->post('administrator/remove_class_student', 'View\StudentController::removeClassStudent'); // alias to avoid 404s
|
||||
@@ -363,7 +359,6 @@ $routes->get('/teacher/teacher_contactus', 'View\TeacherController::contactusTea
|
||||
|
||||
$routes->get('/teacher/exam-drafts', 'View\ExamDraftController::teacherIndex', ['filter' => 'auth:teacher,teacher_assistant,teacher_dashboard,read']);
|
||||
$routes->post('/teacher/exam-drafts', 'View\ExamDraftController::teacherStore', ['filter' => 'auth:teacher,teacher_assistant,teacher_dashboard,read']);
|
||||
$routes->get('/teacher/exam-drafts/status', 'View\ExamDraftController::teacherStatusFeed', ['filter' => 'auth:teacher,teacher_assistant,teacher_dashboard,read']);
|
||||
|
||||
|
||||
|
||||
@@ -393,8 +388,8 @@ $routes->get('print-requests/file/(:segment)', 'PrintRequests::serveFile/$1', ['
|
||||
$routes->get('print-requests/file/(:segment)/(:alpha)', 'PrintRequests::serveFile/$1/$2', ['filter' => 'auth:teacher,teacher_assistant,admin']);
|
||||
$routes->get('uploads/print_requests/(:segment)', 'PrintRequests::serveFile/$1', ['filter' => 'auth:teacher,teacher_assistant,admin']);
|
||||
|
||||
$routes->get('exam-drafts/files/teacher/(:segment)', 'View\FilesController::examDraftTeacher/$1', ['filter' => 'auth:teacher,teacher_assistant,admin,administrator,principal']);
|
||||
$routes->get('exam-drafts/files/final/(:segment)', 'View\FilesController::examDraftFinal/$1', ['filter' => 'auth:teacher,teacher_assistant,admin,administrator,principal']);
|
||||
$routes->get('exam-drafts/files/teacher/(:segment)', 'View\FilesController::examDraftTeacher/$1', ['filter' => 'auth:teacher,teacher_assistant,admin']);
|
||||
$routes->get('exam-drafts/files/final/(:segment)', 'View\FilesController::examDraftFinal/$1', ['filter' => 'auth:teacher,teacher_assistant,admin']);
|
||||
|
||||
|
||||
|
||||
@@ -406,8 +401,6 @@ $routes->get('teacher/progress/submit', 'ClassProgressController::create', ['fil
|
||||
$routes->post('teacher/progress/store', 'ClassProgressController::store', ['filter' => 'auth:teacher,teacher_assistant']);
|
||||
$routes->get('teacher/progress/history', 'ClassProgressController::history', ['filter' => 'auth:teacher,teacher_assistant']);
|
||||
$routes->get('teacher/progress/view/(:num)', 'ClassProgressController::view/$1', ['filter' => 'auth:teacher,teacher_assistant']);
|
||||
$routes->get('teacher/progress/edit/(:num)', 'ClassProgressController::edit/$1', ['filter' => 'auth:teacher,teacher_assistant']);
|
||||
$routes->post('teacher/progress/update/(:num)', 'ClassProgressController::update/$1', ['filter' => 'auth:teacher,teacher_assistant']);
|
||||
$routes->get('teacher/progress/attachment/(:num)', 'ClassProgressController::attachment/$1', ['filter' => 'auth:teacher,teacher_assistant']);
|
||||
$routes->get('teacher/progress/attachment-file/(:num)', 'ClassProgressController::attachmentFile/$1', ['filter' => 'auth:teacher,teacher_assistant']);
|
||||
$routes->get('parent/progress', 'ParentProgressController::index', ['filter' => 'auth:parent']);
|
||||
@@ -506,7 +499,6 @@ $routes->get('grading/project/(:num)', 'View\ProjectController::showProjectMngt/
|
||||
$routes->post('grading/updateProject', 'View\ProjectController::updateProject');
|
||||
|
||||
$routes->get('grading/below-60', 'View\GradingController::belowSixty', ['filter' => 'auth:read']);
|
||||
$routes->get('grading/below-60/email/edit', 'View\GradingController::editBelowSixtyEmail', ['filter' => 'auth:read']);
|
||||
$routes->post('grading/below-60/email', 'View\GradingController::sendBelowSixtyEmail', ['filter' => 'auth:read']);
|
||||
$routes->post('grading/below-60/status', 'View\GradingController::updateBelowSixtyStatus', ['filter' => 'auth:read']);
|
||||
$routes->get('grading/below-60/schedule', 'View\GradingController::scheduleBelowSixty', ['filter' => 'auth:read']);
|
||||
@@ -535,12 +527,9 @@ $routes->post('payment/event_charges', 'View\EventController::eventUpdate');
|
||||
|
||||
// Parent event participation
|
||||
$routes->get('administrator/event-charges', 'View\EventController::eventShow');
|
||||
$routes->post('administrator/event-charges/remove/(:num)', 'View\EventController::removeCharge/$1');
|
||||
$routes->post('administrator/event-charges/payment/(:num)', 'View\EventController::toggleEventPayment/$1');
|
||||
$routes->post('administrator/event-charges/waiver/(:num)', 'View\EventController::toggleWaiverStatus/$1');
|
||||
$routes->get('administrator/get-students-with-charges', 'View\EventController::getStudentsWithCharges');
|
||||
|
||||
$routes->get('parent/events', 'View\ParentController::parentEventPage', ['filter' => 'auth:parent']);
|
||||
$routes->get('parent/events', 'View\ParentController::parentEventPage');
|
||||
// parent event participation page
|
||||
$routes->post('parent/updateParticipation', 'View\ParentController::updateParticipation'); // handle parent participation updates
|
||||
|
||||
@@ -745,9 +734,6 @@ $routes->post('/administrator/teacher-submissions/notify', 'View\AdministratorCo
|
||||
$routes->get('/administrator/exam-drafts', 'View\ExamDraftController::adminIndex', ['filter' => 'auth:admin']);
|
||||
$routes->post('/administrator/exam-drafts/review', 'View\ExamDraftController::adminReview', ['filter' => 'auth:admin']);
|
||||
$routes->post('/administrator/exam-drafts/upload-legacy', 'View\ExamDraftController::adminUploadLegacy', ['filter' => 'auth:admin']);
|
||||
$routes->get('/principal/exam-drafts', 'View\ExamDraftController::principalIndex', ['filter' => 'auth:admin']);
|
||||
$routes->post('/principal/exam-drafts/review', 'View\ExamDraftController::principalReview', ['filter' => 'auth:admin']);
|
||||
$routes->post('/principal/exam-drafts/upload-legacy', 'View\ExamDraftController::principalUploadLegacy', ['filter' => 'auth:admin']);
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
@@ -1016,8 +1002,6 @@ $routes->group('family', static function ($routes) {
|
||||
$routes->get('index', 'View\FamilyAdminController::index');
|
||||
$routes->get('search', 'View\FamilyAdminController::search');
|
||||
$routes->get('card', 'View\FamilyAdminController::card');
|
||||
$routes->get('compose-email', 'View\FamilyAdminController::composeEmail');
|
||||
$routes->post('compose-email/send', 'View\FamilyAdminController::sendComposeEmail');
|
||||
});
|
||||
// Convenience alias
|
||||
$routes->get('family', 'View\FamilyAdminController::index');
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+38
-102
@@ -111,23 +111,6 @@ class AdminProgressController extends BaseController
|
||||
);
|
||||
$sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays);
|
||||
$sectionSubjectCounts = $this->buildSectionSubjectCounts($rows);
|
||||
$lowProgressSectionIds = [];
|
||||
if ($expectedDays > 0) {
|
||||
foreach ($filteredSections as $section) {
|
||||
$sectionId = (int) ($section['class_section_id'] ?? 0);
|
||||
if ($sectionId === 0) {
|
||||
continue;
|
||||
}
|
||||
$stat = $sectionStats[$sectionId] ?? null;
|
||||
$percent = $stat['percent'] ?? 0;
|
||||
if ($stat === null) {
|
||||
$percent = 0;
|
||||
}
|
||||
if ($percent < 50) {
|
||||
$lowProgressSectionIds[] = $sectionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view('admin/class_progress_list', [
|
||||
'reportGroupsBySection' => $reportGroupsBySection,
|
||||
@@ -138,7 +121,6 @@ class AdminProgressController extends BaseController
|
||||
'sectionStats' => $sectionStats,
|
||||
'sectionSubjectCounts' => $sectionSubjectCounts,
|
||||
'expectedDays' => $expectedDays,
|
||||
'lowProgressSectionIds' => $lowProgressSectionIds,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -432,11 +414,13 @@ class AdminProgressController extends BaseController
|
||||
$allowedSubjects = array_values(array_filter($allowedSubjects));
|
||||
|
||||
$counts = [];
|
||||
$latestWeekBySection = [];
|
||||
$sectionClassMap = [];
|
||||
foreach ($rows as $row) {
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
if ($sectionId === 0 || $subject === '') {
|
||||
$weekStart = (string) ($row['week_start'] ?? '');
|
||||
if ($sectionId === 0 || $subject === '' || $weekStart === '') {
|
||||
continue;
|
||||
}
|
||||
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
|
||||
@@ -445,41 +429,45 @@ class AdminProgressController extends BaseController
|
||||
if (! isset($sectionClassMap[$sectionId])) {
|
||||
$sectionClassMap[$sectionId] = $this->classSectionModel->getClassId($sectionId);
|
||||
}
|
||||
if (
|
||||
! isset($latestWeekBySection[$sectionId])
|
||||
|| $weekStart > $latestWeekBySection[$sectionId]
|
||||
) {
|
||||
$latestWeekBySection[$sectionId] = $weekStart;
|
||||
}
|
||||
}
|
||||
|
||||
$curriculumUnits = $this->buildCurriculumUnitMap(array_values(array_filter($sectionClassMap)));
|
||||
$curriculumChapters = $this->buildCurriculumChapterMap(array_values(array_filter($sectionClassMap)));
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
if ($sectionId === 0 || $subject === '') {
|
||||
$weekStart = (string) ($row['week_start'] ?? '');
|
||||
if ($sectionId === 0 || $subject === '' || $weekStart === '') {
|
||||
continue;
|
||||
}
|
||||
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
|
||||
continue;
|
||||
}
|
||||
if (empty($latestWeekBySection[$sectionId]) || $weekStart !== $latestWeekBySection[$sectionId]) {
|
||||
continue;
|
||||
}
|
||||
$subjectSlug = $this->resolveSubjectSlug($subject);
|
||||
$classId = $sectionClassMap[$sectionId] ?? null;
|
||||
$chapterToUnit = [];
|
||||
if ($classId && $subjectSlug && ! empty($curriculumUnits[$classId][$subjectSlug]['chapter_to_unit'])) {
|
||||
$chapterToUnit = $curriculumUnits[$classId][$subjectSlug]['chapter_to_unit'];
|
||||
}
|
||||
$unitKeys = $this->extractUnitKeys((string) ($row['unit_title'] ?? ''), $chapterToUnit);
|
||||
foreach ($unitKeys as $unitKey) {
|
||||
$key = $subjectSlug . '|' . $unitKey;
|
||||
$counts[$sectionId][$key] = true;
|
||||
$chapterSet = [];
|
||||
if ($classId && $subjectSlug && ! empty($curriculumChapters[$classId][$subjectSlug])) {
|
||||
$chapterSet = $curriculumChapters[$classId][$subjectSlug];
|
||||
}
|
||||
$counts[$sectionId] = ($counts[$sectionId] ?? 0) + $this->countChapterSegments(
|
||||
(string) ($row['unit_title'] ?? ''),
|
||||
$chapterSet
|
||||
);
|
||||
}
|
||||
|
||||
$totals = [];
|
||||
foreach ($counts as $sectionId => $unitSet) {
|
||||
$totals[$sectionId] = count($unitSet);
|
||||
return $counts;
|
||||
}
|
||||
|
||||
return $totals;
|
||||
}
|
||||
|
||||
protected function buildCurriculumUnitMap(array $classIds): array
|
||||
protected function buildCurriculumChapterMap(array $classIds): array
|
||||
{
|
||||
$classIds = array_values(array_filter(array_map('intval', $classIds)));
|
||||
if (empty($classIds)) {
|
||||
@@ -495,15 +483,10 @@ class AdminProgressController extends BaseController
|
||||
$classId = (int) ($row['class_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
$chapter = trim((string) ($row['chapter_name'] ?? ''));
|
||||
$unitNumber = $row['unit_number'] ?? null;
|
||||
if ($classId === 0 || $subject === '' || $chapter === '') {
|
||||
continue;
|
||||
}
|
||||
if ($unitNumber === null || $unitNumber === '') {
|
||||
continue;
|
||||
}
|
||||
$unitKey = (string) $unitNumber;
|
||||
$map[$classId][$subject]['chapter_to_unit'][$chapter] = $unitKey;
|
||||
$map[$classId][$subject][$chapter] = true;
|
||||
}
|
||||
|
||||
return $map;
|
||||
@@ -521,93 +504,46 @@ class AdminProgressController extends BaseController
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function countUnitSegments(string $unitTitle, array $chapterToUnit): int
|
||||
protected function countChapterSegments(string $unitTitle, array $chapterSet): int
|
||||
{
|
||||
$unitTitle = trim($unitTitle);
|
||||
if ($unitTitle === '') {
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
$parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== '');
|
||||
if (! $parts) {
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$seen = [];
|
||||
foreach ($parts as $part) {
|
||||
[$unitPart, $chapterPart] = $this->splitUnitChapterSegment($part);
|
||||
$key = $this->resolveUnitKey($unitPart, $chapterPart, $chapterToUnit);
|
||||
if ($key === '') {
|
||||
$chapter = $this->extractChapterFromSegment($part);
|
||||
$key = $chapter !== '' ? $chapter : $part;
|
||||
if (! empty($chapterSet) && $chapter !== '' && empty($chapterSet[$chapter])) {
|
||||
$key = $part;
|
||||
}
|
||||
if (isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$key] = true;
|
||||
$count++;
|
||||
}
|
||||
|
||||
return count($seen);
|
||||
return $count > 0 ? $count : 1;
|
||||
}
|
||||
|
||||
protected function splitUnitChapterSegment(string $segment): array
|
||||
protected function extractChapterFromSegment(string $segment): string
|
||||
{
|
||||
$segment = trim($segment);
|
||||
if ($segment === '') {
|
||||
return ['', ''];
|
||||
return '';
|
||||
}
|
||||
$pos = strrpos($segment, '/');
|
||||
if ($pos === false) {
|
||||
return [$segment, ''];
|
||||
return $segment;
|
||||
}
|
||||
$unitPart = trim(substr($segment, 0, $pos));
|
||||
$chapterPart = trim(substr($segment, $pos + 1));
|
||||
return [$unitPart, $chapterPart];
|
||||
}
|
||||
|
||||
protected function extractUnitKeys(string $unitTitle, array $chapterToUnit): array
|
||||
{
|
||||
$unitTitle = trim($unitTitle);
|
||||
if ($unitTitle === '') {
|
||||
return [];
|
||||
}
|
||||
$parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== '');
|
||||
if (! $parts) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$keys = [];
|
||||
foreach ($parts as $part) {
|
||||
[$unitPart, $chapterPart] = $this->splitUnitChapterSegment($part);
|
||||
$key = $this->resolveUnitKey($unitPart, $chapterPart, $chapterToUnit);
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$keys[$key] = true;
|
||||
}
|
||||
|
||||
return array_keys($keys);
|
||||
}
|
||||
|
||||
protected function resolveUnitKey(string $unitPart, string $chapterPart, array $chapterToUnit): string
|
||||
{
|
||||
if ($chapterPart !== '' && ! empty($chapterToUnit[$chapterPart])) {
|
||||
return (string) $chapterToUnit[$chapterPart];
|
||||
}
|
||||
if (empty($chapterToUnit)) {
|
||||
if (preg_match('/\bunit\s*(\d+)\b/i', $unitPart, $matches)) {
|
||||
return (string) $matches[1];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
if (preg_match('/\bunit\s*(\d+)\b/i', $unitPart, $matches)) {
|
||||
return (string) $matches[1];
|
||||
}
|
||||
if ($unitPart !== '') {
|
||||
return $unitPart;
|
||||
}
|
||||
if ($chapterPart !== '') {
|
||||
return $chapterPart;
|
||||
}
|
||||
return '';
|
||||
return trim(substr($segment, $pos + 1));
|
||||
}
|
||||
|
||||
protected function buildSectionStat(int $submitted, int $expectedDays): array
|
||||
|
||||
Executable → Regular
Executable → Regular
+6
-76
@@ -56,32 +56,13 @@ class AuthController extends Controller
|
||||
|
||||
public function loginMask()
|
||||
{
|
||||
$redirectTo = $this->sanitizeRedirectTarget((string) ($this->request->getGet('redirect_to') ?? ''));
|
||||
|
||||
if (session()->get('is_logged_in')) {
|
||||
if ($redirectTo !== null) {
|
||||
return redirect()->to($redirectTo);
|
||||
}
|
||||
|
||||
$roles = session()->get('roles') ?? [];
|
||||
if (empty($roles) && session()->get('role')) {
|
||||
$roles = [session()->get('role')];
|
||||
}
|
||||
|
||||
if (!empty($roles)) {
|
||||
return $this->redirectToDashboard($roles);
|
||||
}
|
||||
}
|
||||
|
||||
return view('user/login', [
|
||||
'redirect_to' => $redirectTo ?? '',
|
||||
]);
|
||||
// Serve the login view directly here
|
||||
return view('user/login'); // Adjust the view path as needed
|
||||
}
|
||||
|
||||
public function login()
|
||||
{
|
||||
log_message('info', 'Processing login form submission.');
|
||||
$redirectTo = $this->sanitizeRedirectTarget((string) ($this->request->getPost('redirect_to') ?? $this->request->getGet('redirect_to') ?? ''));
|
||||
|
||||
// Step 1: Get email, password, and IP from the request
|
||||
$email = $this->request->getPost('email');
|
||||
@@ -117,7 +98,7 @@ class AuthController extends Controller
|
||||
$this->logLoginAttempt($user['id'], $user['email'], $ip, $this->request->getUserAgent());
|
||||
|
||||
// ✅ Step 7: Call loginUser() to set session and redirect
|
||||
return $this->loginUser($user, $redirectTo);
|
||||
return $this->loginUser($user);
|
||||
} else {
|
||||
// Step 8: Password mismatch — log failed attempt
|
||||
$this->handleFailedLogin($user['id'], $user['email'], $ip);
|
||||
@@ -488,7 +469,6 @@ class AuthController extends Controller
|
||||
// Generate a secure token for the password reset
|
||||
helper('text');
|
||||
$token = bin2hex(random_bytes(48));
|
||||
$tokenHash = hash('sha256', $token);
|
||||
|
||||
// Calculate the expiration time for the token (1 hour from now)
|
||||
$expires_at = Time::now()->addHours(1);
|
||||
@@ -497,7 +477,7 @@ class AuthController extends Controller
|
||||
$passwordResetModel = new PasswordResetModel();
|
||||
$passwordResetModel->insert([
|
||||
'email' => $email,
|
||||
'token' => $tokenHash,
|
||||
'token' => $token,
|
||||
'created_at' => Time::now(),
|
||||
'expires_at' => $expires_at,
|
||||
]);
|
||||
@@ -510,7 +490,7 @@ class AuthController extends Controller
|
||||
return true;
|
||||
}
|
||||
|
||||
private function loginUser($user, ?string $redirectTo = null)
|
||||
private function loginUser($user)
|
||||
{
|
||||
$userRoleModel = new UserRoleModel();
|
||||
$roles = $userRoleModel->select('roles.name')
|
||||
@@ -544,17 +524,9 @@ class AuthController extends Controller
|
||||
if (count($roleNames) === 1) {
|
||||
// One role → set and redirect directly
|
||||
session()->set('role', $roleNames[0]);
|
||||
if ($redirectTo !== null) {
|
||||
return redirect()->to($redirectTo);
|
||||
}
|
||||
return $this->redirectToDashboard([$roleNames[0]]);
|
||||
} else {
|
||||
// Multiple roles → redirect to role selection view
|
||||
if ($redirectTo !== null) {
|
||||
session()->set('post_login_redirect', $redirectTo);
|
||||
} else {
|
||||
session()->remove('post_login_redirect');
|
||||
}
|
||||
return redirect()->to('/select-role');
|
||||
}
|
||||
|
||||
@@ -619,17 +591,12 @@ private function redirectToDashboard(array $roles)
|
||||
{
|
||||
$selectedRole = $this->request->getPost('selected_role');
|
||||
$availableRoles = session()->get('roles');
|
||||
$redirectTo = $this->sanitizeRedirectTarget((string) ($this->request->getPost('redirect_to') ?? session()->get('post_login_redirect') ?? ''));
|
||||
|
||||
if (!$selectedRole || !in_array($selectedRole, $availableRoles)) {
|
||||
return redirect()->to('/select-role')->with('error', 'Invalid role selected.');
|
||||
}
|
||||
|
||||
session()->set('role', $selectedRole);
|
||||
session()->remove('post_login_redirect');
|
||||
if ($redirectTo !== null) {
|
||||
return redirect()->to($redirectTo);
|
||||
}
|
||||
return $this->redirectToDashboard([$selectedRole]);
|
||||
}
|
||||
|
||||
@@ -641,44 +608,7 @@ private function redirectToDashboard(array $roles)
|
||||
return redirect()->to('/login')->with('error', 'No roles available.');
|
||||
}
|
||||
|
||||
return view('auth/select_role', [
|
||||
'roles' => $roles,
|
||||
'redirect_to' => (string) (session()->get('post_login_redirect') ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
private function sanitizeRedirectTarget(string $redirectTo): ?string
|
||||
{
|
||||
$redirectTo = trim($redirectTo);
|
||||
if ($redirectTo === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('#^https?://#i', $redirectTo)) {
|
||||
$appHost = (string) parse_url(base_url('/'), PHP_URL_HOST);
|
||||
$targetHost = (string) parse_url($redirectTo, PHP_URL_HOST);
|
||||
$targetPath = (string) parse_url($redirectTo, PHP_URL_PATH);
|
||||
$targetQuery = (string) parse_url($redirectTo, PHP_URL_QUERY);
|
||||
|
||||
if ($appHost === '' || $targetHost === '' || strcasecmp($appHost, $targetHost) !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$redirectTo = $targetPath !== '' ? $targetPath : '/';
|
||||
if ($targetQuery !== '') {
|
||||
$redirectTo .= '?' . $targetQuery;
|
||||
}
|
||||
}
|
||||
|
||||
if (!str_starts_with($redirectTo, '/')) {
|
||||
$redirectTo = '/' . ltrim($redirectTo, '/');
|
||||
}
|
||||
|
||||
if (str_starts_with($redirectTo, '//')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $redirectTo;
|
||||
return view('auth/select_role', ['roles' => $roles]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Executable → Regular
Executable → Regular
+1
-380
@@ -28,10 +28,6 @@ class ClassProgressController extends BaseController
|
||||
'db_subject' => 'Quran/Arabic',
|
||||
],
|
||||
];
|
||||
|
||||
/** Unit column for teacher custom Islamic / Quran rows; must match teacher form JS. Segment: "Custom / {text}". */
|
||||
public const CUSTOM_UNIT_ROW_LABEL = 'Custom';
|
||||
|
||||
protected ClassProgressReportModel $reportModel;
|
||||
protected ClassProgressAttachmentModel $attachmentModel;
|
||||
protected TeacherClassModel $teacherClassModel;
|
||||
@@ -102,10 +98,6 @@ class ClassProgressController extends BaseController
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
if (! $this->hasIslamicUnitSelection()) {
|
||||
return redirect()->back()->withInput()->with('error', 'Please select at least one Islamic Studies unit.');
|
||||
}
|
||||
|
||||
$attachmentErrors = $this->validateAttachmentFiles($subjectSections);
|
||||
if (! empty($attachmentErrors)) {
|
||||
return redirect()->back()->withInput()->with('errors', $attachmentErrors);
|
||||
@@ -127,32 +119,6 @@ class ClassProgressController extends BaseController
|
||||
return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.');
|
||||
}
|
||||
|
||||
$confirmOverwrite = (bool) $this->request->getPost('confirm_overwrite');
|
||||
$existingReports = $this->reportModel
|
||||
->select('id')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('week_start', $weekStart)
|
||||
->where('teacher_id', $teacherId)
|
||||
->findAll();
|
||||
|
||||
if (! $confirmOverwrite && ! empty($existingReports)) {
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->with('warning', 'A progress report already exists for this week, are you sure you want to override it?')
|
||||
->with('confirm_overwrite', true);
|
||||
}
|
||||
|
||||
if ($confirmOverwrite && ! empty($existingReports)) {
|
||||
$existingIds = array_values(array_filter(array_map(
|
||||
static fn (array $row): int => (int) ($row['id'] ?? 0),
|
||||
$existingReports
|
||||
)));
|
||||
if (! empty($existingIds)) {
|
||||
$this->attachmentModel->whereIn('report_id', $existingIds)->delete();
|
||||
$this->reportModel->whereIn('id', $existingIds)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
$status = self::DEFAULT_STATUS;
|
||||
|
||||
$reportsCreated = 0;
|
||||
@@ -310,262 +276,6 @@ class ClassProgressController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$teacherId = (int) session()->get('user_id');
|
||||
$row = $this->reportModel
|
||||
->select('class_progress_reports.*, cs.class_section_name')
|
||||
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
||||
->where('class_progress_reports.id', (int) $id)
|
||||
->first();
|
||||
|
||||
if (! $row) {
|
||||
throw new PageNotFoundException('Progress report not found.');
|
||||
}
|
||||
|
||||
[$semester, $schoolYear] = $this->resolveCurrentTerm();
|
||||
$allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear);
|
||||
if (empty($allowedTeacherIds)) {
|
||||
if ($teacherId !== (int) $row['teacher_id']) {
|
||||
throw new PageNotFoundException('Progress report not found.');
|
||||
}
|
||||
$allowedTeacherIds = [(int) $row['teacher_id']];
|
||||
} elseif (! in_array($teacherId, $allowedTeacherIds, true)) {
|
||||
throw new PageNotFoundException('Progress report not found.');
|
||||
}
|
||||
|
||||
$weeklyReports = $this->reportModel
|
||||
->select('class_progress_reports.*')
|
||||
->whereIn('teacher_id', $allowedTeacherIds)
|
||||
->where('class_section_id', $row['class_section_id'])
|
||||
->where('week_start', $row['week_start'])
|
||||
->orderBy('subject', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$reportMap = [];
|
||||
foreach ($weeklyReports as $report) {
|
||||
$subject = (string) ($report['subject'] ?? '');
|
||||
if ($subject === '') {
|
||||
continue;
|
||||
}
|
||||
$reportMap[$subject] = $report;
|
||||
}
|
||||
|
||||
$subjectReports = [];
|
||||
foreach (self::SUBJECT_SECTIONS as $slug => $section) {
|
||||
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
||||
$report = $reportMap[$subjectName] ?? null;
|
||||
if (! $report) {
|
||||
continue;
|
||||
}
|
||||
$parsed = $this->parseUnitChapterSummary((string) ($report['unit_title'] ?? ''));
|
||||
$subjectReports[$slug] = [
|
||||
'report_id' => (int) ($report['id'] ?? 0),
|
||||
'covered' => $report['covered'] ?? '',
|
||||
'homework' => $report['homework'] ?? '',
|
||||
'unit_title' => $report['unit_title'] ?? '',
|
||||
'unit_values' => $parsed['units'],
|
||||
'chapter_values' => $parsed['chapters'],
|
||||
];
|
||||
}
|
||||
|
||||
$assignments = $this->loadTeacherSections($teacherId);
|
||||
$classId = null;
|
||||
$classSectionName = $row['class_section_name'] ?? '';
|
||||
foreach ($assignments as $assignment) {
|
||||
if ((int) ($assignment['class_section_id'] ?? 0) === (int) $row['class_section_id']) {
|
||||
$classId = $assignment['class_id'] ?? null;
|
||||
$classSectionName = $assignment['class_section_name'] ?? $classSectionName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$subjectCurriculum = [];
|
||||
if ($classId) {
|
||||
foreach (self::SUBJECT_SECTIONS as $slug => $section) {
|
||||
$subjectCurriculum[$slug] = $this->curriculumModel->getOptionsForClass((int) $classId, $slug);
|
||||
}
|
||||
}
|
||||
|
||||
$sundayOptions = $this->buildSundayOptions();
|
||||
if (!in_array($row['week_start'], $sundayOptions, true)) {
|
||||
array_unshift($sundayOptions, $row['week_start']);
|
||||
}
|
||||
|
||||
return view('teacher/class_progress_submit', [
|
||||
'subjectSections' => self::SUBJECT_SECTIONS,
|
||||
'subjectCurriculum' => $subjectCurriculum,
|
||||
'classSectionId' => $row['class_section_id'],
|
||||
'classSectionName' => $classSectionName,
|
||||
'classId' => $classId,
|
||||
'sundayOptions' => $sundayOptions,
|
||||
'defaultWeekStart' => $row['week_start'],
|
||||
'existingWeekEnd' => $row['week_end'],
|
||||
'existingReports' => $subjectReports,
|
||||
'isEdit' => true,
|
||||
'formAction' => base_url('teacher/progress/update/' . (int) $row['id']),
|
||||
'submitLabel' => 'Update Progress',
|
||||
]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
$teacherId = (int) session()->get('user_id');
|
||||
$row = $this->reportModel->find((int) $id);
|
||||
if (! $row) {
|
||||
throw new PageNotFoundException('Progress report not found.');
|
||||
}
|
||||
|
||||
[$semester, $schoolYear] = $this->resolveCurrentTerm();
|
||||
$allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear);
|
||||
if (empty($allowedTeacherIds)) {
|
||||
if ($teacherId !== (int) $row['teacher_id']) {
|
||||
throw new PageNotFoundException('Progress report not found.');
|
||||
}
|
||||
$allowedTeacherIds = [(int) $row['teacher_id']];
|
||||
} elseif (! in_array($teacherId, $allowedTeacherIds, true)) {
|
||||
throw new PageNotFoundException('Progress report not found.');
|
||||
}
|
||||
|
||||
$subjectSections = self::SUBJECT_SECTIONS;
|
||||
$rules = [
|
||||
'class_section_id' => 'required|integer',
|
||||
'week_start' => 'required|valid_date[Y-m-d]',
|
||||
'week_end' => 'required|valid_date[Y-m-d]',
|
||||
];
|
||||
foreach ($subjectSections as $slug => $section) {
|
||||
$rules["covered_$slug"] = 'required|string';
|
||||
$rules["homework_$slug"] = 'permit_empty|string';
|
||||
$rules["unit_{$slug}.*"] = 'permit_empty|string|max_length[120]';
|
||||
$rules["chapter_{$slug}.*"] = 'permit_empty|string|max_length[120]';
|
||||
}
|
||||
|
||||
if (! $this->validate($rules)) {
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
if (! $this->hasIslamicUnitSelection()) {
|
||||
return redirect()->back()->withInput()->with('error', 'Please select at least one Islamic Studies unit.');
|
||||
}
|
||||
|
||||
$attachmentErrors = $this->validateAttachmentFiles($subjectSections);
|
||||
if (! empty($attachmentErrors)) {
|
||||
return redirect()->back()->withInput()->with('errors', $attachmentErrors);
|
||||
}
|
||||
|
||||
$weekStart = (string) $this->request->getPost('week_start');
|
||||
$weekEnd = (string) $this->request->getPost('week_end');
|
||||
if ($weekStart && ! $weekEnd) {
|
||||
$weekEnd = $this->buildWeekEndFromStart($weekStart);
|
||||
}
|
||||
if ($weekStart && $weekEnd && strtotime($weekEnd) < strtotime($weekStart)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Week end must be the same as or after the week start.');
|
||||
}
|
||||
|
||||
$classSectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
if ($classSectionId === 0) {
|
||||
return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.');
|
||||
}
|
||||
|
||||
$confirmOverwrite = (bool) $this->request->getPost('confirm_overwrite');
|
||||
if ($weekStart && $weekStart !== (string) ($row['week_start'] ?? '')) {
|
||||
$conflicts = $this->reportModel
|
||||
->select('id')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('week_start', $weekStart)
|
||||
->where('teacher_id', $teacherId)
|
||||
->findAll();
|
||||
if (! $confirmOverwrite && ! empty($conflicts)) {
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->with('warning', 'A progress report already exists for this week, are you sure you want to override it?')
|
||||
->with('confirm_overwrite', true);
|
||||
}
|
||||
if ($confirmOverwrite && ! empty($conflicts)) {
|
||||
$conflictIds = array_values(array_filter(array_map(
|
||||
static fn (array $row): int => (int) ($row['id'] ?? 0),
|
||||
$conflicts
|
||||
)));
|
||||
if (! empty($conflictIds)) {
|
||||
$this->attachmentModel->whereIn('report_id', $conflictIds)->delete();
|
||||
$this->reportModel->whereIn('id', $conflictIds)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$weeklyReports = $this->reportModel
|
||||
->select('class_progress_reports.*')
|
||||
->whereIn('teacher_id', $allowedTeacherIds)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('week_start', $row['week_start'])
|
||||
->orderBy('subject', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$reportMap = [];
|
||||
foreach ($weeklyReports as $report) {
|
||||
$subject = (string) ($report['subject'] ?? '');
|
||||
if ($subject === '') {
|
||||
continue;
|
||||
}
|
||||
$reportMap[$subject] = $report;
|
||||
}
|
||||
|
||||
$reportsUpdated = 0;
|
||||
$flagsInput = $this->request->getPost('flags');
|
||||
foreach ($subjectSections as $slug => $section) {
|
||||
$covered = trim((string) $this->request->getPost("covered_$slug"));
|
||||
if ($covered === '') {
|
||||
continue;
|
||||
}
|
||||
$homework = trim((string) $this->request->getPost("homework_$slug"));
|
||||
$unitTitle = $this->buildUnitChapterSummary($slug);
|
||||
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
||||
$existing = $reportMap[$subjectName] ?? null;
|
||||
if ($unitTitle === null && $existing) {
|
||||
$unitTitle = $existing['unit_title'] ?? null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'class_section_id' => $classSectionId,
|
||||
'week_start' => $weekStart,
|
||||
'week_end' => $weekEnd,
|
||||
'subject' => $subjectName,
|
||||
'unit_title' => $unitTitle,
|
||||
'covered' => $covered,
|
||||
'homework' => $homework ?: null,
|
||||
];
|
||||
if ($flagsInput !== null) {
|
||||
$data['flags_json'] = $this->normalizeFlags($flagsInput);
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
$this->reportModel->update((int) $existing['id'], $data);
|
||||
$reportId = (int) $existing['id'];
|
||||
} else {
|
||||
$data['teacher_id'] = $teacherId;
|
||||
$data['status'] = self::DEFAULT_STATUS;
|
||||
$reportId = (int) $this->reportModel->insert($data, true);
|
||||
}
|
||||
|
||||
$attachmentField = "attachment_$slug";
|
||||
$attachments = $this->request->getFileMultiple($attachmentField) ?? [];
|
||||
$storedAttachments = $this->storeAttachments($reportId, $attachments);
|
||||
if (! empty($storedAttachments)) {
|
||||
$this->attachmentModel->insertBatch($storedAttachments);
|
||||
if (empty($existing['attachment_path'] ?? '')) {
|
||||
$this->reportModel->update($reportId, ['attachment_path' => $storedAttachments[0]['file_path']]);
|
||||
}
|
||||
}
|
||||
$reportsUpdated++;
|
||||
}
|
||||
|
||||
if ($reportsUpdated === 0) {
|
||||
return redirect()->back()->withInput()->with('error', 'Please provide progress for at least one subject.');
|
||||
}
|
||||
|
||||
return redirect()->to('teacher/progress/history')->with('success', 'Progress reports updated.');
|
||||
}
|
||||
|
||||
public function attachment($id)
|
||||
{
|
||||
$row = $this->reportModel->find((int)$id);
|
||||
@@ -709,37 +419,6 @@ class ClassProgressController extends BaseController
|
||||
return $flags ? json_encode($flags) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split stored unit_title into curriculum lines vs custom teacher-typed subjects ("Custom / …").
|
||||
* Keep in sync with teacher form, {@see buildUnitChapterSummary()}, and admin views.
|
||||
*
|
||||
* @return array{curriculum: list<string>, custom: list<string>}
|
||||
*/
|
||||
public static function splitUnitTitleForDisplay(string $unitTitle): array
|
||||
{
|
||||
$unitTitle = trim($unitTitle);
|
||||
if ($unitTitle === '') {
|
||||
return ['curriculum' => [], 'custom' => []];
|
||||
}
|
||||
|
||||
$segments = preg_split('/\s*;\s*/', $unitTitle, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$curriculum = [];
|
||||
$custom = [];
|
||||
foreach ($segments as $seg) {
|
||||
$seg = trim((string) $seg);
|
||||
if ($seg === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^Custom\s*\/\s*(.+)$/iu', $seg, $m)) {
|
||||
$custom[] = trim($m[1]);
|
||||
} else {
|
||||
$curriculum[] = $seg;
|
||||
}
|
||||
}
|
||||
|
||||
return ['curriculum' => $curriculum, 'custom' => $custom];
|
||||
}
|
||||
|
||||
protected function buildUnitChapterSummary(string $slug): ?string
|
||||
{
|
||||
$unitValues = array_map('trim', (array) $this->request->getPost("unit_$slug"));
|
||||
@@ -752,12 +431,9 @@ class ClassProgressController extends BaseController
|
||||
if ($unit === '' && $chapter === '') {
|
||||
continue;
|
||||
}
|
||||
if (strcasecmp($unit, self::CUSTOM_UNIT_ROW_LABEL) === 0 && $chapter !== '') {
|
||||
$unit = self::CUSTOM_UNIT_ROW_LABEL;
|
||||
}
|
||||
$segment = $unit;
|
||||
if ($chapter !== '') {
|
||||
$segment = $segment !== '' ? $unit . ' / ' . $chapter : $chapter;
|
||||
$segment = $segment !== '' ? $segment . ' / ' . $chapter : $chapter;
|
||||
}
|
||||
if ($segment === '') {
|
||||
continue;
|
||||
@@ -768,64 +444,9 @@ class ClassProgressController extends BaseController
|
||||
return null;
|
||||
}
|
||||
$summary = implode(' ; ', $parts);
|
||||
|
||||
return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary;
|
||||
}
|
||||
|
||||
protected function hasIslamicUnitSelection(): bool
|
||||
{
|
||||
$unitValues = array_map('trim', (array) $this->request->getPost('unit_islamic'));
|
||||
$chapterValues = array_map('trim', (array) $this->request->getPost('chapter_islamic'));
|
||||
$count = max(count($unitValues), count($chapterValues));
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$unit = $unitValues[$i] ?? '';
|
||||
$chapter = $chapterValues[$i] ?? '';
|
||||
if ($unit !== '' || $chapter !== '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function parseUnitChapterSummary(string $summary): array
|
||||
{
|
||||
$summary = trim($summary);
|
||||
if ($summary === '') {
|
||||
return ['units' => [], 'chapters' => []];
|
||||
}
|
||||
|
||||
$units = [];
|
||||
$chapters = [];
|
||||
$segments = preg_split('/\s*;\s*/', $summary, -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($segments as $segment) {
|
||||
$segment = trim($segment);
|
||||
if ($segment === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^Custom\s*\/\s*(.+)$/iu', $segment, $m)) {
|
||||
$units[] = self::CUSTOM_UNIT_ROW_LABEL;
|
||||
$chapters[] = trim($m[1]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$parts = preg_split('/\s*\/\s*/', $segment, 2);
|
||||
if (count($parts) === 2) {
|
||||
$u = trim($parts[0]);
|
||||
if (strcasecmp($u, self::CUSTOM_UNIT_ROW_LABEL) === 0) {
|
||||
$u = self::CUSTOM_UNIT_ROW_LABEL;
|
||||
}
|
||||
$units[] = $u;
|
||||
$chapters[] = trim($parts[1]);
|
||||
} else {
|
||||
$units[] = $segment;
|
||||
$chapters[] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return ['units' => $units, 'chapters' => $chapters];
|
||||
}
|
||||
|
||||
protected function buildSundayOptions(int $count = 12): array
|
||||
{
|
||||
$range = $this->resolveProgressDateRange();
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+22
-58
@@ -29,12 +29,18 @@ class ParentProgressController extends BaseController
|
||||
|
||||
public function index()
|
||||
{
|
||||
$students = $this->getParentStudents();
|
||||
$sectionIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn (array $student): int => (int) ($student['class_section_id'] ?? 0),
|
||||
$students
|
||||
))));
|
||||
$sectionIds = $this->getParentSectionIds();
|
||||
$sectionOptions = $this->buildSectionOptions($sectionIds);
|
||||
$subjectSections = ClassProgressController::SUBJECT_SECTIONS;
|
||||
$selectedSectionId = (int) $this->request->getGet('class_section_id');
|
||||
$validSectionIds = array_keys($sectionOptions);
|
||||
|
||||
if ($selectedSectionId === 0 && ! empty($validSectionIds)) {
|
||||
$selectedSectionId = $validSectionIds[0];
|
||||
}
|
||||
if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) {
|
||||
$selectedSectionId = $validSectionIds[0] ?? null;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
if (! empty($sectionIds)) {
|
||||
@@ -44,32 +50,23 @@ class ParentProgressController extends BaseController
|
||||
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
|
||||
->whereIn('class_progress_reports.class_section_id', $sectionIds);
|
||||
|
||||
if ($selectedSectionId) {
|
||||
$builder->where('class_progress_reports.class_section_id', $selectedSectionId);
|
||||
}
|
||||
|
||||
$rows = $builder
|
||||
->orderBy('week_start', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
$studentReportGroups = [];
|
||||
foreach ($students as $student) {
|
||||
$studentId = (int) ($student['student_id'] ?? 0);
|
||||
if ($studentId === 0) {
|
||||
continue;
|
||||
}
|
||||
$classSectionId = (int) ($student['class_section_id'] ?? 0);
|
||||
$studentRows = $classSectionId
|
||||
? array_values(array_filter(
|
||||
$rows,
|
||||
static fn (array $row): bool => (int) ($row['class_section_id'] ?? 0) === $classSectionId
|
||||
))
|
||||
: [];
|
||||
$studentReportGroups[$studentId] = $this->groupReportsByWeek($studentRows);
|
||||
}
|
||||
$reportGroups = $this->groupReportsByWeek($rows);
|
||||
|
||||
return view('parent/class_progress_list', [
|
||||
'students' => $students,
|
||||
'studentReportGroups' => $studentReportGroups,
|
||||
'reportGroups' => $reportGroups,
|
||||
'subjectSections' => $subjectSections,
|
||||
'hasStudents' => ! empty($students),
|
||||
'classSectionOptions' => $sectionOptions,
|
||||
'selectedSectionId' => $selectedSectionId,
|
||||
'hasSections' => ! empty($sectionIds),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -201,53 +198,20 @@ class ParentProgressController extends BaseController
|
||||
return $options;
|
||||
}
|
||||
|
||||
protected function getParentStudents(): array
|
||||
{
|
||||
$parentId = (int) session()->get('user_id');
|
||||
if ($parentId === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->db->table('enrollments e')
|
||||
->select('e.student_id, e.class_section_id, e.updated_at, e.created_at, s.firstname, s.lastname, cs.class_section_name')
|
||||
->join('students s', 's.id = e.student_id')
|
||||
->join('classSection cs', 'cs.class_section_id = e.class_section_id', 'left')
|
||||
->where('e.parent_id', $parentId)
|
||||
->where('e.is_withdrawn', 0)
|
||||
->orderBy('e.updated_at', 'DESC')
|
||||
->orderBy('e.created_at', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$students = [];
|
||||
foreach ($rows as $row) {
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
if ($studentId === 0 || isset($students[$studentId])) {
|
||||
continue;
|
||||
}
|
||||
$students[$studentId] = $row;
|
||||
}
|
||||
|
||||
return array_values($students);
|
||||
}
|
||||
|
||||
protected function groupReportsByWeek(array $rows): array
|
||||
{
|
||||
$reportGroups = [];
|
||||
foreach ($rows as $row) {
|
||||
$row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
||||
$weekStart = $row['week_start'] ?? '';
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
if ($weekStart === '' || $sectionId === 0) {
|
||||
$key = $row['week_start'] ?? '';
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$key = $weekStart . ':' . $sectionId;
|
||||
if (! isset($reportGroups[$key])) {
|
||||
$reportGroups[$key] = [
|
||||
'week_start' => $row['week_start'] ?? '',
|
||||
'week_end' => $row['week_end'] ?? '',
|
||||
'class_section_name' => $row['class_section_name'] ?? '',
|
||||
'class_section_id' => $sectionId,
|
||||
'reports' => [],
|
||||
];
|
||||
}
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+32
-549
@@ -28,8 +28,6 @@ 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;
|
||||
@@ -418,10 +416,8 @@ class AdministratorController extends BaseController
|
||||
$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
|
||||
@@ -698,26 +694,15 @@ class AdministratorController extends BaseController
|
||||
|
||||
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)
|
||||
))));
|
||||
$semester = (string)($this->semester ?? '');
|
||||
$schoolYear = (string)($this->schoolYear ?? '');
|
||||
|
||||
$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')
|
||||
$assignmentRows = $this->db->table('teacher_class tc')
|
||||
->select([
|
||||
'tc.class_section_id',
|
||||
'cs.class_section_name',
|
||||
@@ -728,97 +713,11 @@ class AdministratorController extends BaseController
|
||||
])
|
||||
->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);
|
||||
}
|
||||
->where('tc.school_year', $schoolYear)
|
||||
->where('tc.semester', $semester)
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$teachersBySection = [];
|
||||
foreach ($assignmentRows as $assignment) {
|
||||
@@ -844,7 +743,7 @@ class AdministratorController extends BaseController
|
||||
$entry = &$teachersBySection[$sectionId];
|
||||
if (!isset($entry)) {
|
||||
$entry = [
|
||||
'class_section' => $assignment['class_section_name'] ?? ($sectionMap[$sectionId] ?? "Section {$sectionId}"),
|
||||
'class_section' => $assignment['class_section_name'] ?? "Section {$sectionId}",
|
||||
'teachers' => [],
|
||||
];
|
||||
}
|
||||
@@ -864,49 +763,35 @@ class AdministratorController extends BaseController
|
||||
$missingItemCount = 0;
|
||||
$allTeacherIds = [];
|
||||
$allClassSectionIds = [];
|
||||
$examTerm = $this->resolveExamTermLabel($semester);
|
||||
$examScoreField = $examTerm === 'final' ? 'final_exam_score' : 'midterm_exam_score';
|
||||
|
||||
foreach ($sectionMap as $classSectionId => $sectionName) {
|
||||
foreach ($teachersBySection as $classSectionId => $section) {
|
||||
$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('semester', $semester)
|
||||
->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
|
||||
$scoreRecords = $semesterScores
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('school_year', $schoolYear);
|
||||
if (!empty($semesterCandidates)) {
|
||||
$scoreQuery->whereIn('semester', $semesterCandidates);
|
||||
}
|
||||
$scoreRecords = $scoreQuery->findAll();
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->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] ?? ''));
|
||||
$midtermValue = trim((string)($score['midterm_exam_score'] ?? ''));
|
||||
if ($midtermValue !== '') {
|
||||
$midtermStudents[$sid] = true;
|
||||
}
|
||||
@@ -920,15 +805,13 @@ class AdministratorController extends BaseController
|
||||
$midtermCommentStudents = [];
|
||||
$ptapCommentStudents = [];
|
||||
if (!empty($studentIds)) {
|
||||
$commentQuery = $scoreComments
|
||||
$comments = $scoreComments
|
||||
->select('student_id, score_type, comment')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('score_type', [$examTerm, 'ptap']);
|
||||
if (!empty($semesterCandidates)) {
|
||||
$commentQuery->whereIn('semester', $semesterCandidates);
|
||||
}
|
||||
$comments = $commentQuery->findAll();
|
||||
->whereIn('score_type', ['midterm', 'ptap'])
|
||||
->findAll();
|
||||
foreach ($comments as $comment) {
|
||||
$sid = (int)($comment['student_id'] ?? 0);
|
||||
if ($sid <= 0) {
|
||||
@@ -939,7 +822,7 @@ class AdministratorController extends BaseController
|
||||
continue;
|
||||
}
|
||||
$type = strtolower(trim((string)($comment['score_type'] ?? '')));
|
||||
if ($type === $examTerm) {
|
||||
if ($type === 'midterm') {
|
||||
$midtermCommentStudents[$sid] = true;
|
||||
}
|
||||
if ($type === 'ptap') {
|
||||
@@ -948,17 +831,14 @@ class AdministratorController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
$attendanceQuery = $attendanceDays
|
||||
$attendanceRow = $attendanceDays
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', $today);
|
||||
if (!empty($semesterCandidates)) {
|
||||
$attendanceQuery->whereIn('semester', $semesterCandidates);
|
||||
}
|
||||
$attendanceRow = $attendanceQuery->first();
|
||||
->where('date', $today)
|
||||
->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) {
|
||||
@@ -979,27 +859,18 @@ class AdministratorController extends BaseController
|
||||
$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);
|
||||
$missingItemsForSection = $this->buildMissingItems($statusDetails);
|
||||
$missingItemCount += count($missingItemsForSection);
|
||||
$totalStatuses += count($statusDetails);
|
||||
|
||||
$rows[] = [
|
||||
'class_section' => $sectionMap[$classSectionId] ?? ($section['class_section'] ?? "Section {$classSectionId}"),
|
||||
'class_section' => $section['class_section'] ?? "Section {$classSectionId}",
|
||||
'class_section_id' => $classSectionId,
|
||||
'teachers' => $teacherList,
|
||||
'midterm_score_status' => $midtermScoreStatus,
|
||||
@@ -1007,9 +878,6 @@ class AdministratorController extends BaseController
|
||||
'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,
|
||||
];
|
||||
@@ -1071,183 +939,15 @@ class AdministratorController extends BaseController
|
||||
'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) {
|
||||
@@ -1304,10 +1004,6 @@ class AdministratorController extends BaseController
|
||||
|
||||
$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;
|
||||
|
||||
@@ -1323,13 +1019,6 @@ class AdministratorController extends BaseController
|
||||
$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),
|
||||
@@ -1342,46 +1031,10 @@ class AdministratorController extends BaseController
|
||||
}
|
||||
|
||||
$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>"
|
||||
. "<p>Administration is gently reminding you to wrap up any remaining score submissions and/or comments for {$sectionName}.</p>"
|
||||
. $missingNote
|
||||
. $progressNote
|
||||
. $examDraftNote
|
||||
. $homeworkNote
|
||||
. ($nonScoreOnly ? '' : "<p>Visit <a href=\"{$scoreUrl}\">Teacher Score Submission</a> to address any remaining items.</p>")
|
||||
. "<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'] ?? '';
|
||||
@@ -1443,170 +1096,6 @@ class AdministratorController extends BaseController
|
||||
];
|
||||
}
|
||||
|
||||
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 [
|
||||
@@ -1616,20 +1105,14 @@ class AdministratorController extends BaseController
|
||||
];
|
||||
}
|
||||
|
||||
private function buildMissingItems(array $statusMap, string $semester): array
|
||||
private function buildMissingItems(array $statusMap): 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,
|
||||
'midterm_score_status' => 'midterm scores',
|
||||
'midterm_comment_status' => 'midterm comments',
|
||||
'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 = [];
|
||||
|
||||
Executable → Regular
-6
@@ -119,12 +119,7 @@ class AssignmentController extends BaseController
|
||||
}
|
||||
|
||||
$students = [];
|
||||
$seenStudentIds = [];
|
||||
foreach ($studentClasses as $studentClass) {
|
||||
$sid = (int)($studentClass['student_id'] ?? 0);
|
||||
if ($sid <= 0 || isset($seenStudentIds[$sid])) {
|
||||
continue;
|
||||
}
|
||||
if ($sectionSemester === '' && !empty($studentClass['semester'])) {
|
||||
$sectionSemester = (string)$studentClass['semester'];
|
||||
}
|
||||
@@ -154,7 +149,6 @@ class AssignmentController extends BaseController
|
||||
'tuition_paid' => esc($student['tuition_paid'] ? 'Yes' : 'No'),
|
||||
'school_id' => esc($student['school_id']),
|
||||
];
|
||||
$seenStudentIds[$sid] = true;
|
||||
}
|
||||
|
||||
$sectionSemesterDisplay = $sectionSemester !== '' ? $sectionSemester : ((string)($this->semester ?? ''));
|
||||
|
||||
Executable → Regular
Executable → Regular
-5
@@ -1004,13 +1004,9 @@ public function showUpdateAttendanceForm()
|
||||
}
|
||||
|
||||
$hasRoster = false;
|
||||
$seenStudents = [];
|
||||
|
||||
foreach ($students as $sc) {
|
||||
$studentId = (int)$sc['student_id'];
|
||||
if ($studentId <= 0 || isset($seenStudents[$studentId])) {
|
||||
continue;
|
||||
}
|
||||
$student = $this->studentModel
|
||||
->select('id, firstname, lastname, school_id')
|
||||
->find($studentId);
|
||||
@@ -1018,7 +1014,6 @@ public function showUpdateAttendanceForm()
|
||||
|
||||
$studentsBySection[$secCode][] = $student;
|
||||
$hasRoster = true;
|
||||
$seenStudents[$studentId] = true;
|
||||
|
||||
// Attendance history
|
||||
$qb = $this->attendanceDataModel
|
||||
|
||||
Executable → Regular
+2
-54
@@ -2003,58 +2003,6 @@ class AttendanceTrackingController extends BaseController
|
||||
return [$subject, $body];
|
||||
}
|
||||
|
||||
private function buildFallbackTemplate(string $code, string $variant, array $context): array
|
||||
{
|
||||
$studentName = (string) ($context['{{student_name}}'] ?? 'the student');
|
||||
$incident = (string) ($context['{{incident_date}}'] ?? date('Y-m-d'));
|
||||
$parentName = (string) ($context['{{parent_name}}'] ?? 'Parent/Guardian');
|
||||
$phone = (string) ($context['{{school_phone}}'] ?? 'the school office');
|
||||
$voicemail = (string) ($context['{{voicemail_phone}}'] ?? 'your voicemail');
|
||||
|
||||
$subject = match (true) {
|
||||
str_starts_with($code, 'ABS_1') => 'Attendance Notice: Unreported Absence',
|
||||
str_starts_with($code, 'ABS_2') => 'Attendance Follow-Up: Two Consecutive Absences',
|
||||
str_starts_with($code, 'ABS_3') => 'Attendance Follow-Up: Three Absences',
|
||||
str_starts_with($code, 'LATE_2') => 'Attendance Notice: Repeated Lateness',
|
||||
str_starts_with($code, 'LATE_3'),
|
||||
str_starts_with($code, 'LATE_4'),
|
||||
str_starts_with($code, 'MIX') => 'Attendance Follow-Up: Repeated Lateness and Absence',
|
||||
default => 'Attendance Notice',
|
||||
};
|
||||
|
||||
$intro = "<p>Insha Allah this email finds you well";
|
||||
if ($variant === 'answered') {
|
||||
$intro .= ", <strong>{$parentName}</strong>. Jazakum Allahu khayran for taking the time to speak with us today.</p>";
|
||||
} elseif ($variant === 'no_answer') {
|
||||
$intro .= ".</p><p>We tried to reach you but could not connect and left a voice message at <strong>{$voicemail}</strong>.</p>";
|
||||
} else {
|
||||
$intro .= ".</p>";
|
||||
}
|
||||
|
||||
$details = match (true) {
|
||||
str_starts_with($code, 'ABS_1')
|
||||
=> "<p>This is to let you know that <strong>{$studentName}</strong> was absent on <strong>{$incident}</strong> without prior notice.</p>",
|
||||
str_starts_with($code, 'ABS_2')
|
||||
=> "<p>This is a reminder that <strong>{$studentName}</strong> has had repeated absences, most recently on <strong>{$incident}</strong>, without prior notice.</p>",
|
||||
str_starts_with($code, 'ABS_3')
|
||||
=> "<p>This is a reminder that <strong>{$studentName}</strong> has been absent three times, most recently on <strong>{$incident}</strong>, without prior notice.</p>",
|
||||
str_starts_with($code, 'LATE_2')
|
||||
=> "<p>This is a reminder that <strong>{$studentName}</strong> has been late multiple times, most recently on <strong>{$incident}</strong>.</p>",
|
||||
str_starts_with($code, 'LATE_3'),
|
||||
str_starts_with($code, 'LATE_4')
|
||||
=> "<p>This is a follow-up that <strong>{$studentName}</strong> has had repeated lateness concerns, most recently on <strong>{$incident}</strong>.</p>",
|
||||
str_starts_with($code, 'MIX')
|
||||
=> "<p>This is a follow-up that <strong>{$studentName}</strong> has had a mix of repeated lateness and absence concerns, most recently on <strong>{$incident}</strong>.</p>",
|
||||
default
|
||||
=> "<p>We'd like to inform you about <strong>{$studentName}</strong>'s recent attendance concern dated <strong>{$incident}</strong>.</p>",
|
||||
};
|
||||
|
||||
$closing = "<p>If your child will be absent or late due to illness or family commitments, please let us know ahead of time.</p>"
|
||||
. "<p>You can email/call/text us at <strong>{$phone}</strong>.</p>";
|
||||
|
||||
return [$subject, $intro . $details . $closing];
|
||||
}
|
||||
|
||||
|
||||
public function compose()
|
||||
{
|
||||
@@ -2085,8 +2033,8 @@ class AttendanceTrackingController extends BaseController
|
||||
$rendered = $this->renderTemplate($code, $variant, $ctx);
|
||||
|
||||
if (!$rendered) {
|
||||
log_message('warning', 'Attendance email template missing; using fallback compose body. code=' . $code . ' variant=' . $variant);
|
||||
$rendered = $this->buildFallbackTemplate($code, $variant, $ctx);
|
||||
return redirect()->to(site_url('attendance/violations'))
|
||||
->with('error', 'Template not found for ' . $code . ' (' . $variant . ').');
|
||||
}
|
||||
|
||||
[$subject, $body] = $rendered;
|
||||
|
||||
Executable → Regular
+20
-134
@@ -10,8 +10,6 @@ use CodeIgniter\I18n\Time;
|
||||
|
||||
class AuthorizedUsersController extends ResourceController
|
||||
{
|
||||
private const TOKEN_TTL_HOURS = 24;
|
||||
|
||||
protected $userModel;
|
||||
protected $authorizedUserModel;
|
||||
|
||||
@@ -20,30 +18,6 @@ class AuthorizedUsersController extends ResourceController
|
||||
$this->userModel = new UserModel();
|
||||
$this->authorizedUserModel = new AuthorizedUserModel();
|
||||
}
|
||||
|
||||
private function requireLogin()
|
||||
{
|
||||
if (!session()->get('is_logged_in')) {
|
||||
return $this->failUnauthorized('Authentication required.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function requireOwnership(array $authorizedUser)
|
||||
{
|
||||
$userId = (int) session()->get('user_id');
|
||||
if ($userId <= 0 || (int) ($authorizedUser['user_id'] ?? 0) !== $userId) {
|
||||
return $this->failForbidden('You do not have access to this resource.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function hashToken(string $token): string
|
||||
{
|
||||
return hash('sha256', $token);
|
||||
}
|
||||
/**
|
||||
* Return a list of authorized users for the logged-in main user.
|
||||
*
|
||||
@@ -51,9 +25,6 @@ class AuthorizedUsersController extends ResourceController
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if ($resp = $this->requireLogin()) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$userId = session()->get('user_id');
|
||||
$authorizedUsers = $this->authorizedUserModel->where('user_id', $userId)->findAll();
|
||||
@@ -69,20 +40,12 @@ class AuthorizedUsersController extends ResourceController
|
||||
*/
|
||||
public function show($id = null)
|
||||
{
|
||||
if ($resp = $this->requireLogin()) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$authorizedUser = $this->authorizedUserModel->find($id);
|
||||
|
||||
if (!$authorizedUser) {
|
||||
return $this->failNotFound('Authorized user not found.');
|
||||
}
|
||||
|
||||
if ($resp = $this->requireOwnership($authorizedUser)) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
return $this->respond($authorizedUser);
|
||||
}
|
||||
|
||||
@@ -93,10 +56,6 @@ class AuthorizedUsersController extends ResourceController
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if ($resp = $this->requireLogin()) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$email = strtolower($this->request->getPost('email'));
|
||||
|
||||
// Validate email
|
||||
@@ -107,20 +66,19 @@ class AuthorizedUsersController extends ResourceController
|
||||
$user = $this->userModel->where('email', $email)->first();
|
||||
|
||||
if (!$user) {
|
||||
return $this->respondCreated(['message' => 'Authorized user added. A confirmation email has been sent.']);
|
||||
return $this->failNotFound('No user found with this email.');
|
||||
}
|
||||
|
||||
// Generate a token for confirmation
|
||||
helper('text');
|
||||
$token = bin2hex(random_bytes(48));
|
||||
$tokenHash = $this->hashToken($token);
|
||||
|
||||
// Add entry to the authorized_users table
|
||||
$this->authorizedUserModel->insert([
|
||||
'user_id' => session()->get('user_id'), // Main user ID
|
||||
'authorized_user_id' => $user['id'],
|
||||
'email' => $email,
|
||||
'token' => $tokenHash,
|
||||
'token' => $token,
|
||||
'status' => 'Pending'
|
||||
]);
|
||||
|
||||
@@ -138,10 +96,6 @@ class AuthorizedUsersController extends ResourceController
|
||||
*/
|
||||
public function update($id = null)
|
||||
{
|
||||
if ($resp = $this->requireLogin()) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
// Fetch the authorized user
|
||||
$authorizedUser = $this->authorizedUserModel->find($id);
|
||||
|
||||
@@ -149,10 +103,6 @@ class AuthorizedUsersController extends ResourceController
|
||||
return $this->failNotFound('Authorized user not found.');
|
||||
}
|
||||
|
||||
if ($resp = $this->requireOwnership($authorizedUser)) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
// Update the authorized user’s information (e.g., email)
|
||||
$email = strtolower($this->request->getPost('email'));
|
||||
if ($email && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
@@ -172,20 +122,12 @@ class AuthorizedUsersController extends ResourceController
|
||||
*/
|
||||
public function delete($id = null)
|
||||
{
|
||||
if ($resp = $this->requireLogin()) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$authorizedUser = $this->authorizedUserModel->find($id);
|
||||
|
||||
if (!$authorizedUser) {
|
||||
return $this->failNotFound('Authorized user not found.');
|
||||
}
|
||||
|
||||
if ($resp = $this->requireOwnership($authorizedUser)) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
// Delete the authorized user record
|
||||
$this->authorizedUserModel->delete($id);
|
||||
|
||||
@@ -205,28 +147,16 @@ class AuthorizedUsersController extends ResourceController
|
||||
return $this->fail('Invalid confirmation link.');
|
||||
}
|
||||
|
||||
$tokenHash = $this->hashToken($token);
|
||||
$authorizedUser = $this->authorizedUserModel
|
||||
->groupStart()
|
||||
->where('token', $tokenHash)
|
||||
->orWhere('token', $token)
|
||||
->groupEnd()
|
||||
->where('created_at >=', Time::now()->subHours(self::TOKEN_TTL_HOURS)->toDateTimeString())
|
||||
->first();
|
||||
$authorizedUser = $this->authorizedUserModel->where('token', $token)->first();
|
||||
|
||||
if (!$authorizedUser) {
|
||||
return $this->fail('Invalid or expired confirmation link.');
|
||||
}
|
||||
|
||||
// Mark the authorized user as active and rotate token for password setup
|
||||
$nextToken = bin2hex(random_bytes(48));
|
||||
$nextTokenHash = $this->hashToken($nextToken);
|
||||
$this->authorizedUserModel->update($authorizedUser['id'], [
|
||||
'status' => 'Active',
|
||||
'token' => $nextTokenHash,
|
||||
]);
|
||||
// Mark the authorized user as active
|
||||
$this->authorizedUserModel->update($authorizedUser['id'], ['status' => 'Active', 'token' => null]);
|
||||
|
||||
return redirect()->to('/set_authorized_user_password/' . $authorizedUser['authorized_user_id'] . '?token=' . $nextToken);
|
||||
return redirect()->to('/set_authorized_user_password/' . $authorizedUser['authorized_user_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,36 +167,13 @@ class AuthorizedUsersController extends ResourceController
|
||||
*/
|
||||
public function setPassword($authorizedUserId)
|
||||
{
|
||||
$token = (string) $this->request->getGet('token');
|
||||
if ($token === '') {
|
||||
return $this->fail('Invalid confirmation link.');
|
||||
}
|
||||
|
||||
$tokenHash = $this->hashToken($token);
|
||||
$authorizedUser = $this->authorizedUserModel
|
||||
->groupStart()
|
||||
->where('token', $tokenHash)
|
||||
->orWhere('token', $token)
|
||||
->groupEnd()
|
||||
->where('authorized_user_id', $authorizedUserId)
|
||||
->where('status', 'Active')
|
||||
->where('updated_at >=', Time::now()->subHours(self::TOKEN_TTL_HOURS)->toDateTimeString())
|
||||
->first();
|
||||
|
||||
if (!$authorizedUser) {
|
||||
return $this->fail('Invalid or expired confirmation link.');
|
||||
}
|
||||
|
||||
$user = $this->userModel->find($authorizedUserId);
|
||||
|
||||
if (!$user) {
|
||||
return $this->failNotFound('User not found.');
|
||||
}
|
||||
|
||||
return view('user/set_authorized_user_password', [
|
||||
'userId' => $authorizedUserId,
|
||||
'token' => $token,
|
||||
]);
|
||||
return view('user/set_authorized_user_password', ['userId' => $authorizedUserId]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,59 +181,38 @@ class AuthorizedUsersController extends ResourceController
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function savePassword($authorizedUserId = null)
|
||||
/*
|
||||
public function savePassword()
|
||||
{
|
||||
// Validate the request
|
||||
$validation = \Config\Services::validation();
|
||||
$validation->setRules([
|
||||
'password' => [
|
||||
'label' => 'Password',
|
||||
'rules' => 'required|min_length[8]|regex_match[/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\\-=\\+*#$%&!?])[A-Za-z\\d@\\-=\\+*#$%&!?]{8,}$/]',
|
||||
],
|
||||
'password' => 'required|min_length[6]',
|
||||
'password_confirm' => 'required|matches[password]',
|
||||
'user_id' => 'required|integer',
|
||||
'token' => 'required',
|
||||
'user_id' => 'required|integer'
|
||||
]);
|
||||
|
||||
if (!$this->validate($validation->getRules())) {
|
||||
return $this->failValidationErrors($validation->getErrors());
|
||||
}
|
||||
|
||||
$userId = (int) $this->request->getPost('user_id');
|
||||
$token = (string) $this->request->getPost('token');
|
||||
$authorizedUserId = $authorizedUserId !== null ? (int) $authorizedUserId : $userId;
|
||||
// Get the validated input
|
||||
$userId = $this->request->getPost('user_id');
|
||||
$password = $this->request->getPost('password');
|
||||
|
||||
if ($userId <= 0 || $authorizedUserId <= 0 || $userId !== $authorizedUserId) {
|
||||
return $this->fail('Invalid request.');
|
||||
}
|
||||
$model = new UserModel();
|
||||
$user = $model->find($userId);
|
||||
|
||||
$tokenHash = $this->hashToken($token);
|
||||
$authorizedUser = $this->authorizedUserModel
|
||||
->groupStart()
|
||||
->where('token', $tokenHash)
|
||||
->orWhere('token', $token)
|
||||
->groupEnd()
|
||||
->where('authorized_user_id', $authorizedUserId)
|
||||
->where('status', 'Active')
|
||||
->where('updated_at >=', Time::now()->subHours(self::TOKEN_TTL_HOURS)->toDateTimeString())
|
||||
->first();
|
||||
|
||||
if (!$authorizedUser) {
|
||||
return $this->fail('Invalid or expired confirmation link.');
|
||||
}
|
||||
|
||||
$user = $this->userModel->find($authorizedUserId);
|
||||
if (!$user) {
|
||||
return $this->failNotFound('User not found.');
|
||||
}
|
||||
|
||||
$password = (string) $this->request->getPost('password');
|
||||
$hashedPassword = pbkdf2_hash($password);
|
||||
|
||||
$this->userModel->update($authorizedUserId, ['password' => $hashedPassword]);
|
||||
$this->authorizedUserModel->update($authorizedUser['id'], ['token' => null]);
|
||||
// Save the password
|
||||
$model->update($userId, ['password' => password_hash($password, PASSWORD_DEFAULT)]);
|
||||
|
||||
return $this->respond(['message' => 'Password has been successfully set.']);
|
||||
}
|
||||
*/
|
||||
/**
|
||||
* Sends a confirmation email to the authorized user.
|
||||
*
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+2
-5
@@ -750,9 +750,7 @@ class DiscountController extends BaseController
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
$discountableTotal = $tuitionSubtotal + $additionalSubtotal;
|
||||
$nonDiscountableTotal = $eventSubtotal;
|
||||
$newTotal = round($discountableTotal + $nonDiscountableTotal, 2);
|
||||
$newTotal = round($tuitionSubtotal + $eventSubtotal + $additionalSubtotal, 2);
|
||||
|
||||
// ---- Payments / Discounts / Refunds ----
|
||||
$db = $this->db;
|
||||
@@ -796,8 +794,7 @@ class DiscountController extends BaseController
|
||||
->get()->getRowArray();
|
||||
$totalRefundPaid = (float)($refundRow['total_refund_paid'] ?? 0);
|
||||
|
||||
$appliedDiscount = min($totalDisc, $discountableTotal);
|
||||
$newBalance = max(0.0, $newTotal - $appliedDiscount - $totalPaid - $totalRefundPaid);
|
||||
$newBalance = max(0.0, $newTotal - $totalDisc - $totalPaid - $totalRefundPaid);
|
||||
$newStatus = ($newBalance <= 0.00001) ? 'Paid' : (($totalPaid > 0) ? 'Partially Paid' : 'Unpaid');
|
||||
|
||||
$updateData = [
|
||||
|
||||
Executable → Regular
Executable → Regular
-157
@@ -170,163 +170,6 @@ class EmailController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send one email with a single To recipient and many CC recipients.
|
||||
*
|
||||
* @param string $recipient
|
||||
* @param string[] $ccRecipients
|
||||
* @param string $subject
|
||||
* @param string $htmlMessage
|
||||
* @param string|null $profile
|
||||
* @param string|null $replyToEmail
|
||||
* @param string|null $replyToName
|
||||
* @param array $attachments
|
||||
*/
|
||||
public function sendEmailWithCc(
|
||||
string $recipient,
|
||||
array $ccRecipients,
|
||||
string $subject,
|
||||
string $htmlMessage,
|
||||
?string $profile = null,
|
||||
?string $replyToEmail = null,
|
||||
?string $replyToName = null,
|
||||
array $attachments = []
|
||||
): bool {
|
||||
$ccRecipients = array_values(array_unique(array_filter(array_map(static function ($email) {
|
||||
$email = trim((string) $email);
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : null;
|
||||
}, $ccRecipients))));
|
||||
|
||||
if ($ccRecipients === []) {
|
||||
return $this->sendEmail($recipient, $subject, $htmlMessage, $profile, $replyToEmail, $replyToName, $attachments);
|
||||
}
|
||||
|
||||
$autoload = APPPATH . '../vendor/autoload.php';
|
||||
if (is_file($autoload)) {
|
||||
require_once $autoload;
|
||||
}
|
||||
|
||||
$profile = $this->resolveProfile($profile);
|
||||
$cfg = $this->getProfileConfig($profile);
|
||||
|
||||
if (empty($cfg['host']) || empty($cfg['user']) || $cfg['pass'] === '') {
|
||||
log_message('error', "[mail:$profile] Missing SMTP config (host/user/pass). Check .env MAIL_{$this->envKeyFromProfile($profile)}_* or MAIL_DEFAULT_*.");
|
||||
return false;
|
||||
}
|
||||
|
||||
$debugEnabled = (bool) env('MAIL_DEBUG', false);
|
||||
$timeout = (int) env('MAIL_TIMEOUT', 15);
|
||||
$keepAlive = (bool) env('MAIL_KEEPALIVE', false);
|
||||
$verifyPeer = filter_var(env('MAIL_VERIFY_PEER', 'true'), FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
$targetHost = $cfg['host'];
|
||||
$targetPort = (int) $cfg['port'];
|
||||
|
||||
$resolved = @gethostbyname($targetHost);
|
||||
if (!$resolved || $resolved === $targetHost) {
|
||||
log_message('debug', "[mail:$profile] DNS resolve note: host=$targetHost, resolved=$resolved");
|
||||
}
|
||||
|
||||
$sockOk = @fsockopen($targetHost, $targetPort, $errno, $errstr, 5);
|
||||
if (!$sockOk) {
|
||||
log_message('error', "[mail:$profile] Socket preflight failed to {$targetHost}:{$targetPort} (errno=$errno, err=$errstr). Likely firewall/port/encryption mismatch or wrong host.");
|
||||
} else {
|
||||
fclose($sockOk);
|
||||
}
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
try {
|
||||
if ($debugEnabled) {
|
||||
ob_start();
|
||||
}
|
||||
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $cfg['host'];
|
||||
$mail->Port = $cfg['port'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $cfg['user'];
|
||||
$mail->Password = $cfg['pass'];
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = $timeout;
|
||||
$mail->SMTPKeepAlive = $keepAlive;
|
||||
$mail->SMTPAutoTLS = true;
|
||||
|
||||
if ($cfg['encryption'] === 'ssl') {
|
||||
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||
} else {
|
||||
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||
}
|
||||
|
||||
$mail->SMTPOptions = [
|
||||
'ssl' => [
|
||||
'verify_peer' => $verifyPeer,
|
||||
'verify_peer_name' => $verifyPeer,
|
||||
'allow_self_signed' => !$verifyPeer,
|
||||
],
|
||||
];
|
||||
|
||||
$mail->SMTPDebug = $debugEnabled ? 3 : 0;
|
||||
$mail->Debugoutput = 'error_log';
|
||||
|
||||
$mail->setFrom($cfg['fromEmail'], $cfg['fromName']);
|
||||
if (!empty($cfg['returnPath'])) {
|
||||
$mail->Sender = $cfg['returnPath'];
|
||||
}
|
||||
|
||||
$mail->clearReplyTos();
|
||||
$rtEmail = env('MAIL_DEFAULT_REPLY_TO');
|
||||
$rtName = env('MAIL_DEFAULT_REPLY_TO_NAME');
|
||||
if (!$rtEmail || !filter_var($rtEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
$rtEmail = $replyToEmail ?: ($cfg['replyTo'] ?: $cfg['fromEmail']);
|
||||
}
|
||||
if (!$rtName) {
|
||||
$rtName = $replyToName ?: ($cfg['replyToName'] ?: $cfg['fromName']);
|
||||
}
|
||||
$rtName = $this->sanitizeReplyToName($rtName, $cfg['fromName']);
|
||||
if ($rtEmail) {
|
||||
$mail->addReplyTo($rtEmail, $rtName);
|
||||
}
|
||||
|
||||
if (!empty($cfg['dkim']['domain']) && !empty($cfg['dkim']['private']) && !empty($cfg['dkim']['selector'])) {
|
||||
$mail->DKIM_domain = $cfg['dkim']['domain'];
|
||||
$mail->DKIM_private = $cfg['dkim']['private'];
|
||||
$mail->DKIM_selector = $cfg['dkim']['selector'];
|
||||
$mail->DKIM_identity = $cfg['fromEmail'];
|
||||
}
|
||||
|
||||
$mail->addAddress($recipient);
|
||||
foreach ($ccRecipients as $ccRecipient) {
|
||||
$mail->addCC($ccRecipient);
|
||||
}
|
||||
|
||||
foreach ($attachments as $att) {
|
||||
if (!empty($att['path']) && is_file($att['path'])) {
|
||||
$mail->addAttachment($att['path'], $att['name'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $htmlMessage;
|
||||
|
||||
$ok = $mail->send();
|
||||
|
||||
$dbg = $debugEnabled ? (ob_get_clean() ?: '') : '';
|
||||
if ($ok) {
|
||||
log_message('info', "[mail:$profile] Sent to {$recipient} with " . count($ccRecipients) . " CC recipient(s), subj='{$subject}' via {$cfg['host']}:{$cfg['port']}/{$cfg['encryption']}");
|
||||
return true;
|
||||
}
|
||||
|
||||
log_message('error', "[mail:$profile] Failed: {$mail->ErrorInfo}. Debug: {$dbg}");
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
$dbg = $debugEnabled ? (ob_get_clean() ?: '') : '';
|
||||
log_message('error', "[mail:$profile] Exception: {$e->getMessage()} | PHPMailer: {$mail->ErrorInfo} | Debug: {$dbg}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve null/alias profile names to a canonical env prefix. */
|
||||
private function resolveProfile(?string $profile): string
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user