Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e2d12e524 | |||
| 7bc999643a | |||
| 89e95b7851 | |||
| 2543df3d33 | |||
| 112d073235 | |||
| 2611730ec6 | |||
| 05dad52e10 | |||
| d158650be9 | |||
| 61facee902 | |||
| ed67836701 | |||
| d2abbc1458 | |||
| b52475ff0b | |||
| b2026812d5 | |||
| 58445b2a48 | |||
| 0f8a1fa0b1 | |||
| fee07bcceb | |||
| 70a6e2c104 | |||
| 11c93d3e82 | |||
| 7c5028a76d |
@@ -63,7 +63,7 @@ session.expiration = 43200
|
|||||||
database.default.hostname = 127.0.0.1
|
database.default.hostname = 127.0.0.1
|
||||||
database.default.database = school
|
database.default.database = school
|
||||||
database.default.username = root
|
database.default.username = root
|
||||||
database.default.password =
|
database.default.password = rootpassword
|
||||||
database.default.DBDriver = MySQLi
|
database.default.DBDriver = MySQLi
|
||||||
database.default.DBPrefix =
|
database.default.DBPrefix =
|
||||||
database.default.port = 3306
|
database.default.port = 3306
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -82,7 +82,7 @@ class App extends BaseConfig
|
|||||||
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
|
public string $permittedURIChars = 'a-z 0-9~%.:_\-,';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
|
|||||||
+46
-38
@@ -9,43 +9,51 @@ class Database extends Config
|
|||||||
public string $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR;
|
public string $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR;
|
||||||
public string $defaultGroup = 'default';
|
public string $defaultGroup = 'default';
|
||||||
|
|
||||||
public array $default = [
|
public array $default = [];
|
||||||
'DSN' => '',
|
public array $tests = [];
|
||||||
'hostname' => 'localhost',
|
|
||||||
'username' => 'u280815660_melabidi',
|
|
||||||
'password' => '>tNxlRzP/W8',
|
|
||||||
'database' => 'u280815660_school',
|
|
||||||
'DBDriver' => 'MySQLi',
|
|
||||||
'DBPrefix' => '',
|
|
||||||
'pConnect' => false,
|
|
||||||
'DBDebug' => (ENVIRONMENT !== 'development'),
|
|
||||||
'charset' => 'utf8',
|
|
||||||
'DBCollat' => 'utf8_general_ci',
|
|
||||||
'swapPre' => '',
|
|
||||||
'encrypt' => false,
|
|
||||||
'compress' => false,
|
|
||||||
'strictOn' => false,
|
|
||||||
'failover' => [],
|
|
||||||
'port' => 3306,
|
|
||||||
];
|
|
||||||
|
|
||||||
public array $tests = [
|
public function __construct()
|
||||||
'DSN' => '',
|
{
|
||||||
'hostname' => 'localhost',
|
parent::__construct();
|
||||||
'username' => 'u280815660_melabidi',
|
|
||||||
'password' => '>tNxlRzP/W8',
|
$this->default = [
|
||||||
'database' => 'u280815660_school',
|
'DSN' => '',
|
||||||
'DBDriver' => 'MySQLi',
|
'hostname' => env('database.default.hostname'),
|
||||||
'DBPrefix' => 'db_',
|
'username' => env('database.default.username'),
|
||||||
'pConnect' => false,
|
'password' => env('database.default.password'),
|
||||||
'DBDebug' => true,
|
'database' => env('database.default.database'),
|
||||||
'charset' => 'utf8',
|
'DBDriver' => env('database.default.DBDriver', 'MySQLi'),
|
||||||
'DBCollat' => 'utf8_general_ci',
|
'DBPrefix' => '',
|
||||||
'swapPre' => '',
|
'pConnect' => false,
|
||||||
'encrypt' => false,
|
'DBDebug' => (ENVIRONMENT !== 'development'),
|
||||||
'compress' => false,
|
'charset' => 'utf8',
|
||||||
'strictOn' => false,
|
'DBCollat' => 'utf8_general_ci',
|
||||||
'failover' => [],
|
'swapPre' => '',
|
||||||
'port' => 3306,
|
'encrypt' => false,
|
||||||
];
|
'compress' => false,
|
||||||
|
'strictOn' => false,
|
||||||
|
'failover' => [],
|
||||||
|
'port' => (int) env('database.default.port', 3306),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->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_'),
|
||||||
|
'pConnect' => false,
|
||||||
|
'DBDebug' => true,
|
||||||
|
'charset' => 'utf8',
|
||||||
|
'DBCollat' => 'utf8_general_ci',
|
||||||
|
'swapPre' => '',
|
||||||
|
'encrypt' => false,
|
||||||
|
'compress' => false,
|
||||||
|
'strictOn' => false,
|
||||||
|
'failover' => [],
|
||||||
|
'port' => (int) env('database.tests.port', env('database.default.port', 3306)),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,6 +230,10 @@ $routes->get('reset_password', 'View\UserController::resetPassword');
|
|||||||
|
|
||||||
//$routes->get('/blocked', 'View\UserController::blocked');
|
//$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('assign_class_student', 'View\StudentController::assignClassStudent');
|
||||||
$routes->post('remove_class_student', 'View\StudentController::removeClassStudent');
|
$routes->post('remove_class_student', 'View\StudentController::removeClassStudent');
|
||||||
$routes->post('administrator/remove_class_student', 'View\StudentController::removeClassStudent'); // alias to avoid 404s
|
$routes->post('administrator/remove_class_student', 'View\StudentController::removeClassStudent'); // alias to avoid 404s
|
||||||
@@ -359,6 +363,7 @@ $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->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->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']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -401,6 +406,8 @@ $routes->get('teacher/progress/submit', 'ClassProgressController::create', ['fil
|
|||||||
$routes->post('teacher/progress/store', 'ClassProgressController::store', ['filter' => 'auth:teacher,teacher_assistant']);
|
$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/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/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/(: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('teacher/progress/attachment-file/(:num)', 'ClassProgressController::attachmentFile/$1', ['filter' => 'auth:teacher,teacher_assistant']);
|
||||||
$routes->get('parent/progress', 'ParentProgressController::index', ['filter' => 'auth:parent']);
|
$routes->get('parent/progress', 'ParentProgressController::index', ['filter' => 'auth:parent']);
|
||||||
|
|||||||
@@ -111,6 +111,23 @@ class AdminProgressController extends BaseController
|
|||||||
);
|
);
|
||||||
$sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays);
|
$sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays);
|
||||||
$sectionSubjectCounts = $this->buildSectionSubjectCounts($rows);
|
$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', [
|
return view('admin/class_progress_list', [
|
||||||
'reportGroupsBySection' => $reportGroupsBySection,
|
'reportGroupsBySection' => $reportGroupsBySection,
|
||||||
@@ -121,6 +138,7 @@ class AdminProgressController extends BaseController
|
|||||||
'sectionStats' => $sectionStats,
|
'sectionStats' => $sectionStats,
|
||||||
'sectionSubjectCounts' => $sectionSubjectCounts,
|
'sectionSubjectCounts' => $sectionSubjectCounts,
|
||||||
'expectedDays' => $expectedDays,
|
'expectedDays' => $expectedDays,
|
||||||
|
'lowProgressSectionIds' => $lowProgressSectionIds,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,13 +432,11 @@ class AdminProgressController extends BaseController
|
|||||||
$allowedSubjects = array_values(array_filter($allowedSubjects));
|
$allowedSubjects = array_values(array_filter($allowedSubjects));
|
||||||
|
|
||||||
$counts = [];
|
$counts = [];
|
||||||
$latestWeekBySection = [];
|
|
||||||
$sectionClassMap = [];
|
$sectionClassMap = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||||
$subject = (string) ($row['subject'] ?? '');
|
$subject = (string) ($row['subject'] ?? '');
|
||||||
$weekStart = (string) ($row['week_start'] ?? '');
|
if ($sectionId === 0 || $subject === '') {
|
||||||
if ($sectionId === 0 || $subject === '' || $weekStart === '') {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
|
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
|
||||||
@@ -429,45 +445,41 @@ class AdminProgressController extends BaseController
|
|||||||
if (! isset($sectionClassMap[$sectionId])) {
|
if (! isset($sectionClassMap[$sectionId])) {
|
||||||
$sectionClassMap[$sectionId] = $this->classSectionModel->getClassId($sectionId);
|
$sectionClassMap[$sectionId] = $this->classSectionModel->getClassId($sectionId);
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
! isset($latestWeekBySection[$sectionId])
|
|
||||||
|| $weekStart > $latestWeekBySection[$sectionId]
|
|
||||||
) {
|
|
||||||
$latestWeekBySection[$sectionId] = $weekStart;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$curriculumChapters = $this->buildCurriculumChapterMap(array_values(array_filter($sectionClassMap)));
|
$curriculumUnits = $this->buildCurriculumUnitMap(array_values(array_filter($sectionClassMap)));
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||||
$subject = (string) ($row['subject'] ?? '');
|
$subject = (string) ($row['subject'] ?? '');
|
||||||
$weekStart = (string) ($row['week_start'] ?? '');
|
if ($sectionId === 0 || $subject === '') {
|
||||||
if ($sectionId === 0 || $subject === '' || $weekStart === '') {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
|
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (empty($latestWeekBySection[$sectionId]) || $weekStart !== $latestWeekBySection[$sectionId]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$subjectSlug = $this->resolveSubjectSlug($subject);
|
$subjectSlug = $this->resolveSubjectSlug($subject);
|
||||||
$classId = $sectionClassMap[$sectionId] ?? null;
|
$classId = $sectionClassMap[$sectionId] ?? null;
|
||||||
$chapterSet = [];
|
$chapterToUnit = [];
|
||||||
if ($classId && $subjectSlug && ! empty($curriculumChapters[$classId][$subjectSlug])) {
|
if ($classId && $subjectSlug && ! empty($curriculumUnits[$classId][$subjectSlug]['chapter_to_unit'])) {
|
||||||
$chapterSet = $curriculumChapters[$classId][$subjectSlug];
|
$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;
|
||||||
}
|
}
|
||||||
$counts[$sectionId] = ($counts[$sectionId] ?? 0) + $this->countChapterSegments(
|
|
||||||
(string) ($row['unit_title'] ?? ''),
|
|
||||||
$chapterSet
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $counts;
|
$totals = [];
|
||||||
|
foreach ($counts as $sectionId => $unitSet) {
|
||||||
|
$totals[$sectionId] = count($unitSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $totals;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function buildCurriculumChapterMap(array $classIds): array
|
protected function buildCurriculumUnitMap(array $classIds): array
|
||||||
{
|
{
|
||||||
$classIds = array_values(array_filter(array_map('intval', $classIds)));
|
$classIds = array_values(array_filter(array_map('intval', $classIds)));
|
||||||
if (empty($classIds)) {
|
if (empty($classIds)) {
|
||||||
@@ -483,10 +495,15 @@ class AdminProgressController extends BaseController
|
|||||||
$classId = (int) ($row['class_id'] ?? 0);
|
$classId = (int) ($row['class_id'] ?? 0);
|
||||||
$subject = (string) ($row['subject'] ?? '');
|
$subject = (string) ($row['subject'] ?? '');
|
||||||
$chapter = trim((string) ($row['chapter_name'] ?? ''));
|
$chapter = trim((string) ($row['chapter_name'] ?? ''));
|
||||||
|
$unitNumber = $row['unit_number'] ?? null;
|
||||||
if ($classId === 0 || $subject === '' || $chapter === '') {
|
if ($classId === 0 || $subject === '' || $chapter === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$map[$classId][$subject][$chapter] = true;
|
if ($unitNumber === null || $unitNumber === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$unitKey = (string) $unitNumber;
|
||||||
|
$map[$classId][$subject]['chapter_to_unit'][$chapter] = $unitKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $map;
|
return $map;
|
||||||
@@ -504,46 +521,93 @@ class AdminProgressController extends BaseController
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function countChapterSegments(string $unitTitle, array $chapterSet): int
|
protected function countUnitSegments(string $unitTitle, array $chapterToUnit): int
|
||||||
{
|
{
|
||||||
$unitTitle = trim($unitTitle);
|
$unitTitle = trim($unitTitle);
|
||||||
if ($unitTitle === '') {
|
if ($unitTitle === '') {
|
||||||
return 1;
|
return 0;
|
||||||
}
|
}
|
||||||
$parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== '');
|
$parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== '');
|
||||||
if (! $parts) {
|
if (! $parts) {
|
||||||
return 1;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$count = 0;
|
|
||||||
$seen = [];
|
$seen = [];
|
||||||
foreach ($parts as $part) {
|
foreach ($parts as $part) {
|
||||||
$chapter = $this->extractChapterFromSegment($part);
|
[$unitPart, $chapterPart] = $this->splitUnitChapterSegment($part);
|
||||||
$key = $chapter !== '' ? $chapter : $part;
|
$key = $this->resolveUnitKey($unitPart, $chapterPart, $chapterToUnit);
|
||||||
if (! empty($chapterSet) && $chapter !== '' && empty($chapterSet[$chapter])) {
|
if ($key === '') {
|
||||||
$key = $part;
|
$key = $part;
|
||||||
}
|
}
|
||||||
if (isset($seen[$key])) {
|
if (isset($seen[$key])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$seen[$key] = true;
|
$seen[$key] = true;
|
||||||
$count++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $count > 0 ? $count : 1;
|
return count($seen);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function extractChapterFromSegment(string $segment): string
|
protected function splitUnitChapterSegment(string $segment): array
|
||||||
{
|
{
|
||||||
$segment = trim($segment);
|
$segment = trim($segment);
|
||||||
if ($segment === '') {
|
if ($segment === '') {
|
||||||
return '';
|
return ['', ''];
|
||||||
}
|
}
|
||||||
$pos = strrpos($segment, '/');
|
$pos = strrpos($segment, '/');
|
||||||
if ($pos === false) {
|
if ($pos === false) {
|
||||||
return $segment;
|
return [$segment, ''];
|
||||||
}
|
}
|
||||||
return trim(substr($segment, $pos + 1));
|
$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 '';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function buildSectionStat(int $submitted, int $expectedDays): array
|
protected function buildSectionStat(int $submitted, int $expectedDays): array
|
||||||
|
|||||||
@@ -469,6 +469,7 @@ class AuthController extends Controller
|
|||||||
// Generate a secure token for the password reset
|
// Generate a secure token for the password reset
|
||||||
helper('text');
|
helper('text');
|
||||||
$token = bin2hex(random_bytes(48));
|
$token = bin2hex(random_bytes(48));
|
||||||
|
$tokenHash = hash('sha256', $token);
|
||||||
|
|
||||||
// Calculate the expiration time for the token (1 hour from now)
|
// Calculate the expiration time for the token (1 hour from now)
|
||||||
$expires_at = Time::now()->addHours(1);
|
$expires_at = Time::now()->addHours(1);
|
||||||
@@ -477,7 +478,7 @@ class AuthController extends Controller
|
|||||||
$passwordResetModel = new PasswordResetModel();
|
$passwordResetModel = new PasswordResetModel();
|
||||||
$passwordResetModel->insert([
|
$passwordResetModel->insert([
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'token' => $token,
|
'token' => $tokenHash,
|
||||||
'created_at' => Time::now(),
|
'created_at' => Time::now(),
|
||||||
'expires_at' => $expires_at,
|
'expires_at' => $expires_at,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class ClassProgressController extends BaseController
|
|||||||
'db_subject' => 'Quran/Arabic',
|
'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 ClassProgressReportModel $reportModel;
|
||||||
protected ClassProgressAttachmentModel $attachmentModel;
|
protected ClassProgressAttachmentModel $attachmentModel;
|
||||||
protected TeacherClassModel $teacherClassModel;
|
protected TeacherClassModel $teacherClassModel;
|
||||||
@@ -98,6 +102,10 @@ class ClassProgressController extends BaseController
|
|||||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
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);
|
$attachmentErrors = $this->validateAttachmentFiles($subjectSections);
|
||||||
if (! empty($attachmentErrors)) {
|
if (! empty($attachmentErrors)) {
|
||||||
return redirect()->back()->withInput()->with('errors', $attachmentErrors);
|
return redirect()->back()->withInput()->with('errors', $attachmentErrors);
|
||||||
@@ -119,6 +127,32 @@ class ClassProgressController extends BaseController
|
|||||||
return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.');
|
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;
|
$status = self::DEFAULT_STATUS;
|
||||||
|
|
||||||
$reportsCreated = 0;
|
$reportsCreated = 0;
|
||||||
@@ -276,6 +310,262 @@ 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)
|
public function attachment($id)
|
||||||
{
|
{
|
||||||
$row = $this->reportModel->find((int)$id);
|
$row = $this->reportModel->find((int)$id);
|
||||||
@@ -419,6 +709,37 @@ class ClassProgressController extends BaseController
|
|||||||
return $flags ? json_encode($flags) : null;
|
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
|
protected function buildUnitChapterSummary(string $slug): ?string
|
||||||
{
|
{
|
||||||
$unitValues = array_map('trim', (array) $this->request->getPost("unit_$slug"));
|
$unitValues = array_map('trim', (array) $this->request->getPost("unit_$slug"));
|
||||||
@@ -431,9 +752,12 @@ class ClassProgressController extends BaseController
|
|||||||
if ($unit === '' && $chapter === '') {
|
if ($unit === '' && $chapter === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (strcasecmp($unit, self::CUSTOM_UNIT_ROW_LABEL) === 0 && $chapter !== '') {
|
||||||
|
$unit = self::CUSTOM_UNIT_ROW_LABEL;
|
||||||
|
}
|
||||||
$segment = $unit;
|
$segment = $unit;
|
||||||
if ($chapter !== '') {
|
if ($chapter !== '') {
|
||||||
$segment = $segment !== '' ? $segment . ' / ' . $chapter : $chapter;
|
$segment = $segment !== '' ? $unit . ' / ' . $chapter : $chapter;
|
||||||
}
|
}
|
||||||
if ($segment === '') {
|
if ($segment === '') {
|
||||||
continue;
|
continue;
|
||||||
@@ -444,9 +768,64 @@ class ClassProgressController extends BaseController
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$summary = implode(' ; ', $parts);
|
$summary = implode(' ; ', $parts);
|
||||||
|
|
||||||
return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary;
|
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
|
protected function buildSundayOptions(int $count = 12): array
|
||||||
{
|
{
|
||||||
$range = $this->resolveProgressDateRange();
|
$range = $this->resolveProgressDateRange();
|
||||||
|
|||||||
@@ -29,18 +29,12 @@ class ParentProgressController extends BaseController
|
|||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$sectionIds = $this->getParentSectionIds();
|
$students = $this->getParentStudents();
|
||||||
$sectionOptions = $this->buildSectionOptions($sectionIds);
|
$sectionIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn (array $student): int => (int) ($student['class_section_id'] ?? 0),
|
||||||
|
$students
|
||||||
|
))));
|
||||||
$subjectSections = ClassProgressController::SUBJECT_SECTIONS;
|
$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 = [];
|
$rows = [];
|
||||||
if (! empty($sectionIds)) {
|
if (! empty($sectionIds)) {
|
||||||
@@ -50,23 +44,32 @@ class ParentProgressController extends BaseController
|
|||||||
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
|
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
|
||||||
->whereIn('class_progress_reports.class_section_id', $sectionIds);
|
->whereIn('class_progress_reports.class_section_id', $sectionIds);
|
||||||
|
|
||||||
if ($selectedSectionId) {
|
|
||||||
$builder->where('class_progress_reports.class_section_id', $selectedSectionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows = $builder
|
$rows = $builder
|
||||||
->orderBy('week_start', 'DESC')
|
->orderBy('week_start', 'DESC')
|
||||||
->findAll();
|
->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
$reportGroups = $this->groupReportsByWeek($rows);
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
return view('parent/class_progress_list', [
|
return view('parent/class_progress_list', [
|
||||||
'reportGroups' => $reportGroups,
|
'students' => $students,
|
||||||
|
'studentReportGroups' => $studentReportGroups,
|
||||||
'subjectSections' => $subjectSections,
|
'subjectSections' => $subjectSections,
|
||||||
'classSectionOptions' => $sectionOptions,
|
'hasStudents' => ! empty($students),
|
||||||
'selectedSectionId' => $selectedSectionId,
|
|
||||||
'hasSections' => ! empty($sectionIds),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,20 +201,53 @@ class ParentProgressController extends BaseController
|
|||||||
return $options;
|
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
|
protected function groupReportsByWeek(array $rows): array
|
||||||
{
|
{
|
||||||
$reportGroups = [];
|
$reportGroups = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
$row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
||||||
$key = $row['week_start'] ?? '';
|
$weekStart = $row['week_start'] ?? '';
|
||||||
if ($key === '') {
|
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||||
|
if ($weekStart === '' || $sectionId === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
$key = $weekStart . ':' . $sectionId;
|
||||||
if (! isset($reportGroups[$key])) {
|
if (! isset($reportGroups[$key])) {
|
||||||
$reportGroups[$key] = [
|
$reportGroups[$key] = [
|
||||||
'week_start' => $row['week_start'] ?? '',
|
'week_start' => $row['week_start'] ?? '',
|
||||||
'week_end' => $row['week_end'] ?? '',
|
'week_end' => $row['week_end'] ?? '',
|
||||||
'class_section_name' => $row['class_section_name'] ?? '',
|
'class_section_name' => $row['class_section_name'] ?? '',
|
||||||
|
'class_section_id' => $sectionId,
|
||||||
'reports' => [],
|
'reports' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ use App\Models\ScoreCommentModel;
|
|||||||
use App\Models\SemesterScoreModel;
|
use App\Models\SemesterScoreModel;
|
||||||
use App\Models\TeacherClassModel;
|
use App\Models\TeacherClassModel;
|
||||||
use App\Models\TeacherSubmissionNotificationHistoryModel;
|
use App\Models\TeacherSubmissionNotificationHistoryModel;
|
||||||
|
use App\Models\ExamDraftModel;
|
||||||
|
use App\Models\HomeworkModel;
|
||||||
use App\Services\SemesterRangeService;
|
use App\Services\SemesterRangeService;
|
||||||
|
|
||||||
use CodeIgniter\Events\Events;
|
use CodeIgniter\Events\Events;
|
||||||
@@ -696,15 +698,26 @@ class AdministratorController extends BaseController
|
|||||||
|
|
||||||
public function teacherSubmissionsReport()
|
public function teacherSubmissionsReport()
|
||||||
{
|
{
|
||||||
$semester = (string)($this->semester ?? '');
|
$semester = (string)($this->configModel->getConfig('semester') ?? $this->semester ?? '');
|
||||||
$schoolYear = (string)($this->schoolYear ?? '');
|
$schoolYear = (string)($this->configModel->getConfig('school_year') ?? $this->schoolYear ?? '');
|
||||||
|
$semesterResolver = new SemesterRangeService($this->configModel);
|
||||||
|
$semesterNorm = $semesterResolver->normalizeSemester($semester);
|
||||||
|
$semesterFilter = $semesterNorm !== '' ? $semesterNorm : $semester;
|
||||||
|
$semesterCandidates = $this->buildSemesterCandidates($semesterFilter);
|
||||||
|
$lowProgressRaw = (string) $this->request->getGet('low_progress_sections');
|
||||||
|
$lowProgressSectionIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
'intval',
|
||||||
|
preg_split('/\s*,\s*/', $lowProgressRaw, -1, PREG_SPLIT_NO_EMPTY)
|
||||||
|
))));
|
||||||
|
|
||||||
$scoreComments = new ScoreCommentModel();
|
$scoreComments = new ScoreCommentModel();
|
||||||
$semesterScores = new SemesterScoreModel();
|
$semesterScores = new SemesterScoreModel();
|
||||||
$attendanceDays = new AttendanceDayModel();
|
$attendanceDays = new AttendanceDayModel();
|
||||||
|
$examDrafts = new ExamDraftModel();
|
||||||
|
$homeworkModel = new HomeworkModel();
|
||||||
$historyModel = new TeacherSubmissionNotificationHistoryModel();
|
$historyModel = new TeacherSubmissionNotificationHistoryModel();
|
||||||
|
|
||||||
$assignmentRows = $this->db->table('teacher_class tc')
|
$assignmentQuery = $this->db->table('teacher_class tc')
|
||||||
->select([
|
->select([
|
||||||
'tc.class_section_id',
|
'tc.class_section_id',
|
||||||
'cs.class_section_name',
|
'cs.class_section_name',
|
||||||
@@ -715,11 +728,97 @@ class AdministratorController extends BaseController
|
|||||||
])
|
])
|
||||||
->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left')
|
->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left')
|
||||||
->join('users u', 'u.id = tc.teacher_id', 'left')
|
->join('users u', 'u.id = tc.teacher_id', 'left')
|
||||||
->where('tc.school_year', $schoolYear)
|
->orderBy('cs.class_section_name', 'ASC');
|
||||||
->where('tc.semester', $semester)
|
|
||||||
->orderBy('cs.class_section_name', 'ASC')
|
$filteredQuery = clone $assignmentQuery;
|
||||||
->get()
|
if ($schoolYear !== '') {
|
||||||
->getResultArray();
|
$filteredQuery = $filteredQuery->where('tc.school_year', $schoolYear);
|
||||||
|
}
|
||||||
|
if (!empty($semesterCandidates)) {
|
||||||
|
$filteredQuery = $filteredQuery->whereIn('tc.semester', $semesterCandidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignmentRows = $filteredQuery->get()->getResultArray();
|
||||||
|
if (empty($assignmentRows) && ($schoolYear !== '' || $semester !== '')) {
|
||||||
|
$assignmentRows = $assignmentQuery->get()->getResultArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
$studentCounts = $this->studentClassModel->getStudentCountsBySection($schoolYear !== '' ? $schoolYear : null);
|
||||||
|
$sectionRows = $this->classSectionModel
|
||||||
|
->select('class_section_id, class_section_name')
|
||||||
|
->orderBy('class_section_name', 'ASC')
|
||||||
|
->findAll();
|
||||||
|
$sectionMap = [];
|
||||||
|
foreach ($sectionRows as $sectionRow) {
|
||||||
|
$sectionId = (int) ($sectionRow['class_section_id'] ?? 0);
|
||||||
|
if ($sectionId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (empty($studentCounts[$sectionId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sectionMap[$sectionId] = $sectionRow['class_section_name'] ?? "Section {$sectionId}";
|
||||||
|
}
|
||||||
|
$sectionIds = array_keys($sectionMap);
|
||||||
|
|
||||||
|
[$progressExpectedWeeks, $progressSubmittedBySection] = $this->buildClassProgressStats($sectionIds);
|
||||||
|
$examDraftCounts = [];
|
||||||
|
$examDraftDeadline = $this->resolveTeacherDashboardExamDraftDeadline($semester, $schoolYear);
|
||||||
|
$examDraftDeadlineConfig = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? ''));
|
||||||
|
$examDraftDeadlineFormatted = '';
|
||||||
|
if ($examDraftDeadlineConfig !== '') {
|
||||||
|
$parsedUi = $this->parseExamDraftDeadlineConfigValue();
|
||||||
|
$examDraftDeadlineFormatted = $parsedUi !== null ? $parsedUi->format('M j, Y') : '';
|
||||||
|
}
|
||||||
|
$homeworkCounts = [];
|
||||||
|
if (! empty($sectionIds)) {
|
||||||
|
$draftBuilder = $examDrafts
|
||||||
|
->select('class_section_id')
|
||||||
|
->whereIn('class_section_id', $sectionIds);
|
||||||
|
if ($schoolYear !== '') {
|
||||||
|
$draftBuilder->where('school_year', $schoolYear);
|
||||||
|
}
|
||||||
|
if (!empty($semesterCandidates)) {
|
||||||
|
$draftBuilder->whereIn('semester', $semesterCandidates);
|
||||||
|
}
|
||||||
|
if ($this->db->fieldExists('is_legacy', 'exam_drafts')) {
|
||||||
|
$draftBuilder->where('is_legacy', 0);
|
||||||
|
}
|
||||||
|
$draftRows = $draftBuilder->findAll();
|
||||||
|
foreach ($draftRows as $draft) {
|
||||||
|
$sectionId = (int) ($draft['class_section_id'] ?? 0);
|
||||||
|
if ($sectionId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$examDraftCounts[$sectionId] = ($examDraftCounts[$sectionId] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$homeworkBuilder = $homeworkModel
|
||||||
|
->select('class_section_id, homework_index')
|
||||||
|
->whereIn('class_section_id', $sectionIds);
|
||||||
|
if ($schoolYear !== '') {
|
||||||
|
$homeworkBuilder->where('school_year', $schoolYear);
|
||||||
|
}
|
||||||
|
if (!empty($semesterCandidates)) {
|
||||||
|
$homeworkBuilder->whereIn('semester', $semesterCandidates);
|
||||||
|
}
|
||||||
|
$homeworkRows = $homeworkBuilder
|
||||||
|
->where('score IS NOT NULL', null, false)
|
||||||
|
->where('score !=', '')
|
||||||
|
->groupBy('class_section_id, homework_index')
|
||||||
|
->findAll();
|
||||||
|
foreach ($homeworkRows as $row) {
|
||||||
|
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||||
|
if ($sectionId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$homeworkCounts[$sectionId] = ($homeworkCounts[$sectionId] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($lowProgressSectionIds)) {
|
||||||
|
$lowProgressSectionIds = $this->resolveLowProgressSectionIds($sectionIds);
|
||||||
|
}
|
||||||
|
|
||||||
$teachersBySection = [];
|
$teachersBySection = [];
|
||||||
foreach ($assignmentRows as $assignment) {
|
foreach ($assignmentRows as $assignment) {
|
||||||
@@ -745,7 +844,7 @@ class AdministratorController extends BaseController
|
|||||||
$entry = &$teachersBySection[$sectionId];
|
$entry = &$teachersBySection[$sectionId];
|
||||||
if (!isset($entry)) {
|
if (!isset($entry)) {
|
||||||
$entry = [
|
$entry = [
|
||||||
'class_section' => $assignment['class_section_name'] ?? "Section {$sectionId}",
|
'class_section' => $assignment['class_section_name'] ?? ($sectionMap[$sectionId] ?? "Section {$sectionId}"),
|
||||||
'teachers' => [],
|
'teachers' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -765,35 +864,49 @@ class AdministratorController extends BaseController
|
|||||||
$missingItemCount = 0;
|
$missingItemCount = 0;
|
||||||
$allTeacherIds = [];
|
$allTeacherIds = [];
|
||||||
$allClassSectionIds = [];
|
$allClassSectionIds = [];
|
||||||
foreach ($teachersBySection as $classSectionId => $section) {
|
$examTerm = $this->resolveExamTermLabel($semester);
|
||||||
|
$examScoreField = $examTerm === 'final' ? 'final_exam_score' : 'midterm_exam_score';
|
||||||
|
|
||||||
|
foreach ($sectionMap as $classSectionId => $sectionName) {
|
||||||
$classSectionId = (int)$classSectionId;
|
$classSectionId = (int)$classSectionId;
|
||||||
if ($classSectionId <= 0) {
|
if ($classSectionId <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$studentEntries = $this->studentClassModel
|
$studentQuery = $this->studentClassModel
|
||||||
->select('student_id')
|
->select('student_id')
|
||||||
->where('class_section_id', $classSectionId)
|
->where('class_section_id', $classSectionId)
|
||||||
->where('semester', $semester)
|
->where('school_year', $schoolYear);
|
||||||
->where('school_year', $schoolYear)
|
if (!empty($semesterCandidates)) {
|
||||||
->findAll();
|
$studentQuery->whereIn('semester', $semesterCandidates);
|
||||||
|
}
|
||||||
|
$studentEntries = $studentQuery->findAll();
|
||||||
|
if (empty($studentEntries)) {
|
||||||
|
$studentEntries = $this->studentClassModel
|
||||||
|
->select('student_id')
|
||||||
|
->where('class_section_id', $classSectionId)
|
||||||
|
->where('school_year', $schoolYear)
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
$studentIds = array_filter(array_map(static fn($entry) => (int)($entry['student_id'] ?? 0), $studentEntries));
|
$studentIds = array_filter(array_map(static fn($entry) => (int)($entry['student_id'] ?? 0), $studentEntries));
|
||||||
$expected = count($studentIds);
|
$expected = count($studentIds);
|
||||||
|
|
||||||
$midtermStudents = [];
|
$midtermStudents = [];
|
||||||
$participationStudents = [];
|
$participationStudents = [];
|
||||||
if ($classSectionId > 0) {
|
if ($classSectionId > 0) {
|
||||||
$scoreRecords = $semesterScores
|
$scoreQuery = $semesterScores
|
||||||
->where('class_section_id', $classSectionId)
|
->where('class_section_id', $classSectionId)
|
||||||
->where('semester', $semester)
|
->where('school_year', $schoolYear);
|
||||||
->where('school_year', $schoolYear)
|
if (!empty($semesterCandidates)) {
|
||||||
->findAll();
|
$scoreQuery->whereIn('semester', $semesterCandidates);
|
||||||
|
}
|
||||||
|
$scoreRecords = $scoreQuery->findAll();
|
||||||
foreach ($scoreRecords as $score) {
|
foreach ($scoreRecords as $score) {
|
||||||
$sid = (int)($score['student_id'] ?? 0);
|
$sid = (int)($score['student_id'] ?? 0);
|
||||||
if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) {
|
if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$midtermValue = trim((string)($score['midterm_exam_score'] ?? ''));
|
$midtermValue = trim((string)($score[$examScoreField] ?? ''));
|
||||||
if ($midtermValue !== '') {
|
if ($midtermValue !== '') {
|
||||||
$midtermStudents[$sid] = true;
|
$midtermStudents[$sid] = true;
|
||||||
}
|
}
|
||||||
@@ -807,13 +920,15 @@ class AdministratorController extends BaseController
|
|||||||
$midtermCommentStudents = [];
|
$midtermCommentStudents = [];
|
||||||
$ptapCommentStudents = [];
|
$ptapCommentStudents = [];
|
||||||
if (!empty($studentIds)) {
|
if (!empty($studentIds)) {
|
||||||
$comments = $scoreComments
|
$commentQuery = $scoreComments
|
||||||
->select('student_id, score_type, comment')
|
->select('student_id, score_type, comment')
|
||||||
->whereIn('student_id', $studentIds)
|
->whereIn('student_id', $studentIds)
|
||||||
->where('semester', $semester)
|
|
||||||
->where('school_year', $schoolYear)
|
->where('school_year', $schoolYear)
|
||||||
->whereIn('score_type', ['midterm', 'ptap'])
|
->whereIn('score_type', [$examTerm, 'ptap']);
|
||||||
->findAll();
|
if (!empty($semesterCandidates)) {
|
||||||
|
$commentQuery->whereIn('semester', $semesterCandidates);
|
||||||
|
}
|
||||||
|
$comments = $commentQuery->findAll();
|
||||||
foreach ($comments as $comment) {
|
foreach ($comments as $comment) {
|
||||||
$sid = (int)($comment['student_id'] ?? 0);
|
$sid = (int)($comment['student_id'] ?? 0);
|
||||||
if ($sid <= 0) {
|
if ($sid <= 0) {
|
||||||
@@ -824,7 +939,7 @@ class AdministratorController extends BaseController
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$type = strtolower(trim((string)($comment['score_type'] ?? '')));
|
$type = strtolower(trim((string)($comment['score_type'] ?? '')));
|
||||||
if ($type === 'midterm') {
|
if ($type === $examTerm) {
|
||||||
$midtermCommentStudents[$sid] = true;
|
$midtermCommentStudents[$sid] = true;
|
||||||
}
|
}
|
||||||
if ($type === 'ptap') {
|
if ($type === 'ptap') {
|
||||||
@@ -833,14 +948,17 @@ class AdministratorController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$attendanceRow = $attendanceDays
|
$attendanceQuery = $attendanceDays
|
||||||
->where('class_section_id', $classSectionId)
|
->where('class_section_id', $classSectionId)
|
||||||
->where('semester', $semester)
|
|
||||||
->where('school_year', $schoolYear)
|
->where('school_year', $schoolYear)
|
||||||
->where('date', $today)
|
->where('date', $today);
|
||||||
->first();
|
if (!empty($semesterCandidates)) {
|
||||||
|
$attendanceQuery->whereIn('semester', $semesterCandidates);
|
||||||
|
}
|
||||||
|
$attendanceRow = $attendanceQuery->first();
|
||||||
$attendanceSubmitted = $attendanceRow && in_array(strtolower((string)($attendanceRow['status'] ?? '')), ['submitted', 'published', 'finalized'], true);
|
$attendanceSubmitted = $attendanceRow && in_array(strtolower((string)($attendanceRow['status'] ?? '')), ['submitted', 'published', 'finalized'], true);
|
||||||
|
|
||||||
|
$section = $teachersBySection[$classSectionId] ?? ['teachers' => []];
|
||||||
$teacherList = $section['teachers'] ?? [];
|
$teacherList = $section['teachers'] ?? [];
|
||||||
if (!empty($teacherList)) {
|
if (!empty($teacherList)) {
|
||||||
usort($teacherList, function ($a, $b) {
|
usort($teacherList, function ($a, $b) {
|
||||||
@@ -861,18 +979,27 @@ class AdministratorController extends BaseController
|
|||||||
$participationStatus = $this->submissionStatus(count($participationStudents), $expected);
|
$participationStatus = $this->submissionStatus(count($participationStudents), $expected);
|
||||||
$ptapCommentStatus = $this->submissionStatus(count($ptapCommentStudents), $expected);
|
$ptapCommentStatus = $this->submissionStatus(count($ptapCommentStudents), $expected);
|
||||||
$attendanceStatus = $this->attendanceStatus($attendanceSubmitted);
|
$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 = [
|
$statusDetails = [
|
||||||
'midterm_score_status' => $midtermScoreStatus,
|
'midterm_score_status' => $midtermScoreStatus,
|
||||||
'midterm_comment_status' => $midtermCommentStatus,
|
'midterm_comment_status' => $midtermCommentStatus,
|
||||||
'participation_status' => $participationStatus,
|
'participation_status' => $participationStatus,
|
||||||
'ptap_comment_status' => $ptapCommentStatus,
|
'ptap_comment_status' => $ptapCommentStatus,
|
||||||
|
'class_progress_status' => $classProgressStatus,
|
||||||
|
'exam_draft_status' => $examDraftStatus,
|
||||||
|
'homework_status' => $homeworkStatus,
|
||||||
];
|
];
|
||||||
$missingItemsForSection = $this->buildMissingItems($statusDetails);
|
$missingItemsForSection = $this->buildMissingItems($statusDetails, $semester);
|
||||||
$missingItemCount += count($missingItemsForSection);
|
$missingItemCount += count($missingItemsForSection);
|
||||||
$totalStatuses += count($statusDetails);
|
$totalStatuses += count($statusDetails);
|
||||||
|
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
'class_section' => $section['class_section'] ?? "Section {$classSectionId}",
|
'class_section' => $sectionMap[$classSectionId] ?? ($section['class_section'] ?? "Section {$classSectionId}"),
|
||||||
'class_section_id' => $classSectionId,
|
'class_section_id' => $classSectionId,
|
||||||
'teachers' => $teacherList,
|
'teachers' => $teacherList,
|
||||||
'midterm_score_status' => $midtermScoreStatus,
|
'midterm_score_status' => $midtermScoreStatus,
|
||||||
@@ -880,6 +1007,9 @@ class AdministratorController extends BaseController
|
|||||||
'participation_status' => $participationStatus,
|
'participation_status' => $participationStatus,
|
||||||
'ptap_comment_status' => $ptapCommentStatus,
|
'ptap_comment_status' => $ptapCommentStatus,
|
||||||
'attendance_status' => $attendanceStatus,
|
'attendance_status' => $attendanceStatus,
|
||||||
|
'class_progress_status' => $classProgressStatus,
|
||||||
|
'exam_draft_status' => $examDraftStatus,
|
||||||
|
'homework_status' => $homeworkStatus,
|
||||||
'missing_items' => $missingItemsForSection,
|
'missing_items' => $missingItemsForSection,
|
||||||
'student_count' => $expected,
|
'student_count' => $expected,
|
||||||
];
|
];
|
||||||
@@ -941,15 +1071,183 @@ class AdministratorController extends BaseController
|
|||||||
'schoolYear' => $schoolYear,
|
'schoolYear' => $schoolYear,
|
||||||
'notificationHistory' => $historyMap,
|
'notificationHistory' => $historyMap,
|
||||||
'summary' => $summary,
|
'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()
|
public function sendTeacherSubmissionNotifications()
|
||||||
{$notify = $this->request->getPost('notify');
|
{$notify = $this->request->getPost('notify');
|
||||||
if (!is_array($notify)) {
|
if (!is_array($notify)) {
|
||||||
return redirect()->back()->with('info', 'Select at least one teacher to 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') ?? [];
|
$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 = [];
|
$targets = [];
|
||||||
foreach ($notify as $sectionIdRaw => $teachers) {
|
foreach ($notify as $sectionIdRaw => $teachers) {
|
||||||
@@ -1006,6 +1304,10 @@ class AdministratorController extends BaseController
|
|||||||
|
|
||||||
$historyModel = new TeacherSubmissionNotificationHistoryModel();
|
$historyModel = new TeacherSubmissionNotificationHistoryModel();
|
||||||
$scoreUrl = site_url('/');
|
$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;
|
$sentCount = 0;
|
||||||
$failCount = 0;
|
$failCount = 0;
|
||||||
|
|
||||||
@@ -1021,6 +1323,13 @@ class AdministratorController extends BaseController
|
|||||||
$subject = "Reminder: Complete submissions for {$sectionName}";
|
$subject = "Reminder: Complete submissions for {$sectionName}";
|
||||||
$missingPayload = $missingItemsPayload[$classSectionId][$teacherId] ?? '';
|
$missingPayload = $missingItemsPayload[$classSectionId][$teacherId] ?? '';
|
||||||
$missingItems = $this->parseMissingItemsPayload((string)$missingPayload);
|
$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)) {
|
if (!empty($missingItems)) {
|
||||||
$missingText = htmlspecialchars(
|
$missingText = htmlspecialchars(
|
||||||
$this->formatMissingItemsText($missingItems),
|
$this->formatMissingItemsText($missingItems),
|
||||||
@@ -1033,10 +1342,46 @@ class AdministratorController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$subject = "Reminder: Complete submissions for {$sectionName}";
|
$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>"
|
$body = "<p>Dear {$teacherName},</p>"
|
||||||
. "<p>Administration is gently reminding you to wrap up any remaining score submissions and/or comments for {$sectionName}.</p>"
|
. "<p>Administration is gently reminding you to wrap up any remaining "
|
||||||
|
. ($nonScoreOnly ? "submissions for {$sectionName}." : "score submissions, comments, and related items for {$sectionName}.")
|
||||||
|
. "</p>"
|
||||||
. $missingNote
|
. $missingNote
|
||||||
. "<p>Visit <a href=\"{$scoreUrl}\">Teacher Score Submission</a> to address any remaining items.</p>"
|
. $progressNote
|
||||||
|
. $examDraftNote
|
||||||
|
. $homeworkNote
|
||||||
|
. ($nonScoreOnly ? '' : "<p>Visit <a href=\"{$scoreUrl}\">Teacher Score Submission</a> to address any remaining items.</p>")
|
||||||
. "<p>Thank you,<br>Al Rahma Administration</p>";
|
. "<p>Thank you,<br>Al Rahma Administration</p>";
|
||||||
|
|
||||||
$email = $teacher['email'] ?? '';
|
$email = $teacher['email'] ?? '';
|
||||||
@@ -1098,6 +1443,170 @@ 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
|
private function attendanceStatus(bool $submitted): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -1107,14 +1616,20 @@ class AdministratorController extends BaseController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildMissingItems(array $statusMap): array
|
private function buildMissingItems(array $statusMap, string $semester): array
|
||||||
{
|
{
|
||||||
|
$examTerm = $this->resolveExamTermLabel($semester);
|
||||||
|
$examScoreLabel = $examTerm === 'final' ? 'final scores' : 'midterm scores';
|
||||||
|
$examCommentLabel = $examTerm === 'final' ? 'final comments' : 'midterm comments';
|
||||||
$labels = [
|
$labels = [
|
||||||
'midterm_score_status' => 'midterm scores',
|
'midterm_score_status' => $examScoreLabel,
|
||||||
'midterm_comment_status' => 'midterm comments',
|
'midterm_comment_status' => $examCommentLabel,
|
||||||
'participation_status' => 'participation',
|
'participation_status' => 'participation',
|
||||||
'ptap_comment_status' => 'PTAP comments',
|
'ptap_comment_status' => 'PTAP comments',
|
||||||
'attendance_status' => 'attendance',
|
'attendance_status' => 'attendance',
|
||||||
|
'class_progress_status' => 'class progress',
|
||||||
|
'exam_draft_status' => 'exam draft',
|
||||||
|
'homework_status' => 'homework',
|
||||||
];
|
];
|
||||||
|
|
||||||
$items = [];
|
$items = [];
|
||||||
|
|||||||
@@ -119,7 +119,12 @@ class AssignmentController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$students = [];
|
$students = [];
|
||||||
|
$seenStudentIds = [];
|
||||||
foreach ($studentClasses as $studentClass) {
|
foreach ($studentClasses as $studentClass) {
|
||||||
|
$sid = (int)($studentClass['student_id'] ?? 0);
|
||||||
|
if ($sid <= 0 || isset($seenStudentIds[$sid])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ($sectionSemester === '' && !empty($studentClass['semester'])) {
|
if ($sectionSemester === '' && !empty($studentClass['semester'])) {
|
||||||
$sectionSemester = (string)$studentClass['semester'];
|
$sectionSemester = (string)$studentClass['semester'];
|
||||||
}
|
}
|
||||||
@@ -149,6 +154,7 @@ class AssignmentController extends BaseController
|
|||||||
'tuition_paid' => esc($student['tuition_paid'] ? 'Yes' : 'No'),
|
'tuition_paid' => esc($student['tuition_paid'] ? 'Yes' : 'No'),
|
||||||
'school_id' => esc($student['school_id']),
|
'school_id' => esc($student['school_id']),
|
||||||
];
|
];
|
||||||
|
$seenStudentIds[$sid] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sectionSemesterDisplay = $sectionSemester !== '' ? $sectionSemester : ((string)($this->semester ?? ''));
|
$sectionSemesterDisplay = $sectionSemester !== '' ? $sectionSemester : ((string)($this->semester ?? ''));
|
||||||
|
|||||||
@@ -1004,9 +1004,13 @@ public function showUpdateAttendanceForm()
|
|||||||
}
|
}
|
||||||
|
|
||||||
$hasRoster = false;
|
$hasRoster = false;
|
||||||
|
$seenStudents = [];
|
||||||
|
|
||||||
foreach ($students as $sc) {
|
foreach ($students as $sc) {
|
||||||
$studentId = (int)$sc['student_id'];
|
$studentId = (int)$sc['student_id'];
|
||||||
|
if ($studentId <= 0 || isset($seenStudents[$studentId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$student = $this->studentModel
|
$student = $this->studentModel
|
||||||
->select('id, firstname, lastname, school_id')
|
->select('id, firstname, lastname, school_id')
|
||||||
->find($studentId);
|
->find($studentId);
|
||||||
@@ -1014,6 +1018,7 @@ public function showUpdateAttendanceForm()
|
|||||||
|
|
||||||
$studentsBySection[$secCode][] = $student;
|
$studentsBySection[$secCode][] = $student;
|
||||||
$hasRoster = true;
|
$hasRoster = true;
|
||||||
|
$seenStudents[$studentId] = true;
|
||||||
|
|
||||||
// Attendance history
|
// Attendance history
|
||||||
$qb = $this->attendanceDataModel
|
$qb = $this->attendanceDataModel
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use CodeIgniter\I18n\Time;
|
|||||||
|
|
||||||
class AuthorizedUsersController extends ResourceController
|
class AuthorizedUsersController extends ResourceController
|
||||||
{
|
{
|
||||||
|
private const TOKEN_TTL_HOURS = 24;
|
||||||
|
|
||||||
protected $userModel;
|
protected $userModel;
|
||||||
protected $authorizedUserModel;
|
protected $authorizedUserModel;
|
||||||
|
|
||||||
@@ -18,6 +20,30 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
$this->userModel = new UserModel();
|
$this->userModel = new UserModel();
|
||||||
$this->authorizedUserModel = new AuthorizedUserModel();
|
$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.
|
* Return a list of authorized users for the logged-in main user.
|
||||||
*
|
*
|
||||||
@@ -25,7 +51,10 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requireLogin()) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
$userId = session()->get('user_id');
|
$userId = session()->get('user_id');
|
||||||
$authorizedUsers = $this->authorizedUserModel->where('user_id', $userId)->findAll();
|
$authorizedUsers = $this->authorizedUserModel->where('user_id', $userId)->findAll();
|
||||||
|
|
||||||
@@ -40,12 +69,20 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
*/
|
*/
|
||||||
public function show($id = null)
|
public function show($id = null)
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requireLogin()) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
$authorizedUser = $this->authorizedUserModel->find($id);
|
$authorizedUser = $this->authorizedUserModel->find($id);
|
||||||
|
|
||||||
if (!$authorizedUser) {
|
if (!$authorizedUser) {
|
||||||
return $this->failNotFound('Authorized user not found.');
|
return $this->failNotFound('Authorized user not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($resp = $this->requireOwnership($authorizedUser)) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->respond($authorizedUser);
|
return $this->respond($authorizedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +93,10 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
*/
|
*/
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requireLogin()) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
$email = strtolower($this->request->getPost('email'));
|
$email = strtolower($this->request->getPost('email'));
|
||||||
|
|
||||||
// Validate email
|
// Validate email
|
||||||
@@ -66,19 +107,20 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
$user = $this->userModel->where('email', $email)->first();
|
$user = $this->userModel->where('email', $email)->first();
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
return $this->failNotFound('No user found with this email.');
|
return $this->respondCreated(['message' => 'Authorized user added. A confirmation email has been sent.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a token for confirmation
|
// Generate a token for confirmation
|
||||||
helper('text');
|
helper('text');
|
||||||
$token = bin2hex(random_bytes(48));
|
$token = bin2hex(random_bytes(48));
|
||||||
|
$tokenHash = $this->hashToken($token);
|
||||||
|
|
||||||
// Add entry to the authorized_users table
|
// Add entry to the authorized_users table
|
||||||
$this->authorizedUserModel->insert([
|
$this->authorizedUserModel->insert([
|
||||||
'user_id' => session()->get('user_id'), // Main user ID
|
'user_id' => session()->get('user_id'), // Main user ID
|
||||||
'authorized_user_id' => $user['id'],
|
'authorized_user_id' => $user['id'],
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'token' => $token,
|
'token' => $tokenHash,
|
||||||
'status' => 'Pending'
|
'status' => 'Pending'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -96,6 +138,10 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
*/
|
*/
|
||||||
public function update($id = null)
|
public function update($id = null)
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requireLogin()) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch the authorized user
|
// Fetch the authorized user
|
||||||
$authorizedUser = $this->authorizedUserModel->find($id);
|
$authorizedUser = $this->authorizedUserModel->find($id);
|
||||||
|
|
||||||
@@ -103,6 +149,10 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
return $this->failNotFound('Authorized user not found.');
|
return $this->failNotFound('Authorized user not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($resp = $this->requireOwnership($authorizedUser)) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the authorized user’s information (e.g., email)
|
// Update the authorized user’s information (e.g., email)
|
||||||
$email = strtolower($this->request->getPost('email'));
|
$email = strtolower($this->request->getPost('email'));
|
||||||
if ($email && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if ($email && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
@@ -122,12 +172,20 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
*/
|
*/
|
||||||
public function delete($id = null)
|
public function delete($id = null)
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requireLogin()) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
$authorizedUser = $this->authorizedUserModel->find($id);
|
$authorizedUser = $this->authorizedUserModel->find($id);
|
||||||
|
|
||||||
if (!$authorizedUser) {
|
if (!$authorizedUser) {
|
||||||
return $this->failNotFound('Authorized user not found.');
|
return $this->failNotFound('Authorized user not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($resp = $this->requireOwnership($authorizedUser)) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the authorized user record
|
// Delete the authorized user record
|
||||||
$this->authorizedUserModel->delete($id);
|
$this->authorizedUserModel->delete($id);
|
||||||
|
|
||||||
@@ -147,16 +205,28 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
return $this->fail('Invalid confirmation link.');
|
return $this->fail('Invalid confirmation link.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$authorizedUser = $this->authorizedUserModel->where('token', $token)->first();
|
$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();
|
||||||
|
|
||||||
if (!$authorizedUser) {
|
if (!$authorizedUser) {
|
||||||
return $this->fail('Invalid or expired confirmation link.');
|
return $this->fail('Invalid or expired confirmation link.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the authorized user as active
|
// Mark the authorized user as active and rotate token for password setup
|
||||||
$this->authorizedUserModel->update($authorizedUser['id'], ['status' => 'Active', 'token' => null]);
|
$nextToken = bin2hex(random_bytes(48));
|
||||||
|
$nextTokenHash = $this->hashToken($nextToken);
|
||||||
|
$this->authorizedUserModel->update($authorizedUser['id'], [
|
||||||
|
'status' => 'Active',
|
||||||
|
'token' => $nextTokenHash,
|
||||||
|
]);
|
||||||
|
|
||||||
return redirect()->to('/set_authorized_user_password/' . $authorizedUser['authorized_user_id']);
|
return redirect()->to('/set_authorized_user_password/' . $authorizedUser['authorized_user_id'] . '?token=' . $nextToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,13 +237,36 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
*/
|
*/
|
||||||
public function setPassword($authorizedUserId)
|
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);
|
$user = $this->userModel->find($authorizedUserId);
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
return $this->failNotFound('User not found.');
|
return $this->failNotFound('User not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('user/set_authorized_user_password', ['userId' => $authorizedUserId]);
|
return view('user/set_authorized_user_password', [
|
||||||
|
'userId' => $authorizedUserId,
|
||||||
|
'token' => $token,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,38 +274,59 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
*
|
*
|
||||||
* @return ResponseInterface
|
* @return ResponseInterface
|
||||||
*/
|
*/
|
||||||
/*
|
public function savePassword($authorizedUserId = null)
|
||||||
public function savePassword()
|
|
||||||
{
|
{
|
||||||
// Validate the request
|
|
||||||
$validation = \Config\Services::validation();
|
$validation = \Config\Services::validation();
|
||||||
$validation->setRules([
|
$validation->setRules([
|
||||||
'password' => 'required|min_length[6]',
|
'password' => [
|
||||||
|
'label' => 'Password',
|
||||||
|
'rules' => 'required|min_length[8]|regex_match[/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\\-=\\+*#$%&!?])[A-Za-z\\d@\\-=\\+*#$%&!?]{8,}$/]',
|
||||||
|
],
|
||||||
'password_confirm' => 'required|matches[password]',
|
'password_confirm' => 'required|matches[password]',
|
||||||
'user_id' => 'required|integer'
|
'user_id' => 'required|integer',
|
||||||
|
'token' => 'required',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$this->validate($validation->getRules())) {
|
if (!$this->validate($validation->getRules())) {
|
||||||
return $this->failValidationErrors($validation->getErrors());
|
return $this->failValidationErrors($validation->getErrors());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the validated input
|
$userId = (int) $this->request->getPost('user_id');
|
||||||
$userId = $this->request->getPost('user_id');
|
$token = (string) $this->request->getPost('token');
|
||||||
$password = $this->request->getPost('password');
|
$authorizedUserId = $authorizedUserId !== null ? (int) $authorizedUserId : $userId;
|
||||||
|
|
||||||
$model = new UserModel();
|
if ($userId <= 0 || $authorizedUserId <= 0 || $userId !== $authorizedUserId) {
|
||||||
$user = $model->find($userId);
|
return $this->fail('Invalid request.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$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) {
|
if (!$user) {
|
||||||
return $this->failNotFound('User not found.');
|
return $this->failNotFound('User not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the password
|
$password = (string) $this->request->getPost('password');
|
||||||
$model->update($userId, ['password' => password_hash($password, PASSWORD_DEFAULT)]);
|
$hashedPassword = pbkdf2_hash($password);
|
||||||
|
|
||||||
|
$this->userModel->update($authorizedUserId, ['password' => $hashedPassword]);
|
||||||
|
$this->authorizedUserModel->update($authorizedUser['id'], ['token' => null]);
|
||||||
|
|
||||||
return $this->respond(['message' => 'Password has been successfully set.']);
|
return $this->respond(['message' => 'Password has been successfully set.']);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Sends a confirmation email to the authorized user.
|
* Sends a confirmation email to the authorized user.
|
||||||
*
|
*
|
||||||
@@ -242,4 +356,4 @@ class AuthorizedUsersController extends ResourceController
|
|||||||
log_message('error', 'Failed to send authorized user confirmation email to ' . $email);
|
log_message('error', 'Failed to send authorized user confirmation email to ' . $email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,58 +93,6 @@ class EventController extends ResourceController
|
|||||||
'created_by' => session()->get('user_id'),
|
'created_by' => session()->get('user_id'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($eventId) {
|
|
||||||
$amount = (float) $this->request->getPost('amount');
|
|
||||||
$semester = (string) $this->request->getPost('semester');
|
|
||||||
$schoolYear = (string) $this->request->getPost('school_year');
|
|
||||||
$userId = (int) (session()->get('user_id') ?? 0);
|
|
||||||
|
|
||||||
$enrollments = $this->enrollmentModel
|
|
||||||
->select('enrollments.student_id, students.parent_id')
|
|
||||||
->join('students', 'students.id = enrollments.student_id', 'left')
|
|
||||||
->where('enrollments.school_year', $schoolYear)
|
|
||||||
->whereIn('enrollments.enrollment_status', ['enrolled', 'payment pending'])
|
|
||||||
->findAll();
|
|
||||||
|
|
||||||
$parentIds = [];
|
|
||||||
foreach ($enrollments as $row) {
|
|
||||||
$studentId = (int) ($row['student_id'] ?? 0);
|
|
||||||
$parentId = (int) ($row['parent_id'] ?? 0);
|
|
||||||
if ($studentId <= 0 || $parentId <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$exists = $this->eventChargesModel
|
|
||||||
->where('event_id', $eventId)
|
|
||||||
->where('student_id', $studentId)
|
|
||||||
->where('school_year', $schoolYear)
|
|
||||||
->where('semester', $semester)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($exists) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->eventChargesModel->insert([
|
|
||||||
'event_id' => $eventId,
|
|
||||||
'parent_id' => $parentId,
|
|
||||||
'student_id' => $studentId,
|
|
||||||
'participation' => 'yes',
|
|
||||||
'charged' => $amount,
|
|
||||||
'school_year' => $schoolYear,
|
|
||||||
'semester' => $semester,
|
|
||||||
'updated_by' => $userId ?: null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$parentIds[] = $parentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
$parentIds = array_unique($parentIds);
|
|
||||||
foreach ($parentIds as $pid) {
|
|
||||||
$this->invoiceController->generateInvoice((string) $pid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->to('/administrator/events')->with('success', 'Event created successfully');
|
return redirect()->to('/administrator/events')->with('success', 'Event created successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,17 +188,24 @@ class EventController extends ResourceController
|
|||||||
|
|
||||||
$parents = $this->userModel->getParents();
|
$parents = $this->userModel->getParents();
|
||||||
$events = $this->eventModel->getActiveEvents($this->schoolYear);
|
$events = $this->eventModel->getActiveEvents($this->schoolYear);
|
||||||
|
$filterEventId = (int) ($this->request->getGet('event_id') ?? 0);
|
||||||
|
|
||||||
$charges = $this->eventChargesModel
|
$chargesBuilder = $this->eventChargesModel
|
||||||
->select('event_charges.*,
|
->select('event_charges.*,
|
||||||
users.firstname AS parent_firstname, users.lastname AS parent_lastname,
|
users.firstname AS parent_firstname, users.lastname AS parent_lastname,
|
||||||
students.firstname AS student_firstname, students.lastname AS student_lastname,
|
students.firstname AS student_firstname, students.lastname AS student_lastname,
|
||||||
events.event_name')
|
events.event_name, events.description AS event_description, events.amount AS event_amount')
|
||||||
->join('users', 'users.id = event_charges.parent_id', 'left')
|
->join('users', 'users.id = event_charges.parent_id', 'left')
|
||||||
->join('students', 'students.id = event_charges.student_id', 'left')
|
->join('students', 'students.id = event_charges.student_id', 'left')
|
||||||
->join('events', 'events.id = event_charges.event_id', 'left')
|
->join('events', 'events.id = event_charges.event_id', 'left')
|
||||||
->where('event_charges.school_year', $schoolYear)
|
->where('event_charges.school_year', $schoolYear)
|
||||||
->where('event_charges.semester', $semester)
|
->where('event_charges.semester', $semester);
|
||||||
|
|
||||||
|
if ($filterEventId > 0) {
|
||||||
|
$chargesBuilder->where('event_charges.event_id', $filterEventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$charges = $chargesBuilder
|
||||||
->orderBy('event_charges.created_at', 'DESC')
|
->orderBy('event_charges.created_at', 'DESC')
|
||||||
->findAll();
|
->findAll();
|
||||||
|
|
||||||
@@ -260,6 +215,7 @@ class EventController extends ResourceController
|
|||||||
'events' => $events,
|
'events' => $events,
|
||||||
'school_year' => $schoolYear,
|
'school_year' => $schoolYear,
|
||||||
'semester' => $semester,
|
'semester' => $semester,
|
||||||
|
'filterEventId' => $filterEventId,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -271,8 +271,8 @@ class FilesController extends Controller
|
|||||||
|
|
||||||
private function buildDraftDownloadName(string $filename, string $subdir): string
|
private function buildDraftDownloadName(string $filename, string $subdir): string
|
||||||
{
|
{
|
||||||
$column = $subdir === 'finals' ? 'final_file' : 'teacher_file';
|
|
||||||
$db = Database::connect();
|
$db = Database::connect();
|
||||||
|
$column = $subdir === 'finals' ? 'final_file' : $this->resolveExamDraftFileColumn($db);
|
||||||
$row = $db->table('exam_drafts ed')
|
$row = $db->table('exam_drafts ed')
|
||||||
->select('ed.version, ed.exam_type, ed.class_section_id, cs.class_section_name')
|
->select('ed.version, ed.exam_type, ed.class_section_id, cs.class_section_name')
|
||||||
->join('classSection cs', 'cs.class_section_id = ed.class_section_id', 'left')
|
->join('classSection cs', 'cs.class_section_id = ed.class_section_id', 'left')
|
||||||
@@ -301,4 +301,20 @@ class FilesController extends Controller
|
|||||||
$value = trim($value, '_');
|
$value = trim($value, '_');
|
||||||
return $value === '' ? 'Exam' : mb_strtolower($value);
|
return $value === '' ? 'Exam' : mb_strtolower($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveExamDraftFileColumn($db): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$fields = $db->getFieldNames('exam_drafts');
|
||||||
|
if (in_array('teacher_file', $fields, true)) {
|
||||||
|
return 'teacher_file';
|
||||||
|
}
|
||||||
|
if (in_array('author_file', $fields, true)) {
|
||||||
|
return 'author_file';
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
log_message('error', 'FilesController::resolveExamDraftFileColumn error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
return 'teacher_file';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,25 +216,28 @@ public function financialReport()
|
|||||||
$schoolYears[] = (string)$schoolYear;
|
$schoolYears[] = (string)$schoolYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$eventFeesTotal = $this->getEventFeesTotal($schoolYear, $dateFrom, $dateTo);
|
||||||
|
|
||||||
// JSON API support
|
// JSON API support
|
||||||
if ($this->wantsJson() || strtolower((string)($this->request->getGet('format') ?? '')) === 'json') {
|
if ($this->wantsJson() || strtolower((string)($this->request->getGet('format') ?? '')) === 'json') {
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'selectedYear' => $schoolYear,
|
'selectedYear' => $schoolYear,
|
||||||
'dateFrom' => $dateFrom,
|
'dateFrom' => $dateFrom,
|
||||||
'dateTo' => $dateTo,
|
'dateTo' => $dateTo,
|
||||||
'schoolYears' => $schoolYears,
|
'schoolYears' => $schoolYears,
|
||||||
'invoices' => $invoices,
|
'invoices' => $invoices,
|
||||||
'payments' => $payments,
|
'payments' => $payments,
|
||||||
'paymentBreakdown' => $paymentBreakdown,
|
'paymentBreakdown' => $paymentBreakdown,
|
||||||
'paymentTotals' => $paymentTotals,
|
'paymentTotals' => $paymentTotals,
|
||||||
'refunds' => $refunds,
|
'refunds' => $refunds,
|
||||||
'expenses' => $expenses,
|
'expenses' => $expenses,
|
||||||
'reimbursements' => $reimbursements,
|
'reimbursements' => $reimbursements,
|
||||||
'discounts' => $discounts,
|
'discounts' => $discounts,
|
||||||
'csrf_token' => csrf_token(),
|
'eventFeesTotal' => $eventFeesTotal,
|
||||||
'csrf_hash' => csrf_hash(),
|
'csrf_token' => csrf_token(),
|
||||||
]);
|
'csrf_hash' => csrf_hash(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('payment/financial_report', [
|
return view('payment/financial_report', [
|
||||||
@@ -246,6 +249,7 @@ public function financialReport()
|
|||||||
'expenses' => $expenses,
|
'expenses' => $expenses,
|
||||||
'reimbursements' => $reimbursements,
|
'reimbursements' => $reimbursements,
|
||||||
'discounts' => $discounts,
|
'discounts' => $discounts,
|
||||||
|
'eventFeesTotal' => $eventFeesTotal,
|
||||||
'selectedYear' => $schoolYear,
|
'selectedYear' => $schoolYear,
|
||||||
'schoolYears' => $schoolYears,
|
'schoolYears' => $schoolYears,
|
||||||
'dateFrom' => $dateFrom,
|
'dateFrom' => $dateFrom,
|
||||||
@@ -1058,6 +1062,7 @@ public function financialReport()
|
|||||||
$amountCollected = $totalPaid;
|
$amountCollected = $totalPaid;
|
||||||
$netAmount = ($totalCharges - $totalDiscounts - $totalRefunds);
|
$netAmount = ($totalCharges - $totalDiscounts - $totalRefunds);
|
||||||
|
|
||||||
|
$totalEventFees = $this->getEventFeesTotal($schoolYear, $invoiceDateFrom, $invoiceDateTo);
|
||||||
return [
|
return [
|
||||||
'schoolYear' => $schoolYear,
|
'schoolYear' => $schoolYear,
|
||||||
'dateFrom' => $dateFrom,
|
'dateFrom' => $dateFrom,
|
||||||
@@ -1073,9 +1078,28 @@ public function financialReport()
|
|||||||
'amountCollected' => $amountCollected,
|
'amountCollected' => $amountCollected,
|
||||||
'totalUnpaid' => $totalUnpaid,
|
'totalUnpaid' => $totalUnpaid,
|
||||||
'netAmount' => $netAmount,
|
'netAmount' => $netAmount,
|
||||||
|
'totalEventFees' => $totalEventFees,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getEventFeesTotal(?string $schoolYear, ?string $dateFrom, ?string $dateTo): float
|
||||||
|
{
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$builder = $db->table('event_charges ec')
|
||||||
|
->select('COALESCE(SUM(ec.charged),0) AS amount', false);
|
||||||
|
if (!empty($schoolYear)) {
|
||||||
|
$builder->where('ec.school_year', $schoolYear);
|
||||||
|
}
|
||||||
|
if (!empty($dateFrom)) {
|
||||||
|
$builder->where('DATE(ec.created_at) >=', $dateFrom);
|
||||||
|
}
|
||||||
|
if (!empty($dateTo)) {
|
||||||
|
$builder->where('DATE(ec.created_at) <=', $dateTo);
|
||||||
|
}
|
||||||
|
$row = $builder->get()->getRowArray();
|
||||||
|
return $row ? (float)($row['amount'] ?? 0) : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Management page: list parents with outstanding balances (> 0) for a school year.
|
* Management page: list parents with outstanding balances (> 0) for a school year.
|
||||||
@@ -1226,6 +1250,23 @@ public function financialReport()
|
|||||||
$byParent[$pid]['total_balance'] += $extra;
|
$byParent[$pid]['total_balance'] += $extra;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$eventFeesPerParent = [];
|
||||||
|
try {
|
||||||
|
$eventFeesRows = $db->table('event_charges ec')
|
||||||
|
->select('ec.parent_id, COALESCE(SUM(ec.charged),0) AS event_fees', false)
|
||||||
|
->where('ec.school_year', $schoolYear)
|
||||||
|
->groupBy('ec.parent_id')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
foreach ($eventFeesRows as $row) {
|
||||||
|
$pid = (int)($row['parent_id'] ?? 0);
|
||||||
|
if ($pid <= 0) continue;
|
||||||
|
$eventFeesPerParent[$pid] = (float)($row['event_fees'] ?? 0);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// ignore, fallback to no event fees data
|
||||||
|
}
|
||||||
|
|
||||||
// Reduce into rows list; only parents with positive balance
|
// Reduce into rows list; only parents with positive balance
|
||||||
// Also compute remaining installments and suggested monthly amount
|
// Also compute remaining installments and suggested monthly amount
|
||||||
$rows = [];
|
$rows = [];
|
||||||
@@ -1283,10 +1324,11 @@ public function financialReport()
|
|||||||
$parentIds = array_values(array_unique(array_map(static fn($r) => (int)($r['parent_id'] ?? 0), $rows)));
|
$parentIds = array_values(array_unique(array_map(static fn($r) => (int)($r['parent_id'] ?? 0), $rows)));
|
||||||
$hasPayments = [];
|
$hasPayments = [];
|
||||||
$paidTotals = [];
|
$paidTotals = [];
|
||||||
|
$paymentCounts = [];
|
||||||
if (!empty($parentIds)) {
|
if (!empty($parentIds)) {
|
||||||
try {
|
try {
|
||||||
$pRows = $db->table('payments')
|
$pRows = $db->table('payments')
|
||||||
->select('parent_id, SUM(paid_amount) AS total_paid')
|
->select('parent_id, SUM(paid_amount) AS total_paid, COUNT(*) AS payment_count')
|
||||||
->whereIn('parent_id', $parentIds)
|
->whereIn('parent_id', $parentIds)
|
||||||
->where('school_year', $schoolYear)
|
->where('school_year', $schoolYear)
|
||||||
->groupBy('parent_id')
|
->groupBy('parent_id')
|
||||||
@@ -1296,6 +1338,7 @@ public function financialReport()
|
|||||||
if ($pid > 0) {
|
if ($pid > 0) {
|
||||||
$hasPayments[$pid] = true;
|
$hasPayments[$pid] = true;
|
||||||
$paidTotals[$pid] = (float)($pr['total_paid'] ?? 0);
|
$paidTotals[$pid] = (float)($pr['total_paid'] ?? 0);
|
||||||
|
$paymentCounts[$pid] = (int)($pr['payment_count'] ?? 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -1303,7 +1346,7 @@ public function financialReport()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$dataRows = array_map(function(array $r) use ($hasPayments, $paidTotals, $nextInstallmentYmd) {
|
$dataRows = array_map(function(array $r) use ($hasPayments, $paidTotals, $paymentCounts, $nextInstallmentYmd, $eventFeesPerParent) {
|
||||||
$pid = (int)($r['parent_id'] ?? 0);
|
$pid = (int)($r['parent_id'] ?? 0);
|
||||||
$name = trim((string)($r['firstname'] ?? '') . ' ' . (string)($r['lastname'] ?? ''));
|
$name = trim((string)($r['firstname'] ?? '') . ' ' . (string)($r['lastname'] ?? ''));
|
||||||
return [
|
return [
|
||||||
@@ -1317,8 +1360,10 @@ public function financialReport()
|
|||||||
'installment_amount' => (float)($r['installment_amount'] ?? 0),
|
'installment_amount' => (float)($r['installment_amount'] ?? 0),
|
||||||
'type' => isset($hasPayments[$pid]) ? 'installment' : 'no_payment',
|
'type' => isset($hasPayments[$pid]) ? 'installment' : 'no_payment',
|
||||||
'total_paid' => isset($paidTotals[$pid]) ? (float)$paidTotals[$pid] : 0.0,
|
'total_paid' => isset($paidTotals[$pid]) ? (float)$paidTotals[$pid] : 0.0,
|
||||||
|
'payment_count' => isset($paymentCounts[$pid]) ? (int)$paymentCounts[$pid] : 0,
|
||||||
'has_installment'=> isset($hasPayments[$pid]) ? 1 : 0,
|
'has_installment'=> isset($hasPayments[$pid]) ? 1 : 0,
|
||||||
'next_installment' => $nextInstallmentYmd,
|
'next_installment' => $nextInstallmentYmd,
|
||||||
|
'event_fees' => (float)($eventFeesPerParent[$pid] ?? 0),
|
||||||
];
|
];
|
||||||
}, $rows);
|
}, $rows);
|
||||||
|
|
||||||
|
|||||||
@@ -421,17 +421,45 @@ class FlagController extends Controller
|
|||||||
log_message('debug', 'Flag state: ' . $this->request->getPost('flag_state'));
|
log_message('debug', 'Flag state: ' . $this->request->getPost('flag_state'));
|
||||||
|
|
||||||
$currentFlagModel = new CurrentFlagModel();
|
$currentFlagModel = new CurrentFlagModel();
|
||||||
|
$userId = session()->get('user_id');
|
||||||
|
|
||||||
// Get the new flag state from the form
|
// Get the new flag state from the form
|
||||||
$newState = $this->request->getPost('flag_state');
|
$newState = $this->request->getPost('flag_state');
|
||||||
|
$stateDescription = (string) ($this->request->getPost('state_description') ?? '');
|
||||||
|
$actionTaken = (string) ($this->request->getPost('action_taken') ?? '');
|
||||||
|
|
||||||
if (!$newState) {
|
if (!$newState) {
|
||||||
session()->setFlashdata('error', 'incident state not provided.');
|
session()->setFlashdata('error', 'incident state not provided.');
|
||||||
return $this->index();
|
return $this->index();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$update = ['flag_state' => $newState];
|
||||||
|
if ($newState === 'Closed') {
|
||||||
|
$update['updated_by_closed'] = $userId;
|
||||||
|
if ($stateDescription !== '') {
|
||||||
|
$update['close_description'] = $stateDescription;
|
||||||
|
}
|
||||||
|
if ($actionTaken !== '') {
|
||||||
|
$update['action_taken'] = $actionTaken;
|
||||||
|
}
|
||||||
|
} elseif ($newState === 'Canceled') {
|
||||||
|
$update['updated_by_canceled'] = $userId;
|
||||||
|
if ($stateDescription !== '') {
|
||||||
|
$update['cancel_description'] = $stateDescription;
|
||||||
|
}
|
||||||
|
if ($actionTaken !== '') {
|
||||||
|
$update['action_taken'] = $actionTaken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update the flag state in the database
|
// Update the flag state in the database
|
||||||
if ($currentFlagModel->update($id, ['flag_state' => $newState])) {
|
if ($currentFlagModel->update($id, $update)) {
|
||||||
|
if ($newState === 'Closed' || $newState === 'Canceled') {
|
||||||
|
$flagData = $currentFlagModel->find($id);
|
||||||
|
if ($flagData) {
|
||||||
|
return $this->moveToHistory($flagData);
|
||||||
|
}
|
||||||
|
}
|
||||||
session()->setFlashdata('success', 'Incident state updated successfully!');
|
session()->setFlashdata('success', 'Incident state updated successfully!');
|
||||||
} else {
|
} else {
|
||||||
$errors = $currentFlagModel->errors();
|
$errors = $currentFlagModel->errors();
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use App\Models\PlacementLevelModel;
|
|||||||
use App\Models\PlacementBatchModel;
|
use App\Models\PlacementBatchModel;
|
||||||
use App\Models\PlacementScoreModel;
|
use App\Models\PlacementScoreModel;
|
||||||
use App\Models\GradingLockModel;
|
use App\Models\GradingLockModel;
|
||||||
|
use App\Services\NavbarService;
|
||||||
|
|
||||||
//use App\Models\ScoreModel;
|
//use App\Models\ScoreModel;
|
||||||
|
|
||||||
@@ -277,7 +278,7 @@ class GradingController extends Controller
|
|||||||
$semEsc = $this->db->escape($semester);
|
$semEsc = $this->db->escape($semester);
|
||||||
$yrEsc = $this->db->escape($schoolYear);
|
$yrEsc = $this->db->escape($schoolYear);
|
||||||
|
|
||||||
$rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear);
|
$rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear, $semester);
|
||||||
|
|
||||||
// Preload quiz/homework/project/participation/midterm score counts to distinguish true zeros from empty scores
|
// Preload quiz/homework/project/participation/midterm score counts to distinguish true zeros from empty scores
|
||||||
$quizCounts = [];
|
$quizCounts = [];
|
||||||
@@ -423,7 +424,7 @@ class GradingController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reload rows after refresh
|
// Reload rows after refresh
|
||||||
$rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear);
|
$rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear, $semester);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build structures keyed by BUSINESS section id
|
// Build structures keyed by BUSINESS section id
|
||||||
@@ -1079,11 +1080,14 @@ class GradingController extends Controller
|
|||||||
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
||||||
$rows = $this->fetchBelowSixtyRows($schoolYear, $semester);
|
$rows = $this->fetchBelowSixtyRows($schoolYear, $semester);
|
||||||
|
|
||||||
|
$canViewGrading = $this->userHasMenuUrl('grading');
|
||||||
|
|
||||||
return view('grading/below_sixty', [
|
return view('grading/below_sixty', [
|
||||||
'rows' => $rows,
|
'rows' => $rows,
|
||||||
'semester' => $semester,
|
'semester' => $semester,
|
||||||
'schoolYear' => $schoolYear,
|
'schoolYear' => $schoolYear,
|
||||||
'schoolYears' => $schoolYears,
|
'schoolYears' => $schoolYears,
|
||||||
|
'canViewGrading' => $canViewGrading,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1212,6 +1216,14 @@ class GradingController extends Controller
|
|||||||
|
|
||||||
$flagModel = new CurrentFlagModel();
|
$flagModel = new CurrentFlagModel();
|
||||||
$semKey = strtolower(trim($semester));
|
$semKey = strtolower(trim($semester));
|
||||||
|
$redirectUrl = base_url('grading/below-60');
|
||||||
|
$query = http_build_query([
|
||||||
|
'semester' => $semester,
|
||||||
|
'school_year' => $schoolYear,
|
||||||
|
]);
|
||||||
|
if ($query !== '') {
|
||||||
|
$redirectUrl .= '?' . $query;
|
||||||
|
}
|
||||||
|
|
||||||
$existing = $flagModel
|
$existing = $flagModel
|
||||||
->where('student_id', $studentId)
|
->where('student_id', $studentId)
|
||||||
@@ -1222,11 +1234,14 @@ class GradingController extends Controller
|
|||||||
|
|
||||||
$userId = (int)(session()->get('user_id') ?? 0) ?: null;
|
$userId = (int)(session()->get('user_id') ?? 0) ?: null;
|
||||||
$now = utc_now();
|
$now = utc_now();
|
||||||
|
$ok = true;
|
||||||
|
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
$data = [
|
$data = [
|
||||||
'flag_state' => $status,
|
'flag_state' => $status,
|
||||||
'flag_datetime' => $now,
|
'flag_datetime' => $now,
|
||||||
|
'semester' => $semester,
|
||||||
|
'school_year' => $schoolYear,
|
||||||
'updated_at' => $now,
|
'updated_at' => $now,
|
||||||
];
|
];
|
||||||
if ($status === 'Open') {
|
if ($status === 'Open') {
|
||||||
@@ -1242,7 +1257,7 @@ class GradingController extends Controller
|
|||||||
$data['close_description'] = trim($prev . PHP_EOL . $note);
|
$data['close_description'] = trim($prev . PHP_EOL . $note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$flagModel->update((int)$existing['id'], $data);
|
$ok = (bool) $flagModel->update((int)$existing['id'], $data);
|
||||||
} else {
|
} else {
|
||||||
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
|
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
|
||||||
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
|
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
|
||||||
@@ -1265,10 +1280,21 @@ class GradingController extends Controller
|
|||||||
$data['updated_by_closed'] = $userId;
|
$data['updated_by_closed'] = $userId;
|
||||||
if ($note !== '') $data['close_description'] = $note;
|
if ($note !== '') $data['close_description'] = $note;
|
||||||
}
|
}
|
||||||
$flagModel->insert($data);
|
$ok = (bool) $flagModel->insert($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->back()->with('status', 'Status updated.');
|
if (!$ok) {
|
||||||
|
log_message('error', 'updateBelowSixtyStatus failed', [
|
||||||
|
'student_id' => $studentId,
|
||||||
|
'semester' => $semester,
|
||||||
|
'school_year' => $schoolYear,
|
||||||
|
'status' => $status,
|
||||||
|
'errors' => $flagModel->errors(),
|
||||||
|
]);
|
||||||
|
return redirect()->to($redirectUrl)->with('error', 'Failed to update status.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to($redirectUrl)->with('status', 'Status updated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scheduleBelowSixty()
|
public function scheduleBelowSixty()
|
||||||
@@ -1551,7 +1577,7 @@ class GradingController extends Controller
|
|||||||
* @param string $schoolYear Raw school year value for filtering student_class
|
* @param string $schoolYear Raw school year value for filtering student_class
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function buildGradingRows(string $semEsc, string $yrEsc, string $schoolYear): array
|
private function buildGradingRows(string $semEsc, string $yrEsc, string $schoolYear, string $semesterRaw): array
|
||||||
{
|
{
|
||||||
$builder = $this->db->table('student_class sc')
|
$builder = $this->db->table('student_class sc')
|
||||||
->select([
|
->select([
|
||||||
@@ -1579,6 +1605,7 @@ class GradingController extends Controller
|
|||||||
'ss_b.class_section_id AS matched_biz_csid',
|
'ss_b.class_section_id AS matched_biz_csid',
|
||||||
'ss_p.class_section_id AS matched_pk_csid'
|
'ss_p.class_section_id AS matched_pk_csid'
|
||||||
])
|
])
|
||||||
|
->distinct()
|
||||||
->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left')
|
->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left')
|
||||||
->join('students s', 's.id = sc.student_id', 'inner')
|
->join('students s', 's.id = sc.student_id', 'inner')
|
||||||
->join(
|
->join(
|
||||||
@@ -1793,9 +1820,10 @@ class GradingController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$statusMap = [];
|
$statusMap = [];
|
||||||
|
$noteMap = [];
|
||||||
if (!empty($studentIds)) {
|
if (!empty($studentIds)) {
|
||||||
$flagRows = $this->db->table('current_flag')
|
$flagRows = $this->db->table('current_flag')
|
||||||
->select('student_id, flag_state')
|
->select('student_id, flag_state, open_description, close_description')
|
||||||
->where('flag', 'grade')
|
->where('flag', 'grade')
|
||||||
->where('school_year', $schoolYear)
|
->where('school_year', $schoolYear)
|
||||||
->where("LOWER(TRIM(semester))", $semesterKey)
|
->where("LOWER(TRIM(semester))", $semesterKey)
|
||||||
@@ -1806,6 +1834,12 @@ class GradingController extends Controller
|
|||||||
$sid = (int)($row['student_id'] ?? 0);
|
$sid = (int)($row['student_id'] ?? 0);
|
||||||
if ($sid <= 0) continue;
|
if ($sid <= 0) continue;
|
||||||
$statusMap[$sid] = (string)($row['flag_state'] ?? '');
|
$statusMap[$sid] = (string)($row['flag_state'] ?? '');
|
||||||
|
$openNote = trim((string)($row['open_description'] ?? ''));
|
||||||
|
$closeNote = trim((string)($row['close_description'] ?? ''));
|
||||||
|
$noteMap[$sid] = [
|
||||||
|
'open' => $openNote,
|
||||||
|
'closed' => $closeNote,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1814,6 +1848,15 @@ class GradingController extends Controller
|
|||||||
$row['comment'] = $commentMap[$sid] ?? '';
|
$row['comment'] = $commentMap[$sid] ?? '';
|
||||||
$flagState = strtolower(trim((string)($statusMap[$sid] ?? '')));
|
$flagState = strtolower(trim((string)($statusMap[$sid] ?? '')));
|
||||||
$row['status'] = ($flagState === 'closed' || $flagState === 'canceled') ? 'Closed' : 'Open';
|
$row['status'] = ($flagState === 'closed' || $flagState === 'canceled') ? 'Closed' : 'Open';
|
||||||
|
$noteBag = $noteMap[$sid] ?? ['open' => '', 'closed' => ''];
|
||||||
|
$rawNote = $row['status'] === 'Closed' ? (string)$noteBag['closed'] : (string)$noteBag['open'];
|
||||||
|
if ($rawNote !== '') {
|
||||||
|
$lines = preg_split('/\R/', $rawNote);
|
||||||
|
$lines = array_values(array_filter(array_map('trim', $lines), static fn($val) => $val !== ''));
|
||||||
|
$row['note'] = $lines ? end($lines) : '';
|
||||||
|
} else {
|
||||||
|
$row['note'] = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
unset($row);
|
unset($row);
|
||||||
|
|
||||||
@@ -1863,6 +1906,54 @@ class GradingController extends Controller
|
|||||||
return $row;
|
return $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function userHasMenuUrl(string $needle): bool
|
||||||
|
{
|
||||||
|
$needle = strtolower(trim($needle));
|
||||||
|
if ($needle === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawRole = session()->get('role');
|
||||||
|
$roles = is_array($rawRole) ? $rawRole : [$rawRole ?? 'guest'];
|
||||||
|
$roles = array_values(array_filter(array_map('strval', $roles)));
|
||||||
|
if (empty($roles)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = new NavbarService();
|
||||||
|
$menu = $service->getMenuForRoles($roles);
|
||||||
|
if (empty($menu)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalize = static function (string $url) use ($needle): string {
|
||||||
|
$url = strtolower(trim($url));
|
||||||
|
if ($url === '') return '';
|
||||||
|
$url = preg_replace('#^https?://[^/]+/#i', '', $url);
|
||||||
|
$url = ltrim($url, '/');
|
||||||
|
return $url;
|
||||||
|
};
|
||||||
|
|
||||||
|
$target = $normalize($needle);
|
||||||
|
$stack = $menu;
|
||||||
|
while (!empty($stack)) {
|
||||||
|
$node = array_shift($stack);
|
||||||
|
if (!empty($node['url'])) {
|
||||||
|
$url = $normalize((string)$node['url']);
|
||||||
|
if ($url !== '' && $url === $target) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($node['children']) && is_array($node['children'])) {
|
||||||
|
foreach ($node['children'] as $child) {
|
||||||
|
$stack[] = $child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private function fetchBelowSixtyParentName(int $studentId): string
|
private function fetchBelowSixtyParentName(int $studentId): string
|
||||||
{
|
{
|
||||||
$parentName = 'Parent/Guardian';
|
$parentName = 'Parent/Guardian';
|
||||||
|
|||||||
@@ -482,6 +482,7 @@ class HomeworkController extends Controller
|
|||||||
// Step 1: Get student IDs from student_class table
|
// Step 1: Get student IDs from student_class table
|
||||||
$studentClassRows = $this->studentClassModel
|
$studentClassRows = $this->studentClassModel
|
||||||
->select('student_id')
|
->select('student_id')
|
||||||
|
->distinct()
|
||||||
->where('class_section_id', $classSectionId)
|
->where('class_section_id', $classSectionId)
|
||||||
->where('school_year', $schoolYear)
|
->where('school_year', $schoolYear)
|
||||||
->findAll();
|
->findAll();
|
||||||
@@ -534,10 +535,7 @@ class HomeworkController extends Controller
|
|||||||
private function getStudentsWithHomeworkScores($classSectionId, $homeworkHeaders, $semester, $schoolYear)
|
private function getStudentsWithHomeworkScores($classSectionId, $homeworkHeaders, $semester, $schoolYear)
|
||||||
{
|
{
|
||||||
$semVariants = $this->getSemesterVariants($semester);
|
$semVariants = $this->getSemesterVariants($semester);
|
||||||
$studentClasses = $this->studentClassModel
|
$studentClasses = $this->studentClassModel->getClassStudents($classSectionId, $schoolYear, null);
|
||||||
->active()
|
|
||||||
->where('student_class.class_section_id', $classSectionId)
|
|
||||||
->findAll();
|
|
||||||
$students = [];
|
$students = [];
|
||||||
|
|
||||||
foreach ($studentClasses as $sc) {
|
foreach ($studentClasses as $sc) {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class HomeworkTrackingController extends BaseController
|
|||||||
|
|
||||||
$hasHomework = [];
|
$hasHomework = [];
|
||||||
$hwEnteredAt = [];
|
$hwEnteredAt = [];
|
||||||
|
$homeworkSubmissionCounts = [];
|
||||||
foreach ($rows as $r) {
|
foreach ($rows as $r) {
|
||||||
$csid = (int)($r['class_section_id'] ?? 0);
|
$csid = (int)($r['class_section_id'] ?? 0);
|
||||||
$hi = (int)($r['homework_index'] ?? 0);
|
$hi = (int)($r['homework_index'] ?? 0);
|
||||||
@@ -94,6 +95,7 @@ class HomeworkTrackingController extends BaseController
|
|||||||
$hasHomework[$csid][$hi] = true;
|
$hasHomework[$csid][$hi] = true;
|
||||||
$dateStr = substr((string)($r['first_created'] ?? ''), 0, 10);
|
$dateStr = substr((string)($r['first_created'] ?? ''), 0, 10);
|
||||||
$hwEnteredAt[$csid][$hi] = $dateStr ?: null;
|
$hwEnteredAt[$csid][$hi] = $dateStr ?: null;
|
||||||
|
$homeworkSubmissionCounts[$csid] = ($homeworkSubmissionCounts[$csid] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +219,7 @@ class HomeworkTrackingController extends BaseController
|
|||||||
'teachers' => $teachersPage,
|
'teachers' => $teachersPage,
|
||||||
'hasHomework' => $hasHomework,
|
'hasHomework' => $hasHomework,
|
||||||
'hwEnteredAt' => $hwEnteredAt,
|
'hwEnteredAt' => $hwEnteredAt,
|
||||||
|
'homeworkSubmissionCounts' => $homeworkSubmissionCounts,
|
||||||
'hasHomeworkByDate' => $hasHomeworkByDate,
|
'hasHomeworkByDate' => $hasHomeworkByDate,
|
||||||
'hwEnteredAtByDate' => $hwEnteredAtByDate,
|
'hwEnteredAtByDate' => $hwEnteredAtByDate,
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ class LandingPageController extends BaseController
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Teacher class sections only apply to the teacher role; other roles have no teacher_class rows.
|
||||||
|
if (strtolower(trim((string) $this->getUserRole())) !== 'teacher') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Get all class assignments for this teacher in the current term
|
// Get all class assignments for this teacher in the current term
|
||||||
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId(
|
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId(
|
||||||
(int)$user_id,
|
(int)$user_id,
|
||||||
@@ -106,7 +111,7 @@ class LandingPageController extends BaseController
|
|||||||
$ids = array_values(array_filter(array_unique($ids)));
|
$ids = array_values(array_filter(array_unique($ids)));
|
||||||
|
|
||||||
if (empty($ids)) {
|
if (empty($ids)) {
|
||||||
log_message('error', "No class section found for user ID: $user_id");
|
log_message('warning', "No class section found for user ID: $user_id");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,8 +873,12 @@ class LandingPageController extends BaseController
|
|||||||
|
|
||||||
protected function getUserRole()
|
protected function getUserRole()
|
||||||
{
|
{
|
||||||
// Assuming you have a session variable storing the user role
|
$active = session()->get('active_role');
|
||||||
return session()->get('user_role', 'guest'); // Default to guest if not set
|
if ($active !== null && $active !== '') {
|
||||||
|
return (string) $active;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) (session()->get('role') ?? 'guest');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getUserRoleFromDatabase($user_id)
|
protected function getUserRoleFromDatabase($user_id)
|
||||||
|
|||||||
@@ -717,6 +717,7 @@ class ParentController extends BaseController
|
|||||||
|
|
||||||
// Step 1: Generate a secure token for email verification
|
// Step 1: Generate a secure token for email verification
|
||||||
$token = bin2hex(random_bytes(48));
|
$token = bin2hex(random_bytes(48));
|
||||||
|
$tokenHash = hash('sha256', $token);
|
||||||
|
|
||||||
// Step 2: Determine user type based on relationship
|
// Step 2: Determine user type based on relationship
|
||||||
$userType = in_array(strtolower($relationToStudent), ['wife', 'husband']) ? 'Secondary' : 'Tertiary';
|
$userType = in_array(strtolower($relationToStudent), ['wife', 'husband']) ? 'Secondary' : 'Tertiary';
|
||||||
@@ -776,7 +777,7 @@ class ParentController extends BaseController
|
|||||||
'state' => strtoupper($userData['state']),
|
'state' => strtoupper($userData['state']),
|
||||||
'zip' => $userData['zip'],
|
'zip' => $userData['zip'],
|
||||||
'accept_school_policy' => $userData['accept_school_policy'] ?? 0,
|
'accept_school_policy' => $userData['accept_school_policy'] ?? 0,
|
||||||
'token' => $token,
|
'token' => $tokenHash,
|
||||||
'is_verified' => 0,
|
'is_verified' => 0,
|
||||||
'status' => 'Inactive',
|
'status' => 'Inactive',
|
||||||
'user_type' => $userType,
|
'user_type' => $userType,
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ class ProjectController extends Controller
|
|||||||
// Step 1: Get student IDs from student_class table
|
// Step 1: Get student IDs from student_class table
|
||||||
$studentClassRows = $studentClassModel
|
$studentClassRows = $studentClassModel
|
||||||
->select('student_id')
|
->select('student_id')
|
||||||
|
->distinct()
|
||||||
->where('class_section_id', $classSectionId)
|
->where('class_section_id', $classSectionId)
|
||||||
->where('school_year', $schoolYear)
|
->where('school_year', $schoolYear)
|
||||||
->findAll();
|
->findAll();
|
||||||
@@ -494,10 +495,7 @@ class ProjectController extends Controller
|
|||||||
$studentModel = new StudentModel();
|
$studentModel = new StudentModel();
|
||||||
$projectModel = new ProjectModel();
|
$projectModel = new ProjectModel();
|
||||||
|
|
||||||
$studentClasses = $studentClassModel
|
$studentClasses = $studentClassModel->getClassStudents($classSectionId, $this->schoolYear, null);
|
||||||
->active()
|
|
||||||
->where('student_class.class_section_id', $classSectionId)
|
|
||||||
->findAll();
|
|
||||||
$students = [];
|
$students = [];
|
||||||
|
|
||||||
foreach ($studentClasses as $sc) {
|
foreach ($studentClasses as $sc) {
|
||||||
|
|||||||
@@ -173,16 +173,8 @@ class RegisterController extends Controller
|
|||||||
$existingUser = $this->userModel->where('email', $post['email'])->first();
|
$existingUser = $this->userModel->where('email', $post['email'])->first();
|
||||||
|
|
||||||
if ($existingUser) {
|
if ($existingUser) {
|
||||||
// Step 2: Check if the user has a token (i.e., not verified yet)
|
return redirect()->back()->withInput()->with('error',
|
||||||
if (!empty($existingUser['token']) && $existingUser['is_verified'] == 0) {
|
'This email address is already registered. Please check your email or log in.');
|
||||||
// User exists and is unverified
|
|
||||||
return redirect()->back()->withInput()->with('error',
|
|
||||||
'This email address is already registered and is pending activation. Please check your email to activate your account.');
|
|
||||||
} else {
|
|
||||||
// User exists and is already active or has no token
|
|
||||||
return redirect()->back()->withInput()->with('error',
|
|
||||||
'The email address you entered is already in use. Please try a different one.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ───────────── 6. Determine role ───────────── */
|
/* ───────────── 6. Determine role ───────────── */
|
||||||
@@ -194,6 +186,7 @@ class RegisterController extends Controller
|
|||||||
|
|
||||||
/* ───────────── 7. Build & insert user ───────────── */
|
/* ───────────── 7. Build & insert user ───────────── */
|
||||||
$token = bin2hex(random_bytes(48));
|
$token = bin2hex(random_bytes(48));
|
||||||
|
$tokenHash = hash('sha256', $token);
|
||||||
$userData = [
|
$userData = [
|
||||||
'firstname' => $post['firstname'],
|
'firstname' => $post['firstname'],
|
||||||
'lastname' => $post['lastname'],
|
'lastname' => $post['lastname'],
|
||||||
@@ -205,7 +198,7 @@ class RegisterController extends Controller
|
|||||||
'city' => $post['city'],
|
'city' => $post['city'],
|
||||||
'state' => $post['state'],
|
'state' => $post['state'],
|
||||||
'zip' => $post['zip'],
|
'zip' => $post['zip'],
|
||||||
'token' => $token,
|
'token' => $tokenHash,
|
||||||
'is_verified'=> 0,
|
'is_verified'=> 0,
|
||||||
'accept_school_policy' => (int) $post['accept_school_policy'],
|
'accept_school_policy' => (int) $post['accept_school_policy'],
|
||||||
'status' => 'Inactive',
|
'status' => 'Inactive',
|
||||||
@@ -357,4 +350,4 @@ class RegisterController extends Controller
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1287,14 +1287,14 @@ class ScoreController extends Controller
|
|||||||
public function viewStudentScore()
|
public function viewStudentScore()
|
||||||
{
|
{
|
||||||
$parentId = session()->get('user_id');
|
$parentId = session()->get('user_id');
|
||||||
$userType = $_SESSION['user_type'];
|
$userType = session()->get('user_type') ?? '';
|
||||||
$firstParentId = null;
|
$firstParentId = null;
|
||||||
$releaseFall = $this->getParentScoresReleasedForSemester('Fall');
|
$releaseFall = $this->getParentScoresReleasedForSemester('Fall');
|
||||||
$releaseSpring = $this->getParentScoresReleasedForSemester('Spring');
|
$releaseSpring = $this->getParentScoresReleasedForSemester('Spring');
|
||||||
$releaseAny = $releaseFall || $releaseSpring;
|
$releaseAny = $releaseFall || $releaseSpring;
|
||||||
|
|
||||||
// Identify the firstparent based on user type
|
// Identify the firstparent based on user type
|
||||||
if ($userType === 'primary') {
|
if ($userType === 'primary' || $userType === '') {
|
||||||
$firstParentId = $parentId;
|
$firstParentId = $parentId;
|
||||||
} elseif ($userType === 'secondary') {
|
} elseif ($userType === 'secondary') {
|
||||||
$parentData = $this->db->table('parents')
|
$parentData = $this->db->table('parents')
|
||||||
|
|||||||
@@ -81,10 +81,10 @@ class ScorePredictor extends Controller
|
|||||||
s.school_id,
|
s.school_id,
|
||||||
s.firstname,
|
s.firstname,
|
||||||
s.lastname,
|
s.lastname,
|
||||||
fall.semester_score as fall_score,
|
MAX(fall.semester_score) as fall_score,
|
||||||
spring.semester_score as spring_score');
|
MAX(spring.semester_score) as spring_score');
|
||||||
// Also select class section for per-class trophy decision
|
// Reduce duplication from restored students while keeping a stable class section.
|
||||||
$builder->select('sc.class_section_id as class_section_id');
|
$builder->select('MAX(sc.class_section_id) as class_section_id');
|
||||||
$yearEsc = $this->db->escape($selectedYear);
|
$yearEsc = $this->db->escape($selectedYear);
|
||||||
$builder->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $yearEsc, 'left');
|
$builder->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $yearEsc, 'left');
|
||||||
$builder->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left');
|
$builder->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left');
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class SubjectCurriculumController extends BaseController
|
|||||||
->orderBy('classes.class_name', 'ASC')
|
->orderBy('classes.class_name', 'ASC')
|
||||||
->orderBy('subject', 'ASC')
|
->orderBy('subject', 'ASC')
|
||||||
->orderBy('unit_number', 'ASC')
|
->orderBy('unit_number', 'ASC')
|
||||||
|
->orderBy("CAST(SUBSTRING_INDEX(subject_curriculum_items.chapter_name, '.', 1) AS UNSIGNED)", 'ASC', false)
|
||||||
->orderBy('chapter_name', 'ASC')
|
->orderBy('chapter_name', 'ASC')
|
||||||
->get()
|
->get()
|
||||||
->getResultArray();
|
->getResultArray();
|
||||||
|
|||||||
@@ -297,12 +297,16 @@ class TeacherController extends BaseController
|
|||||||
$schoolYear = $forYear ?: ((string)($this->schoolYear ?? 'Not Set'));
|
$schoolYear = $forYear ?: ((string)($this->schoolYear ?? 'Not Set'));
|
||||||
|
|
||||||
$teachers = $this->teacherModel->getTeachersAndTAs();
|
$teachers = $this->teacherModel->getTeachersAndTAs();
|
||||||
// Prefer class sections for the selected year, fallback to all if none
|
// Prefer class sections for the selected year if the column exists, otherwise fallback to all.
|
||||||
$classSections = $classSectionModel
|
if ($this->db->fieldExists('school_year', 'classSection')) {
|
||||||
->where('school_year', $schoolYear)
|
$classSections = $classSectionModel
|
||||||
->orderBy('class_section_name', 'ASC')
|
->where('school_year', $schoolYear)
|
||||||
->findAll();
|
->orderBy('class_section_name', 'ASC')
|
||||||
if (empty($classSections)) {
|
->findAll();
|
||||||
|
if (empty($classSections)) {
|
||||||
|
$classSections = $classSectionModel->orderBy('class_section_name', 'ASC')->findAll();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
$classSections = $classSectionModel->orderBy('class_section_name', 'ASC')->findAll();
|
$classSections = $classSectionModel->orderBy('class_section_name', 'ASC')->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ require_once APPPATH . 'Helpers/pbkdf2_helper.php';
|
|||||||
|
|
||||||
class UserController extends BaseController
|
class UserController extends BaseController
|
||||||
{
|
{
|
||||||
|
private const ACTIVATION_TTL_HOURS = 48;
|
||||||
protected $userModel;
|
protected $userModel;
|
||||||
protected $roleModel;
|
protected $roleModel;
|
||||||
protected $userRoleModel;
|
protected $userRoleModel;
|
||||||
@@ -49,6 +50,37 @@ class UserController extends BaseController
|
|||||||
$this->resetRequestModel = new PasswordResetRequestModel();
|
$this->resetRequestModel = new PasswordResetRequestModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function denyAccess(string $message)
|
||||||
|
{
|
||||||
|
if ($this->request->isAJAX() || $this->request->getHeaderLine('Accept') === 'application/json') {
|
||||||
|
return service('response')
|
||||||
|
->setStatusCode(403)
|
||||||
|
->setJSON(['status' => 'error', 'message' => $message]);
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->setFlashdata('error', $message);
|
||||||
|
return redirect()->to('/access_denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function requirePermission(string $permission)
|
||||||
|
{
|
||||||
|
if (!session()->get('is_logged_in')) {
|
||||||
|
return redirect()->to('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int) session()->get('user_id');
|
||||||
|
if ($userId <= 0 || !has_permission($userId, $permission)) {
|
||||||
|
return $this->denyAccess("You don't have permission to use this feature.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hashToken(string $token): string
|
||||||
|
{
|
||||||
|
return hash('sha256', $token);
|
||||||
|
}
|
||||||
|
|
||||||
// Method to show the home page
|
// Method to show the home page
|
||||||
public function home()
|
public function home()
|
||||||
{
|
{
|
||||||
@@ -75,6 +107,10 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
public function userList()
|
public function userList()
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requirePermission('read_user')) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
helper('url');
|
helper('url');
|
||||||
|
|
||||||
return view('user/user_list', [
|
return view('user/user_list', [
|
||||||
@@ -85,6 +121,10 @@ class UserController extends BaseController
|
|||||||
// Method to show the list of users
|
// Method to show the list of users
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requirePermission('read_user')) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch users along with their assigned roles
|
// Fetch users along with their assigned roles
|
||||||
$builder = $this->db->table('users');
|
$builder = $this->db->table('users');
|
||||||
$builder->select('users.id, users.firstname, users.lastname, users.email, user_roles.role_id, roles.name as role, users.status, users.updated_at');
|
$builder->select('users.id, users.firstname, users.lastname, users.email, user_roles.role_id, roles.name as role, users.status, users.updated_at');
|
||||||
@@ -122,6 +162,10 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
public function userListData()
|
public function userListData()
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requirePermission('read_user')) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'users' => $this->buildUsersWithRoles(),
|
'users' => $this->buildUsersWithRoles(),
|
||||||
]);
|
]);
|
||||||
@@ -253,6 +297,10 @@ class UserController extends BaseController
|
|||||||
// Method to store a new user
|
// Method to store a new user
|
||||||
public function store()
|
public function store()
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requirePermission('edit_user')) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate input data
|
// Validate input data
|
||||||
$validation = \Config\Services::validation();
|
$validation = \Config\Services::validation();
|
||||||
$validation->setRules([
|
$validation->setRules([
|
||||||
@@ -315,6 +363,10 @@ class UserController extends BaseController
|
|||||||
// Method to show the form for editing an existing user
|
// Method to show the form for editing an existing user
|
||||||
public function edit($id)
|
public function edit($id)
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requirePermission('edit_user')) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
$data['user'] = $this->userModel->find($id);
|
$data['user'] = $this->userModel->find($id);
|
||||||
$data['roles'] = $this->roleModel->findAll();
|
$data['roles'] = $this->roleModel->findAll();
|
||||||
$userRoles = $this->userRoleModel->where('user_id', $id)->findAll();
|
$userRoles = $this->userRoleModel->where('user_id', $id)->findAll();
|
||||||
@@ -327,6 +379,10 @@ class UserController extends BaseController
|
|||||||
// Method to delete an existing user
|
// Method to delete an existing user
|
||||||
public function delete($id)
|
public function delete($id)
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requirePermission('edit_user')) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
$this->userModel->delete($id);
|
$this->userModel->delete($id);
|
||||||
|
|
||||||
// Delete the user's roles from the user_roles table
|
// Delete the user's roles from the user_roles table
|
||||||
@@ -369,27 +425,21 @@ class UserController extends BaseController
|
|||||||
$email = strtolower($this->request->getPost('email'));
|
$email = strtolower($this->request->getPost('email'));
|
||||||
$user = $this->userModel->where('email', $email)->first();
|
$user = $this->userModel->where('email', $email)->first();
|
||||||
|
|
||||||
// --- Handle unknown email ---
|
// --- Handle unknown or unverified email ---
|
||||||
if (!$user) {
|
if (!$user || (int) $user['is_verified'] === 0) {
|
||||||
session()->setFlashdata('error', 'If this email is registered, you will receive a reset link.');
|
session()->setFlashdata('success', 'If this email is registered, you will receive a reset link.');
|
||||||
log_message('info', "Password reset requested for non-existing user {$email}");
|
log_message('info', "Password reset requested for {$email} (user missing or unverified).");
|
||||||
return redirect()->back();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Handle unverified accounts ---
|
|
||||||
if ((int) $user['is_verified'] === 0) {
|
|
||||||
session()->setFlashdata('error', 'Please check your email and complete the account activation process before resetting your password.');
|
|
||||||
log_message('info', "Password reset blocked for unverified user {$email}");
|
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Verified user: continue with reset ---
|
// --- Verified user: continue with reset ---
|
||||||
$token = bin2hex(random_bytes(48));
|
$token = bin2hex(random_bytes(48));
|
||||||
|
$tokenHash = $this->hashToken($token);
|
||||||
$expires_at = Time::now()->addHours(1);
|
$expires_at = Time::now()->addHours(1);
|
||||||
|
|
||||||
$this->passwordResetModel->insert([
|
$this->passwordResetModel->insert([
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'token' => $token,
|
'token' => $tokenHash,
|
||||||
'created_at' => Time::now(),
|
'created_at' => Time::now(),
|
||||||
'expires_at' => $expires_at,
|
'expires_at' => $expires_at,
|
||||||
]);
|
]);
|
||||||
@@ -447,7 +497,12 @@ class UserController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// You may want to validate the token here
|
// You may want to validate the token here
|
||||||
$resetEntry = $this->passwordResetModel->where('token', $token)
|
$tokenHash = $this->hashToken($token);
|
||||||
|
$resetEntry = $this->passwordResetModel
|
||||||
|
->groupStart()
|
||||||
|
->where('token', $tokenHash)
|
||||||
|
->orWhere('token', $token)
|
||||||
|
->groupEnd()
|
||||||
->where('expires_at >=', Time::now())
|
->where('expires_at >=', Time::now())
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
@@ -462,6 +517,10 @@ class UserController extends BaseController
|
|||||||
//This function processes the new password submission, validating the token, updating the user's password, and cleaning up the reset entry.
|
//This function processes the new password submission, validating the token, updating the user's password, and cleaning up the reset entry.
|
||||||
public function processResetPassword()
|
public function processResetPassword()
|
||||||
{
|
{
|
||||||
|
if (strtolower($this->request->getMethod()) !== 'post') {
|
||||||
|
return redirect()->to('/')->with('error', 'Invalid request.');
|
||||||
|
}
|
||||||
|
|
||||||
$token = $this->request->getPost('token');
|
$token = $this->request->getPost('token');
|
||||||
$newPassword = $this->request->getPost('password');
|
$newPassword = $this->request->getPost('password');
|
||||||
$passConfirm = $this->request->getPost('pass_confirm');
|
$passConfirm = $this->request->getPost('pass_confirm');
|
||||||
@@ -490,7 +549,12 @@ class UserController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the password reset entry
|
// Find the password reset entry
|
||||||
$resetEntry = $this->passwordResetModel->where('token', $token)
|
$tokenHash = $this->hashToken($token);
|
||||||
|
$resetEntry = $this->passwordResetModel
|
||||||
|
->groupStart()
|
||||||
|
->where('token', $tokenHash)
|
||||||
|
->orWhere('token', $token)
|
||||||
|
->groupEnd()
|
||||||
->where('expires_at >=', Time::now())
|
->where('expires_at >=', Time::now())
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
@@ -519,7 +583,12 @@ class UserController extends BaseController
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Delete the used token from the password reset table
|
// Delete the used token from the password reset table
|
||||||
$this->passwordResetModel->where('token', $token)->delete();
|
$this->passwordResetModel
|
||||||
|
->groupStart()
|
||||||
|
->where('token', $tokenHash)
|
||||||
|
->orWhere('token', $token)
|
||||||
|
->groupEnd()
|
||||||
|
->delete();
|
||||||
|
|
||||||
// Retrieve the user's IP address from the request
|
// Retrieve the user's IP address from the request
|
||||||
$ipAddress = $this->request->getIPAddress();
|
$ipAddress = $this->request->getIPAddress();
|
||||||
@@ -546,10 +615,16 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
public function confirm($token)
|
public function confirm($token)
|
||||||
{
|
{
|
||||||
log_message('info', 'Processing email confirmation with token: ' . $token);
|
log_message('info', 'Processing email confirmation.');
|
||||||
|
|
||||||
|
$tokenHash = $this->hashToken($token);
|
||||||
$user = $this->userModel->where('token', $token)->first();
|
$user = $this->userModel
|
||||||
|
->groupStart()
|
||||||
|
->where('token', $tokenHash)
|
||||||
|
->orWhere('token', $token)
|
||||||
|
->groupEnd()
|
||||||
|
->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
|
||||||
|
->first();
|
||||||
|
|
||||||
if (!$user || $user['is_verified'] == 1) {
|
if (!$user || $user['is_verified'] == 1) {
|
||||||
return redirect()->to('/invalid_token');
|
return redirect()->to('/invalid_token');
|
||||||
@@ -570,7 +645,14 @@ class UserController extends BaseController
|
|||||||
{
|
{
|
||||||
//echo "Reached setPassword with token: " . esc($token);
|
//echo "Reached setPassword with token: " . esc($token);
|
||||||
//echo "Token received: " . $token;
|
//echo "Token received: " . $token;
|
||||||
$user = $this->userModel->where('token', $token)->first();
|
$tokenHash = $this->hashToken($token);
|
||||||
|
$user = $this->userModel
|
||||||
|
->groupStart()
|
||||||
|
->where('token', $tokenHash)
|
||||||
|
->orWhere('token', $token)
|
||||||
|
->groupEnd()
|
||||||
|
->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
|
||||||
|
->first();
|
||||||
|
|
||||||
if (!$user || $user['is_verified'] == 1) {
|
if (!$user || $user['is_verified'] == 1) {
|
||||||
return redirect()->to('/invalid_token');
|
return redirect()->to('/invalid_token');
|
||||||
@@ -584,6 +666,10 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
public function savePassword()
|
public function savePassword()
|
||||||
{
|
{
|
||||||
|
if (strtolower($this->request->getMethod()) !== 'post') {
|
||||||
|
return redirect()->to('/')->with('error', 'Invalid request.');
|
||||||
|
}
|
||||||
|
|
||||||
$validation = \Config\Services::validation();
|
$validation = \Config\Services::validation();
|
||||||
$validation->setRules([
|
$validation->setRules([
|
||||||
'password' => [
|
'password' => [
|
||||||
@@ -615,9 +701,17 @@ class UserController extends BaseController
|
|||||||
$token = $this->request->getPost('token');
|
$token = $this->request->getPost('token');
|
||||||
$password = $this->request->getPost('password');
|
$password = $this->request->getPost('password');
|
||||||
|
|
||||||
$user = $this->userModel->where('id', $userId)->where('token', $token)->first();
|
$tokenHash = $this->hashToken($token);
|
||||||
|
$user = $this->userModel
|
||||||
|
->where('id', $userId)
|
||||||
|
->groupStart()
|
||||||
|
->where('token', $tokenHash)
|
||||||
|
->orWhere('token', $token)
|
||||||
|
->groupEnd()
|
||||||
|
->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
|
||||||
|
->first();
|
||||||
|
|
||||||
log_message('debug', "Attempting to set password for user $userId with token $token");
|
log_message('debug', "Attempting to set password for user $userId");
|
||||||
|
|
||||||
if (!$user || $user['is_verified'] == 1) {
|
if (!$user || $user['is_verified'] == 1) {
|
||||||
return redirect()->to('/invalid_token');
|
return redirect()->to('/invalid_token');
|
||||||
@@ -670,20 +764,39 @@ class UserController extends BaseController
|
|||||||
$roleKey = (string) $this->request->getPost('role');
|
$roleKey = (string) $this->request->getPost('role');
|
||||||
log_message('info', 'Role selected: ' . $roleKey);
|
log_message('info', 'Role selected: ' . $roleKey);
|
||||||
|
|
||||||
$roleModel = new RoleModel();
|
|
||||||
$route = $roleModel->getRouteByNameOrSlug($roleKey);
|
|
||||||
|
|
||||||
if ($route === null) {
|
|
||||||
log_message('error', 'Invalid or inactive role selected: ' . $roleKey);
|
|
||||||
return redirect()->back()->with('error', 'Invalid role selected.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$userId = (int) session()->get('user_id');
|
$userId = (int) session()->get('user_id');
|
||||||
log_message('info', 'User ID: ' . $userId);
|
log_message('info', 'User ID: ' . $userId);
|
||||||
|
|
||||||
|
if ($userId <= 0) {
|
||||||
|
return $this->denyAccess("You don't have permission to use this feature.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleRow = $this->db->table('user_roles ur')
|
||||||
|
->join('roles r', 'r.id = ur.role_id', 'inner')
|
||||||
|
->select('r.name, r.slug, r.dashboard_route')
|
||||||
|
->where('ur.user_id', $userId)
|
||||||
|
->where('r.is_active', 1)
|
||||||
|
->groupStart()
|
||||||
|
->where('LOWER(r.name)', strtolower($roleKey))
|
||||||
|
->orWhere('LOWER(r.slug)', strtolower($roleKey))
|
||||||
|
->groupEnd()
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if (empty($roleRow)) {
|
||||||
|
log_message('error', 'Invalid or unassigned role selected: ' . $roleKey);
|
||||||
|
return redirect()->back()->with('error', 'Invalid role selected.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$route = $roleRow['dashboard_route'] ?? null;
|
||||||
|
if ($route === null) {
|
||||||
|
log_message('error', 'No dashboard route configured for role: ' . $roleKey);
|
||||||
|
return redirect()->back()->with('error', 'Invalid role selected.');
|
||||||
|
}
|
||||||
|
|
||||||
// Persist the *exact* role name or slug—choose your convention.
|
// Persist the *exact* role name or slug—choose your convention.
|
||||||
// If you want to store the canonical name, fetch the row and use $row['name'].
|
// Store the canonical name to avoid arbitrary role strings.
|
||||||
$this->userModel->update($userId, ['role' => $roleKey]);
|
$this->userModel->update($userId, ['role' => $roleRow['name']]);
|
||||||
log_message('info', 'Role updated in database.');
|
log_message('info', 'Role updated in database.');
|
||||||
|
|
||||||
log_message('info', 'Redirecting to role dashboard: ' . $route);
|
log_message('info', 'Redirecting to role dashboard: ' . $route);
|
||||||
@@ -702,6 +815,10 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
public function delete_role($roleId)
|
public function delete_role($roleId)
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requirePermission('edit_user')) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch the role to be deleted
|
// Fetch the role to be deleted
|
||||||
$role = $this->roleModel->find($roleId);
|
$role = $this->roleModel->find($roleId);
|
||||||
if (!$role) {
|
if (!$role) {
|
||||||
@@ -731,6 +848,10 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
public function loginActivity()
|
public function loginActivity()
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requirePermission('view_login_activity')) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
helper('url');
|
helper('url');
|
||||||
|
|
||||||
$perPage = (int) ($this->request->getGet('per_page') ?? 25);
|
$perPage = (int) ($this->request->getGet('per_page') ?? 25);
|
||||||
@@ -743,6 +864,10 @@ class UserController extends BaseController
|
|||||||
|
|
||||||
public function loginActivityData()
|
public function loginActivityData()
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requirePermission('view_login_activity')) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
$perPage = (int) ($this->request->getGet('per_page') ?? 25);
|
$perPage = (int) ($this->request->getGet('per_page') ?? 25);
|
||||||
$page = (int) ($this->request->getGet('page') ?? 1);
|
$page = (int) ($this->request->getGet('page') ?? 1);
|
||||||
|
|
||||||
@@ -752,6 +877,10 @@ class UserController extends BaseController
|
|||||||
// Method to update an existing user
|
// Method to update an existing user
|
||||||
public function updateUser()
|
public function updateUser()
|
||||||
{
|
{
|
||||||
|
if ($resp = $this->requirePermission('edit_user')) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
if (strtolower($this->request->getMethod()) !== 'post') {
|
if (strtolower($this->request->getMethod()) !== 'post') {
|
||||||
return redirect()->to(site_url('user/user_list'))->with('error', 'Invalid request.');
|
return redirect()->to(site_url('user/user_list'))->with('error', 'Invalid request.');
|
||||||
}
|
}
|
||||||
@@ -800,9 +929,6 @@ class UserController extends BaseController
|
|||||||
'status' => trim((string)$this->request->getPost('status')),
|
'status' => trim((string)$this->request->getPost('status')),
|
||||||
'is_suspended' => $toBool('is_suspended'),
|
'is_suspended' => $toBool('is_suspended'),
|
||||||
'is_verified' => $toBool('is_verified'),
|
'is_verified' => $toBool('is_verified'),
|
||||||
'token' => trim((string)$this->request->getPost('token')),
|
|
||||||
'updated_at' => $toDT('updated_at'),
|
|
||||||
'created_at' => $toDT('created_at'),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class AddReviewRevisionToExamDrafts extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
if (! $this->db->fieldExists('review_revision', 'exam_drafts')) {
|
||||||
|
$this->forge->addColumn('exam_drafts', [
|
||||||
|
'review_revision' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 0,
|
||||||
|
'after' => 'version',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
if ($this->db->fieldExists('review_revision', 'exam_drafts')) {
|
||||||
|
$this->forge->dropColumn('exam_drafts', 'review_revision');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,15 @@ namespace App\Models;
|
|||||||
|
|
||||||
use CodeIgniter\Model;
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weekly class progress reports (one row per subject per week).
|
||||||
|
*
|
||||||
|
* unit_title stores a compact summary of unit/chapter rows: segments joined with " ; ".
|
||||||
|
* Teacher-entered custom Islamic or Quran topics use the prefix "Custom / {text}"
|
||||||
|
* (see {@see \App\Controllers\ClassProgressController::CUSTOM_UNIT_ROW_LABEL} and
|
||||||
|
* {@see \App\Controllers\ClassProgressController::splitUnitTitleForDisplay()}).
|
||||||
|
* DB column is VARCHAR(120); controller truncates to match.
|
||||||
|
*/
|
||||||
class ClassProgressReportModel extends Model
|
class ClassProgressReportModel extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'class_progress_reports';
|
protected $table = 'class_progress_reports';
|
||||||
@@ -27,7 +36,42 @@ class ClassProgressReportModel extends Model
|
|||||||
'flags_json',
|
'flags_json',
|
||||||
'attachment_path',
|
'attachment_path',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $useTimestamps = true;
|
protected $useTimestamps = true;
|
||||||
protected $createdField = 'created_at';
|
protected $createdField = 'created_at';
|
||||||
protected $updatedField = 'updated_at';
|
protected $updatedField = 'updated_at';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation is performed in {@see \App\Controllers\ClassProgressController} on HTTP input.
|
||||||
|
* Rules below document schema limits and can be enabled if you set {@see $skipValidation} to false.
|
||||||
|
*/
|
||||||
|
protected $skipValidation = true;
|
||||||
|
|
||||||
|
protected $validationRules = [
|
||||||
|
'class_section_id' => 'required|integer',
|
||||||
|
'teacher_id' => 'required|integer',
|
||||||
|
'week_start' => 'required|valid_date[Y-m-d]',
|
||||||
|
'week_end' => 'required|valid_date[Y-m-d]',
|
||||||
|
'subject' => 'required|string|max_length[160]',
|
||||||
|
'unit_title' => 'permit_empty|string|max_length[120]',
|
||||||
|
'covered' => 'permit_empty|string',
|
||||||
|
'homework' => 'permit_empty|string',
|
||||||
|
'assessment' => 'permit_empty|string',
|
||||||
|
'status' => 'permit_empty|in_list[on_track,slightly_behind,behind]',
|
||||||
|
'status_notes' => 'permit_empty|string|max_length[200]',
|
||||||
|
'class_notes' => 'permit_empty|string',
|
||||||
|
'next_week_plan' => 'permit_empty|string',
|
||||||
|
'support_needed' => 'permit_empty|string',
|
||||||
|
'flags_json' => 'permit_empty|string',
|
||||||
|
'attachment_path' => 'permit_empty|string|max_length[255]',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $validationMessages = [
|
||||||
|
'unit_title' => [
|
||||||
|
'max_length' => 'Unit and chapter summary cannot exceed 120 characters.',
|
||||||
|
],
|
||||||
|
'subject' => [
|
||||||
|
'max_length' => 'Subject cannot exceed 160 characters.',
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ class ConfigurationModel extends Model
|
|||||||
*/
|
*/
|
||||||
public function getConfigValueByKey(string $key)
|
public function getConfigValueByKey(string $key)
|
||||||
{
|
{
|
||||||
// Deterministic read in case historical duplicates exist
|
// Use a fresh builder to avoid stale state from shared model builder.
|
||||||
$result = $this->where('config_key', $key)
|
$builder = $this->db->table($this->table);
|
||||||
|
$result = $builder->where('config_key', $key)
|
||||||
->orderBy('id', 'DESC')
|
->orderBy('id', 'DESC')
|
||||||
->first();
|
->get(1)
|
||||||
return $result ? $result['config_value'] : null;
|
->getRowArray();
|
||||||
|
return $result['config_value'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class EventChargesModel extends Model
|
|||||||
|
|
||||||
public function getChargesWithEventInfo($parentId = null, $schoolYear = null, $semester = null)
|
public function getChargesWithEventInfo($parentId = null, $schoolYear = null, $semester = null)
|
||||||
{
|
{
|
||||||
$builder = $this->select('event_charges.*, events.event_name, events.amount')
|
$builder = $this->select('event_charges.*, events.event_name, events.amount AS event_amount, events.description AS event_description')
|
||||||
->join('events', 'events.id = event_charges.event_id', 'left');
|
->join('events', 'events.id = event_charges.event_id', 'left');
|
||||||
|
|
||||||
if ($parentId) {
|
if ($parentId) {
|
||||||
|
|||||||
@@ -8,19 +8,51 @@ class ExamDraftModel extends Model
|
|||||||
{
|
{
|
||||||
protected $table = 'exam_drafts';
|
protected $table = 'exam_drafts';
|
||||||
protected $primaryKey = 'id';
|
protected $primaryKey = 'id';
|
||||||
|
protected $returnType = 'array';
|
||||||
|
protected $useTimestamps = true;
|
||||||
|
protected $createdField = 'created_at';
|
||||||
|
protected $updatedField = 'updated_at';
|
||||||
|
protected bool $updateOnlyChanged = false;
|
||||||
|
|
||||||
|
/** @var list<string> Stored in `status` column */
|
||||||
|
public const STATUSES = [
|
||||||
|
'submitted',
|
||||||
|
'accepted',
|
||||||
|
'review needed',
|
||||||
|
'rejected',
|
||||||
|
'canceled',
|
||||||
|
'under review',
|
||||||
|
'legacy',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var list<string> Stored in `acceptance_type` when status is finalized */
|
||||||
|
public const ACCEPTANCE_TYPES = [
|
||||||
|
'as_is',
|
||||||
|
'minor_edits',
|
||||||
|
];
|
||||||
|
|
||||||
protected $allowedFields = [
|
protected $allowedFields = [
|
||||||
'teacher_id',
|
'teacher_id',
|
||||||
|
'author_id',
|
||||||
'class_section_id',
|
'class_section_id',
|
||||||
'semester',
|
'semester',
|
||||||
'school_year',
|
'school_year',
|
||||||
'exam_type',
|
'exam_type',
|
||||||
'draft_title',
|
'draft_title',
|
||||||
|
'author_comment',
|
||||||
'description',
|
'description',
|
||||||
'teacher_file',
|
'teacher_file',
|
||||||
'teacher_filename',
|
'teacher_filename',
|
||||||
|
'author_file',
|
||||||
|
'author_filename',
|
||||||
'status',
|
'status',
|
||||||
|
'acceptance_type',
|
||||||
|
'review_revision',
|
||||||
|
'reviewer_id',
|
||||||
'admin_id',
|
'admin_id',
|
||||||
'is_legacy',
|
'is_legacy',
|
||||||
|
'reviewer_comment',
|
||||||
|
'reviewer_comments',
|
||||||
'admin_comments',
|
'admin_comments',
|
||||||
'reviewed_at',
|
'reviewed_at',
|
||||||
'final_file',
|
'final_file',
|
||||||
@@ -29,9 +61,40 @@ class ExamDraftModel extends Model
|
|||||||
'version',
|
'version',
|
||||||
'previous_draft_id',
|
'previous_draft_id',
|
||||||
];
|
];
|
||||||
protected $returnType = 'array';
|
|
||||||
protected $useTimestamps = true;
|
/**
|
||||||
protected $createdField = 'created_at';
|
* Applied on insert/update when validation runs (e.g. $model->insert($data, true)).
|
||||||
protected $updatedField = 'updated_at';
|
* Uses if_exist so partial updates still validate only keys present in $data.
|
||||||
protected bool $updateOnlyChanged = false; // force updates even if CI thinks nothing changed
|
*/
|
||||||
|
protected $validationRules = [
|
||||||
|
'status' => 'if_exist|in_list[submitted,accepted,review needed,rejected,canceled,under review,legacy]',
|
||||||
|
'acceptance_type' => 'if_exist|permit_empty|in_list[as_is,minor_edits]',
|
||||||
|
'author_id' => 'if_exist|is_natural_no_zero',
|
||||||
|
'class_section_id' => 'if_exist|is_natural_no_zero',
|
||||||
|
'version' => 'if_exist|is_natural_no_zero',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $validationMessages = [
|
||||||
|
'status' => [
|
||||||
|
'in_list' => 'Invalid exam draft status.',
|
||||||
|
],
|
||||||
|
'acceptance_type' => [
|
||||||
|
'in_list' => 'Acceptance must be as_is or minor_edits.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
protected array $casts = [
|
||||||
|
'teacher_id' => 'int',
|
||||||
|
'author_id' => 'int',
|
||||||
|
'class_section_id' => 'int',
|
||||||
|
'reviewer_id' => '?int',
|
||||||
|
'admin_id' => '?int',
|
||||||
|
'review_revision' => 'int',
|
||||||
|
'version' => 'int',
|
||||||
|
'previous_draft_id' => '?int',
|
||||||
|
'is_legacy' => 'boolean',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class SubjectCurriculumModel extends Model
|
|||||||
return $this->where('class_id', $classId)
|
return $this->where('class_id', $classId)
|
||||||
->where('subject', $subject)
|
->where('subject', $subject)
|
||||||
->orderBy('unit_number', 'ASC')
|
->orderBy('unit_number', 'ASC')
|
||||||
|
->orderBy("CAST(SUBSTRING_INDEX(chapter_name, '.', 1) AS UNSIGNED)", 'ASC', false)
|
||||||
->orderBy('chapter_name', 'ASC')
|
->orderBy('chapter_name', 'ASC')
|
||||||
->findAll();
|
->findAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class TeacherClassModel extends Model
|
|||||||
$builder = $this->db->table('teacher_class tc')
|
$builder = $this->db->table('teacher_class tc')
|
||||||
->select([
|
->select([
|
||||||
'cs.class_section_name',
|
'cs.class_section_name',
|
||||||
'cs.id AS class_section_pk',
|
'cs.class_section_id AS class_section_pk',
|
||||||
'cs.class_section_id',
|
'cs.class_section_id',
|
||||||
'c.id AS class_id',
|
'c.id AS class_id',
|
||||||
'c.class_name',
|
'c.class_name',
|
||||||
@@ -181,10 +181,10 @@ class TeacherClassModel extends Model
|
|||||||
{
|
{
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
// Adjust table/column names if yours differ (e.g., cs.id vs cs.class_section_id)
|
// Adjust table/column names if yours differ (e.g., cs.class_section_id vs cs.id)
|
||||||
$row = $db->table('classSection cs')
|
$row = $db->table('classSection cs')
|
||||||
->select('u.id AS teacher_id, u.firstname, u.lastname')
|
->select('u.id AS teacher_id, u.firstname, u.lastname')
|
||||||
->join('teacher_class tc', 'tc.class_section_id = cs.id', 'inner')
|
->join('teacher_class tc', 'tc.class_section_id = cs.class_section_id', 'inner')
|
||||||
->join('users u', 'u.id = tc.teacher_id', 'inner')
|
->join('users u', 'u.id = tc.teacher_id', 'inner')
|
||||||
->where('cs.class_section_name', $classSectionName)
|
->where('cs.class_section_name', $classSectionName)
|
||||||
->where('tc.school_year', $schoolYear)
|
->where('tc.school_year', $schoolYear)
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ class TeacherModel extends Model
|
|||||||
public function getTeachersAndTAs(): array
|
public function getTeachersAndTAs(): array
|
||||||
{
|
{
|
||||||
return $this->db->table('users u')
|
return $this->db->table('users u')
|
||||||
->select('u.id, u.firstname, u.lastname, u.email, u.cellphone, r.name as role')
|
->select('u.id, u.firstname, u.lastname, u.email, u.cellphone, MIN(r.name) as role')
|
||||||
->join('user_roles ur', 'ur.user_id = u.id')
|
->join('user_roles ur', 'ur.user_id = u.id')
|
||||||
->join('roles r', 'r.id = ur.role_id')
|
->join('roles r', 'r.id = ur.role_id')
|
||||||
->whereIn('r.name', ['teacher', 'teacher_assistant']) // ✅ Filter only relevant roles
|
->whereIn('r.name', ['teacher', 'teacher_assistant']) // ✅ Filter only relevant roles
|
||||||
->groupBy('u.id')
|
->groupBy('u.id, u.firstname, u.lastname, u.email, u.cellphone')
|
||||||
->orderBy('u.lastname', 'ASC')
|
->orderBy('u.lastname', 'ASC')
|
||||||
->get()
|
->get()
|
||||||
->getResultArray();
|
->getResultArray();
|
||||||
@@ -72,4 +72,4 @@ public function getTeachersAndTAs(): array
|
|||||||
->get()
|
->get()
|
||||||
->getResultArray();
|
->getResultArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,8 +167,13 @@ class TimeService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sourceTz = $sourceTz ?: $this->serverTimezone;
|
|
||||||
$targetTz = $targetTz ?: $this->userTimezone();
|
$targetTz = $targetTz ?: $this->userTimezone();
|
||||||
|
if ($sourceTz === null && $this->isDateOnlyString($value)) {
|
||||||
|
// Date-only strings should not shift across timezones.
|
||||||
|
$sourceTz = $targetTz;
|
||||||
|
} else {
|
||||||
|
$sourceTz = $sourceTz ?: $this->serverTimezone;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($value instanceof Time) {
|
if ($value instanceof Time) {
|
||||||
@@ -204,4 +209,14 @@ class TimeService
|
|||||||
{
|
{
|
||||||
return (string) ($this->toUTC($value, $fromTz, $format) ?? '');
|
return (string) ($this->toUTC($value, $fromTz, $format) ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isDateOnlyString($value): bool
|
||||||
|
{
|
||||||
|
if (!is_string($value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = trim($value);
|
||||||
|
return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,23 @@
|
|||||||
<h3 class="mb-0">Class Progress Reports</h3>
|
<h3 class="mb-0">Class Progress Reports</h3>
|
||||||
<div class="text-muted">Filter by week, class, and status</div>
|
<div class="text-muted">Filter by week, class, and status</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<?php
|
||||||
|
$lowProgressSectionIds = $lowProgressSectionIds ?? [];
|
||||||
|
$lowProgressQuery = implode(',', $lowProgressSectionIds);
|
||||||
|
$lowProgressUrl = base_url('administrator/teacher-submissions');
|
||||||
|
if ($lowProgressQuery !== '') {
|
||||||
|
$lowProgressUrl .= '?low_progress_sections=' . rawurlencode($lowProgressQuery);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<a
|
||||||
|
class="btn btn-sm btn-outline-warning <?= empty($lowProgressSectionIds) ? 'disabled' : '' ?>"
|
||||||
|
href="<?= esc($lowProgressUrl) ?>"
|
||||||
|
<?= empty($lowProgressSectionIds) ? 'tabindex="-1" aria-disabled="true"' : '' ?>
|
||||||
|
>
|
||||||
|
Teachers < 50%
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -115,7 +132,7 @@
|
|||||||
$submissionLabel = $expectedDays > 0
|
$submissionLabel = $expectedDays > 0
|
||||||
? ('Submitted: ' . (int) $stat['submitted'] . ' / ' . (int) $expectedDays . ' (' . $percentLabel . ')')
|
? ('Submitted: ' . (int) $stat['submitted'] . ' / ' . (int) $expectedDays . ' (' . $percentLabel . ')')
|
||||||
: 'Submitted: N/A';
|
: 'Submitted: N/A';
|
||||||
$subjectLabel = 'Subjects: ' . $subjectCount;
|
$subjectLabel = 'Units: ' . $subjectCount;
|
||||||
?>
|
?>
|
||||||
<div class="accordion-item mb-2">
|
<div class="accordion-item mb-2">
|
||||||
<h2 class="accordion-header" id="<?= esc($headingId) ?>">
|
<h2 class="accordion-header" id="<?= esc($headingId) ?>">
|
||||||
@@ -151,12 +168,33 @@
|
|||||||
$weekLabel .= ' – ' . date('M d, Y', strtotime($group['week_end']));
|
$weekLabel .= ' – ' . date('M d, Y', strtotime($group['week_end']));
|
||||||
}
|
}
|
||||||
$reports = $group['reports'] ?? [];
|
$reports = $group['reports'] ?? [];
|
||||||
$teacherName = '';
|
$teacherCounts = [];
|
||||||
|
$teacherLatest = [];
|
||||||
foreach ($reports as $report) {
|
foreach ($reports as $report) {
|
||||||
if (! empty($report['teacher_name'])) {
|
$name = trim((string) ($report['teacher_name'] ?? ''));
|
||||||
$teacherName = $report['teacher_name'];
|
if ($name === '') {
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
|
$teacherCounts[$name] = ($teacherCounts[$name] ?? 0) + 1;
|
||||||
|
$stamp = (string) ($report['updated_at'] ?? $report['created_at'] ?? '');
|
||||||
|
if ($stamp !== '' && (!isset($teacherLatest[$name]) || $stamp > $teacherLatest[$name])) {
|
||||||
|
$teacherLatest[$name] = $stamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$teacherLabel = '-';
|
||||||
|
if (!empty($teacherCounts)) {
|
||||||
|
$bestName = '';
|
||||||
|
$bestCount = -1;
|
||||||
|
$bestStamp = '';
|
||||||
|
foreach ($teacherCounts as $name => $count) {
|
||||||
|
$stamp = $teacherLatest[$name] ?? '';
|
||||||
|
if ($count > $bestCount || ($count === $bestCount && $stamp > $bestStamp)) {
|
||||||
|
$bestName = $name;
|
||||||
|
$bestCount = $count;
|
||||||
|
$bestStamp = $stamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$teacherLabel = $bestName ?: '-';
|
||||||
}
|
}
|
||||||
$exampleId = $reports ? reset($reports)['id'] : null;
|
$exampleId = $reports ? reset($reports)['id'] : null;
|
||||||
?>
|
?>
|
||||||
@@ -176,12 +214,22 @@
|
|||||||
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong>
|
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong>
|
||||||
<span class="badge <?= $badgeClass ?>"><?= esc($statusTag) ?></span>
|
<span class="badge <?= $badgeClass ?>"><?= esc($statusTag) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted"><?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?></div>
|
<div class="small">
|
||||||
|
<?php if ($report): ?>
|
||||||
|
<?= view('admin/partials/class_progress_unit_display', [
|
||||||
|
'unitTitle' => (string) ($report['unit_title'] ?? ''),
|
||||||
|
'isQuran' => ($subjectName === 'Quran/Arabic'),
|
||||||
|
'compact' => true,
|
||||||
|
]) ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted">No submission</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><?= esc($teacherName ?: '-') ?></td>
|
<td><?= esc($teacherLabel) ?></td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<?php if ($exampleId): ?>
|
<?php if ($exampleId): ?>
|
||||||
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('admin/progress/view/' . $exampleId) ?>">View</a>
|
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('admin/progress/view/' . $exampleId) ?>">View</a>
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
<?php
|
<?php
|
||||||
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
||||||
$isQuran = $subjectName === 'Quran/Arabic';
|
$isQuran = $subjectName === 'Quran/Arabic';
|
||||||
$unitLabel = $isQuran ? 'Surah / Custom Arabic' : 'Unit / Chapter';
|
|
||||||
$homeworkLabel = $isQuran ? 'Arabic Practice / Homework' : 'Assigned Homework';
|
$homeworkLabel = $isQuran ? 'Arabic Practice / Homework' : 'Assigned Homework';
|
||||||
$report = $reportsBySubject[$subjectName] ?? null;
|
$report = $reportsBySubject[$subjectName] ?? null;
|
||||||
?>
|
?>
|
||||||
@@ -51,7 +50,13 @@
|
|||||||
<?php if (! $report): ?>
|
<?php if (! $report): ?>
|
||||||
<div class="text-muted">No entry submitted for this subject this week.</div>
|
<div class="text-muted">No entry submitted for this subject this week.</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="mb-2"><strong><?= $unitLabel ?>:</strong> <?= esc($report['unit_title'] ?: '-') ?></div>
|
<div class="mb-2">
|
||||||
|
<?= view('admin/partials/class_progress_unit_display', [
|
||||||
|
'unitTitle' => (string) ($report['unit_title'] ?? ''),
|
||||||
|
'isQuran' => $isQuran,
|
||||||
|
'compact' => false,
|
||||||
|
]) ?>
|
||||||
|
</div>
|
||||||
<?php if (!empty($report['materials'])): ?>
|
<?php if (!empty($report['materials'])): ?>
|
||||||
<div class="mb-2"><strong>Materials:</strong> <?= esc($report['materials']) ?></div>
|
<div class="mb-2"><strong>Materials:</strong> <?= esc($report['materials']) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Renders class progress unit_title with explicit curriculum vs custom subject lines.
|
||||||
|
*
|
||||||
|
* @var string $unitTitle
|
||||||
|
* @var bool $isQuran
|
||||||
|
* @var bool $compact If true, tighter layout for the list accordion.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use App\Controllers\ClassProgressController;
|
||||||
|
|
||||||
|
$unitTitle = trim((string) ($unitTitle ?? ''));
|
||||||
|
$compact = ! empty($compact);
|
||||||
|
$isQuran = ! empty($isQuran);
|
||||||
|
|
||||||
|
$split = ClassProgressController::splitUnitTitleForDisplay($unitTitle);
|
||||||
|
$curriculumLines = $split['curriculum'];
|
||||||
|
$customTopics = $split['custom'];
|
||||||
|
|
||||||
|
$labelCurriculum = $isQuran ? 'Surah / curriculum' : 'Unit / chapter';
|
||||||
|
$labelCustom = $isQuran ? 'Custom Surah / Arabic' : 'Custom subject(s)';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if ($unitTitle === ''): ?>
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
<?php elseif ($compact): ?>
|
||||||
|
<?php if (! empty($customTopics)): ?>
|
||||||
|
<div class="small">
|
||||||
|
<span class="badge text-bg-info me-1"><?= $isQuran ? 'Custom' : 'Custom subject' ?></span><?= esc(implode(', ', $customTopics)) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (! empty($curriculumLines)): ?>
|
||||||
|
<div class="small <?= ! empty($customTopics) ? 'text-muted mt-1' : 'text-muted' ?>"><?= esc(implode(' · ', $curriculumLines)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (empty($customTopics) && empty($curriculumLines)): ?>
|
||||||
|
<div class="small text-muted"><?= esc($unitTitle) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php if (! empty($curriculumLines)): ?>
|
||||||
|
<div class="mb-2"><strong><?= esc($labelCurriculum) ?>:</strong> <?= esc(implode(' ; ', $curriculumLines)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (! empty($customTopics)): ?>
|
||||||
|
<div class="mb-2"><strong><?= esc($labelCustom) ?>:</strong> <?= esc(implode(' ; ', $customTopics)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (empty($curriculumLines) && empty($customTopics)): ?>
|
||||||
|
<div class="mb-2"><strong><?= esc($labelCurriculum) ?>:</strong> <?= esc($unitTitle) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
@@ -696,7 +696,17 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($studentsBySection[$sectionKey] as $student): ?>
|
<?php
|
||||||
|
$uniqueStudents = [];
|
||||||
|
foreach ($studentsBySection[$sectionKey] as $student) {
|
||||||
|
$sid = (int)($student['id'] ?? 0);
|
||||||
|
if ($sid <= 0 || isset($uniqueStudents[$sid])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$uniqueStudents[$sid] = $student;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php foreach ($uniqueStudents as $student): ?>
|
||||||
<?php
|
<?php
|
||||||
$sid = (int)$student['id'];
|
$sid = (int)$student['id'];
|
||||||
$entryMap = $recAt($__attendanceData[$sectionKey][$sid] ?? []);
|
$entryMap = $recAt($__attendanceData[$sectionKey][$sid] ?? []);
|
||||||
|
|||||||
@@ -250,6 +250,54 @@
|
|||||||
$analysisSectionTotals[] = (int)($s['total_students'] ?? 0);
|
$analysisSectionTotals[] = (int)($s['total_students'] ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Student absences/late list ----
|
||||||
|
$sectionLabelByKey = [];
|
||||||
|
foreach ($grades as $classId => $sections) {
|
||||||
|
foreach ($sections as $section) {
|
||||||
|
$sectionKey = (string)($section['class_section_id'] ?? ($section['id'] ?? ''));
|
||||||
|
if ($sectionKey === '') continue;
|
||||||
|
$secNameRaw = trim((string)($section['class_section_name'] ?? ''));
|
||||||
|
$sectionLabelByKey[$sectionKey] = $secNameRaw !== '' ? $secNameRaw : ('Section ' . $sectionKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$studentIssueRows = [];
|
||||||
|
foreach ($studentsBySection as $sectionKey => $students) {
|
||||||
|
foreach ($students as $stu) {
|
||||||
|
$sid = (int)($stu['id'] ?? 0);
|
||||||
|
if ($sid <= 0) continue;
|
||||||
|
$entries = $attendanceData[$sectionKey][$sid] ?? [];
|
||||||
|
if (!is_array($entries)) continue;
|
||||||
|
$abs = 0;
|
||||||
|
$late = 0;
|
||||||
|
foreach ($entries as $e) {
|
||||||
|
$d = substr((string)($e['date'] ?? ''), 0, 10);
|
||||||
|
if ($d === '') continue;
|
||||||
|
if ($filterStart !== '' && $d < $filterStart) continue;
|
||||||
|
if ($filterEnd !== '' && $d > $filterEnd) continue;
|
||||||
|
$st = strtolower(trim((string)($e['status'] ?? '')));
|
||||||
|
if ($st === 'absent') {
|
||||||
|
$abs++;
|
||||||
|
} elseif ($st === 'late') {
|
||||||
|
$late++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (($abs + $late) <= 0) continue;
|
||||||
|
$studentIssueRows[] = [
|
||||||
|
'name' => trim((string)($stu['firstname'] ?? '') . ' ' . (string)($stu['lastname'] ?? '')),
|
||||||
|
'section' => $sectionLabelByKey[(string)$sectionKey] ?? ('Section ' . $sectionKey),
|
||||||
|
'absent' => $abs,
|
||||||
|
'late' => $late,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($studentIssueRows, static function ($a, $b) {
|
||||||
|
$sec = strcmp($a['section'], $b['section']);
|
||||||
|
if ($sec !== 0) return $sec;
|
||||||
|
return strcmp($a['name'], $b['name']);
|
||||||
|
});
|
||||||
|
|
||||||
$totalDaysForPercent = 0;
|
$totalDaysForPercent = 0;
|
||||||
if ($filterStart === '' && $filterEnd === '' && !empty($totalPassedDays)) {
|
if ($filterStart === '' && $filterEnd === '' && !empty($totalPassedDays)) {
|
||||||
$totalDaysForPercent = (int)$totalPassedDays;
|
$totalDaysForPercent = (int)$totalPassedDays;
|
||||||
@@ -373,6 +421,37 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="attn-analysis-card attn-analysis-wide">
|
||||||
|
<h6>Students With Absences / Late</h6>
|
||||||
|
<div class="attn-analysis-scroll">
|
||||||
|
<table id="studentIssueTable" class="attn-analysis-table no-mgmt-sticky" data-no-mgmt-sticky>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Student Name</th>
|
||||||
|
<th>Class Section</th>
|
||||||
|
<th class="text-end">Nbr of ABS</th>
|
||||||
|
<th class="text-end">Nbr of LATE</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($studentIssueRows)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center text-muted">No absences or late records in the selected range.</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($studentIssueRows as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= esc($row['name']) ?></td>
|
||||||
|
<td><?= esc($row['section']) ?></td>
|
||||||
|
<td class="text-end"><?= (int)$row['absent'] ?></td>
|
||||||
|
<td class="text-end"><?= (int)$row['late'] ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -480,6 +559,13 @@
|
|||||||
info: false,
|
info: false,
|
||||||
order: [[0, 'asc']]
|
order: [[0, 'asc']]
|
||||||
});
|
});
|
||||||
|
$('#studentIssueTable').DataTable({
|
||||||
|
paging: true,
|
||||||
|
searching: true,
|
||||||
|
info: true,
|
||||||
|
order: [[1, 'asc'], [0, 'asc']],
|
||||||
|
pageLength: 25
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<h2>Create Event</h2>
|
<h2>Create Event</h2>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$defaultCategories = ['Fun Event-1', 'Fun Event-2', 'Fun Event-3'];
|
||||||
|
$existingCategories = array_map('strval', $categories ?? []);
|
||||||
|
$allCategories = array_unique(array_merge($defaultCategories, $existingCategories));
|
||||||
|
?>
|
||||||
<form method="post" action="<?= site_url('administrator/events/create') ?>" enctype="multipart/form-data">
|
<form method="post" action="<?= site_url('administrator/events/create') ?>" enctype="multipart/form-data">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -15,7 +20,7 @@
|
|||||||
<label class="form-label">Category</label>
|
<label class="form-label">Category</label>
|
||||||
<select name="event_category" class="form-control" required>
|
<select name="event_category" class="form-control" required>
|
||||||
<option value="" selected disabled>Select category</option>
|
<option value="" selected disabled>Select category</option>
|
||||||
<?php foreach (($categories ?? []) as $category): ?>
|
<?php foreach ($allCategories as $category): ?>
|
||||||
<option value="<?= esc($category) ?>"><?= esc(ucwords($category)) ?></option>
|
<option value="<?= esc($category) ?>"><?= esc(ucwords($category)) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -11,44 +11,85 @@
|
|||||||
<div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
|
<div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$selectedEvent = null;
|
||||||
|
if (!empty($filterEventId)) {
|
||||||
|
foreach ($events as $event) {
|
||||||
|
if ((int)$event['id'] === (int)$filterEventId) {
|
||||||
|
$selectedEvent = $event;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
<!-- Add New Charge Form -->
|
<!-- Add New Charge Form -->
|
||||||
<form action="<?= site_url('payment/event_charges') ?>" method="post">
|
<div class="card mb-4">
|
||||||
<?= csrf_field() ?>
|
<div class="card-body">
|
||||||
<div class="row">
|
<form action="<?= site_url('payment/event_charges') ?>" method="post">
|
||||||
<div class="col-md-3 mt-3">
|
<?= csrf_field() ?>
|
||||||
<label for="event_id" class="form-label">Select Event</label>
|
<div class="row g-3">
|
||||||
<select name="event_id" id="event_id" class="form-select" required>
|
<div class="col-md-4">
|
||||||
<option value="">-- Select Event --</option>
|
<label for="event_id" class="form-label">Select Event</label>
|
||||||
<?php foreach ($events as $event): ?>
|
<select name="event_id" id="event_id" class="form-select" required>
|
||||||
<option value="<?= esc($event['id']) ?>">
|
<option value="">-- All events --</option>
|
||||||
<?= esc($event['event_name']) ?>
|
<?php foreach ($events as $event): ?>
|
||||||
</option>
|
<option value="<?= esc($event['id']) ?>" <?= isset($filterEventId) && $filterEventId == $event['id'] ? 'selected' : '' ?>>
|
||||||
<?php endforeach; ?>
|
<?= esc($event['event_name']) ?>
|
||||||
</select>
|
</option>
|
||||||
</div>
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 mt-3">
|
<?php if ($selectedEvent): ?>
|
||||||
<label for="parent_id" class="form-label">Parent</label>
|
<div class="col-md-8">
|
||||||
<select id="parent_id" name="parent_id" class="form-select" required>
|
<div class="border rounded bg-light p-3 h-100">
|
||||||
<option value="">-- Select Parent --</option>
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<?php foreach ($parents as $parent): ?>
|
<h6 class="m-0"><?= esc($selectedEvent['event_name']) ?></h6>
|
||||||
<option value="<?= $parent['id'] ?>">
|
<span class="badge bg-info text-dark">
|
||||||
<?= esc($parent['firstname'] . ' ' . $parent['lastname']) ?> (<?= esc($parent['school_id']) ?>)
|
$<?= esc(number_format($selectedEvent['amount'] ?? 0, 2)) ?> fee
|
||||||
</option>
|
</span>
|
||||||
<?php endforeach; ?>
|
</div>
|
||||||
</select>
|
<p class="mb-1 text-muted small">
|
||||||
</div>
|
<?= esc($selectedEvent['description'] ?: 'No description provided for this event.') ?>
|
||||||
|
</p>
|
||||||
|
<?php if (!empty($selectedEvent['expiration_date'])): ?>
|
||||||
|
<small class="text-secondary">
|
||||||
|
Expires: <?= esc(local_date($selectedEvent['expiration_date'], 'm-d-Y')) ?>
|
||||||
|
</small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="col-12 mt-3" id="studentCheckboxContainer" style="display: none;">
|
</div>
|
||||||
<label>Select Students:</label>
|
|
||||||
<div id="studentList" class="row"></div>
|
<div class="row g-3 mt-2">
|
||||||
</div>
|
<div class="col-md-4">
|
||||||
|
<label for="parent_id" class="form-label">Parents List</label>
|
||||||
|
<select id="parent_id" name="parent_id" class="form-select" required>
|
||||||
|
<option value="">-- Select Parent --</option>
|
||||||
|
<?php foreach ($parents as $parent): ?>
|
||||||
|
<option value="<?= $parent['id'] ?>">
|
||||||
|
<?= esc($parent['firstname'] . ' ' . $parent['lastname']) ?> (<?= esc($parent['school_id']) ?>)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4" id="studentCheckboxContainer" style="display: none;">
|
||||||
|
<label class="form-label">Select Students:</label>
|
||||||
|
<div id="studentList" class="row g-3"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-2 mt-3">
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
<a href="<?= site_url('administrator/events') ?>" class="btn btn-success">Event List</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mt-3">Submit</button>
|
|
||||||
<a href="<?= site_url('administrator/events') ?>" class="btn btn-success mt-3">Event List</a>
|
|
||||||
</form>
|
|
||||||
<br>
|
|
||||||
<!-- Charge Tables grouped by event -->
|
<!-- Charge Tables grouped by event -->
|
||||||
<?php
|
<?php
|
||||||
$grouped = [];
|
$grouped = [];
|
||||||
@@ -70,16 +111,18 @@
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered table-striped mb-0 no-mgmt-sticky">
|
<table class="table table-bordered table-striped mb-0 no-mgmt-sticky">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Parent Name</th>
|
<th>Parent Name</th>
|
||||||
<th>Student Name</th>
|
<th>Student Name</th>
|
||||||
<th>Charged Amount</th>
|
<th>Charged Amount</th>
|
||||||
<th>Is Participating</th>
|
<th>Is Participating</th>
|
||||||
<th>Semester</th>
|
<th>Semester</th>
|
||||||
<th>Year</th>
|
<th>Year</th>
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
</tr>
|
<th>Description</th>
|
||||||
|
<th>Event Fees</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($rows as $charge): ?>
|
<?php foreach ($rows as $charge): ?>
|
||||||
@@ -100,6 +143,8 @@
|
|||||||
<td><?= esc($charge['semester'] ?? '-') ?></td>
|
<td><?= esc($charge['semester'] ?? '-') ?></td>
|
||||||
<td><?= esc($charge['school_year'] ?? '-') ?></td>
|
<td><?= esc($charge['school_year'] ?? '-') ?></td>
|
||||||
<td><?= esc(!empty($charge['created_at']) ? local_datetime($charge['created_at'], 'm-d-Y H:i') : '') ?></td>
|
<td><?= esc(!empty($charge['created_at']) ? local_datetime($charge['created_at'], 'm-d-Y H:i') : '') ?></td>
|
||||||
|
<td><?= esc($charge['event_description'] ?? '—') ?></td>
|
||||||
|
<td>$<?= esc(number_format($charge['event_amount'] ?? 0, 2)) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -156,7 +201,19 @@ function loadStudentsWithCharges() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#parent_id').on('change', loadStudentsWithCharges);
|
$(function() {
|
||||||
$('#event_id').on('change', loadStudentsWithCharges);
|
$('#parent_id').on('change', loadStudentsWithCharges);
|
||||||
|
|
||||||
|
$('#event_id').on('change', function() {
|
||||||
|
let eventId = $(this).val();
|
||||||
|
let url = new URL(window.location.href);
|
||||||
|
if (eventId) {
|
||||||
|
url.searchParams.set('event_id', eventId);
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete('event_id');
|
||||||
|
}
|
||||||
|
window.location.href = url.toString();
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Event Name</th>
|
<th>Event Name</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>Amount</th>
|
<th>Description</th>
|
||||||
|
<th>Event Fees</th>
|
||||||
<th>Expiration Date</th>
|
<th>Expiration Date</th>
|
||||||
<th>Semester</th>
|
<th>Semester</th>
|
||||||
<th>School Year</th>
|
<th>School Year</th>
|
||||||
@@ -33,7 +34,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><?= esc($event['event_name']) ?></td>
|
<td><?= esc($event['event_name']) ?></td>
|
||||||
<td><?= esc($event['event_category'] ?? '—') ?></td>
|
<td><?= esc($event['event_category'] ?? '—') ?></td>
|
||||||
<td><?= esc($event['amount']) ?></td>
|
<td><?= esc(!empty($event['description']) ? $event['description'] : '—') ?></td>
|
||||||
|
<td>$<?= esc(number_format($event['amount'], 2)) ?></td>
|
||||||
<td><?= esc(!empty($event['expiration_date']) ? local_date($event['expiration_date'], 'm-d-Y') : '') ?></td>
|
<td><?= esc(!empty($event['expiration_date']) ? local_date($event['expiration_date'], 'm-d-Y') : '') ?></td>
|
||||||
<td><?= esc($event['semester']) ?></td>
|
<td><?= esc($event['semester']) ?></td>
|
||||||
<td><?= esc($event['school_year']) ?></td>
|
<td><?= esc($event['school_year']) ?></td>
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
<?= $this->extend('layout/management_layout') ?>
|
<?= $this->extend('layout/management_layout') ?>
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
|
<?php
|
||||||
|
$statusBadges = $statusBadges ?? [];
|
||||||
|
$drafts = $drafts ?? [];
|
||||||
|
$legacyByClass = $legacyByClass ?? [];
|
||||||
|
$classSections = $classSections ?? [];
|
||||||
|
$examTypes = $examTypes ?? [];
|
||||||
|
$visibleClasses = $visibleClasses ?? [];
|
||||||
|
$classDraftGroups = $classDraftGroups ?? [];
|
||||||
|
$newSubmissionClasses = $newSubmissionClasses ?? [];
|
||||||
|
$schoolYear = $schoolYear ?? '';
|
||||||
|
$semester = $semester ?? '';
|
||||||
|
$maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||||
|
$allowedExtensions = $allowedExtensions ?? ['doc', 'docx', 'pdf'];
|
||||||
|
|
||||||
|
$renderBadge = static function (string $status, array $badges): string {
|
||||||
|
$b = $badges[$status] ?? ['label' => $status, 'class' => 'bg-secondary text-white'];
|
||||||
|
$style = !empty($b['style']) ? ' style="' . esc($b['style']) . '"' : '';
|
||||||
|
return '<span class="badge ' . esc($b['class']) . '"' . $style . '>' . esc($b['label']) . '</span>';
|
||||||
|
};
|
||||||
|
|
||||||
|
$fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensions));
|
||||||
|
?>
|
||||||
<div class="container-fluid px-4 py-4">
|
<div class="container-fluid px-4 py-4">
|
||||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
|
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="h3 mb-1">Exam Draft Submissions</h1>
|
<h1 class="h3 mb-1">Exam draft submissions</h1>
|
||||||
<p class="text-muted mb-0 small">
|
<p class="text-muted mb-0 small">
|
||||||
<?= esc($semester ?: 'Semester') ?> <?= esc($schoolYear ?: '') ?>
|
<?= esc($semester ?: 'Semester') ?> <?= esc($schoolYear ?: '') ?>
|
||||||
</p>
|
</p>
|
||||||
@@ -15,13 +37,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (session()->getFlashdata('success')): ?>
|
<?php if (session()->getFlashdata('success')): ?>
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||||
<?= esc(session()->getFlashdata('success')) ?>
|
|
||||||
</div>
|
|
||||||
<?php elseif (session()->getFlashdata('error')): ?>
|
<?php elseif (session()->getFlashdata('error')): ?>
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||||
<?= esc(session()->getFlashdata('error')) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<ul class="nav nav-pills mb-3" id="examDraftTabs" role="tablist">
|
<ul class="nav nav-pills mb-3" id="examDraftTabs" role="tablist">
|
||||||
@@ -32,177 +50,253 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="legacy-tab" data-bs-toggle="pill" data-bs-target="#legacy" type="button" role="tab" aria-controls="legacy" aria-selected="false">
|
<button class="nav-link" id="legacy-tab" data-bs-toggle="pill" data-bs-target="#legacy" type="button" role="tab" aria-controls="legacy" aria-selected="false">
|
||||||
Legacy Exams
|
Legacy exams
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content" id="examDraftTabsContent">
|
<div class="tab-content" id="examDraftTabsContent">
|
||||||
<div class="tab-pane fade show active" id="submissions" role="tabpanel" aria-labelledby="submissions-tab">
|
<div class="tab-pane fade show active" id="submissions" role="tabpanel" aria-labelledby="submissions-tab">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<?php if (empty($drafts)): ?>
|
<?php if (empty($visibleClasses)): ?>
|
||||||
<p class="text-muted mb-0">No exam drafts have been submitted yet.</p>
|
<p class="text-muted mb-0">No classes with enrolled students are available for this term.</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="table-responsive">
|
<div class="accordion exam-drafts-accordion" id="examDraftsAccordion">
|
||||||
<table class="table table-striped table-bordered align-middle mb-0 exam-drafts-table no-mgmt-sticky">
|
<?php foreach ($visibleClasses as $classSectionId => $classInfo): ?>
|
||||||
<thead class="table-light">
|
<?php $classDraftsForSection = $classDraftGroups[$classSectionId] ?? []; ?>
|
||||||
<tr>
|
<?php $collapseId = 'classDraftGroup_' . $classSectionId; ?>
|
||||||
<th>Teacher</th>
|
<div class="accordion-item mb-3">
|
||||||
<th>Class</th>
|
<h2 class="accordion-header" id="heading_<?= esc($collapseId) ?>">
|
||||||
<th>Title / Type</th>
|
<button class="accordion-button collapsed px-3" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
|
||||||
<th>Version</th>
|
<div class="d-flex flex-column flex-lg-row w-100 justify-content-between gap-2">
|
||||||
<th>Submitted</th>
|
<div>
|
||||||
<th>Status</th>
|
<strong><?= esc($classInfo['class_section_name'] ?? ('Class ' . $classSectionId)) ?></strong>
|
||||||
<th>Files</th>
|
<div class="small text-muted">
|
||||||
<th>PDF</th>
|
<?= esc($classInfo['student_count'] ?? 0) ?> students · <?= esc(count($classDraftsForSection)) ?> submissions
|
||||||
<th>Review</th>
|
<?php if (!empty($newSubmissionClasses[$classSectionId])): ?>
|
||||||
</tr>
|
<span class="badge bg-info text-dark ms-2">New submission</span>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($drafts as $draft): ?>
|
|
||||||
<?php
|
|
||||||
$teacherName = trim(($draft['teacher_first'] ?? '') . ' ' . ($draft['teacher_last'] ?? ''));
|
|
||||||
if ($teacherName === '') {
|
|
||||||
$teacherName = ($draft['admin_id'] ?? null) === ($draft['teacher_id'] ?? null)
|
|
||||||
? 'Admin Upload'
|
|
||||||
: 'User #' . ($draft['teacher_id'] ?? 'N/A');
|
|
||||||
}
|
|
||||||
$statusInfo = $statusBadges[$draft['status'] ?? 'pending'] ?? ['label' => 'Unknown', 'class' => 'bg-secondary text-white'];
|
|
||||||
$adminName = trim(($draft['admin_first'] ?? '') . ' ' . ($draft['admin_last'] ?? ''));
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td><?= esc($teacherName) ?></td>
|
|
||||||
<td><?= esc($draft['class_section_name'] ?? 'Unknown') ?></td>
|
|
||||||
<td>
|
|
||||||
<strong><?= esc($draft['draft_title'] ?? 'Untitled') ?></strong>
|
|
||||||
<div class="small text-muted"><?= esc($draft['exam_type'] ?? 'N/A') ?></div>
|
|
||||||
<?php if (!empty($draft['description'])): ?>
|
|
||||||
<div class="small text-muted"><?= esc($draft['description']) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td class="text-nowrap">v<?= esc($draft['version'] ?? 1) ?></td>
|
|
||||||
<td class="text-nowrap"><?= esc((!empty($draft['created_at']) ? local_datetime($draft['created_at'], 'M j, Y g:i A') : '—')) ?></td>
|
|
||||||
<td>
|
|
||||||
<span class="badge <?= esc($statusInfo['class']) ?>">
|
|
||||||
<?= esc($statusInfo['label']) ?>
|
|
||||||
</span>
|
|
||||||
<?php if (!empty($draft['reviewed_at'])): ?>
|
|
||||||
<div class="small text-muted mt-1">
|
|
||||||
Reviewed <?= esc(local_datetime($draft['reviewed_at'], 'M j, Y g:i A')) ?>
|
|
||||||
<?php if ($adminName !== ''): ?>
|
|
||||||
by <?= esc($adminName) ?>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
</div>
|
||||||
</td>
|
<span class="badge bg-secondary align-self-start">
|
||||||
<td>
|
<?= esc(count($classDraftsForSection)) ?>
|
||||||
<?php if (!empty($draft['teacher_file'])): ?>
|
<?= count($classDraftsForSection) === 1 ? 'submission' : 'submissions' ?>
|
||||||
<div>
|
</span>
|
||||||
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank">
|
</div>
|
||||||
<?= esc($draft['teacher_filename'] ?? 'Submitted draft') ?>
|
</button>
|
||||||
</a>
|
</h2>
|
||||||
</div>
|
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="heading_<?= esc($collapseId) ?>">
|
||||||
<?php else: ?>
|
<div class="accordion-body pt-0 px-3">
|
||||||
<span class="text-muted small">No teacher file</span>
|
<?php if (empty($classDraftsForSection)): ?>
|
||||||
<?php endif; ?>
|
<p class="text-muted mb-0">No submissions yet for this class.</p>
|
||||||
<?php if (!empty($draft['final_file'])): ?>
|
<?php else: ?>
|
||||||
<div class="mt-2">
|
<div class="table-responsive">
|
||||||
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" class="link-success small">
|
<table class="table table-striped table-bordered align-middle mb-0 exam-drafts-table no-mgmt-sticky">
|
||||||
<?= esc($draft['final_filename'] ?? 'Final draft') ?>
|
<thead class="table-light">
|
||||||
</a>
|
<tr>
|
||||||
</div>
|
<th>Teacher</th>
|
||||||
<?php endif; ?>
|
<th>Class</th>
|
||||||
</td>
|
<th>Title & AuthorNote</th>
|
||||||
<td class="text-nowrap">
|
<th>Version</th>
|
||||||
<?php
|
<th>Date-Time</th>
|
||||||
$pdfFile = $draft['final_pdf_file'] ?? null;
|
<th>Status</th>
|
||||||
// Fallback: if final_file itself is a pdf, use it
|
<th>Files</th>
|
||||||
if (empty($pdfFile) && !empty($draft['final_file']) && strtolower(pathinfo($draft['final_file'], PATHINFO_EXTENSION)) === 'pdf') {
|
<th>Reviewer Action</th>
|
||||||
$pdfFile = $draft['final_file'];
|
<th>Final version</th>
|
||||||
}
|
</tr>
|
||||||
?>
|
</thead>
|
||||||
<?php if (!empty($pdfFile)): ?>
|
<tbody>
|
||||||
<a href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" class="link-success small">
|
<?php foreach ($classDraftsForSection as $draft): ?>
|
||||||
PDF
|
<?php
|
||||||
</a>
|
$teacherName = trim(($draft['teacher_first'] ?? '') . ' ' . ($draft['teacher_last'] ?? ''));
|
||||||
<?php else: ?>
|
if ($teacherName === '') {
|
||||||
<span class="text-muted small">—</span>
|
$teacherName = ($draft['admin_id'] ?? null) === ($draft['teacher_id'] ?? null)
|
||||||
<?php endif; ?>
|
? 'Admin upload'
|
||||||
</td>
|
: 'User #' . ($draft['teacher_id'] ?? 'N/A');
|
||||||
<td>
|
}
|
||||||
<form method="post" action="<?= base_url('/administrator/exam-drafts/review') ?>" enctype="multipart/form-data">
|
$st = strtolower((string) ($draft['status'] ?? ''));
|
||||||
<?= csrf_field() ?>
|
$adminName = trim(($draft['admin_first'] ?? '') . ' ' . ($draft['admin_last'] ?? ''));
|
||||||
<input type="hidden" name="draft_id" value="<?= esc($draft['id']) ?>">
|
$authorNote = $draft['author_comment'] ?? $draft['description'] ?? '';
|
||||||
<div class="mb-2">
|
$reviewerNote = $draft['reviewer_comment'] ?? $draft['admin_comments'] ?? '';
|
||||||
<textarea
|
?>
|
||||||
name="admin_comments"
|
<tr>
|
||||||
rows="2"
|
<td><?= esc($teacherName) ?></td>
|
||||||
class="form-control form-control-sm"
|
<td><?= esc($draft['class_section_name'] ?? 'Unknown') ?></td>
|
||||||
placeholder="Optional note for the teacher"
|
<td>
|
||||||
><?= esc($draft['admin_comments'] ?? '') ?></textarea>
|
<strong><?= esc($draft['draft_title'] ?? 'Untitled') ?></strong>
|
||||||
</div>
|
<div class="small text-muted"><?= esc($draft['exam_type'] ?? 'N/A') ?></div>
|
||||||
<div class="mb-2">
|
<?php if ($authorNote !== ''): ?>
|
||||||
<label class="form-label small mb-1">Status</label>
|
<div class="small mt-1">
|
||||||
<select name="review_status" class="form-select form-select-sm">
|
<span class="text-muted fw-semibold">Author:</span>
|
||||||
<?php foreach ($statusOptions as $option): ?>
|
<?= esc($authorNote) ?>
|
||||||
<option value="<?= esc($option) ?>" <?= ($option === ($draft['status'] ?? '')) ? 'selected' : '' ?>>
|
</div>
|
||||||
<?= esc(ucfirst($option)) ?>
|
<?php endif; ?>
|
||||||
</option>
|
<?php if ($reviewerNote !== ''): ?>
|
||||||
|
<div class="small mt-1 border-top pt-1">
|
||||||
|
<span class="text-muted fw-semibold">Reviewer:</span>
|
||||||
|
<?= esc($reviewerNote) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap">v<?= esc((string) ($draft['version'] ?? 1)) ?></td>
|
||||||
|
<td class="text-nowrap"><?= esc(!empty($draft['created_at']) ? local_datetime($draft['created_at'], 'M j, Y g:i A') : '—') ?></td>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$badgeData = $statusBadges[$st] ?? ['label' => $st, 'class' => 'bg-secondary text-white'];
|
||||||
|
$badgeStyle = !empty($badgeData['style']) ? ' style="' . esc($badgeData['style']) . '"' : '';
|
||||||
|
?>
|
||||||
|
<span class="badge <?= esc($badgeData['class']) ?> js-status-badge" data-status="<?= esc($st) ?>"<?= $badgeStyle ?>>
|
||||||
|
<?= esc($badgeData['label']) ?>
|
||||||
|
</span>
|
||||||
|
<div class="small text-muted mt-1 js-acceptance-note">
|
||||||
|
<?php if ($st === 'accepted' && !empty($draft['acceptance_type'])): ?>
|
||||||
|
<?= $draft['acceptance_type'] === 'minor_edits' ? 'Accepted w/ minor edits' : 'Accepted as is' ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($draft['reviewed_at'])): ?>
|
||||||
|
<div class="small text-muted mt-1">
|
||||||
|
Reviewed <?= esc(local_datetime($draft['reviewed_at'], 'M j, Y g:i A')) ?>
|
||||||
|
<?php if ($adminName !== ''): ?>
|
||||||
|
by <?= esc($adminName) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php $formId = 'review_form_' . (int) ($draft['id'] ?? 0); ?>
|
||||||
|
<?php $revNumber = max(1, (int) ($draft['version'] ?? 1)); ?>
|
||||||
|
<?php if (!empty($draft['teacher_file'])): ?>
|
||||||
|
<div>
|
||||||
|
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank" rel="noopener">
|
||||||
|
<?= esc('Ver' . $revNumber . ' ' . ($draft['teacher_filename'] ?? 'Submitted draft')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted small">No teacher file</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($draft['final_file']) && strtolower((string) ($draft['status'] ?? '')) === 'accepted'): ?>
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" rel="noopener" class="link-success small">
|
||||||
|
<?= esc('Final ' . ($draft['final_filename'] ?? 'Final draft')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($draft['review_files'])): ?>
|
||||||
|
<div class="mt-2">
|
||||||
|
<?php
|
||||||
|
$reviewLinks = [];
|
||||||
|
foreach ($draft['review_files'] as $rf) {
|
||||||
|
$rev = max(1, (int) ($rf['review_revision'] ?? 1));
|
||||||
|
$name = $rf['final_filename'] ?? 'Review file';
|
||||||
|
$file = $rf['final_file'] ?? '';
|
||||||
|
if ($file !== '') {
|
||||||
|
$reviewLinks[] = '<a href="' . esc(base_url('exam-drafts/files/final/' . $file)) . '" target="_blank" rel="noopener" class="link-success small">' . esc('Ver' . $revNumber . '_' . $rev . ' ' . $name) . '</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?= !empty($reviewLinks) ? implode(' | ', $reviewLinks) : '' ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="mt-2">
|
||||||
|
<?php $fileInputId = 'review_file_' . (int) ($draft['id'] ?? 0); ?>
|
||||||
|
<input type="file" name="final_file" id="<?= esc($fileInputId) ?>" class="form-control form-control-sm" accept="<?= esc($fileAccept) ?>" form="<?= esc($formId) ?>">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary mt-2" form="<?= esc($formId) ?>">Upload review file</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="min-width: 280px;">
|
||||||
|
<?= form_open_multipart(base_url('administrator/exam-drafts/review'), ['class' => 'vstack gap-2', 'id' => $formId]) ?>
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="draft_id" value="<?= (int) ($draft['id'] ?? 0) ?>">
|
||||||
|
<?php $selectedStatus = ''; ?>
|
||||||
|
<select name="review_status" class="form-select form-select-sm" required>
|
||||||
|
<option value="" <?= $selectedStatus === '' ? 'selected' : '' ?> disabled>— Select status —</option>
|
||||||
|
<option value="accepted" <?= $selectedStatus === 'accepted' ? 'selected' : '' ?>>Accepted</option>
|
||||||
|
<option value="legacy" <?= $selectedStatus === 'legacy' ? 'selected' : '' ?>>Legacy</option>
|
||||||
|
<option value="canceled" <?= $selectedStatus === 'canceled' ? 'selected' : '' ?>>Canceled</option>
|
||||||
|
<option value="rejected" <?= $selectedStatus === 'rejected' ? 'selected' : '' ?>>Rejected</option>
|
||||||
|
<option value="review needed" <?= $selectedStatus === 'review needed' ? 'selected' : '' ?>>Review needed</option>
|
||||||
|
<option value="under review" <?= $selectedStatus === 'under review' ? 'selected' : '' ?>>Under review</option>
|
||||||
|
</select>
|
||||||
|
<div class="small js-acceptance-group d-none">
|
||||||
|
<span class="d-block mb-1">As is / Minor edits</span>
|
||||||
|
<?php $acc = (string) ($draft['acceptance_type'] ?? ''); ?>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="radio" name="acceptance_type" value="as_is" id="as_is_<?= (int) ($draft['id'] ?? 0) ?>" <?= $acc !== 'minor_edits' ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="as_is_<?= (int) ($draft['id'] ?? 0) ?>">As is</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="radio" name="acceptance_type" value="minor_edits" id="minor_<?= (int) ($draft['id'] ?? 0) ?>" <?= $acc === 'minor_edits' ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="minor_<?= (int) ($draft['id'] ?? 0) ?>">Minor edits</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea name="reviewer_comment" class="form-control form-control-sm js-review-comment" rows="3" placeholder="Feedback for the teacher"><?= esc($draft['reviewer_comment'] ?? $draft['reviewer_comments'] ?? $draft['admin_comments'] ?? '') ?></textarea>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<span class="text-muted small js-review-status"></span>
|
||||||
|
</div>
|
||||||
|
<?= form_close() ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap" style="min-width: 220px;">
|
||||||
|
<?php
|
||||||
|
$pdfFile = $draft['final_pdf_file'] ?? null;
|
||||||
|
if (empty($pdfFile) && !empty($draft['final_file']) && strtolower(pathinfo($draft['final_file'], PATHINFO_EXTENSION)) === 'pdf') {
|
||||||
|
$pdfFile = $draft['final_file'];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php if (!empty($pdfFile)): ?>
|
||||||
|
<div class="mt-2 d-flex flex-wrap gap-2">
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" rel="noopener">View</a>
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" rel="noopener" download>Download</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
<div class="mb-2">
|
</div>
|
||||||
<label class="form-label small mb-1">Upload final draft</label>
|
<?php endif; ?>
|
||||||
<input type="file" name="final_file" class="form-control form-control-sm">
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-sm btn-primary w-100">
|
</div>
|
||||||
Save review
|
<?php endforeach; ?>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="legacy" role="tabpanel" aria-labelledby="legacy-tab">
|
<div class="tab-pane fade" id="legacy" role="tabpanel" aria-labelledby="legacy-tab">
|
||||||
<div class="card border-primary-subtle mb-3">
|
<div class="card border-primary-subtle mb-3">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<strong>Upload Old / Legacy Exam</strong>
|
<strong>Upload old / legacy exam</strong>
|
||||||
<div class="small text-muted">Store historic exams as finalized records.</div>
|
<div class="small text-muted">Store historic exams as accepted records.</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge bg-primary-subtle text-primary">Admin only</span>
|
<span class="badge bg-primary-subtle text-primary">Admin only</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="<?= base_url('/administrator/exam-drafts/upload-legacy') ?>" enctype="multipart/form-data" class="row g-3">
|
<?= form_open_multipart(base_url('administrator/exam-drafts/upload-legacy'), ['class' => 'row g-3']) ?>
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label small">Class Section</label>
|
<label class="form-label small">Class sections</label>
|
||||||
<select name="class_section_id" class="form-select" required>
|
<select name="class_section_ids[]" class="form-select" multiple required size="6">
|
||||||
<option value="">Select class</option>
|
|
||||||
<?php foreach ($classSections as $cs): ?>
|
<?php foreach ($classSections as $cs): ?>
|
||||||
<option value="<?= esc($cs['class_section_id']) ?>"><?= esc($cs['class_section_name']) ?></option>
|
<option value="<?= esc($cs['class_section_id'] ?? '') ?>"><?= esc($cs['class_section_name'] ?? '') ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
<div class="form-text">Hold Ctrl (Windows) or Command (Mac) to select multiple sections.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label small">School Year</label>
|
<label class="form-label small">School year</label>
|
||||||
<input type="text" name="school_year" class="form-control" value="<?= esc($schoolYear) ?>" placeholder="2025-2026" required>
|
<input type="text" name="school_year" class="form-control" value="<?= esc($schoolYear) ?>" placeholder="2025-2026" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label small">Semester</label>
|
<label class="form-label small">Semester</label>
|
||||||
<select name="semester" class="form-select" required>
|
<input type="text" name="semester" class="form-control" value="<?= esc($semester) ?>" placeholder="Fall / Spring" required>
|
||||||
<option value="Fall" <?= ($semester === 'Fall') ? 'selected' : '' ?>>Fall</option>
|
|
||||||
<option value="Spring" <?= ($semester === 'Spring') ? 'selected' : '' ?>>Spring</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label small">Exam Type</label>
|
<label class="form-label small">Exam type</label>
|
||||||
<select name="exam_type" class="form-select">
|
<select name="exam_type" class="form-select">
|
||||||
<option value="">— Select type —</option>
|
<option value="">— Select type —</option>
|
||||||
<?php foreach ($examTypes as $type): ?>
|
<?php foreach ($examTypes as $type): ?>
|
||||||
@@ -211,14 +305,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label small">Upload File</label>
|
<label class="form-label small">Upload file</label>
|
||||||
<input type="file" name="old_exam_file" class="form-control" accept=".doc,.docx,.pdf" required>
|
<input type="file" name="old_exam_file" class="form-control" accept="<?= esc($fileAccept) ?>" required>
|
||||||
<div class="form-text">Allowed: <?= esc(implode(', ', $allowedExtensions)) ?> • Max <?= number_format($maxUploadBytes / 1024 / 1024, 0) ?> MB</div>
|
<div class="form-text">Allowed: <?= esc(implode(', ', $allowedExtensions)) ?> • Max <?= number_format($maxUploadBytes / 1024 / 1024, 0) ?> MB</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button type="submit" class="btn btn-primary">Upload Legacy Exam</button>
|
<button type="submit" class="btn btn-primary">Upload legacy exam</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<?= form_close() ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -231,20 +325,21 @@
|
|||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h6 class="mb-2"><?= esc($group['class_section_name'] ?? 'Class') ?></h6>
|
<h6 class="mb-2"><?= esc($group['class_section_name'] ?? 'Class') ?></h6>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<?php foreach ($group['items'] as $item): ?>
|
<?php foreach ($group['items'] ?? [] as $item): ?>
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-start">
|
<?php $lst = strtolower((string) ($item['status'] ?? '')); ?>
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-start flex-wrap gap-2">
|
||||||
<div class="me-3">
|
<div class="me-3">
|
||||||
<div class="fw-semibold"><?= esc($item['draft_title'] ?? 'Legacy Exam') ?></div>
|
<div class="fw-semibold"><?= esc($item['draft_title'] ?? 'Legacy exam') ?></div>
|
||||||
<div class="small text-muted">
|
<div class="small text-muted">
|
||||||
<?= esc($item['exam_type'] ?? 'N/A') ?> •
|
<?= esc($item['exam_type'] ?? 'N/A') ?> •
|
||||||
<?= esc($item['semester'] ?? '') ?> <?= esc($item['school_year'] ?? '') ?>
|
<?= esc($item['semester'] ?? '') ?> <?= esc($item['school_year'] ?? '') ?>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="small mt-1"><?= $renderBadge($lst, $statusBadges) ?></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end">
|
<div class="text-end d-flex flex-wrap gap-2 justify-content-end">
|
||||||
<?php if (!empty($item['final_file'])): ?>
|
<?php if (!empty($item['final_file'])): ?>
|
||||||
<a href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" class="btn btn-sm btn-outline-primary">
|
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" rel="noopener">View</a>
|
||||||
Download
|
<a class="btn btn-sm btn-outline-secondary" href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" rel="noopener" download>Download</a>
|
||||||
</a>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="text-muted small">File missing</span>
|
<span class="text-muted small">File missing</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -260,6 +355,208 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const submissionsTab = document.getElementById('submissions-tab');
|
||||||
|
if (submissionsTab) {
|
||||||
|
submissionsTab.addEventListener('click', () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const statusBadges = <?= json_encode($statusBadges, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
|
const csrfTokenName = <?= json_encode(csrf_token(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
|
const csrfCookieNames = <?= json_encode(array_values(array_filter([
|
||||||
|
config('Security')->csrfCookieName ?? null,
|
||||||
|
config('Security')->cookieName ?? null,
|
||||||
|
'csrf_cookie_name',
|
||||||
|
])), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
|
|
||||||
|
const readCookie = (name) => {
|
||||||
|
const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[$()*+.?[\\\]^{|}-]/g, '\\$&') + '=([^;]*)'));
|
||||||
|
return match ? decodeURIComponent(match[1]) : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncCsrfToken = (form) => {
|
||||||
|
let tokenValue = '';
|
||||||
|
csrfCookieNames.some((name) => {
|
||||||
|
tokenValue = readCookie(name);
|
||||||
|
return tokenValue !== '';
|
||||||
|
});
|
||||||
|
if (!tokenValue && form) {
|
||||||
|
const tokenInput = form.querySelector(`input[name="${csrfTokenName}"]`);
|
||||||
|
if (tokenInput && tokenInput.value) {
|
||||||
|
tokenValue = tokenInput.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tokenValue || !form) {
|
||||||
|
return tokenValue;
|
||||||
|
}
|
||||||
|
const tokenInput = form.querySelector(`input[name="${csrfTokenName}"]`);
|
||||||
|
if (tokenInput) {
|
||||||
|
tokenInput.value = tokenValue;
|
||||||
|
}
|
||||||
|
return tokenValue;
|
||||||
|
};
|
||||||
|
const updateRow = (row) => {
|
||||||
|
const statusSelect = row.querySelector('select[name="review_status"]');
|
||||||
|
const acceptanceGroup = row.querySelector('.js-acceptance-group');
|
||||||
|
if (!statusSelect || !acceptanceGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
acceptanceGroup.classList.toggle('d-none', statusSelect.value !== 'accepted');
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStatusUI = (row) => {
|
||||||
|
const statusSelect = row.querySelector('select[name="review_status"]');
|
||||||
|
const badge = row.querySelector('.js-status-badge');
|
||||||
|
if (!statusSelect || !badge || statusSelect.value === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const status = statusSelect.value;
|
||||||
|
const badgeData = statusBadges[status] || { label: status, class: 'bg-secondary text-white' };
|
||||||
|
badge.textContent = badgeData.label || status;
|
||||||
|
badge.className = `badge ${badgeData.class} js-status-badge`;
|
||||||
|
if (badgeData.style) {
|
||||||
|
badge.setAttribute('style', badgeData.style);
|
||||||
|
} else {
|
||||||
|
badge.removeAttribute('style');
|
||||||
|
}
|
||||||
|
badge.dataset.status = status;
|
||||||
|
|
||||||
|
const acceptanceNote = row.querySelector('.js-acceptance-note');
|
||||||
|
if (acceptanceNote) {
|
||||||
|
if (status === 'accepted') {
|
||||||
|
const acc = row.querySelector('input[name="acceptance_type"]:checked');
|
||||||
|
acceptanceNote.textContent = acc && acc.value === 'minor_edits'
|
||||||
|
? 'Accepted w/ minor edits'
|
||||||
|
: 'Accepted as is';
|
||||||
|
} else {
|
||||||
|
acceptanceNote.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveRow = (row, options = {}) => {
|
||||||
|
const form = row.querySelector('form');
|
||||||
|
if (!form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const statusSelect = form.querySelector('select[name="review_status"]');
|
||||||
|
if (!statusSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const statusLabel = form.querySelector('.js-review-status');
|
||||||
|
const data = new FormData(form);
|
||||||
|
data.delete('final_file');
|
||||||
|
data.delete('old_exam_file');
|
||||||
|
if (options.commitComment) {
|
||||||
|
const commentInput = form.querySelector('textarea[name="reviewer_comment"]');
|
||||||
|
if (commentInput) {
|
||||||
|
const raw = commentInput.value || '';
|
||||||
|
const committed = raw.endsWith('\n') ? raw : raw + '\n';
|
||||||
|
data.set('reviewer_comment', committed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const csrfToken = syncCsrfToken(form);
|
||||||
|
if (csrfToken) {
|
||||||
|
data.set(csrfTokenName, csrfToken);
|
||||||
|
}
|
||||||
|
if (statusLabel) {
|
||||||
|
statusLabel.textContent = 'Saving...';
|
||||||
|
}
|
||||||
|
fetch(form.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'X-CSRF-TOKEN': csrfToken,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((resp) => {
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error('Save failed');
|
||||||
|
}
|
||||||
|
syncCsrfToken(form);
|
||||||
|
updateStatusUI(row);
|
||||||
|
if (statusLabel) {
|
||||||
|
statusLabel.textContent = 'Saved';
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
if (statusLabel && statusLabel.textContent === 'Saved') {
|
||||||
|
statusLabel.textContent = '';
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (statusLabel) {
|
||||||
|
statusLabel.textContent = 'Error saving';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounce = (fn, wait) => {
|
||||||
|
let timer;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => fn(...args), wait);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedSave = debounce(saveRow, 500);
|
||||||
|
|
||||||
|
document.querySelectorAll('.exam-drafts-table tbody tr').forEach(updateRow);
|
||||||
|
|
||||||
|
document.addEventListener('change', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = target.closest('tr');
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target instanceof HTMLSelectElement && target.name === 'review_status') {
|
||||||
|
updateRow(row);
|
||||||
|
debouncedSave(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target instanceof HTMLInputElement && target.name === 'acceptance_type') {
|
||||||
|
debouncedSave(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('input', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof HTMLTextAreaElement) || target.name !== 'reviewer_comment') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = target.closest('tr');
|
||||||
|
if (row && target.value.endsWith('\n')) {
|
||||||
|
const lastCommitted = target.dataset.lastCommitted || '';
|
||||||
|
if (target.value !== lastCommitted) {
|
||||||
|
target.dataset.lastCommitted = target.value;
|
||||||
|
debouncedSave(row, { commitComment: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('blur', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof HTMLTextAreaElement) || target.name !== 'reviewer_comment') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = target.closest('tr');
|
||||||
|
if (row) {
|
||||||
|
const lastCommitted = target.dataset.lastCommitted || '';
|
||||||
|
if (target.value !== lastCommitted) {
|
||||||
|
target.dataset.lastCommitted = target.value;
|
||||||
|
debouncedSave(row, { commitComment: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
<?= $this->section('styles') ?>
|
<?= $this->section('styles') ?>
|
||||||
@@ -270,13 +567,15 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
.exam-drafts-table th {
|
.exam-drafts-table th {
|
||||||
min-width: 120px;
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.exam-drafts-table td {
|
.exam-drafts-table td {
|
||||||
max-width: 220px;
|
max-width: none;
|
||||||
}
|
}
|
||||||
.exam-drafts-table {
|
.exam-drafts-table {
|
||||||
table-layout: auto;
|
table-layout: auto;
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
.exam-drafts-table thead th {
|
.exam-drafts-table thead th {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
|
|||||||
@@ -144,58 +144,86 @@
|
|||||||
<span class="text-muted small"><?= count($entries) ?> record<?= count($entries) === 1 ? '' : 's' ?></span>
|
<span class="text-muted small"><?= count($entries) ?> record<?= count($entries) === 1 ? '' : 's' ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<?php if (empty($entries)): ?>
|
||||||
<table class="table table-bordered table-hover align-middle mb-0" data-no-mgmt-sticky>
|
<div class="text-center text-muted">No curriculum records yet.</div>
|
||||||
<thead class="table-light">
|
<?php else: ?>
|
||||||
<tr>
|
<?php
|
||||||
<th>Class</th>
|
$entriesByClass = [];
|
||||||
<th>Subject</th>
|
foreach ($entries as $entry) {
|
||||||
<th>Unit</th>
|
$className = ($entry['class_name'] ?? '') ?: '—';
|
||||||
<th>Unit title</th>
|
if (!isset($entriesByClass[$className])) {
|
||||||
<th>Chapter / Surah</th>
|
$entriesByClass[$className] = [];
|
||||||
<th>Updated</th>
|
}
|
||||||
<th style="min-width: 170px;">Actions</th>
|
$entriesByClass[$className][] = $entry;
|
||||||
</tr>
|
}
|
||||||
</thead>
|
?>
|
||||||
<tbody>
|
<div class="accordion" id="curriculumAccordion">
|
||||||
<?php if (empty($entries)): ?>
|
<?php $accIndex = 0; ?>
|
||||||
<tr>
|
<?php foreach ($entriesByClass as $className => $classEntries): ?>
|
||||||
<td colspan="7" class="text-center text-muted">No curriculum records yet.</td>
|
<?php
|
||||||
</tr>
|
$accIndex++;
|
||||||
<?php else: ?>
|
$collapseId = 'curriculum-class-' . $accIndex;
|
||||||
<?php foreach ($entries as $entry): ?>
|
$headingId = 'curriculum-heading-' . $accIndex;
|
||||||
<?php
|
?>
|
||||||
$subjectLabel = $subjectLabels[$entry['subject']] ?? ucfirst($entry['subject'] ?? '');
|
<div class="accordion-item mb-2">
|
||||||
$updatedAt = $entry['updated_at'] ?? $entry['created_at'] ?? '';
|
<h2 class="accordion-header" id="<?= esc($headingId) ?>">
|
||||||
$updatedDisplay = '';
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
|
||||||
if ($updatedAt) {
|
<?= esc($className) ?>
|
||||||
try {
|
<span class="badge bg-secondary ms-2"><?= count($classEntries) ?> entries</span>
|
||||||
$updatedDisplay = (new \DateTime($updatedAt))->format('M d, Y H:i');
|
</button>
|
||||||
} catch (\Exception $e) {
|
</h2>
|
||||||
$updatedDisplay = $updatedAt;
|
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="<?= esc($headingId) ?>" data-bs-parent="#curriculumAccordion">
|
||||||
}
|
<div class="accordion-body">
|
||||||
}
|
<div class="table-responsive">
|
||||||
?>
|
<table class="table table-bordered table-hover align-middle mb-0" data-no-mgmt-sticky>
|
||||||
<tr>
|
<thead class="table-light">
|
||||||
<td><?= esc(($entry['class_name'] ?? '') ?: '—') ?></td>
|
<tr>
|
||||||
<td><?= esc($subjectLabel) ?></td>
|
<th>Subject</th>
|
||||||
<td><?= $entry['unit_number'] ? esc((string)$entry['unit_number']) : '—' ?></td>
|
<th>Unit</th>
|
||||||
<td><?= esc(($entry['unit_title'] ?? '') ?: '—') ?></td>
|
<th>Unit title</th>
|
||||||
<td><?= esc(($entry['chapter_name'] ?? '') ?: '—') ?></td>
|
<th>Chapter / Surah</th>
|
||||||
<td><?= esc($updatedDisplay ?: '—') ?></td>
|
<th>Updated</th>
|
||||||
<td class="d-flex gap-2 flex-wrap">
|
<th style="min-width: 170px;">Actions</th>
|
||||||
<a href="<?= site_url('administrator/subject-curriculum/edit/' . $entry['id']) ?>" class="btn btn-sm btn-outline-primary">Edit</a>
|
</tr>
|
||||||
<form action="<?= site_url('administrator/subject-curriculum/delete/' . $entry['id']) ?>" method="post" onsubmit="return confirm('Remove this curriculum entry?');">
|
</thead>
|
||||||
<?= csrf_field() ?>
|
<tbody>
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
<?php foreach ($classEntries as $entry): ?>
|
||||||
</form>
|
<?php
|
||||||
</td>
|
$subjectLabel = $subjectLabels[$entry['subject']] ?? ucfirst($entry['subject'] ?? '');
|
||||||
</tr>
|
$updatedAt = $entry['updated_at'] ?? $entry['created_at'] ?? '';
|
||||||
<?php endforeach; ?>
|
$updatedDisplay = '';
|
||||||
<?php endif; ?>
|
if ($updatedAt) {
|
||||||
</tbody>
|
try {
|
||||||
</table>
|
$updatedDisplay = (new \DateTime($updatedAt))->format('M d, Y H:i');
|
||||||
</div>
|
} catch (\Exception $e) {
|
||||||
|
$updatedDisplay = $updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td><?= esc($subjectLabel) ?></td>
|
||||||
|
<td><?= $entry['unit_number'] ? esc((string)$entry['unit_number']) : '—' ?></td>
|
||||||
|
<td><?= esc(($entry['unit_title'] ?? '') ?: '—') ?></td>
|
||||||
|
<td><?= esc(($entry['chapter_name'] ?? '') ?: '—') ?></td>
|
||||||
|
<td><?= esc($updatedDisplay ?: '—') ?></td>
|
||||||
|
<td class="d-flex gap-2 flex-wrap">
|
||||||
|
<a href="<?= site_url('administrator/subject-curriculum/edit/' . $entry['id']) ?>" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||||
|
<form action="<?= site_url('administrator/subject-curriculum/delete/' . $entry['id']) ?>" method="post" onsubmit="return confirm('Remove this curriculum entry?');">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,7 +63,15 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="9" class="text-center text-muted">Loading...</td>
|
<td class="text-center text-muted">Loading...</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -198,10 +206,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (!teachers || teachers.length === 0) {
|
if (!teachers || teachers.length === 0) {
|
||||||
var emptyRow = document.createElement('tr');
|
var emptyRow = document.createElement('tr');
|
||||||
var emptyCell = document.createElement('td');
|
var emptyCell = document.createElement('td');
|
||||||
emptyCell.colSpan = 9;
|
|
||||||
emptyCell.className = 'text-center text-muted';
|
emptyCell.className = 'text-center text-muted';
|
||||||
emptyCell.textContent = 'No teachers found.';
|
emptyCell.textContent = 'No teachers found.';
|
||||||
emptyRow.appendChild(emptyCell);
|
emptyRow.appendChild(emptyCell);
|
||||||
|
for (var i = 0; i < 8; i++) {
|
||||||
|
emptyRow.appendChild(document.createElement('td'));
|
||||||
|
}
|
||||||
tbody.appendChild(emptyRow);
|
tbody.appendChild(emptyRow);
|
||||||
} else {
|
} else {
|
||||||
teachers.forEach(function (teacher, index) {
|
teachers.forEach(function (teacher, index) {
|
||||||
@@ -312,7 +322,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted">Loading...</td></tr>';
|
tbody.innerHTML = '<tr>'
|
||||||
|
+ '<td class="text-center text-muted">Loading...</td>'
|
||||||
|
+ '<td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>'
|
||||||
|
+ '</tr>';
|
||||||
|
|
||||||
var url = apiList + (selectedYear ? ('?schoolYear=' + encodeURIComponent(selectedYear)) : '');
|
var url = apiList + (selectedYear ? ('?schoolYear=' + encodeURIComponent(selectedYear)) : '');
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
|
|||||||
@@ -21,140 +21,329 @@
|
|||||||
$totalItems = max(0, (int)($summary['total_items'] ?? 0));
|
$totalItems = max(0, (int)($summary['total_items'] ?? 0));
|
||||||
?>
|
?>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="border rounded-3 p-3 mb-4 bg-light">
|
<?php
|
||||||
<div class="d-flex flex-wrap gap-4 align-items-center">
|
$termExamLabel = (isset($semester) && strtolower((string) $semester) === 'spring') ? 'Final' : 'Midterm';
|
||||||
<div>
|
$lowProgressSectionIds = $lowProgressSectionIds ?? [];
|
||||||
<div class="text-uppercase small text-muted">Submission completion</div>
|
$flaggedClasses = count($lowProgressSectionIds);
|
||||||
<div class="h4 fw-semibold mb-1"><?= esc($completionPercent) ?>%</div>
|
$pageNotifications = [];
|
||||||
<div class="progress" style="height:6px;">
|
if ($missingItemsCount > 0) {
|
||||||
<div
|
$pageNotifications[] = [
|
||||||
class="progress-bar bg-primary"
|
'level' => 'danger',
|
||||||
role="progressbar"
|
'message' => "{$missingItemsCount} missing item" . ($missingItemsCount === 1 ? '' : 's') . " awaiting teacher uploads.",
|
||||||
style="width: <?= esc($completionPercent) ?>%;"
|
];
|
||||||
aria-valuenow="<?= esc($completionPercent) ?>"
|
}
|
||||||
aria-valuemin="0"
|
if ($completionPercent < 70) {
|
||||||
aria-valuemax="100"
|
$pageNotifications[] = [
|
||||||
></div>
|
'level' => 'warning',
|
||||||
|
'message' => "Submission completion is below 70% — follow up with remaining teachers.",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($flaggedClasses > 0) {
|
||||||
|
$pageNotifications[] = [
|
||||||
|
'level' => 'warning',
|
||||||
|
'message' => "Progress is under 50% for {$flaggedClasses} class section" . ($flaggedClasses === 1 ? '' : 's') . ".",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (empty($rows)) {
|
||||||
|
$pageNotifications[] = [
|
||||||
|
'level' => 'info',
|
||||||
|
'message' => 'No class-section assignments submitted yet; encourage teachers to upload drafts.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (empty($pageNotifications)) {
|
||||||
|
$pageNotifications[] = [
|
||||||
|
'level' => 'info',
|
||||||
|
'message' => 'All tracked classes are current. Use the controls below to send reminders.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$examDraftDeadlineConfig = $examDraftDeadlineConfig ?? '';
|
||||||
|
$examDraftDeadlineFormatted = $examDraftDeadlineFormatted ?? '';
|
||||||
|
?>
|
||||||
|
<?php if (session()->getFlashdata('success')): ?>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<?= esc(session()->getFlashdata('success')) ?>
|
||||||
|
</div>
|
||||||
|
<?php elseif (session()->getFlashdata('warning')): ?>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<?= esc(session()->getFlashdata('warning')) ?>
|
||||||
|
</div>
|
||||||
|
<?php elseif (session()->getFlashdata('info')): ?>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<?= esc(session()->getFlashdata('info')) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
$groupedRows = [];
|
||||||
|
foreach ($rows as $idx => $row) {
|
||||||
|
$classKey = (string) ($row['class_section_id'] ?? $row['class_section'] ?? 'class_' . $idx);
|
||||||
|
$groupedRows[$classKey][] = $row;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="row g-3 mb-4 align-items-stretch">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="row g-3 summary-row">
|
||||||
|
<div class="col-12 col-sm-6 col-xl-3">
|
||||||
|
<div class="summary-card h-100 shadow-sm border-0">
|
||||||
|
<div class="summary-card-body">
|
||||||
|
<div class="summary-label text-uppercase">Completion</div>
|
||||||
|
<div class="summary-value"><?= esc($completionPercent) ?>%</div>
|
||||||
|
<div class="progress summary-progress">
|
||||||
|
<div
|
||||||
|
class="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: <?= esc($completionPercent) ?>%;"
|
||||||
|
aria-valuenow="<?= esc($completionPercent) ?>"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 col-xl-3">
|
||||||
|
<div class="summary-card h-100 shadow-sm border-0">
|
||||||
|
<div class="summary-card-body">
|
||||||
|
<div class="summary-label text-uppercase">Missing items</div>
|
||||||
|
<div class="summary-value <?= $missingItemsCount > 0 ? 'text-danger' : '' ?>"><?= esc($missingItemsCount) ?></div>
|
||||||
|
<div class="summary-note"><?= esc($submittedItems) ?> submitted / <?= esc($totalItems) ?> total</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 col-xl-3">
|
||||||
|
<div class="summary-card h-100 shadow-sm border-0">
|
||||||
|
<div class="summary-card-body">
|
||||||
|
<div class="summary-label text-uppercase">Submissions</div>
|
||||||
|
<div class="summary-value"><?= esc($submittedItems) ?></div>
|
||||||
|
<div class="summary-note"><?= esc($totalItems) ?> teachers tracked</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 col-xl-3">
|
||||||
|
<div class="summary-card h-100 shadow-sm border-0">
|
||||||
|
<div class="summary-card-body">
|
||||||
|
<div class="summary-label text-uppercase">Flagged</div>
|
||||||
|
<div class="summary-value"><?= esc($flaggedClasses) ?></div>
|
||||||
|
<div class="summary-note">Sections under 50% complete</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<div class="text-uppercase small text-muted">Missing items</div>
|
<div class="col-lg-4">
|
||||||
<div class="h4 fw-semibold text-danger mb-0"><?= esc($missingItemsCount) ?></div>
|
<div class="card page-notifications-card h-100 shadow-sm">
|
||||||
</div>
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<div class="text-muted small">
|
<span>Page notifications</span>
|
||||||
<?= esc($submittedItems) ?> submitted / <?= esc($totalItems) ?> total items
|
<span class="badge bg-light text-dark"><?= count($pageNotifications) ?> alerts</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="page-notifications-list mb-0">
|
||||||
|
<?php foreach ($pageNotifications as $notification): ?>
|
||||||
|
<li class="page-notification-item page-notification-<?= esc($notification['level']) ?>">
|
||||||
|
<span class="notification-indicator" aria-hidden="true"></span>
|
||||||
|
<span><?= esc($notification['message']) ?></span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="<?= site_url('administrator/teacher-submissions/notify') ?>">
|
<form method="post" action="<?= site_url('administrator/teacher-submissions/notify') ?>">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<div class="table-responsive">
|
<?php if (empty($rows)): ?>
|
||||||
<table
|
<p class="text-center text-muted mt-4">No teacher-class assignments found for this term.</p>
|
||||||
class="table table-striped table-bordered m-0 align-middle teacher-submissions-table"
|
<?php else: ?>
|
||||||
data-no-mgmt-sticky
|
<div class="table-control-bar mb-3 d-flex flex-wrap align-items-center gap-3">
|
||||||
data-no-dt-fixedheader
|
<div class="d-flex flex-wrap align-items-center gap-3">
|
||||||
>
|
<label class="form-check form-check-inline mb-0">
|
||||||
<thead class="table-light">
|
<input class="form-check-input" type="checkbox" name="notify_midterm_score" value="1">
|
||||||
<tr>
|
<span class="form-check-label small">Include <?= esc($termExamLabel) ?> score</span>
|
||||||
<th>Class Section</th>
|
</label>
|
||||||
<th>Teacher</th>
|
<label class="form-check form-check-inline mb-0">
|
||||||
<th class="text-center">Midterm Score</th>
|
<input class="form-check-input" type="checkbox" name="notify_midterm_comment" value="1">
|
||||||
<th class="text-center">Midterm Comment</th>
|
<span class="form-check-label small">Include <?= esc($termExamLabel) ?> comment</span>
|
||||||
<th class="text-center">Participation</th>
|
</label>
|
||||||
<th class="text-center">PTAP Comment</th>
|
<label class="form-check form-check-inline mb-0">
|
||||||
<th class="text-center">Notifications</th>
|
<input class="form-check-input" type="checkbox" name="notify_participation" value="1">
|
||||||
</tr>
|
<span class="form-check-label small">Include participation</span>
|
||||||
</thead>
|
</label>
|
||||||
<tbody>
|
<label class="form-check form-check-inline mb-0">
|
||||||
<?php if (!empty($rows)): ?>
|
<input class="form-check-input" type="checkbox" name="notify_ptap_comment" value="1">
|
||||||
<?php foreach ($rows as $row): ?>
|
<span class="form-check-label small">Include PTAP comment</span>
|
||||||
<tr>
|
</label>
|
||||||
<td><?= esc($row['class_section']) ?></td>
|
<label class="form-check form-check-inline mb-0">
|
||||||
<td>
|
<input class="form-check-input" type="checkbox" name="notify_class_progress" value="1">
|
||||||
<?php if (!empty($row['teachers'])): ?>
|
<span class="form-check-label small">Include class progress</span>
|
||||||
<?php foreach ($row['teachers'] as $teacher): ?>
|
</label>
|
||||||
<div><?= esc($teacher['label'] ?? 'Teacher') ?></div>
|
<label class="form-check form-check-inline mb-0">
|
||||||
<?php endforeach; ?>
|
<input class="form-check-input" type="checkbox" name="notify_exam_draft" value="1">
|
||||||
<?php else: ?>
|
<span class="form-check-label small">Include exam draft</span>
|
||||||
<span class="text-muted small">Unassigned</span>
|
</label>
|
||||||
<?php endif; ?>
|
<label class="form-check form-check-inline mb-0">
|
||||||
</td>
|
<input class="form-check-input" type="checkbox" name="homework_notify_all" value="1">
|
||||||
<?php foreach ([
|
<span class="form-check-label small">Include homework</span>
|
||||||
'midterm_score_status',
|
</label>
|
||||||
'midterm_comment_status',
|
</div>
|
||||||
'participation_status',
|
<div class="small text-muted">
|
||||||
'ptap_comment_status'
|
<span class="text-uppercase" style="font-size:0.65rem;">Exam draft deadline</span><br>
|
||||||
] as $statusKey): ?>
|
<?php if ($examDraftDeadlineConfig !== ''): ?>
|
||||||
<?php $status = $row[$statusKey] ?? ['label' => 'N/A', 'badge' => 'bg-secondary']; ?>
|
<?= esc($examDraftDeadlineConfig) ?>
|
||||||
<td class="text-center">
|
<?php if ($examDraftDeadlineFormatted !== ''): ?>
|
||||||
<span class="badge <?= esc($status['badge'] ?? 'bg-secondary') ?>">
|
<span class="text-muted"> → <?= esc($examDraftDeadlineFormatted) ?></span>
|
||||||
<?= esc($status['label'] ?? 'N/A') ?>
|
<?php endif; ?>
|
||||||
</span>
|
|
||||||
<?php if (!empty($status['detail'])): ?>
|
|
||||||
<div class="small text-muted"><?= esc($status['detail']) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<td>
|
|
||||||
<?php if (!empty($row['teachers'])): ?>
|
|
||||||
<?php $missingPayload = base64_encode(json_encode($row['missing_items'] ?? [])); ?>
|
|
||||||
<?php foreach ($row['teachers'] as $teacher): ?>
|
|
||||||
<?php $history = $notificationHistory[$row['class_section_id']][$teacher['id']] ?? []; ?>
|
|
||||||
<?php $lastEntry = $history[0] ?? null; ?>
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="d-flex align-items-center gap-2 mb-1">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="form-check-input"
|
|
||||||
name="notify[<?= esc($row['class_section_id']) ?>][<?= esc($teacher['id']) ?>]"
|
|
||||||
value="1"
|
|
||||||
/>
|
|
||||||
<span class="fw-semibold"><?= esc($teacher['label'] ?? 'Teacher') ?></span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="missing_items[<?= esc($row['class_section_id']) ?>][<?= esc($teacher['id']) ?>]"
|
|
||||||
value="<?= esc($missingPayload) ?>"
|
|
||||||
/>
|
|
||||||
<div class="small text-muted">
|
|
||||||
<?php if ($lastEntry !== null): ?>
|
|
||||||
<span>
|
|
||||||
Last <?= esc($lastEntry['status'] === 'sent' ? 'sent' : 'attempted') ?> on <?= esc($lastEntry['sent_at_text'] ?: 'N/A') ?>
|
|
||||||
by <?= esc($lastEntry['admin_name'] ?? '') ?>
|
|
||||||
</span>
|
|
||||||
<?php if (count($history) > 1): ?>
|
|
||||||
<div>History: <?= count($history) ?> entries</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<span>No notifications sent yet</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="text-muted small">No teacher assigned</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($row['missing_items'])): ?>
|
|
||||||
<div class="small text-danger mt-1">
|
|
||||||
Outstanding: <?= esc(implode(', ', $row['missing_items'])) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<tr>
|
<span class="text-warning">Not set</span>
|
||||||
<td colspan="7" class="text-center">No teacher-class assignments found for this term.</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
<div class="accordion" id="teacherSubmissionAccordion">
|
||||||
<div class="mt-3 text-end">
|
<?php foreach ($groupedRows as $sectionKey => $sectionRows): ?>
|
||||||
<button type="submit" class="btn btn-primary" <?= empty($rows) ? 'disabled' : '' ?>>
|
<?php
|
||||||
Send notifications to selected teachers
|
$firstRow = $sectionRows[0] ?? [];
|
||||||
</button>
|
$sectionLabel = esc($firstRow['class_section'] ?? 'Class section');
|
||||||
</div>
|
$collapseId = 'teacherSectionCollapse_' . md5($sectionKey);
|
||||||
|
$badgeStatus = count($sectionRows) === 1 ? 'submission' : 'submissions';
|
||||||
|
?>
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="heading_<?= esc($collapseId) ?>">
|
||||||
|
<button
|
||||||
|
class="accordion-button collapsed d-flex justify-content-between align-items-center"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#<?= esc($collapseId) ?>"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="<?= esc($collapseId) ?>"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<strong><?= $sectionLabel ?></strong>
|
||||||
|
<div class="small text-muted"><?= esc(count($sectionRows)) ?> <?= $badgeStatus ?></div>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-primary-subtle text-primary"><?= count($sectionRows) ?> rows</span>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="heading_<?= esc($collapseId) ?>">
|
||||||
|
<div class="accordion-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table
|
||||||
|
class="table table-striped table-bordered m-0 align-middle teacher-submissions-table mb-0"
|
||||||
|
data-no-mgmt-sticky
|
||||||
|
data-no-dt-fixedheader
|
||||||
|
>
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Class-Section</th>
|
||||||
|
<th>Teachers Name</th>
|
||||||
|
<th class="text-center"><?= esc($termExamLabel) ?> Score</th>
|
||||||
|
<th class="text-center"><?= esc($termExamLabel) ?> Comment</th>
|
||||||
|
<th class="text-center">Participation</th>
|
||||||
|
<th class="text-center">PTAP Comment</th>
|
||||||
|
<th class="text-center">Class Progress</th>
|
||||||
|
<th class="text-center">Exam Draft</th>
|
||||||
|
<th class="text-center">Homework</th>
|
||||||
|
<th class="text-center">Notifications</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($sectionRows as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= esc($row['class_section']) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if (!empty($row['teachers'])): ?>
|
||||||
|
<?php foreach ($row['teachers'] as $teacher): ?>
|
||||||
|
<div><?= esc($teacher['label'] ?? 'Teacher') ?></div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted small">Unassigned</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<?php foreach ([
|
||||||
|
'midterm_score_status',
|
||||||
|
'midterm_comment_status',
|
||||||
|
'participation_status',
|
||||||
|
'ptap_comment_status',
|
||||||
|
'class_progress_status',
|
||||||
|
'exam_draft_status',
|
||||||
|
] as $statusKey): ?>
|
||||||
|
<?php $status = $row[$statusKey] ?? ['label' => 'N/A', 'badge' => 'bg-secondary']; ?>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge <?= esc($status['badge'] ?? 'bg-secondary') ?>">
|
||||||
|
<?= esc($status['label'] ?? 'N/A') ?>
|
||||||
|
</span>
|
||||||
|
<?php if (!empty($status['detail'])): ?>
|
||||||
|
<div class="small text-muted"><?= esc($status['detail']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php $homeworkStatus = $row['homework_status'] ?? ['label' => 'N/A', 'badge' => 'bg-secondary']; ?>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge <?= esc($homeworkStatus['badge'] ?? 'bg-secondary') ?>">
|
||||||
|
<?= esc($homeworkStatus['label'] ?? 'N/A') ?>
|
||||||
|
</span>
|
||||||
|
<?php if (!empty($homeworkStatus['detail'])): ?>
|
||||||
|
<div class="small text-muted"><?= esc($homeworkStatus['detail']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if (!empty($row['teachers'])): ?>
|
||||||
|
<?php $missingPayload = base64_encode(json_encode($row['missing_items'] ?? [])); ?>
|
||||||
|
<?php foreach ($row['teachers'] as $teacher): ?>
|
||||||
|
<?php $history = $notificationHistory[$row['class_section_id']][$teacher['id']] ?? []; ?>
|
||||||
|
<?php $lastEntry = $history[0] ?? null; ?>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
name="notify[<?= esc($row['class_section_id']) ?>][<?= esc($teacher['id']) ?>]"
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
|
<span class="fw-semibold"><?= esc($teacher['label'] ?? 'Teacher') ?></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="missing_items[<?= esc($row['class_section_id']) ?>][<?= esc($teacher['id']) ?>]"
|
||||||
|
value="<?= esc($missingPayload) ?>"
|
||||||
|
/>
|
||||||
|
<div class="small text-muted">
|
||||||
|
<?php if ($lastEntry !== null): ?>
|
||||||
|
<span>
|
||||||
|
Last <?= esc($lastEntry['status'] === 'sent' ? 'sent' : 'attempted') ?> on <?= esc($lastEntry['sent_at_text'] ?: 'N/A') ?>
|
||||||
|
by <?= esc($lastEntry['admin_name'] ?? '') ?>
|
||||||
|
</span>
|
||||||
|
<?php if (count($history) > 1): ?>
|
||||||
|
<div>History: <?= count($history) ?> entries</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<span>No notifications sent yet</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted small">No teacher assigned</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($row['missing_items'])): ?>
|
||||||
|
<div class="small text-danger mt-1">
|
||||||
|
Outstanding: <?= esc(implode(', ', $row['missing_items'])) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-end">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Send notifications to selected teachers
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,5 +375,77 @@
|
|||||||
.card-body .table-responsive .teacher-submissions-table td {
|
.card-body .table-responsive .teacher-submissions-table td {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
background: var(--bs-white);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
.summary-card-body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
.summary-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.summary-value {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
.summary-note {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.summary-progress {
|
||||||
|
height: 6px;
|
||||||
|
margin-top: 0.65rem;
|
||||||
|
}
|
||||||
|
.summary-progress .progress-bar {
|
||||||
|
background: var(--bs-primary);
|
||||||
|
}
|
||||||
|
.page-notifications-card {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
.page-notifications-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.page-notification-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.page-notification-info .notification-indicator {
|
||||||
|
background: #0dcaf0;
|
||||||
|
}
|
||||||
|
.page-notification-warning .notification-indicator {
|
||||||
|
background: #ffc107;
|
||||||
|
}
|
||||||
|
.page-notification-danger .notification-indicator {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
.notification-indicator {
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.table-control-bar {
|
||||||
|
border: 1px solid rgba(0,0,0,0.08);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.table-control-bar .form-check-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-left: 0.15rem;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|||||||
@@ -107,7 +107,10 @@
|
|||||||
<td><?= esc($flag['flag_state']) ?></td>
|
<td><?= esc($flag['flag_state']) ?></td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<form id="flagForm_<?= $flag['id'] ?>" method="post">
|
<form id="flagForm_<?= $flag['id'] ?>" method="post"
|
||||||
|
action="<?= site_url('flags/update_state/' . (int) $flag['id']) ?>"
|
||||||
|
data-action-close="<?= site_url('flags/closeFlag/' . (int) $flag['id']) ?>"
|
||||||
|
data-action-cancel="<?= site_url('flags/cancelFlag/' . (int) $flag['id']) ?>">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
<select name="flag_state" class="form-select" id="flag_state_<?= $flag['id'] ?>"
|
<select name="flag_state" class="form-select" id="flag_state_<?= $flag['id'] ?>"
|
||||||
@@ -347,9 +350,9 @@
|
|||||||
|
|
||||||
// Set form action based on flag state
|
// Set form action based on flag state
|
||||||
if (flagState === "Closed") {
|
if (flagState === "Closed") {
|
||||||
form.action = `/flags/closeFlag/${currentFlagId}`;
|
form.action = form.dataset.actionClose || form.action;
|
||||||
} else if (flagState === "Canceled") {
|
} else if (flagState === "Canceled") {
|
||||||
form.action = `/flags/cancelFlag/${currentFlagId}`;
|
form.action = form.dataset.actionCancel || form.action;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Description set for form submission:", description); // For debugging
|
console.log("Description set for form submission:", description); // For debugging
|
||||||
@@ -357,6 +360,10 @@
|
|||||||
|
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('descriptionModal'));
|
const modal = bootstrap.Modal.getInstance(document.getElementById('descriptionModal'));
|
||||||
modal.hide();
|
modal.hide();
|
||||||
|
|
||||||
|
if (form && form.action) {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('flagStateDescription').addEventListener('input', function() {
|
document.getElementById('flagStateDescription').addEventListener('input', function() {
|
||||||
|
|||||||
@@ -11,9 +11,13 @@
|
|||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
<?= esc(ucfirst($semester ?? '')) ?> • <?= esc($schoolYear ?? '') ?>
|
<?= esc(ucfirst($semester ?? '')) ?> • <?= esc($schoolYear ?? '') ?>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>">
|
<?php if (!empty($canViewGrading)): ?>
|
||||||
Back to Grading
|
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>">
|
||||||
</a>
|
Back to Grading
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted small">You do not have access to the Grading page.</span>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
@@ -89,7 +93,7 @@
|
|||||||
<option value="Open" <?= ($row['status'] ?? 'Open') === 'Open' ? 'selected' : '' ?>>Open</option>
|
<option value="Open" <?= ($row['status'] ?? 'Open') === 'Open' ? 'selected' : '' ?>>Open</option>
|
||||||
<option value="Closed" <?= ($row['status'] ?? '') === 'Closed' ? 'selected' : '' ?>>Closed</option>
|
<option value="Closed" <?= ($row['status'] ?? '') === 'Closed' ? 'selected' : '' ?>>Closed</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" name="note" class="form-control form-control-sm" style="width: 140px;" placeholder="Note (optional)">
|
<input type="text" name="note" class="form-control form-control-sm" style="width: 140px;" placeholder="Note (optional)" value="<?= esc((string)($row['note'] ?? '')) ?>">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-secondary">Update</button>
|
<button type="submit" class="btn btn-sm btn-outline-secondary">Update</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ $todayYmd = local_date(utc_now(), 'Y-m-d');
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="sticky-col">Grade</th>
|
<th class="sticky-col">Grade</th>
|
||||||
<th class="sticky-col-2" style="min-width:300px; width:300px;">Teacher & TAs</th>
|
<th class="sticky-col-2" style="min-width:300px; width:300px;">Teacher & TAs</th>
|
||||||
<?php foreach (($headerDates ?? []) as $i => $label): $ymd = $sundays[$i] ?? ''; ?>
|
<th class="text-center">HW Submitted</th>
|
||||||
|
<?php foreach (($headerDates ?? []) as $i => $label): $ymd = $sundays[$i] ?? ''; ?>
|
||||||
<?php $isCal = !empty($eventDays[$ymd]); $isFuture = ($ymd > $todayYmd); ?>
|
<?php $isCal = !empty($eventDays[$ymd]); $isFuture = ($ymd > $todayYmd); ?>
|
||||||
<th class="text-center <?= $isCal ? 'bg-warning' : ($isFuture ? 'bg-future' : '') ?>" title="<?= esc($ymd) ?>"><?= esc($label) ?></th>
|
<th class="text-center <?= $isCal ? 'bg-warning' : ($isFuture ? 'bg-future' : '') ?>" title="<?= esc($ymd) ?>"><?= esc($label) ?></th>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -100,6 +101,9 @@ $todayYmd = local_date(utc_now(), 'Y-m-d');
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div><strong>TA<?= count($taNames) !== 1 ? 's' : '' ?>:</strong> <?= !empty($taNames) ? esc(implode(', ', $taNames)) : '—' ?></div>
|
<div><strong>TA<?= count($taNames) !== 1 ? 's' : '' ?>:</strong> <?= !empty($taNames) ? esc(implode(', ', $taNames)) : '—' ?></div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<?= (int)($homeworkSubmissionCounts[$csid] ?? 0) ?>
|
||||||
|
</td>
|
||||||
<?php foreach (($sundays ?? []) as $ymd): ?>
|
<?php foreach (($sundays ?? []) as $ymd): ?>
|
||||||
<?php if (!empty($eventDays[$ymd])): ?>
|
<?php if (!empty($eventDays[$ymd])): ?>
|
||||||
<td class="bg-warning text-center">—</td>
|
<td class="bg-warning text-center">—</td>
|
||||||
|
|||||||
@@ -7,79 +7,105 @@
|
|||||||
<div class="text-muted">Review the weekly reports your child’s teachers submit.</div>
|
<div class="text-muted">Review the weekly reports your child’s teachers submit.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end text-muted small">
|
<div class="text-end text-muted small">
|
||||||
Reports are grouped by Sunday; click any row to read the full details.
|
Reports are grouped by student; click a name to expand weekly details.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (! $hasSections): ?>
|
<?php if (! $hasStudents): ?>
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
We couldn’t find any current enrollment for your account. Once your child is assigned to a Sunday class, their teacher’s progress reports will appear here.
|
We couldn’t find any current enrollment for your account. Once your child is assigned to a Sunday class, their teacher’s progress reports will appear here.
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (empty($reportGroups)): ?>
|
<?php if (! empty($students)): ?>
|
||||||
<div class="alert alert-secondary">No reports submitted yet.</div>
|
<div class="accordion" id="parentProgressAccordion">
|
||||||
<?php else: ?>
|
<?php foreach ($students as $index => $student): ?>
|
||||||
<div class="card shadow-sm">
|
<?php
|
||||||
<div class="table-responsive">
|
$studentId = (int) ($student['student_id'] ?? 0);
|
||||||
<table class="table table-hover align-middle mb-0">
|
$studentName = trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''));
|
||||||
<thead class="table-light">
|
$studentName = $studentName !== '' ? $studentName : 'Student';
|
||||||
<tr>
|
$className = $student['class_section_name'] ?? '';
|
||||||
<th>Week</th>
|
$collapseId = 'student-progress-' . $studentId;
|
||||||
<th>Subjects</th>
|
$headingId = 'student-progress-heading-' . $studentId;
|
||||||
<th class="text-end">Details</th>
|
$reportGroups = $studentReportGroups[$studentId] ?? [];
|
||||||
</tr>
|
?>
|
||||||
</thead>
|
<div class="accordion-item">
|
||||||
<tbody>
|
<h2 class="accordion-header" id="<?= esc($headingId) ?>">
|
||||||
<?php foreach ($reportGroups as $group): ?>
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
|
||||||
<?php
|
<div class="d-flex flex-column flex-md-row align-items-md-center gap-1 gap-md-3">
|
||||||
$start = $group['week_start'] ?? '';
|
<span class="fw-semibold"><?= esc($studentName) ?></span>
|
||||||
$end = $group['week_end'] ?? '';
|
<?php if ($className !== ''): ?>
|
||||||
$weekLabel = $start ? date('M d, Y', strtotime($start)) : '-';
|
<span class="text-muted small">Class: <?= esc($className) ?></span>
|
||||||
if ($end) {
|
<?php endif; ?>
|
||||||
$weekLabel .= ' – ' . date('M d, Y', strtotime($end));
|
</div>
|
||||||
}
|
</button>
|
||||||
$reports = $group['reports'] ?? [];
|
</h2>
|
||||||
$exampleReport = $reports ? reset($reports) : null;
|
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="<?= esc($headingId) ?>" data-bs-parent="#parentProgressAccordion">
|
||||||
?>
|
<div class="accordion-body">
|
||||||
<tr>
|
<?php if (empty($reportGroups)): ?>
|
||||||
<td>
|
<div class="alert alert-secondary mb-0">No reports submitted yet.</div>
|
||||||
<div class="fw-semibold"><?= esc($weekLabel) ?></div>
|
<?php else: ?>
|
||||||
<?php if (!empty($group['class_section_name'])): ?>
|
<div class="table-responsive">
|
||||||
<div class="text-muted small">Class: <?= esc($group['class_section_name']) ?></div>
|
<table class="table table-hover align-middle mb-0">
|
||||||
<?php endif; ?>
|
<thead class="table-light">
|
||||||
</td>
|
<tr>
|
||||||
<td>
|
<th>Week</th>
|
||||||
<div class="d-flex flex-column gap-2">
|
<th>Subjects</th>
|
||||||
<?php foreach ($subjectSections as $slug => $section): ?>
|
<th class="text-end">Details</th>
|
||||||
<?php
|
</tr>
|
||||||
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
</thead>
|
||||||
$report = $reports[$subjectName] ?? null;
|
<tbody>
|
||||||
$statusLabel = $report ? ($report['status_label'] ?? 'Unknown') : 'No submission';
|
<?php foreach ($reportGroups as $group): ?>
|
||||||
$badgeClass = $report ? 'bg-secondary' : 'bg-light text-muted';
|
<?php
|
||||||
?>
|
$start = $group['week_start'] ?? '';
|
||||||
<div class="border rounded-3 p-2">
|
$end = $group['week_end'] ?? '';
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
$weekLabel = $start ? date('M d, Y', strtotime($start)) : '-';
|
||||||
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong>
|
if ($end) {
|
||||||
<span class="badge <?= esc($badgeClass) ?>"><?= esc($statusLabel) ?></span>
|
$weekLabel .= ' – ' . date('M d, Y', strtotime($end));
|
||||||
</div>
|
}
|
||||||
<div class="small text-muted">
|
$reports = $group['reports'] ?? [];
|
||||||
<?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?>
|
$exampleReport = $reports ? reset($reports) : null;
|
||||||
</div>
|
?>
|
||||||
</div>
|
<tr>
|
||||||
<?php endforeach; ?>
|
<td>
|
||||||
</div>
|
<div class="fw-semibold"><?= esc($weekLabel) ?></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td>
|
||||||
<?php if ($exampleReport): ?>
|
<div class="d-flex flex-column gap-2">
|
||||||
<a href="<?= base_url('parent/progress/view/' . $exampleReport['id']) ?>" class="btn btn-sm btn-outline-primary">View Weekly Details</a>
|
<?php foreach ($subjectSections as $slug => $section): ?>
|
||||||
<?php endif; ?>
|
<?php
|
||||||
</td>
|
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
||||||
</tr>
|
$report = $reports[$subjectName] ?? null;
|
||||||
<?php endforeach; ?>
|
$statusLabel = $report ? ($report['status_label'] ?? 'Unknown') : 'No submission';
|
||||||
</tbody>
|
$badgeClass = $report ? 'bg-secondary' : 'bg-light text-muted';
|
||||||
</table>
|
?>
|
||||||
</div>
|
<div class="border rounded-3 p-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong>
|
||||||
|
<span class="badge <?= esc($badgeClass) ?>"><?= esc($statusLabel) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted">
|
||||||
|
<?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<?php if ($exampleReport): ?>
|
||||||
|
<a href="<?= base_url('parent/progress/view/' . $exampleReport['id']) ?>" class="btn btn-sm btn-outline-primary">View Weekly Details</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,9 +42,6 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Event Description -->
|
|
||||||
<p class="card-text"><?= esc($event['description']) ?></p>
|
|
||||||
|
|
||||||
<!-- Participation Table -->
|
<!-- Participation Table -->
|
||||||
<form method="post" action="<?= site_url('parent/updateParticipation') ?>">
|
<form method="post" action="<?= site_url('parent/updateParticipation') ?>">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
@@ -53,11 +50,13 @@
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-bordered">
|
<table class="table table-sm table-bordered">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Student First Name</th>
|
<th>Student First Name</th>
|
||||||
<th>Student Last Name</th>
|
<th>Student Last Name</th>
|
||||||
<th class="text-center">Participate</th>
|
<th class="text-center">Participate</th>
|
||||||
</tr>
|
<th>Description</th>
|
||||||
|
<th>Event Fees</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($yourStudents as $student): ?>
|
<?php foreach ($yourStudents as $student): ?>
|
||||||
@@ -84,6 +83,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td><?= esc($event['description'] ?: 'No description') ?></td>
|
||||||
|
<td class="text-nowrap">$<?= esc(number_format((float) ($event['amount'] ?? 0), 2)) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -54,6 +54,11 @@
|
|||||||
<button class="btn btn-primary" onclick="window.print()">Print Report</button>
|
<button class="btn btn-primary" onclick="window.print()">Print Report</button>
|
||||||
<a id="summaryReportLink" href="<?= base_url('financial-report/financialReportSummary') ?>" class="btn btn-info">Display Summary Report</a>
|
<a id="summaryReportLink" href="<?= base_url('financial-report/financialReportSummary') ?>" class="btn btn-info">Display Summary Report</a>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($eventFeesTotal)): ?>
|
||||||
|
<div class="alert alert-info mb-3">
|
||||||
|
<strong>Event fees total:</strong> $<?= number_format((float)$eventFeesTotal, 2) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table id="invoicesTable" class="table table-bordered table-striped align-middle w-100">
|
<table id="invoicesTable" class="table table-bordered table-striped align-middle w-100">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="summaryBody">
|
<tbody id="summaryBody">
|
||||||
<tr><td>Total Charges</td><td class="text-right" id="sumCharges">$0.00</td></tr>
|
<tr><td>Total Charges</td><td class="text-right" id="sumCharges">$0.00</td></tr>
|
||||||
|
<tr><td>Event Fees</td><td class="text-right" id="sumEventFees">$0.00</td></tr>
|
||||||
<tr><td>Total Extra Charges</td><td class="text-right" id="sumExtraCharges">$0.00</td></tr>
|
<tr><td>Total Extra Charges</td><td class="text-right" id="sumExtraCharges">$0.00</td></tr>
|
||||||
<tr><td>Total Discounts</td><td class="text-right" id="sumDiscounts">$0.00</td></tr>
|
<tr><td>Total Discounts</td><td class="text-right" id="sumDiscounts">$0.00</td></tr>
|
||||||
<tr><td>Total Refunds</td><td class="text-right" id="sumRefunds">$0.00</td></tr>
|
<tr><td>Total Refunds</td><td class="text-right" id="sumRefunds">$0.00</td></tr>
|
||||||
@@ -98,6 +99,9 @@ function loadSummary(){
|
|||||||
if (!d || d.ok !== true) return;
|
if (!d || d.ok !== true) return;
|
||||||
document.getElementById('summaryPeriod').textContent = 'Report for School Year: ' + (d.schoolYear||'');
|
document.getElementById('summaryPeriod').textContent = 'Report for School Year: ' + (d.schoolYear||'');
|
||||||
document.getElementById('sumCharges').textContent = fmt(d.totalCharges);
|
document.getElementById('sumCharges').textContent = fmt(d.totalCharges);
|
||||||
|
if (document.getElementById('sumEventFees')) {
|
||||||
|
document.getElementById('sumEventFees').textContent = fmt(d.totalEventFees || 0);
|
||||||
|
}
|
||||||
if (document.getElementById('sumExtraCharges')) {
|
if (document.getElementById('sumExtraCharges')) {
|
||||||
document.getElementById('sumExtraCharges').textContent = fmt(d.totalExtraCharges || 0);
|
document.getElementById('sumExtraCharges').textContent = fmt(d.totalExtraCharges || 0);
|
||||||
}
|
}
|
||||||
@@ -127,10 +131,10 @@ function renderCharts(d){
|
|||||||
const summaryCtx = document.getElementById('summaryChart').getContext('2d');
|
const summaryCtx = document.getElementById('summaryChart').getContext('2d');
|
||||||
window._summaryChart = new Chart(summaryCtx, {
|
window._summaryChart = new Chart(summaryCtx, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: { labels: ['Charges','Paid','Unpaid','Discounts','Refunds','Expenses','Reimbursements','Net'],
|
data: { labels: ['Charges','Event Fees','Paid','Unpaid','Discounts','Refunds','Expenses','Reimbursements','Net'],
|
||||||
datasets: [{ label:'Amount (USD)', data:[
|
datasets: [{ label:'Amount (USD)', data:[
|
||||||
d.totalCharges||0, d.totalPaid||0, d.totalUnpaid||0, d.totalDiscounts||0, d.totalRefunds||0, d.totalExpenses||0, d.totalReimbursements||0, d.netAmount||0
|
d.totalCharges||0, d.totalEventFees||0, d.totalPaid||0, d.totalUnpaid||0, d.totalDiscounts||0, d.totalRefunds||0, d.totalExpenses||0, d.totalReimbursements||0, d.netAmount||0
|
||||||
], backgroundColor: ['#007bff','#28a745','#ffc107','#17a2b8','#ffc107','#dc3545','#6f42c1','#20c997']}] },
|
], backgroundColor: ['#007bff','#6610f2','#28a745','#ffc107','#17a2b8','#ffc107','#dc3545','#6f42c1','#20c997']}] },
|
||||||
options: { responsive:true, scales:{ y:{ beginAtZero:true }}}
|
options: { responsive:true, scales:{ y:{ beginAtZero:true }}}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,11 +181,12 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
window._summaryChart = new Chart(summaryCtx, {
|
window._summaryChart = new Chart(summaryCtx, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
labels: ['Charges', 'Paid', 'Unpaid', 'Discounts', 'Refunds', 'Expenses', 'Reimbursements', 'Net'],
|
labels: ['Charges', 'Event Fees', 'Paid', 'Unpaid', 'Discounts', 'Refunds', 'Expenses', 'Reimbursements', 'Net'],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Amount (USD)',
|
label: 'Amount (USD)',
|
||||||
data: [
|
data: [
|
||||||
<?= (float)$totalCharges ?>,
|
<?= (float)$totalCharges ?>,
|
||||||
|
<?= (float)($totalEventFees ?? 0) ?>,
|
||||||
<?= (float)$totalPaid ?>,
|
<?= (float)$totalPaid ?>,
|
||||||
<?= (float)$totalUnpaid ?>,
|
<?= (float)$totalUnpaid ?>,
|
||||||
<?= (float)$totalDiscounts ?>,
|
<?= (float)$totalDiscounts ?>,
|
||||||
@@ -191,7 +196,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
<?= (float)$netAmount ?>
|
<?= (float)$netAmount ?>
|
||||||
],
|
],
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
'#007bff', '#28a745', '#ffc107', '#17a2b8', '#ffc107', '#dc3545', '#6f42c1', '#20c997'
|
'#007bff', '#6610f2', '#28a745', '#ffc107', '#17a2b8', '#ffc107', '#dc3545', '#6f42c1', '#20c997'
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
/** @var string $school_year */
|
/** @var string $school_year */
|
||||||
/** @var array<string> $schoolYears */
|
/** @var array<string> $schoolYears */
|
||||||
/** @var array<int,array{parent_id:int,parent_name:string,email:string,total_invoice:float,total_balance:float,total_discount:float,total_paid:float,remaining_installments:int,installment_amount:float,type:string,has_installment?:int,next_installment?:string}> $rows */
|
/** @var array<int,array{parent_id:int,parent_name:string,email:string,total_invoice:float,total_balance:float,total_discount:float,total_paid:float,payment_count:int,remaining_installments:int,installment_amount:float,type:string,has_installment?:int,next_installment?:string}> $rows */
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?= $this->extend('layout/management_layout') ?>
|
<?= $this->extend('layout/management_layout') ?>
|
||||||
@@ -15,8 +15,6 @@
|
|||||||
.table thead th { background: var(--mgmt-thead-bg, #f1f3f5); }
|
.table thead th { background: var(--mgmt-thead-bg, #f1f3f5); }
|
||||||
.actions { white-space: nowrap; }
|
.actions { white-space: nowrap; }
|
||||||
.actions .btn { --bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; }
|
.actions .btn { --bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; }
|
||||||
.email-cell { max-width: 280px; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
@media (max-width: 576px){ .email-cell { max-width: 180px; } }
|
|
||||||
/* Disable sticky header for this table to avoid overlap */
|
/* Disable sticky header for this table to avoid overlap */
|
||||||
table.no-mgmt-sticky thead th { position: static !important; }
|
table.no-mgmt-sticky thead th { position: static !important; }
|
||||||
</style>
|
</style>
|
||||||
@@ -49,13 +47,15 @@
|
|||||||
<button class="btn btn-primary btn-sm" type="submit">Send Reminders (All)</button>
|
<button class="btn btn-primary btn-sm" type="submit">Send Reminders (All)</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<?php $sumEventFees = 0.0; ?>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table id="unpaidTable" class="table table-sm align-middle no-mgmt-sticky" data-no-mgmt-sticky>
|
<table id="unpaidTable" class="table table-sm align-middle no-mgmt-sticky" data-no-mgmt-sticky>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Parent</th>
|
<th>Parent</th>
|
||||||
<th>Email</th>
|
<th class="text-center">Nbr of Installements</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
|
<th class="text-end">Event Fees</th>
|
||||||
<th class="text-end">Invoice Amount</th>
|
<th class="text-end">Invoice Amount</th>
|
||||||
<th class="text-end">Applied Discount</th>
|
<th class="text-end">Applied Discount</th>
|
||||||
<th class="text-end">Paid Amount</th>
|
<th class="text-end">Paid Amount</th>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($rows)): ?>
|
<?php if (empty($rows)): ?>
|
||||||
<tr><td colspan="12" class="text-center text-muted py-4">No parents with outstanding balance.</td></tr>
|
<tr><td colspan="13" class="text-center text-muted py-4">No parents with outstanding balance.</td></tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php
|
<?php
|
||||||
$sumInvoice = 0.0;
|
$sumInvoice = 0.0;
|
||||||
@@ -86,20 +86,22 @@
|
|||||||
</a>
|
</a>
|
||||||
<small class="text-muted">#<?= (int)$r['parent_id'] ?></small>
|
<small class="text-muted">#<?= (int)$r['parent_id'] ?></small>
|
||||||
</td>
|
</td>
|
||||||
<td class="email-cell"><a href="mailto:<?= esc($r['email']) ?>"><?= esc($r['email']) ?></a></td>
|
<td class="text-center"><?= (int)($r['payment_count'] ?? 0) ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?php if (($r['type'] ?? '') === 'no_payment'): ?>
|
<?php if (($r['type'] ?? '') === 'no_payment'): ?>
|
||||||
<span class="badge bg-danger badge-type">no payment</span>
|
<span class="badge bg-danger badge-type">no payment</span>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="badge bg-success badge-type">installment</span>
|
<span class="badge bg-success badge-type">installment</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<?php $sumInvoice += (float)($r['total_invoice'] ?? 0); ?>
|
<?php $sumInvoice += (float)($r['total_invoice'] ?? 0); ?>
|
||||||
<?php $sumPaid += (float)($r['total_paid'] ?? 0); ?>
|
<?php $sumEventFees += (float)($r['event_fees'] ?? 0); ?>
|
||||||
|
<?php $sumPaid += (float)($r['total_paid'] ?? 0); ?>
|
||||||
<?php $sumDisc += (float)($r['total_discount'] ?? 0); ?>
|
<?php $sumDisc += (float)($r['total_discount'] ?? 0); ?>
|
||||||
<?php $sumInstAmt += (float)($r['installment_amount'] ?? 0); ?>
|
<?php $sumInstAmt += (float)($r['installment_amount'] ?? 0); ?>
|
||||||
<?php $sumBal += (float)($r['total_balance'] ?? 0); ?>
|
<?php $sumBal += (float)($r['total_balance'] ?? 0); ?>
|
||||||
<td class="text-end">$<?= number_format((float)($r['total_invoice'] ?? 0), 2) ?></td>
|
<td class="text-end">$<?= number_format((float)($r['event_fees'] ?? 0), 2) ?></td>
|
||||||
|
<td class="text-end">$<?= number_format((float)($r['total_invoice'] ?? 0), 2) ?></td>
|
||||||
<td class="text-end text-success">-$<?= number_format((float)($r['total_discount'] ?? 0), 2) ?></td>
|
<td class="text-end text-success">-$<?= number_format((float)($r['total_discount'] ?? 0), 2) ?></td>
|
||||||
<td class="text-end">$<?= number_format((float)($r['total_paid'] ?? 0), 2) ?></td>
|
<td class="text-end">$<?= number_format((float)($r['total_paid'] ?? 0), 2) ?></td>
|
||||||
<td class="text-center"><?= (int)($r['remaining_installments'] ?? 0) ?></td>
|
<td class="text-center"><?= (int)($r['remaining_installments'] ?? 0) ?></td>
|
||||||
@@ -131,6 +133,7 @@
|
|||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="3" class="text-end">Totals:</th>
|
<th colspan="3" class="text-end">Totals:</th>
|
||||||
|
<th class="text-end">$<?= number_format($sumEventFees, 2) ?></th>
|
||||||
<th class="text-end">$<?= number_format($sumInvoice, 2) ?></th>
|
<th class="text-end">$<?= number_format($sumInvoice, 2) ?></th>
|
||||||
<th class="text-end text-success">-$<?= number_format($sumDisc, 2) ?></th>
|
<th class="text-end text-success">-$<?= number_format($sumDisc, 2) ?></th>
|
||||||
<th class="text-end">$<?= number_format($sumPaid, 2) ?></th>
|
<th class="text-end">$<?= number_format($sumPaid, 2) ?></th>
|
||||||
|
|||||||
@@ -250,6 +250,30 @@
|
|||||||
modalInstance.show();
|
modalInstance.show();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderNameCell = (user) => {
|
||||||
|
const td = document.createElement('td');
|
||||||
|
const fullName = `${user?.firstname ?? ''} ${user?.lastname ?? ''}`.trim();
|
||||||
|
const label = fullName !== '' ? fullName : '—';
|
||||||
|
const roleList = Array.isArray(user?.roles)
|
||||||
|
? user.roles.map((role) => (role || '').toString().toLowerCase())
|
||||||
|
: [];
|
||||||
|
const isParent = roleList.includes('parent');
|
||||||
|
const uid = Number(user?.id || 0);
|
||||||
|
|
||||||
|
if (isParent && uid > 0) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = '#';
|
||||||
|
link.className = 'text-decoration-none';
|
||||||
|
link.setAttribute('data-family-guardian-id', String(uid));
|
||||||
|
link.textContent = label;
|
||||||
|
td.appendChild(link);
|
||||||
|
return td;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.textContent = label;
|
||||||
|
return td;
|
||||||
|
};
|
||||||
|
|
||||||
const renderTable = () => {
|
const renderTable = () => {
|
||||||
if (!tableBody) return;
|
if (!tableBody) return;
|
||||||
|
|
||||||
@@ -280,9 +304,7 @@
|
|||||||
accountCell.textContent = user.account_id ?? '';
|
accountCell.textContent = user.account_id ?? '';
|
||||||
row.appendChild(accountCell);
|
row.appendChild(accountCell);
|
||||||
|
|
||||||
const nameCell = document.createElement('td');
|
row.appendChild(renderNameCell(user));
|
||||||
nameCell.textContent = `${user.firstname ?? ''} ${user.lastname ?? ''}`.trim();
|
|
||||||
row.appendChild(nameCell);
|
|
||||||
|
|
||||||
const emailCell = document.createElement('td');
|
const emailCell = document.createElement('td');
|
||||||
emailCell.textContent = user.email ?? '';
|
emailCell.textContent = user.email ?? '';
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<?php if ($exampleReport): ?>
|
<?php if ($exampleReport): ?>
|
||||||
<a href="<?= base_url('teacher/progress/view/' . $exampleReport['id']) ?>" class="btn btn-sm btn-outline-primary">View Weekly Details</a>
|
<a href="<?= base_url('teacher/progress/view/' . $exampleReport['id']) ?>" class="btn btn-sm btn-outline-primary">View Weekly Details</a>
|
||||||
|
<a href="<?= base_url('teacher/progress/edit/' . $exampleReport['id']) ?>" class="btn btn-sm btn-outline-secondary ms-1">Edit</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<?= $this->extend('layout/main_layout') ?>
|
<?= $this->extend('layout/main_layout') ?>
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<?php
|
<?php
|
||||||
|
$isEdit = (bool) ($isEdit ?? false);
|
||||||
|
$formAction = $formAction ?? base_url('teacher/progress/store');
|
||||||
|
$submitLabel = $submitLabel ?? 'Submit Progress';
|
||||||
$hasClass = !empty($classSectionId);
|
$hasClass = !empty($classSectionId);
|
||||||
$assignedClassName = $classSectionName ?? '';
|
$assignedClassName = $classSectionName ?? '';
|
||||||
$sundayOptions = $sundayOptions ?? [];
|
$sundayOptions = $sundayOptions ?? [];
|
||||||
$defaultWeekStart = $defaultWeekStart ?? ($sundayOptions[0] ?? '');
|
$defaultWeekStart = $defaultWeekStart ?? ($sundayOptions[0] ?? '');
|
||||||
$weekStartSelected = set_value('week_start', $defaultWeekStart);
|
$weekStartSelected = set_value('week_start', $defaultWeekStart);
|
||||||
$weekEndValue = set_value('week_end');
|
$weekEndValue = set_value('week_end', $existingWeekEnd ?? '');
|
||||||
|
$existingReports = $existingReports ?? [];
|
||||||
if (!$weekEndValue && $weekStartSelected) {
|
if (!$weekEndValue && $weekStartSelected) {
|
||||||
try {
|
try {
|
||||||
$dt = new \DateTime($weekStartSelected);
|
$dt = new \DateTime($weekStartSelected);
|
||||||
@@ -31,9 +35,15 @@
|
|||||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3">
|
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-0">
|
<h3 class="mb-0">
|
||||||
<?= esc($classSectionName ? "Class {$classSectionName} Progress Submission" : 'Class Progress Submission') ?>
|
<?php if ($isEdit): ?>
|
||||||
|
<?= esc($classSectionName ? "Edit {$classSectionName} Progress" : 'Edit Class Progress') ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= esc($classSectionName ? "Class {$classSectionName} Progress Submission" : 'Class Progress Submission') ?>
|
||||||
|
<?php endif; ?>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="text-muted">Submit weekly progress for a single subject</div>
|
<div class="text-muted">
|
||||||
|
<?= $isEdit ? 'Update your weekly progress submission.' : 'Submit weekly progress for a single subject' ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="<?= base_url('teacher/progress/history') ?>" class="btn btn-outline-secondary">My Submissions</a>
|
<a href="<?= base_url('teacher/progress/history') ?>" class="btn btn-outline-secondary">My Submissions</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,6 +51,10 @@
|
|||||||
<?php if (session()->getFlashdata('success')): ?>
|
<?php if (session()->getFlashdata('success')): ?>
|
||||||
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php $overwritePrompt = session()->getFlashdata('confirm_overwrite'); ?>
|
||||||
|
<?php if (session()->getFlashdata('warning') && ! $overwritePrompt): ?>
|
||||||
|
<div class="alert alert-warning"><?= esc(session()->getFlashdata('warning')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (session()->getFlashdata('error')): ?>
|
<?php if (session()->getFlashdata('error')): ?>
|
||||||
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
|
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -54,16 +68,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<form action="<?= base_url('teacher/progress/store') ?>" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
|
<form action="<?= esc($formAction) ?>" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="class_section_id" value="<?= esc($classSectionId ?? '') ?>">
|
<input type="hidden" name="class_section_id" value="<?= esc($classSectionId ?? '') ?>">
|
||||||
|
<input type="hidden" name="confirm_overwrite" id="confirmOverwriteInput" value="0">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card shadow-sm mb-3">
|
<div class="card shadow-sm mb-3">
|
||||||
<div class="card-header bg-white d-flex flex-wrap align-items-center justify-content-between gap-3">
|
<div class="card-header bg-white d-flex flex-wrap align-items-center justify-content-between gap-3">
|
||||||
<strong class="mb-0">Date Selection</strong>
|
<strong class="mb-0">Date Selection</strong>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<select id="weekStartSelect" name="week_start" class="form-select form-select-sm" required>
|
<select id="weekStartSelect" name="week_start" class="form-select form-select-sm" required <?= $isEdit ? 'data-original-week="' . esc($weekStartSelected) . '"' : '' ?>>
|
||||||
<option value="">Select week</option>
|
<option value="">Select week</option>
|
||||||
<?php foreach ($sundayOptions as $sunday): ?>
|
<?php foreach ($sundayOptions as $sunday): ?>
|
||||||
<?php
|
<?php
|
||||||
@@ -96,8 +111,10 @@
|
|||||||
?>
|
?>
|
||||||
<?php foreach ($subjectSections as $slug => $section): ?>
|
<?php foreach ($subjectSections as $slug => $section): ?>
|
||||||
<?php
|
<?php
|
||||||
$unitValues = old("unit_$slug") ?? [];
|
$unitValues = old("unit_$slug") ?? ($existingReports[$slug]['unit_values'] ?? []);
|
||||||
$chapterValues = old("chapter_$slug") ?? [];
|
$chapterValues = old("chapter_$slug") ?? ($existingReports[$slug]['chapter_values'] ?? []);
|
||||||
|
$coveredValue = old("covered_$slug", $existingReports[$slug]['covered'] ?? '');
|
||||||
|
$homeworkValue = old("homework_$slug", $existingReports[$slug]['homework'] ?? '');
|
||||||
$rowsCount = max(count($unitValues), count($chapterValues));
|
$rowsCount = max(count($unitValues), count($chapterValues));
|
||||||
?>
|
?>
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
@@ -166,6 +183,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-text small text-muted">Add a custom Surah or Arabic target.</div>
|
<div class="form-text small text-muted">Add a custom Surah or Arabic target.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<?php elseif ($slug === 'islamic'): ?>
|
||||||
|
<div class="px-2 py-2 border-top">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" class="form-control" placeholder="Type subject or topic" data-custom-input data-subject="<?= esc($slug) ?>">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-custom-entry data-subject="<?= esc($slug) ?>">Add</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text small text-muted">Add a subject or unit not listed above (e.g. Seerah, Fiqh, Akhlaq).</div>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,11 +213,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label mb-1">What has been covered?</label>
|
<label class="form-label mb-1">What has been covered?</label>
|
||||||
<textarea name="covered_<?= esc($slug) ?>" class="form-control" rows="4" required placeholder="What was taught? Key topics, activities, memorization, etc."><?= esc(old("covered_$slug")) ?></textarea>
|
<textarea name="covered_<?= esc($slug) ?>" class="form-control" rows="4" required placeholder="What was taught? Key topics, activities, memorization, etc."><?= esc($coveredValue) ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label mb-1">Assigned homework:</label>
|
<label class="form-label mb-1">Assigned homework:</label>
|
||||||
<textarea name="homework_<?= esc($slug) ?>" class="form-control" rows="3" placeholder="Homework, practice quizzes, review pages"><?= esc(old("homework_$slug")) ?></textarea>
|
<textarea name="homework_<?= esc($slug) ?>" class="form-control" rows="3" placeholder="Homework, practice quizzes, review pages"><?= esc($homeworkValue) ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label mb-1">Attachment (optional):</label>
|
<label class="form-label mb-1">Attachment (optional):</label>
|
||||||
@@ -207,7 +232,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body d-flex flex-column">
|
<div class="card-body d-flex flex-column">
|
||||||
<button class="btn btn-primary w-100 mt-auto" type="submit" <?= $hasClass ? '' : 'disabled' ?>>Submit Progress</button>
|
<button class="btn btn-primary w-100 mt-auto" type="submit" <?= $hasClass ? '' : 'disabled' ?>><?= esc($submitLabel) ?></button>
|
||||||
<?php if (! $hasClass): ?>
|
<?php if (! $hasClass): ?>
|
||||||
<div class="text-muted small mt-2">
|
<div class="text-muted small mt-2">
|
||||||
You are not assigned to a class. Contact the administrator to submit progress.
|
You are not assigned to a class. Contact the administrator to submit progress.
|
||||||
@@ -224,12 +249,78 @@
|
|||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
<?= $this->section('scripts') ?>
|
<?= $this->section('scripts') ?>
|
||||||
|
<?php
|
||||||
|
$submitSuccess = session()->getFlashdata('success');
|
||||||
|
$submitError = session()->getFlashdata('error');
|
||||||
|
$overwriteWarning = session()->getFlashdata('warning');
|
||||||
|
$overwritePrompt = session()->getFlashdata('confirm_overwrite');
|
||||||
|
?>
|
||||||
|
<?php if ($submitSuccess || $submitError): ?>
|
||||||
|
<div class="modal fade" id="submissionStatusModal" tabindex="-1" aria-labelledby="submissionStatusLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="submissionStatusLabel">
|
||||||
|
<?= $submitSuccess ? 'Submission Successful' : 'Submission Failed' ?>
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<?= esc($submitSuccess ?: $submitError) ?>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($overwritePrompt && $overwriteWarning): ?>
|
||||||
|
<div class="modal fade" id="overwriteConfirmModal" tabindex="-1" aria-labelledby="overwriteConfirmLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="overwriteConfirmLabel">Override Existing Report</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<?= esc($overwriteWarning) ?>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="confirmOverwriteButton">Override</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
'use strict';
|
'use strict';
|
||||||
const forms = document.querySelectorAll('.needs-validation');
|
const forms = document.querySelectorAll('.needs-validation');
|
||||||
Array.from(forms).forEach(form => {
|
Array.from(forms).forEach(form => {
|
||||||
form.addEventListener('submit', event => {
|
form.addEventListener('submit', event => {
|
||||||
|
const originalWeek = weekStartSelect?.dataset.originalWeek || '';
|
||||||
|
if (originalWeek && weekStartSelect && weekStartSelect.value && weekStartSelect.value !== originalWeek) {
|
||||||
|
const ok = confirm('A progress report already exists for the original week. Change the week and override any existing report for the new date?');
|
||||||
|
if (!ok) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const confirmInput = document.getElementById('confirmOverwriteInput');
|
||||||
|
if (confirmInput) {
|
||||||
|
confirmInput.value = '1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const islamicUnits = form.querySelectorAll('input[name="unit_islamic[]"]');
|
||||||
|
const hasIslamicUnit = Array.from(islamicUnits).some(input => input.value.trim() !== '');
|
||||||
|
if (!hasIslamicUnit) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
alert('Please select at least one Islamic Studies unit.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!form.checkValidity()) {
|
if (!form.checkValidity()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -302,6 +393,9 @@
|
|||||||
if (isQuran) {
|
if (isQuran) {
|
||||||
return isCustom ? 'Custom' : 'Surah';
|
return isCustom ? 'Custom' : 'Surah';
|
||||||
}
|
}
|
||||||
|
if (subject === 'islamic' && isCustom) {
|
||||||
|
return 'Custom';
|
||||||
|
}
|
||||||
return parts.join(' – ');
|
return parts.join(' – ');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -362,12 +456,42 @@
|
|||||||
if (!input) return;
|
if (!input) return;
|
||||||
const customValue = input.value.trim();
|
const customValue = input.value.trim();
|
||||||
if (customValue === '') return;
|
if (customValue === '') return;
|
||||||
const unitValue = buildUnitDisplay(subject, '', '', { isQuran: subject === 'quran', isCustom: true });
|
const unitValue = buildUnitDisplay(subject, '', '', {
|
||||||
|
isQuran: subject === 'quran',
|
||||||
|
isCustom: subject === 'quran' || subject === 'islamic',
|
||||||
|
});
|
||||||
appendUnitChapterRow(subject, unitValue, customValue);
|
appendUnitChapterRow(subject, unitValue, customValue);
|
||||||
input.value = '';
|
input.value = '';
|
||||||
hideMenus();
|
hideMenus();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const statusModalEl = document.getElementById('submissionStatusModal');
|
||||||
|
if (statusModalEl && typeof bootstrap !== 'undefined') {
|
||||||
|
const statusModal = new bootstrap.Modal(statusModalEl);
|
||||||
|
statusModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
const overwriteModalEl = document.getElementById('overwriteConfirmModal');
|
||||||
|
if (overwriteModalEl && typeof bootstrap !== 'undefined') {
|
||||||
|
const overwriteModal = new bootstrap.Modal(overwriteModalEl);
|
||||||
|
overwriteModal.show();
|
||||||
|
const confirmButton = document.getElementById('confirmOverwriteButton');
|
||||||
|
const confirmInput = document.getElementById('confirmOverwriteInput');
|
||||||
|
if (confirmButton && confirmInput) {
|
||||||
|
confirmButton.addEventListener('click', () => {
|
||||||
|
confirmInput.value = '1';
|
||||||
|
const form = confirmButton.closest('form') || document.querySelector('form.needs-validation');
|
||||||
|
if (form && typeof form.requestSubmit === 'function') {
|
||||||
|
form.requestSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (form) {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|||||||
+287
-14
@@ -1,34 +1,307 @@
|
|||||||
<?= $this->extend('layout/main_layout') ?>
|
<?= $this->extend('layout/main_layout') ?>
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<div class="container-xxl py-5">
|
<?php
|
||||||
|
$statusBadges = $statusBadges ?? [];
|
||||||
|
$drafts = $drafts ?? [];
|
||||||
|
$legacyExams = $legacyExams ?? [];
|
||||||
|
$assignments = $assignments ?? [];
|
||||||
|
$selectedClassSection = $selectedClassSection ?? 0;
|
||||||
|
$examTypes = $examTypes ?? [];
|
||||||
|
$schoolYear = $schoolYear ?? '';
|
||||||
|
$semester = $semester ?? '';
|
||||||
|
$maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
.teacher-drafts-page,
|
||||||
|
.teacher-drafts-page * {
|
||||||
|
font-family: Arial, sans-serif !important;
|
||||||
|
}
|
||||||
|
.teacher-drafts-table {
|
||||||
|
table-layout: auto;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
.teacher-drafts-table th {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.teacher-drafts-table td {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
.teacher-drafts-table th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.teacher-drafts-table td.title-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="container-xxl py-5 teacher-drafts-page">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Drafts</h1>
|
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4">
|
||||||
<?php if (!empty($draftMessages)): ?>
|
<h1 class="mb-0">Exam drafts</h1>
|
||||||
|
<?php if (!empty($legacyExams)): ?>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="legacyToggle">Legacy Exams</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (session()->getFlashdata('success')): ?>
|
||||||
|
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (session()->getFlashdata('error')): ?>
|
||||||
|
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<?= form_open_multipart(base_url('teacher/exam-drafts'), ['class' => 'row g-3', 'id' => 'examDraftForm']) ?>
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="exam_type">Exam type <span class="text-danger">*</span></label>
|
||||||
|
<select name="exam_type" id="exam_type" class="form-select" required>
|
||||||
|
<option value="">— Select —</option>
|
||||||
|
<?php foreach ($examTypes as $t): ?>
|
||||||
|
<option value="<?= esc($t) ?>" <?= old('exam_type') === $t ? 'selected' : '' ?>><?= esc($t) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<label class="form-label" for="author_comment">Author comment</label>
|
||||||
|
<textarea name="author_comment" id="author_comment" class="form-control" rows="2" placeholder="Optional note for reviewers"><?= esc(old('author_comment') ?? '') ?></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label" for="draft_file">File (Word) <span class="text-danger">*</span></label>
|
||||||
|
<input type="file" name="draft_file" id="draft_file" class="form-control" accept=".doc,.docx" required>
|
||||||
|
<div class="form-text">Max <?= esc(number_format($maxUploadBytes / 1048576, 1)) ?> MB.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 d-flex flex-wrap gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Submit for review</button>
|
||||||
|
</div>
|
||||||
|
<?= form_close() ?>
|
||||||
|
<?php if ($schoolYear !== '' || $semester !== ''): ?>
|
||||||
|
<p class="text-muted small mb-0 mt-2">School year: <?= esc($schoolYear) ?> · Semester: <?= esc($semester) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$renderBadge = static function (string $status, array $badges): string {
|
||||||
|
$b = $badges[$status] ?? ['label' => $status, 'class' => 'bg-secondary text-white'];
|
||||||
|
$style = !empty($b['style']) ? ' style="' . esc($b['style']) . '"' : '';
|
||||||
|
return '<span class="badge ' . esc($b['class']) . ' js-status-badge" data-status="' . esc($status) . '"' . $style . '>' . esc($b['label']) . '</span>';
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
|
||||||
|
<h2 class="h5 mb-3">Your submissions</h2>
|
||||||
|
<?php if (!empty($drafts)): ?>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped align-middle teacher-drafts-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Subject</th>
|
<th>Class</th>
|
||||||
<th>Date</th>
|
<th>Title / type</th>
|
||||||
<th>Actions</th>
|
<th>Ver.</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>File link</th>
|
||||||
|
<th>Reviewer comment</th>
|
||||||
|
<th>Last Update</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($draftMessages as $message): ?>
|
<?php foreach ($drafts as $d): ?>
|
||||||
<tr>
|
<?php
|
||||||
<td><?= esc($message['subject']) ?></td>
|
$st = strtolower((string) ($d['status'] ?? ''));
|
||||||
<td><?= esc(!empty($message['created_at']) ? local_datetime($message['created_at'], 'm-d-Y H:i') : '') ?></td>
|
$badgeHtml = $renderBadge($st, $statusBadges);
|
||||||
|
?>
|
||||||
|
<tr data-draft-id="<?= (int) ($d['id'] ?? 0) ?>">
|
||||||
|
<td><?= esc($d['class_section_name'] ?? '') ?></td>
|
||||||
|
<td class="title-cell"><?= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?></td>
|
||||||
|
<td><?= esc((string) ($d['version'] ?? '')) ?></td>
|
||||||
|
<td><?= $badgeHtml ?></td>
|
||||||
<td>
|
<td>
|
||||||
<a href="<?= base_url('messages/edit/' . $message['id']) ?>">Edit</a> |
|
<?php $revNumber = max(1, (int) ($d['version'] ?? 1)); ?>
|
||||||
<a href="<?= base_url('messages/delete/' . $message['id']) ?>">Delete</a>
|
<?php if (!empty($d['teacher_file'])): ?>
|
||||||
|
<div>
|
||||||
|
<a href="<?= base_url('exam-drafts/files/teacher/' . $d['teacher_file']) ?>" target="_blank" rel="noopener">
|
||||||
|
<?= esc('Ver' . $revNumber . ' ' . ($d['teacher_filename'] ?? 'Submitted draft')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($d['final_file'])): ?>
|
||||||
|
<div class="mt-1">
|
||||||
|
<a href="<?= base_url('exam-drafts/files/final/' . $d['final_file']) ?>" target="_blank" rel="noopener" class="link-success small">
|
||||||
|
<?= esc('Final ' . ($d['final_filename'] ?? 'Final draft')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($d['review_files'])): ?>
|
||||||
|
<div class="mt-1">
|
||||||
|
<?php
|
||||||
|
$reviewLinks = [];
|
||||||
|
foreach ($d['review_files'] as $rf) {
|
||||||
|
$rev = max(1, (int) ($rf['review_revision'] ?? 1));
|
||||||
|
$name = $rf['final_filename'] ?? 'Review file';
|
||||||
|
$file = $rf['final_file'] ?? '';
|
||||||
|
if ($file !== '') {
|
||||||
|
$reviewLinks[] = '<a href="' . esc(base_url('exam-drafts/files/final/' . $file)) . '" target="_blank" rel="noopener" class="link-success small">' . esc('Ver' . $revNumber . '_' . $rev . ' ' . $name) . '</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?= !empty($reviewLinks) ? implode(' | ', $reviewLinks) : '' ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (empty($d['teacher_file']) && empty($d['final_file'])): ?>
|
||||||
|
—
|
||||||
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
<td><?= nl2br(esc($d['reviewer_comment'] ?? $d['reviewer_comments'] ?? $d['admin_comments'] ?? '—')) ?></td>
|
||||||
|
<td><?= esc(!empty($d['updated_at']) ? local_datetime($d['updated_at'], 'm-d-Y H:i') : '') ?></td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p>No drafts found.</p>
|
<p class="text-muted">No submissions yet.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($legacyExams)): ?>
|
||||||
|
<div id="legacySection" class="table-responsive d-none mt-4">
|
||||||
|
<table class="table table-sm table-striped align-middle teacher-drafts-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Class</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Ver.</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>School year</th>
|
||||||
|
<th>Semester</th>
|
||||||
|
<th>Exam type</th>
|
||||||
|
<th>Updated</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($legacyExams as $d): ?>
|
||||||
|
<?php $st = strtolower((string) ($d['status'] ?? '')); ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= esc($d['class_section_name'] ?? '') ?></td>
|
||||||
|
<td class="title-cell"><?= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?></td>
|
||||||
|
<td><?= esc((string) ($d['version'] ?? '')) ?></td>
|
||||||
|
<td><?= $renderBadge($st, $statusBadges) ?></td>
|
||||||
|
<td><?= esc($d['school_year'] ?? '') ?></td>
|
||||||
|
<td><?= esc($d['semester'] ?? '') ?></td>
|
||||||
|
<td><?= esc($d['exam_type'] ?? '') ?></td>
|
||||||
|
<td><?= esc(!empty($d['updated_at']) ? local_datetime($d['updated_at'], 'm-d-Y H:i') : '') ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if (!empty($d['final_file'])): ?>
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $d['final_file']) ?>" target="_blank" rel="noopener">View</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section('scripts') ?>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const csrfTokenName = <?= json_encode(csrf_token(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
|
const csrfCookieNames = <?= json_encode(array_values(array_filter([
|
||||||
|
config('Security')->csrfCookieName ?? null,
|
||||||
|
config('Security')->cookieName ?? null,
|
||||||
|
'csrf_cookie_name',
|
||||||
|
])), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
|
|
||||||
|
const readCookie = (name) => {
|
||||||
|
const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[$()*+.?[\\\]^{|}-]/g, '\\$&') + '=([^;]*)'));
|
||||||
|
return match ? decodeURIComponent(match[1]) : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncCsrfToken = (form) => {
|
||||||
|
let tokenValue = '';
|
||||||
|
csrfCookieNames.some((name) => {
|
||||||
|
tokenValue = readCookie(name);
|
||||||
|
return tokenValue !== '';
|
||||||
|
});
|
||||||
|
if (!tokenValue || !form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tokenInput = form.querySelector(`input[name="${csrfTokenName}"]`);
|
||||||
|
if (tokenInput) {
|
||||||
|
tokenInput.value = tokenValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const draftForm = document.getElementById('examDraftForm');
|
||||||
|
if (draftForm) {
|
||||||
|
draftForm.addEventListener('submit', () => syncCsrfToken(draftForm));
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusBadges = <?= json_encode($statusBadges, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||||
|
const rows = new Map();
|
||||||
|
document.querySelectorAll('tr[data-draft-id]').forEach((row) => {
|
||||||
|
const id = row.getAttribute('data-draft-id');
|
||||||
|
if (id) {
|
||||||
|
rows.set(id, row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateRow = (row, status, acceptanceType) => {
|
||||||
|
const badge = row.querySelector('.js-status-badge');
|
||||||
|
if (badge) {
|
||||||
|
const badgeData = statusBadges[status] || { label: status, class: 'bg-secondary text-white' };
|
||||||
|
badge.textContent = badgeData.label || status;
|
||||||
|
badge.className = `badge ${badgeData.class} js-status-badge`;
|
||||||
|
if (badgeData.style) {
|
||||||
|
badge.setAttribute('style', badgeData.style);
|
||||||
|
} else {
|
||||||
|
badge.removeAttribute('style');
|
||||||
|
}
|
||||||
|
badge.dataset.status = status;
|
||||||
|
}
|
||||||
|
const note = row.querySelector('.js-acceptance-note');
|
||||||
|
if (note) {
|
||||||
|
if (status === 'accepted' && acceptanceType) {
|
||||||
|
note.textContent = acceptanceType === 'minor_edits' ? 'With minor edits' : 'As is';
|
||||||
|
} else {
|
||||||
|
note.textContent = '—';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const poll = () => {
|
||||||
|
fetch('<?= base_url('teacher/exam-drafts/status') ?>', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||||
|
.then((resp) => resp.ok ? resp.json() : null)
|
||||||
|
.then((data) => {
|
||||||
|
if (!data || !Array.isArray(data.drafts)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.drafts.forEach((draft) => {
|
||||||
|
const row = rows.get(String(draft.id));
|
||||||
|
if (row) {
|
||||||
|
updateRow(row, draft.status, draft.acceptance_type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rows.size > 0) {
|
||||||
|
poll();
|
||||||
|
setInterval(poll, 15000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyToggle = document.getElementById('legacyToggle');
|
||||||
|
const legacySection = document.getElementById('legacySection');
|
||||||
|
if (legacyToggle && legacySection) {
|
||||||
|
legacyToggle.addEventListener('click', () => {
|
||||||
|
legacySection.classList.toggle('d-none');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|||||||
@@ -137,6 +137,9 @@
|
|||||||
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank">
|
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank">
|
||||||
<?= esc($draft['teacher_filename'] ?? 'Download submitted file') ?>
|
<?= esc($draft['teacher_filename'] ?? 'Download submitted file') ?>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank" class="btn btn-sm btn-outline-secondary ms-2">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="text-muted small">No file uploaded</div>
|
<div class="text-muted small">No file uploaded</div>
|
||||||
@@ -152,6 +155,9 @@
|
|||||||
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" class="link-success small">
|
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" class="link-success small">
|
||||||
<?= esc($draft['final_filename'] ?? 'Download final draft') ?>
|
<?= esc($draft['final_filename'] ?? 'Download final draft') ?>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" class="btn btn-sm btn-outline-secondary ms-2">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
@@ -199,6 +205,9 @@
|
|||||||
<a href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" class="btn btn-sm btn-outline-primary">
|
<a href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" class="btn btn-sm btn-outline-secondary ms-2">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="text-muted small">File missing</span>
|
<span class="text-muted small">File missing</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -308,6 +308,7 @@
|
|||||||
id="submitScoresLockBtn"
|
id="submitScoresLockBtn"
|
||||||
formaction="<?= base_url('/teacher/submit-scores-lock') ?>"
|
formaction="<?= base_url('/teacher/submit-scores-lock') ?>"
|
||||||
formmethod="post"
|
formmethod="post"
|
||||||
|
data-confirm-message="Once you submit, you cannot add any scores or comments. Do you want to continue?"
|
||||||
<?= $scoresLocked ? 'disabled' : '' ?>>
|
<?= $scoresLocked ? 'disabled' : '' ?>>
|
||||||
<?= $scoresLocked ? 'Scores Locked' : 'Submit Semester Scores' ?>
|
<?= $scoresLocked ? 'Scores Locked' : 'Submit Semester Scores' ?>
|
||||||
</button>
|
</button>
|
||||||
@@ -430,6 +431,14 @@
|
|||||||
|
|
||||||
form.addEventListener('submit', function(event) {
|
form.addEventListener('submit', function(event) {
|
||||||
syncCsrfToken();
|
syncCsrfToken();
|
||||||
|
const submitter = event.submitter;
|
||||||
|
if (submitter && submitter.id === 'submitScoresLockBtn') {
|
||||||
|
const message = submitter.dataset.confirmMessage || 'Submit semester scores?';
|
||||||
|
if (!confirm(message)) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const errors = [];
|
const errors = [];
|
||||||
form.querySelectorAll('textarea[data-first-name]').forEach(function(field) {
|
form.querySelectorAll('textarea[data-first-name]').forEach(function(field) {
|
||||||
const value = field.value.trim();
|
const value = field.value.trim();
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<?= $this->extend('layout/register_layout') ?>
|
||||||
|
<?= $this->section('content') ?>
|
||||||
|
|
||||||
|
<div class="registration-form container mt-5 mb-5">
|
||||||
|
<form method="post" action="<?= base_url('set_authorized_user_password/' . $userId) ?>" onsubmit="return validatePassword()" autocomplete="off">
|
||||||
|
<?= csrf_field(); ?>
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<a href="<?= base_url('/') ?>">
|
||||||
|
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt="" style="width: 180px; height: 120px;">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-center text-success" style="font-family: Arial, sans-serif;">Create Your Password</h3>
|
||||||
|
<br>
|
||||||
|
<input type="hidden" name="user_id" value="<?= esc($userId); ?>" required>
|
||||||
|
<input type="hidden" name="token" value="<?= esc($token ?? ''); ?>" required>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<input type="password"
|
||||||
|
class="form-control item"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Enter new password"
|
||||||
|
maxlength="40"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
oncopy="return false"
|
||||||
|
oncut="return false"
|
||||||
|
onpaste="return false">
|
||||||
|
<span class="toggle-password" onclick="togglePassword('password', this)">
|
||||||
|
<i class="fa-solid fa-eye"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<small id="passwordHelp" class="text-danger d-none">
|
||||||
|
Password must be at least 8 characters long, contain a number, an uppercase letter, a lowercase letter, and one special character: @, -, =, +, *, #, $, %, &, !
|
||||||
|
</small>
|
||||||
|
<small id="passwordCopyWarning" class="text-muted d-none">
|
||||||
|
Copy and paste are disabled for security reasons.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<input type="password"
|
||||||
|
class="form-control item"
|
||||||
|
id="password_confirm"
|
||||||
|
name="password_confirm"
|
||||||
|
placeholder="Confirm new password"
|
||||||
|
maxlength="40"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
oncopy="return false"
|
||||||
|
oncut="return false"
|
||||||
|
onpaste="return false">
|
||||||
|
<span class="toggle-password" onclick="togglePassword('password_confirm', this)">
|
||||||
|
<i class="fa-solid fa-eye"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<small id="confirmPasswordHelp" class="text-danger d-none">
|
||||||
|
Passwords do not match.
|
||||||
|
</small>
|
||||||
|
<small id="confirmCopyWarning" class="text-muted d-none">
|
||||||
|
Copy and paste are disabled for security reasons.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 d-grid">
|
||||||
|
<button type="submit" class="btn btn-success item">Save Password</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section('scripts') ?>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const showWarning = (inputId, warningId) => {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const warning = document.getElementById(warningId);
|
||||||
|
|
||||||
|
['copy', 'paste', 'cut'].forEach(eventName => {
|
||||||
|
input.addEventListener(eventName, (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
warning.classList.remove('d-none');
|
||||||
|
|
||||||
|
if (warning.timeout) clearTimeout(warning.timeout);
|
||||||
|
warning.timeout = setTimeout(() => {
|
||||||
|
warning.classList.add('d-none');
|
||||||
|
}, 4000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
showWarning('password', 'passwordCopyWarning');
|
||||||
|
showWarning('password_confirm', 'confirmCopyWarning');
|
||||||
|
});
|
||||||
|
|
||||||
|
function togglePassword(fieldId, iconContainer) {
|
||||||
|
const input = document.getElementById(fieldId);
|
||||||
|
const icon = iconContainer.querySelector('i');
|
||||||
|
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
icon.classList.remove('fa-eye');
|
||||||
|
icon.classList.add('fa-eye-slash');
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
icon.classList.remove('fa-eye-slash');
|
||||||
|
icon.classList.add('fa-eye');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePassword() {
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const passwordConfirm = document.getElementById('password_confirm').value;
|
||||||
|
const passwordHelp = document.getElementById('passwordHelp');
|
||||||
|
const confirmPasswordHelp = document.getElementById('confirmPasswordHelp');
|
||||||
|
|
||||||
|
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\-=\+*#$%&!?])[A-Za-z\d@\-=\+*#$%&!?]{8,}$/;
|
||||||
|
|
||||||
|
let valid = true;
|
||||||
|
|
||||||
|
if (!passwordRegex.test(password)) {
|
||||||
|
passwordHelp.classList.remove('d-none');
|
||||||
|
valid = false;
|
||||||
|
} else {
|
||||||
|
passwordHelp.classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== passwordConfirm) {
|
||||||
|
confirmPasswordHelp.classList.remove('d-none');
|
||||||
|
valid = false;
|
||||||
|
} else {
|
||||||
|
confirmPasswordHelp.classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
+253
-258
@@ -1,259 +1,254 @@
|
|||||||
Grade,Unit,Unit Title,chapter
|
Grade,Unit,Unit Title,chapter
|
||||||
1,1,Aqaid: Our Belief,Allahﷻ: Our Creator
|
1,1,Aqaid: Our Belief,1. Allah: Our Creator
|
||||||
1,1,Aqaid: Our Belief,Islam
|
1,1,Aqaid: Our Belief,2. Islam
|
||||||
1,1,Aqaid: Our Belief,Our Faith
|
1,1,Aqaid: Our Belief,3. Our Faith
|
||||||
1,1,Aqaid: Our Belief,Nabi Muhammadﷺ
|
1,1,Aqaid: Our Belief,4. Nabi Muhammad (s)
|
||||||
1,1,Aqaid: Our Belief,The Qur’an
|
1,1,Aqaid: Our Belief,5. The Qur’an
|
||||||
1,2,Knowing Allahﷻ,Allahﷻ Loves Us
|
1,2,Knowing Allah,6. Allah Loves Us
|
||||||
1,2,Knowing Allahﷻ,Remembering Allahﷻ
|
1,2,Knowing Allah,7. Remembering Allah
|
||||||
1,2,Knowing Allahﷻ,Allahﷻ Rewards Us
|
1,2,Knowing Allah,8. Allah Rewards Us
|
||||||
1,3,Our Ibadat,Five Pillars of Islam
|
1,3,Our Ibadat,9. Five Pillars of Islam
|
||||||
1,3,Our Ibadat,Shahadah: The First Pillar
|
1,3,Our Ibadat,10. Shahadah: The First Pillar
|
||||||
1,3,Our Ibadat,Salah: The Second Pillar
|
1,3,Our Ibadat,11. Salat: The Second Pillar
|
||||||
1,3,Our Ibadat,Zakat: The Third Pillar
|
1,3,Our Ibadat,12. Zakat: The Third Pillar
|
||||||
1,3,Our Ibadat,Fasting: The Fourth Pillar
|
1,3,Our Ibadat,13. Fasting: The Fourth Pillar
|
||||||
1,3,Our Ibadat,Hajj: The Fifth Pillar
|
1,3,Our Ibadat,14. Hajj: The Fifth Pillar
|
||||||
1,4,Messengers of Allah,Adam (A): The First Nabi
|
1,4,Messengers of Allah,15. Adam (A): The First Nabi
|
||||||
1,4,Messengers of Allah,Nuh (A): Saved From Flood
|
1,4,Messengers of Allah,16. Nuh (A): Saved From the Great Flood
|
||||||
1,4,Messengers of Allah,Ibrahim (A): Never Listen to Shaitan
|
1,4,Messengers of Allah,17. Ibrahim (A): Never Listen to Shaitan
|
||||||
1,4,Messengers of Allah,Musa (A): Challenging A Bad Ruler
|
1,4,Messengers of Allah,18. Musa (A): Challenging a Bad Ruler
|
||||||
1,4,Messengers of Allah,Isa (A): A Great Nabi of Allahﷻ
|
1,4,Messengers of Allah,19. Isa (A): A Great Nabi of Allah
|
||||||
1,5,Other Basics of Islam,Angels: They Always Work for Allahﷻ
|
1,5,Other Basics of Islam,20. Angels: They Always Work for Allah
|
||||||
1,5,Other Basics of Islam,Shaitan: Our Enemy
|
1,5,Other Basics of Islam,21. Shaitan: Our Enemy
|
||||||
1,5,Other Basics of Islam,Makkah and Madinah
|
1,5,Other Basics of Islam,22. Makkah and Madinah
|
||||||
1,5,Other Basics of Islam,Eid: Two Festivals
|
1,5,Other Basics of Islam,23. Eid: Two Festivals
|
||||||
1,6,Akhlaq and Adab in Islam,Good Manners
|
1,6,Akhlaq and Adab in Islam,24. Good Manners
|
||||||
1,6,Akhlaq and Adab in Islam,Kindness and Sharing
|
1,6,Akhlaq and Adab in Islam,25. Kindness and Sharing
|
||||||
1,6,Akhlaq and Adab in Islam,Respect
|
1,6,Akhlaq and Adab in Islam,26. Respect
|
||||||
1,6,Akhlaq and Adab in Islam,Forgiveness
|
1,6,Akhlaq and Adab in Islam,27. Forgiveness
|
||||||
1,6,Akhlaq and Adab in Islam,Thanking Allahﷻ
|
1,6,Akhlaq and Adab in Islam,28. Thanking Allah
|
||||||
2,1,The Creator—His Message,Allahﷻ: Our Creator
|
2,1,The Creator and His Message,1. Allah: Our Creator
|
||||||
2,1,The Creator—His Message,How Does Allahﷻ Create?
|
2,1,The Creator and His Message,2. How Does Allah Create?
|
||||||
2,1,The Creator—His Message,Allahﷻ: What Does He Do?
|
2,1,The Creator and His Message,3. What Does Allah Do?
|
||||||
2,1,The Creator—His Message,What Does Allahﷻ Not Do
|
2,1,The Creator and His Message,4, Allah: What Does He Not Do
|
||||||
2,1,The Creator—His Message,The Qur’an
|
2,1,The Creator and His Message,5. The Qur’an
|
||||||
2,1,The Creator—His Message,Hadith and Sunnah
|
2,1,The Creator and His Message,6. Hadith and Sunnah
|
||||||
2,2,Our Ibadat,Shahadah: The First Pillar
|
2,2,Our Ibadat,7. Shahadah: The First Pillar
|
||||||
2,2,Our Ibadat,Salah: The Second Pillar
|
2,2,Our Ibadat,8. Salat: The Second Pillar
|
||||||
2,2,Our Ibadat,Zakah: The Third Pillar
|
2,2,Our Ibadat,9. Zakah: The Third Pillar
|
||||||
2,2,Our Ibadat,Sawm: The Fourth Pillar
|
2,2,Our Ibadat,10. Sawm: The Fourth Pillar
|
||||||
2,2,Our Ibadat,Hajj: The Fifth Pillar
|
2,2,Our Ibadat,11. Hajj: The Fifth Pillar
|
||||||
2,2,Our Ibadat,Wudu: Keeping Our Bodies Clean
|
2,2,Our Ibadat,12. Wudu: Cleaning Before Salat
|
||||||
2,3,Messengers of Allah,Ibrahim (A): A Friend of Allah
|
2,3,The Messengers of Allah,13. Ibrahim (A): A Friend of Allah
|
||||||
2,3,Messengers of Allah,Yaqub (A) and Yusuf (A)
|
2,3,The Messengers of Allah,14. Yaqub (A) and Yusuf (A)
|
||||||
2,3,Messengers of Allah,Musa (A) and Harun (A)
|
2,3,The Messengers of Allah,15. Musa (A) and Harun (A)
|
||||||
2,3,Messengers of Allah,Yunus (A)
|
2,3,The Messengers of Allah,16. Yunus (A)
|
||||||
2,3,Messengers of Allah,Nabi Muhammadﷺ
|
2,3,The Messengers of Allah,17. Nabi Muhammad ﷺ
|
||||||
2,4,Learning About Islam,Obey Allahﷻ Obey Rasulﷺ
|
2,4,Learning About Islam,18. Obey Allah, Obey Rasul ﷺ
|
||||||
2,4,Learning About Islam,Day of Judgment
|
2,4,Learning About Islam,19. Day of Judgment
|
||||||
2,4,Learning About Islam,Our Masjid
|
2,4,Learning About Islam,20. Our Masjid
|
||||||
2,4,Learning About Islam,Islamic Phrases
|
2,4,Learning About Islam,21. Islamic Phrases
|
||||||
2,4,Learning About Islam,Food that We May Eat
|
2,4,Learning About Islam,22. Food that We May Eat
|
||||||
2,5,Akhlaq and Adab in Islam,Truthfulness
|
2,5,Akhlaq and Adab in Islam,23. Truthfulness
|
||||||
2,5,Akhlaq and Adab in Islam,Kindness
|
2,5,Akhlaq and Adab in Islam,24. Kindness
|
||||||
2,5,Akhlaq and Adab in Islam,Respect
|
2,5,Akhlaq and Adab in Islam,25. Respect
|
||||||
2,5,Akhlaq and Adab in Islam,Responsibility
|
2,5,Akhlaq and Adab in Islam,26. Responsibility
|
||||||
2,5,Akhlaq and Adab in Islam,Obedience
|
2,5,Akhlaq and Adab in Islam,27. Obedience
|
||||||
2,5,Akhlaq and Adab in Islam,Cleanliness
|
2,5,Akhlaq and Adab in Islam,28. Cleanliness
|
||||||
2,5,Akhlaq and Adab in Islam,Honesty
|
2,5,Akhlaq and Adab in Islam,29. Honesty
|
||||||
3,1,Knowing About Allah,What Does Allahﷻ Do?
|
3,1,Knowing About Allāh ﷺ,1. Who Is Allāh ﷺ?
|
||||||
3,1,Knowing About Allah,What Allahﷻ Is and Is Not
|
3,1,Knowing About Allāh ﷺ,2. What Allāh ﷺ Is and Is Not
|
||||||
3,1,Knowing About Allah,Allahﷻ: The Most-Merciful
|
3,1,Knowing About Allāh ﷺ,3. Allāh ﷺ: The Most-Merciful, Most-Rewarding
|
||||||
3,1,Knowing About Allah,Allahﷻ: The Best Judge
|
3,1,Knowing About Allāh ﷺ,4. Allāh ﷺ: The Best Judge
|
||||||
3,2,What Islam Says,We Are Muslims: We Have ‘Iman
|
3,1,Knowing About Allāh ﷺ,5. What Does Allāh ﷺ Want Us to Do?
|
||||||
3,2,What Islam Says,What Does Allahﷻ Want Us to Do?
|
3,2,Teachings of Islam,6. We Are Muslims: We Have ‘Īmān
|
||||||
3,2,What Islam Says,Hadith
|
3,2,Teachings of Islam,7. Belief in the Qur’ān
|
||||||
3,2,What Islam Says,Jinn
|
3,2,Teachings of Islam,8. Belief in the Messengers
|
||||||
3,2,What Islam Says,Muslims in North America
|
3,2,Teachings of Islam,9. Hadīth and Sunnah
|
||||||
3,2,What Islam Says,The Right Path: The Straight Path
|
3,2,Teachings of Islam,10. Jinn
|
||||||
3,3,Why Do We Worship,Shahadah: Allahﷻ is One
|
3,2,Teachings of Islam,11. Muslims in North America
|
||||||
3,3,Why Do We Worship,Types of Salat
|
3,2,Teachings of Islam,12. The Straight Path: The Right Path
|
||||||
3,3,Why Do We Worship,Why We Make Salat
|
3,3,Nabi Muhammad ﷺ,13. Kindness of Rasūlullāh ﷺ
|
||||||
3,3,Why Do We Worship,Why Do We pay Zakat?
|
3,3,Nabi Muhammad ﷺ,14. How Rasūlullāh ﷺ Treated Others
|
||||||
3,3,Why Do We Worship,Why Do We Fast?
|
3,3,Nabi Muhammad ﷺ,15. Our Relationship With Rasūlullāh ﷺ
|
||||||
3,3,Why Do We Worship,Why Do We Go for Hajj?
|
3,4,Messengers of Allāh ﷺ,16. Ismā‘īl (A) and Ishāq (A): Nabi of Allāh ﷺ
|
||||||
3,4,Life of Nabi Muhammadﷺ,The Nabiﷺ in Makkah
|
3,4,Messengers of Allāh ﷺ,17. Shu‘aib (A): A Nabi of Allāh ﷺ
|
||||||
3,4,Life of Nabi Muhammadﷺ,The Nabiﷺ in Madinah
|
3,4,Messengers of Allāh ﷺ,18. Dāwūd (A): A Nabi of Allāh ﷺ
|
||||||
3,4,Life of Nabi Muhammadﷺ,How Rasulullahﷺ Treated Others
|
3,4,Messengers of Allāh ﷺ,19. ‘Īsā (A): A Nabi of Allāh ﷺ
|
||||||
3,5,Messengers of Allah,Isma‘il (A) and Ishaq (A)
|
3,5,Learning About Islam,20. The Ka‘bah
|
||||||
3,5,Messengers of Allah,Dawud (A): A Nabi of Allahﷻ
|
3,5,Learning About Islam,21. Masjid an-Nabawī: The Nabi’s Masjid
|
||||||
3,5,Messengers of Allah,‘Isa (A): A Nabi of Allahﷻ
|
3,5,Learning About Islam,22. Bilāl ibn Rabāh
|
||||||
3,6,Akhlaq and Adab in Islam,Being Kind: A Virtue of the Believers
|
3,5,Learning About Islam,23. Zaid ibn Hārithah
|
||||||
3,6,Akhlaq and Adab in Islam,Forgiveness: A Good Quality
|
3,6,Akhlaq and Adab in Islam,24. Ways To Be a Good Person
|
||||||
3,6,Akhlaq and Adab in Islam,Good Deeds: A Duty of the Believers
|
3,6,Akhlaq and Adab in Islam,25. Kindness: A Virtue of the Believers
|
||||||
3,6,Akhlaq and Adab in Islam,Cleanliness: A Quality of Believers
|
3,6,Akhlaq and Adab in Islam,26. Forgiveness: A Quality of the Believers
|
||||||
3,6,Akhlaq and Adab in Islam,A Muslim Family
|
3,6,Akhlaq and Adab in Islam,27. Good Deeds: A Duty of the Believers
|
||||||
3,6,Akhlaq and Adab in Islam,Perseverance: Never Give Up
|
3,6,Akhlaq and Adab in Islam,28. Perseverance: Never Give Up
|
||||||
3,6,Akhlaq and Adab in Islam,Punctuality: Doing Things on Time
|
3,6,Akhlaq and Adab in Islam,29. Punctuality: Doing Things on Time
|
||||||
4,1,Knowing the Creator,Rewards of Allahﷻ: Everybody Receives Them
|
4,1,Knowing the Creator,1. Rewards of Allah: Everybody Receives Them
|
||||||
4,1,Knowing the Creator,Discipline of Allahﷻ
|
4,1,Knowing the Creator,1. Discipline of Allah: Because He Loves Us
|
||||||
4,1,Knowing the Creator,Names of Allahﷻ
|
4,1,Knowing the Creator,3. Names of Allah
|
||||||
4,1,Knowing the Creator,Books of Allahﷻ
|
4,1,Knowing the Creator,4. Books of Allah
|
||||||
4,2,How Islam Changed Arabia,Pre-Islamic Arabia
|
4,2,How Islam Changed Arabia,5. Pre-Islamic Arabia: Age of Ignorance
|
||||||
4,2,How Islam Changed Arabia,The Year of the Elephant
|
4,2,How Islam Changed Arabia,6. The Year of the Elephant
|
||||||
4,2,How Islam Changed Arabia,Early Life of Muhammadﷺ
|
4,2,How Islam Changed Arabia,7. Early Life of Muhammad ﷺ
|
||||||
4,2,How Islam Changed Arabia,Life Before Becoming a Nabi
|
4,2,How Islam Changed Arabia,8. Life Before Becoming a Nabi
|
||||||
4,2,How Islam Changed Arabia,First Revelation
|
4,2,How Islam Changed Arabia,9. First Revelation
|
||||||
4,2,How Islam Changed Arabia,Makkah Period
|
4,2,How Islam Changed Arabia,10. Makkah Period: The Early Years of the Muslims
|
||||||
4,2,How Islam Changed Arabia,Hijrat to Madinah
|
4,2,How Islam Changed Arabia,11. Hijrat to Madinah: The Migration that Shaped History
|
||||||
4,2,How Islam Changed Arabia,Madinah Period
|
4,2,How Islam Changed Arabia,12. Madinah Period: Islam Prospers
|
||||||
4,3,The Rightly Guided Khalifah,Abu Bakr: The First Khalifah
|
4,3,The Rightly Guided Khalifah,13. Abū Bakr (R): The First Khalifah
|
||||||
4,3,The Rightly Guided Khalifah,‘Umar ibn al-Khattab
|
4,3,The Rightly Guided Khalifah,14. ‘Umar al-Khaṭṭāb (R): The Second Khalifah
|
||||||
4,3,The Rightly Guided Khalifah,‘Uthman ibn ‘Affan
|
4,3,The Rightly Guided Khalifah,15. ‘Uthman Ibn ‘Affān (R): The Third Khalifah
|
||||||
4,3,The Rightly Guided Khalifah,‘Ali ibn Abu Talib
|
4,3,The Rightly Guided Khalifah,16. ‘Ali Ibn Abu Ṭālib (R): The Fourth Khalifah
|
||||||
4,4,The Messengers of Allah,Hud (A): Struggle to Guide People
|
4,4,Messengers of Allah,17. Hūd (A): Struggle to Guide Mankind
|
||||||
4,4,The Messengers of Allah,Salih (A): To Guide the Misguided
|
4,4,Messengers of Allah,18. Ṣāliḥ (A): Struggle to Guide the Misguided
|
||||||
4,4,The Messengers of Allah,Musa (A): His Life and Actions
|
4,4,Messengers of Allah,19. Mūsā (A): His Life and Achievements
|
||||||
4,4,The Messengers of Allah,Sulaiman (A): A Humble King
|
4,4,Messengers of Allah,20. Sulaimān (A): A King and a Servant of Allah ﷺ
|
||||||
4,5,Fiqh of Salat,Preparation for Salat
|
4,5,Fiqh of Salat,21. Preparation for Salat
|
||||||
4,5,Fiqh of Salat,Requirements of Salat
|
4,5,Fiqh of Salat,22. The Requirements of Salat
|
||||||
4,5,Fiqh of Salat,Mubtilat us-Salat
|
4,5,Fiqh of Salat,23. Mubṭilāt-us-Salāt: Things that Invalidate Salāt
|
||||||
4,5,Fiqh of Salat,How to Pray Behind an Imam
|
4,5,Fiqh of Salat,24. How to Pray Behind an Imām
|
||||||
4,6,General Islamic Topics,Compilers of Hadith
|
4,6,General Islamic Topics,25. Compilers of Hadīth
|
||||||
4,6,General Islamic Topics,Shaitan’s Mode of Operation
|
4,6,General Islamic Topics,26. Shaitan’s Mode of Operation
|
||||||
4,6,General Islamic Topics,Day of Judgment
|
4,6,General Islamic Topics,27. Day of Judgment: The Day of Ultimate Justice
|
||||||
4,6,General Islamic Topics,Eid: Its Significance
|
4,6,General Islamic Topics,28. ‘Eid: Significance of the Festivities
|
||||||
4,6,General Islamic Topics,Truthfulness: A Quality of Muslim
|
4,6,General Islamic Topics,29. Truthfulness: An Important Quality for Muslims
|
||||||
4,6,General Islamic Topics,Perseverance: Keep on Trying
|
4,6,General Islamic Topics,30. Perseverance: Keep on Trying
|
||||||
5,1,"The Creator, His Message","Tawhid, Kafir, Kufr, Shirk, Nifaq"
|
5,1,The Creator,1. His Message, and His Messengers,Tawhid, Kafir, Kufr, Shirk, Nifaq
|
||||||
5,1,"The Creator, His Message",Why Should We Worship Allahﷻ?
|
5,1,The Creator,2. His Message, and His Messengers,Why Should We Worship Allah?
|
||||||
5,1,"The Creator, His Message",Revelation of the Qur’an
|
5,1,The Creator,3. His Message, and His Messengers,The Revelation of the Qur’an
|
||||||
5,1,"The Creator, His Message",Characteristics of the Messengers
|
5,1,The Creator,4. His Message, and His Messengers,Characteristics of the Messengers
|
||||||
5,2,"The Battles, Developments",Pledges of ‘Aqabah
|
5,2,The Battles and Other Developments,5. Pledges of ‘Aqabah: Invitation to Migrate
|
||||||
5,2,"The Battles, Developments",The Battle of Badr
|
5,2,The Battles and Other Developments,6. The Battle of Badr: Allah Supports the Righteous
|
||||||
5,2,"The Battles, Developments",The Battle of Uhud
|
5,2,The Battles and Other Developments,7. The Battle of Uhud: Obey Allah and Obey the Rasul ﷺ
|
||||||
5,2,"The Battles, Developments",The Battle of the Trench
|
5,2,The Battles and Other Developments,8. The Battle of the Trench: A Bloodless Battle
|
||||||
5,2,"The Battles, Developments",The Treaty of Hudaibiyah
|
5,2,The Battles and Other Developments,9. The Treaty of Hudaibiyah: A Clear Victory
|
||||||
5,2,"The Battles, Developments",Liberation of Makkah
|
5,2,The Battles and Other Developments,10. Liberation of Makkah: A Bloodless Victory
|
||||||
5,3,The Messengers of Allah,Adam (A): The Creation of Mankind
|
5,3,Stories of the Messengers of Allah,11. Adam (A): The Creation of Human Beings
|
||||||
5,3,The Messengers of Allah,Ibrahim (A) Debate with Polytheists
|
5,3,Stories of the Messengers of Allah,12. Ibrahim (A): His Debate with the Polytheists
|
||||||
5,3,The Messengers of Allah,Ibrahim (A): Plan Against Idols
|
5,3,Stories of the Messengers of Allah,13. Ibrahim (A): His Plan Against the Idols
|
||||||
5,3,The Messengers of Allah,Luqman (A): A Wise Man’s Lifelong Teachings
|
5,3,Stories of the Messengers of Allah,14. Luqmān (A): A Wise Man’s Lifelong Advice
|
||||||
5,3,The Messengers of Allah,Yusuf (A): His Childhood
|
5,3,Stories of the Messengers of Allah,15. Yūsuf (A): His Childhood and Life in Aziz’s Home
|
||||||
5,3,The Messengers of Allah,Yusuf (A): His Righteousness
|
5,3,Stories of the Messengers of Allah,16. Yūsuf (A): Standing Up for Righteousness
|
||||||
5,3,The Messengers of Allah,Yusuf (A): Dream Comes True
|
5,3,Stories of the Messengers of Allah,17. Yūsuf (A): A Childhood Dream Comes True
|
||||||
5,3,The Messengers of Allah,"Ayyub (A): Patience, Perseverance"
|
5,4,Islam in The World,20. Major Masājid in the World
|
||||||
5,3,The Messengers of Allah,"Zakariyyah (A), Yahya (A)"
|
5,5,Islamic Values and Teachings,21. Upholding Truth: A Duty of All Believers
|
||||||
5,4,Islam in the World,Major Masajid in the World
|
5,5,Islamic Values and Teachings,22. Responsibility and Punctuality
|
||||||
5,5,"Islamic Values, Teachings",Upholding Truth: A Duty for All Believers
|
5,5,Islamic Values and Teachings,23. My Mind, My Body: The Body is a Mirror of the Mind
|
||||||
5,5,"Islamic Values, Teachings",Responsibility and Punctuality
|
5,5,Islamic Values and Teachings,24. Kindness and Forgiveness
|
||||||
5,5,"Islamic Values, Teachings",My Mind My Body
|
5,5,Islamic Values and Teachings,25. The Middle Path: Ways to Avoid the Two Extremes
|
||||||
5,5,"Islamic Values, Teachings",Kindness and Forgiveness
|
5,5,Islamic Values and Teachings,26. Salat: Its Significance
|
||||||
5,5,"Islamic Values, Teachings",The Middle Path: Ways to Avoid Two Extremes
|
5,5,Islamic Values and Teachings,27. Sawm: Its Significance
|
||||||
5,5,"Islamic Values, Teachings",Salat: Its Significance
|
5,5,Islamic Values and Teachings,28. Zakat and Sadaqah: Similarities and Differences
|
||||||
5,5,"Islamic Values, Teachings",Sawm: Its Significance
|
6,1,The Creator,1. His Message, and His Messengers,Tawhid, Kafir, Kufr, Shirk, Nifaq
|
||||||
5,5,"Islamic Values, Teachings",Zakat and Sadaqah: Similarities and Differences
|
6,1,The Creator,2. His Message, and His Messengers,Why Should We Worship Allah?
|
||||||
6,1,The Creator—His Message,Attributes of Allahﷻ
|
6,1,The Creator,3. His Message, and His Messengers,The Revelation of the Qur’an
|
||||||
6,1,The Creator—His Message,The Promise of Allahﷻ
|
6,1,The Creator,4. His Message, and His Messengers,Characteristics of the Messengers
|
||||||
6,2,The Qur’an and Hadith,Objectives of the Qur’an?
|
6,2,The Battles and Other Developments,5. Pledges of ‘Aqabah: Invitation to Migrate
|
||||||
6,2,The Qur’an and Hadith,Compilation of the Qur’an
|
6,2,The Battles and Other Developments,6. The Battle of Badr: Allah Supports the Righteous
|
||||||
6,2,The Qur’an and Hadith,Previous Scriptures and the Qur’an
|
6,2,The Battles and Other Developments,7. The Battle of Uhud: Obey Allah and Obey the Rasul ﷺ
|
||||||
6,2,The Qur’an and Hadith,Compilation of Hadith
|
6,2,The Battles and Other Developments,8. The Battle of the Trench: A Bloodless Battle
|
||||||
6,3,Fundamentals of Deen,Importance of Shahadah
|
6,2,The Battles and Other Developments,9. The Treaty of Hudaibiyah: A Clear Victory
|
||||||
6,3,Fundamentals of Deen,Khushu in Salat
|
6,2,The Battles and Other Developments,10. Liberation of Makkah: A Bloodless Victory
|
||||||
6,3,Fundamentals of Deen,Taqwa: A Quality of Believers
|
6,3,Stories of the Messengers of Allah,11. Adam (A): The Creation of Human Beings
|
||||||
6,4,Messengers of Allah,Nuh (A)
|
6,3,Stories of the Messengers of Allah,12. Ibrahim (A): His Debate with the Polytheists
|
||||||
6,4,Messengers of Allah,"Talut, Jalut, and Dawud (A)"
|
6,3,Stories of the Messengers of Allah,13. Ibrahim (A): His Plan Against the Idols
|
||||||
6,4,Messengers of Allah,Dawud (A) and Sulaiman (A)
|
6,3,Stories of the Messengers of Allah,14. Luqmān (A): A Wise Man’s Lifelong Advice
|
||||||
6,4,Messengers of Allah,Musa (A) and Fir‘awn
|
6,3,Stories of the Messengers of Allah,15. Yūsuf (A): His Childhood and Life in Aziz’s Home
|
||||||
6,4,Messengers of Allah,Musa (A) and Khidir
|
6,3,Stories of the Messengers of Allah,16. Yūsuf (A): Standing Up for Righteousness
|
||||||
6,4,Messengers of Allah,‘Isa (A) and Maryam (ra)
|
6,3,Stories of the Messengers of Allah,17. Yūsuf (A): A Childhood Dream Comes True
|
||||||
6,5,Some Prominent Muslimah,Khadijah (ra)
|
6,4,Islam in The World,20. Major Masājid in the World
|
||||||
6,5,Some Prominent Muslimah,‘Aishah (ra)
|
6,5,Islamic Values and Teachings,21. Upholding Truth: A Duty of All Believers
|
||||||
6,5,Some Prominent Muslimah,Fatimah (ra)
|
6,5,Islamic Values and Teachings,22. Responsibility and Punctuality
|
||||||
6,5,Some Prominent Muslimah,Some Prominent Muslimahs
|
6,5,Islamic Values and Teachings,23. My Mind, My Body: The Body is a Mirror of the Mind
|
||||||
6,6,Knowledge Enrichment,Al-Qiyamah: The Awakening
|
6,5,Islamic Values and Teachings,24. Kindness and Forgiveness
|
||||||
6,6,Knowledge Enrichment,Ruh and Nafs: An Overview
|
6,5,Islamic Values and Teachings,25. The Middle Path: Ways to Avoid the Two Extremes
|
||||||
6,6,Knowledge Enrichment,Angels and Jinn: An Overview
|
6,5,Islamic Values and Teachings,26. Salat: Its Significance
|
||||||
6,6,Knowledge Enrichment,Shaitan: The Invisible Enemy
|
6,5,Islamic Values and Teachings,27. Sawm: Its Significance
|
||||||
6,7,The Current Society,My Friend Is Muslim Now
|
6,5,Islamic Values and Teachings,28. Zakat and Sadaqah: Similarities and Differences
|
||||||
6,7,The Current Society,Friendship
|
7,1,The Creator,1. Why Islam? What is Islam?
|
||||||
6,7,The Current Society,Muslims Around the World
|
7,1,The Creator,2. Belief in Allah
|
||||||
6,7,The Current Society,People of Other Faith
|
7,1,The Creator,3. The Qur’an: Its Qualitative Names
|
||||||
6,8,Developing Islamic Values,Greed and Dishonesty
|
7,1,The Creator,4. Istighfār: Seeking Forgiveness and Protection
|
||||||
6,8,Developing Islamic Values,Avoiding Extravagance
|
7,1,The Creator,5. Allah: Angry or Kind?
|
||||||
7,1,The Creator,Why Islam? what is Islam?
|
7,2,Stories of the Messengers,6. Ādam (A): The Trial of the First Messenger
|
||||||
7,1,The Creator,Belief in Allahﷻ
|
7,2,Stories of the Messengers,7. The Life of Ibrāhīm (A): Beginning a Nation
|
||||||
7,1,The Creator,The Qur’an: Its Qualitative Names
|
7,2,Stories of the Messengers,8. The Sacrifice of Ibrāhīm (A)
|
||||||
7,1,The Creator,Istighfar: Seeking Forgiveness of Allahﷻ
|
7,2,Stories of the Messengers,9. Lūt (A): A Message for Modern Societies
|
||||||
7,1,The Creator,Allahﷻ: Angry or Kind
|
7,2,Stories of the Messengers,10. Yūsuf (A): The Will to Overcome Temptation
|
||||||
7,2,Stories of the Messengers,Adam (A): Trial of the Messenger
|
7,3,Stories from the Qur’an,11. The Companions of the Cave
|
||||||
7,2,Stories of the Messengers,Life of Ibrahim (A)
|
7,3,Stories from the Qur’an,12. Dhu al-Qarnain: The Journey of a King
|
||||||
7,2,Stories of the Messengers,Sacrifice of Ibrahim (A)
|
7,3,Stories from the Qur’an,13. Effective Debate and Negotiation Styles in the Qur’an
|
||||||
7,2,Stories of the Messengers,Lut (A): Message for Modern Societies
|
7,4,Two Companions Who Shaped Islam,14. Abū Sufyān: His Life and Achievements
|
||||||
7,2,Stories of the Messengers,Yusuf (A)—The Will to Overcome Temptation
|
7,4,Two Companions Who Shaped Islam,15. Khālid Ibn al-Walīd: The “Sword of Allah”
|
||||||
7,3,Stories from the Qur’an,The Companions of the Cave
|
7,5,Knowledge Enrichment,16. Character of the Messengers
|
||||||
7,3,Stories from the Qur’an,Dhul Qurnain: Journey of a King
|
7,5,Knowledge Enrichment,17. Rasūlullāh’s Marriages
|
||||||
7,3,Stories from the Qur’an,Effective Debate and Negotiation Styles in the Qur’an
|
7,5,Knowledge Enrichment,18. Lailatul Qadr: The Night of Majesty
|
||||||
7,4,Two Companions,Abu Sufyan
|
7,5,Knowledge Enrichment,19. Fasting During Ramadan: The Month of Benefits
|
||||||
7,4,Two Companions,Khalid Ibn Walid (R)
|
7,5,Knowledge Enrichment,20. My Family is Muslim Now
|
||||||
7,5,Knowledge Enrichment,The character of the Messengers
|
7,5,Knowledge Enrichment,21. Science in the Qur’an
|
||||||
7,5,Knowledge Enrichment,Rasulullahﷺ Marriages
|
7,5,Knowledge Enrichment,22. Lessons From Past Civilizations
|
||||||
7,5,Knowledge Enrichment,Lailatul Qadr
|
7,6,Akhlaq and Adab in Islam,23. Amr Bil Ma’rūf: Enjoin Good Deeds
|
||||||
7,5,Knowledge Enrichment,Fasting During Ramadan
|
7,6,Akhlaq and Adab in Islam,24. Guard Your Tongue: Think Before You Speak
|
||||||
7,5,Knowledge Enrichment,My Family is Muslim Now
|
7,6,Akhlaq and Adab in Islam,25. Islamic Greeting: Wishing Peace
|
||||||
7,5,Knowledge Enrichment,Science in the Qur’an
|
7,6,Akhlaq and Adab in Islam,26. How to Achieve Success
|
||||||
7,5,Knowledge Enrichment,Lessons from Past Civilizations
|
7,6,Akhlaq and Adab in Islam,27. Permitted and Prohibited
|
||||||
7,6,Teachings of the Qur’an,Amr Bil Ma‘ruf
|
7,6,Akhlaq and Adab in Islam,28. Types of Behavior Allah Loves
|
||||||
7,6,Teachings of the Qur’an,Guard Your Tongue
|
8,1,Knowing the Creator,1. Divine Names
|
||||||
7,6,Teachings of the Qur’an,Islamic Greetings
|
8,1,Knowing the Creator,2. Sunan of Allah
|
||||||
7,6,Teachings of the Qur’an,How to Achieve Success
|
8,1,Knowing the Creator,3. Objectives of the Qur’an
|
||||||
7,6,Teachings of the Qur’an,Permitted and Prohibited
|
8,1,Knowing the Creator,4. Lessons from Sūrah al-Hujurāt
|
||||||
7,6,Teachings of the Qur’an,Types of Behavior Allahﷻ Loves
|
8,1,Knowing the Creator,5. True Piety: A Synthesis of Belief, Practice, and Conduct
|
||||||
8,1,Knowing the Creator,Divine Names
|
8,1,Knowing the Creator,6. Āyatul Kursi: The Throne Verse
|
||||||
8,1,Knowing the Creator,Sunan of Allahﷻ
|
8,2,Knowing the Messenger ﷺ,7. The Person Muhammad ﷺ
|
||||||
8,1,Knowing the Creator,Objectives of the Qur’an
|
8,2,Knowing the Messenger ﷺ,8. Farewell Pilgrimage
|
||||||
8,1,Knowing the Creator,Surah Hujurat: Its Teachings
|
8,2,Knowing the Messenger ﷺ,9. Finality of Prophethood
|
||||||
8,1,Knowing the Creator,True Piety: Analysis of Verse 2:177
|
8,2,Knowing the Messenger ﷺ,10. Hadith: Collection and Classification
|
||||||
8,1,Knowing the Creator,Ayatul Kursi
|
8,3,Challenges in Madinah,11. Hypocrites
|
||||||
8,2,Knowing the Messengerﷺ,The Person Muhammadﷺ
|
8,3,Challenges in Madinah,12. Banu Qaynuqa: Threat Within Madinah
|
||||||
8,2,Knowing the Messengerﷺ,Farewell Pilgrimage
|
8,3,Challenges in Madinah,13. Banu Nadir: Treachery Within Madinah
|
||||||
8,2,Knowing the Messengerﷺ,Finality of Prophethood
|
8,3,Challenges in Madinah,14. Banu Qurayzah
|
||||||
8,2,Knowing the Messengerﷺ,"Hadith: Collection, Classification"
|
8,3,Challenges in Madinah,15. Mission to Tabūk: A Test of Steadfastness
|
||||||
8,3,Challenges in Madinah,Hypocrites
|
8,4,Islamic Ethical Framework,16. Friends and Friendship: Who is a Good Friend?
|
||||||
8,3,Challenges in Madinah,Banu Qaynuqa
|
8,4,Islamic Ethical Framework,17. Friendship With Non-Muslims
|
||||||
8,3,Challenges in Madinah,Banu Nadir
|
8,4,Islamic Ethical Framework,18. Dating: How Islam Views the Practice
|
||||||
8,3,Challenges in Madinah,Banu Qurayzah
|
8,4,Islamic Ethical Framework,19. Hold Firmly the Rope of Allah
|
||||||
8,3,Challenges in Madinah,Mission to Tabuk
|
8,4,Islamic Ethical Framework,20. Elements of a Bad Life
|
||||||
8,4,Islamic Ethical Framework,Friends and Friendship
|
8,5,Islamic Values and Teachings,21. Duties Towards Parents
|
||||||
8,4,Islamic Ethical Framework,Friendship With Non-Muslims
|
8,5,Islamic Values and Teachings,22. Hope, Hopefulness, Hopelessness
|
||||||
8,4,Islamic Ethical Framework,Dating in Islam
|
8,5,Islamic Values and Teachings,23. Trials in Life: Everyone Will Experience Them
|
||||||
8,4,Islamic Ethical Framework,Hold Firmly the Rope of Allah
|
8,5,Islamic Values and Teachings,24. Permitted and Prohibited Food
|
||||||
8,4,Islamic Ethical Framework,Elements of Bad Life
|
8,5,Islamic Values and Teachings,25. Performance of Hajj
|
||||||
8,5,"Islamic Values, Teachings",Duties Toward Parents
|
8,5,Islamic Values and Teachings,26. Parables in the Qur’an
|
||||||
8,5,"Islamic Values, Teachings","Hope, Hopefulness, Hopelessness"
|
8,6,Islam After the Messenger ﷺ,27. Early History of Shi‘ah Muslims
|
||||||
8,5,"Islamic Values, Teachings",Trials in Life
|
8,6,Islam After the Messenger ﷺ,28. Umayyad Dynasty
|
||||||
8,5,"Islamic Values, Teachings",Permitted and Prohibited Food
|
8,6,Islam After the Messenger ﷺ,29. Abbasid Dynasty
|
||||||
8,5,"Islamic Values, Teachings",Performance of Hajj
|
9,1,A Reflection on the Divine,1. Signs of Allahﷻ in Nature
|
||||||
8,5,"Islamic Values, Teachings",Parables in the Qur’an
|
9,1,A Reflection on the Divine,2. Pondering the Qur’an
|
||||||
8,6,Islam After the Rasul (S),Origin and History of Shi‘ah
|
9,1,A Reflection on the Divine,3. Preservation and Compilation of the Qur’an
|
||||||
8,6,Islam After the Rasul (S),Ummayad Dynasty
|
9,1,A Reflection on the Divine,4. Ibadat—Easy Ways to Do It
|
||||||
8,6,Islam After the Rasul (S),Abbasid Dynasty
|
9,1,A Reflection on the Divine,5. Surah Baqarah—Statement of Faith and Commitment
|
||||||
9,1,A Reflection on the Divine,Signs of Allahﷻ in Nature
|
9,2,Islam and Muslim,6. Why Human Beings Are Superior
|
||||||
9,1,A Reflection on the Divine,Pondering the Qur’an
|
9,2,Islam and Muslim,7. Life Cycle of Truth
|
||||||
9,1,A Reflection on the Divine,Preservation and Compilation of the Qur’an
|
9,2,Islam and Muslim,8. Is Islam a Violent Religion?
|
||||||
9,1,A Reflection on the Divine,Ibadat—Easy Ways to Do It
|
9,2,Islam and Muslim,9. Present Life: Vanity, Deception, Play
|
||||||
9,1,A Reflection on the Divine,Surah Baqarah—Statement of Faith and Commitment
|
9,2,Islam and Muslim,10. Shariah
|
||||||
9,2,Islam and Muslim,Why Human Beings Are Superior
|
9,2,Islam and Muslim,11. Justice in Islam
|
||||||
9,2,Islam and Muslim,Life Cycle of Truth
|
9,3,Ethical Standard in Islam,12. Choices We Make
|
||||||
9,2,Islam and Muslim,Is Islam a Violent Religion?
|
9,3,Ethical Standard in Islam,13. Peer Pressure
|
||||||
9,2,Islam and Muslim,"Present Life: Vanity, Deception, Play"
|
9,3,Ethical Standard in Islam,14. Islamic Perspective on Dating
|
||||||
9,2,Islam and Muslim,Shariah
|
9,3,Ethical Standard in Islam,15. Indecency
|
||||||
9,2,Islam and Muslim,Justice in Islam
|
9,3,Ethical Standard in Islam,16. Alcohol and Gambling
|
||||||
9,3,Ethical Standard in Islam,Choices We Make
|
9,3,Ethical Standard in Islam,17. Permitted and Prohibited Food
|
||||||
9,3,Ethical Standard in Islam,Peer Pressure
|
9,3,Ethical Standard in Islam,18. Food of the People of the Book
|
||||||
9,3,Ethical Standard in Islam,Islamic Perspective on Dating
|
9,3,Ethical Standard in Islam,19. Let Ramadan Bring The Best in Us
|
||||||
9,3,Ethical Standard in Islam,Indecency
|
9,4,Essays on Rasulullahﷺ,20. Khadijah (ra)
|
||||||
9,3,Ethical Standard in Islam,Alcohol and Gambling
|
9,4,Essays on Rasulullahﷺ,21. Rasulullahﷺ Multiple Marriages
|
||||||
9,3,Ethical Standard in Islam,Permitted and Prohibited Food
|
9,4,Essays on Rasulullahﷺ,22. Marriage to Zainab (ra)
|
||||||
9,3,Ethical Standard in Islam,Food of the People of the Book
|
9,4,Essays on Rasulullahﷺ,23. Rasulullahﷺ: A Great Army General
|
||||||
9,3,Ethical Standard in Islam,Let Ramadan Bring The Best in Us
|
9,4,Essays on Rasulullahﷺ,24. Prophecy of Muhammadﷺ in the Bible
|
||||||
9,4,Essays on Rasulullahﷺ,Khadijah (ra)
|
9,4,Essays on Rasulullahﷺ,25. Allegations Against Rasulullahﷺ
|
||||||
9,4,Essays on Rasulullahﷺ,Rasulullahﷺ Multiple Marriages
|
9,5,Faith-Based Wealth Building,26. Faith Based Wealth Building
|
||||||
9,4,Essays on Rasulullahﷺ,Marriage to Zainab (ra)
|
9,5,Faith-Based Wealth Building,27. Earn, Save, Spend, Invest
|
||||||
9,4,Essays on Rasulullahﷺ,Rasulullahﷺ: A Great Army General
|
9,5,Faith-Based Wealth Building,28. Let Investment Work for You
|
||||||
9,4,Essays on Rasulullahﷺ,Prophecy of Muhammadﷺ in the Bible
|
|
||||||
9,4,Essays on Rasulullahﷺ,Allegations Against Rasulullahﷺ
|
|
||||||
9,5,Faith-Based Wealth Building,Faith Based Wealth Building
|
|
||||||
9,5,Faith-Based Wealth Building,"Earn, Save, Spend, Invest"
|
|
||||||
9,5,Faith-Based Wealth Building,Let Investment Work for You
|
|
||||||
|
|||||||
|
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -1,45 +0,0 @@
|
|||||||
Grade,Surah
|
|
||||||
1,Al-Fatihah
|
|
||||||
1,An-Nas
|
|
||||||
1,Al-Falaq
|
|
||||||
1,Al-Ikhlas
|
|
||||||
2,Al-Masad
|
|
||||||
2,An-Nasr
|
|
||||||
2,Al-Kafirun
|
|
||||||
2,Al-Kawthar
|
|
||||||
2,Al-Ma'un
|
|
||||||
3,Quraysh
|
|
||||||
3,Al-Fil
|
|
||||||
3,Al-Humazah
|
|
||||||
3,Al-'Asr
|
|
||||||
3,At-Takathur
|
|
||||||
4,Al-Qari'ah
|
|
||||||
4,Al-'Adiyat
|
|
||||||
4,Az-Zalzalah
|
|
||||||
4,Al-Bayyinah
|
|
||||||
4,Al-Qadr
|
|
||||||
5,Al-'Alaq
|
|
||||||
5,At-Tin
|
|
||||||
5,Ash-Sharh
|
|
||||||
5,Ad-Duhaa
|
|
||||||
5,Al-Layl
|
|
||||||
6,Ash-Shams
|
|
||||||
6,Al-Balad
|
|
||||||
6,Al-Fajr
|
|
||||||
6,Al-Ghashiyah
|
|
||||||
6,Al-A'la
|
|
||||||
7,At-Tariq
|
|
||||||
7,Al-Buruj
|
|
||||||
7,Al-Inshiqaq
|
|
||||||
7,Al-Mutaffifin
|
|
||||||
7,Al-Infitar
|
|
||||||
8,At-Takwir
|
|
||||||
8,Abasa
|
|
||||||
8,Al-Mursalat
|
|
||||||
8,An-Naba
|
|
||||||
9,Al-Mulk
|
|
||||||
9,Al-Qalam
|
|
||||||
9,Al-Haqqah
|
|
||||||
9,Al-Ma'arij
|
|
||||||
9,Nuh
|
|
||||||
9,Al-Jinn
|
|
||||||
|
Binary file not shown.
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user