Compare commits

...

12 Commits

Author SHA1 Message Date
root ed67836701 fix class progress unit numbers calculation 2026-03-31 00:16:55 -04:00
root d2abbc1458 fix duplicate restored student 2026-03-30 23:50:20 -04:00
root b52475ff0b fix teacher submissions 2026-03-30 18:46:04 -04:00
root b2026812d5 AVP-87 Multiple Class updated not showing for multiple students in parent portal 2026-03-29 14:03:50 -04:00
root 58445b2a48 fix all issues 2026-03-24 01:02:36 -04:00
root 0f8a1fa0b1 Fixed the date shift by making date-only strings stay in the user timezone instead of being converted from UTC, which caused the one-day rollback. 2026-03-03 16:46:36 -05:00
Administrator fee07bcceb Merge branch 'develop_fix_bugs' into 'develop'
remove zip file

See merge request root/alrahma_sunday_school!4
2026-03-03 16:24:23 +00:00
root 70a6e2c104 remove zip file 2026-03-02 15:18:30 -05:00
Administrator 11c93d3e82 Merge branch 'develop_fix_bugs' into 'develop'
update financial controller and fix curriculum subjects

See merge request root/alrahma_sunday_school!3
2026-03-02 05:06:01 +00:00
root 7c5028a76d update financial controller and fix curriculum subjects 2026-03-02 00:04:38 -05:00
Administrator aa1260afd6 Merge branch 'develop_fix_bugs' into 'develop'
Develop fix bugs

See merge request root/alrahma_sunday_school!2
2026-03-01 22:25:37 +00:00
root 35b9cba882 AVP-85 Active link when clicking of Parent's email address 2026-03-01 17:24:20 -05:00
47 changed files with 2696 additions and 683 deletions
+1 -1
View File
@@ -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
BIN
View File
Binary file not shown.
+46 -38
View File
@@ -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)),
];
}
} }
+8
View File
@@ -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
@@ -401,6 +405,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']);
@@ -1003,6 +1009,8 @@ $routes->group('family', static function ($routes) {
$routes->get('index', 'View\FamilyAdminController::index'); $routes->get('index', 'View\FamilyAdminController::index');
$routes->get('search', 'View\FamilyAdminController::search'); $routes->get('search', 'View\FamilyAdminController::search');
$routes->get('card', 'View\FamilyAdminController::card'); $routes->get('card', 'View\FamilyAdminController::card');
$routes->get('compose-email', 'View\FamilyAdminController::composeEmail');
$routes->post('compose-email/send', 'View\FamilyAdminController::sendComposeEmail');
}); });
// Convenience alias // Convenience alias
$routes->get('family', 'View\FamilyAdminController::index'); $routes->get('family', 'View\FamilyAdminController::index');
+102 -38
View File
@@ -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
+2 -1
View File
@@ -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,
]); ]);
+275
View File
@@ -119,6 +119,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 +302,227 @@ 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);
}
}
return view('teacher/class_progress_submit', [
'subjectSections' => self::SUBJECT_SECTIONS,
'subjectCurriculum' => $subjectCurriculum,
'classSectionId' => $row['class_section_id'],
'classSectionName' => $classSectionName,
'classId' => $classId,
'sundayOptions' => [$row['week_start']],
'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());
}
$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.');
}
$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);
@@ -447,6 +694,34 @@ class ClassProgressController extends BaseController
return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary; return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary;
} }
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;
}
$parts = preg_split('/\s*\/\s*/', $segment, 2);
if (count($parts) === 2) {
$units[] = trim($parts[0]);
$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();
+58 -22
View File
@@ -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' => [],
]; ];
} }
+488 -35
View File
@@ -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;
@@ -416,8 +418,10 @@ class AdministratorController extends BaseController
$totalStudents = (int) ( $totalStudents = (int) (
$this->db->table('student_class') $this->db->table('student_class')
->select('COUNT(DISTINCT student_class.student_id) AS cnt') ->select('COUNT(DISTINCT student_class.student_id) AS cnt')
->join('students', 'students.id = student_class.student_id', 'inner')
->where('student_class.school_year', $this->schoolYear) ->where('student_class.school_year', $this->schoolYear)
->where('student_class.class_section_id IS NOT NULL', null, false) ->where('student_class.class_section_id IS NOT NULL', null, false)
->where('students.is_active', 1)
->get() ->get()
->getRow('cnt') ->getRow('cnt')
?? 0 ?? 0
@@ -694,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',
@@ -713,11 +728,91 @@ 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->resolveExamDraftDeadline($semester, $schoolYear);
$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) {
@@ -743,7 +838,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' => [],
]; ];
} }
@@ -763,35 +858,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;
} }
@@ -805,13 +914,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) {
@@ -822,7 +933,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') {
@@ -831,14 +942,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) {
@@ -859,18 +973,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,
@@ -878,6 +1001,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,
]; ];
@@ -939,15 +1065,181 @@ class AdministratorController extends BaseController
'schoolYear' => $schoolYear, 'schoolYear' => $schoolYear,
'notificationHistory' => $historyMap, 'notificationHistory' => $historyMap,
'summary' => $summary, 'summary' => $summary,
'lowProgressSectionIds' => $lowProgressSectionIds,
]); ]);
} }
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) {
@@ -1004,6 +1296,9 @@ 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');
$sentCount = 0; $sentCount = 0;
$failCount = 0; $failCount = 0;
@@ -1019,6 +1314,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),
@@ -1031,10 +1333,45 @@ 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>";
}
$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'] ?? '';
@@ -1096,6 +1433,116 @@ 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,
];
}
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 [
@@ -1105,14 +1552,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 users information (e.g., email) // Update the authorized users 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);
} }
} }
} }
@@ -406,4 +406,82 @@ class FamilyAdminController extends BaseController
return service('response')->setBody(view('family/card', ['f' => $family])); return service('response')->setBody(view('family/card', ['f' => $family]));
} }
public function composeEmail()
{
$to = trim((string)$this->request->getGet('to'));
$name = trim((string)$this->request->getGet('name'));
$returnUrl = trim((string)$this->request->getGet('return_url'));
if ($returnUrl === '') {
$returnUrl = trim((string)$this->request->getServer('HTTP_REFERER'));
}
if ($returnUrl === '') {
$returnUrl = site_url('family');
}
return view('family/compose_email', [
'to' => $to,
'name' => $name,
'return_url' => $returnUrl,
]);
}
public function sendComposeEmail()
{
if (!$this->request->is('post')) {
return redirect()->to(site_url('family'));
}
$to = trim((string)$this->request->getPost('to'));
$subject = trim((string)$this->request->getPost('subject'));
$html = (string)($this->request->getPost('html') ?? '');
$returnUrl = trim((string)$this->request->getPost('return_url'));
if ($returnUrl === '' || !$this->isLocalReturnUrl($returnUrl)) {
$returnUrl = site_url('family');
}
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
return redirect()->back()->withInput()->with('error', 'Please enter a valid email address.');
}
if ($subject === '') {
return redirect()->back()->withInput()->with('error', 'Subject is required.');
}
if (trim($html) === '') {
return redirect()->back()->withInput()->with('error', 'Email body is required.');
}
$wrapped = view('emails/custom_html', [
'subject' => $subject,
'body_html' => $html,
]);
$mailer = new \App\Controllers\View\EmailController();
$ok = $mailer->sendEmail($to, $subject, $wrapped, 'communication');
if ($ok) {
return redirect()->to($returnUrl)->with('status', 'Email sent.');
}
return redirect()->to($returnUrl)->with('error', 'Unable to send email.');
}
private function isLocalReturnUrl(string $url): bool
{
$url = trim($url);
if ($url === '') return false;
$parts = parse_url($url);
if ($parts === false) return false;
if (!empty($parts['scheme']) || !empty($parts['host'])) {
$base = parse_url(site_url('/'));
if (!$base || empty($base['host'])) return false;
$hostMatch = strcasecmp((string)($parts['host'] ?? ''), (string)$base['host']) === 0;
return $hostMatch;
}
// Relative URL (e.g., /family?x=1 or family?x=1)
return true;
}
} }
+5 -2
View File
@@ -1283,10 +1283,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 +1297,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 +1305,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) {
$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,6 +1319,7 @@ 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,
]; ];
+29 -1
View File
@@ -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();
+98 -7
View File
@@ -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';
+2 -4
View File
@@ -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) {
+2 -1
View File
@@ -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,
+2 -4
View File
@@ -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) {
+5 -12
View File
@@ -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
} }
+4 -4
View File
@@ -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();
+160 -34
View File
@@ -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
+6 -4
View File
@@ -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;
} }
/** /**
+1
View File
@@ -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();
} }
+16 -1
View File
@@ -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);
}
} }
+18 -1
View File
@@ -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 &lt; 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) ?>">
@@ -517,37 +517,28 @@
<section class="mb-5"> <section class="mb-5">
<h2 class="mb-4 text-center">Statistics</h2> <h2 class="mb-4 text-center">Statistics</h2>
<div class="stats-row"> <div class="stats-row">
<!-- Students -->
<div class="stat-circle circle-primary"> <div class="stat-circle circle-primary">
<div class="stat-icon"><i class="bi bi-people-fill"></i></div> <div class="stat-icon"><i class="fas fa-user-graduate"></i></div>
<div class="stat-value" data-stat="students">—</div> <div class="stat-value" data-stat="students">—</div>
<div class="stat-title">Students</div> <div class="stat-title">Students</div>
</div> </div>
<!-- Teachers -->
<div class="stat-circle circle-info"> <div class="stat-circle circle-info">
<div class="stat-icon"><i class="bi bi-person-video2"></i></div> <div class="stat-icon"><i class="fas fa-chalkboard-teacher"></i></div>
<div class="stat-value" data-stat="teachers">—</div> <div class="stat-value" data-stat="teachers">—</div>
<div class="stat-title">Teachers</div> <div class="stat-title">Teachers</div>
</div> </div>
<!-- Teacher Assistants -->
<div class="stat-circle circle-success"> <div class="stat-circle circle-success">
<div class="stat-icon"><i class="bi bi-person-badge-fill"></i></div> <div class="stat-icon"><i class="fas fa-user-cog"></i></div>
<div class="stat-value" data-stat="teacherAssistants">—</div> <div class="stat-value" data-stat="teacherAssistants">—</div>
<div class="stat-title">Teachers' Assistants</div> <div class="stat-title">TAs</div>
</div> </div>
<!-- Admins -->
<div class="stat-circle circle-warning"> <div class="stat-circle circle-warning">
<div class="stat-icon"><i class="bi bi-shield-lock-fill"></i></div> <div class="stat-icon"><i class="fas fa-user-shield"></i></div>
<div class="stat-value" data-stat="admins">—</div> <div class="stat-value" data-stat="admins">—</div>
<div class="stat-title">Admins</div> <div class="stat-title">Admins</div>
</div> </div>
<!-- Parents -->
<div class="stat-circle circle-light"> <div class="stat-circle circle-light">
<div class="stat-icon"><i class="bi bi-people"></i></div> <div class="stat-icon"><i class="fas fa-users"></i></div>
<div class="stat-value" data-stat="parents">—</div> <div class="stat-value" data-stat="parents">—</div>
<div class="stat-title">Parents</div> <div class="stat-title">Parents</div>
</div> </div>
+11 -1
View File
@@ -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] ?? []);
@@ -71,9 +71,20 @@
max-height: none; max-height: none;
overflow: visible; overflow: visible;
} }
.attn-pie-wrap {
width: 320px;
height: 320px;
margin: 0 auto;
}
@media (max-width: 1200px) { @media (max-width: 1200px) {
.attn-analysis-half { grid-column: span 12; } .attn-analysis-half { grid-column: span 12; }
} }
@media (max-width: 576px) {
.attn-pie-wrap {
width: 180px;
height: 180px;
}
}
</style> </style>
<?= $this->endSection() ?> <?= $this->endSection() ?>
@@ -239,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;
@@ -291,7 +350,9 @@
<div class="attn-analysis-grid"> <div class="attn-analysis-grid">
<div class="attn-analysis-card attn-analysis-half"> <div class="attn-analysis-card attn-analysis-half">
<h6>Overall Distribution</h6> <h6>Overall Distribution</h6>
<canvas id="attnPieChart" height="50" aria-label="Overall attendance pie chart"></canvas> <div class="attn-pie-wrap">
<canvas id="attnPieChart" aria-label="Overall attendance pie chart"></canvas>
</div>
<table class="attn-analysis-table mt-3 no-mgmt-sticky" data-no-mgmt-sticky> <table class="attn-analysis-table mt-3 no-mgmt-sticky" data-no-mgmt-sticky>
<thead> <thead>
<tr> <tr>
@@ -360,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>
@@ -415,6 +507,8 @@
}] }]
}, },
options: { options: {
responsive: true,
maintainAspectRatio: false,
plugins: { plugins: {
tooltip: { tooltip: {
callbacks: { callbacks: {
@@ -465,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>
@@ -21,6 +21,28 @@
$totalItems = max(0, (int)($summary['total_items'] ?? 0)); $totalItems = max(0, (int)($summary['total_items'] ?? 0));
?> ?>
<div class="card-body"> <div class="card-body">
<?php
$termExamLabel = (isset($semester) && strtolower((string) $semester) === 'spring') ? 'Final' : 'Midterm';
?>
<?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 $lowProgressSectionIds = $lowProgressSectionIds ?? []; ?>
<?php if (!empty($lowProgressSectionIds)): ?>
<div class="alert alert-warning">
Showing teachers for class sections with progress submissions below 50%.
</div>
<?php endif; ?>
<div class="border rounded-3 p-3 mb-4 bg-light"> <div class="border rounded-3 p-3 mb-4 bg-light">
<div class="d-flex flex-wrap gap-4 align-items-center"> <div class="d-flex flex-wrap gap-4 align-items-center">
<div> <div>
@@ -56,17 +78,63 @@
> >
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th>Class Section</th> <th>Class-Section</th>
<th>Teacher</th> <th>Teachers Name</th>
<th class="text-center">Midterm Score</th> <th class="text-center">
<th class="text-center">Midterm Comment</th> <?= esc($termExamLabel) ?> Score
<th class="text-center">Participation</th> <div class="form-check d-inline-flex align-items-center gap-1 ms-2">
<th class="text-center">PTAP Comment</th> <input class="form-check-input" type="checkbox" name="notify_midterm_score" id="notifyMidtermScore" value="1">
<label class="form-check-label small" for="notifyMidtermScore">Include</label>
</div>
</th>
<th class="text-center">
<?= esc($termExamLabel) ?> Comment
<div class="form-check d-inline-flex align-items-center gap-1 ms-2">
<input class="form-check-input" type="checkbox" name="notify_midterm_comment" id="notifyMidtermComment" value="1">
<label class="form-check-label small" for="notifyMidtermComment">Include</label>
</div>
</th>
<th class="text-center">
Participation
<div class="form-check d-inline-flex align-items-center gap-1 ms-2">
<input class="form-check-input" type="checkbox" name="notify_participation" id="notifyParticipation" value="1">
<label class="form-check-label small" for="notifyParticipation">Include</label>
</div>
</th>
<th class="text-center">
PTAP Comment
<div class="form-check d-inline-flex align-items-center gap-1 ms-2">
<input class="form-check-input" type="checkbox" name="notify_ptap_comment" id="notifyPtapComment" value="1">
<label class="form-check-label small" for="notifyPtapComment">Include</label>
</div>
</th>
<th class="text-center">
Class Progress
<div class="form-check d-inline-flex align-items-center gap-1 ms-2">
<input class="form-check-input" type="checkbox" name="notify_class_progress" id="notifyClassProgress" value="1">
<label class="form-check-label small" for="notifyClassProgress">Include</label>
</div>
</th>
<th class="text-center">
Exam Draft
<div class="form-check d-inline-flex align-items-center gap-1 ms-2">
<input class="form-check-input" type="checkbox" name="notify_exam_draft" id="notifyExamDraft" value="1">
<label class="form-check-label small" for="notifyExamDraft">Include</label>
</div>
</th>
<th class="text-center">
Homework
<div class="form-check d-inline-flex align-items-center gap-1 ms-2">
<input class="form-check-input" type="checkbox" name="homework_notify_all" id="homeworkNotifyAll" value="1">
<label class="form-check-label small" for="homeworkNotifyAll">Include</label>
</div>
</th>
<th class="text-center">Notifications</th> <th class="text-center">Notifications</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (!empty($rows)): ?> <?php if (!empty($rows)): ?>
<?php $homeworkToggleRendered = false; ?>
<?php foreach ($rows as $row): ?> <?php foreach ($rows as $row): ?>
<tr> <tr>
<td><?= esc($row['class_section']) ?></td> <td><?= esc($row['class_section']) ?></td>
@@ -83,7 +151,9 @@
'midterm_score_status', 'midterm_score_status',
'midterm_comment_status', 'midterm_comment_status',
'participation_status', 'participation_status',
'ptap_comment_status' 'ptap_comment_status',
'class_progress_status',
'exam_draft_status',
] as $statusKey): ?> ] as $statusKey): ?>
<?php $status = $row[$statusKey] ?? ['label' => 'N/A', 'badge' => 'bg-secondary']; ?> <?php $status = $row[$statusKey] ?? ['label' => 'N/A', 'badge' => 'bg-secondary']; ?>
<td class="text-center"> <td class="text-center">
@@ -95,6 +165,15 @@
<?php endif; ?> <?php endif; ?>
</td> </td>
<?php endforeach; ?> <?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> <td>
<?php if (!empty($row['teachers'])): ?> <?php if (!empty($row['teachers'])): ?>
<?php $missingPayload = base64_encode(json_encode($row['missing_items'] ?? [])); ?> <?php $missingPayload = base64_encode(json_encode($row['missing_items'] ?? [])); ?>
+4
View File
@@ -0,0 +1,4 @@
<?= $this->extend('layout/email_layout') ?>
<?= $this->section('content') ?>
<?= $body_html ?? '' ?>
<?= $this->endSection() ?>
+12 -1
View File
@@ -22,6 +22,10 @@ if ($title === '' || preg_match('/^\s*Family\s+of\s+User\s*\d+\s*$/i', $title))
} }
$title = $lastName !== '' ? ('Family of ' . $lastName) : 'Family'; $title = $lastName !== '' ? ('Family of ' . $lastName) : 'Family';
} }
$returnUrl = trim((string)service('request')->getServer('HTTP_REFERER'));
if ($returnUrl === '') {
$returnUrl = site_url('family');
}
?> ?>
<div class="family-card-root" data-family-title="<?= esc($title) ?>"> <div class="family-card-root" data-family-title="<?= esc($title) ?>">
@@ -187,7 +191,14 @@ if ($title === '' || preg_match('/^\s*Family\s+of\s+User\s*\d+\s*$/i', $title))
<td class="small" data-label="Contact"> <td class="small" data-label="Contact">
<div> <div>
<i class="bi bi-envelope me-1 text-muted"></i> <i class="bi bi-envelope me-1 text-muted"></i>
<?= !empty($g['email']) ? ('<a href="mailto:'.esc($g['email']).'">'.esc($g['email']).'</a>') : '<span class="text-muted">—</span>' ?> <?php
$gEmail = trim((string)($g['email'] ?? ''));
$gName = trim((string)($g['firstname'] ?? '') . ' ' . (string)($g['lastname'] ?? ''));
$composeUrl = $gEmail !== ''
? site_url('family/compose-email?to=' . rawurlencode($gEmail) . '&name=' . rawurlencode($gName) . '&return_url=' . rawurlencode($returnUrl))
: '';
?>
<?= $gEmail !== '' ? ('<a href="' . esc($composeUrl) . '" target="_blank" rel="noopener">' . esc($gEmail) . '</a>') : '<span class="text-muted">—</span>' ?>
</div> </div>
<div class="mt-1"> <div class="mt-1">
<i class="bi bi-telephone me-1 text-muted"></i> <i class="bi bi-telephone me-1 text-muted"></i>
+87
View File
@@ -0,0 +1,87 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container my-4">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div>
<h2 class="h4 mb-1">Compose Email</h2>
<?php if (!empty($name)): ?>
<div class="text-muted"><?= esc($name) ?></div>
<?php endif; ?>
</div>
<a class="btn btn-outline-secondary btn-sm" href="<?= esc((string)($return_url ?? site_url('family'))) ?>">Back</a>
</div>
<form class="card shadow-sm" method="post" action="<?= site_url('family/compose-email/send') ?>">
<?= csrf_field() ?>
<input type="hidden" name="html" id="compose_html" value="">
<input type="hidden" name="return_url" value="<?= esc((string)($return_url ?? '')) ?>">
<div class="card-body">
<div class="mb-3">
<label class="form-label" for="composeTo">To</label>
<input type="email" class="form-control" id="composeTo" name="to" value="<?= esc((string)($to ?? '')) ?>" placeholder="parent@example.com" required>
</div>
<div class="mb-3">
<label class="form-label" for="composeSubject">Subject</label>
<input type="text" class="form-control" id="composeSubject" name="subject" placeholder="Subject" required>
</div>
<div class="mb-3">
<label class="form-label" for="composeEditor">Body (Rich Text)</label>
<textarea class="form-control" id="composeEditor" rows="14"></textarea>
<div class="form-text">Use the toolbar to format your message before sending.</div>
<noscript>
<div class="alert alert-warning mt-2">
JavaScript is disabled. The message will be sent from the hidden <code>html</code> field.
</div>
</noscript>
</div>
<div class="d-flex justify-content-end gap-2">
<button type="submit" class="btn btn-primary">Send Email</button>
</div>
</div>
</form>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script src="<?= base_url('assets/tinymce/tinymce.min.js') ?>"></script>
<script>
(function () {
const form = document.querySelector('form[action$="family/compose-email/send"]');
const hiddenHtml = document.getElementById('compose_html');
tinymce.init({
selector: '#composeEditor',
base_url: '<?= base_url('assets/tinymce') ?>',
suffix: '.min',
license_key: 'gpl',
height: 420,
menubar: true,
branding: false,
promotion: false,
plugins: 'advlist autolink lists link image charmap preview anchor ' +
'searchreplace visualblocks code fullscreen insertdatetime media table ' +
'help wordcount emoticons codesample',
toolbar: 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | ' +
'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | ' +
'link image media table | emoticons codesample | removeformat | preview code',
convert_urls: false,
paste_data_images: true,
image_caption: true,
content_style: 'body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size: 14px; }',
setup(editor) {
editor.on('keyup change undo redo SetContent', function () {
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
});
if (form) {
form.addEventListener('submit', function () {
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
});
}
}
});
})();
</script>
<?= $this->endSection() ?>
+10 -3
View File
@@ -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() {
+8 -4
View File
@@ -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>
+173 -14
View File
@@ -322,6 +322,72 @@
min-height: 400px; min-height: 400px;
} }
.home-stats {
background: #ffffff;
border-radius: 16px;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
padding: 2.5rem 1.5rem;
}
.home-stats .stats-row {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: center;
}
.home-stats .stat-circle {
width: 190px;
height: 190px;
border-radius: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #ffffff;
font-weight: 700;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.12);
}
.home-stats .stat-value {
font-size: 1.8rem;
line-height: 1;
}
.home-stats .stat-icon {
font-size: 2rem;
margin-bottom: .2rem;
opacity: .9;
}
.home-stats .stat-title {
font-size: 1.1rem;
margin-top: .25rem;
font-weight: 500;
text-align: center;
line-height: 1.2;
}
.home-stats .circle-primary {
background: radial-gradient(circle at 30% 30%, #0d6efd, #0044aa);
}
.home-stats .circle-success {
background: radial-gradient(circle at 30% 30%, #198754, #0c4f2c);
}
.home-stats .circle-warning {
background: radial-gradient(circle at 30% 30%, #daa544ff, #a66f00);
}
.home-stats .circle-info {
background: radial-gradient(circle at 30% 30%, #0dcaf0, #047e9c);
}
.home-stats .circle-light {
background: radial-gradient(circle at 30% 30%, #63af4cff, #00eb7dff);
}
@media (max-width: 992px) { @media (max-width: 992px) {
.image-column { .image-column {
min-height: 300px; min-height: 300px;
@@ -332,6 +398,13 @@
border-radius: 0 0 10px 10px !important; border-radius: 0 0 10px 10px !important;
} }
} }
@media (max-width: 768px) {
.home-stats .stat-circle {
width: 160px;
height: 160px;
}
}
</style> </style>
</head> </head>
@@ -448,8 +521,43 @@
<h4 class="fw-bold my-0"><u>Join Al Rahma family today! Click here for a quick guide on how to create an account.</u></h4> <h4 class="fw-bold my-0"><u>Join Al Rahma family today! Click here for a quick guide on how to create an account.</u></h4>
</a> </a>
</section> </section>
<!-- Statistics Start -->
<div class="container-xxl py-2 content-section">
<div class="container">
<div class="home-stats" data-dashboard-endpoint="<?= site_url('api/administrator/dashboard') ?>">
<h1 class="d-flex justify-content-center p-md-1">Active Participants</h1>
<br>
<div class="stats-row">
<div class="stat-circle circle-primary">
<div class="stat-icon"><i class="fa-solid fa-user-graduate"></i></div>
<div class="stat-value" data-stat="students">—</div>
<div class="stat-title">Students</div>
</div>
<div class="stat-circle circle-info">
<div class="stat-icon"><i class="fa-solid fa-chalkboard-user"></i></div>
<div class="stat-value" data-stat="teachers">—</div>
<div class="stat-title">Teachers</div>
</div>
<div class="stat-circle circle-success">
<div class="stat-icon"><i class="fa-solid fa-user-gear"></i></div>
<div class="stat-value" data-stat="teacherAssistants">—</div>
<div class="stat-title">TAs</div>
</div>
<div class="stat-circle circle-warning">
<div class="stat-icon"><i class="fa-solid fa-user-shield"></i></div>
<div class="stat-value" data-stat="admins">—</div>
<div class="stat-title">Admins</div>
</div>
<div class="stat-circle circle-light">
<div class="stat-icon"><i class="fa-solid fa-people-group"></i></div>
<div class="stat-value" data-stat="parents">—</div>
<div class="stat-title">Parents</div>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics End -->
<div class="container-xxl py-2 content-section"> <div class="container-xxl py-2 content-section">
<div class="container"> <div class="container">
<div class="row bg-light squared align-items-center"> <div class="row bg-light squared align-items-center">
@@ -461,18 +569,18 @@
<p class="mb-4">Under the umbrella of ISGL, Al Rahma Sunday School has been serving the community for over thirty years. Throughout the decades, it has provided students with a strong foundation in Islamic Studies and Quran, fostering both knowledge and character. With its dedicated teachers and well-rounded academic program, the school continues to guide generations of Muslim youth in their faith, values and practice of Islam.</p> <p class="mb-4">Under the umbrella of ISGL, Al Rahma Sunday School has been serving the community for over thirty years. Throughout the decades, it has provided students with a strong foundation in Islamic Studies and Quran, fostering both knowledge and character. With its dedicated teachers and well-rounded academic program, the school continues to guide generations of Muslim youth in their faith, values and practice of Islam.</p>
<p>For more information, click below to visit ISGL official website.</p> <p>For more information, click below to visit ISGL official website.</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0"> <table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr> <tr>
<td align="center" bgcolor="#28a745" style="border-radius:6px;"> <td align="center" bgcolor="#28a745" style="border-radius:6px;">
<a href="https://isgl.org" <a href="https://isgl.org"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style="display:inline-block;padding:12px 18px;color:#ffffff;text-decoration:none;font-weight:600;"> style="display:inline-block;padding:12px 18px;color:#ffffff;text-decoration:none;font-weight:600;">
ISGL Official Website ISGL Official Website
</a> </a>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
<!-- Image Column - Fixed with correct path --> <!-- Image Column - Fixed with correct path -->
@@ -677,6 +785,57 @@
} }
}); });
</script> </script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const container = document.querySelector('.home-stats[data-dashboard-endpoint]');
if (!container) return;
const endpoint = container.dataset.dashboardEndpoint;
const statElements = container.querySelectorAll('[data-stat]');
const numberFormatter = new Intl.NumberFormat();
let isLoading = false;
const loadStats = function() {
if (isLoading) return;
isLoading = true;
fetch(endpoint, {
headers: {
'Accept': 'application/json'
},
credentials: 'same-origin',
})
.then(function(response) {
if (!response.ok) {
throw new Error('Request failed');
}
return response.json();
})
.then(function(payload) {
const counts = payload && typeof payload === 'object' && payload.counts ?
payload.counts :
{};
statElements.forEach(function(element) {
const key = element.dataset.stat;
const value = counts[key];
element.textContent = Number.isFinite(value) ?
numberFormatter.format(value) :
'—';
});
})
.catch(function() {})
.finally(function() {
isLoading = false;
});
};
loadStats();
setInterval(loadStats, 60000);
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
loadStats();
}
});
});
</script>
</body> </body>
</html> </html>
+91 -65
View File
@@ -7,79 +7,105 @@
<div class="text-muted">Review the weekly reports your childs teachers submit.</div> <div class="text-muted">Review the weekly reports your childs 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 couldnt find any current enrollment for your account. Once your child is assigned to a Sunday class, their teachers progress reports will appear here. We couldnt find any current enrollment for your account. Once your child is assigned to a Sunday class, their teachers 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>
+3 -5
View File
@@ -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>
@@ -54,7 +52,7 @@
<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">Invoice Amount</th> <th class="text-end">Invoice Amount</th>
<th class="text-end">Applied Discount</th> <th class="text-end">Applied Discount</th>
@@ -86,7 +84,7 @@
</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>
+25 -3
View File
@@ -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>
+127 -28
View File
@@ -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,33 +68,48 @@
</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"> <?php if ($isEdit): ?>
<select id="weekStartSelect" name="week_start" class="form-select form-select-sm" required> <div class="text-muted small">
<option value="">Select week</option> <?php
<?php foreach ($sundayOptions as $sunday): ?> try {
<?php $displayStart = (new \DateTime($weekStartSelected))->format('M d, Y');
try { } catch (\Exception $e) {
$startDt = new \DateTime($sunday); $displayStart = $weekStartSelected;
$displayStart = $startDt->format('M d, Y'); }
} catch (\Exception $e) { ?>
$displayStart = $sunday; Week of <?= esc($displayStart ?: 'N/A') ?>
} </div>
?> <input type="hidden" name="week_start" value="<?= esc($weekStartSelected) ?>">
<option value="<?= esc($sunday) ?>" <?= $sunday === $weekStartSelected ? 'selected' : '' ?>> <?php else: ?>
<?= esc($displayStart) ?> <div class="d-flex align-items-center gap-2">
</option> <select id="weekStartSelect" name="week_start" class="form-select form-select-sm" required>
<?php endforeach; ?> <option value="">Select week</option>
</select> <?php foreach ($sundayOptions as $sunday): ?>
<div class="invalid-feedback">Week start is required.</div> <?php
</div> try {
$startDt = new \DateTime($sunday);
$displayStart = $startDt->format('M d, Y');
} catch (\Exception $e) {
$displayStart = $sunday;
}
?>
<option value="<?= esc($sunday) ?>" <?= $sunday === $weekStartSelected ? 'selected' : '' ?>>
<?= esc($displayStart) ?>
</option>
<?php endforeach; ?>
</select>
<div class="invalid-feedback">Week start is required.</div>
</div>
<?php endif; ?>
</div> </div>
<div class="card-body"> <div class="card-body">
<input type="hidden" name="week_end" id="weekEndInput" value="<?= esc($weekEndValue) ?>" required> <input type="hidden" name="week_end" id="weekEndInput" value="<?= esc($weekEndValue) ?>" required>
@@ -96,8 +125,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">
@@ -188,11 +219,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 +238,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,6 +255,51 @@
<?= $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';
@@ -368,6 +444,29 @@
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) {
form.submit();
}
});
}
}
})(); })();
</script> </script>
<?= $this->endSection() ?> <?= $this->endSection() ?>
@@ -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
View File
@@ -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 Quran 1,1,Aqaid: Our Belief,5. The Quran
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 CreatorHis Message,Allah: Our Creator 2,1,The Creator and His Message,1. Allah: Our Creator
2,1,The CreatorHis Message,How Does Allah Create? 2,1,The Creator and His Message,2. How Does Allah Create?
2,1,The CreatorHis Message,Allahﷻ: What Does He Do? 2,1,The Creator and His Message,3. What Does Allah Do?
2,1,The CreatorHis Message,What Does Allahﷻ Not Do 2,1,The Creator and His Message,4, Allah: What Does He Not Do
2,1,The CreatorHis Message,The Quran 2,1,The Creator and His Message,5. The Quran
2,1,The CreatorHis 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. Shuaib (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,Ismail (A) and Ishaq (A) 3,5,Learning About Islam,20. The Kabah
3,5,Messengers of Allah,Dawud (A): A Nabi of Allahﷻ 3,5,Learning About Islam,21. Masjid an-Nabawī: The Nabis 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. Mubilā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,Shaitans Mode of Operation 4,6,General Islamic Topics,26. Shaitans 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 Quran 5,1,The Creator,3. His Message, and His Messengers,The Revelation of the Quran
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 Mans Lifelong Teachings 5,3,Stories of the Messengers of Allah,14. Luqmān (A): A Wise Mans 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 Azizs 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 CreatorHis Message,Attributes of Allahﷻ 6,1,The Creator,3. His Message, and His Messengers,The Revelation of the Quran
6,1,The CreatorHis Message,The Promise of Allahﷻ 6,1,The Creator,4. His Message, and His Messengers,Characteristics of the Messengers
6,2,The Quran and Hadith,Objectives of the Quran? 6,2,The Battles and Other Developments,5. Pledges of Aqabah: Invitation to Migrate
6,2,The Quran and Hadith,Compilation of the Quran 6,2,The Battles and Other Developments,6. The Battle of Badr: Allah Supports the Righteous
6,2,The Quran and Hadith,Previous Scriptures and the Quran 6,2,The Battles and Other Developments,7. The Battle of Uhud: Obey Allah and Obey the Rasul ﷺ
6,2,The Quran 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 Mans Lifelong Advice
6,4,Messengers of Allah,Musa (A) and Firawn 6,3,Stories of the Messengers of Allah,15. Yūsuf (A): His Childhood and Life in Azizs 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 Quran: 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 Quran: 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 Quran,11. The Companions of the Cave
7,2,Stories of the Messengers,Life of Ibrahim (A) 7,3,Stories from the Quran,12. Dhu al-Qarnain: The Journey of a King
7,2,Stories of the Messengers,Sacrifice of Ibrahim (A) 7,3,Stories from the Quran,13. Effective Debate and Negotiation Styles in the Quran
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 Quran,The Companions of the Cave 7,5,Knowledge Enrichment,16. Character of the Messengers
7,3,Stories from the Quran,Dhul Qurnain: Journey of a King 7,5,Knowledge Enrichment,17. Rasūlullāhs Marriages
7,3,Stories from the Quran,Effective Debate and Negotiation Styles in the Quran 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 Quran
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 Marū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 Quran 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 Quran,Amr Bil Maruf 7,6,Akhlaq and Adab in Islam,28. Types of Behavior Allah Loves
7,6,Teachings of the Quran,Guard Your Tongue 8,1,Knowing the Creator,1. Divine Names
7,6,Teachings of the Quran,Islamic Greetings 8,1,Knowing the Creator,2. Sunan of Allah
7,6,Teachings of the Quran,How to Achieve Success 8,1,Knowing the Creator,3. Objectives of the Quran
7,6,Teachings of the Quran,Permitted and Prohibited 8,1,Knowing the Creator,4. Lessons from Sūrah al-Hujurāt
7,6,Teachings of the Quran,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 Quran 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 Quran
8,5,"Islamic Values, Teachings","Hope, Hopefulness, Hopelessness" 8,6,Islam After the Messenger ﷺ,27. Early History of Shiah 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 Quran 9,1,A Reflection on the Divine,2. Pondering the Quran
8,6,Islam After the Rasul (S),Origin and History of Shiah 9,1,A Reflection on the Divine,3. Preservation and Compilation of the Quran
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 Quran 9,2,Islam and Muslim,7. Life Cycle of Truth
9,1,A Reflection on the Divine,Preservation and Compilation of the Quran 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
1 Grade Unit Unit Title chapter
2 1 1 Aqaid: Our Belief Allahﷻ: Our Creator 1. Allah: Our Creator
3 1 1 Aqaid: Our Belief Islam 2. Islam
4 1 1 Aqaid: Our Belief Our Faith 3. Our Faith
5 1 1 Aqaid: Our Belief Nabi Muhammadﷺ 4. Nabi Muhammad (s)
6 1 1 Aqaid: Our Belief The Qur’an 5. The Qur’an
7 1 2 Knowing Allahﷻ Knowing Allah Allahﷻ Loves Us 6. Allah Loves Us
8 1 2 Knowing Allahﷻ Knowing Allah Remembering Allahﷻ 7. Remembering Allah
9 1 2 Knowing Allahﷻ Knowing Allah Allahﷻ Rewards Us 8. Allah Rewards Us
10 1 3 Our Ibadat Five Pillars of Islam 9. Five Pillars of Islam
11 1 3 Our Ibadat Shahadah: The First Pillar 10. Shahadah: The First Pillar
12 1 3 Our Ibadat Salah: The Second Pillar 11. Salat: The Second Pillar
13 1 3 Our Ibadat Zakat: The Third Pillar 12. Zakat: The Third Pillar
14 1 3 Our Ibadat Fasting: The Fourth Pillar 13. Fasting: The Fourth Pillar
15 1 3 Our Ibadat Hajj: The Fifth Pillar 14. Hajj: The Fifth Pillar
16 1 4 Messengers of Allah Adam (A): The First Nabi 15. Adam (A): The First Nabi
17 1 4 Messengers of Allah Nuh (A): Saved From Flood 16. Nuh (A): Saved From the Great Flood
18 1 4 Messengers of Allah Ibrahim (A): Never Listen to Shaitan 17. Ibrahim (A): Never Listen to Shaitan
19 1 4 Messengers of Allah Musa (A): Challenging A Bad Ruler 18. Musa (A): Challenging a Bad Ruler
20 1 4 Messengers of Allah Isa (A): A Great Nabi of Allahﷻ 19. Isa (A): A Great Nabi of Allah
21 1 5 Other Basics of Islam Angels: They Always Work for Allahﷻ 20. Angels: They Always Work for Allah
22 1 5 Other Basics of Islam Shaitan: Our Enemy 21. Shaitan: Our Enemy
23 1 5 Other Basics of Islam Makkah and Madinah 22. Makkah and Madinah
24 1 5 Other Basics of Islam Eid: Two Festivals 23. Eid: Two Festivals
25 1 6 Akhlaq and Adab in Islam Good Manners 24. Good Manners
26 1 6 Akhlaq and Adab in Islam Kindness and Sharing 25. Kindness and Sharing
27 1 6 Akhlaq and Adab in Islam Respect 26. Respect
28 1 6 Akhlaq and Adab in Islam Forgiveness 27. Forgiveness
29 1 6 Akhlaq and Adab in Islam Thanking Allahﷻ 28. Thanking Allah
30 2 1 The Creator—His Message The Creator and His Message Allahﷻ: Our Creator 1. Allah: Our Creator
31 2 1 The Creator—His Message The Creator and His Message How Does Allahﷻ Create? 2. How Does Allah Create?
32 2 1 The Creator—His Message The Creator and His Message Allahﷻ: What Does He Do? 3. What Does Allah Do?
33 2 1 The Creator—His Message The Creator and His Message What Does Allahﷻ Not Do 4
34 2 1 The Creator—His Message The Creator and His Message The Qur’an 5. The Qur’an
35 2 1 The Creator—His Message The Creator and His Message Hadith and Sunnah 6. Hadith and Sunnah
36 2 2 Our Ibadat Shahadah: The First Pillar 7. Shahadah: The First Pillar
37 2 2 Our Ibadat Salah: The Second Pillar 8. Salat: The Second Pillar
38 2 2 Our Ibadat Zakah: The Third Pillar 9. Zakah: The Third Pillar
39 2 2 Our Ibadat Sawm: The Fourth Pillar 10. Sawm: The Fourth Pillar
40 2 2 Our Ibadat Hajj: The Fifth Pillar 11. Hajj: The Fifth Pillar
41 2 2 Our Ibadat Wudu: Keeping Our Bodies Clean 12. Wudu: Cleaning Before Salat
42 2 3 Messengers of Allah The Messengers of Allah Ibrahim (A): A Friend of Allah 13. Ibrahim (A): A Friend of Allah
43 2 3 Messengers of Allah The Messengers of Allah Yaqub (A) and Yusuf (A) 14. Yaqub (A) and Yusuf (A)
44 2 3 Messengers of Allah The Messengers of Allah Musa (A) and Harun (A) 15. Musa (A) and Harun (A)
45 2 3 Messengers of Allah The Messengers of Allah Yunus (A) 16. Yunus (A)
46 2 3 Messengers of Allah The Messengers of Allah Nabi Muhammadﷺ 17. Nabi Muhammad ﷺ
47 2 4 Learning About Islam Obey Allahﷻ Obey Rasulﷺ 18. Obey Allah
48 2 4 Learning About Islam Day of Judgment 19. Day of Judgment
49 2 4 Learning About Islam Our Masjid 20. Our Masjid
50 2 4 Learning About Islam Islamic Phrases 21. Islamic Phrases
51 2 4 Learning About Islam Food that We May Eat 22. Food that We May Eat
52 2 5 Akhlaq and Adab in Islam Truthfulness 23. Truthfulness
53 2 5 Akhlaq and Adab in Islam Kindness 24. Kindness
54 2 5 Akhlaq and Adab in Islam Respect 25. Respect
55 2 5 Akhlaq and Adab in Islam Responsibility 26. Responsibility
56 2 5 Akhlaq and Adab in Islam Obedience 27. Obedience
57 2 5 Akhlaq and Adab in Islam Cleanliness 28. Cleanliness
58 2 5 Akhlaq and Adab in Islam Honesty 29. Honesty
59 3 1 Knowing About Allah Knowing About Allāh ﷺ What Does Allahﷻ Do? 1. Who Is Allāh ﷺ?
60 3 1 Knowing About Allah Knowing About Allāh ﷺ What Allahﷻ Is and Is Not 2. What Allāh ﷺ Is and Is Not
61 3 1 Knowing About Allah Knowing About Allāh ﷺ Allahﷻ: The Most-Merciful 3. Allāh ﷺ: The Most-Merciful
62 3 1 Knowing About Allah Knowing About Allāh ﷺ Allahﷻ: The Best Judge 4. Allāh ﷺ: The Best Judge
63 3 2 1 What Islam Says Knowing About Allāh ﷺ We Are Muslims: We Have ‘Iman 5. What Does Allāh ﷺ Want Us to Do?
64 3 2 What Islam Says Teachings of Islam What Does Allahﷻ Want Us to Do? 6. We Are Muslims: We Have ‘Īmān
65 3 2 What Islam Says Teachings of Islam Hadith 7. Belief in the Qur’ān
66 3 2 What Islam Says Teachings of Islam Jinn 8. Belief in the Messengers
67 3 2 What Islam Says Teachings of Islam Muslims in North America 9. Hadīth and Sunnah
68 3 2 What Islam Says Teachings of Islam The Right Path: The Straight Path 10. Jinn
69 3 3 2 Why Do We Worship Teachings of Islam Shahadah: Allahﷻ is One 11. Muslims in North America
70 3 3 2 Why Do We Worship Teachings of Islam Types of Salat 12. The Straight Path: The Right Path
71 3 3 Why Do We Worship Nabi Muhammad ﷺ Why We Make Salat 13. Kindness of Rasūlullāh ﷺ
72 3 3 Why Do We Worship Nabi Muhammad ﷺ Why Do We pay Zakat? 14. How Rasūlullāh ﷺ Treated Others
73 3 3 Why Do We Worship Nabi Muhammad ﷺ Why Do We Fast? 15. Our Relationship With Rasūlullāh ﷺ
74 3 3 4 Why Do We Worship Messengers of Allāh ﷺ Why Do We Go for Hajj? 16. Ismā‘īl (A) and Ishāq (A): Nabi of Allāh ﷺ
75 3 4 Life of Nabi Muhammadﷺ Messengers of Allāh ﷺ The Nabiﷺ in Makkah 17. Shu‘aib (A): A Nabi of Allāh ﷺ
76 3 4 Life of Nabi Muhammadﷺ Messengers of Allāh ﷺ The Nabiﷺ in Madinah 18. Dāwūd (A): A Nabi of Allāh ﷺ
77 3 4 Life of Nabi Muhammadﷺ Messengers of Allāh ﷺ How Rasulullahﷺ Treated Others 19. ‘Īsā (A): A Nabi of Allāh ﷺ
78 3 5 Messengers of Allah Learning About Islam Isma‘il (A) and Ishaq (A) 20. The Ka‘bah
79 3 5 Messengers of Allah Learning About Islam Dawud (A): A Nabi of Allahﷻ 21. Masjid an-Nabawī: The Nabi’s Masjid
80 3 5 Messengers of Allah Learning About Islam ‘Isa (A): A Nabi of Allahﷻ 22. Bilāl ibn Rabāh
81 3 6 5 Akhlaq and Adab in Islam Learning About Islam Being Kind: A Virtue of the Believers 23. Zaid ibn Hārithah
82 3 6 Akhlaq and Adab in Islam Forgiveness: A Good Quality 24. Ways To Be a Good Person
83 3 6 Akhlaq and Adab in Islam Good Deeds: A Duty of the Believers 25. Kindness: A Virtue of the Believers
84 3 6 Akhlaq and Adab in Islam Cleanliness: A Quality of Believers 26. Forgiveness: A Quality of the Believers
85 3 6 Akhlaq and Adab in Islam A Muslim Family 27. Good Deeds: A Duty of the Believers
86 3 6 Akhlaq and Adab in Islam Perseverance: Never Give Up 28. Perseverance: Never Give Up
87 3 6 Akhlaq and Adab in Islam Punctuality: Doing Things on Time 29. Punctuality: Doing Things on Time
88 4 1 Knowing the Creator Rewards of Allahﷻ: Everybody Receives Them 1. Rewards of Allah: Everybody Receives Them
89 4 1 Knowing the Creator Discipline of Allahﷻ 1. Discipline of Allah: Because He Loves Us
90 4 1 Knowing the Creator Names of Allahﷻ 3. Names of Allah
91 4 1 Knowing the Creator Books of Allahﷻ 4. Books of Allah
92 4 2 How Islam Changed Arabia Pre-Islamic Arabia 5. Pre-Islamic Arabia: Age of Ignorance
93 4 2 How Islam Changed Arabia The Year of the Elephant 6. The Year of the Elephant
94 4 2 How Islam Changed Arabia Early Life of Muhammadﷺ 7. Early Life of Muhammad ﷺ
95 4 2 How Islam Changed Arabia Life Before Becoming a Nabi 8. Life Before Becoming a Nabi
96 4 2 How Islam Changed Arabia First Revelation 9. First Revelation
97 4 2 How Islam Changed Arabia Makkah Period 10. Makkah Period: The Early Years of the Muslims
98 4 2 How Islam Changed Arabia Hijrat to Madinah 11. Hijrat to Madinah: The Migration that Shaped History
99 4 2 How Islam Changed Arabia Madinah Period 12. Madinah Period: Islam Prospers
100 4 3 The Rightly Guided Khalifah Abu Bakr: The First Khalifah 13. Abū Bakr (R): The First Khalifah
101 4 3 The Rightly Guided Khalifah ‘Umar ibn al-Khattab 14. ‘Umar al-Khaṭṭāb (R): The Second Khalifah
102 4 3 The Rightly Guided Khalifah ‘Uthman ibn ‘Affan 15. ‘Uthman Ibn ‘Affān (R): The Third Khalifah
103 4 3 The Rightly Guided Khalifah ‘Ali ibn Abu Talib 16. ‘Ali Ibn Abu Ṭālib (R): The Fourth Khalifah
104 4 4 The Messengers of Allah Messengers of Allah Hud (A): Struggle to Guide People 17. Hūd (A): Struggle to Guide Mankind
105 4 4 The Messengers of Allah Messengers of Allah Salih (A): To Guide the Misguided 18. Ṣāliḥ (A): Struggle to Guide the Misguided
106 4 4 The Messengers of Allah Messengers of Allah Musa (A): His Life and Actions 19. Mūsā (A): His Life and Achievements
107 4 4 The Messengers of Allah Messengers of Allah Sulaiman (A): A Humble King 20. Sulaimān (A): A King and a Servant of Allah ﷺ
108 4 5 Fiqh of Salat Preparation for Salat 21. Preparation for Salat
109 4 5 Fiqh of Salat Requirements of Salat 22. The Requirements of Salat
110 4 5 Fiqh of Salat Mubtilat us-Salat 23. Mubṭilāt-us-Salāt: Things that Invalidate Salāt
111 4 5 Fiqh of Salat How to Pray Behind an Imam 24. How to Pray Behind an Imām
112 4 6 General Islamic Topics Compilers of Hadith 25. Compilers of Hadīth
113 4 6 General Islamic Topics Shaitan’s Mode of Operation 26. Shaitan’s Mode of Operation
114 4 6 General Islamic Topics Day of Judgment 27. Day of Judgment: The Day of Ultimate Justice
115 4 6 General Islamic Topics Eid: Its Significance 28. ‘Eid: Significance of the Festivities
116 4 6 General Islamic Topics Truthfulness: A Quality of Muslim 29. Truthfulness: An Important Quality for Muslims
117 4 6 General Islamic Topics Perseverance: Keep on Trying 30. Perseverance: Keep on Trying
118 5 1 The Creator, His Message The Creator Tawhid, Kafir, Kufr, Shirk, Nifaq 1. His Message
119 5 1 The Creator, His Message The Creator Why Should We Worship Allahﷻ? 2. His Message
120 5 1 The Creator, His Message The Creator Revelation of the Qur’an 3. His Message
121 5 1 The Creator, His Message The Creator Characteristics of the Messengers 4. His Message
122 5 2 The Battles, Developments The Battles and Other Developments Pledges of ‘Aqabah 5. Pledges of ‘Aqabah: Invitation to Migrate
123 5 2 The Battles, Developments The Battles and Other Developments The Battle of Badr 6. The Battle of Badr: Allah Supports the Righteous
124 5 2 The Battles, Developments The Battles and Other Developments The Battle of Uhud 7. The Battle of Uhud: Obey Allah and Obey the Rasul ﷺ
125 5 2 The Battles, Developments The Battles and Other Developments The Battle of the Trench 8. The Battle of the Trench: A Bloodless Battle
126 5 2 The Battles, Developments The Battles and Other Developments The Treaty of Hudaibiyah 9. The Treaty of Hudaibiyah: A Clear Victory
127 5 2 The Battles, Developments The Battles and Other Developments Liberation of Makkah 10. Liberation of Makkah: A Bloodless Victory
128 5 3 The Messengers of Allah Stories of the Messengers of Allah Adam (A): The Creation of Mankind 11. Adam (A): The Creation of Human Beings
129 5 3 The Messengers of Allah Stories of the Messengers of Allah Ibrahim (A) Debate with Polytheists 12. Ibrahim (A): His Debate with the Polytheists
130 5 3 The Messengers of Allah Stories of the Messengers of Allah Ibrahim (A): Plan Against Idols 13. Ibrahim (A): His Plan Against the Idols
131 5 3 The Messengers of Allah Stories of the Messengers of Allah Luqman (A): A Wise Man’s Lifelong Teachings 14. Luqmān (A): A Wise Man’s Lifelong Advice
132 5 3 The Messengers of Allah Stories of the Messengers of Allah Yusuf (A): His Childhood 15. Yūsuf (A): His Childhood and Life in Aziz’s Home
133 5 3 The Messengers of Allah Stories of the Messengers of Allah Yusuf (A): His Righteousness 16. Yūsuf (A): Standing Up for Righteousness
134 5 3 The Messengers of Allah Stories of the Messengers of Allah Yusuf (A): Dream Comes True 17. Yūsuf (A): A Childhood Dream Comes True
135 5 3 4 The Messengers of Allah Islam in The World Ayyub (A): Patience, Perseverance 20. Major Masājid in the World
136 5 3 5 The Messengers of Allah Islamic Values and Teachings Zakariyyah (A), Yahya (A) 21. Upholding Truth: A Duty of All Believers
137 5 4 5 Islam in the World Islamic Values and Teachings Major Masajid in the World 22. Responsibility and Punctuality
138 5 5 Islamic Values, Teachings Islamic Values and Teachings Upholding Truth: A Duty for All Believers 23. My Mind
139 5 5 Islamic Values, Teachings Islamic Values and Teachings Responsibility and Punctuality 24. Kindness and Forgiveness
140 5 5 Islamic Values, Teachings Islamic Values and Teachings My Mind My Body 25. The Middle Path: Ways to Avoid the Two Extremes
141 5 5 Islamic Values, Teachings Islamic Values and Teachings Kindness and Forgiveness 26. Salat: Its Significance
142 5 5 Islamic Values, Teachings Islamic Values and Teachings The Middle Path: Ways to Avoid Two Extremes 27. Sawm: Its Significance
143 5 5 Islamic Values, Teachings Islamic Values and Teachings Salat: Its Significance 28. Zakat and Sadaqah: Similarities and Differences
144 5 6 5 1 Islamic Values, Teachings The Creator Sawm: Its Significance 1. His Message
145 5 6 5 1 Islamic Values, Teachings The Creator Zakat and Sadaqah: Similarities and Differences 2. His Message
146 6 1 The Creator—His Message The Creator Attributes of Allahﷻ 3. His Message
147 6 1 The Creator—His Message The Creator The Promise of Allahﷻ 4. His Message
148 6 2 The Qur’an and Hadith The Battles and Other Developments Objectives of the Qur’an? 5. Pledges of ‘Aqabah: Invitation to Migrate
149 6 2 The Qur’an and Hadith The Battles and Other Developments Compilation of the Qur’an 6. The Battle of Badr: Allah Supports the Righteous
150 6 2 The Qur’an and Hadith The Battles and Other Developments Previous Scriptures and the Qur’an 7. The Battle of Uhud: Obey Allah and Obey the Rasul ﷺ
151 6 2 The Qur’an and Hadith The Battles and Other Developments Compilation of Hadith 8. The Battle of the Trench: A Bloodless Battle
152 6 3 2 Fundamentals of Deen The Battles and Other Developments Importance of Shahadah 9. The Treaty of Hudaibiyah: A Clear Victory
153 6 3 2 Fundamentals of Deen The Battles and Other Developments Khushu in Salat 10. Liberation of Makkah: A Bloodless Victory
154 6 3 Fundamentals of Deen Stories of the Messengers of Allah Taqwa: A Quality of Believers 11. Adam (A): The Creation of Human Beings
155 6 4 3 Messengers of Allah Stories of the Messengers of Allah Nuh (A) 12. Ibrahim (A): His Debate with the Polytheists
156 6 4 3 Messengers of Allah Stories of the Messengers of Allah Talut, Jalut, and Dawud (A) 13. Ibrahim (A): His Plan Against the Idols
157 6 4 3 Messengers of Allah Stories of the Messengers of Allah Dawud (A) and Sulaiman (A) 14. Luqmān (A): A Wise Man’s Lifelong Advice
158 6 4 3 Messengers of Allah Stories of the Messengers of Allah Musa (A) and Fir‘awn 15. Yūsuf (A): His Childhood and Life in Aziz’s Home
159 6 4 3 Messengers of Allah Stories of the Messengers of Allah Musa (A) and Khidir 16. Yūsuf (A): Standing Up for Righteousness
160 6 4 3 Messengers of Allah Stories of the Messengers of Allah ‘Isa (A) and Maryam (ra) 17. Yūsuf (A): A Childhood Dream Comes True
161 6 5 4 Some Prominent Muslimah Islam in The World Khadijah (ra) 20. Major Masājid in the World
162 6 5 Some Prominent Muslimah Islamic Values and Teachings ‘Aishah (ra) 21. Upholding Truth: A Duty of All Believers
163 6 5 Some Prominent Muslimah Islamic Values and Teachings Fatimah (ra) 22. Responsibility and Punctuality
164 6 5 Some Prominent Muslimah Islamic Values and Teachings Some Prominent Muslimahs 23. My Mind
165 6 6 5 Knowledge Enrichment Islamic Values and Teachings Al-Qiyamah: The Awakening 24. Kindness and Forgiveness
166 6 6 5 Knowledge Enrichment Islamic Values and Teachings Ruh and Nafs: An Overview 25. The Middle Path: Ways to Avoid the Two Extremes
167 6 6 5 Knowledge Enrichment Islamic Values and Teachings Angels and Jinn: An Overview 26. Salat: Its Significance
168 6 6 5 Knowledge Enrichment Islamic Values and Teachings Shaitan: The Invisible Enemy 27. Sawm: Its Significance
169 6 7 5 The Current Society Islamic Values and Teachings My Friend Is Muslim Now 28. Zakat and Sadaqah: Similarities and Differences
170 6 7 7 1 The Current Society The Creator Friendship 1. Why Islam? What is Islam?
171 6 7 7 1 The Current Society The Creator Muslims Around the World 2. Belief in Allah
172 6 7 7 1 The Current Society The Creator People of Other Faith 3. The Qur’an: Its Qualitative Names
173 6 7 8 1 Developing Islamic Values The Creator Greed and Dishonesty 4. Istighfār: Seeking Forgiveness and Protection
174 6 7 8 1 Developing Islamic Values The Creator Avoiding Extravagance 5. Allah: Angry or Kind?
175 7 1 2 The Creator Stories of the Messengers Why Islam? what is Islam? 6. Ādam (A): The Trial of the First Messenger
176 7 1 2 The Creator Stories of the Messengers Belief in Allahﷻ 7. The Life of Ibrāhīm (A): Beginning a Nation
177 7 1 2 The Creator Stories of the Messengers The Qur’an: Its Qualitative Names 8. The Sacrifice of Ibrāhīm (A)
178 7 1 2 The Creator Stories of the Messengers Istighfar: Seeking Forgiveness of Allahﷻ 9. Lūt (A): A Message for Modern Societies
179 7 1 2 The Creator Stories of the Messengers Allahﷻ: Angry or Kind 10. Yūsuf (A): The Will to Overcome Temptation
180 7 2 3 Stories of the Messengers Stories from the Qur’an Adam (A): Trial of the Messenger 11. The Companions of the Cave
181 7 2 3 Stories of the Messengers Stories from the Qur’an Life of Ibrahim (A) 12. Dhu al-Qarnain: The Journey of a King
182 7 2 3 Stories of the Messengers Stories from the Qur’an Sacrifice of Ibrahim (A) 13. Effective Debate and Negotiation Styles in the Qur’an
183 7 2 4 Stories of the Messengers Two Companions Who Shaped Islam Lut (A): Message for Modern Societies 14. Abū Sufyān: His Life and Achievements
184 7 2 4 Stories of the Messengers Two Companions Who Shaped Islam Yusuf (A)—The Will to Overcome Temptation 15. Khālid Ibn al-Walīd: The “Sword of Allah”
185 7 3 5 Stories from the Qur’an Knowledge Enrichment The Companions of the Cave 16. Character of the Messengers
186 7 3 5 Stories from the Qur’an Knowledge Enrichment Dhul Qurnain: Journey of a King 17. Rasūlullāh’s Marriages
187 7 3 5 Stories from the Qur’an Knowledge Enrichment Effective Debate and Negotiation Styles in the Qur’an 18. Lailatul Qadr: The Night of Majesty
188 7 4 5 Two Companions Knowledge Enrichment Abu Sufyan 19. Fasting During Ramadan: The Month of Benefits
189 7 4 5 Two Companions Knowledge Enrichment Khalid Ibn Walid (R) 20. My Family is Muslim Now
190 7 5 Knowledge Enrichment The character of the Messengers 21. Science in the Qur’an
191 7 5 Knowledge Enrichment Rasulullahﷺ Marriages 22. Lessons From Past Civilizations
192 7 5 6 Knowledge Enrichment Akhlaq and Adab in Islam Lailatul Qadr 23. Amr Bil Ma’rūf: Enjoin Good Deeds
193 7 5 6 Knowledge Enrichment Akhlaq and Adab in Islam Fasting During Ramadan 24. Guard Your Tongue: Think Before You Speak
194 7 5 6 Knowledge Enrichment Akhlaq and Adab in Islam My Family is Muslim Now 25. Islamic Greeting: Wishing Peace
195 7 5 6 Knowledge Enrichment Akhlaq and Adab in Islam Science in the Qur’an 26. How to Achieve Success
196 7 5 6 Knowledge Enrichment Akhlaq and Adab in Islam Lessons from Past Civilizations 27. Permitted and Prohibited
197 7 6 Teachings of the Qur’an Akhlaq and Adab in Islam Amr Bil Ma‘ruf 28. Types of Behavior Allah Loves
198 7 8 6 1 Teachings of the Qur’an Knowing the Creator Guard Your Tongue 1. Divine Names
199 7 8 6 1 Teachings of the Qur’an Knowing the Creator Islamic Greetings 2. Sunan of Allah
200 7 8 6 1 Teachings of the Qur’an Knowing the Creator How to Achieve Success 3. Objectives of the Qur’an
201 7 8 6 1 Teachings of the Qur’an Knowing the Creator Permitted and Prohibited 4. Lessons from Sūrah al-Hujurāt
202 7 8 6 1 Teachings of the Qur’an Knowing the Creator Types of Behavior Allahﷻ Loves 5. True Piety: A Synthesis of Belief
203 8 1 Knowing the Creator Divine Names 6. Āyatul Kursi: The Throne Verse
204 8 1 2 Knowing the Creator Knowing the Messenger ﷺ Sunan of Allahﷻ 7. The Person Muhammad ﷺ
205 8 1 2 Knowing the Creator Knowing the Messenger ﷺ Objectives of the Qur’an 8. Farewell Pilgrimage
206 8 1 2 Knowing the Creator Knowing the Messenger ﷺ Surah Hujurat: Its Teachings 9. Finality of Prophethood
207 8 1 2 Knowing the Creator Knowing the Messenger ﷺ True Piety: Analysis of Verse 2:177 10. Hadith: Collection and Classification
208 8 1 3 Knowing the Creator Challenges in Madinah Ayatul Kursi 11. Hypocrites
209 8 2 3 Knowing the Messengerﷺ Challenges in Madinah The Person Muhammadﷺ 12. Banu Qaynuqa: Threat Within Madinah
210 8 2 3 Knowing the Messengerﷺ Challenges in Madinah Farewell Pilgrimage 13. Banu Nadir: Treachery Within Madinah
211 8 2 3 Knowing the Messengerﷺ Challenges in Madinah Finality of Prophethood 14. Banu Qurayzah
212 8 2 3 Knowing the Messengerﷺ Challenges in Madinah Hadith: Collection, Classification 15. Mission to Tabūk: A Test of Steadfastness
213 8 3 4 Challenges in Madinah Islamic Ethical Framework Hypocrites 16. Friends and Friendship: Who is a Good Friend?
214 8 3 4 Challenges in Madinah Islamic Ethical Framework Banu Qaynuqa 17. Friendship With Non-Muslims
215 8 3 4 Challenges in Madinah Islamic Ethical Framework Banu Nadir 18. Dating: How Islam Views the Practice
216 8 3 4 Challenges in Madinah Islamic Ethical Framework Banu Qurayzah 19. Hold Firmly the Rope of Allah
217 8 3 4 Challenges in Madinah Islamic Ethical Framework Mission to Tabuk 20. Elements of a Bad Life
218 8 4 5 Islamic Ethical Framework Islamic Values and Teachings Friends and Friendship 21. Duties Towards Parents
219 8 4 5 Islamic Ethical Framework Islamic Values and Teachings Friendship With Non-Muslims 22. Hope
220 8 4 5 Islamic Ethical Framework Islamic Values and Teachings Dating in Islam 23. Trials in Life: Everyone Will Experience Them
221 8 4 5 Islamic Ethical Framework Islamic Values and Teachings Hold Firmly the Rope of Allah 24. Permitted and Prohibited Food
222 8 4 5 Islamic Ethical Framework Islamic Values and Teachings Elements of Bad Life 25. Performance of Hajj
223 8 5 Islamic Values, Teachings Islamic Values and Teachings Duties Toward Parents 26. Parables in the Qur’an
224 8 5 6 Islamic Values, Teachings Islam After the Messenger ﷺ Hope, Hopefulness, Hopelessness 27. Early History of Shi‘ah Muslims
225 8 5 6 Islamic Values, Teachings Islam After the Messenger ﷺ Trials in Life 28. Umayyad Dynasty
226 8 5 6 Islamic Values, Teachings Islam After the Messenger ﷺ Permitted and Prohibited Food 29. Abbasid Dynasty
227 8 9 5 1 Islamic Values, Teachings A Reflection on the Divine Performance of Hajj 1. Signs of Allahﷻ in Nature
228 8 9 5 1 Islamic Values, Teachings A Reflection on the Divine Parables in the Qur’an 2. Pondering the Qur’an
229 8 9 6 1 Islam After the Rasul (S) A Reflection on the Divine Origin and History of Shi‘ah 3. Preservation and Compilation of the Qur’an
230 8 9 6 1 Islam After the Rasul (S) A Reflection on the Divine Ummayad Dynasty 4. Ibadat—Easy Ways to Do It
231 8 9 6 1 Islam After the Rasul (S) A Reflection on the Divine Abbasid Dynasty 5. Surah Baqarah—Statement of Faith and Commitment
232 9 1 2 A Reflection on the Divine Islam and Muslim Signs of Allahﷻ in Nature 6. Why Human Beings Are Superior
233 9 1 2 A Reflection on the Divine Islam and Muslim Pondering the Qur’an 7. Life Cycle of Truth
234 9 1 2 A Reflection on the Divine Islam and Muslim Preservation and Compilation of the Qur’an 8. Is Islam a Violent Religion?
235 9 1 2 A Reflection on the Divine Islam and Muslim Ibadat—Easy Ways to Do It 9. Present Life: Vanity
236 9 1 2 A Reflection on the Divine Islam and Muslim Surah Baqarah—Statement of Faith and Commitment 10. Shariah
237 9 2 Islam and Muslim Why Human Beings Are Superior 11. Justice in Islam
238 9 2 3 Islam and Muslim Ethical Standard in Islam Life Cycle of Truth 12. Choices We Make
239 9 2 3 Islam and Muslim Ethical Standard in Islam Is Islam a Violent Religion? 13. Peer Pressure
240 9 2 3 Islam and Muslim Ethical Standard in Islam Present Life: Vanity, Deception, Play 14. Islamic Perspective on Dating
241 9 2 3 Islam and Muslim Ethical Standard in Islam Shariah 15. Indecency
242 9 2 3 Islam and Muslim Ethical Standard in Islam Justice in Islam 16. Alcohol and Gambling
243 9 3 Ethical Standard in Islam Choices We Make 17. Permitted and Prohibited Food
244 9 3 Ethical Standard in Islam Peer Pressure 18. Food of the People of the Book
245 9 3 Ethical Standard in Islam Islamic Perspective on Dating 19. Let Ramadan Bring The Best in Us
246 9 3 4 Ethical Standard in Islam Essays on Rasulullahﷺ Indecency 20. Khadijah (ra)
247 9 3 4 Ethical Standard in Islam Essays on Rasulullahﷺ Alcohol and Gambling 21. Rasulullahﷺ Multiple Marriages
248 9 3 4 Ethical Standard in Islam Essays on Rasulullahﷺ Permitted and Prohibited Food 22. Marriage to Zainab (ra)
249 9 3 4 Ethical Standard in Islam Essays on Rasulullahﷺ Food of the People of the Book 23. Rasulullahﷺ: A Great Army General
250 9 3 4 Ethical Standard in Islam Essays on Rasulullahﷺ Let Ramadan Bring The Best in Us 24. Prophecy of Muhammadﷺ in the Bible
251 9 4 Essays on Rasulullahﷺ Khadijah (ra) 25. Allegations Against Rasulullahﷺ
252 9 4 5 Essays on Rasulullahﷺ Faith-Based Wealth Building Rasulullahﷺ Multiple Marriages 26. Faith Based Wealth Building
253 9 4 5 Essays on Rasulullahﷺ Faith-Based Wealth Building Marriage to Zainab (ra) 27. Earn
254 9 4 5 Essays on Rasulullahﷺ Faith-Based Wealth Building Rasulullahﷺ: A Great Army General 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
-45
View File
@@ -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
1 Grade Surah
2 1 Al-Fatihah
3 1 An-Nas
4 1 Al-Falaq
5 1 Al-Ikhlas
6 2 Al-Masad
7 2 An-Nasr
8 2 Al-Kafirun
9 2 Al-Kawthar
10 2 Al-Ma'un
11 3 Quraysh
12 3 Al-Fil
13 3 Al-Humazah
14 3 Al-'Asr
15 3 At-Takathur
16 4 Al-Qari'ah
17 4 Al-'Adiyat
18 4 Az-Zalzalah
19 4 Al-Bayyinah
20 4 Al-Qadr
21 5 Al-'Alaq
22 5 At-Tin
23 5 Ash-Sharh
24 5 Ad-Duhaa
25 5 Al-Layl
26 6 Ash-Shams
27 6 Al-Balad
28 6 Al-Fajr
29 6 Al-Ghashiyah
30 6 Al-A'la
31 7 At-Tariq
32 7 Al-Buruj
33 7 Al-Inshiqaq
34 7 Al-Mutaffifin
35 7 Al-Infitar
36 8 At-Takwir
37 8 Abasa
38 8 Al-Mursalat
39 8 An-Naba
40 9 Al-Mulk
41 9 Al-Qalam
42 9 Al-Haqqah
43 9 Al-Ma'arij
44 9 Nuh
45 9 Al-Jinn
BIN
View File
Binary file not shown.
File diff suppressed because one or more lines are too long