Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35988e9ce1 | |||
| 5fc4d8be30 | |||
| dcfd1f9542 | |||
| b99f816261 | |||
| 931e30b6f1 | |||
| 189bb1a628 | |||
| f04934039e | |||
| c13dcb07d1 | |||
| dd1d492c30 | |||
| a448288878 | |||
| 821e9cdc81 | |||
| 4e2d12e524 | |||
| 7bc999643a | |||
| 89e95b7851 | |||
| 2543df3d33 | |||
| 112d073235 | |||
| 2611730ec6 | |||
| 05dad52e10 | |||
| d158650be9 | |||
| 61facee902 |
@@ -1,184 +0,0 @@
|
||||
#--------------------------------------------------------------------
|
||||
# Example Environment Configuration file
|
||||
#
|
||||
# This file can be used as a starting point for your own
|
||||
# custom .env files, and contains most of the possible settings
|
||||
# available in a default install.
|
||||
#
|
||||
# By default, all of the settings are commented out. If you want
|
||||
# to override the setting, you must un-comment it by removing the '#'
|
||||
# at the beginning of the line.
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# ENVIRONMENT
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
CI_ENVIRONMENT = development
|
||||
|
||||
# Which profile to use if none is supplied to sendEmail()
|
||||
# Options: default, communication, payment, ...
|
||||
MAIL_PROFILE_DEFAULT=default
|
||||
|
||||
# ===== DEFAULT (system) =====
|
||||
# ---- Legacy fallbacks (kept for compatibility) ----
|
||||
SMTP_HOST = smtp.gmail.com
|
||||
SMTP_USER = alrahma.sunday.school@gmail.com
|
||||
SMTP_PASS = "psnp emdq dykw ypul"
|
||||
SMTP_PORT = 587
|
||||
SMTP_ENCRYPTION = tls # was ssl; use tls for port 587
|
||||
|
||||
# Reply-To for all mail profiles
|
||||
MAIL_DEFAULT_REPLY_TO=alrahma.isgl@gmail.com
|
||||
MAIL_DEFAULT_REPLY_TO_NAME="Al Rahma Sunday School"
|
||||
MAIL_COMMUNICATION_REPLY_TO=alrahma.isgl@gmail.com
|
||||
MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications"
|
||||
MAIL_PAYMENT_REPLY_TO=alrahma.isgl@gmail.com
|
||||
MAIL_PAYMENT_REPLY_TO_NAME="School Payments"
|
||||
|
||||
# Optional sender directory you already use elsewhere
|
||||
MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma No-Reply\"},\
|
||||
\"registration\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Register Office\"},\
|
||||
\"notifications\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Notifications\"},\
|
||||
\"finance\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Finance Office\"}}"
|
||||
|
||||
# Principal notification recipient for TimeOff requests (comma-separated allowed)
|
||||
PRINCIPAL_EMAIL="melabidi@alrahmaisgl.org"
|
||||
MAIL_PARENT_REPORT_TO="alrahma.isgl@gmail.com"
|
||||
MAIL_FROM_ADDRESS = "alrahma.sunday.school@gmail.com"
|
||||
|
||||
session.expiration = 43200
|
||||
#--------------------------------------------------------------------
|
||||
# APP
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# If you have trouble with `.`, you could also use `_`.
|
||||
app_baseURL = 'http://localhost:8080/'
|
||||
# app.forceGlobalSecureRequests = true
|
||||
# app.CSPEnabled = false
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# DATABASE
|
||||
#--------------------------------------------------------------------
|
||||
database.default.hostname = 127.0.0.1
|
||||
database.default.database = school
|
||||
database.default.username = root
|
||||
database.default.password = rootpassword
|
||||
database.default.DBDriver = MySQLi
|
||||
database.default.DBPrefix =
|
||||
database.default.port = 3306
|
||||
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# CONTENT SECURITY POLICY
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# contentsecuritypolicy.reportOnly = false
|
||||
# contentsecuritypolicy.defaultSrc = 'none'
|
||||
# contentsecuritypolicy.scriptSrc = 'self'
|
||||
# contentsecuritypolicy.styleSrc = 'self'
|
||||
# contentsecuritypolicy.imageSrc = 'self'
|
||||
# contentsecuritypolicy.baseURI = null
|
||||
# contentsecuritypolicy.childSrc = null
|
||||
# contentsecuritypolicy.connectSrc = 'self'
|
||||
# contentsecuritypolicy.fontSrc = null
|
||||
# contentsecuritypolicy.formAction = null
|
||||
# contentsecuritypolicy.frameAncestors = null
|
||||
# contentsecuritypolicy.frameSrc = null
|
||||
# contentsecuritypolicy.mediaSrc = null
|
||||
# contentsecuritypolicy.objectSrc = null
|
||||
# contentsecuritypolicy.pluginTypes = null
|
||||
# contentsecuritypolicy.reportURI = null
|
||||
# contentsecuritypolicy.sandbox = false
|
||||
# contentsecuritypolicy.upgradeInsecureRequests = false
|
||||
# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}'
|
||||
# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}'
|
||||
# contentsecuritypolicy.autoNonce = true
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# COOKIE
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# cookie.prefix = ''
|
||||
# cookie.expires = 0
|
||||
# cookie.path = '/'
|
||||
# cookie.domain = ''
|
||||
# cookie.secure = false
|
||||
# cookie.httponly = false
|
||||
# cookie.samesite = 'Lax'
|
||||
# cookie.raw = false
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# ENCRYPTION
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# encryption.key =
|
||||
# encryption.driver = OpenSSL
|
||||
# encryption.blockSize = 16
|
||||
# encryption.digest = SHA512
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# HONEYPOT
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# honeypot.hidden = 'true'
|
||||
# honeypot.label = 'Fill This Field'
|
||||
# honeypot.name = 'honeypot'
|
||||
# honeypot.template = '<label>{label}</label><input type="text" name="{name}" value=""/>'
|
||||
# honeypot.container = '<div style="display:none">{template}</div>'
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# SECURITY
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# security.csrfProtection = 'cookie'
|
||||
# security.tokenRandomize = false
|
||||
# security.tokenName = 'csrf_token_name'
|
||||
# security.headerName = 'X-CSRF-TOKEN'
|
||||
# security.cookieName = 'csrf_cookie_name'
|
||||
# security.expires = 7200
|
||||
# security.regenerate = true
|
||||
# security.redirect = false
|
||||
# security.samesite = 'Lax'
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# SESSION
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
|
||||
# session.cookieName = 'ci_session'
|
||||
# session.expiration = 7200
|
||||
# session.savePath = null
|
||||
# session.matchIP = false
|
||||
# session.timeToUpdate = 300
|
||||
# session.regenerateDestroy = false
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# LOGGER
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
logger.threshold = 4
|
||||
MAIL_DEBUG = true
|
||||
MAIL_TIMEOUT = 15
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# CURLRequest
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# curlrequest.shareOptions = false
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
mail.protocol = smtp
|
||||
mail.SMTPHost = ${SMTP_HOST}
|
||||
mail.SMTPUser = ${SMTP_USER}
|
||||
mail.SMTPPass = ${SMTP_PASS}
|
||||
mail.SMTPPort = ${SMTP_PORT}
|
||||
mail.SMTPCrypto = tls
|
||||
|
||||
mail.fromEmail = ${SMTP_USER}
|
||||
mail.fromName = "Al Rahma Sunday School"
|
||||
mail.mailType = text
|
||||
mail.charset = UTF-8
|
||||
@@ -23,17 +23,32 @@
|
||||
|
||||
/public/build/
|
||||
/public/hot/
|
||||
/public/uploads/**
|
||||
!/public/uploads/**/index.html
|
||||
!/public/uploads/.gitkeep
|
||||
|
||||
/uploads/**
|
||||
!/uploads/**/index.html
|
||||
!/uploads/.gitkeep
|
||||
|
||||
docker-compose.override.yml
|
||||
docker-compose.local.yml
|
||||
|
||||
*.log
|
||||
.swp
|
||||
.swo
|
||||
*~
|
||||
.tmp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
.codex/
|
||||
|
||||
/coverage/
|
||||
/build/
|
||||
/builds
|
||||
/phpunit.xml.cache
|
||||
/.phpunit.result.cache
|
||||
/.phpunit.result.cache
|
||||
/writable/reports/*
|
||||
/writable/test.txt
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Controllers\View\EmailController;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\TeacherSubmissionNotificationHistoryModel;
|
||||
use App\Models\UserModel;
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use Config\Database;
|
||||
|
||||
class SendExamDraftDeadlineReminders extends BaseCommand
|
||||
{
|
||||
protected $group = 'Exam Drafts';
|
||||
protected $name = 'exam-drafts:deadline-reminders';
|
||||
protected $description = 'Sends exam draft deadline reminders to teachers who have not submitted drafts.';
|
||||
|
||||
public function run(array $params)
|
||||
{
|
||||
helper('date');
|
||||
|
||||
$configModel = new ConfigurationModel();
|
||||
$schoolYear = (string) ($configModel->getConfig('school_year') ?? '');
|
||||
$semester = (string) ($configModel->getConfig('semester') ?? '');
|
||||
$deadlineValue = trim((string) ($configModel->getConfig('exam_draft_deadline') ?? ''));
|
||||
if ($deadlineValue === '') {
|
||||
CLI::write('exam_draft_deadline is not configured.', 'yellow');
|
||||
return;
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone(config('App')->appTimezone ?? 'UTC');
|
||||
try {
|
||||
$deadline = new \DateTimeImmutable($deadlineValue, $tz);
|
||||
} catch (\Throwable $e) {
|
||||
CLI::write('Invalid exam_draft_deadline value.', 'red');
|
||||
return;
|
||||
}
|
||||
$deadline = $deadline->setTime(0, 0, 0);
|
||||
$today = new \DateTimeImmutable('now', $tz);
|
||||
$today = $today->setTime(0, 0, 0);
|
||||
|
||||
if ($today > $deadline) {
|
||||
CLI::write('Deadline has passed. No reminders sent.', 'yellow');
|
||||
return;
|
||||
}
|
||||
|
||||
$daysToDeadline = (int) $today->diff($deadline)->format('%r%a');
|
||||
if (!$this->shouldSendOnDay($daysToDeadline)) {
|
||||
CLI::write('No reminder scheduled for today.', 'yellow');
|
||||
return;
|
||||
}
|
||||
|
||||
$db = Database::connect();
|
||||
$teacherClassRows = $db->table('teacher_class')
|
||||
->select('teacher_id, class_section_id, school_year, semester')
|
||||
->when($schoolYear !== '', static function ($builder) use ($schoolYear) {
|
||||
return $builder->where('school_year', $schoolYear);
|
||||
})
|
||||
->when($semester !== '', static function ($builder) use ($semester) {
|
||||
return $builder->where('semester', $semester);
|
||||
})
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($teacherClassRows)) {
|
||||
CLI::write('No teacher assignments found.', 'yellow');
|
||||
return;
|
||||
}
|
||||
|
||||
$draftTable = 'exam_drafts';
|
||||
$fields = $db->getFieldNames($draftTable);
|
||||
$teacherColumn = in_array('teacher_id', $fields, true) ? 'teacher_id' : 'author_id';
|
||||
$hasStatusColumn = in_array('status', $fields, true);
|
||||
$hasIsLegacyColumn = in_array('is_legacy', $fields, true);
|
||||
|
||||
$draftsQuery = $db->table($draftTable)
|
||||
->select("{$teacherColumn} AS teacher_id, class_section_id")
|
||||
->when($schoolYear !== '', static function ($builder) use ($schoolYear) {
|
||||
return $builder->where('school_year', $schoolYear);
|
||||
})
|
||||
->when($semester !== '', static function ($builder) use ($semester) {
|
||||
return $builder->where('semester', $semester);
|
||||
});
|
||||
|
||||
if ($hasStatusColumn) {
|
||||
$draftsQuery = $draftsQuery->where('status !=', 'legacy');
|
||||
}
|
||||
if ($hasIsLegacyColumn) {
|
||||
$draftsQuery = $draftsQuery->where('is_legacy', 0);
|
||||
}
|
||||
|
||||
$draftRows = $draftsQuery->get()->getResultArray();
|
||||
$submittedMap = [];
|
||||
foreach ($draftRows as $row) {
|
||||
$key = (int) ($row['teacher_id'] ?? 0) . '|' . (int) ($row['class_section_id'] ?? 0);
|
||||
$submittedMap[$key] = true;
|
||||
}
|
||||
|
||||
$missingByTeacher = [];
|
||||
foreach ($teacherClassRows as $row) {
|
||||
$teacherId = (int) ($row['teacher_id'] ?? 0);
|
||||
$classSectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
if ($teacherId <= 0 || $classSectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$key = $teacherId . '|' . $classSectionId;
|
||||
if (!isset($submittedMap[$key])) {
|
||||
$missingByTeacher[$teacherId][] = $classSectionId;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($missingByTeacher)) {
|
||||
CLI::write('All teachers have submitted drafts.', 'green');
|
||||
return;
|
||||
}
|
||||
|
||||
$userModel = new UserModel();
|
||||
$classSectionModel = new ClassSectionModel();
|
||||
$historyModel = new TeacherSubmissionNotificationHistoryModel();
|
||||
$mailer = new EmailController();
|
||||
|
||||
$classSections = $classSectionModel
|
||||
->select('class_section_id, class_section_name')
|
||||
->findAll();
|
||||
$classLookup = [];
|
||||
foreach ($classSections as $section) {
|
||||
$classLookup[(int) ($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? '';
|
||||
}
|
||||
|
||||
$todayStamp = $today->format('Y-m-d');
|
||||
foreach ($missingByTeacher as $teacherId => $sectionIds) {
|
||||
$teacher = $userModel->find($teacherId);
|
||||
$email = $teacher['email'] ?? '';
|
||||
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$alreadySent = $historyModel
|
||||
->where('teacher_id', $teacherId)
|
||||
->where('notification_category', 'exam_draft_deadline')
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->like('sent_at', $todayStamp)
|
||||
->countAllResults() > 0;
|
||||
if ($alreadySent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classNames = array_map(static function ($id) use ($classLookup) {
|
||||
return $classLookup[(int) $id] ?? "Class {$id}";
|
||||
}, $sectionIds);
|
||||
$classList = implode(', ', $classNames);
|
||||
$teacherName = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
|
||||
if ($teacherName === '') {
|
||||
$teacherName = 'Teacher';
|
||||
}
|
||||
|
||||
$subject = 'Reminder: Exam draft submission deadline';
|
||||
$body = '<p>Dear ' . esc($teacherName) . ',</p>'
|
||||
. '<p>This is a reminder to submit your exam draft(s) for: ' . esc($classList) . '.</p>'
|
||||
. '<p>Deadline: <strong>' . esc($deadline->format('Y-m-d')) . '</strong></p>'
|
||||
. '<p>Please submit your draft at <a href="' . esc(base_url('teacher/exam-drafts')) . '">Teacher Exam Drafts</a>.</p>'
|
||||
. '<p>Thank you.</p>';
|
||||
|
||||
$status = 'failed';
|
||||
if ($mailer->sendEmail($email, $subject, $body, 'notifications')) {
|
||||
$status = 'sent';
|
||||
CLI::write("Sent reminder to {$email}", 'green');
|
||||
}
|
||||
|
||||
$historyModel->insert([
|
||||
'teacher_id' => $teacherId,
|
||||
'class_section_id' => 0,
|
||||
'admin_id' => null,
|
||||
'notification_category' => 'exam_draft_deadline',
|
||||
'message' => strip_tags($body),
|
||||
'status' => $status,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'sent_at' => utc_now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldSendOnDay(int $daysToDeadline): bool
|
||||
{
|
||||
if ($daysToDeadline < 0) {
|
||||
return false;
|
||||
}
|
||||
if ($daysToDeadline <= 4) {
|
||||
return true;
|
||||
}
|
||||
return in_array($daysToDeadline, [28, 21, 14, 7], true);
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ class App extends BaseConfig
|
||||
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
||||
|
|
||||
*/
|
||||
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
|
||||
public string $permittedURIChars = 'a-z 0-9~%.:_\-,';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
|
||||
@@ -363,6 +363,7 @@ $routes->get('/teacher/teacher_contactus', 'View\TeacherController::contactusTea
|
||||
|
||||
$routes->get('/teacher/exam-drafts', 'View\ExamDraftController::teacherIndex', ['filter' => 'auth:teacher,teacher_assistant,teacher_dashboard,read']);
|
||||
$routes->post('/teacher/exam-drafts', 'View\ExamDraftController::teacherStore', ['filter' => 'auth:teacher,teacher_assistant,teacher_dashboard,read']);
|
||||
$routes->get('/teacher/exam-drafts/status', 'View\ExamDraftController::teacherStatusFeed', ['filter' => 'auth:teacher,teacher_assistant,teacher_dashboard,read']);
|
||||
|
||||
|
||||
|
||||
@@ -534,9 +535,11 @@ $routes->post('payment/event_charges', 'View\EventController::eventUpdate');
|
||||
|
||||
// Parent event participation
|
||||
$routes->get('administrator/event-charges', 'View\EventController::eventShow');
|
||||
$routes->post('administrator/event-charges/remove/(:num)', 'View\EventController::removeCharge/$1');
|
||||
$routes->post('administrator/event-charges/payment/(:num)', 'View\EventController::toggleEventPayment/$1');
|
||||
$routes->get('administrator/get-students-with-charges', 'View\EventController::getStudentsWithCharges');
|
||||
|
||||
$routes->get('parent/events', 'View\ParentController::parentEventPage');
|
||||
$routes->get('parent/events', 'View\ParentController::parentEventPage', ['filter' => 'auth:parent']);
|
||||
// parent event participation page
|
||||
$routes->post('parent/updateParticipation', 'View\ParentController::updateParticipation'); // handle parent participation updates
|
||||
|
||||
|
||||
@@ -56,13 +56,32 @@ class AuthController extends Controller
|
||||
|
||||
public function loginMask()
|
||||
{
|
||||
// Serve the login view directly here
|
||||
return view('user/login'); // Adjust the view path as needed
|
||||
$redirectTo = $this->sanitizeRedirectTarget((string) ($this->request->getGet('redirect_to') ?? ''));
|
||||
|
||||
if (session()->get('is_logged_in')) {
|
||||
if ($redirectTo !== null) {
|
||||
return redirect()->to($redirectTo);
|
||||
}
|
||||
|
||||
$roles = session()->get('roles') ?? [];
|
||||
if (empty($roles) && session()->get('role')) {
|
||||
$roles = [session()->get('role')];
|
||||
}
|
||||
|
||||
if (!empty($roles)) {
|
||||
return $this->redirectToDashboard($roles);
|
||||
}
|
||||
}
|
||||
|
||||
return view('user/login', [
|
||||
'redirect_to' => $redirectTo ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function login()
|
||||
{
|
||||
log_message('info', 'Processing login form submission.');
|
||||
$redirectTo = $this->sanitizeRedirectTarget((string) ($this->request->getPost('redirect_to') ?? $this->request->getGet('redirect_to') ?? ''));
|
||||
|
||||
// Step 1: Get email, password, and IP from the request
|
||||
$email = $this->request->getPost('email');
|
||||
@@ -98,7 +117,7 @@ class AuthController extends Controller
|
||||
$this->logLoginAttempt($user['id'], $user['email'], $ip, $this->request->getUserAgent());
|
||||
|
||||
// ✅ Step 7: Call loginUser() to set session and redirect
|
||||
return $this->loginUser($user);
|
||||
return $this->loginUser($user, $redirectTo);
|
||||
} else {
|
||||
// Step 8: Password mismatch — log failed attempt
|
||||
$this->handleFailedLogin($user['id'], $user['email'], $ip);
|
||||
@@ -491,7 +510,7 @@ class AuthController extends Controller
|
||||
return true;
|
||||
}
|
||||
|
||||
private function loginUser($user)
|
||||
private function loginUser($user, ?string $redirectTo = null)
|
||||
{
|
||||
$userRoleModel = new UserRoleModel();
|
||||
$roles = $userRoleModel->select('roles.name')
|
||||
@@ -525,9 +544,17 @@ class AuthController extends Controller
|
||||
if (count($roleNames) === 1) {
|
||||
// One role → set and redirect directly
|
||||
session()->set('role', $roleNames[0]);
|
||||
if ($redirectTo !== null) {
|
||||
return redirect()->to($redirectTo);
|
||||
}
|
||||
return $this->redirectToDashboard([$roleNames[0]]);
|
||||
} else {
|
||||
// Multiple roles → redirect to role selection view
|
||||
if ($redirectTo !== null) {
|
||||
session()->set('post_login_redirect', $redirectTo);
|
||||
} else {
|
||||
session()->remove('post_login_redirect');
|
||||
}
|
||||
return redirect()->to('/select-role');
|
||||
}
|
||||
|
||||
@@ -592,12 +619,17 @@ private function redirectToDashboard(array $roles)
|
||||
{
|
||||
$selectedRole = $this->request->getPost('selected_role');
|
||||
$availableRoles = session()->get('roles');
|
||||
$redirectTo = $this->sanitizeRedirectTarget((string) ($this->request->getPost('redirect_to') ?? session()->get('post_login_redirect') ?? ''));
|
||||
|
||||
if (!$selectedRole || !in_array($selectedRole, $availableRoles)) {
|
||||
return redirect()->to('/select-role')->with('error', 'Invalid role selected.');
|
||||
}
|
||||
|
||||
session()->set('role', $selectedRole);
|
||||
session()->remove('post_login_redirect');
|
||||
if ($redirectTo !== null) {
|
||||
return redirect()->to($redirectTo);
|
||||
}
|
||||
return $this->redirectToDashboard([$selectedRole]);
|
||||
}
|
||||
|
||||
@@ -609,7 +641,44 @@ private function redirectToDashboard(array $roles)
|
||||
return redirect()->to('/login')->with('error', 'No roles available.');
|
||||
}
|
||||
|
||||
return view('auth/select_role', ['roles' => $roles]);
|
||||
return view('auth/select_role', [
|
||||
'roles' => $roles,
|
||||
'redirect_to' => (string) (session()->get('post_login_redirect') ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
private function sanitizeRedirectTarget(string $redirectTo): ?string
|
||||
{
|
||||
$redirectTo = trim($redirectTo);
|
||||
if ($redirectTo === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('#^https?://#i', $redirectTo)) {
|
||||
$appHost = (string) parse_url(base_url('/'), PHP_URL_HOST);
|
||||
$targetHost = (string) parse_url($redirectTo, PHP_URL_HOST);
|
||||
$targetPath = (string) parse_url($redirectTo, PHP_URL_PATH);
|
||||
$targetQuery = (string) parse_url($redirectTo, PHP_URL_QUERY);
|
||||
|
||||
if ($appHost === '' || $targetHost === '' || strcasecmp($appHost, $targetHost) !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$redirectTo = $targetPath !== '' ? $targetPath : '/';
|
||||
if ($targetQuery !== '') {
|
||||
$redirectTo .= '?' . $targetQuery;
|
||||
}
|
||||
}
|
||||
|
||||
if (!str_starts_with($redirectTo, '/')) {
|
||||
$redirectTo = '/' . ltrim($redirectTo, '/');
|
||||
}
|
||||
|
||||
if (str_starts_with($redirectTo, '//')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $redirectTo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ class ClassProgressController extends BaseController
|
||||
'db_subject' => 'Quran/Arabic',
|
||||
],
|
||||
];
|
||||
|
||||
/** Unit column for teacher custom Islamic / Quran rows; must match teacher form JS. Segment: "Custom / {text}". */
|
||||
public const CUSTOM_UNIT_ROW_LABEL = 'Custom';
|
||||
|
||||
protected ClassProgressReportModel $reportModel;
|
||||
protected ClassProgressAttachmentModel $attachmentModel;
|
||||
protected TeacherClassModel $teacherClassModel;
|
||||
@@ -98,6 +102,10 @@ class ClassProgressController extends BaseController
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
if (! $this->hasIslamicUnitSelection()) {
|
||||
return redirect()->back()->withInput()->with('error', 'Please select at least one Islamic Studies unit.');
|
||||
}
|
||||
|
||||
$attachmentErrors = $this->validateAttachmentFiles($subjectSections);
|
||||
if (! empty($attachmentErrors)) {
|
||||
return redirect()->back()->withInput()->with('errors', $attachmentErrors);
|
||||
@@ -379,13 +387,18 @@ class ClassProgressController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
$sundayOptions = $this->buildSundayOptions();
|
||||
if (!in_array($row['week_start'], $sundayOptions, true)) {
|
||||
array_unshift($sundayOptions, $row['week_start']);
|
||||
}
|
||||
|
||||
return view('teacher/class_progress_submit', [
|
||||
'subjectSections' => self::SUBJECT_SECTIONS,
|
||||
'subjectCurriculum' => $subjectCurriculum,
|
||||
'classSectionId' => $row['class_section_id'],
|
||||
'classSectionName' => $classSectionName,
|
||||
'classId' => $classId,
|
||||
'sundayOptions' => [$row['week_start']],
|
||||
'sundayOptions' => $sundayOptions,
|
||||
'defaultWeekStart' => $row['week_start'],
|
||||
'existingWeekEnd' => $row['week_end'],
|
||||
'existingReports' => $subjectReports,
|
||||
@@ -431,6 +444,10 @@ class ClassProgressController extends BaseController
|
||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
if (! $this->hasIslamicUnitSelection()) {
|
||||
return redirect()->back()->withInput()->with('error', 'Please select at least one Islamic Studies unit.');
|
||||
}
|
||||
|
||||
$attachmentErrors = $this->validateAttachmentFiles($subjectSections);
|
||||
if (! empty($attachmentErrors)) {
|
||||
return redirect()->back()->withInput()->with('errors', $attachmentErrors);
|
||||
@@ -450,6 +467,32 @@ class ClassProgressController extends BaseController
|
||||
return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.');
|
||||
}
|
||||
|
||||
$confirmOverwrite = (bool) $this->request->getPost('confirm_overwrite');
|
||||
if ($weekStart && $weekStart !== (string) ($row['week_start'] ?? '')) {
|
||||
$conflicts = $this->reportModel
|
||||
->select('id')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('week_start', $weekStart)
|
||||
->where('teacher_id', $teacherId)
|
||||
->findAll();
|
||||
if (! $confirmOverwrite && ! empty($conflicts)) {
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->with('warning', 'A progress report already exists for this week, are you sure you want to override it?')
|
||||
->with('confirm_overwrite', true);
|
||||
}
|
||||
if ($confirmOverwrite && ! empty($conflicts)) {
|
||||
$conflictIds = array_values(array_filter(array_map(
|
||||
static fn (array $row): int => (int) ($row['id'] ?? 0),
|
||||
$conflicts
|
||||
)));
|
||||
if (! empty($conflictIds)) {
|
||||
$this->attachmentModel->whereIn('report_id', $conflictIds)->delete();
|
||||
$this->reportModel->whereIn('id', $conflictIds)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$weeklyReports = $this->reportModel
|
||||
->select('class_progress_reports.*')
|
||||
->whereIn('teacher_id', $allowedTeacherIds)
|
||||
@@ -666,6 +709,37 @@ class ClassProgressController extends BaseController
|
||||
return $flags ? json_encode($flags) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split stored unit_title into curriculum lines vs custom teacher-typed subjects ("Custom / …").
|
||||
* Keep in sync with teacher form, {@see buildUnitChapterSummary()}, and admin views.
|
||||
*
|
||||
* @return array{curriculum: list<string>, custom: list<string>}
|
||||
*/
|
||||
public static function splitUnitTitleForDisplay(string $unitTitle): array
|
||||
{
|
||||
$unitTitle = trim($unitTitle);
|
||||
if ($unitTitle === '') {
|
||||
return ['curriculum' => [], 'custom' => []];
|
||||
}
|
||||
|
||||
$segments = preg_split('/\s*;\s*/', $unitTitle, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$curriculum = [];
|
||||
$custom = [];
|
||||
foreach ($segments as $seg) {
|
||||
$seg = trim((string) $seg);
|
||||
if ($seg === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^Custom\s*\/\s*(.+)$/iu', $seg, $m)) {
|
||||
$custom[] = trim($m[1]);
|
||||
} else {
|
||||
$curriculum[] = $seg;
|
||||
}
|
||||
}
|
||||
|
||||
return ['curriculum' => $curriculum, 'custom' => $custom];
|
||||
}
|
||||
|
||||
protected function buildUnitChapterSummary(string $slug): ?string
|
||||
{
|
||||
$unitValues = array_map('trim', (array) $this->request->getPost("unit_$slug"));
|
||||
@@ -678,9 +752,12 @@ class ClassProgressController extends BaseController
|
||||
if ($unit === '' && $chapter === '') {
|
||||
continue;
|
||||
}
|
||||
if (strcasecmp($unit, self::CUSTOM_UNIT_ROW_LABEL) === 0 && $chapter !== '') {
|
||||
$unit = self::CUSTOM_UNIT_ROW_LABEL;
|
||||
}
|
||||
$segment = $unit;
|
||||
if ($chapter !== '') {
|
||||
$segment = $segment !== '' ? $segment . ' / ' . $chapter : $chapter;
|
||||
$segment = $segment !== '' ? $unit . ' / ' . $chapter : $chapter;
|
||||
}
|
||||
if ($segment === '') {
|
||||
continue;
|
||||
@@ -691,9 +768,26 @@ class ClassProgressController extends BaseController
|
||||
return null;
|
||||
}
|
||||
$summary = implode(' ; ', $parts);
|
||||
|
||||
return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary;
|
||||
}
|
||||
|
||||
protected function hasIslamicUnitSelection(): bool
|
||||
{
|
||||
$unitValues = array_map('trim', (array) $this->request->getPost('unit_islamic'));
|
||||
$chapterValues = array_map('trim', (array) $this->request->getPost('chapter_islamic'));
|
||||
$count = max(count($unitValues), count($chapterValues));
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$unit = $unitValues[$i] ?? '';
|
||||
$chapter = $chapterValues[$i] ?? '';
|
||||
if ($unit !== '' || $chapter !== '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function parseUnitChapterSummary(string $summary): array
|
||||
{
|
||||
$summary = trim($summary);
|
||||
@@ -709,9 +803,19 @@ class ClassProgressController extends BaseController
|
||||
if ($segment === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^Custom\s*\/\s*(.+)$/iu', $segment, $m)) {
|
||||
$units[] = self::CUSTOM_UNIT_ROW_LABEL;
|
||||
$chapters[] = trim($m[1]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$parts = preg_split('/\s*\/\s*/', $segment, 2);
|
||||
if (count($parts) === 2) {
|
||||
$units[] = trim($parts[0]);
|
||||
$u = trim($parts[0]);
|
||||
if (strcasecmp($u, self::CUSTOM_UNIT_ROW_LABEL) === 0) {
|
||||
$u = self::CUSTOM_UNIT_ROW_LABEL;
|
||||
}
|
||||
$units[] = $u;
|
||||
$chapters[] = trim($parts[1]);
|
||||
} else {
|
||||
$units[] = $segment;
|
||||
|
||||
@@ -763,7 +763,13 @@ class AdministratorController extends BaseController
|
||||
|
||||
[$progressExpectedWeeks, $progressSubmittedBySection] = $this->buildClassProgressStats($sectionIds);
|
||||
$examDraftCounts = [];
|
||||
$examDraftDeadline = $this->resolveExamDraftDeadline($semester, $schoolYear);
|
||||
$examDraftDeadline = $this->resolveTeacherDashboardExamDraftDeadline($semester, $schoolYear);
|
||||
$examDraftDeadlineConfig = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? ''));
|
||||
$examDraftDeadlineFormatted = '';
|
||||
if ($examDraftDeadlineConfig !== '') {
|
||||
$parsedUi = $this->parseExamDraftDeadlineConfigValue();
|
||||
$examDraftDeadlineFormatted = $parsedUi !== null ? $parsedUi->format('M j, Y') : '';
|
||||
}
|
||||
$homeworkCounts = [];
|
||||
if (! empty($sectionIds)) {
|
||||
$draftBuilder = $examDrafts
|
||||
@@ -1066,6 +1072,8 @@ class AdministratorController extends BaseController
|
||||
'notificationHistory' => $historyMap,
|
||||
'summary' => $summary,
|
||||
'lowProgressSectionIds' => $lowProgressSectionIds,
|
||||
'examDraftDeadlineConfig' => $examDraftDeadlineConfig,
|
||||
'examDraftDeadlineFormatted' => $examDraftDeadlineFormatted,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1299,6 +1307,7 @@ class AdministratorController extends BaseController
|
||||
$progressUrl = site_url('teacher/progress/history');
|
||||
$examDraftUrl = site_url('teacher/exam-drafts');
|
||||
$homeworkUrl = site_url('teacher/addHomework');
|
||||
$examDraftDeadlineEmailHtml = $this->buildExamDraftDeadlineEmailHtml();
|
||||
$sentCount = 0;
|
||||
$failCount = 0;
|
||||
|
||||
@@ -1347,7 +1356,8 @@ class AdministratorController extends BaseController
|
||||
} else {
|
||||
$draftLabel = 'exam draft';
|
||||
}
|
||||
$examDraftNote = "<p>" . ucfirst($draftLabel) . " submissions can be updated at <a href=\"{$examDraftUrl}\">Teacher Exam Drafts</a>.</p>";
|
||||
$examDraftNote = "<p>" . ucfirst($draftLabel) . " submissions can be updated at <a href=\"{$examDraftUrl}\">Teacher Exam Drafts</a>.</p>"
|
||||
. $examDraftDeadlineEmailHtml;
|
||||
}
|
||||
$homeworkNote = '';
|
||||
if (in_array('homework', $missingItems, true)) {
|
||||
@@ -1485,6 +1495,60 @@ class AdministratorController extends BaseController
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Exam draft due date for the teacher submissions dashboard: prefers the configuration key
|
||||
* `exam_draft_deadline` (same as automated reminders); otherwise fall/spring exam deadlines.
|
||||
*/
|
||||
private function resolveTeacherDashboardExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable
|
||||
{
|
||||
$fromExamDraftKey = $this->parseExamDraftDeadlineConfigValue();
|
||||
if ($fromExamDraftKey !== null) {
|
||||
return $fromExamDraftKey;
|
||||
}
|
||||
|
||||
return $this->resolveExamDraftDeadline($semester, $schoolYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the `exam_draft_deadline` configuration value using the application timezone (midnight that calendar day).
|
||||
*/
|
||||
private function parseExamDraftDeadlineConfigValue(): ?\DateTimeImmutable
|
||||
{
|
||||
$raw = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? ''));
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
$tz = new \DateTimeZone(config('App')->appTimezone ?? 'UTC');
|
||||
try {
|
||||
$deadline = new \DateTimeImmutable($raw, $tz);
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $deadline->setTime(0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML snippet for reminder emails when exam draft is included (deadline from exam_draft_deadline config).
|
||||
*/
|
||||
private function buildExamDraftDeadlineEmailHtml(): string
|
||||
{
|
||||
$raw = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? ''));
|
||||
if ($raw === '') {
|
||||
return '';
|
||||
}
|
||||
$parsed = $this->parseExamDraftDeadlineConfigValue();
|
||||
$display = $parsed !== null
|
||||
? htmlspecialchars($parsed->format('l, F j, Y'), ENT_QUOTES, 'UTF-8')
|
||||
: htmlspecialchars($raw, ENT_QUOTES, 'UTF-8');
|
||||
$rawEsc = htmlspecialchars($raw, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
return '<p><strong>Exam draft submission deadline</strong> (<code>exam_draft_deadline</code>): '
|
||||
. "<strong>{$display}</strong>"
|
||||
. ($parsed !== null && $rawEsc !== $display ? " <span style=\"color:#555;\">(configured value: {$rawEsc})</span>" : '')
|
||||
. '.</p>';
|
||||
}
|
||||
|
||||
private function resolveExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable
|
||||
{
|
||||
$semesterKey = strtolower(trim($semester));
|
||||
|
||||
@@ -750,7 +750,9 @@ class DiscountController extends BaseController
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
$newTotal = round($tuitionSubtotal + $eventSubtotal + $additionalSubtotal, 2);
|
||||
$discountableTotal = $tuitionSubtotal + $additionalSubtotal;
|
||||
$nonDiscountableTotal = $eventSubtotal;
|
||||
$newTotal = round($discountableTotal + $nonDiscountableTotal, 2);
|
||||
|
||||
// ---- Payments / Discounts / Refunds ----
|
||||
$db = $this->db;
|
||||
@@ -794,7 +796,8 @@ class DiscountController extends BaseController
|
||||
->get()->getRowArray();
|
||||
$totalRefundPaid = (float)($refundRow['total_refund_paid'] ?? 0);
|
||||
|
||||
$newBalance = max(0.0, $newTotal - $totalDisc - $totalPaid - $totalRefundPaid);
|
||||
$appliedDiscount = min($totalDisc, $discountableTotal);
|
||||
$newBalance = max(0.0, $newTotal - $appliedDiscount - $totalPaid - $totalRefundPaid);
|
||||
$newStatus = ($newBalance <= 0.00001) ? 'Paid' : (($totalPaid > 0) ? 'Partially Paid' : 'Unpaid');
|
||||
|
||||
$updateData = [
|
||||
|
||||
@@ -170,6 +170,163 @@ class EmailController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send one email with a single To recipient and many CC recipients.
|
||||
*
|
||||
* @param string $recipient
|
||||
* @param string[] $ccRecipients
|
||||
* @param string $subject
|
||||
* @param string $htmlMessage
|
||||
* @param string|null $profile
|
||||
* @param string|null $replyToEmail
|
||||
* @param string|null $replyToName
|
||||
* @param array $attachments
|
||||
*/
|
||||
public function sendEmailWithCc(
|
||||
string $recipient,
|
||||
array $ccRecipients,
|
||||
string $subject,
|
||||
string $htmlMessage,
|
||||
?string $profile = null,
|
||||
?string $replyToEmail = null,
|
||||
?string $replyToName = null,
|
||||
array $attachments = []
|
||||
): bool {
|
||||
$ccRecipients = array_values(array_unique(array_filter(array_map(static function ($email) {
|
||||
$email = trim((string) $email);
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : null;
|
||||
}, $ccRecipients))));
|
||||
|
||||
if ($ccRecipients === []) {
|
||||
return $this->sendEmail($recipient, $subject, $htmlMessage, $profile, $replyToEmail, $replyToName, $attachments);
|
||||
}
|
||||
|
||||
$autoload = APPPATH . '../vendor/autoload.php';
|
||||
if (is_file($autoload)) {
|
||||
require_once $autoload;
|
||||
}
|
||||
|
||||
$profile = $this->resolveProfile($profile);
|
||||
$cfg = $this->getProfileConfig($profile);
|
||||
|
||||
if (empty($cfg['host']) || empty($cfg['user']) || $cfg['pass'] === '') {
|
||||
log_message('error', "[mail:$profile] Missing SMTP config (host/user/pass). Check .env MAIL_{$this->envKeyFromProfile($profile)}_* or MAIL_DEFAULT_*.");
|
||||
return false;
|
||||
}
|
||||
|
||||
$debugEnabled = (bool) env('MAIL_DEBUG', false);
|
||||
$timeout = (int) env('MAIL_TIMEOUT', 15);
|
||||
$keepAlive = (bool) env('MAIL_KEEPALIVE', false);
|
||||
$verifyPeer = filter_var(env('MAIL_VERIFY_PEER', 'true'), FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
$targetHost = $cfg['host'];
|
||||
$targetPort = (int) $cfg['port'];
|
||||
|
||||
$resolved = @gethostbyname($targetHost);
|
||||
if (!$resolved || $resolved === $targetHost) {
|
||||
log_message('debug', "[mail:$profile] DNS resolve note: host=$targetHost, resolved=$resolved");
|
||||
}
|
||||
|
||||
$sockOk = @fsockopen($targetHost, $targetPort, $errno, $errstr, 5);
|
||||
if (!$sockOk) {
|
||||
log_message('error', "[mail:$profile] Socket preflight failed to {$targetHost}:{$targetPort} (errno=$errno, err=$errstr). Likely firewall/port/encryption mismatch or wrong host.");
|
||||
} else {
|
||||
fclose($sockOk);
|
||||
}
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
try {
|
||||
if ($debugEnabled) {
|
||||
ob_start();
|
||||
}
|
||||
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $cfg['host'];
|
||||
$mail->Port = $cfg['port'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $cfg['user'];
|
||||
$mail->Password = $cfg['pass'];
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = $timeout;
|
||||
$mail->SMTPKeepAlive = $keepAlive;
|
||||
$mail->SMTPAutoTLS = true;
|
||||
|
||||
if ($cfg['encryption'] === 'ssl') {
|
||||
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||
} else {
|
||||
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||
}
|
||||
|
||||
$mail->SMTPOptions = [
|
||||
'ssl' => [
|
||||
'verify_peer' => $verifyPeer,
|
||||
'verify_peer_name' => $verifyPeer,
|
||||
'allow_self_signed' => !$verifyPeer,
|
||||
],
|
||||
];
|
||||
|
||||
$mail->SMTPDebug = $debugEnabled ? 3 : 0;
|
||||
$mail->Debugoutput = 'error_log';
|
||||
|
||||
$mail->setFrom($cfg['fromEmail'], $cfg['fromName']);
|
||||
if (!empty($cfg['returnPath'])) {
|
||||
$mail->Sender = $cfg['returnPath'];
|
||||
}
|
||||
|
||||
$mail->clearReplyTos();
|
||||
$rtEmail = env('MAIL_DEFAULT_REPLY_TO');
|
||||
$rtName = env('MAIL_DEFAULT_REPLY_TO_NAME');
|
||||
if (!$rtEmail || !filter_var($rtEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
$rtEmail = $replyToEmail ?: ($cfg['replyTo'] ?: $cfg['fromEmail']);
|
||||
}
|
||||
if (!$rtName) {
|
||||
$rtName = $replyToName ?: ($cfg['replyToName'] ?: $cfg['fromName']);
|
||||
}
|
||||
$rtName = $this->sanitizeReplyToName($rtName, $cfg['fromName']);
|
||||
if ($rtEmail) {
|
||||
$mail->addReplyTo($rtEmail, $rtName);
|
||||
}
|
||||
|
||||
if (!empty($cfg['dkim']['domain']) && !empty($cfg['dkim']['private']) && !empty($cfg['dkim']['selector'])) {
|
||||
$mail->DKIM_domain = $cfg['dkim']['domain'];
|
||||
$mail->DKIM_private = $cfg['dkim']['private'];
|
||||
$mail->DKIM_selector = $cfg['dkim']['selector'];
|
||||
$mail->DKIM_identity = $cfg['fromEmail'];
|
||||
}
|
||||
|
||||
$mail->addAddress($recipient);
|
||||
foreach ($ccRecipients as $ccRecipient) {
|
||||
$mail->addCC($ccRecipient);
|
||||
}
|
||||
|
||||
foreach ($attachments as $att) {
|
||||
if (!empty($att['path']) && is_file($att['path'])) {
|
||||
$mail->addAttachment($att['path'], $att['name'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $htmlMessage;
|
||||
|
||||
$ok = $mail->send();
|
||||
|
||||
$dbg = $debugEnabled ? (ob_get_clean() ?: '') : '';
|
||||
if ($ok) {
|
||||
log_message('info', "[mail:$profile] Sent to {$recipient} with " . count($ccRecipients) . " CC recipient(s), subj='{$subject}' via {$cfg['host']}:{$cfg['port']}/{$cfg['encryption']}");
|
||||
return true;
|
||||
}
|
||||
|
||||
log_message('error', "[mail:$profile] Failed: {$mail->ErrorInfo}. Debug: {$dbg}");
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
$dbg = $debugEnabled ? (ob_get_clean() ?: '') : '';
|
||||
log_message('error', "[mail:$profile] Exception: {$e->getMessage()} | PHPMailer: {$mail->ErrorInfo} | Debug: {$dbg}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve null/alias profile names to a canonical env prefix. */
|
||||
private function resolveProfile(?string $profile): string
|
||||
{
|
||||
|
||||
@@ -6,8 +6,18 @@ use App\Models\EventModel;
|
||||
use App\Models\EventChargesModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\StudentModel;
|
||||
use App\Models\StudentClassModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\EnrollmentModel;
|
||||
use App\Models\InvoiceModel;
|
||||
use App\Models\InvoiceEventModel;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\ParentModel;
|
||||
use App\Models\PaymentModel;
|
||||
use App\Models\CalendarModel;
|
||||
use App\Services\EmailService;
|
||||
use Config\Database;
|
||||
#use ???
|
||||
use App\Controllers\View\InvoiceController;
|
||||
use CodeIgniter\RESTful\ResourceController;
|
||||
|
||||
@@ -19,10 +29,18 @@ class EventController extends ResourceController
|
||||
protected $configModel;
|
||||
protected $eventModel;
|
||||
protected $invoiceController;
|
||||
protected $invoiceModel;
|
||||
protected $invoiceEventModel;
|
||||
protected $studentClassModel;
|
||||
protected $classSectionModel;
|
||||
protected $paymentModel;
|
||||
protected $parentModel;
|
||||
protected $emailService;
|
||||
protected $schoolYear;
|
||||
protected $semester;
|
||||
protected $categories;
|
||||
protected $enrollmentModel;
|
||||
private ?bool $eventChargesHasCreatedBy = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -32,7 +50,14 @@ class EventController extends ResourceController
|
||||
$this->userModel = new UserModel(); // Add this
|
||||
$this->eventModel = new EventModel();
|
||||
$this->invoiceController = new InvoiceController();
|
||||
$this->invoiceModel = new InvoiceModel();
|
||||
$this->invoiceEventModel = new InvoiceEventModel();
|
||||
$this->studentClassModel = new StudentClassModel();
|
||||
$this->classSectionModel = new ClassSectionModel();
|
||||
$this->paymentModel = new PaymentModel();
|
||||
$this->enrollmentModel = new EnrollmentModel();
|
||||
$this->parentModel = new ParentModel();
|
||||
$this->emailService = new EmailService();
|
||||
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
@@ -44,6 +69,22 @@ class EventController extends ResourceController
|
||||
];
|
||||
}
|
||||
|
||||
private function eventChargesSupportsCreatedBy(): bool
|
||||
{
|
||||
if ($this->eventChargesHasCreatedBy !== null) {
|
||||
return $this->eventChargesHasCreatedBy;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::connect();
|
||||
$this->eventChargesHasCreatedBy = $db->fieldExists('created_by', 'event_charges');
|
||||
} catch (\Throwable $e) {
|
||||
$this->eventChargesHasCreatedBy = false;
|
||||
}
|
||||
|
||||
return $this->eventChargesHasCreatedBy;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$eventModel = new EventModel();
|
||||
@@ -93,59 +134,52 @@ class EventController extends ResourceController
|
||||
'created_by' => session()->get('user_id'),
|
||||
]);
|
||||
|
||||
if ($eventId) {
|
||||
$amount = (float) $this->request->getPost('amount');
|
||||
$semester = (string) $this->request->getPost('semester');
|
||||
$schoolYear = (string) $this->request->getPost('school_year');
|
||||
$userId = (int) (session()->get('user_id') ?? 0);
|
||||
if ($eventId && $this->request->getPost('add_to_calendar')) {
|
||||
$calendarModel = new CalendarModel();
|
||||
|
||||
$enrollments = $this->enrollmentModel
|
||||
->select('enrollments.student_id, students.parent_id')
|
||||
->join('students', 'students.id = enrollments.student_id', 'left')
|
||||
->where('enrollments.school_year', $schoolYear)
|
||||
->whereIn('enrollments.enrollment_status', ['enrolled', 'payment pending'])
|
||||
->findAll();
|
||||
$title = trim((string)$this->request->getPost('event_name'));
|
||||
$date = (string)$this->request->getPost('expiration_date');
|
||||
$schoolYear = (string)$this->request->getPost('school_year');
|
||||
$semester = (string)$this->request->getPost('semester');
|
||||
|
||||
$parentIds = [];
|
||||
foreach ($enrollments as $row) {
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
$parentId = (int) ($row['parent_id'] ?? 0);
|
||||
if ($studentId <= 0 || $parentId <= 0) {
|
||||
continue;
|
||||
if ($title !== '' && $date !== '' && $schoolYear !== '') {
|
||||
$data = [
|
||||
'title' => $title,
|
||||
'description' => (string)$this->request->getPost('description'),
|
||||
'event_type' => 'Event',
|
||||
'date' => $date,
|
||||
'notify_parent' => $this->request->getPost('notify_parent') ? 1 : 0,
|
||||
'notify_teacher' => $this->request->getPost('notify_teacher') ? 1 : 0,
|
||||
'notify_admin' => $this->request->getPost('notify_admin') ? 1 : 0,
|
||||
'no_school' => 0,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester ?: $this->semester,
|
||||
];
|
||||
if (!$calendarModel->supportsEventType()) {
|
||||
unset($data['event_type']);
|
||||
}
|
||||
|
||||
$exists = $this->eventChargesModel
|
||||
->where('event_id', $eventId)
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->first();
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
$dupQuery = $calendarModel
|
||||
->where('school_year', $data['school_year'])
|
||||
->where('date', $data['date'])
|
||||
->where('title', $data['title']);
|
||||
if ($calendarModel->supportsEventType() && isset($data['event_type'])) {
|
||||
$dupQuery = $dupQuery->where('event_type', $data['event_type']);
|
||||
}
|
||||
$existing = $dupQuery->first();
|
||||
if (!$existing) {
|
||||
$calendarModel->save($data);
|
||||
}
|
||||
|
||||
$this->eventChargesModel->insert([
|
||||
'event_id' => $eventId,
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'participation' => 'yes',
|
||||
'charged' => $amount,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'updated_by' => $userId ?: null,
|
||||
]);
|
||||
|
||||
$parentIds[] = $parentId;
|
||||
}
|
||||
|
||||
$parentIds = array_unique($parentIds);
|
||||
foreach ($parentIds as $pid) {
|
||||
$this->invoiceController->generateInvoice((string) $pid);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->to('/administrator/events')->with('success', 'Event created successfully');
|
||||
$redirect = redirect()->to('/administrator/events')->with('success', 'Event created successfully');
|
||||
if ($eventId && $this->request->getPost('send_email_parent')) {
|
||||
$emailStatus = $this->broadcastEventToParents((int) $eventId);
|
||||
$redirect = $this->applyEventEmailFlash($redirect, $emailStatus);
|
||||
}
|
||||
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
return view('administrator/events/create_event', [
|
||||
@@ -186,7 +220,12 @@ class EventController extends ResourceController
|
||||
]);
|
||||
|
||||
if ($updated) {
|
||||
return redirect()->to('/administrator/events')->with('success', 'Event updated successfully');
|
||||
$redirect = redirect()->to('/administrator/events')->with('success', 'Event updated successfully');
|
||||
if ($this->request->getPost('send_email_parent')) {
|
||||
$emailStatus = $this->broadcastEventToParents((int) $id);
|
||||
$redirect = $this->applyEventEmailFlash($redirect, $emailStatus);
|
||||
}
|
||||
return $redirect;
|
||||
} else {
|
||||
log_message('debug', 'GET detected');
|
||||
return redirect()->back()->with('error', 'Failed to update event');
|
||||
@@ -199,6 +238,106 @@ class EventController extends ResourceController
|
||||
]);
|
||||
}
|
||||
|
||||
private function broadcastEventToParents(int $eventId): array
|
||||
{
|
||||
$event = $this->eventModel->find($eventId);
|
||||
if (!$event) {
|
||||
return ['ok' => false, 'count' => 0, 'message' => 'Event email was skipped because the event could not be loaded.'];
|
||||
}
|
||||
|
||||
$emails = $this->collectParentBroadcastEmails((string) ($event['school_year'] ?? ''));
|
||||
if ($emails === []) {
|
||||
return ['ok' => false, 'count' => 0, 'message' => 'Event email was skipped because no parent email addresses were found.'];
|
||||
}
|
||||
|
||||
$subject = 'School Event: ' . trim((string) ($event['event_name'] ?? 'Upcoming Event'));
|
||||
$innerBody = view('emails/event_broadcast', [
|
||||
'event' => $event,
|
||||
'eventUrl' => base_url('parent/events'),
|
||||
'flyerUrl' => !empty($event['flyer']) ? base_url('uploads/' . ltrim((string) $event['flyer'], '/')) : null,
|
||||
]);
|
||||
$body = view('emails/_wrap_layout', [
|
||||
'title' => $subject,
|
||||
'subject' => $subject,
|
||||
'body_html' => $innerBody,
|
||||
]);
|
||||
|
||||
$ok = $this->emailService->sendCc($emails, $subject, $body, 'general');
|
||||
$batchCount = $this->emailService->getLastCcBatchCount();
|
||||
|
||||
return [
|
||||
'ok' => $ok,
|
||||
'count' => count($emails),
|
||||
'batches' => $batchCount,
|
||||
'message' => $ok
|
||||
? 'Event email was sent in ' . $batchCount . ' CC batch(es) for ' . count($emails) . ' recipient(s).'
|
||||
: 'The event was saved, but sending the parent CC email failed.',
|
||||
];
|
||||
}
|
||||
|
||||
private function collectParentBroadcastEmails(string $schoolYear = ''): array
|
||||
{
|
||||
$emails = [];
|
||||
|
||||
$db = Database::connect();
|
||||
|
||||
$parentRole = $db->table('roles')
|
||||
->select('id')
|
||||
->where('LOWER(name)', 'parent')
|
||||
->get()
|
||||
->getRow();
|
||||
|
||||
if ($parentRole) {
|
||||
$firstParentQuery = $db->table('users')
|
||||
->select('users.email')
|
||||
->join('user_roles', 'user_roles.user_id = users.id')
|
||||
->where('user_roles.role_id', (int) $parentRole->id)
|
||||
->where('users.email IS NOT NULL', null, false)
|
||||
->where('users.email !=', '');
|
||||
|
||||
if ($schoolYear !== '' && $db->fieldExists('school_year', 'users')) {
|
||||
$firstParentQuery->where('users.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
foreach ($firstParentQuery->get()->getResultArray() as $row) {
|
||||
$email = trim((string) ($row['email'] ?? ''));
|
||||
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$emails[strtolower($email)] = $email;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$secondParentQuery = $db->table('parents')
|
||||
->select('secondparent_email')
|
||||
->where('secondparent_email IS NOT NULL', null, false)
|
||||
->where('secondparent_email !=', '');
|
||||
|
||||
if ($schoolYear !== '' && $db->fieldExists('school_year', 'parents')) {
|
||||
$secondParentQuery->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
foreach ($secondParentQuery->get()->getResultArray() as $row) {
|
||||
$email = trim((string) ($row['secondparent_email'] ?? ''));
|
||||
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$emails[strtolower($email)] = $email;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($emails);
|
||||
}
|
||||
|
||||
private function applyEventEmailFlash($redirect, array $emailStatus)
|
||||
{
|
||||
if (!empty($emailStatus['message'])) {
|
||||
return $redirect->with(
|
||||
($emailStatus['ok'] ?? false) ? 'email_status' : 'email_error',
|
||||
$emailStatus['message']
|
||||
);
|
||||
}
|
||||
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
|
||||
public function delete($id = null)
|
||||
{
|
||||
@@ -218,6 +357,19 @@ class EventController extends ResourceController
|
||||
$parentIds[] = $charge['parent_id'];
|
||||
}
|
||||
|
||||
// Also remove any invoice_event rows tied to this event
|
||||
$eventName = $event['event_name'] ?? null;
|
||||
if ($eventName) {
|
||||
$builder = $this->invoiceEventModel->where('event_name', $eventName);
|
||||
if (!empty($event['school_year'])) {
|
||||
$builder->where('school_year', $event['school_year']);
|
||||
}
|
||||
if (!empty($event['semester'])) {
|
||||
$builder->where('semester', $event['semester']);
|
||||
}
|
||||
$builder->delete();
|
||||
}
|
||||
|
||||
// Delete event
|
||||
$this->eventModel->delete($id);
|
||||
|
||||
@@ -239,27 +391,102 @@ class EventController extends ResourceController
|
||||
$semester = $this->request->getGet('semester') ?? $this->semester;
|
||||
|
||||
$parents = $this->userModel->getParents();
|
||||
$events = $this->eventModel->getActiveEvents($this->schoolYear);
|
||||
$events = $this->eventModel->getActiveEvents($schoolYear);
|
||||
$filterEventId = (int) ($this->request->getGet('event_id') ?? 0);
|
||||
$filterParentId = (int) ($this->request->getGet('parent_id') ?? 0);
|
||||
|
||||
$charges = $this->eventChargesModel
|
||||
$chargesBuilder = $this->eventChargesModel
|
||||
->select('event_charges.*,
|
||||
users.firstname AS parent_firstname, users.lastname AS parent_lastname,
|
||||
students.firstname AS student_firstname, students.lastname AS student_lastname,
|
||||
events.event_name')
|
||||
events.event_name, events.description AS event_description, events.amount AS event_amount')
|
||||
->join('users', 'users.id = event_charges.parent_id', 'left')
|
||||
->join('students', 'students.id = event_charges.student_id', 'left')
|
||||
->join('events', 'events.id = event_charges.event_id', 'left')
|
||||
->where('event_charges.school_year', $schoolYear)
|
||||
->where('event_charges.semester', $semester)
|
||||
->where('event_charges.semester', $semester);
|
||||
|
||||
if ($filterEventId > 0) {
|
||||
$chargesBuilder->where('event_charges.event_id', $filterEventId);
|
||||
}
|
||||
|
||||
$charges = $chargesBuilder
|
||||
->orderBy('event_charges.created_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
foreach ($charges as &$charge) {
|
||||
if (empty($charge['class_section_id']) && !empty($charge['student_id'])) {
|
||||
$sections = $this->studentClassModel->getClassSectionIdsByStudentId(
|
||||
(int)$charge['student_id'],
|
||||
$schoolYear
|
||||
);
|
||||
if (!empty($sections)) {
|
||||
$charge['class_section_id'] = $sections[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($charge);
|
||||
|
||||
$parentBalances = [];
|
||||
try {
|
||||
$balanceRows = $this->invoiceModel
|
||||
->select('parent_id, COALESCE(SUM(balance),0) AS total_balance')
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->groupBy('parent_id')
|
||||
->findAll();
|
||||
|
||||
foreach ($balanceRows as $row) {
|
||||
$parentBalances[(int)($row['parent_id'] ?? 0)] = (float)($row['total_balance'] ?? 0.0);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Failed to load parent balances: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$sectionIds = array_unique(array_filter(array_column($charges, 'class_section_id')));
|
||||
$classSectionNames = [];
|
||||
if (!empty($sectionIds)) {
|
||||
$sections = $this->classSectionModel
|
||||
->select('class_section_id, class_section_name')
|
||||
->whereIn('class_section_id', $sectionIds)
|
||||
->findAll();
|
||||
foreach ($sections as $section) {
|
||||
$classSectionNames[(int)($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
$semesterOptions = (new EventChargesModel())
|
||||
->select('semester')
|
||||
->distinct()
|
||||
->orderBy('semester', 'ASC')
|
||||
->findColumn('semester');
|
||||
$semesterOptions = array_values(array_filter(array_unique($semesterOptions ?? [])));
|
||||
if (empty($semesterOptions)) {
|
||||
$semesterOptions = [$this->semester];
|
||||
}
|
||||
|
||||
$schoolYearOptions = (new EventChargesModel())
|
||||
->select('school_year')
|
||||
->distinct()
|
||||
->orderBy('school_year', 'ASC')
|
||||
->findColumn('school_year');
|
||||
$schoolYearOptions = array_values(array_filter(array_unique($schoolYearOptions ?? [])));
|
||||
if (empty($schoolYearOptions)) {
|
||||
$schoolYearOptions = [$this->schoolYear];
|
||||
}
|
||||
|
||||
return view('administrator/events/event_charges', [
|
||||
'charges' => $charges,
|
||||
'parents' => $parents,
|
||||
'events' => $events,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'filterEventId' => $filterEventId,
|
||||
'filterParentId' => $filterParentId,
|
||||
'parentBalances' => $parentBalances,
|
||||
'classSectionNames' => $classSectionNames,
|
||||
'semesterOptions' => $semesterOptions,
|
||||
'schoolYearOptions' => $schoolYearOptions,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -272,13 +499,24 @@ class EventController extends ResourceController
|
||||
$parentId = $this->request->getPost('parent_id');
|
||||
$eventId = $this->request->getPost('event_id');
|
||||
$participations = $this->request->getPost('participation') ?? [];
|
||||
$externalParticipants = $this->request->getPost('external_participants') ?? [];
|
||||
|
||||
if (!$parentId || !$eventId || empty($participations)) {
|
||||
// Allow "external only" submissions (no enrolled students selected).
|
||||
if (!$eventId || (empty($participations) && empty($externalParticipants))) {
|
||||
return redirect()->back()->with('error', 'Missing required information.');
|
||||
}
|
||||
if (!empty($participations) && !$parentId) {
|
||||
return redirect()->back()->with('error', 'Select a parent when adding enrolled students.');
|
||||
}
|
||||
|
||||
$userId = session()->get('user_id');
|
||||
$event = $this->eventModel->getEvent($eventId, $schoolYear);
|
||||
if (!$event) {
|
||||
return redirect()->back()->with('error', 'Selected event not found for the chosen school year.');
|
||||
}
|
||||
|
||||
$parentsForInvoice = [];
|
||||
$supportsCreatedBy = $this->eventChargesSupportsCreatedBy();
|
||||
|
||||
foreach ($participations as $studentId => $value) {
|
||||
$existing = $this->eventChargesModel->where([
|
||||
@@ -296,33 +534,392 @@ class EventController extends ResourceController
|
||||
continue;
|
||||
}
|
||||
|
||||
// value is 'yes'
|
||||
$sectionIds = $this->studentClassModel->getClassSectionIdsByStudentId($studentId, $schoolYear);
|
||||
$classSectionId = !empty($sectionIds) ? $sectionIds[0] : null;
|
||||
|
||||
if ($existing) {
|
||||
$this->eventChargesModel->update($existing['id'], [
|
||||
'participation' => 'yes',
|
||||
'charged' => $event['amount'],
|
||||
'updated_by' => $userId
|
||||
]);
|
||||
$updateData = [
|
||||
'participation' => 'yes',
|
||||
'updated_by' => $userId,
|
||||
'class_section_id'=> $classSectionId,
|
||||
];
|
||||
if (isset($event['amount']) && ((float)$event['amount'] !== (float)($existing['charged'] ?? 0))) {
|
||||
$updateData['charged'] = $event['amount'];
|
||||
}
|
||||
$this->eventChargesModel->update($existing['id'], $updateData);
|
||||
continue;
|
||||
} else {
|
||||
$this->eventChargesModel->insert([
|
||||
$insertData = [
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'event_id' => $eventId,
|
||||
'participation' => 'yes',
|
||||
'charged' => $event['amount'],
|
||||
'event_paid' => 0,
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId
|
||||
]);
|
||||
];
|
||||
if ($supportsCreatedBy) {
|
||||
$insertData['created_by'] = $userId;
|
||||
}
|
||||
$this->eventChargesModel->insert($insertData);
|
||||
}
|
||||
}
|
||||
|
||||
$this->invoiceController->generateInvoice($parentId);
|
||||
foreach ($externalParticipants as $pieces) {
|
||||
$firstname = $this->normalizeExternalName($pieces['firstname'] ?? '');
|
||||
$lastname = $this->normalizeExternalName($pieces['lastname'] ?? '');
|
||||
$note = trim($pieces['note'] ?? '');
|
||||
$parentFirst = $this->normalizeExternalName($pieces['parent_firstname'] ?? '');
|
||||
$parentLast = $this->normalizeExternalName($pieces['parent_lastname'] ?? '');
|
||||
$parentPhone = trim($pieces['parent_phone'] ?? '');
|
||||
$parentEmail = trim((string)($pieces['parent_email'] ?? ''));
|
||||
$markPaid = (string)($pieces['paid'] ?? '0') === '1';
|
||||
|
||||
if (!$firstname && !$lastname) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matchedParentId = $this->findExistingParentIdByExternalContact(
|
||||
$parentFirst,
|
||||
$parentLast,
|
||||
$parentEmail,
|
||||
$parentPhone
|
||||
);
|
||||
|
||||
// Match external participants by kid name + event context.
|
||||
// Parent contact info can change (or be fixed later) and should be updated, not used for matching.
|
||||
$matchConditions = [
|
||||
'event_id' => $eventId,
|
||||
'external_firstname' => $firstname,
|
||||
'external_lastname' => $lastname,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
];
|
||||
if ($matchedParentId) {
|
||||
$matchConditions['parent_id'] = $matchedParentId;
|
||||
} else {
|
||||
// External-only parent: match by external parent contact to avoid duplicates.
|
||||
$matchConditions['parent_id'] = null;
|
||||
$matchConditions['external_parent_firstname'] = $parentFirst ?: null;
|
||||
$matchConditions['external_parent_lastname'] = $parentLast ?: null;
|
||||
$matchConditions['external_parent_phone'] = $parentPhone ?: null;
|
||||
$matchConditions['external_parent_email'] = $parentEmail ?: null;
|
||||
}
|
||||
$existingExternal = $this->eventChargesModel->where($matchConditions)->first();
|
||||
|
||||
if ($existingExternal) {
|
||||
$this->eventChargesModel->update($existingExternal['id'], [
|
||||
'participation' => 'yes',
|
||||
'charged' => $event['amount'],
|
||||
'updated_by' => $userId,
|
||||
// Preserve existing payment status; only update contact fields when provided.
|
||||
'external_note' => $note !== '' ? $note : ($existingExternal['external_note'] ?? null),
|
||||
'external_parent_firstname' => $parentFirst !== '' ? $parentFirst : ($existingExternal['external_parent_firstname'] ?? null),
|
||||
'external_parent_lastname' => $parentLast !== '' ? $parentLast : ($existingExternal['external_parent_lastname'] ?? null),
|
||||
'external_parent_phone' => $parentPhone !== '' ? $parentPhone : ($existingExternal['external_parent_phone'] ?? null),
|
||||
'external_parent_email' => $parentEmail !== '' ? $parentEmail : ($existingExternal['external_parent_email'] ?? null),
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$insertData = [
|
||||
'parent_id' => $matchedParentId ?: null,
|
||||
'student_id' => null,
|
||||
'event_id' => $eventId,
|
||||
'participation' => 'yes',
|
||||
'charged' => $event['amount'],
|
||||
'event_paid' => $markPaid ? 1 : 0,
|
||||
'external_firstname' => $firstname,
|
||||
'external_lastname' => $lastname,
|
||||
'external_note' => $note,
|
||||
'external_parent_firstname' => $parentFirst,
|
||||
'external_parent_lastname' => $parentLast,
|
||||
'external_parent_phone' => $parentPhone,
|
||||
'external_parent_email' => $parentEmail,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'updated_by' => $userId,
|
||||
];
|
||||
if ($supportsCreatedBy) {
|
||||
$insertData['created_by'] = $userId;
|
||||
}
|
||||
$this->eventChargesModel->insert($insertData);
|
||||
|
||||
$newChargeId = (int)$this->eventChargesModel->getInsertID();
|
||||
if ($markPaid && $newChargeId > 0) {
|
||||
$this->applyEventPaymentStatus($newChargeId, true);
|
||||
}
|
||||
|
||||
if ($matchedParentId && $this->parentHasEnrollment($matchedParentId, (string)$schoolYear)) {
|
||||
$parentsForInvoice[$matchedParentId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parentId && $this->parentHasEnrollment((int)$parentId, (string)$schoolYear)) {
|
||||
$parentsForInvoice[(int)$parentId] = true;
|
||||
}
|
||||
|
||||
foreach (array_keys($parentsForInvoice) as $pid) {
|
||||
$this->invoiceController->generateInvoice((string)$pid, (string)$schoolYear, (string)$semester, false);
|
||||
$this->fixInvoiceStatusAfterCharge((int)$pid, (string)$schoolYear);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', 'Event charges updated successfully.');
|
||||
}
|
||||
|
||||
public function removeCharge($chargeId = null)
|
||||
{
|
||||
if (!$chargeId) {
|
||||
return redirect()->back()->with('error', 'Invalid charge.');
|
||||
}
|
||||
|
||||
$charge = $this->eventChargesModel->find($chargeId);
|
||||
if (!$charge) {
|
||||
return redirect()->back()->with('error', 'Charge not found.');
|
||||
}
|
||||
|
||||
$paymentId = (int)($charge['event_payment_id'] ?? 0);
|
||||
if ($paymentId > 0) {
|
||||
$this->paymentModel->delete($paymentId);
|
||||
}
|
||||
|
||||
$this->eventChargesModel->delete($chargeId);
|
||||
if (!empty($charge['parent_id']) && $this->parentHasEnrollment((int)$charge['parent_id'], (string)($charge['school_year'] ?? ''))) {
|
||||
$this->invoiceController->generateInvoice((string)$charge['parent_id'], (string)($charge['school_year'] ?? $this->schoolYear), (string)($charge['semester'] ?? $this->semester), false);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', 'Participation removed, invoices updated.');
|
||||
}
|
||||
|
||||
public function toggleEventPayment($chargeId = null)
|
||||
{
|
||||
if (!$chargeId) {
|
||||
return redirect()->back()->with('error', 'Invalid charge.');
|
||||
}
|
||||
|
||||
$isPaid = $this->request->getPost('paid') === '1';
|
||||
$meta = $this->applyEventPaymentStatus((int)$chargeId, $isPaid);
|
||||
if (!$meta) {
|
||||
return redirect()->back()->with('error', 'Charge not found.');
|
||||
}
|
||||
|
||||
if (!empty($meta['invoice_id'])) {
|
||||
$this->invoiceController->generateInvoice((string)$meta['parent_id'], (string)$meta['school_year'], (string)($meta['semester'] ?? $this->semester), false);
|
||||
$this->fixInvoiceStatusAfterCharge((int)$meta['parent_id'], (string)$meta['school_year']);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', 'Event payment status updated.');
|
||||
}
|
||||
|
||||
private function parentHasEnrollment(int $parentId, string $schoolYear): bool
|
||||
{
|
||||
if ($parentId <= 0 || $schoolYear === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->enrollmentModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->countAllResults() > 0;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function findExistingParentIdByExternalContact(string $first, string $last, string $email, string $phone): ?int
|
||||
{
|
||||
$first = trim($first);
|
||||
$last = trim($last);
|
||||
$email = trim($email);
|
||||
$phoneDigits = preg_replace('/\D+/', '', (string)$phone);
|
||||
|
||||
if ($first === '' || $last === '' || $email === '' || $phoneDigits === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::connect();
|
||||
$expr = "REPLACE(REPLACE(REPLACE(REPLACE(cellphone, '(', ''), ')', ''), '-', ''), ' ', '') =";
|
||||
|
||||
$row = $this->userModel
|
||||
->select('id')
|
||||
->where('LOWER(firstname)', strtolower($first))
|
||||
->where('LOWER(lastname)', strtolower($last))
|
||||
->where('LOWER(email)', strtolower($email))
|
||||
->where($expr, $db->escapeString($phoneDigits), false)
|
||||
->first();
|
||||
|
||||
$id = (int)($row['id'] ?? 0);
|
||||
return $id > 0 ? $id : null;
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Failed to match external parent to users: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function syncInvoicePaymentSummary(int $invoiceId): void
|
||||
{
|
||||
if ($invoiceId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$invoice = $this->invoiceModel->find($invoiceId);
|
||||
if (!$invoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
|
||||
|
||||
$paidSum = 0.0;
|
||||
try {
|
||||
$qb = $db->table('payments')
|
||||
->select('COALESCE(SUM(paid_amount),0) AS tot')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->where('paid_amount >', 0);
|
||||
|
||||
if ($db->fieldExists('status', 'payments')) {
|
||||
$qb->groupStart()
|
||||
->whereNotIn('status', $exclude)
|
||||
->orWhere('status IS NULL', null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
if ($db->fieldExists('is_void', 'payments')) {
|
||||
$qb->groupStart()
|
||||
->where('is_void', 0)
|
||||
->orWhere('is_void IS NULL', null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$row = $qb->get()->getRowArray();
|
||||
$paidSum = (float)($row['tot'] ?? 0.0);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Failed to sum payments for invoice ' . $invoiceId . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$discountSum = 0.0;
|
||||
try {
|
||||
$row = $db->table('discount_usages')
|
||||
->select('COALESCE(SUM(discount_amount),0) AS tot')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
$discountSum = (float)($row['tot'] ?? 0.0);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Failed to sum discounts for invoice ' . $invoiceId . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$refundSum = 0.0;
|
||||
try {
|
||||
$row = $db->table('refunds')
|
||||
->select('COALESCE(SUM(refund_paid_amount),0) AS tot')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->whereIn('status', ['Partial', 'Paid'])
|
||||
->get()
|
||||
->getRowArray();
|
||||
$refundSum = (float)($row['tot'] ?? 0.0);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Failed to sum refunds for invoice ' . $invoiceId . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$total = (float)($invoice['total_amount'] ?? 0.0);
|
||||
$newBalance = round($total - $discountSum - $refundSum - $paidSum, 2);
|
||||
$newStatus = ($newBalance <= 0.00001)
|
||||
? 'Paid'
|
||||
: (($paidSum > 0) ? 'Partially Paid' : 'Unpaid');
|
||||
|
||||
$this->invoiceModel->update($invoiceId, [
|
||||
'paid_amount' => $paidSum,
|
||||
'balance' => $newBalance,
|
||||
'status' => $newStatus,
|
||||
'updated_at' => utc_now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function applyEventPaymentStatus(int $chargeId, bool $isPaid): ?array
|
||||
{
|
||||
if ($chargeId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$charge = $this->eventChargesModel->find($chargeId);
|
||||
if (!$charge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$event = $this->eventModel->find($charge['event_id']);
|
||||
$eventAmount = max(0.0, (float)($event['amount'] ?? 0));
|
||||
$paymentId = (int)($charge['event_payment_id'] ?? 0);
|
||||
|
||||
$parentId = (int)($charge['parent_id'] ?? 0);
|
||||
$schoolYear = (string)($charge['school_year'] ?? $this->schoolYear);
|
||||
$semester = (string)($charge['semester'] ?? $this->semester);
|
||||
|
||||
if ($parentId > 0 && $eventAmount > 0) {
|
||||
$hasEnrollment = $this->parentHasEnrollment($parentId, $schoolYear);
|
||||
$invoiceQuery = $this->invoiceModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('id', 'DESC');
|
||||
$invoice = $invoiceQuery->first();
|
||||
|
||||
if (!$invoice && $hasEnrollment) {
|
||||
$this->invoiceController->generateInvoice((string)$parentId, $schoolYear, $semester, false);
|
||||
$invoice = $invoiceQuery->first();
|
||||
}
|
||||
|
||||
if ($invoice) {
|
||||
$invoiceId = (int)($invoice['id'] ?? 0);
|
||||
if ($invoiceId > 0) {
|
||||
$this->syncInvoicePaymentSummary($invoiceId);
|
||||
$invoice = $this->invoiceModel->find($invoiceId) ?? $invoice;
|
||||
}
|
||||
|
||||
if ($isPaid && !$paymentId) {
|
||||
$paymentSchoolYear = (string)($invoice['school_year'] ?? $schoolYear);
|
||||
$paymentSemester = (string)($invoice['semester'] ?? $semester);
|
||||
$paymentId = (int)($this->createEventPayment(
|
||||
$parentId,
|
||||
(int)$invoice['id'],
|
||||
$eventAmount,
|
||||
$paymentSchoolYear,
|
||||
$paymentSemester,
|
||||
(float)($invoice['total_amount'] ?? 0.0),
|
||||
(float)($invoice['paid_amount'] ?? 0.0),
|
||||
(float)($invoice['balance'] ?? 0.0)
|
||||
) ?? 0);
|
||||
} elseif (!$isPaid && $paymentId > 0) {
|
||||
$this->paymentModel->delete($paymentId);
|
||||
$paymentId = 0;
|
||||
}
|
||||
|
||||
if (!empty($invoiceId)) {
|
||||
$this->syncInvoicePaymentSummary($invoiceId);
|
||||
}
|
||||
} elseif (!$hasEnrollment) {
|
||||
// External-only parent: do not create an invoice/payment row; just persist the paid flag on the charge.
|
||||
$paymentId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$this->eventChargesModel->update($chargeId, [
|
||||
'event_paid' => $isPaid ? 1 : 0,
|
||||
'charged' => $eventAmount,
|
||||
'event_payment_id' => $paymentId > 0 ? $paymentId : null,
|
||||
]);
|
||||
|
||||
return [
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'invoice_id' => isset($invoiceId) && $invoiceId > 0 ? $invoiceId : null,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -336,10 +933,18 @@ class EventController extends ResourceController
|
||||
$students = $this->studentModel->where('parent_id', $parentId)->findAll();
|
||||
|
||||
// Get student_ids that already have charges
|
||||
$chargedStudentIds = $this->eventChargesModel
|
||||
$eventId = $this->request->getGet('event_id');
|
||||
|
||||
$chargesBuilder = $this->eventChargesModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if (!empty($eventId)) {
|
||||
$chargesBuilder->where('event_id', $eventId);
|
||||
}
|
||||
|
||||
$chargedStudentIds = $chargesBuilder
|
||||
->groupBy('student_id')
|
||||
->select('student_id')
|
||||
->findColumn('student_id');
|
||||
@@ -356,5 +961,98 @@ class EventController extends ResourceController
|
||||
return $this->response->setJSON($data);
|
||||
}
|
||||
|
||||
private function createEventPayment(
|
||||
int $parentId,
|
||||
int $invoiceId,
|
||||
float $amount,
|
||||
string $schoolYear,
|
||||
?string $semester,
|
||||
float $invoiceTotal = 0.0,
|
||||
float $invoicePaid = 0.0,
|
||||
float $invoiceBalance = 0.0
|
||||
): ?int
|
||||
{
|
||||
if ($amount <= 0 || $invoiceId <= 0 || $parentId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$monthSemester = $semester ?: $this->semester;
|
||||
$newPaid = (float)$invoicePaid + $amount;
|
||||
$newBalance = (float)$invoiceBalance - $amount;
|
||||
$paymentStatus = ($newBalance <= 0.00001)
|
||||
? 'Paid'
|
||||
: (($newPaid > 0) ? 'Partially Paid' : 'Unpaid');
|
||||
|
||||
$exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
|
||||
$priorCount = $this->paymentModel
|
||||
->where('invoice_id', $invoiceId)
|
||||
->where('paid_amount >', 0)
|
||||
->whereNotIn('status', $exclude)
|
||||
->countAllResults();
|
||||
$installmentSeq = $priorCount + 1;
|
||||
|
||||
$data = [
|
||||
'parent_id' => $parentId,
|
||||
'invoice_id' => $invoiceId,
|
||||
'total_amount' => $invoiceTotal > 0 ? $invoiceTotal : $amount,
|
||||
'paid_amount' => $amount,
|
||||
'balance' => $newBalance,
|
||||
'number_of_installments' => $installmentSeq,
|
||||
'payment_method' => 'cash',
|
||||
'payment_date' => utc_now(),
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $monthSemester,
|
||||
'status' => $paymentStatus,
|
||||
'transaction_id' => $this->paymentModel->generateNewTransactionId(),
|
||||
'updated_by' => session()->get('user_id'),
|
||||
];
|
||||
|
||||
if (!$this->paymentModel->insert($data)) {
|
||||
log_message('error', 'Failed to insert event payment: ' . json_encode($this->paymentModel->errors()));
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)$this->paymentModel->getInsertID();
|
||||
}
|
||||
|
||||
private function normalizeExternalName(?string $value): string
|
||||
{
|
||||
$raw = trim((string)$value);
|
||||
if ($raw === '') {
|
||||
return '';
|
||||
}
|
||||
$parts = preg_split('/\s+/', $raw);
|
||||
$formatted = array_map(function ($part) {
|
||||
$part = trim($part);
|
||||
if ($part === '') {
|
||||
return '';
|
||||
}
|
||||
return mb_strtoupper(mb_substr($part, 0, 1)) . mb_strtolower(mb_substr($part, 1));
|
||||
}, $parts);
|
||||
return implode(' ', array_filter($formatted, fn($p) => $p !== ''));
|
||||
}
|
||||
|
||||
private function fixInvoiceStatusAfterCharge($parentId, $schoolYear)
|
||||
{
|
||||
if (!$parentId || !$schoolYear) {
|
||||
return;
|
||||
}
|
||||
|
||||
$invoices = $this->invoiceModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
|
||||
foreach ($invoices as $invoice) {
|
||||
$status = strtolower(trim($invoice['status'] ?? ''));
|
||||
$balance = (float)($invoice['balance'] ?? 0.0);
|
||||
|
||||
if ($balance <= 0.00001 && $status !== 'paid') {
|
||||
$this->invoiceModel->update($invoice['id'], ['status' => 'Paid']);
|
||||
} elseif ($status === 'paid' && $balance > 0) {
|
||||
$this->invoiceModel->update($invoice['id'], ['status' => 'Unpaid']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -271,8 +271,8 @@ class FilesController extends Controller
|
||||
|
||||
private function buildDraftDownloadName(string $filename, string $subdir): string
|
||||
{
|
||||
$column = $subdir === 'finals' ? 'final_file' : 'teacher_file';
|
||||
$db = Database::connect();
|
||||
$column = $subdir === 'finals' ? 'final_file' : $this->resolveExamDraftFileColumn($db);
|
||||
$row = $db->table('exam_drafts ed')
|
||||
->select('ed.version, ed.exam_type, ed.class_section_id, cs.class_section_name')
|
||||
->join('classSection cs', 'cs.class_section_id = ed.class_section_id', 'left')
|
||||
@@ -301,4 +301,20 @@ class FilesController extends Controller
|
||||
$value = trim($value, '_');
|
||||
return $value === '' ? 'Exam' : mb_strtolower($value);
|
||||
}
|
||||
|
||||
private function resolveExamDraftFileColumn($db): string
|
||||
{
|
||||
try {
|
||||
$fields = $db->getFieldNames('exam_drafts');
|
||||
if (in_array('teacher_file', $fields, true)) {
|
||||
return 'teacher_file';
|
||||
}
|
||||
if (in_array('author_file', $fields, true)) {
|
||||
return 'author_file';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'FilesController::resolveExamDraftFileColumn error: ' . $e->getMessage());
|
||||
}
|
||||
return 'teacher_file';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,17 +68,45 @@ public function financialReport()
|
||||
}
|
||||
|
||||
// === Invoices ===
|
||||
$invoices = $invoiceModel
|
||||
->select("invoices.*, CONCAT(users.firstname, ' ', users.lastname) AS parent_name")
|
||||
->join('users', 'users.id = invoices.parent_id')
|
||||
->findAll();
|
||||
// Business rule: one active invoice per parent per school year.
|
||||
// If legacy data has multiple invoices, show only the latest one to avoid reporting older invoices as "Unpaid"
|
||||
// after new charges/payments update the current invoice.
|
||||
$db = \Config\Database::connect();
|
||||
if (!empty($schoolYear)) {
|
||||
$latestSub = $db->table('invoices')
|
||||
->select('parent_id, MAX(id) AS max_id')
|
||||
->where('school_year', $schoolYear)
|
||||
->groupBy('parent_id')
|
||||
->getCompiledSelect();
|
||||
|
||||
$invBuilder = $db->table('invoices')
|
||||
->select("invoices.*, CONCAT(users.firstname, ' ', users.lastname) AS parent_name", false)
|
||||
->join("($latestSub) latest", 'latest.max_id = invoices.id', 'inner', false)
|
||||
->join('users', 'users.id = invoices.parent_id', 'left');
|
||||
|
||||
if ($hasFrom) {
|
||||
$invBuilder->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) >=', $dateFrom);
|
||||
}
|
||||
if ($hasTo) {
|
||||
$invBuilder->where('DATE(COALESCE(invoices.issue_date, invoices.created_at)) <=', $dateTo);
|
||||
}
|
||||
|
||||
$invoices = $invBuilder->get()->getResultArray();
|
||||
} else {
|
||||
$invoices = $invoiceModel
|
||||
->select("invoices.*, CONCAT(users.firstname, ' ', users.lastname) AS parent_name")
|
||||
->join('users', 'users.id = invoices.parent_id')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
$invoiceIds = array_values(array_unique(array_filter(array_map(static function ($inv) {
|
||||
$id = (int)($inv['id'] ?? 0);
|
||||
return $id > 0 ? $id : null;
|
||||
}, $invoices))));
|
||||
|
||||
// Helper to build a fresh, filtered PaymentModel each time (so filters don't get lost between queries)
|
||||
$buildPaymentModel = function () use ($schoolYear, $dateFrom, $dateTo) {
|
||||
$pm = new PaymentModel();
|
||||
if ($schoolYear) {
|
||||
$pm->where('school_year', $schoolYear);
|
||||
}
|
||||
if (!empty($dateFrom)) {
|
||||
$pm->where('DATE(payment_date) >=', $dateFrom);
|
||||
}
|
||||
@@ -114,7 +142,13 @@ public function financialReport()
|
||||
};
|
||||
|
||||
// === Payments aggregated by invoice_id (for the "Paid" column) ===
|
||||
$payments = $applyPaymentFilters($buildPaymentModel())
|
||||
$paymentsQuery = $applyPaymentFilters($buildPaymentModel());
|
||||
if (!empty($invoiceIds)) {
|
||||
$paymentsQuery->whereIn('invoice_id', $invoiceIds);
|
||||
} else {
|
||||
$paymentsQuery->where('invoice_id', -1);
|
||||
}
|
||||
$payments = $paymentsQuery
|
||||
->select('invoice_id, SUM(paid_amount) AS paid_amount')
|
||||
->where('invoice_id IS NOT NULL')
|
||||
->groupBy('invoice_id')
|
||||
@@ -138,6 +172,7 @@ public function financialReport()
|
||||
SUM(paid_amount) AS amount
|
||||
")
|
||||
->where('invoice_id IS NOT NULL')
|
||||
->whereIn('invoice_id', $invoiceIds ?: [-1])
|
||||
->groupBy('invoice_id, method')
|
||||
->findAll();
|
||||
|
||||
@@ -161,6 +196,7 @@ public function financialReport()
|
||||
SUM(CASE WHEN LOWER(TRIM(payment_method)) IN ('credit','card','credit card','debit','debit card','visa','mastercard') THEN paid_amount ELSE 0 END) AS total_credit
|
||||
")
|
||||
->where('invoice_id IS NOT NULL')
|
||||
->whereIn('invoice_id', $invoiceIds ?: [-1])
|
||||
->first() ?? [];
|
||||
|
||||
$paymentTotals = [
|
||||
@@ -216,25 +252,28 @@ public function financialReport()
|
||||
$schoolYears[] = (string)$schoolYear;
|
||||
}
|
||||
|
||||
$eventFeesTotal = $this->getEventFeesTotal($schoolYear, $dateFrom, $dateTo);
|
||||
|
||||
// JSON API support
|
||||
if ($this->wantsJson() || strtolower((string)($this->request->getGet('format') ?? '')) === 'json') {
|
||||
return $this->response->setJSON([
|
||||
'ok' => true,
|
||||
'selectedYear' => $schoolYear,
|
||||
'dateFrom' => $dateFrom,
|
||||
'dateTo' => $dateTo,
|
||||
return $this->response->setJSON([
|
||||
'ok' => true,
|
||||
'selectedYear' => $schoolYear,
|
||||
'dateFrom' => $dateFrom,
|
||||
'dateTo' => $dateTo,
|
||||
'schoolYears' => $schoolYears,
|
||||
'invoices' => $invoices,
|
||||
'payments' => $payments,
|
||||
'paymentBreakdown' => $paymentBreakdown,
|
||||
'paymentTotals' => $paymentTotals,
|
||||
'refunds' => $refunds,
|
||||
'expenses' => $expenses,
|
||||
'reimbursements' => $reimbursements,
|
||||
'discounts' => $discounts,
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
'refunds' => $refunds,
|
||||
'expenses' => $expenses,
|
||||
'reimbursements' => $reimbursements,
|
||||
'discounts' => $discounts,
|
||||
'eventFeesTotal' => $eventFeesTotal,
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
return view('payment/financial_report', [
|
||||
@@ -246,6 +285,7 @@ public function financialReport()
|
||||
'expenses' => $expenses,
|
||||
'reimbursements' => $reimbursements,
|
||||
'discounts' => $discounts,
|
||||
'eventFeesTotal' => $eventFeesTotal,
|
||||
'selectedYear' => $schoolYear,
|
||||
'schoolYears' => $schoolYears,
|
||||
'dateFrom' => $dateFrom,
|
||||
@@ -1058,6 +1098,7 @@ public function financialReport()
|
||||
$amountCollected = $totalPaid;
|
||||
$netAmount = ($totalCharges - $totalDiscounts - $totalRefunds);
|
||||
|
||||
$totalEventFees = $this->getEventFeesTotal($schoolYear, $invoiceDateFrom, $invoiceDateTo);
|
||||
return [
|
||||
'schoolYear' => $schoolYear,
|
||||
'dateFrom' => $dateFrom,
|
||||
@@ -1073,9 +1114,28 @@ public function financialReport()
|
||||
'amountCollected' => $amountCollected,
|
||||
'totalUnpaid' => $totalUnpaid,
|
||||
'netAmount' => $netAmount,
|
||||
'totalEventFees' => $totalEventFees,
|
||||
];
|
||||
}
|
||||
|
||||
private function getEventFeesTotal(?string $schoolYear, ?string $dateFrom, ?string $dateTo): float
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('event_charges ec')
|
||||
->select('COALESCE(SUM(ec.charged),0) AS amount', false);
|
||||
if (!empty($schoolYear)) {
|
||||
$builder->where('ec.school_year', $schoolYear);
|
||||
}
|
||||
if (!empty($dateFrom)) {
|
||||
$builder->where('DATE(ec.created_at) >=', $dateFrom);
|
||||
}
|
||||
if (!empty($dateTo)) {
|
||||
$builder->where('DATE(ec.created_at) <=', $dateTo);
|
||||
}
|
||||
$row = $builder->get()->getRowArray();
|
||||
return $row ? (float)($row['amount'] ?? 0) : 0.0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Management page: list parents with outstanding balances (> 0) for a school year.
|
||||
@@ -1226,6 +1286,23 @@ public function financialReport()
|
||||
$byParent[$pid]['total_balance'] += $extra;
|
||||
}
|
||||
|
||||
$eventFeesPerParent = [];
|
||||
try {
|
||||
$eventFeesRows = $db->table('event_charges ec')
|
||||
->select('ec.parent_id, COALESCE(SUM(ec.charged),0) AS event_fees', false)
|
||||
->where('ec.school_year', $schoolYear)
|
||||
->groupBy('ec.parent_id')
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($eventFeesRows as $row) {
|
||||
$pid = (int)($row['parent_id'] ?? 0);
|
||||
if ($pid <= 0) continue;
|
||||
$eventFeesPerParent[$pid] = (float)($row['event_fees'] ?? 0);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore, fallback to no event fees data
|
||||
}
|
||||
|
||||
// Reduce into rows list; only parents with positive balance
|
||||
// Also compute remaining installments and suggested monthly amount
|
||||
$rows = [];
|
||||
@@ -1305,7 +1382,7 @@ public function financialReport()
|
||||
}
|
||||
}
|
||||
|
||||
$dataRows = array_map(function(array $r) use ($hasPayments, $paidTotals, $paymentCounts, $nextInstallmentYmd) {
|
||||
$dataRows = array_map(function(array $r) use ($hasPayments, $paidTotals, $paymentCounts, $nextInstallmentYmd, $eventFeesPerParent) {
|
||||
$pid = (int)($r['parent_id'] ?? 0);
|
||||
$name = trim((string)($r['firstname'] ?? '') . ' ' . (string)($r['lastname'] ?? ''));
|
||||
return [
|
||||
@@ -1322,6 +1399,7 @@ public function financialReport()
|
||||
'payment_count' => isset($paymentCounts[$pid]) ? (int)$paymentCounts[$pid] : 0,
|
||||
'has_installment'=> isset($hasPayments[$pid]) ? 1 : 0,
|
||||
'next_installment' => $nextInstallmentYmd,
|
||||
'event_fees' => (float)($eventFeesPerParent[$pid] ?? 0),
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ class HomeworkTrackingController extends BaseController
|
||||
|
||||
$hasHomework = [];
|
||||
$hwEnteredAt = [];
|
||||
$homeworkSubmissionCounts = [];
|
||||
foreach ($rows as $r) {
|
||||
$csid = (int)($r['class_section_id'] ?? 0);
|
||||
$hi = (int)($r['homework_index'] ?? 0);
|
||||
@@ -94,6 +95,7 @@ class HomeworkTrackingController extends BaseController
|
||||
$hasHomework[$csid][$hi] = true;
|
||||
$dateStr = substr((string)($r['first_created'] ?? ''), 0, 10);
|
||||
$hwEnteredAt[$csid][$hi] = $dateStr ?: null;
|
||||
$homeworkSubmissionCounts[$csid] = ($homeworkSubmissionCounts[$csid] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +219,7 @@ class HomeworkTrackingController extends BaseController
|
||||
'teachers' => $teachersPage,
|
||||
'hasHomework' => $hasHomework,
|
||||
'hwEnteredAt' => $hwEnteredAt,
|
||||
'homeworkSubmissionCounts' => $homeworkSubmissionCounts,
|
||||
'hasHomeworkByDate' => $hasHomeworkByDate,
|
||||
'hwEnteredAtByDate' => $hwEnteredAtByDate,
|
||||
'page' => $page,
|
||||
|
||||
@@ -381,18 +381,24 @@ class InvoiceController extends ResourceController
|
||||
return false;
|
||||
}
|
||||
|
||||
public function generateInvoice(string $parentId = null)
|
||||
public function generateInvoice(
|
||||
string $parentId = null,
|
||||
?string $schoolYearOverride = null,
|
||||
?string $semesterOverride = null,
|
||||
bool $recalculateDiscounts = true
|
||||
)
|
||||
{
|
||||
$isAjax = $this->request->isAJAX() || str_contains(strtolower($this->request->getHeaderLine('Accept')), 'application/json');
|
||||
if ($parentId == null) {
|
||||
$parentId = (int)$this->request->getPost('parent_id');
|
||||
}
|
||||
$schoolYear = (string) $this->schoolYear;
|
||||
$schoolYear = (string) ($schoolYearOverride ?: $this->schoolYear);
|
||||
$semester = (string) ($semesterOverride ?: $this->semester);
|
||||
|
||||
// Fetch enrolled + withdrawn students
|
||||
$enrollments = $this->enrollmentModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $this->schoolYear)
|
||||
->where('school_year', $schoolYear)
|
||||
->findAll();
|
||||
|
||||
if (empty($enrollments)) {
|
||||
@@ -444,21 +450,23 @@ class InvoiceController extends ResourceController
|
||||
$tuitionFee = $fees['tuition_fee'];
|
||||
|
||||
// ✅ Fetch event charges
|
||||
$eventsList = $this->chargesModel->getChargesWithEventInfo($parentId, $this->schoolYear);
|
||||
$eventsList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear);
|
||||
$eventchargeTotal = array_sum(array_column($eventsList, 'charged'));
|
||||
|
||||
$totalDiscount = $this->recalculateAndUpdateDiscount(
|
||||
$parentId,
|
||||
$this->schoolYear,
|
||||
$tuitionFee,
|
||||
$enrollments
|
||||
);
|
||||
$totalDiscount = 0.0;
|
||||
if ($recalculateDiscounts) {
|
||||
$totalDiscount = $this->recalculateAndUpdateDiscount(
|
||||
$parentId,
|
||||
$schoolYear,
|
||||
$tuitionFee,
|
||||
$enrollments
|
||||
);
|
||||
}
|
||||
|
||||
$semester = $this->semester;
|
||||
// ✅ Refunds PAID to the parent for this year (Partial/Paid)
|
||||
$refundPaid = (float) $this->refundModel->getTotalApprovedRefundByParentIdAndSchoolYear($parentId, $this->schoolYear);
|
||||
$refundPaid = (float) $this->refundModel->getTotalApprovedRefundByParentIdAndSchoolYear($parentId, $schoolYear);
|
||||
|
||||
$totalPaid = $this->paymentModel->getTotalPaidByParentId($parentId, $this->schoolYear);
|
||||
$totalPaid = $this->paymentModel->getTotalPaidByParentId($parentId, $schoolYear);
|
||||
|
||||
$discountedTuition = max(0, $tuitionFee);
|
||||
$totalAmount = $discountedTuition + $eventchargeTotal;
|
||||
@@ -469,17 +477,22 @@ class InvoiceController extends ResourceController
|
||||
- $refundPaid // approved refunds paid to parent
|
||||
- $totalPaid; // payments received
|
||||
|
||||
// Your invoice fetch
|
||||
$existingInvoices = $this->invoiceModel->getInvoicesByParentId($parentId, $this->schoolYear);
|
||||
// Business rule: single invoice per parent per school year.
|
||||
// If legacy duplicates exist, prefer the invoice that already has a discount applied,
|
||||
// otherwise use the latest invoice for the parent/year.
|
||||
$invoice = $this->selectActiveInvoiceForParentYear((int)$parentId, $schoolYear);
|
||||
|
||||
$updated = false;
|
||||
|
||||
$updatedIds = [];
|
||||
if (!empty($existingInvoices)) {
|
||||
foreach ($existingInvoices as $invoice) {
|
||||
if (!isset($invoice['id'])) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($invoice) && isset($invoice['id'])) {
|
||||
$paymentExclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
|
||||
$paymentsHasStatus = false;
|
||||
$paymentsHasVoid = false;
|
||||
try {
|
||||
$paymentsHasStatus = $this->db->fieldExists('status', 'payments');
|
||||
$paymentsHasVoid = $this->db->fieldExists('is_void', 'payments');
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
// Preserve applied additional charges and recalc this invoice only
|
||||
$extrasSum = 0.0;
|
||||
@@ -487,7 +500,7 @@ class InvoiceController extends ResourceController
|
||||
$rows = $this->db->table('additional_charges')
|
||||
->select('charge_type, amount')
|
||||
->where('invoice_id', (int)$invoice['id'])
|
||||
->where('school_year', $this->schoolYear)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('status', 'applied')
|
||||
->get()->getResultArray();
|
||||
foreach ($rows as $r) {
|
||||
@@ -520,7 +533,7 @@ class InvoiceController extends ResourceController
|
||||
$r = $this->db->table('refunds')
|
||||
->select('COALESCE(SUM(refund_paid_amount),0) AS tot')
|
||||
->where('invoice_id', (int)$invoice['id'])
|
||||
->where('school_year', $this->schoolYear)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('status', ['Partial','Paid'])
|
||||
->get()->getRowArray();
|
||||
$invRefunds = (float)($r['tot'] ?? 0.0);
|
||||
@@ -528,8 +541,33 @@ class InvoiceController extends ResourceController
|
||||
log_message('error', 'refund sum failed for invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Payments recorded on this invoice (denormalized field kept in sync by PaymentController)
|
||||
$paidOnInv = (float)($invoice['paid_amount'] ?? 0.0);
|
||||
// Payments recorded on this invoice (sum payments to avoid stale invoice.paid_amount)
|
||||
$paidOnInv = 0.0;
|
||||
try {
|
||||
$qb = $this->db->table('payments')
|
||||
->select('COALESCE(SUM(paid_amount),0) AS tot')
|
||||
->where('invoice_id', (int)$invoice['id'])
|
||||
->where('paid_amount >', 0);
|
||||
|
||||
if ($paymentsHasStatus) {
|
||||
$qb->groupStart()
|
||||
->whereNotIn('status', $paymentExclude)
|
||||
->orWhere('status IS NULL', null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
if ($paymentsHasVoid) {
|
||||
$qb->groupStart()
|
||||
->where('is_void', 0)
|
||||
->orWhere('is_void IS NULL', null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$row = $qb->get()->getRowArray();
|
||||
$paidOnInv = (float)($row['tot'] ?? 0.0);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'payment sum failed for invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
|
||||
$paidOnInv = (float)($invoice['paid_amount'] ?? 0.0);
|
||||
}
|
||||
|
||||
$newBalance = $newTotal - $invDiscount - $invRefunds - $paidOnInv;
|
||||
$newStatus = ($newBalance <= 0.00001)
|
||||
@@ -547,7 +585,6 @@ class InvoiceController extends ResourceController
|
||||
$updatedIds[] = (int)$invoice['id'];
|
||||
log_message('info', "Updated invoice ID {$invoice['id']} for parent ID {$parentId}.");
|
||||
$updated = true;
|
||||
}
|
||||
} else {
|
||||
// Generate invoice number
|
||||
$schoolId = $this->userModel->getSchoolIdByUserId($parentId);
|
||||
@@ -578,7 +615,7 @@ class InvoiceController extends ResourceController
|
||||
// Initial balance equals the created total; discounts/refunds/payments will adjust later
|
||||
'balance' => $totalAmount,
|
||||
'status' => 'Unpaid',
|
||||
'school_year' => $this->schoolYear,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'issue_date' => $issueUtc,
|
||||
'due_date' => $dueUtc,
|
||||
@@ -615,6 +652,47 @@ class InvoiceController extends ResourceController
|
||||
->with('success', $updated ? 'Invoice updated.' : 'Invoice created.');
|
||||
}
|
||||
|
||||
private function selectActiveInvoiceForParentYear(int $parentId, string $schoolYear): ?array
|
||||
{
|
||||
if ($parentId <= 0 || $schoolYear === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prefer invoices flagged as having discounts.
|
||||
$invoice = $this->invoiceModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('has_discount', 1)
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
if (!empty($invoice)) {
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
// Fallback: prefer invoices with discount_usages rows.
|
||||
try {
|
||||
$row = $this->db->table('invoices i')
|
||||
->select('i.*')
|
||||
->join('discount_usages du', 'du.invoice_id = i.id', 'inner')
|
||||
->where('i.parent_id', $parentId)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->orderBy('i.id', 'DESC')
|
||||
->get()
|
||||
->getRowArray();
|
||||
if (!empty($row)) {
|
||||
return $row;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
// Final fallback: latest invoice for parent/year.
|
||||
return $this->invoiceModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
}
|
||||
|
||||
private function recalculateAndUpdateDiscount(
|
||||
int $parentId,
|
||||
string $schoolYear,
|
||||
@@ -678,7 +756,7 @@ class InvoiceController extends ResourceController
|
||||
log_message('error', 'additional_charges sum failed for discount recalculation invoice ' . (int)$invoice['id'] . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$baseTotal = round($tuitionFee + $eventchargeTotal + $extrasSum, 2);
|
||||
$baseTotal = round($tuitionFee + $extrasSum, 2);
|
||||
|
||||
// Recalculate discount
|
||||
if ($discountUsage['discount_type'] === 'percent') {
|
||||
@@ -1266,6 +1344,12 @@ class InvoiceController extends ResourceController
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($studentName === 'N/A') {
|
||||
$externalName = trim((string)($event['external_firstname'] ?? '') . ' ' . (string)($event['external_lastname'] ?? ''));
|
||||
if ($externalName !== '') {
|
||||
$studentName = $externalName . ' (external)';
|
||||
}
|
||||
}
|
||||
|
||||
$dt = $toLocal($event['created_at'] ?? null, false);
|
||||
$amount = (float)($event['charged'] ?? 0.0);
|
||||
@@ -1612,12 +1696,24 @@ private function getGradeLevel($grade): array
|
||||
$invoice['last_payment_date'] = $lastPayments[$invoice['id']]['last_payment_date'] ?? null;
|
||||
}
|
||||
|
||||
$invoiceEventCharges = [];
|
||||
foreach ($invoices as $invoice) {
|
||||
$year = $invoice['school_year'] ?? $this->schoolYear;
|
||||
$sem = $invoice['semester'] ?? $this->semester;
|
||||
$invoiceEventCharges[(int)$invoice['id']] = $this->chargesModel->getChargesWithEventInfo(
|
||||
$invoice['parent_id'],
|
||||
$year,
|
||||
$sem
|
||||
);
|
||||
}
|
||||
|
||||
return view('/parent/invoice_payment', [
|
||||
'invoices' => $invoices,
|
||||
'schoolYears' => $schoolYears,
|
||||
'selectedYear' => $selectedYear,
|
||||
'currentSchoolYear' => $currentSchoolYear,
|
||||
'dueDate' => $this->dueDate
|
||||
'dueDate' => $this->dueDate,
|
||||
'invoiceEventCharges' => $invoiceEventCharges,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,11 @@ class LandingPageController extends BaseController
|
||||
return null;
|
||||
}
|
||||
|
||||
// Teacher class sections only apply to the teacher role; other roles have no teacher_class rows.
|
||||
if (strtolower(trim((string) $this->getUserRole())) !== 'teacher') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get all class assignments for this teacher in the current term
|
||||
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId(
|
||||
(int)$user_id,
|
||||
@@ -106,7 +111,7 @@ class LandingPageController extends BaseController
|
||||
$ids = array_values(array_filter(array_unique($ids)));
|
||||
|
||||
if (empty($ids)) {
|
||||
log_message('error', "No class section found for user ID: $user_id");
|
||||
log_message('warning', "No class section found for user ID: $user_id");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -868,8 +873,12 @@ class LandingPageController extends BaseController
|
||||
|
||||
protected function getUserRole()
|
||||
{
|
||||
// Assuming you have a session variable storing the user role
|
||||
return session()->get('user_role', 'guest'); // Default to guest if not set
|
||||
$active = session()->get('active_role');
|
||||
if ($active !== null && $active !== '') {
|
||||
return (string) $active;
|
||||
}
|
||||
|
||||
return (string) (session()->get('role') ?? 'guest');
|
||||
}
|
||||
|
||||
protected function getUserRoleFromDatabase($user_id)
|
||||
|
||||
@@ -1287,14 +1287,14 @@ class ScoreController extends Controller
|
||||
public function viewStudentScore()
|
||||
{
|
||||
$parentId = session()->get('user_id');
|
||||
$userType = $_SESSION['user_type'];
|
||||
$userType = session()->get('user_type') ?? '';
|
||||
$firstParentId = null;
|
||||
$releaseFall = $this->getParentScoresReleasedForSemester('Fall');
|
||||
$releaseSpring = $this->getParentScoresReleasedForSemester('Spring');
|
||||
$releaseAny = $releaseFall || $releaseSpring;
|
||||
|
||||
// Identify the firstparent based on user type
|
||||
if ($userType === 'primary') {
|
||||
if ($userType === 'primary' || $userType === '') {
|
||||
$firstParentId = $parentId;
|
||||
} elseif ($userType === 'secondary') {
|
||||
$parentData = $this->db->table('parents')
|
||||
|
||||
@@ -297,12 +297,16 @@ class TeacherController extends BaseController
|
||||
$schoolYear = $forYear ?: ((string)($this->schoolYear ?? 'Not Set'));
|
||||
|
||||
$teachers = $this->teacherModel->getTeachersAndTAs();
|
||||
// Prefer class sections for the selected year, fallback to all if none
|
||||
$classSections = $classSectionModel
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
if (empty($classSections)) {
|
||||
// Prefer class sections for the selected year if the column exists, otherwise fallback to all.
|
||||
if ($this->db->fieldExists('school_year', 'classSection')) {
|
||||
$classSections = $classSectionModel
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
if (empty($classSections)) {
|
||||
$classSections = $classSectionModel->orderBy('class_section_name', 'ASC')->findAll();
|
||||
}
|
||||
} else {
|
||||
$classSections = $classSectionModel->orderBy('class_section_name', 'ASC')->findAll();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddReviewRevisionToExamDrafts extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (! $this->db->fieldExists('review_revision', 'exam_drafts')) {
|
||||
$this->forge->addColumn('exam_drafts', [
|
||||
'review_revision' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
'after' => 'version',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->db->fieldExists('review_revision', 'exam_drafts')) {
|
||||
$this->forge->dropColumn('exam_drafts', 'review_revision');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddEventPaidFlagToCharges extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$fields = [
|
||||
'event_paid' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
'after' => 'charged',
|
||||
],
|
||||
];
|
||||
|
||||
$this->forge->addColumn('event_charges', $fields);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropColumn('event_charges', 'event_paid');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddClassSectionToEventCharges extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$fields = [
|
||||
'class_section_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
'after' => 'event_paid',
|
||||
],
|
||||
];
|
||||
|
||||
if (! $this->db->fieldExists('class_section_id', 'event_charges')) {
|
||||
$this->forge->addColumn('event_charges', $fields);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->db->fieldExists('class_section_id', 'event_charges')) {
|
||||
$this->forge->dropColumn('event_charges', 'class_section_id');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddEventPaymentRefToEventCharges extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$fields = [
|
||||
'event_payment_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
'after' => 'class_section_id',
|
||||
],
|
||||
];
|
||||
|
||||
if (! $this->db->fieldExists('event_payment_id', 'event_charges')) {
|
||||
$this->forge->addColumn('event_charges', $fields);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->db->fieldExists('event_payment_id', 'event_charges')) {
|
||||
$this->forge->dropColumn('event_charges', 'event_payment_id');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddExternalParticipantFieldsToEventCharges extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$fields = [
|
||||
'external_firstname' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 150,
|
||||
'null' => true,
|
||||
'after' => 'event_payment_id',
|
||||
],
|
||||
'external_lastname' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 150,
|
||||
'null' => true,
|
||||
],
|
||||
'external_note' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 254,
|
||||
'null' => true,
|
||||
],
|
||||
];
|
||||
|
||||
if (! $this->db->fieldExists('external_firstname', 'event_charges')) {
|
||||
$this->forge->addColumn('event_charges', $fields);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
foreach (['external_firstname', 'external_lastname', 'external_note'] as $field) {
|
||||
if ($this->db->fieldExists($field, 'event_charges')) {
|
||||
$this->forge->dropColumn('event_charges', $field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddExternalParentInfoToEventCharges extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$fields = [
|
||||
'external_parent_firstname' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 150,
|
||||
'null' => true,
|
||||
'after' => 'external_note',
|
||||
],
|
||||
'external_parent_lastname' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 150,
|
||||
'null' => true,
|
||||
],
|
||||
'external_parent_phone' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
],
|
||||
];
|
||||
|
||||
if (! $this->db->fieldExists('external_parent_firstname', 'event_charges')) {
|
||||
$this->forge->addColumn('event_charges', $fields);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
foreach (['external_parent_firstname', 'external_parent_lastname', 'external_parent_phone'] as $field) {
|
||||
if ($this->db->fieldExists($field, 'event_charges')) {
|
||||
$this->forge->dropColumn('event_charges', $field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddExternalParentEmailToEventCharges extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$fields = [
|
||||
'external_parent_email' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 254,
|
||||
'null' => true,
|
||||
'after' => 'external_parent_phone',
|
||||
],
|
||||
];
|
||||
|
||||
if (! $this->db->fieldExists('external_parent_email', 'event_charges')) {
|
||||
$this->forge->addColumn('event_charges', $fields);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->db->fieldExists('external_parent_email', 'event_charges')) {
|
||||
$this->forge->dropColumn('event_charges', 'external_parent_email');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,13 @@ class AuthFilter implements FilterInterface
|
||||
|
||||
// Must be logged in
|
||||
if (empty($userRoles) || empty($userId)) {
|
||||
return redirect()->to('/login');
|
||||
$target = ltrim($request->getUri()->getPath(), '/');
|
||||
$query = $request->getUri()->getQuery();
|
||||
if ($query !== '') {
|
||||
$target .= '?' . $query;
|
||||
}
|
||||
|
||||
return redirect()->to('/login?redirect_to=' . rawurlencode('/' . ltrim($target, '/')));
|
||||
}
|
||||
|
||||
// Enforce 12-hour session lifetime
|
||||
@@ -110,7 +116,14 @@ class AuthFilter implements FilterInterface
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->to('/login')->with('error', 'Your session has expired. Please log in again.');
|
||||
$target = ltrim($request->getUri()->getPath(), '/');
|
||||
$query = $request->getUri()->getQuery();
|
||||
if ($query !== '') {
|
||||
$target .= '?' . $query;
|
||||
}
|
||||
|
||||
return redirect()->to('/login?redirect_to=' . rawurlencode('/' . ltrim($target, '/')))
|
||||
->with('error', 'Your session has expired. Please log in again.');
|
||||
}
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/AuthHelper.php';
|
||||
@@ -4,6 +4,15 @@ namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
/**
|
||||
* Weekly class progress reports (one row per subject per week).
|
||||
*
|
||||
* unit_title stores a compact summary of unit/chapter rows: segments joined with " ; ".
|
||||
* Teacher-entered custom Islamic or Quran topics use the prefix "Custom / {text}"
|
||||
* (see {@see \App\Controllers\ClassProgressController::CUSTOM_UNIT_ROW_LABEL} and
|
||||
* {@see \App\Controllers\ClassProgressController::splitUnitTitleForDisplay()}).
|
||||
* DB column is VARCHAR(120); controller truncates to match.
|
||||
*/
|
||||
class ClassProgressReportModel extends Model
|
||||
{
|
||||
protected $table = 'class_progress_reports';
|
||||
@@ -27,7 +36,42 @@ class ClassProgressReportModel extends Model
|
||||
'flags_json',
|
||||
'attachment_path',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
/**
|
||||
* Validation is performed in {@see \App\Controllers\ClassProgressController} on HTTP input.
|
||||
* Rules below document schema limits and can be enabled if you set {@see $skipValidation} to false.
|
||||
*/
|
||||
protected $skipValidation = true;
|
||||
|
||||
protected $validationRules = [
|
||||
'class_section_id' => 'required|integer',
|
||||
'teacher_id' => 'required|integer',
|
||||
'week_start' => 'required|valid_date[Y-m-d]',
|
||||
'week_end' => 'required|valid_date[Y-m-d]',
|
||||
'subject' => 'required|string|max_length[160]',
|
||||
'unit_title' => 'permit_empty|string|max_length[120]',
|
||||
'covered' => 'permit_empty|string',
|
||||
'homework' => 'permit_empty|string',
|
||||
'assessment' => 'permit_empty|string',
|
||||
'status' => 'permit_empty|in_list[on_track,slightly_behind,behind]',
|
||||
'status_notes' => 'permit_empty|string|max_length[200]',
|
||||
'class_notes' => 'permit_empty|string',
|
||||
'next_week_plan' => 'permit_empty|string',
|
||||
'support_needed' => 'permit_empty|string',
|
||||
'flags_json' => 'permit_empty|string',
|
||||
'attachment_path' => 'permit_empty|string|max_length[255]',
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'unit_title' => [
|
||||
'max_length' => 'Unit and chapter summary cannot exceed 120 characters.',
|
||||
],
|
||||
'subject' => [
|
||||
'max_length' => 'Subject cannot exceed 160 characters.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -15,8 +15,19 @@ class EventChargesModel extends Model
|
||||
'student_id',
|
||||
'participation',
|
||||
'charged',
|
||||
'event_paid',
|
||||
'event_payment_id',
|
||||
'class_section_id',
|
||||
'external_firstname',
|
||||
'external_lastname',
|
||||
'external_note',
|
||||
'external_parent_firstname',
|
||||
'external_parent_lastname',
|
||||
'external_parent_phone',
|
||||
'external_parent_email',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
@@ -30,7 +41,7 @@ class EventChargesModel extends Model
|
||||
|
||||
public function getChargesWithEventInfo($parentId = null, $schoolYear = null, $semester = null)
|
||||
{
|
||||
$builder = $this->select('event_charges.*, events.event_name, events.amount')
|
||||
$builder = $this->select('event_charges.*, events.event_name, events.amount AS event_amount, events.description AS event_description')
|
||||
->join('events', 'events.id = event_charges.event_id', 'left');
|
||||
|
||||
if ($parentId) {
|
||||
|
||||
@@ -8,19 +8,51 @@ class ExamDraftModel extends Model
|
||||
{
|
||||
protected $table = 'exam_drafts';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected bool $updateOnlyChanged = false;
|
||||
|
||||
/** @var list<string> Stored in `status` column */
|
||||
public const STATUSES = [
|
||||
'submitted',
|
||||
'accepted',
|
||||
'review needed',
|
||||
'rejected',
|
||||
'canceled',
|
||||
'under review',
|
||||
'legacy',
|
||||
];
|
||||
|
||||
/** @var list<string> Stored in `acceptance_type` when status is finalized */
|
||||
public const ACCEPTANCE_TYPES = [
|
||||
'as_is',
|
||||
'minor_edits',
|
||||
];
|
||||
|
||||
protected $allowedFields = [
|
||||
'teacher_id',
|
||||
'author_id',
|
||||
'class_section_id',
|
||||
'semester',
|
||||
'school_year',
|
||||
'exam_type',
|
||||
'draft_title',
|
||||
'author_comment',
|
||||
'description',
|
||||
'teacher_file',
|
||||
'teacher_filename',
|
||||
'author_file',
|
||||
'author_filename',
|
||||
'status',
|
||||
'acceptance_type',
|
||||
'review_revision',
|
||||
'reviewer_id',
|
||||
'admin_id',
|
||||
'is_legacy',
|
||||
'reviewer_comment',
|
||||
'reviewer_comments',
|
||||
'admin_comments',
|
||||
'reviewed_at',
|
||||
'final_file',
|
||||
@@ -29,9 +61,40 @@ class ExamDraftModel extends Model
|
||||
'version',
|
||||
'previous_draft_id',
|
||||
];
|
||||
protected $returnType = 'array';
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected bool $updateOnlyChanged = false; // force updates even if CI thinks nothing changed
|
||||
|
||||
/**
|
||||
* Applied on insert/update when validation runs (e.g. $model->insert($data, true)).
|
||||
* Uses if_exist so partial updates still validate only keys present in $data.
|
||||
*/
|
||||
protected $validationRules = [
|
||||
'status' => 'if_exist|in_list[submitted,accepted,review needed,rejected,canceled,under review,legacy]',
|
||||
'acceptance_type' => 'if_exist|permit_empty|in_list[as_is,minor_edits]',
|
||||
'author_id' => 'if_exist|is_natural_no_zero',
|
||||
'class_section_id' => 'if_exist|is_natural_no_zero',
|
||||
'version' => 'if_exist|is_natural_no_zero',
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'status' => [
|
||||
'in_list' => 'Invalid exam draft status.',
|
||||
],
|
||||
'acceptance_type' => [
|
||||
'in_list' => 'Acceptance must be as_is or minor_edits.',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $casts = [
|
||||
'teacher_id' => 'int',
|
||||
'author_id' => 'int',
|
||||
'class_section_id' => 'int',
|
||||
'reviewer_id' => '?int',
|
||||
'admin_id' => '?int',
|
||||
'review_revision' => 'int',
|
||||
'version' => 'int',
|
||||
'previous_draft_id' => '?int',
|
||||
'is_legacy' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class TeacherClassModel extends Model
|
||||
$builder = $this->db->table('teacher_class tc')
|
||||
->select([
|
||||
'cs.class_section_name',
|
||||
'cs.id AS class_section_pk',
|
||||
'cs.class_section_id AS class_section_pk',
|
||||
'cs.class_section_id',
|
||||
'c.id AS class_id',
|
||||
'c.class_name',
|
||||
@@ -181,10 +181,10 @@ class TeacherClassModel extends Model
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// Adjust table/column names if yours differ (e.g., cs.id vs cs.class_section_id)
|
||||
// Adjust table/column names if yours differ (e.g., cs.class_section_id vs cs.id)
|
||||
$row = $db->table('classSection cs')
|
||||
->select('u.id AS teacher_id, u.firstname, u.lastname')
|
||||
->join('teacher_class tc', 'tc.class_section_id = cs.id', 'inner')
|
||||
->join('teacher_class tc', 'tc.class_section_id = cs.class_section_id', 'inner')
|
||||
->join('users u', 'u.id = tc.teacher_id', 'inner')
|
||||
->where('cs.class_section_name', $classSectionName)
|
||||
->where('tc.school_year', $schoolYear)
|
||||
|
||||
@@ -34,11 +34,11 @@ class TeacherModel extends Model
|
||||
public function getTeachersAndTAs(): array
|
||||
{
|
||||
return $this->db->table('users u')
|
||||
->select('u.id, u.firstname, u.lastname, u.email, u.cellphone, r.name as role')
|
||||
->select('u.id, u.firstname, u.lastname, u.email, u.cellphone, MIN(r.name) as role')
|
||||
->join('user_roles ur', 'ur.user_id = u.id')
|
||||
->join('roles r', 'r.id = ur.role_id')
|
||||
->whereIn('r.name', ['teacher', 'teacher_assistant']) // ✅ Filter only relevant roles
|
||||
->groupBy('u.id')
|
||||
->groupBy('u.id, u.firstname, u.lastname, u.email, u.cellphone')
|
||||
->orderBy('u.lastname', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
@@ -72,4 +72,4 @@ public function getTeachersAndTAs(): array
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ class EmailService
|
||||
{
|
||||
protected array $senders;
|
||||
protected EmailController $mailer;
|
||||
protected int $lastCcBatchCount = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -73,6 +74,45 @@ class EmailService
|
||||
return $allOk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send one email with all recipients in CC.
|
||||
*/
|
||||
public function sendCc(array $ccList, string $subject, string $message, string $fromKey = 'general', array $attachments = [], array $headers = []): bool
|
||||
{
|
||||
$this->lastCcBatchCount = 0;
|
||||
|
||||
if (empty($ccList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$sender = $this->getSenderDetails($fromKey);
|
||||
$to = trim((string) ($sender['email'] ?? ''));
|
||||
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
$fallback = trim((string) (getenv('SMTP_USER') ?: ''));
|
||||
$to = filter_var($fallback, FILTER_VALIDATE_EMAIL) ? $fallback : 'no-reply@alrahmaisgl.org';
|
||||
}
|
||||
|
||||
$attachmentsPayload = $this->normalizeAttachments($attachments);
|
||||
$chunks = array_chunk(array_values($ccList), 10);
|
||||
$this->lastCcBatchCount = count($chunks);
|
||||
$allOk = true;
|
||||
|
||||
foreach ($chunks as $index => $chunk) {
|
||||
$ok = $this->mailer->sendEmailWithCc($to, $chunk, $subject, $message, $fromKey, null, null, $attachmentsPayload);
|
||||
if (!$ok) {
|
||||
$allOk = false;
|
||||
log_message('error', 'EmailService::sendCc failed for batch ' . ($index + 1) . ' of ' . count($chunks));
|
||||
}
|
||||
}
|
||||
|
||||
return $allOk;
|
||||
}
|
||||
|
||||
public function getLastCcBatchCount(): int
|
||||
{
|
||||
return $this->lastCcBatchCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize various attachment inputs to the format expected by EmailController.
|
||||
*/
|
||||
|
||||
@@ -212,42 +212,42 @@
|
||||
<div class="col-4">
|
||||
<img
|
||||
class="img-fluid rounded bg-light p-1"
|
||||
src=""assets/images/classes-1.jpg"
|
||||
src="assets/images/classes-1.jpg"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img
|
||||
class="img-fluid rounded bg-light p-1"
|
||||
src=""assets/images/classes-2.jpg"
|
||||
src="assets/images/classes-2.jpg"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img
|
||||
class="img-fluid rounded bg-light p-1"
|
||||
src=""assets/images/classes-3.jpg"
|
||||
src="assets/images/classes-3.jpg"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img
|
||||
class="img-fluid rounded bg-light p-1"
|
||||
src=""assets/images/classes-4.jpg"
|
||||
src="assets/images/classes-4.jpg"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img
|
||||
class="img-fluid rounded bg-light p-1"
|
||||
src=""assets/images/classes-5.jpg"
|
||||
src="assets/images/classes-5.jpg"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img
|
||||
class="img-fluid rounded bg-light p-1"
|
||||
src=""assets/images/classes-6.jpg"
|
||||
src="assets/images/classes-6.jpg"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -170,22 +170,22 @@
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-1.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-2.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-3.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-4.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-5.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-6.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,4 +236,4 @@
|
||||
<script src="<?= base_url('js/main.js') ?>"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -168,12 +168,33 @@
|
||||
$weekLabel .= ' – ' . date('M d, Y', strtotime($group['week_end']));
|
||||
}
|
||||
$reports = $group['reports'] ?? [];
|
||||
$teacherName = '';
|
||||
$teacherCounts = [];
|
||||
$teacherLatest = [];
|
||||
foreach ($reports as $report) {
|
||||
if (! empty($report['teacher_name'])) {
|
||||
$teacherName = $report['teacher_name'];
|
||||
break;
|
||||
$name = trim((string) ($report['teacher_name'] ?? ''));
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
$teacherCounts[$name] = ($teacherCounts[$name] ?? 0) + 1;
|
||||
$stamp = (string) ($report['updated_at'] ?? $report['created_at'] ?? '');
|
||||
if ($stamp !== '' && (!isset($teacherLatest[$name]) || $stamp > $teacherLatest[$name])) {
|
||||
$teacherLatest[$name] = $stamp;
|
||||
}
|
||||
}
|
||||
$teacherLabel = '-';
|
||||
if (!empty($teacherCounts)) {
|
||||
$bestName = '';
|
||||
$bestCount = -1;
|
||||
$bestStamp = '';
|
||||
foreach ($teacherCounts as $name => $count) {
|
||||
$stamp = $teacherLatest[$name] ?? '';
|
||||
if ($count > $bestCount || ($count === $bestCount && $stamp > $bestStamp)) {
|
||||
$bestName = $name;
|
||||
$bestCount = $count;
|
||||
$bestStamp = $stamp;
|
||||
}
|
||||
}
|
||||
$teacherLabel = $bestName ?: '-';
|
||||
}
|
||||
$exampleId = $reports ? reset($reports)['id'] : null;
|
||||
?>
|
||||
@@ -193,12 +214,22 @@
|
||||
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong>
|
||||
<span class="badge <?= $badgeClass ?>"><?= esc($statusTag) ?></span>
|
||||
</div>
|
||||
<div class="small text-muted"><?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?></div>
|
||||
<div class="small">
|
||||
<?php if ($report): ?>
|
||||
<?= view('admin/partials/class_progress_unit_display', [
|
||||
'unitTitle' => (string) ($report['unit_title'] ?? ''),
|
||||
'isQuran' => ($subjectName === 'Quran/Arabic'),
|
||||
'compact' => true,
|
||||
]) ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">No submission</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><?= esc($teacherName ?: '-') ?></td>
|
||||
<td><?= esc($teacherLabel) ?></td>
|
||||
<td class="text-end">
|
||||
<?php if ($exampleId): ?>
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('admin/progress/view/' . $exampleId) ?>">View</a>
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
<?php
|
||||
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
||||
$isQuran = $subjectName === 'Quran/Arabic';
|
||||
$unitLabel = $isQuran ? 'Surah / Custom Arabic' : 'Unit / Chapter';
|
||||
$homeworkLabel = $isQuran ? 'Arabic Practice / Homework' : 'Assigned Homework';
|
||||
$report = $reportsBySubject[$subjectName] ?? null;
|
||||
?>
|
||||
@@ -51,7 +50,13 @@
|
||||
<?php if (! $report): ?>
|
||||
<div class="text-muted">No entry submitted for this subject this week.</div>
|
||||
<?php else: ?>
|
||||
<div class="mb-2"><strong><?= $unitLabel ?>:</strong> <?= esc($report['unit_title'] ?: '-') ?></div>
|
||||
<div class="mb-2">
|
||||
<?= view('admin/partials/class_progress_unit_display', [
|
||||
'unitTitle' => (string) ($report['unit_title'] ?? ''),
|
||||
'isQuran' => $isQuran,
|
||||
'compact' => false,
|
||||
]) ?>
|
||||
</div>
|
||||
<?php if (!empty($report['materials'])): ?>
|
||||
<div class="mb-2"><strong>Materials:</strong> <?= esc($report['materials']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Renders class progress unit_title with explicit curriculum vs custom subject lines.
|
||||
*
|
||||
* @var string $unitTitle
|
||||
* @var bool $isQuran
|
||||
* @var bool $compact If true, tighter layout for the list accordion.
|
||||
*/
|
||||
|
||||
use App\Controllers\ClassProgressController;
|
||||
|
||||
$unitTitle = trim((string) ($unitTitle ?? ''));
|
||||
$compact = ! empty($compact);
|
||||
$isQuran = ! empty($isQuran);
|
||||
|
||||
$split = ClassProgressController::splitUnitTitleForDisplay($unitTitle);
|
||||
$curriculumLines = $split['curriculum'];
|
||||
$customTopics = $split['custom'];
|
||||
|
||||
$labelCurriculum = $isQuran ? 'Surah / curriculum' : 'Unit / chapter';
|
||||
$labelCustom = $isQuran ? 'Custom Surah / Arabic' : 'Custom subject(s)';
|
||||
?>
|
||||
|
||||
<?php if ($unitTitle === ''): ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php elseif ($compact): ?>
|
||||
<?php if (! empty($customTopics)): ?>
|
||||
<div class="small">
|
||||
<span class="badge text-bg-info me-1"><?= $isQuran ? 'Custom' : 'Custom subject' ?></span><?= esc(implode(', ', $customTopics)) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($curriculumLines)): ?>
|
||||
<div class="small <?= ! empty($customTopics) ? 'text-muted mt-1' : 'text-muted' ?>"><?= esc(implode(' · ', $curriculumLines)) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($customTopics) && empty($curriculumLines)): ?>
|
||||
<div class="small text-muted"><?= esc($unitTitle) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<?php if (! empty($curriculumLines)): ?>
|
||||
<div class="mb-2"><strong><?= esc($labelCurriculum) ?>:</strong> <?= esc(implode(' ; ', $curriculumLines)) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($customTopics)): ?>
|
||||
<div class="mb-2"><strong><?= esc($labelCustom) ?>:</strong> <?= esc(implode(' ; ', $customTopics)) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($curriculumLines) && empty($customTopics)): ?>
|
||||
<div class="mb-2"><strong><?= esc($labelCurriculum) ?>:</strong> <?= esc($unitTitle) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
@@ -4,6 +4,11 @@
|
||||
<div class="container mt-4">
|
||||
<h2>Create Event</h2>
|
||||
|
||||
<?php
|
||||
$defaultCategories = ['Fun Event-1', 'Fun Event-2', 'Fun Event-3'];
|
||||
$existingCategories = array_map('strval', $categories ?? []);
|
||||
$allCategories = array_unique(array_merge($defaultCategories, $existingCategories));
|
||||
?>
|
||||
<form method="post" action="<?= site_url('administrator/events/create') ?>" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<div class="mb-3">
|
||||
@@ -15,7 +20,7 @@
|
||||
<label class="form-label">Category</label>
|
||||
<select name="event_category" class="form-control" required>
|
||||
<option value="" selected disabled>Select category</option>
|
||||
<?php foreach (($categories ?? []) as $category): ?>
|
||||
<?php foreach ($allCategories as $category): ?>
|
||||
<option value="<?= esc($category) ?>"><?= esc(ucwords($category)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
@@ -51,9 +56,69 @@
|
||||
<input type="text" name="school_year" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">School Calendar</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="add_to_calendar" name="add_to_calendar" value="1">
|
||||
<label class="form-check-label" for="add_to_calendar">
|
||||
Add this event to the school calendar
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="calendar_recipients" class="ms-3" style="display:none;">
|
||||
<div class="mb-2 fw-semibold">Visible On Calendars</div>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input me-1" type="checkbox" name="notify_parent" value="1" checked>
|
||||
Parents
|
||||
</label>
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input me-1" type="checkbox" name="notify_teacher" value="1" checked>
|
||||
Teachers
|
||||
</label>
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input me-1" type="checkbox" name="notify_admin" value="1" checked>
|
||||
Admins
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">If none are selected, the event is visible to everyone.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Parent Email Broadcast</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="send_email_parent" name="send_email_parent" value="1">
|
||||
<label class="form-check-label" for="send_email_parent">
|
||||
Send one event email to all parents and add all parent emails in CC
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">The email is sent once. The school sender address stays in To, and all parent emails are placed in CC.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success">Create Event</button>
|
||||
<a href="<?= site_url('administrator/events') ?>" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function () {
|
||||
const toggle = document.getElementById('add_to_calendar');
|
||||
const recipients = document.getElementById('calendar_recipients');
|
||||
if (!toggle || !recipients) return;
|
||||
|
||||
function sync() {
|
||||
recipients.style.display = toggle.checked ? '' : 'none';
|
||||
}
|
||||
toggle.addEventListener('change', sync);
|
||||
sync();
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -70,6 +70,19 @@
|
||||
<input type="text" name="school_year" class="form-control" value="<?= esc($event['school_year']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Parent Email Broadcast</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="send_email_parent" name="send_email_parent" value="1">
|
||||
<label class="form-check-label" for="send_email_parent">
|
||||
Send one event email to all parents and add all parent emails in CC
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Use this when you want to rebroadcast the updated event to all parents in a single email.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Update Event</button>
|
||||
<a href="<?= site_url('administrator/events') ?>" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
|
||||
@@ -11,104 +11,343 @@
|
||||
<div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Add New Charge Form -->
|
||||
<form action="<?= site_url('payment/event_charges') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="row">
|
||||
<div class="col-md-3 mt-3">
|
||||
<label for="event_id" class="form-label">Select Event</label>
|
||||
<select name="event_id" id="event_id" class="form-select" required>
|
||||
<option value="">-- Select Event --</option>
|
||||
<?php foreach ($events as $event): ?>
|
||||
<option value="<?= esc($event['id']) ?>">
|
||||
<?= esc($event['event_name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php
|
||||
$classSectionNames = $classSectionNames ?? [];
|
||||
$semesterOptions = $semesterOptions ?? [];
|
||||
$schoolYearOptions = $schoolYearOptions ?? [];
|
||||
?>
|
||||
|
||||
<div class="col-md-3 mt-3">
|
||||
<label for="parent_id" class="form-label">Parent</label>
|
||||
<select id="parent_id" name="parent_id" class="form-select" required>
|
||||
<option value="">-- Select Parent --</option>
|
||||
<?php foreach ($parents as $parent): ?>
|
||||
<option value="<?= $parent['id'] ?>">
|
||||
<?= esc($parent['firstname'] . ' ' . $parent['lastname']) ?> (<?= esc($parent['school_id']) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-3" id="studentCheckboxContainer" style="display: none;">
|
||||
<label>Select Students:</label>
|
||||
<div id="studentList" class="row"></div>
|
||||
</div>
|
||||
<?php
|
||||
$filterParentId = $filterParentId ?? 0;
|
||||
$parentBalances = $parentBalances ?? [];
|
||||
$selectedEvent = null;
|
||||
if (!empty($filterEventId)) {
|
||||
foreach ($events as $event) {
|
||||
if ((int)$event['id'] === (int)$filterEventId) {
|
||||
$selectedEvent = $event;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form method="get" action="<?= site_url('administrator/event-charges') ?>" class="row g-3 align-items-end">
|
||||
<?php if ($filterEventId > 0): ?>
|
||||
<input type="hidden" name="event_id" value="<?= esc($filterEventId) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if ($filterParentId > 0): ?>
|
||||
<input type="hidden" name="parent_id" value="<?= esc($filterParentId) ?>">
|
||||
<?php endif; ?>
|
||||
<div class="col-md-4">
|
||||
<label for="semester_filter" class="form-label">Semester</label>
|
||||
<select id="semester_filter" name="semester" class="form-select">
|
||||
<option value="">-- All semesters --</option>
|
||||
<?php foreach ($semesterOptions as $option): ?>
|
||||
<option value="<?= esc($option) ?>" <?= $semester === $option ? 'selected' : '' ?>>
|
||||
<?= esc($option) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="school_year_filter" class="form-label">School Year</label>
|
||||
<select id="school_year_filter" name="school_year" class="form-select">
|
||||
<option value="">-- All school years --</option>
|
||||
<?php foreach ($schoolYearOptions as $option): ?>
|
||||
<option value="<?= esc($option) ?>" <?= $school_year === $option ? 'selected' : '' ?>>
|
||||
<?= esc($option) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-outline-primary">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-3">Submit</button>
|
||||
<a href="<?= site_url('administrator/events') ?>" class="btn btn-success mt-3">Event List</a>
|
||||
</form>
|
||||
<br>
|
||||
<!-- Add New Charge Form -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form id="event-participant-form" action="<?= site_url('payment/event_charges') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="school_year" value="<?= esc($school_year) ?>">
|
||||
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label for="event_id" class="form-label">Select Event</label>
|
||||
<select name="event_id" id="event_id" class="form-select" required>
|
||||
<option value="">-- All events --</option>
|
||||
<?php foreach ($events as $event): ?>
|
||||
<option value="<?= esc($event['id']) ?>" data-amount="<?= esc(number_format($event['amount'] ?? 0, 2, '.', '')) ?>" <?= isset($filterEventId) && $filterEventId == $event['id'] ? 'selected' : '' ?>>
|
||||
<?= esc($event['event_name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedEvent): ?>
|
||||
<div class="col-md-8">
|
||||
<div class="border rounded bg-light p-3 h-100">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="m-0"><?= esc($selectedEvent['event_name']) ?></h6>
|
||||
<span class="badge bg-info text-dark">
|
||||
$<?= esc(number_format($selectedEvent['amount'] ?? 0, 2)) ?> fee
|
||||
</span>
|
||||
</div>
|
||||
<p class="mb-1 text-muted small">
|
||||
<?= esc($selectedEvent['description'] ?: 'No description provided for this event.') ?>
|
||||
</p>
|
||||
<?php if (!empty($selectedEvent['expiration_date'])): ?>
|
||||
<small class="text-secondary">
|
||||
Expires: <?= esc(local_date($selectedEvent['expiration_date'], 'm-d-Y')) ?>
|
||||
</small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="parent_id" class="form-label">Parents List</label>
|
||||
<select id="parent_id" name="parent_id" class="form-select">
|
||||
<option value="">-- Select Parent --</option>
|
||||
<?php foreach ($parents as $parent): ?>
|
||||
<option value="<?= esc($parent['id']) ?>" <?= $filterParentId && $filterParentId == $parent['id'] ? 'selected' : '' ?>>
|
||||
<?= esc($parent['firstname'] . ' ' . $parent['lastname']) ?> (<?= esc($parent['school_id']) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" id="studentCheckboxContainer" style="display: none;">
|
||||
<label class="form-label">Select Students:</label>
|
||||
<div id="studentList" class="row g-3"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#externalParticipantModal">
|
||||
Add non-school participant
|
||||
</button>
|
||||
<div class="form-text">After adding participants, click Submit to save them to the database.</div>
|
||||
<div id="externalParticipantsHidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="<?= site_url('administrator/events') ?>" class="btn btn-success">Event List</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Charge Tables grouped by event -->
|
||||
<?php
|
||||
$grouped = [];
|
||||
foreach ($charges as $charge) {
|
||||
$label = $charge['event_name'] ?? 'N/A';
|
||||
$grouped[$label][] = $charge;
|
||||
$eventId = (int)($charge['event_id'] ?? 0);
|
||||
if (!isset($grouped[$eventId])) {
|
||||
$grouped[$eventId] = [
|
||||
'label' => $charge['event_name'] ?? 'N/A',
|
||||
'rows' => [],
|
||||
];
|
||||
}
|
||||
$grouped[$eventId]['rows'][] = $charge;
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if (empty($grouped)): ?>
|
||||
<div class="alert alert-info">No charges found.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($grouped as $eventLabel => $rows): ?>
|
||||
<div class="card mb-4">
|
||||
<?php foreach ($grouped as $eventId => $data): ?>
|
||||
<?php $eventLabel = $data['label']; ?>
|
||||
<?php $rows = $data['rows']; ?>
|
||||
<div class="card mb-4 event-card" data-event-id="<?= esc($eventId) ?>">
|
||||
<div class="card-header bg-light fw-semibold">
|
||||
<?= esc($eventLabel) ?>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<?php
|
||||
$totalParticipants = 0;
|
||||
$totalCharged = 0.0;
|
||||
?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped mb-0 no-mgmt-sticky">
|
||||
<table id="eventTable_<?= esc($eventId) ?>" class="table table-bordered table-striped mb-0 no-mgmt-sticky">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Parent Name</th>
|
||||
<th>Student Name</th>
|
||||
<th>Charged Amount</th>
|
||||
<th>Is Participating</th>
|
||||
<th>Semester</th>
|
||||
<th>Year</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Parent Name</th>
|
||||
<th>Student Name</th>
|
||||
<th>External Info</th>
|
||||
<th>Class Section</th>
|
||||
<th>Charged Amount</th>
|
||||
<th>Created</th>
|
||||
<th>Fees Paid</th>
|
||||
<th>Payment</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $charge): ?>
|
||||
<?php
|
||||
$isParticipating = ($charge['participation'] === 'yes');
|
||||
$isEventPaid = !empty($charge['event_paid']);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($charge['id']) ?></td>
|
||||
<td><?= esc($charge['parent_firstname'] . ' ' . $charge['parent_lastname']) ?></td>
|
||||
<?php
|
||||
$studentName = $charge['student_firstname']
|
||||
? trim($charge['student_firstname'] . ' ' . $charge['student_lastname'])
|
||||
: '';
|
||||
$externalName = trim(
|
||||
($charge['external_firstname'] ?? '') . ' ' .
|
||||
($charge['external_lastname'] ?? '')
|
||||
);
|
||||
$displayName = $studentName ?: ($externalName ?: '—');
|
||||
$externalNote = $charge['external_note'] ?? '';
|
||||
$externalParentLabel = trim(
|
||||
($charge['external_parent_firstname'] ?? '') . ' ' .
|
||||
($charge['external_parent_lastname'] ?? '')
|
||||
);
|
||||
$externalParentPhone = $charge['external_parent_phone'] ?? '';
|
||||
$standardParentName = trim($charge['parent_firstname'] . ' ' . $charge['parent_lastname']);
|
||||
$parentColumnName = $externalParentLabel ?: ($standardParentName ?: '—');
|
||||
?>
|
||||
<td><?= esc($parentColumnName) ?></td>
|
||||
<td>
|
||||
<?= $charge['student_firstname']
|
||||
? esc($charge['student_firstname'] . ' ' . $charge['student_lastname'])
|
||||
: '-' ?>
|
||||
<?= esc($displayName) ?>
|
||||
<?php if ($externalNote): ?>
|
||||
<small class="text-muted d-block"><?= esc($externalNote) ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>$<?= esc(number_format($charge['charged'] ?? 0, 2)) ?></td>
|
||||
<td>
|
||||
<?= $charge['participation']
|
||||
? '<span class="badge bg-success">Yes</span>'
|
||||
: '<span class="badge bg-warning">No</span>' ?>
|
||||
<?php if ($externalParentPhone): ?>
|
||||
<div class="text-muted small"><?= esc($externalParentPhone) ?></div>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= esc($charge['semester'] ?? '-') ?></td>
|
||||
<td><?= esc($charge['school_year'] ?? '-') ?></td>
|
||||
<td>
|
||||
<?= esc($classSectionNames[$charge['class_section_id'] ?? ''] ?? '—') ?>
|
||||
</td>
|
||||
<td>$<?= esc(number_format($charge['event_amount'] ?? 0, 2)) ?></td>
|
||||
<td><?= esc(!empty($charge['created_at']) ? local_datetime($charge['created_at'], 'm-d-Y H:i') : '') ?></td>
|
||||
<?php
|
||||
$feeAmount = (float) ($charge['event_amount'] ?? 0);
|
||||
$hasBalanceRecord = array_key_exists((int)$charge['parent_id'], $parentBalances);
|
||||
$parentBalance = $hasBalanceRecord ? (float)$parentBalances[$charge['parent_id']] : null;
|
||||
$feeIsPaid = $isParticipating && ($isEventPaid || ($hasBalanceRecord && $parentBalance <= 0));
|
||||
?>
|
||||
<td class="text-center">
|
||||
<?php if ($feeIsPaid): ?>
|
||||
<span class="badge bg-success">Paid</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-danger">Unpaid</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<form method="post" action="<?= site_url('administrator/event-charges/payment/' . esc($charge['id'])) ?>" class="d-inline-flex align-items-center">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="paid" value="<?= $feeIsPaid ? '1' : '0' ?>">
|
||||
<input type="checkbox" class="form-check-input" id="eventPayment_<?= esc($charge['id']) ?>"
|
||||
<?= $isEventPaid ? 'checked' : '' ?>
|
||||
onchange="this.form.paid.value = this.checked ? 1 : 0; this.form.submit();">
|
||||
<label class="form-check-label ms-2" for="eventPayment_<?= esc($charge['id']) ?>" aria-hidden="true">Paid</label>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
<div class="d-flex gap-1 align-items-center">
|
||||
<a href="<?= site_url('administrator/event-charges') ?>?event_id=<?= esc($charge['event_id']) ?>&parent_id=<?= esc($charge['parent_id']) ?>#event-participant-form" class="btn btn-outline-primary btn-sm">
|
||||
Edit
|
||||
</a>
|
||||
<form action="<?= site_url('administrator/event-charges/remove/' . esc($charge['id'])) ?>" method="post" onsubmit="return confirm('Remove this participation and update the charge?');">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
if ($isParticipating) {
|
||||
$totalParticipants++;
|
||||
$totalCharged += (float) ($charge['event_amount'] ?? 0);
|
||||
}
|
||||
?>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<th colspan="4">Totals</th>
|
||||
<th>$<?= esc(number_format($totalCharged, 2)) ?></th>
|
||||
<th colspan="5">
|
||||
<span class="badge bg-secondary">
|
||||
<?= esc($totalParticipants) ?> participating
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="externalParticipantModal" tabindex="-1" aria-labelledby="externalParticipantModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form id="externalParticipantForm" class="needs-validation" novalidate>
|
||||
<?= csrf_field() ?>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="externalParticipantModalLabel">Add Non-School Participant</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="small text-muted">Provide the student’s name and the accompanying guardian/contact details.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Student / Kid Info</label>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" id="modalExternalFirstName" placeholder="First name" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" id="modalExternalLastName" placeholder="Last name" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Parent / Contact Info</label>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" id="modalParentFirstName" placeholder="Parent first name" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" id="modalParentLastName" placeholder="Parent last name" required>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input type="text" class="form-control" id="modalParentPhone" placeholder="Parent phone or contact">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input type="email" class="form-control" id="modalParentEmail" placeholder="Parent email" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="modalExternalNote">Note <span class="text-muted small">(optional)</span></label>
|
||||
<textarea rows="2" class="form-control" id="modalExternalNote" placeholder="Additional info (e.g., guest, sibling, etc.)"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="externalParticipantSave">
|
||||
Add to list
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -156,7 +395,307 @@ function loadStudentsWithCharges() {
|
||||
}
|
||||
}
|
||||
|
||||
$('#parent_id').on('change', loadStudentsWithCharges);
|
||||
$('#event_id').on('change', loadStudentsWithCharges);
|
||||
const capitalizeName = (value) => {
|
||||
if (!value) return '';
|
||||
return value
|
||||
.split(' ')
|
||||
.map(part => {
|
||||
const trimmed = part.trim();
|
||||
if (!trimmed) return '';
|
||||
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1).toLowerCase();
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
$(function() {
|
||||
$('#parent_id').on('change', loadStudentsWithCharges);
|
||||
|
||||
$('#event_id').on('change', function() {
|
||||
let eventId = $(this).val();
|
||||
let url = new URL(window.location.href);
|
||||
if (eventId) {
|
||||
url.searchParams.set('event_id', eventId);
|
||||
} else {
|
||||
url.searchParams.delete('event_id');
|
||||
}
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
|
||||
if ($('#event_id').val() && $('#parent_id').val()) {
|
||||
loadStudentsWithCharges();
|
||||
}
|
||||
|
||||
const $externalHidden = $('#externalParticipantsHidden');
|
||||
const storageKey = 'eventChargesExternalParticipants';
|
||||
let externalDrafts = [];
|
||||
let editingKey = null;
|
||||
|
||||
const escapeHtml = (value) => {
|
||||
const s = String(value ?? '');
|
||||
return s.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
|
||||
function buildExternalRow(entry) {
|
||||
const key = entry.key;
|
||||
const isPaid = !!entry.paid;
|
||||
const firstEsc = escapeHtml(entry.firstname);
|
||||
const lastEsc = escapeHtml(entry.lastname);
|
||||
const noteEsc = escapeHtml(entry.note);
|
||||
const parentFirstEsc = escapeHtml(entry.parentFirstname);
|
||||
const parentLastEsc = escapeHtml(entry.parentLastname);
|
||||
const parentPhoneEsc = escapeHtml(entry.parentPhone);
|
||||
const parentEmailEsc = escapeHtml(entry.parentEmail);
|
||||
const label = (firstEsc || lastEsc)
|
||||
? `${firstEsc || ''} ${lastEsc || ''}`.trim()
|
||||
: 'Unnamed participant';
|
||||
const parentLabel = (parentFirstEsc || parentLastEsc)
|
||||
? `<div class="text-muted small">Parent: ${parentFirstEsc} ${parentLastEsc}</div>`
|
||||
: '';
|
||||
const phoneLabel = parentPhoneEsc
|
||||
? `<div class="text-muted small">${parentPhoneEsc}</div>`
|
||||
: '';
|
||||
const parentNameForRow = entry.parentDisplayName || '—';
|
||||
const amountDisplay = ((entry.eventAmount ?? 0) || 0.0).toFixed(2);
|
||||
|
||||
const $wrapper = $(`
|
||||
<div data-key="${key}">
|
||||
<input type="hidden" name="external_participants[${key}][firstname]" value="${firstEsc}">
|
||||
<input type="hidden" name="external_participants[${key}][lastname]" value="${lastEsc}">
|
||||
<input type="hidden" name="external_participants[${key}][note]" value="${noteEsc}">
|
||||
<input type="hidden" name="external_participants[${key}][parent_firstname]" value="${parentFirstEsc}">
|
||||
<input type="hidden" name="external_participants[${key}][parent_lastname]" value="${parentLastEsc}">
|
||||
<input type="hidden" name="external_participants[${key}][parent_phone]" value="${parentPhoneEsc}">
|
||||
<input type="hidden" name="external_participants[${key}][parent_email]" value="${parentEmailEsc}">
|
||||
<input type="hidden" name="external_participants[${key}][paid]" value="${isPaid ? '1' : '0'}">
|
||||
</div>
|
||||
`);
|
||||
$externalHidden.append($wrapper);
|
||||
|
||||
const $tableBody = $(`#eventTable_${entry.eventId} tbody`);
|
||||
if (!$tableBody.length) {
|
||||
return;
|
||||
}
|
||||
const createdDisplay = new Date().toLocaleString();
|
||||
const $previewRow = $(`
|
||||
<tr class="external-preview table-warning" data-key="${key}" data-event-id="${entry.eventId}">
|
||||
<td>—</td>
|
||||
<td>${escapeHtml(parentNameForRow)}</td>
|
||||
<td>
|
||||
<strong>${label}</strong>
|
||||
${noteEsc ? `<div class="text-muted small">${noteEsc}</div>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
${phoneLabel}
|
||||
${phoneLabel ? '' : '<span class="text-muted small">—</span>'}
|
||||
</td>
|
||||
<td>External</td>
|
||||
<td>$${amountDisplay}</td>
|
||||
<td>${createdDisplay}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-warning text-dark">Pending</span>
|
||||
${isPaid ? '<span class="badge bg-success ms-1 external-paid-badge">Paid on save</span>' : '<span class="badge bg-secondary ms-1 external-paid-badge d-none">Paid on save</span>'}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="form-check d-inline-flex align-items-center gap-2 justify-content-center m-0">
|
||||
<input class="form-check-input external-paid-toggle" type="checkbox" data-key="${key}" ${isPaid ? 'checked' : ''}>
|
||||
<span class="small text-muted">On save</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm edit-external-participant" data-key="${key}">
|
||||
Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm remove-external-participant" data-key="${key}">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
$tableBody.append($previewRow);
|
||||
}
|
||||
|
||||
function persistDrafts() {
|
||||
localStorage.setItem(storageKey, JSON.stringify(externalDrafts));
|
||||
}
|
||||
|
||||
function saveExternalDraft(entry) {
|
||||
entry.key = entry.key ?? `${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
entry.paid = !!entry.paid;
|
||||
removeExternalEntryFromDom(entry.key);
|
||||
externalDrafts = externalDrafts.filter((existing) => existing.key !== entry.key);
|
||||
externalDrafts.push(entry);
|
||||
persistDrafts();
|
||||
buildExternalRow(entry);
|
||||
}
|
||||
|
||||
function removeExternalEntryFromDom(key) {
|
||||
$(`tr.external-preview[data-key="${key}"]`).remove();
|
||||
$externalHidden.find(`div[data-key="${key}"]`).remove();
|
||||
}
|
||||
|
||||
function removeDraft(key) {
|
||||
externalDrafts = externalDrafts.filter((entry) => entry.key !== key);
|
||||
persistDrafts();
|
||||
}
|
||||
|
||||
function loadDrafts() {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const saved = JSON.parse(raw);
|
||||
if (!Array.isArray(saved)) {
|
||||
return;
|
||||
}
|
||||
externalDrafts = saved;
|
||||
const currentEvent = $('#event_id').val();
|
||||
externalDrafts.forEach((entry) => {
|
||||
if (String(entry.eventId) === String(currentEvent)) {
|
||||
buildExternalRow(entry);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load external drafts', e);
|
||||
}
|
||||
}
|
||||
|
||||
function getDraftByKey(key) {
|
||||
return externalDrafts.find((entry) => entry.key === key);
|
||||
}
|
||||
|
||||
function openExternalModalForEntry(key) {
|
||||
const entry = getDraftByKey(key);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
editingKey = key;
|
||||
$('#modalExternalFirstName').val(entry.firstname);
|
||||
$('#modalExternalLastName').val(entry.lastname);
|
||||
$('#modalExternalNote').val(entry.note);
|
||||
$('#modalParentFirstName').val(entry.parentFirstname);
|
||||
$('#modalParentLastName').val(entry.parentLastname);
|
||||
$('#modalParentPhone').val(entry.parentPhone);
|
||||
$('#modalParentEmail').val(entry.parentEmail);
|
||||
if (modalInstance) {
|
||||
modalInstance.show();
|
||||
}
|
||||
}
|
||||
|
||||
const modalElement = document.getElementById('externalParticipantModal');
|
||||
const modalInstance = modalElement ? new bootstrap.Modal(modalElement) : null;
|
||||
if (modalElement) {
|
||||
modalElement.addEventListener('hidden.bs.modal', () => {
|
||||
editingKey = null;
|
||||
});
|
||||
}
|
||||
function clearModalInputs() {
|
||||
$('#modalExternalFirstName').val('');
|
||||
$('#modalExternalLastName').val('');
|
||||
$('#modalExternalNote').val('');
|
||||
$('#modalParentFirstName').val('');
|
||||
$('#modalParentLastName').val('');
|
||||
$('#modalParentPhone').val('');
|
||||
$('#modalParentEmail').val('');
|
||||
}
|
||||
|
||||
$('#externalParticipantSave').on('click', function() {
|
||||
const firstName = capitalizeName($('#modalExternalFirstName').val().trim());
|
||||
const lastName = capitalizeName($('#modalExternalLastName').val().trim());
|
||||
const note = $('#modalExternalNote').val().trim();
|
||||
if (!firstName && !lastName) {
|
||||
alert('Please provide at least a first or last name for the non-school participant.');
|
||||
return;
|
||||
}
|
||||
const parentFirst = capitalizeName($('#modalParentFirstName').val().trim());
|
||||
const parentLast = capitalizeName($('#modalParentLastName').val().trim());
|
||||
const parentPhone = $('#modalParentPhone').val().trim();
|
||||
const parentEmail = $('#modalParentEmail').val().trim();
|
||||
const eventId = $('#event_id').val();
|
||||
if (!eventId) {
|
||||
alert('Select an event before adding a non-school participant.');
|
||||
return;
|
||||
}
|
||||
const eventAmount = parseFloat($('#event_id option:selected').data('amount')) || 0;
|
||||
const explicitParentName = `${parentFirst} ${parentLast}`.trim();
|
||||
if (!explicitParentName) {
|
||||
alert('Please enter the external kid parent name.');
|
||||
return;
|
||||
}
|
||||
if (!parentEmail) {
|
||||
alert('Please enter the external kid parent email.');
|
||||
return;
|
||||
}
|
||||
const parentName = explicitParentName;
|
||||
const draftEntry = editingKey ? getDraftByKey(editingKey) : null;
|
||||
const entry = {
|
||||
firstname: firstName,
|
||||
lastname: lastName,
|
||||
note: note,
|
||||
parentFirstname: parentFirst,
|
||||
parentLastname: parentLast,
|
||||
parentPhone: parentPhone,
|
||||
parentEmail: parentEmail,
|
||||
eventId: (draftEntry && draftEntry.eventId) ? draftEntry.eventId : eventId,
|
||||
eventAmount: (draftEntry && draftEntry.eventAmount) ? draftEntry.eventAmount : eventAmount,
|
||||
parentDisplayName: (draftEntry && draftEntry.parentDisplayName) ? draftEntry.parentDisplayName : parentName,
|
||||
paid: (draftEntry && typeof draftEntry.paid !== 'undefined') ? !!draftEntry.paid : false,
|
||||
};
|
||||
if (editingKey) {
|
||||
entry.key = editingKey;
|
||||
}
|
||||
saveExternalDraft(entry);
|
||||
editingKey = null;
|
||||
clearModalInputs();
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove-external-participant', function() {
|
||||
const $entry = $(this).closest('[data-key]');
|
||||
const key = $entry.data('key');
|
||||
removeExternalEntryFromDom(key);
|
||||
if (key) {
|
||||
removeDraft(key);
|
||||
}
|
||||
});
|
||||
$(document).on('click', '.edit-external-participant', function() {
|
||||
const key = $(this).data('key');
|
||||
if (key) {
|
||||
openExternalModalForEntry(key);
|
||||
}
|
||||
});
|
||||
$(document).on('change', '.external-paid-toggle', function() {
|
||||
const key = $(this).data('key');
|
||||
const paid = !!this.checked;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const entry = getDraftByKey(key);
|
||||
if (entry) {
|
||||
entry.paid = paid;
|
||||
persistDrafts();
|
||||
}
|
||||
$externalHidden
|
||||
.find(`div[data-key="${key}"] input[name="external_participants[${key}][paid]"]`)
|
||||
.val(paid ? '1' : '0');
|
||||
const $badge = $(`tr.external-preview[data-key="${key}"] .external-paid-badge`);
|
||||
if (paid) {
|
||||
$badge.removeClass('d-none').addClass('bg-success').text('Paid on save');
|
||||
} else {
|
||||
$badge.addClass('d-none');
|
||||
}
|
||||
});
|
||||
$('#event-participant-form').on('submit', function() {
|
||||
localStorage.removeItem(storageKey);
|
||||
});
|
||||
loadDrafts();
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
<div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (session()->getFlashdata('email_status')): ?>
|
||||
<div class="alert alert-info"><?= session()->getFlashdata('email_status') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (session()->getFlashdata('email_error')): ?>
|
||||
<div class="alert alert-warning"><?= session()->getFlashdata('email_error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($events)): ?>
|
||||
<div class="alert alert-info mb-3 d-inline-block">No events found.</div>
|
||||
<?php else: ?>
|
||||
@@ -21,7 +29,8 @@
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Category</th>
|
||||
<th>Amount</th>
|
||||
<th>Description</th>
|
||||
<th>Event Fees</th>
|
||||
<th>Expiration Date</th>
|
||||
<th>Semester</th>
|
||||
<th>School Year</th>
|
||||
@@ -33,7 +42,8 @@
|
||||
<tr>
|
||||
<td><?= esc($event['event_name']) ?></td>
|
||||
<td><?= esc($event['event_category'] ?? '—') ?></td>
|
||||
<td><?= esc($event['amount']) ?></td>
|
||||
<td><?= esc(!empty($event['description']) ? $event['description'] : '—') ?></td>
|
||||
<td>$<?= esc(number_format($event['amount'], 2)) ?></td>
|
||||
<td><?= esc(!empty($event['expiration_date']) ? local_date($event['expiration_date'], 'm-d-Y') : '') ?></td>
|
||||
<td><?= esc($event['semester']) ?></td>
|
||||
<td><?= esc($event['school_year']) ?></td>
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
$statusBadges = $statusBadges ?? [];
|
||||
$drafts = $drafts ?? [];
|
||||
$legacyByClass = $legacyByClass ?? [];
|
||||
$classSections = $classSections ?? [];
|
||||
$examTypes = $examTypes ?? [];
|
||||
$visibleClasses = $visibleClasses ?? [];
|
||||
$classDraftGroups = $classDraftGroups ?? [];
|
||||
$newSubmissionClasses = $newSubmissionClasses ?? [];
|
||||
$schoolYear = $schoolYear ?? '';
|
||||
$semester = $semester ?? '';
|
||||
$maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
$allowedExtensions = $allowedExtensions ?? ['doc', 'docx', 'pdf'];
|
||||
|
||||
$renderBadge = static function (string $status, array $badges): string {
|
||||
$b = $badges[$status] ?? ['label' => $status, 'class' => 'bg-secondary text-white'];
|
||||
$style = !empty($b['style']) ? ' style="' . esc($b['style']) . '"' : '';
|
||||
return '<span class="badge ' . esc($b['class']) . '"' . $style . '>' . esc($b['label']) . '</span>';
|
||||
};
|
||||
|
||||
$fileAccept = implode(',', array_map(static fn ($x) => '.' . $x, $allowedExtensions));
|
||||
?>
|
||||
<div class="container-fluid px-4 py-4">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Exam Draft Submissions</h1>
|
||||
<h1 class="h3 mb-1">Exam draft submissions</h1>
|
||||
<p class="text-muted mb-0 small">
|
||||
<?= esc($semester ?: 'Semester') ?> <?= esc($schoolYear ?: '') ?>
|
||||
</p>
|
||||
@@ -15,13 +37,9 @@
|
||||
</div>
|
||||
|
||||
<?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 elseif (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; ?>
|
||||
|
||||
<ul class="nav nav-pills mb-3" id="examDraftTabs" role="tablist">
|
||||
@@ -32,177 +50,253 @@
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="legacy-tab" data-bs-toggle="pill" data-bs-target="#legacy" type="button" role="tab" aria-controls="legacy" aria-selected="false">
|
||||
Legacy Exams
|
||||
Legacy exams
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="examDraftTabsContent">
|
||||
<div class="tab-pane fade show active" id="submissions" role="tabpanel" aria-labelledby="submissions-tab">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<?php if (empty($drafts)): ?>
|
||||
<p class="text-muted mb-0">No exam drafts have been submitted yet.</p>
|
||||
<?php if (empty($visibleClasses)): ?>
|
||||
<p class="text-muted mb-0">No classes with enrolled students are available for this term.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered align-middle mb-0 exam-drafts-table no-mgmt-sticky">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Teacher</th>
|
||||
<th>Class</th>
|
||||
<th>Title / Type</th>
|
||||
<th>Version</th>
|
||||
<th>Submitted</th>
|
||||
<th>Status</th>
|
||||
<th>Files</th>
|
||||
<th>PDF</th>
|
||||
<th>Review</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($drafts as $draft): ?>
|
||||
<?php
|
||||
$teacherName = trim(($draft['teacher_first'] ?? '') . ' ' . ($draft['teacher_last'] ?? ''));
|
||||
if ($teacherName === '') {
|
||||
$teacherName = ($draft['admin_id'] ?? null) === ($draft['teacher_id'] ?? null)
|
||||
? 'Admin Upload'
|
||||
: 'User #' . ($draft['teacher_id'] ?? 'N/A');
|
||||
}
|
||||
$statusInfo = $statusBadges[$draft['status'] ?? 'pending'] ?? ['label' => 'Unknown', 'class' => 'bg-secondary text-white'];
|
||||
$adminName = trim(($draft['admin_first'] ?? '') . ' ' . ($draft['admin_last'] ?? ''));
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($teacherName) ?></td>
|
||||
<td><?= esc($draft['class_section_name'] ?? 'Unknown') ?></td>
|
||||
<td>
|
||||
<strong><?= esc($draft['draft_title'] ?? 'Untitled') ?></strong>
|
||||
<div class="small text-muted"><?= esc($draft['exam_type'] ?? 'N/A') ?></div>
|
||||
<?php if (!empty($draft['description'])): ?>
|
||||
<div class="small text-muted"><?= esc($draft['description']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-nowrap">v<?= esc($draft['version'] ?? 1) ?></td>
|
||||
<td class="text-nowrap"><?= esc((!empty($draft['created_at']) ? local_datetime($draft['created_at'], 'M j, Y g:i A') : '—')) ?></td>
|
||||
<td>
|
||||
<span class="badge <?= esc($statusInfo['class']) ?>">
|
||||
<?= esc($statusInfo['label']) ?>
|
||||
</span>
|
||||
<?php if (!empty($draft['reviewed_at'])): ?>
|
||||
<div class="small text-muted mt-1">
|
||||
Reviewed <?= esc(local_datetime($draft['reviewed_at'], 'M j, Y g:i A')) ?>
|
||||
<?php if ($adminName !== ''): ?>
|
||||
by <?= esc($adminName) ?>
|
||||
<div class="accordion exam-drafts-accordion" id="examDraftsAccordion">
|
||||
<?php foreach ($visibleClasses as $classSectionId => $classInfo): ?>
|
||||
<?php $classDraftsForSection = $classDraftGroups[$classSectionId] ?? []; ?>
|
||||
<?php $collapseId = 'classDraftGroup_' . $classSectionId; ?>
|
||||
<div class="accordion-item mb-3">
|
||||
<h2 class="accordion-header" id="heading_<?= esc($collapseId) ?>">
|
||||
<button class="accordion-button collapsed px-3" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
|
||||
<div class="d-flex flex-column flex-lg-row w-100 justify-content-between gap-2">
|
||||
<div>
|
||||
<strong><?= esc($classInfo['class_section_name'] ?? ('Class ' . $classSectionId)) ?></strong>
|
||||
<div class="small text-muted">
|
||||
<?= esc($classInfo['student_count'] ?? 0) ?> students · <?= esc(count($classDraftsForSection)) ?> submissions
|
||||
<?php if (!empty($newSubmissionClasses[$classSectionId])): ?>
|
||||
<span class="badge bg-info text-dark ms-2">New submission</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($draft['teacher_file'])): ?>
|
||||
<div>
|
||||
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank">
|
||||
<?= esc($draft['teacher_filename'] ?? 'Submitted draft') ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">No teacher file</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($draft['final_file'])): ?>
|
||||
<div class="mt-2">
|
||||
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" class="link-success small">
|
||||
<?= esc($draft['final_filename'] ?? 'Final draft') ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
<?php
|
||||
$pdfFile = $draft['final_pdf_file'] ?? null;
|
||||
// Fallback: if final_file itself is a pdf, use it
|
||||
if (empty($pdfFile) && !empty($draft['final_file']) && strtolower(pathinfo($draft['final_file'], PATHINFO_EXTENSION)) === 'pdf') {
|
||||
$pdfFile = $draft['final_file'];
|
||||
}
|
||||
?>
|
||||
<?php if (!empty($pdfFile)): ?>
|
||||
<a href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" class="link-success small">
|
||||
PDF
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="<?= base_url('/administrator/exam-drafts/review') ?>" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="draft_id" value="<?= esc($draft['id']) ?>">
|
||||
<div class="mb-2">
|
||||
<textarea
|
||||
name="admin_comments"
|
||||
rows="2"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Optional note for the teacher"
|
||||
><?= esc($draft['admin_comments'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small mb-1">Status</label>
|
||||
<select name="review_status" class="form-select form-select-sm">
|
||||
<?php foreach ($statusOptions as $option): ?>
|
||||
<option value="<?= esc($option) ?>" <?= ($option === ($draft['status'] ?? '')) ? 'selected' : '' ?>>
|
||||
<?= esc(ucfirst($option)) ?>
|
||||
</option>
|
||||
</div>
|
||||
<span class="badge bg-secondary align-self-start">
|
||||
<?= esc(count($classDraftsForSection)) ?>
|
||||
<?= count($classDraftsForSection) === 1 ? 'submission' : 'submissions' ?>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="heading_<?= esc($collapseId) ?>">
|
||||
<div class="accordion-body pt-0 px-3">
|
||||
<?php if (empty($classDraftsForSection)): ?>
|
||||
<p class="text-muted mb-0">No submissions yet for this class.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered align-middle mb-0 exam-drafts-table no-mgmt-sticky">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Teacher</th>
|
||||
<th>Class</th>
|
||||
<th>Title & AuthorNote</th>
|
||||
<th>Version</th>
|
||||
<th>Date-Time</th>
|
||||
<th>Status</th>
|
||||
<th>Files</th>
|
||||
<th>Reviewer Action</th>
|
||||
<th>Final version</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($classDraftsForSection as $draft): ?>
|
||||
<?php
|
||||
$teacherName = trim(($draft['teacher_first'] ?? '') . ' ' . ($draft['teacher_last'] ?? ''));
|
||||
if ($teacherName === '') {
|
||||
$teacherName = ($draft['admin_id'] ?? null) === ($draft['teacher_id'] ?? null)
|
||||
? 'Admin upload'
|
||||
: 'User #' . ($draft['teacher_id'] ?? 'N/A');
|
||||
}
|
||||
$st = strtolower((string) ($draft['status'] ?? ''));
|
||||
$adminName = trim(($draft['admin_first'] ?? '') . ' ' . ($draft['admin_last'] ?? ''));
|
||||
$authorNote = $draft['author_comment'] ?? $draft['description'] ?? '';
|
||||
$reviewerNote = $draft['reviewer_comment'] ?? $draft['admin_comments'] ?? '';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($teacherName) ?></td>
|
||||
<td><?= esc($draft['class_section_name'] ?? 'Unknown') ?></td>
|
||||
<td>
|
||||
<strong><?= esc($draft['draft_title'] ?? 'Untitled') ?></strong>
|
||||
<div class="small text-muted"><?= esc($draft['exam_type'] ?? 'N/A') ?></div>
|
||||
<?php if ($authorNote !== ''): ?>
|
||||
<div class="small mt-1">
|
||||
<span class="text-muted fw-semibold">Author:</span>
|
||||
<?= esc($authorNote) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($reviewerNote !== ''): ?>
|
||||
<div class="small mt-1 border-top pt-1">
|
||||
<span class="text-muted fw-semibold">Reviewer:</span>
|
||||
<?= esc($reviewerNote) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-nowrap">v<?= esc((string) ($draft['version'] ?? 1)) ?></td>
|
||||
<td class="text-nowrap"><?= esc(!empty($draft['created_at']) ? local_datetime($draft['created_at'], 'M j, Y g:i A') : '—') ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$badgeData = $statusBadges[$st] ?? ['label' => $st, 'class' => 'bg-secondary text-white'];
|
||||
$badgeStyle = !empty($badgeData['style']) ? ' style="' . esc($badgeData['style']) . '"' : '';
|
||||
?>
|
||||
<span class="badge <?= esc($badgeData['class']) ?> js-status-badge" data-status="<?= esc($st) ?>"<?= $badgeStyle ?>>
|
||||
<?= esc($badgeData['label']) ?>
|
||||
</span>
|
||||
<div class="small text-muted mt-1 js-acceptance-note">
|
||||
<?php if ($st === 'accepted' && !empty($draft['acceptance_type'])): ?>
|
||||
<?= $draft['acceptance_type'] === 'minor_edits' ? 'Accepted w/ minor edits' : 'Accepted as is' ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if (!empty($draft['reviewed_at'])): ?>
|
||||
<div class="small text-muted mt-1">
|
||||
Reviewed <?= esc(local_datetime($draft['reviewed_at'], 'M j, Y g:i A')) ?>
|
||||
<?php if ($adminName !== ''): ?>
|
||||
by <?= esc($adminName) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php $formId = 'review_form_' . (int) ($draft['id'] ?? 0); ?>
|
||||
<?php $revNumber = max(1, (int) ($draft['version'] ?? 1)); ?>
|
||||
<?php if (!empty($draft['teacher_file'])): ?>
|
||||
<div>
|
||||
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank" rel="noopener">
|
||||
<?= esc('Ver' . $revNumber . ' ' . ($draft['teacher_filename'] ?? 'Submitted draft')) ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">No teacher file</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($draft['final_file']) && strtolower((string) ($draft['status'] ?? '')) === 'accepted'): ?>
|
||||
<div class="mt-2">
|
||||
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" rel="noopener" class="link-success small">
|
||||
<?= esc('Final ' . ($draft['final_filename'] ?? 'Final draft')) ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($draft['review_files'])): ?>
|
||||
<div class="mt-2">
|
||||
<?php
|
||||
$reviewLinks = [];
|
||||
foreach ($draft['review_files'] as $rf) {
|
||||
$rev = max(1, (int) ($rf['review_revision'] ?? 1));
|
||||
$name = $rf['final_filename'] ?? 'Review file';
|
||||
$file = $rf['final_file'] ?? '';
|
||||
if ($file !== '') {
|
||||
$reviewLinks[] = '<a href="' . esc(base_url('exam-drafts/files/final/' . $file)) . '" target="_blank" rel="noopener" class="link-success small">' . esc('Ver' . $revNumber . '_' . $rev . ' ' . $name) . '</a>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?= !empty($reviewLinks) ? implode(' | ', $reviewLinks) : '' ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="mt-2">
|
||||
<?php $fileInputId = 'review_file_' . (int) ($draft['id'] ?? 0); ?>
|
||||
<input type="file" name="final_file" id="<?= esc($fileInputId) ?>" class="form-control form-control-sm" accept="<?= esc($fileAccept) ?>" form="<?= esc($formId) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary mt-2" form="<?= esc($formId) ?>">Upload review file</button>
|
||||
</div>
|
||||
</td>
|
||||
<td style="min-width: 280px;">
|
||||
<?= form_open_multipart(base_url('administrator/exam-drafts/review'), ['class' => 'vstack gap-2', 'id' => $formId]) ?>
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="draft_id" value="<?= (int) ($draft['id'] ?? 0) ?>">
|
||||
<?php $selectedStatus = ''; ?>
|
||||
<select name="review_status" class="form-select form-select-sm" required>
|
||||
<option value="" <?= $selectedStatus === '' ? 'selected' : '' ?> disabled>— Select status —</option>
|
||||
<option value="accepted" <?= $selectedStatus === 'accepted' ? 'selected' : '' ?>>Accepted</option>
|
||||
<option value="legacy" <?= $selectedStatus === 'legacy' ? 'selected' : '' ?>>Legacy</option>
|
||||
<option value="canceled" <?= $selectedStatus === 'canceled' ? 'selected' : '' ?>>Canceled</option>
|
||||
<option value="rejected" <?= $selectedStatus === 'rejected' ? 'selected' : '' ?>>Rejected</option>
|
||||
<option value="review needed" <?= $selectedStatus === 'review needed' ? 'selected' : '' ?>>Review needed</option>
|
||||
<option value="under review" <?= $selectedStatus === 'under review' ? 'selected' : '' ?>>Under review</option>
|
||||
</select>
|
||||
<div class="small js-acceptance-group d-none">
|
||||
<span class="d-block mb-1">As is / Minor edits</span>
|
||||
<?php $acc = (string) ($draft['acceptance_type'] ?? ''); ?>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="acceptance_type" value="as_is" id="as_is_<?= (int) ($draft['id'] ?? 0) ?>" <?= $acc !== 'minor_edits' ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="as_is_<?= (int) ($draft['id'] ?? 0) ?>">As is</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="acceptance_type" value="minor_edits" id="minor_<?= (int) ($draft['id'] ?? 0) ?>" <?= $acc === 'minor_edits' ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="minor_<?= (int) ($draft['id'] ?? 0) ?>">Minor edits</label>
|
||||
</div>
|
||||
</div>
|
||||
<textarea name="reviewer_comment" class="form-control form-control-sm js-review-comment" rows="3" placeholder="Feedback for the teacher"><?= esc($draft['reviewer_comment'] ?? $draft['reviewer_comments'] ?? $draft['admin_comments'] ?? '') ?></textarea>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<span class="text-muted small js-review-status"></span>
|
||||
</div>
|
||||
<?= form_close() ?>
|
||||
</td>
|
||||
<td class="text-nowrap" style="min-width: 220px;">
|
||||
<?php
|
||||
$pdfFile = $draft['final_pdf_file'] ?? null;
|
||||
if (empty($pdfFile) && !empty($draft['final_file']) && strtolower(pathinfo($draft['final_file'], PATHINFO_EXTENSION)) === 'pdf') {
|
||||
$pdfFile = $draft['final_file'];
|
||||
}
|
||||
?>
|
||||
<?php if (!empty($pdfFile)): ?>
|
||||
<div class="mt-2 d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" rel="noopener">View</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="<?= base_url('exam-drafts/files/final/' . $pdfFile) ?>" target="_blank" rel="noopener" download>Download</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small mb-1">Upload final draft</label>
|
||||
<input type="file" name="final_file" class="form-control form-control-sm">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-primary w-100">
|
||||
Save review
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="legacy" role="tabpanel" aria-labelledby="legacy-tab">
|
||||
<div class="card border-primary-subtle mb-3">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>Upload Old / Legacy Exam</strong>
|
||||
<div class="small text-muted">Store historic exams as finalized records.</div>
|
||||
<strong>Upload old / legacy exam</strong>
|
||||
<div class="small text-muted">Store historic exams as accepted records.</div>
|
||||
</div>
|
||||
<span class="badge bg-primary-subtle text-primary">Admin only</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="<?= base_url('/administrator/exam-drafts/upload-legacy') ?>" enctype="multipart/form-data" class="row g-3">
|
||||
<?= form_open_multipart(base_url('administrator/exam-drafts/upload-legacy'), ['class' => 'row g-3']) ?>
|
||||
<?= csrf_field() ?>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Class Section</label>
|
||||
<select name="class_section_id" class="form-select" required>
|
||||
<option value="">Select class</option>
|
||||
<label class="form-label small">Class sections</label>
|
||||
<select name="class_section_ids[]" class="form-select" multiple required size="6">
|
||||
<?php foreach ($classSections as $cs): ?>
|
||||
<option value="<?= esc($cs['class_section_id']) ?>"><?= esc($cs['class_section_name']) ?></option>
|
||||
<option value="<?= esc($cs['class_section_id'] ?? '') ?>"><?= esc($cs['class_section_name'] ?? '') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Hold Ctrl (Windows) or Command (Mac) to select multiple sections.</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">School Year</label>
|
||||
<label class="form-label small">School year</label>
|
||||
<input type="text" name="school_year" class="form-control" value="<?= esc($schoolYear) ?>" placeholder="2025-2026" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Semester</label>
|
||||
<select name="semester" class="form-select" required>
|
||||
<option value="Fall" <?= ($semester === 'Fall') ? 'selected' : '' ?>>Fall</option>
|
||||
<option value="Spring" <?= ($semester === 'Spring') ? 'selected' : '' ?>>Spring</option>
|
||||
</select>
|
||||
<input type="text" name="semester" class="form-control" value="<?= esc($semester) ?>" placeholder="Fall / Spring" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Exam Type</label>
|
||||
<label class="form-label small">Exam type</label>
|
||||
<select name="exam_type" class="form-select">
|
||||
<option value="">— Select type —</option>
|
||||
<?php foreach ($examTypes as $type): ?>
|
||||
@@ -211,14 +305,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label small">Upload File</label>
|
||||
<input type="file" name="old_exam_file" class="form-control" accept=".doc,.docx,.pdf" required>
|
||||
<label class="form-label small">Upload file</label>
|
||||
<input type="file" name="old_exam_file" class="form-control" accept="<?= esc($fileAccept) ?>" required>
|
||||
<div class="form-text">Allowed: <?= esc(implode(', ', $allowedExtensions)) ?> • Max <?= number_format($maxUploadBytes / 1024 / 1024, 0) ?> MB</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary">Upload Legacy Exam</button>
|
||||
<button type="submit" class="btn btn-primary">Upload legacy exam</button>
|
||||
</div>
|
||||
</form>
|
||||
<?= form_close() ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,20 +325,21 @@
|
||||
<div class="mb-4">
|
||||
<h6 class="mb-2"><?= esc($group['class_section_name'] ?? 'Class') ?></h6>
|
||||
<div class="list-group">
|
||||
<?php foreach ($group['items'] as $item): ?>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-start">
|
||||
<?php foreach ($group['items'] ?? [] as $item): ?>
|
||||
<?php $lst = strtolower((string) ($item['status'] ?? '')); ?>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-start flex-wrap gap-2">
|
||||
<div class="me-3">
|
||||
<div class="fw-semibold"><?= esc($item['draft_title'] ?? 'Legacy Exam') ?></div>
|
||||
<div class="fw-semibold"><?= esc($item['draft_title'] ?? 'Legacy exam') ?></div>
|
||||
<div class="small text-muted">
|
||||
<?= esc($item['exam_type'] ?? 'N/A') ?> •
|
||||
<?= esc($item['semester'] ?? '') ?> <?= esc($item['school_year'] ?? '') ?>
|
||||
</div>
|
||||
<div class="small mt-1"><?= $renderBadge($lst, $statusBadges) ?></div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="text-end d-flex flex-wrap gap-2 justify-content-end">
|
||||
<?php if (!empty($item['final_file'])): ?>
|
||||
<a href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||
Download
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" rel="noopener">View</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" rel="noopener" download>Download</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">File missing</span>
|
||||
<?php endif; ?>
|
||||
@@ -260,6 +355,208 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const submissionsTab = document.getElementById('submissions-tab');
|
||||
if (submissionsTab) {
|
||||
submissionsTab.addEventListener('click', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
const statusBadges = <?= json_encode($statusBadges, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||
const csrfTokenName = <?= json_encode(csrf_token(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||
const csrfCookieNames = <?= json_encode(array_values(array_filter([
|
||||
config('Security')->csrfCookieName ?? null,
|
||||
config('Security')->cookieName ?? null,
|
||||
'csrf_cookie_name',
|
||||
])), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||
|
||||
const readCookie = (name) => {
|
||||
const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[$()*+.?[\\\]^{|}-]/g, '\\$&') + '=([^;]*)'));
|
||||
return match ? decodeURIComponent(match[1]) : '';
|
||||
};
|
||||
|
||||
const syncCsrfToken = (form) => {
|
||||
let tokenValue = '';
|
||||
csrfCookieNames.some((name) => {
|
||||
tokenValue = readCookie(name);
|
||||
return tokenValue !== '';
|
||||
});
|
||||
if (!tokenValue && form) {
|
||||
const tokenInput = form.querySelector(`input[name="${csrfTokenName}"]`);
|
||||
if (tokenInput && tokenInput.value) {
|
||||
tokenValue = tokenInput.value;
|
||||
}
|
||||
}
|
||||
if (!tokenValue || !form) {
|
||||
return tokenValue;
|
||||
}
|
||||
const tokenInput = form.querySelector(`input[name="${csrfTokenName}"]`);
|
||||
if (tokenInput) {
|
||||
tokenInput.value = tokenValue;
|
||||
}
|
||||
return tokenValue;
|
||||
};
|
||||
const updateRow = (row) => {
|
||||
const statusSelect = row.querySelector('select[name="review_status"]');
|
||||
const acceptanceGroup = row.querySelector('.js-acceptance-group');
|
||||
if (!statusSelect || !acceptanceGroup) {
|
||||
return;
|
||||
}
|
||||
acceptanceGroup.classList.toggle('d-none', statusSelect.value !== 'accepted');
|
||||
};
|
||||
|
||||
const updateStatusUI = (row) => {
|
||||
const statusSelect = row.querySelector('select[name="review_status"]');
|
||||
const badge = row.querySelector('.js-status-badge');
|
||||
if (!statusSelect || !badge || statusSelect.value === '') {
|
||||
return;
|
||||
}
|
||||
const status = statusSelect.value;
|
||||
const badgeData = statusBadges[status] || { label: status, class: 'bg-secondary text-white' };
|
||||
badge.textContent = badgeData.label || status;
|
||||
badge.className = `badge ${badgeData.class} js-status-badge`;
|
||||
if (badgeData.style) {
|
||||
badge.setAttribute('style', badgeData.style);
|
||||
} else {
|
||||
badge.removeAttribute('style');
|
||||
}
|
||||
badge.dataset.status = status;
|
||||
|
||||
const acceptanceNote = row.querySelector('.js-acceptance-note');
|
||||
if (acceptanceNote) {
|
||||
if (status === 'accepted') {
|
||||
const acc = row.querySelector('input[name="acceptance_type"]:checked');
|
||||
acceptanceNote.textContent = acc && acc.value === 'minor_edits'
|
||||
? 'Accepted w/ minor edits'
|
||||
: 'Accepted as is';
|
||||
} else {
|
||||
acceptanceNote.textContent = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveRow = (row, options = {}) => {
|
||||
const form = row.querySelector('form');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
const statusSelect = form.querySelector('select[name="review_status"]');
|
||||
if (!statusSelect) {
|
||||
return;
|
||||
}
|
||||
const statusLabel = form.querySelector('.js-review-status');
|
||||
const data = new FormData(form);
|
||||
data.delete('final_file');
|
||||
data.delete('old_exam_file');
|
||||
if (options.commitComment) {
|
||||
const commentInput = form.querySelector('textarea[name="reviewer_comment"]');
|
||||
if (commentInput) {
|
||||
const raw = commentInput.value || '';
|
||||
const committed = raw.endsWith('\n') ? raw : raw + '\n';
|
||||
data.set('reviewer_comment', committed);
|
||||
}
|
||||
}
|
||||
const csrfToken = syncCsrfToken(form);
|
||||
if (csrfToken) {
|
||||
data.set(csrfTokenName, csrfToken);
|
||||
}
|
||||
if (statusLabel) {
|
||||
statusLabel.textContent = 'Saving...';
|
||||
}
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
},
|
||||
})
|
||||
.then((resp) => {
|
||||
if (!resp.ok) {
|
||||
throw new Error('Save failed');
|
||||
}
|
||||
syncCsrfToken(form);
|
||||
updateStatusUI(row);
|
||||
if (statusLabel) {
|
||||
statusLabel.textContent = 'Saved';
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (statusLabel && statusLabel.textContent === 'Saved') {
|
||||
statusLabel.textContent = '';
|
||||
}
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
if (statusLabel) {
|
||||
statusLabel.textContent = 'Error saving';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const debounce = (fn, wait) => {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), wait);
|
||||
};
|
||||
};
|
||||
|
||||
const debouncedSave = debounce(saveRow, 500);
|
||||
|
||||
document.querySelectorAll('.exam-drafts-table tbody tr').forEach(updateRow);
|
||||
|
||||
document.addEventListener('change', (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const row = target.closest('tr');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
if (target instanceof HTMLSelectElement && target.name === 'review_status') {
|
||||
updateRow(row);
|
||||
debouncedSave(row);
|
||||
return;
|
||||
}
|
||||
if (target instanceof HTMLInputElement && target.name === 'acceptance_type') {
|
||||
debouncedSave(row);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('input', (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLTextAreaElement) || target.name !== 'reviewer_comment') {
|
||||
return;
|
||||
}
|
||||
const row = target.closest('tr');
|
||||
if (row && target.value.endsWith('\n')) {
|
||||
const lastCommitted = target.dataset.lastCommitted || '';
|
||||
if (target.value !== lastCommitted) {
|
||||
target.dataset.lastCommitted = target.value;
|
||||
debouncedSave(row, { commitComment: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('blur', (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLTextAreaElement) || target.name !== 'reviewer_comment') {
|
||||
return;
|
||||
}
|
||||
const row = target.closest('tr');
|
||||
if (row) {
|
||||
const lastCommitted = target.dataset.lastCommitted || '';
|
||||
if (target.value !== lastCommitted) {
|
||||
target.dataset.lastCommitted = target.value;
|
||||
debouncedSave(row, { commitComment: true });
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('styles') ?>
|
||||
@@ -270,13 +567,15 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
.exam-drafts-table th {
|
||||
min-width: 120px;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.exam-drafts-table td {
|
||||
max-width: 220px;
|
||||
max-width: none;
|
||||
}
|
||||
.exam-drafts-table {
|
||||
table-layout: auto;
|
||||
width: max-content;
|
||||
}
|
||||
.exam-drafts-table thead th {
|
||||
background-color: #f8f9fa;
|
||||
|
||||
@@ -13,17 +13,19 @@
|
||||
</a>
|
||||
</div>
|
||||
<!-- Search Form -->
|
||||
<!--form method="get" action="<--?= base_url('admin/paypal-transactions') ?>" class="mb-3">
|
||||
<--?= csrf_field(); ?>
|
||||
<!--
|
||||
<form method="get" action="<?= base_url('admin/paypal-transactions') ?>" class="mb-3">
|
||||
<?= csrf_field(); ?>
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" value="<--?= esc($keyword) ?>" class="form-control"
|
||||
<input type="text" name="q" value="<?= esc($keyword) ?>" class="form-control"
|
||||
placeholder="Search by transaction ID, email, order ID, or event type">
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
<--?php if ($keyword): ?>
|
||||
<a href="<--?= base_url('admin/paypal-transactions') ?>" class="btn btn-secondary">Clear</a>
|
||||
<--?php endif; ?>
|
||||
<?php if ($keyword): ?>
|
||||
<a href="<?= base_url('admin/paypal-transactions') ?>" class="btn btn-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form-->
|
||||
</form>
|
||||
-->
|
||||
|
||||
<!-- Transactions Table -->
|
||||
<table id="myTable" class="display table table-striped">
|
||||
|
||||
@@ -144,58 +144,86 @@
|
||||
<span class="text-muted small"><?= count($entries) ?> record<?= count($entries) === 1 ? '' : 's' ?></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle mb-0" data-no-mgmt-sticky>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Subject</th>
|
||||
<th>Unit</th>
|
||||
<th>Unit title</th>
|
||||
<th>Chapter / Surah</th>
|
||||
<th>Updated</th>
|
||||
<th style="min-width: 170px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($entries)): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted">No curriculum records yet.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($entries as $entry): ?>
|
||||
<?php
|
||||
$subjectLabel = $subjectLabels[$entry['subject']] ?? ucfirst($entry['subject'] ?? '');
|
||||
$updatedAt = $entry['updated_at'] ?? $entry['created_at'] ?? '';
|
||||
$updatedDisplay = '';
|
||||
if ($updatedAt) {
|
||||
try {
|
||||
$updatedDisplay = (new \DateTime($updatedAt))->format('M d, Y H:i');
|
||||
} catch (\Exception $e) {
|
||||
$updatedDisplay = $updatedAt;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc(($entry['class_name'] ?? '') ?: '—') ?></td>
|
||||
<td><?= esc($subjectLabel) ?></td>
|
||||
<td><?= $entry['unit_number'] ? esc((string)$entry['unit_number']) : '—' ?></td>
|
||||
<td><?= esc(($entry['unit_title'] ?? '') ?: '—') ?></td>
|
||||
<td><?= esc(($entry['chapter_name'] ?? '') ?: '—') ?></td>
|
||||
<td><?= esc($updatedDisplay ?: '—') ?></td>
|
||||
<td class="d-flex gap-2 flex-wrap">
|
||||
<a href="<?= site_url('administrator/subject-curriculum/edit/' . $entry['id']) ?>" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<form action="<?= site_url('administrator/subject-curriculum/delete/' . $entry['id']) ?>" method="post" onsubmit="return confirm('Remove this curriculum entry?');">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (empty($entries)): ?>
|
||||
<div class="text-center text-muted">No curriculum records yet.</div>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$entriesByClass = [];
|
||||
foreach ($entries as $entry) {
|
||||
$className = ($entry['class_name'] ?? '') ?: '—';
|
||||
if (!isset($entriesByClass[$className])) {
|
||||
$entriesByClass[$className] = [];
|
||||
}
|
||||
$entriesByClass[$className][] = $entry;
|
||||
}
|
||||
?>
|
||||
<div class="accordion" id="curriculumAccordion">
|
||||
<?php $accIndex = 0; ?>
|
||||
<?php foreach ($entriesByClass as $className => $classEntries): ?>
|
||||
<?php
|
||||
$accIndex++;
|
||||
$collapseId = 'curriculum-class-' . $accIndex;
|
||||
$headingId = 'curriculum-heading-' . $accIndex;
|
||||
?>
|
||||
<div class="accordion-item mb-2">
|
||||
<h2 class="accordion-header" id="<?= esc($headingId) ?>">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
|
||||
<?= esc($className) ?>
|
||||
<span class="badge bg-secondary ms-2"><?= count($classEntries) ?> entries</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="<?= esc($headingId) ?>" data-bs-parent="#curriculumAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle mb-0" data-no-mgmt-sticky>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Subject</th>
|
||||
<th>Unit</th>
|
||||
<th>Unit title</th>
|
||||
<th>Chapter / Surah</th>
|
||||
<th>Updated</th>
|
||||
<th style="min-width: 170px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($classEntries as $entry): ?>
|
||||
<?php
|
||||
$subjectLabel = $subjectLabels[$entry['subject']] ?? ucfirst($entry['subject'] ?? '');
|
||||
$updatedAt = $entry['updated_at'] ?? $entry['created_at'] ?? '';
|
||||
$updatedDisplay = '';
|
||||
if ($updatedAt) {
|
||||
try {
|
||||
$updatedDisplay = (new \DateTime($updatedAt))->format('M d, Y H:i');
|
||||
} catch (\Exception $e) {
|
||||
$updatedDisplay = $updatedAt;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($subjectLabel) ?></td>
|
||||
<td><?= $entry['unit_number'] ? esc((string)$entry['unit_number']) : '—' ?></td>
|
||||
<td><?= esc(($entry['unit_title'] ?? '') ?: '—') ?></td>
|
||||
<td><?= esc(($entry['chapter_name'] ?? '') ?: '—') ?></td>
|
||||
<td><?= esc($updatedDisplay ?: '—') ?></td>
|
||||
<td class="d-flex gap-2 flex-wrap">
|
||||
<a href="<?= site_url('administrator/subject-curriculum/edit/' . $entry['id']) ?>" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<form action="<?= site_url('administrator/subject-curriculum/delete/' . $entry['id']) ?>" method="post" onsubmit="return confirm('Remove this curriculum entry?');">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,15 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted">Loading...</td>
|
||||
<td class="text-center text-muted">Loading...</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -198,10 +206,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (!teachers || teachers.length === 0) {
|
||||
var emptyRow = document.createElement('tr');
|
||||
var emptyCell = document.createElement('td');
|
||||
emptyCell.colSpan = 9;
|
||||
emptyCell.className = 'text-center text-muted';
|
||||
emptyCell.textContent = 'No teachers found.';
|
||||
emptyRow.appendChild(emptyCell);
|
||||
for (var i = 0; i < 8; i++) {
|
||||
emptyRow.appendChild(document.createElement('td'));
|
||||
}
|
||||
tbody.appendChild(emptyRow);
|
||||
} else {
|
||||
teachers.forEach(function (teacher, index) {
|
||||
@@ -312,7 +322,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted">Loading...</td></tr>';
|
||||
tbody.innerHTML = '<tr>'
|
||||
+ '<td class="text-center text-muted">Loading...</td>'
|
||||
+ '<td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>'
|
||||
+ '</tr>';
|
||||
|
||||
var url = apiList + (selectedYear ? ('?schoolYear=' + encodeURIComponent(selectedYear)) : '');
|
||||
fetch(url, {
|
||||
|
||||
@@ -23,6 +23,41 @@
|
||||
<div class="card-body">
|
||||
<?php
|
||||
$termExamLabel = (isset($semester) && strtolower((string) $semester) === 'spring') ? 'Final' : 'Midterm';
|
||||
$lowProgressSectionIds = $lowProgressSectionIds ?? [];
|
||||
$flaggedClasses = count($lowProgressSectionIds);
|
||||
$pageNotifications = [];
|
||||
if ($missingItemsCount > 0) {
|
||||
$pageNotifications[] = [
|
||||
'level' => 'danger',
|
||||
'message' => "{$missingItemsCount} missing item" . ($missingItemsCount === 1 ? '' : 's') . " awaiting teacher uploads.",
|
||||
];
|
||||
}
|
||||
if ($completionPercent < 70) {
|
||||
$pageNotifications[] = [
|
||||
'level' => 'warning',
|
||||
'message' => "Submission completion is below 70% — follow up with remaining teachers.",
|
||||
];
|
||||
}
|
||||
if ($flaggedClasses > 0) {
|
||||
$pageNotifications[] = [
|
||||
'level' => 'warning',
|
||||
'message' => "Progress is under 50% for {$flaggedClasses} class section" . ($flaggedClasses === 1 ? '' : 's') . ".",
|
||||
];
|
||||
}
|
||||
if (empty($rows)) {
|
||||
$pageNotifications[] = [
|
||||
'level' => 'info',
|
||||
'message' => 'No class-section assignments submitted yet; encourage teachers to upload drafts.',
|
||||
];
|
||||
}
|
||||
if (empty($pageNotifications)) {
|
||||
$pageNotifications[] = [
|
||||
'level' => 'info',
|
||||
'message' => 'All tracked classes are current. Use the controls below to send reminders.',
|
||||
];
|
||||
}
|
||||
$examDraftDeadlineConfig = $examDraftDeadlineConfig ?? '';
|
||||
$examDraftDeadlineFormatted = $examDraftDeadlineFormatted ?? '';
|
||||
?>
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success">
|
||||
@@ -37,203 +72,278 @@
|
||||
<?= 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="d-flex flex-wrap gap-4 align-items-center">
|
||||
<div>
|
||||
<div class="text-uppercase small text-muted">Submission completion</div>
|
||||
<div class="h4 fw-semibold mb-1"><?= esc($completionPercent) ?>%</div>
|
||||
<div class="progress" style="height:6px;">
|
||||
<div
|
||||
class="progress-bar bg-primary"
|
||||
role="progressbar"
|
||||
style="width: <?= esc($completionPercent) ?>%;"
|
||||
aria-valuenow="<?= esc($completionPercent) ?>"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
<?php
|
||||
$groupedRows = [];
|
||||
foreach ($rows as $idx => $row) {
|
||||
$classKey = (string) ($row['class_section_id'] ?? $row['class_section'] ?? 'class_' . $idx);
|
||||
$groupedRows[$classKey][] = $row;
|
||||
}
|
||||
?>
|
||||
<div class="row g-3 mb-4 align-items-stretch">
|
||||
<div class="col-lg-8">
|
||||
<div class="row g-3 summary-row">
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="summary-card h-100 shadow-sm border-0">
|
||||
<div class="summary-card-body">
|
||||
<div class="summary-label text-uppercase">Completion</div>
|
||||
<div class="summary-value"><?= esc($completionPercent) ?>%</div>
|
||||
<div class="progress summary-progress">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
style="width: <?= esc($completionPercent) ?>%;"
|
||||
aria-valuenow="<?= esc($completionPercent) ?>"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="summary-card h-100 shadow-sm border-0">
|
||||
<div class="summary-card-body">
|
||||
<div class="summary-label text-uppercase">Missing items</div>
|
||||
<div class="summary-value <?= $missingItemsCount > 0 ? 'text-danger' : '' ?>"><?= esc($missingItemsCount) ?></div>
|
||||
<div class="summary-note"><?= esc($submittedItems) ?> submitted / <?= esc($totalItems) ?> total</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="summary-card h-100 shadow-sm border-0">
|
||||
<div class="summary-card-body">
|
||||
<div class="summary-label text-uppercase">Submissions</div>
|
||||
<div class="summary-value"><?= esc($submittedItems) ?></div>
|
||||
<div class="summary-note"><?= esc($totalItems) ?> teachers tracked</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="summary-card h-100 shadow-sm border-0">
|
||||
<div class="summary-card-body">
|
||||
<div class="summary-label text-uppercase">Flagged</div>
|
||||
<div class="summary-value"><?= esc($flaggedClasses) ?></div>
|
||||
<div class="summary-note">Sections under 50% complete</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-uppercase small text-muted">Missing items</div>
|
||||
<div class="h4 fw-semibold text-danger mb-0"><?= esc($missingItemsCount) ?></div>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<?= esc($submittedItems) ?> submitted / <?= esc($totalItems) ?> total items
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card page-notifications-card h-100 shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Page notifications</span>
|
||||
<span class="badge bg-light text-dark"><?= count($pageNotifications) ?> alerts</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="page-notifications-list mb-0">
|
||||
<?php foreach ($pageNotifications as $notification): ?>
|
||||
<li class="page-notification-item page-notification-<?= esc($notification['level']) ?>">
|
||||
<span class="notification-indicator" aria-hidden="true"></span>
|
||||
<span><?= esc($notification['message']) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="<?= site_url('administrator/teacher-submissions/notify') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<div class="table-responsive">
|
||||
<table
|
||||
class="table table-striped table-bordered m-0 align-middle teacher-submissions-table"
|
||||
data-no-mgmt-sticky
|
||||
data-no-dt-fixedheader
|
||||
>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Class-Section</th>
|
||||
<th>Teachers Name</th>
|
||||
<th class="text-center">
|
||||
<?= esc($termExamLabel) ?> Score
|
||||
<div class="form-check d-inline-flex align-items-center gap-1 ms-2">
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($rows)): ?>
|
||||
<?php $homeworkToggleRendered = false; ?>
|
||||
<?php foreach ($rows as $row): ?>
|
||||
<tr>
|
||||
<td><?= esc($row['class_section']) ?></td>
|
||||
<td>
|
||||
<?php if (!empty($row['teachers'])): ?>
|
||||
<?php foreach ($row['teachers'] as $teacher): ?>
|
||||
<div><?= esc($teacher['label'] ?? 'Teacher') ?></div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">Unassigned</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php foreach ([
|
||||
'midterm_score_status',
|
||||
'midterm_comment_status',
|
||||
'participation_status',
|
||||
'ptap_comment_status',
|
||||
'class_progress_status',
|
||||
'exam_draft_status',
|
||||
] as $statusKey): ?>
|
||||
<?php $status = $row[$statusKey] ?? ['label' => 'N/A', 'badge' => 'bg-secondary']; ?>
|
||||
<td class="text-center">
|
||||
<span class="badge <?= esc($status['badge'] ?? 'bg-secondary') ?>">
|
||||
<?= esc($status['label'] ?? 'N/A') ?>
|
||||
</span>
|
||||
<?php if (!empty($status['detail'])): ?>
|
||||
<div class="small text-muted"><?= esc($status['detail']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
<?php $homeworkStatus = $row['homework_status'] ?? ['label' => 'N/A', 'badge' => 'bg-secondary']; ?>
|
||||
<td class="text-center">
|
||||
<span class="badge <?= esc($homeworkStatus['badge'] ?? 'bg-secondary') ?>">
|
||||
<?= esc($homeworkStatus['label'] ?? 'N/A') ?>
|
||||
</span>
|
||||
<?php if (!empty($homeworkStatus['detail'])): ?>
|
||||
<div class="small text-muted"><?= esc($homeworkStatus['detail']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($row['teachers'])): ?>
|
||||
<?php $missingPayload = base64_encode(json_encode($row['missing_items'] ?? [])); ?>
|
||||
<?php foreach ($row['teachers'] as $teacher): ?>
|
||||
<?php $history = $notificationHistory[$row['class_section_id']][$teacher['id']] ?? []; ?>
|
||||
<?php $lastEntry = $history[0] ?? null; ?>
|
||||
<div class="mb-2">
|
||||
<label class="d-flex align-items-center gap-2 mb-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
name="notify[<?= esc($row['class_section_id']) ?>][<?= esc($teacher['id']) ?>]"
|
||||
value="1"
|
||||
/>
|
||||
<span class="fw-semibold"><?= esc($teacher['label'] ?? 'Teacher') ?></span>
|
||||
</label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="missing_items[<?= esc($row['class_section_id']) ?>][<?= esc($teacher['id']) ?>]"
|
||||
value="<?= esc($missingPayload) ?>"
|
||||
/>
|
||||
<div class="small text-muted">
|
||||
<?php if ($lastEntry !== null): ?>
|
||||
<span>
|
||||
Last <?= esc($lastEntry['status'] === 'sent' ? 'sent' : 'attempted') ?> on <?= esc($lastEntry['sent_at_text'] ?: 'N/A') ?>
|
||||
by <?= esc($lastEntry['admin_name'] ?? '') ?>
|
||||
</span>
|
||||
<?php if (count($history) > 1): ?>
|
||||
<div>History: <?= count($history) ?> entries</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<span>No notifications sent yet</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">No teacher assigned</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($row['missing_items'])): ?>
|
||||
<div class="small text-danger mt-1">
|
||||
Outstanding: <?= esc(implode(', ', $row['missing_items'])) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($rows)): ?>
|
||||
<p class="text-center text-muted mt-4">No teacher-class assignments found for this term.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-control-bar mb-3 d-flex flex-wrap align-items-center gap-3">
|
||||
<div class="d-flex flex-wrap align-items-center gap-3">
|
||||
<label class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="notify_midterm_score" value="1">
|
||||
<span class="form-check-label small">Include <?= esc($termExamLabel) ?> score</span>
|
||||
</label>
|
||||
<label class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="notify_midterm_comment" value="1">
|
||||
<span class="form-check-label small">Include <?= esc($termExamLabel) ?> comment</span>
|
||||
</label>
|
||||
<label class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="notify_participation" value="1">
|
||||
<span class="form-check-label small">Include participation</span>
|
||||
</label>
|
||||
<label class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="notify_ptap_comment" value="1">
|
||||
<span class="form-check-label small">Include PTAP comment</span>
|
||||
</label>
|
||||
<label class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="notify_class_progress" value="1">
|
||||
<span class="form-check-label small">Include class progress</span>
|
||||
</label>
|
||||
<label class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="notify_exam_draft" value="1">
|
||||
<span class="form-check-label small">Include exam draft</span>
|
||||
</label>
|
||||
<label class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="homework_notify_all" value="1">
|
||||
<span class="form-check-label small">Include homework</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
<span class="text-uppercase" style="font-size:0.65rem;">Exam draft deadline</span><br>
|
||||
<?php if ($examDraftDeadlineConfig !== ''): ?>
|
||||
<?= esc($examDraftDeadlineConfig) ?>
|
||||
<?php if ($examDraftDeadlineFormatted !== ''): ?>
|
||||
<span class="text-muted"> → <?= esc($examDraftDeadlineFormatted) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">No teacher-class assignments found for this term.</td>
|
||||
</tr>
|
||||
<span class="text-warning">Not set</span>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3 text-end">
|
||||
<button type="submit" class="btn btn-primary" <?= empty($rows) ? 'disabled' : '' ?>>
|
||||
Send notifications to selected teachers
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion" id="teacherSubmissionAccordion">
|
||||
<?php foreach ($groupedRows as $sectionKey => $sectionRows): ?>
|
||||
<?php
|
||||
$firstRow = $sectionRows[0] ?? [];
|
||||
$sectionLabel = esc($firstRow['class_section'] ?? 'Class section');
|
||||
$collapseId = 'teacherSectionCollapse_' . md5($sectionKey);
|
||||
$badgeStatus = count($sectionRows) === 1 ? 'submission' : 'submissions';
|
||||
?>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading_<?= esc($collapseId) ?>">
|
||||
<button
|
||||
class="accordion-button collapsed d-flex justify-content-between align-items-center"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#<?= esc($collapseId) ?>"
|
||||
aria-expanded="false"
|
||||
aria-controls="<?= esc($collapseId) ?>"
|
||||
>
|
||||
<div>
|
||||
<strong><?= $sectionLabel ?></strong>
|
||||
<div class="small text-muted"><?= esc(count($sectionRows)) ?> <?= $badgeStatus ?></div>
|
||||
</div>
|
||||
<span class="badge bg-primary-subtle text-primary"><?= count($sectionRows) ?> rows</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="heading_<?= esc($collapseId) ?>">
|
||||
<div class="accordion-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table
|
||||
class="table table-striped table-bordered m-0 align-middle teacher-submissions-table mb-0"
|
||||
data-no-mgmt-sticky
|
||||
data-no-dt-fixedheader
|
||||
>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Class-Section</th>
|
||||
<th>Teachers Name</th>
|
||||
<th class="text-center"><?= esc($termExamLabel) ?> Score</th>
|
||||
<th class="text-center"><?= esc($termExamLabel) ?> Comment</th>
|
||||
<th class="text-center">Participation</th>
|
||||
<th class="text-center">PTAP Comment</th>
|
||||
<th class="text-center">Class Progress</th>
|
||||
<th class="text-center">Exam Draft</th>
|
||||
<th class="text-center">Homework</th>
|
||||
<th class="text-center">Notifications</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($sectionRows as $row): ?>
|
||||
<tr>
|
||||
<td><?= esc($row['class_section']) ?></td>
|
||||
<td>
|
||||
<?php if (!empty($row['teachers'])): ?>
|
||||
<?php foreach ($row['teachers'] as $teacher): ?>
|
||||
<div><?= esc($teacher['label'] ?? 'Teacher') ?></div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">Unassigned</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php foreach ([
|
||||
'midterm_score_status',
|
||||
'midterm_comment_status',
|
||||
'participation_status',
|
||||
'ptap_comment_status',
|
||||
'class_progress_status',
|
||||
'exam_draft_status',
|
||||
] as $statusKey): ?>
|
||||
<?php $status = $row[$statusKey] ?? ['label' => 'N/A', 'badge' => 'bg-secondary']; ?>
|
||||
<td class="text-center">
|
||||
<span class="badge <?= esc($status['badge'] ?? 'bg-secondary') ?>">
|
||||
<?= esc($status['label'] ?? 'N/A') ?>
|
||||
</span>
|
||||
<?php if (!empty($status['detail'])): ?>
|
||||
<div class="small text-muted"><?= esc($status['detail']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
<?php $homeworkStatus = $row['homework_status'] ?? ['label' => 'N/A', 'badge' => 'bg-secondary']; ?>
|
||||
<td class="text-center">
|
||||
<span class="badge <?= esc($homeworkStatus['badge'] ?? 'bg-secondary') ?>">
|
||||
<?= esc($homeworkStatus['label'] ?? 'N/A') ?>
|
||||
</span>
|
||||
<?php if (!empty($homeworkStatus['detail'])): ?>
|
||||
<div class="small text-muted"><?= esc($homeworkStatus['detail']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($row['teachers'])): ?>
|
||||
<?php $missingPayload = base64_encode(json_encode($row['missing_items'] ?? [])); ?>
|
||||
<?php foreach ($row['teachers'] as $teacher): ?>
|
||||
<?php $history = $notificationHistory[$row['class_section_id']][$teacher['id']] ?? []; ?>
|
||||
<?php $lastEntry = $history[0] ?? null; ?>
|
||||
<div class="mb-2">
|
||||
<label class="d-flex align-items-center gap-2 mb-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
name="notify[<?= esc($row['class_section_id']) ?>][<?= esc($teacher['id']) ?>]"
|
||||
value="1"
|
||||
/>
|
||||
<span class="fw-semibold"><?= esc($teacher['label'] ?? 'Teacher') ?></span>
|
||||
</label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="missing_items[<?= esc($row['class_section_id']) ?>][<?= esc($teacher['id']) ?>]"
|
||||
value="<?= esc($missingPayload) ?>"
|
||||
/>
|
||||
<div class="small text-muted">
|
||||
<?php if ($lastEntry !== null): ?>
|
||||
<span>
|
||||
Last <?= esc($lastEntry['status'] === 'sent' ? 'sent' : 'attempted') ?> on <?= esc($lastEntry['sent_at_text'] ?: 'N/A') ?>
|
||||
by <?= esc($lastEntry['admin_name'] ?? '') ?>
|
||||
</span>
|
||||
<?php if (count($history) > 1): ?>
|
||||
<div>History: <?= count($history) ?> entries</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<span>No notifications sent yet</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">No teacher assigned</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($row['missing_items'])): ?>
|
||||
<div class="small text-danger mt-1">
|
||||
Outstanding: <?= esc(implode(', ', $row['missing_items'])) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="mt-3 text-end">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Send notifications to selected teachers
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -265,5 +375,77 @@
|
||||
.card-body .table-responsive .teacher-submissions-table td {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: var(--bs-white);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
.summary-card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.summary-label {
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.08em;
|
||||
color: #6c757d;
|
||||
}
|
||||
.summary-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
.summary-note {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
.summary-progress {
|
||||
height: 6px;
|
||||
margin-top: 0.65rem;
|
||||
}
|
||||
.summary-progress .progress-bar {
|
||||
background: var(--bs-primary);
|
||||
}
|
||||
.page-notifications-card {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
.page-notifications-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.page-notification-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.page-notification-info .notification-indicator {
|
||||
background: #0dcaf0;
|
||||
}
|
||||
.page-notification-warning .notification-indicator {
|
||||
background: #ffc107;
|
||||
}
|
||||
.page-notification-danger .notification-indicator {
|
||||
background: #dc3545;
|
||||
}
|
||||
.notification-indicator {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
.table-control-bar {
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.table-control-bar .form-check-label {
|
||||
font-size: 0.8rem;
|
||||
margin-left: 0.15rem;
|
||||
text-transform: none;
|
||||
}
|
||||
</style>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -195,22 +195,22 @@
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-1.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-2.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-3.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-4.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-5.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-6.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -267,4 +267,4 @@
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<br>
|
||||
<input type="hidden" name="selected_role" id="selected_role">
|
||||
<input type="hidden" name="redirect_to" value="<?= esc((string) ($redirect_to ?? '')) ?>">
|
||||
|
||||
<div class="d-flex flex-wrap justify-content-center gap-3 mt-3">
|
||||
<?php foreach ($roles as $role): ?>
|
||||
@@ -68,4 +69,4 @@
|
||||
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -155,22 +155,22 @@
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-1.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-2.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-3.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-4.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-5.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-6.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,4 +227,4 @@
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<div class="col-lg-4 col-md-6 wow fadeInUp" data-wow-delay="0.1s">
|
||||
<div class="classes-item">
|
||||
<div class="bg-light rounded-circle w-75 mx-auto p-3">
|
||||
<img class="img-fluid rounded-circle" src=""assets/images/classes-1.jpg" alt="">
|
||||
<img class="img-fluid rounded-circle" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="bg-light rounded p-4 pt-5 mt-n5">
|
||||
<a class="d-block text-center h3 mt-3 mb-4" href="">Quran Learning</a>
|
||||
@@ -104,7 +104,7 @@
|
||||
<div class="col-lg-4 col-md-6 wow fadeInUp" data-wow-delay="0.3s">
|
||||
<div class="classes-item">
|
||||
<div class="bg-light rounded-circle w-75 mx-auto p-3">
|
||||
<img class="img-fluid rounded-circle" src=""assets/images/classes-2.jpg" alt="">
|
||||
<img class="img-fluid rounded-circle" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="bg-light rounded p-4 pt-5 mt-n5">
|
||||
<a class="d-block text-center h3 mt-3 mb-4" href="">Islamic Studies</a>
|
||||
@@ -114,7 +114,7 @@
|
||||
<div class="col-lg-4 col-md-6 wow fadeInUp" data-wow-delay="0.5s">
|
||||
<div class="classes-item">
|
||||
<div class="bg-light rounded-circle w-75 mx-auto p-3">
|
||||
<img class="img-fluid rounded-circle" src=""assets/images/classes-3.jpg" alt="">
|
||||
<img class="img-fluid rounded-circle" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="bg-light rounded p-4 pt-5 mt-n5">
|
||||
<a class="d-block text-center h3 mt-3 mb-4" href="">Arabic Learning</a>
|
||||
@@ -160,22 +160,22 @@
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-1.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-2.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-3.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-4.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-5.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-6.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,4 +228,4 @@
|
||||
<script src="<?= base_url('js/main.js') ?>"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -195,22 +195,22 @@
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-1.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-2.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-3.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-4.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-5.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-6.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -280,4 +280,4 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
$eventName = trim((string) ($event['event_name'] ?? 'Upcoming Event'));
|
||||
$eventCategory = trim((string) ($event['event_category'] ?? ''));
|
||||
$eventDateRaw = trim((string) ($event['expiration_date'] ?? ''));
|
||||
$eventDate = $eventDateRaw !== '' ? date('F j, Y', strtotime($eventDateRaw)) : 'TBD';
|
||||
$amountRaw = $event['amount'] ?? null;
|
||||
$amount = ($amountRaw !== null && $amountRaw !== '') ? number_format((float) $amountRaw, 2) : null;
|
||||
?>
|
||||
<div style="font-family: sans-serif; line-height: 1.6;">
|
||||
<h1 style="font-size: 1.5rem; margin-bottom: 0.5rem;"><?= esc($eventName) ?></h1>
|
||||
<p style="margin:0.25rem 0;"><strong>Date:</strong> <?= esc($eventDate) ?></p>
|
||||
<?php if ($eventCategory !== ''): ?>
|
||||
<p style="margin:0.25rem 0;"><strong>Category:</strong> <?= esc($eventCategory) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($amount !== null): ?>
|
||||
<p style="margin:0.25rem 0;"><strong>Amount:</strong> $<?= esc($amount) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($event['description'])): ?>
|
||||
<p style="margin:0.75rem 0;"><?= nl2br(esc((string) $event['description'])) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($flyerUrl)): ?>
|
||||
<div style="margin:1rem 0;">
|
||||
<img
|
||||
src="<?= esc((string) $flyerUrl) ?>"
|
||||
alt="Event flyer for <?= esc($eventName) ?>"
|
||||
style="display:block; max-width:100%; height:auto; border:1px solid #ddd; border-radius:8px;"
|
||||
>
|
||||
</div>
|
||||
<p style="margin:0.75rem 0;">
|
||||
Flyer:
|
||||
<a href="<?= esc((string) $flyerUrl) ?>"><?= esc((string) $flyerUrl) ?></a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<p style="margin:0.75rem 0;">For more details, please contact the school office or review the parent event page.</p>
|
||||
<p style="margin:0.25rem 0;">
|
||||
<a href="<?= esc((string) $eventUrl) ?>"><?= esc((string) $eventUrl) ?></a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -80,10 +80,10 @@
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= esc($e['approver_firstname'] . ' ' . $e['approver_lastname'] ?? '-') ?></td>
|
||||
<!--td><--?= esc($e['status_reason'] ?? '-') ?></td-->
|
||||
<!-- <td><?= esc($e['status_reason'] ?? '-') ?></td> -->
|
||||
<td>
|
||||
<?php if ($e['receipt_path']): ?>
|
||||
<!--a href="<--?= base_url('writable/' . $e['receipt_path']) ?>" target="_blank">View</a-->
|
||||
<!-- <a href="<?= base_url('writable/' . $e['receipt_path']) ?>" target="_blank">View</a> -->
|
||||
<a href="<?= site_url('receipts/' . $e['receipt_path']) ?>" target="_blank">View receipt</a>
|
||||
<?php else: ?>
|
||||
N/A
|
||||
|
||||
@@ -26,22 +26,22 @@
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-1.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-2.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-3.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-4.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-5.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-6.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,8 @@ $todayYmd = local_date(utc_now(), 'Y-m-d');
|
||||
<tr>
|
||||
<th class="sticky-col">Grade</th>
|
||||
<th class="sticky-col-2" style="min-width:300px; width:300px;">Teacher & TAs</th>
|
||||
<?php foreach (($headerDates ?? []) as $i => $label): $ymd = $sundays[$i] ?? ''; ?>
|
||||
<th class="text-center">HW Submitted</th>
|
||||
<?php foreach (($headerDates ?? []) as $i => $label): $ymd = $sundays[$i] ?? ''; ?>
|
||||
<?php $isCal = !empty($eventDays[$ymd]); $isFuture = ($ymd > $todayYmd); ?>
|
||||
<th class="text-center <?= $isCal ? 'bg-warning' : ($isFuture ? 'bg-future' : '') ?>" title="<?= esc($ymd) ?>"><?= esc($label) ?></th>
|
||||
<?php endforeach; ?>
|
||||
@@ -100,6 +101,9 @@ $todayYmd = local_date(utc_now(), 'Y-m-d');
|
||||
<?php endif; ?>
|
||||
<div><strong>TA<?= count($taNames) !== 1 ? 's' : '' ?>:</strong> <?= !empty($taNames) ? esc(implode(', ', $taNames)) : '—' ?></div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?= (int)($homeworkSubmissionCounts[$csid] ?? 0) ?>
|
||||
</td>
|
||||
<?php foreach (($sundays ?? []) as $ymd): ?>
|
||||
<?php if (!empty($eventDays[$ymd])): ?>
|
||||
<td class="bg-warning text-center">—</td>
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
<strong>Enrollment deadline:</strong>
|
||||
<?= $lastDayOfRegistration ? local_date($lastDayOfRegistration, 'm-d-Y') : 'N/A' ?>
|
||||
</div>
|
||||
<!--div><strong>Withdrawal:</strong> <--?= esc($withdrawalDeadline ?? 'N/A') ?></div-->
|
||||
<!-- <div><strong>Withdrawal:</strong> <?= esc($withdrawalDeadline ?? 'N/A') ?></div> -->
|
||||
<hr class="my-2">
|
||||
<?php if (!empty($enrollments)): ?>
|
||||
<ul class="mb-0 ps-3">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<!--meta name="csrf-token" content="<--?= csrf_hash() ?>"-->
|
||||
<!-- <meta name="csrf-token" content="<?= csrf_hash() ?>"> -->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Al Rahma Sunday School</title>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Meta Information -->
|
||||
<!--meta name="csrf-token" content="<--?= csrf_hash() ?>"-->
|
||||
<!-- <meta name="csrf-token" content="<?= csrf_hash() ?>"> -->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Prevent back/forward cache issues on management pages -->
|
||||
|
||||
@@ -42,9 +42,6 @@
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Event Description -->
|
||||
<p class="card-text"><?= esc($event['description']) ?></p>
|
||||
|
||||
<!-- Participation Table -->
|
||||
<form method="post" action="<?= site_url('parent/updateParticipation') ?>">
|
||||
<?= csrf_field() ?>
|
||||
@@ -53,11 +50,13 @@
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Student First Name</th>
|
||||
<th>Student Last Name</th>
|
||||
<th class="text-center">Participate</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Student First Name</th>
|
||||
<th>Student Last Name</th>
|
||||
<th class="text-center">Participate</th>
|
||||
<th>Description</th>
|
||||
<th>Event Fees</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($yourStudents as $student): ?>
|
||||
@@ -84,6 +83,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><?= esc($event['description'] ?: 'No description') ?></td>
|
||||
<td class="text-nowrap">$<?= esc(number_format((float) ($event['amount'] ?? 0), 2)) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
@@ -143,12 +143,67 @@ $deadline = parseDbDateTime($dueDate, 'UTC', $displayTz); // format either DATE
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (!empty($invoiceEventCharges)): ?>
|
||||
<?php foreach ($invoices as $invoice): ?>
|
||||
<?php $charges = $invoiceEventCharges[(int)($invoice['id'] ?? 0)] ?? []; ?>
|
||||
<?php if (empty($charges)): ?>
|
||||
<?php continue; ?>
|
||||
<?php endif; ?>
|
||||
<div class="card mb-3 shadow-sm">
|
||||
<div class="card-header">
|
||||
<strong>Event charges for Invoice #<?= esc($invoice['invoice_number'] ?? '#'.$invoice['id']) ?></strong>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive mb-0">
|
||||
<table class="table table-sm table-bordered mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Student</th>
|
||||
<th>Fee</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($charges as $charge): ?>
|
||||
<?php
|
||||
$studentName = trim(($charge['student_firstname'] ?? '') . ' ' . ($charge['student_lastname'] ?? ''));
|
||||
if ($studentName === '') {
|
||||
$externalName = trim(($charge['external_firstname'] ?? '') . ' ' . ($charge['external_lastname'] ?? ''));
|
||||
if ($externalName !== '') {
|
||||
$studentName = $externalName . ' (external)';
|
||||
}
|
||||
}
|
||||
$eventName = $charge['event_name'] ?? '—';
|
||||
$paid = !empty($charge['event_paid']) || ((float)($charge['charged'] ?? 0) <= 0);
|
||||
$badgeClass = $paid ? 'success' : 'danger';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($eventName) ?></td>
|
||||
<td><?= esc($studentName ?: '—') ?></td>
|
||||
<td>$<?= number_format((float)($charge['event_amount'] ?? 0), 2) ?></td>
|
||||
<td>
|
||||
<span class="badge bg-<?= $badgeClass ?>">
|
||||
<?= $paid ? 'Paid' : 'Unpaid' ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info mb-3 d-inline-block">
|
||||
No invoice found for the selected school year.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php $invoiceEventCharges = $invoiceEventCharges ?? []; ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
|
||||
@@ -11,7 +11,7 @@ $disableEmergencyBtn = count($emergencies) >= $maxEmergency;
|
||||
<div class="alert alert-info mt-2">You've reached the maximum number of students (<?= $maxChilds ?>).</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($disableEmergencyBtn): ?>
|
||||
<!--div class="alert alert-info mt-2">You've reached the maximum number of emergency contacts (<--?= $maxEmergency ?>).</div-->
|
||||
<!-- <div class="alert alert-info mt-2">You've reached the maximum number of emergency contacts (<?= esc((string) $maxEmergency) ?>).</div> -->
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="alert alert-danger">
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
<button class="btn btn-primary" onclick="window.print()">Print Report</button>
|
||||
<a id="summaryReportLink" href="<?= base_url('financial-report/financialReportSummary') ?>" class="btn btn-info">Display Summary Report</a>
|
||||
</div>
|
||||
<?php if (isset($eventFeesTotal)): ?>
|
||||
<div class="alert alert-info mb-3">
|
||||
<strong>Event fees total:</strong> $<?= number_format((float)$eventFeesTotal, 2) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="table-responsive">
|
||||
<table id="invoicesTable" class="table table-bordered table-striped align-middle w-100">
|
||||
<thead>
|
||||
@@ -95,8 +100,16 @@
|
||||
$balanceCalc = (float)$totalAmount - (float)$discounted - (float)$paid - (float)$refunded;
|
||||
if ($balanceCalc < 0) $balanceCalc = 0.0;
|
||||
$balance = $balanceCalc;
|
||||
$status = ($balanceCalc === 0.0) ? 'Paid' : 'Unpaid';
|
||||
$statusClass = ($status === 'Paid') ? 'bg-success' : 'bg-danger';
|
||||
if ($balanceCalc <= 0.00001) {
|
||||
$status = 'Paid';
|
||||
$statusClass = 'bg-success';
|
||||
} elseif ($paid > 0.00001) {
|
||||
$status = 'Partially Paid';
|
||||
$statusClass = 'bg-warning text-dark';
|
||||
} else {
|
||||
$status = 'Unpaid';
|
||||
$statusClass = 'bg-danger';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($inv['invoice_number']) ?></td>
|
||||
@@ -371,8 +384,15 @@
|
||||
const total = Number(inv.total_amount || 0);
|
||||
let balance = total - paid - discounted - refunded;
|
||||
if (!Number.isFinite(balance) || balance < 0) balance = 0;
|
||||
const status = balance === 0 ? 'Paid' : 'Unpaid';
|
||||
const statusClass = balance === 0 ? 'bg-success' : 'bg-danger';
|
||||
let status = 'Unpaid';
|
||||
let statusClass = 'bg-danger';
|
||||
if (balance <= 0.00001) {
|
||||
status = 'Paid';
|
||||
statusClass = 'bg-success';
|
||||
} else if (paid > 0.00001) {
|
||||
status = 'Partially Paid';
|
||||
statusClass = 'bg-warning text-dark';
|
||||
}
|
||||
const tr = document.createElement('tr');
|
||||
const parentHtml = (inv.parent_id && Number(inv.parent_id) > 0) ?
|
||||
`<a href="<?= site_url('family') ?>?guardian_id=${encodeURIComponent(inv.parent_id)}" class="text-decoration-none" data-family-guardian-id="${inv.parent_id}">${inv.parent_name||''}</a>` :
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
</thead>
|
||||
<tbody id="summaryBody">
|
||||
<tr><td>Total Charges</td><td class="text-right" id="sumCharges">$0.00</td></tr>
|
||||
<tr><td>Event Fees</td><td class="text-right" id="sumEventFees">$0.00</td></tr>
|
||||
<tr><td>Total Extra Charges</td><td class="text-right" id="sumExtraCharges">$0.00</td></tr>
|
||||
<tr><td>Total Discounts</td><td class="text-right" id="sumDiscounts">$0.00</td></tr>
|
||||
<tr><td>Total Refunds</td><td class="text-right" id="sumRefunds">$0.00</td></tr>
|
||||
@@ -98,6 +99,9 @@ function loadSummary(){
|
||||
if (!d || d.ok !== true) return;
|
||||
document.getElementById('summaryPeriod').textContent = 'Report for School Year: ' + (d.schoolYear||'');
|
||||
document.getElementById('sumCharges').textContent = fmt(d.totalCharges);
|
||||
if (document.getElementById('sumEventFees')) {
|
||||
document.getElementById('sumEventFees').textContent = fmt(d.totalEventFees || 0);
|
||||
}
|
||||
if (document.getElementById('sumExtraCharges')) {
|
||||
document.getElementById('sumExtraCharges').textContent = fmt(d.totalExtraCharges || 0);
|
||||
}
|
||||
@@ -127,10 +131,10 @@ function renderCharts(d){
|
||||
const summaryCtx = document.getElementById('summaryChart').getContext('2d');
|
||||
window._summaryChart = new Chart(summaryCtx, {
|
||||
type: 'bar',
|
||||
data: { labels: ['Charges','Paid','Unpaid','Discounts','Refunds','Expenses','Reimbursements','Net'],
|
||||
data: { labels: ['Charges','Event Fees','Paid','Unpaid','Discounts','Refunds','Expenses','Reimbursements','Net'],
|
||||
datasets: [{ label:'Amount (USD)', data:[
|
||||
d.totalCharges||0, d.totalPaid||0, d.totalUnpaid||0, d.totalDiscounts||0, d.totalRefunds||0, d.totalExpenses||0, d.totalReimbursements||0, d.netAmount||0
|
||||
], backgroundColor: ['#007bff','#28a745','#ffc107','#17a2b8','#ffc107','#dc3545','#6f42c1','#20c997']}] },
|
||||
d.totalCharges||0, d.totalEventFees||0, d.totalPaid||0, d.totalUnpaid||0, d.totalDiscounts||0, d.totalRefunds||0, d.totalExpenses||0, d.totalReimbursements||0, d.netAmount||0
|
||||
], backgroundColor: ['#007bff','#6610f2','#28a745','#ffc107','#17a2b8','#ffc107','#dc3545','#6f42c1','#20c997']}] },
|
||||
options: { responsive:true, scales:{ y:{ beginAtZero:true }}}
|
||||
});
|
||||
|
||||
@@ -177,11 +181,12 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
window._summaryChart = new Chart(summaryCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Charges', 'Paid', 'Unpaid', 'Discounts', 'Refunds', 'Expenses', 'Reimbursements', 'Net'],
|
||||
labels: ['Charges', 'Event Fees', 'Paid', 'Unpaid', 'Discounts', 'Refunds', 'Expenses', 'Reimbursements', 'Net'],
|
||||
datasets: [{
|
||||
label: 'Amount (USD)',
|
||||
data: [
|
||||
<?= (float)$totalCharges ?>,
|
||||
<?= (float)($totalEventFees ?? 0) ?>,
|
||||
<?= (float)$totalPaid ?>,
|
||||
<?= (float)$totalUnpaid ?>,
|
||||
<?= (float)$totalDiscounts ?>,
|
||||
@@ -191,7 +196,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
<?= (float)$netAmount ?>
|
||||
],
|
||||
backgroundColor: [
|
||||
'#007bff', '#28a745', '#ffc107', '#17a2b8', '#ffc107', '#dc3545', '#6f42c1', '#20c997'
|
||||
'#007bff', '#6610f2', '#28a745', '#ffc107', '#17a2b8', '#ffc107', '#dc3545', '#6f42c1', '#20c997'
|
||||
]
|
||||
}]
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
<button class="btn btn-primary btn-sm" type="submit">Send Reminders (All)</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php $sumEventFees = 0.0; ?>
|
||||
<div class="table-responsive">
|
||||
<table id="unpaidTable" class="table table-sm align-middle no-mgmt-sticky" data-no-mgmt-sticky>
|
||||
<thead>
|
||||
@@ -54,6 +55,7 @@
|
||||
<th>Parent</th>
|
||||
<th class="text-center">Nbr of Installements</th>
|
||||
<th>Type</th>
|
||||
<th class="text-end">Event Fees</th>
|
||||
<th class="text-end">Invoice Amount</th>
|
||||
<th class="text-end">Applied Discount</th>
|
||||
<th class="text-end">Paid Amount</th>
|
||||
@@ -67,7 +69,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($rows)): ?>
|
||||
<tr><td colspan="12" class="text-center text-muted py-4">No parents with outstanding balance.</td></tr>
|
||||
<tr><td colspan="13" class="text-center text-muted py-4">No parents with outstanding balance.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$sumInvoice = 0.0;
|
||||
@@ -86,18 +88,20 @@
|
||||
</td>
|
||||
<td class="text-center"><?= (int)($r['payment_count'] ?? 0) ?></td>
|
||||
<td>
|
||||
<?php if (($r['type'] ?? '') === 'no_payment'): ?>
|
||||
<span class="badge bg-danger badge-type">no payment</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-success badge-type">installment</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php $sumInvoice += (float)($r['total_invoice'] ?? 0); ?>
|
||||
<?php $sumPaid += (float)($r['total_paid'] ?? 0); ?>
|
||||
<?php if (($r['type'] ?? '') === 'no_payment'): ?>
|
||||
<span class="badge bg-danger badge-type">no payment</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-success badge-type">installment</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php $sumInvoice += (float)($r['total_invoice'] ?? 0); ?>
|
||||
<?php $sumEventFees += (float)($r['event_fees'] ?? 0); ?>
|
||||
<?php $sumPaid += (float)($r['total_paid'] ?? 0); ?>
|
||||
<?php $sumDisc += (float)($r['total_discount'] ?? 0); ?>
|
||||
<?php $sumInstAmt += (float)($r['installment_amount'] ?? 0); ?>
|
||||
<?php $sumBal += (float)($r['total_balance'] ?? 0); ?>
|
||||
<td class="text-end">$<?= number_format((float)($r['total_invoice'] ?? 0), 2) ?></td>
|
||||
<td class="text-end">$<?= number_format((float)($r['event_fees'] ?? 0), 2) ?></td>
|
||||
<td class="text-end">$<?= number_format((float)($r['total_invoice'] ?? 0), 2) ?></td>
|
||||
<td class="text-end text-success">-$<?= number_format((float)($r['total_discount'] ?? 0), 2) ?></td>
|
||||
<td class="text-end">$<?= number_format((float)($r['total_paid'] ?? 0), 2) ?></td>
|
||||
<td class="text-center"><?= (int)($r['remaining_installments'] ?? 0) ?></td>
|
||||
@@ -129,6 +133,7 @@
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="3" class="text-end">Totals:</th>
|
||||
<th class="text-end">$<?= number_format($sumEventFees, 2) ?></th>
|
||||
<th class="text-end">$<?= number_format($sumInvoice, 2) ?></th>
|
||||
<th class="text-end text-success">-$<?= number_format($sumDisc, 2) ?></th>
|
||||
<th class="text-end">$<?= number_format($sumPaid, 2) ?></th>
|
||||
|
||||
@@ -22,16 +22,18 @@
|
||||
<input type="tel" name="phone" class="form-control" value="<?= esc($user['phone'] ?? '') ?>" placeholder="Enter phone number">
|
||||
</div>
|
||||
|
||||
<!--div class="mb-3">
|
||||
<!--
|
||||
<div class="mb-3">
|
||||
<label>Role</label>
|
||||
<select name="role_id" class="form-control" required>
|
||||
<--?php foreach ($roles as $role): ?>
|
||||
<option value="<--?= $role['id'] ?>" <--?= $roleId == $role['id'] ? 'selected' : '' ?>>
|
||||
<--?= esc($role['name']) ?>
|
||||
<?php foreach ($roles as $role): ?>
|
||||
<option value="<?= $role['id'] ?>" <?= $roleId == $role['id'] ? 'selected' : '' ?>>
|
||||
<?= esc($role['name']) ?>
|
||||
</option>
|
||||
<--?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div-->
|
||||
</div>
|
||||
-->
|
||||
<button type="submit" class="btn btn-primary">Update</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!--a href="<--?= site_url('staff/create') ?>" class="btn btn-primary mb-3">Add Staff</a-->
|
||||
<!-- <a href="<?= site_url('staff/create') ?>" class="btn btn-primary mb-3">Add Staff</a> -->
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="myTable" class="display table table-striped table-bordered table-hover w-100" style="width:100%">
|
||||
|
||||
@@ -77,39 +77,25 @@
|
||||
<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">
|
||||
<strong class="mb-0">Date Selection</strong>
|
||||
<?php if ($isEdit): ?>
|
||||
<div class="text-muted small">
|
||||
<?php
|
||||
try {
|
||||
$displayStart = (new \DateTime($weekStartSelected))->format('M d, Y');
|
||||
} catch (\Exception $e) {
|
||||
$displayStart = $weekStartSelected;
|
||||
}
|
||||
?>
|
||||
Week of <?= esc($displayStart ?: 'N/A') ?>
|
||||
</div>
|
||||
<input type="hidden" name="week_start" value="<?= esc($weekStartSelected) ?>">
|
||||
<?php else: ?>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<select id="weekStartSelect" name="week_start" class="form-select form-select-sm" required>
|
||||
<option value="">Select week</option>
|
||||
<?php foreach ($sundayOptions as $sunday): ?>
|
||||
<?php
|
||||
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 class="d-flex align-items-center gap-2">
|
||||
<select id="weekStartSelect" name="week_start" class="form-select form-select-sm" required <?= $isEdit ? 'data-original-week="' . esc($weekStartSelected) . '"' : '' ?>>
|
||||
<option value="">Select week</option>
|
||||
<?php foreach ($sundayOptions as $sunday): ?>
|
||||
<?php
|
||||
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>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input type="hidden" name="week_end" id="weekEndInput" value="<?= esc($weekEndValue) ?>" required>
|
||||
@@ -197,6 +183,14 @@
|
||||
</div>
|
||||
<div class="form-text small text-muted">Add a custom Surah or Arabic target.</div>
|
||||
</div>
|
||||
<?php elseif ($slug === 'islamic'): ?>
|
||||
<div class="px-2 py-2 border-top">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" placeholder="Type subject or topic" data-custom-input data-subject="<?= esc($slug) ?>">
|
||||
<button type="button" class="btn btn-outline-secondary" data-custom-entry data-subject="<?= esc($slug) ?>">Add</button>
|
||||
</div>
|
||||
<div class="form-text small text-muted">Add a subject or unit not listed above (e.g. Seerah, Fiqh, Akhlaq).</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,6 +300,27 @@
|
||||
const forms = document.querySelectorAll('.needs-validation');
|
||||
Array.from(forms).forEach(form => {
|
||||
form.addEventListener('submit', event => {
|
||||
const originalWeek = weekStartSelect?.dataset.originalWeek || '';
|
||||
if (originalWeek && weekStartSelect && weekStartSelect.value && weekStartSelect.value !== originalWeek) {
|
||||
const ok = confirm('A progress report already exists for the original week. Change the week and override any existing report for the new date?');
|
||||
if (!ok) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
const confirmInput = document.getElementById('confirmOverwriteInput');
|
||||
if (confirmInput) {
|
||||
confirmInput.value = '1';
|
||||
}
|
||||
}
|
||||
const islamicUnits = form.querySelectorAll('input[name="unit_islamic[]"]');
|
||||
const hasIslamicUnit = Array.from(islamicUnits).some(input => input.value.trim() !== '');
|
||||
if (!hasIslamicUnit) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
alert('Please select at least one Islamic Studies unit.');
|
||||
return;
|
||||
}
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -378,6 +393,9 @@
|
||||
if (isQuran) {
|
||||
return isCustom ? 'Custom' : 'Surah';
|
||||
}
|
||||
if (subject === 'islamic' && isCustom) {
|
||||
return 'Custom';
|
||||
}
|
||||
return parts.join(' – ');
|
||||
};
|
||||
|
||||
@@ -438,7 +456,10 @@
|
||||
if (!input) return;
|
||||
const customValue = input.value.trim();
|
||||
if (customValue === '') return;
|
||||
const unitValue = buildUnitDisplay(subject, '', '', { isQuran: subject === 'quran', isCustom: true });
|
||||
const unitValue = buildUnitDisplay(subject, '', '', {
|
||||
isQuran: subject === 'quran',
|
||||
isCustom: subject === 'quran' || subject === 'islamic',
|
||||
});
|
||||
appendUnitChapterRow(subject, unitValue, customValue);
|
||||
input.value = '';
|
||||
hideMenus();
|
||||
@@ -461,6 +482,10 @@
|
||||
confirmButton.addEventListener('click', () => {
|
||||
confirmInput.value = '1';
|
||||
const form = confirmButton.closest('form') || document.querySelector('form.needs-validation');
|
||||
if (form && typeof form.requestSubmit === 'function') {
|
||||
form.requestSubmit();
|
||||
return;
|
||||
}
|
||||
if (form) {
|
||||
form.submit();
|
||||
}
|
||||
|
||||
@@ -1,34 +1,307 @@
|
||||
<?= $this->extend('layout/main_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<div class="container-xxl py-5">
|
||||
<?php
|
||||
$statusBadges = $statusBadges ?? [];
|
||||
$drafts = $drafts ?? [];
|
||||
$legacyExams = $legacyExams ?? [];
|
||||
$assignments = $assignments ?? [];
|
||||
$selectedClassSection = $selectedClassSection ?? 0;
|
||||
$examTypes = $examTypes ?? [];
|
||||
$schoolYear = $schoolYear ?? '';
|
||||
$semester = $semester ?? '';
|
||||
$maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
?>
|
||||
<style>
|
||||
.teacher-drafts-page,
|
||||
.teacher-drafts-page * {
|
||||
font-family: Arial, sans-serif !important;
|
||||
}
|
||||
.teacher-drafts-table {
|
||||
table-layout: auto;
|
||||
width: max-content;
|
||||
}
|
||||
.teacher-drafts-table th {
|
||||
min-width: 0;
|
||||
}
|
||||
.teacher-drafts-table td {
|
||||
max-width: none;
|
||||
}
|
||||
.teacher-drafts-table th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.teacher-drafts-table td.title-cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
<div class="container-xxl py-5 teacher-drafts-page">
|
||||
<div class="container">
|
||||
<h1>Drafts</h1>
|
||||
<?php if (!empty($draftMessages)): ?>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4">
|
||||
<h1 class="mb-0">Exam drafts</h1>
|
||||
<?php if (!empty($legacyExams)): ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="legacyToggle">Legacy Exams</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<?= form_open_multipart(base_url('teacher/exam-drafts'), ['class' => 'row g-3', 'id' => 'examDraftForm']) ?>
|
||||
<?= csrf_field() ?>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="exam_type">Exam type <span class="text-danger">*</span></label>
|
||||
<select name="exam_type" id="exam_type" class="form-select" required>
|
||||
<option value="">— Select —</option>
|
||||
<?php foreach ($examTypes as $t): ?>
|
||||
<option value="<?= esc($t) ?>" <?= old('exam_type') === $t ? 'selected' : '' ?>><?= esc($t) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<label class="form-label" for="author_comment">Author comment</label>
|
||||
<textarea name="author_comment" id="author_comment" class="form-control" rows="2" placeholder="Optional note for reviewers"><?= esc(old('author_comment') ?? '') ?></textarea>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="draft_file">File (Word) <span class="text-danger">*</span></label>
|
||||
<input type="file" name="draft_file" id="draft_file" class="form-control" accept=".doc,.docx" required>
|
||||
<div class="form-text">Max <?= esc(number_format($maxUploadBytes / 1048576, 1)) ?> MB.</div>
|
||||
</div>
|
||||
<div class="col-12 d-flex flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-primary">Submit for review</button>
|
||||
</div>
|
||||
<?= form_close() ?>
|
||||
<?php if ($schoolYear !== '' || $semester !== ''): ?>
|
||||
<p class="text-muted small mb-0 mt-2">School year: <?= esc($schoolYear) ?> · Semester: <?= esc($semester) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$renderBadge = static function (string $status, array $badges): string {
|
||||
$b = $badges[$status] ?? ['label' => $status, 'class' => 'bg-secondary text-white'];
|
||||
$style = !empty($b['style']) ? ' style="' . esc($b['style']) . '"' : '';
|
||||
return '<span class="badge ' . esc($b['class']) . ' js-status-badge" data-status="' . esc($status) . '"' . $style . '>' . esc($b['label']) . '</span>';
|
||||
};
|
||||
?>
|
||||
|
||||
<h2 class="h5 mb-3">Your submissions</h2>
|
||||
<?php if (!empty($drafts)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<table class="table table-striped align-middle teacher-drafts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Subject</th>
|
||||
<th>Date</th>
|
||||
<th>Actions</th>
|
||||
<th>Class</th>
|
||||
<th>Title / type</th>
|
||||
<th>Ver.</th>
|
||||
<th>Status</th>
|
||||
<th>File link</th>
|
||||
<th>Reviewer comment</th>
|
||||
<th>Last Update</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($draftMessages as $message): ?>
|
||||
<tr>
|
||||
<td><?= esc($message['subject']) ?></td>
|
||||
<td><?= esc(!empty($message['created_at']) ? local_datetime($message['created_at'], 'm-d-Y H:i') : '') ?></td>
|
||||
<?php foreach ($drafts as $d): ?>
|
||||
<?php
|
||||
$st = strtolower((string) ($d['status'] ?? ''));
|
||||
$badgeHtml = $renderBadge($st, $statusBadges);
|
||||
?>
|
||||
<tr data-draft-id="<?= (int) ($d['id'] ?? 0) ?>">
|
||||
<td><?= esc($d['class_section_name'] ?? '') ?></td>
|
||||
<td class="title-cell"><?= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?></td>
|
||||
<td><?= esc((string) ($d['version'] ?? '')) ?></td>
|
||||
<td><?= $badgeHtml ?></td>
|
||||
<td>
|
||||
<a href="<?= base_url('messages/edit/' . $message['id']) ?>">Edit</a> |
|
||||
<a href="<?= base_url('messages/delete/' . $message['id']) ?>">Delete</a>
|
||||
<?php $revNumber = max(1, (int) ($d['version'] ?? 1)); ?>
|
||||
<?php if (!empty($d['teacher_file'])): ?>
|
||||
<div>
|
||||
<a href="<?= base_url('exam-drafts/files/teacher/' . $d['teacher_file']) ?>" target="_blank" rel="noopener">
|
||||
<?= esc('Ver' . $revNumber . ' ' . ($d['teacher_filename'] ?? 'Submitted draft')) ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($d['final_file'])): ?>
|
||||
<div class="mt-1">
|
||||
<a href="<?= base_url('exam-drafts/files/final/' . $d['final_file']) ?>" target="_blank" rel="noopener" class="link-success small">
|
||||
<?= esc('Final ' . ($d['final_filename'] ?? 'Final draft')) ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($d['review_files'])): ?>
|
||||
<div class="mt-1">
|
||||
<?php
|
||||
$reviewLinks = [];
|
||||
foreach ($d['review_files'] as $rf) {
|
||||
$rev = max(1, (int) ($rf['review_revision'] ?? 1));
|
||||
$name = $rf['final_filename'] ?? 'Review file';
|
||||
$file = $rf['final_file'] ?? '';
|
||||
if ($file !== '') {
|
||||
$reviewLinks[] = '<a href="' . esc(base_url('exam-drafts/files/final/' . $file)) . '" target="_blank" rel="noopener" class="link-success small">' . esc('Ver' . $revNumber . '_' . $rev . ' ' . $name) . '</a>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?= !empty($reviewLinks) ? implode(' | ', $reviewLinks) : '' ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($d['teacher_file']) && empty($d['final_file'])): ?>
|
||||
—
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= nl2br(esc($d['reviewer_comment'] ?? $d['reviewer_comments'] ?? $d['admin_comments'] ?? '—')) ?></td>
|
||||
<td><?= esc(!empty($d['updated_at']) ? local_datetime($d['updated_at'], 'm-d-Y H:i') : '') ?></td>
|
||||
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p>No drafts found.</p>
|
||||
<p class="text-muted">No submissions yet.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($legacyExams)): ?>
|
||||
<div id="legacySection" class="table-responsive d-none mt-4">
|
||||
<table class="table table-sm table-striped align-middle teacher-drafts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Title</th>
|
||||
<th>Ver.</th>
|
||||
<th>Status</th>
|
||||
<th>School year</th>
|
||||
<th>Semester</th>
|
||||
<th>Exam type</th>
|
||||
<th>Updated</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($legacyExams as $d): ?>
|
||||
<?php $st = strtolower((string) ($d['status'] ?? '')); ?>
|
||||
<tr>
|
||||
<td><?= esc($d['class_section_name'] ?? '') ?></td>
|
||||
<td class="title-cell"><?= esc($d['draft_title'] ?? $d['exam_type'] ?? '') ?></td>
|
||||
<td><?= esc((string) ($d['version'] ?? '')) ?></td>
|
||||
<td><?= $renderBadge($st, $statusBadges) ?></td>
|
||||
<td><?= esc($d['school_year'] ?? '') ?></td>
|
||||
<td><?= esc($d['semester'] ?? '') ?></td>
|
||||
<td><?= esc($d['exam_type'] ?? '') ?></td>
|
||||
<td><?= esc(!empty($d['updated_at']) ? local_datetime($d['updated_at'], 'm-d-Y H:i') : '') ?></td>
|
||||
<td>
|
||||
<?php if (!empty($d['final_file'])): ?>
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('exam-drafts/files/final/' . $d['final_file']) ?>" target="_blank" rel="noopener">View</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const csrfTokenName = <?= json_encode(csrf_token(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||
const csrfCookieNames = <?= json_encode(array_values(array_filter([
|
||||
config('Security')->csrfCookieName ?? null,
|
||||
config('Security')->cookieName ?? null,
|
||||
'csrf_cookie_name',
|
||||
])), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||
|
||||
const readCookie = (name) => {
|
||||
const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[$()*+.?[\\\]^{|}-]/g, '\\$&') + '=([^;]*)'));
|
||||
return match ? decodeURIComponent(match[1]) : '';
|
||||
};
|
||||
|
||||
const syncCsrfToken = (form) => {
|
||||
let tokenValue = '';
|
||||
csrfCookieNames.some((name) => {
|
||||
tokenValue = readCookie(name);
|
||||
return tokenValue !== '';
|
||||
});
|
||||
if (!tokenValue || !form) {
|
||||
return;
|
||||
}
|
||||
const tokenInput = form.querySelector(`input[name="${csrfTokenName}"]`);
|
||||
if (tokenInput) {
|
||||
tokenInput.value = tokenValue;
|
||||
}
|
||||
};
|
||||
|
||||
const draftForm = document.getElementById('examDraftForm');
|
||||
if (draftForm) {
|
||||
draftForm.addEventListener('submit', () => syncCsrfToken(draftForm));
|
||||
}
|
||||
|
||||
const statusBadges = <?= json_encode($statusBadges, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
|
||||
const rows = new Map();
|
||||
document.querySelectorAll('tr[data-draft-id]').forEach((row) => {
|
||||
const id = row.getAttribute('data-draft-id');
|
||||
if (id) {
|
||||
rows.set(id, row);
|
||||
}
|
||||
});
|
||||
|
||||
const updateRow = (row, status, acceptanceType) => {
|
||||
const badge = row.querySelector('.js-status-badge');
|
||||
if (badge) {
|
||||
const badgeData = statusBadges[status] || { label: status, class: 'bg-secondary text-white' };
|
||||
badge.textContent = badgeData.label || status;
|
||||
badge.className = `badge ${badgeData.class} js-status-badge`;
|
||||
if (badgeData.style) {
|
||||
badge.setAttribute('style', badgeData.style);
|
||||
} else {
|
||||
badge.removeAttribute('style');
|
||||
}
|
||||
badge.dataset.status = status;
|
||||
}
|
||||
const note = row.querySelector('.js-acceptance-note');
|
||||
if (note) {
|
||||
if (status === 'accepted' && acceptanceType) {
|
||||
note.textContent = acceptanceType === 'minor_edits' ? 'With minor edits' : 'As is';
|
||||
} else {
|
||||
note.textContent = '—';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const poll = () => {
|
||||
fetch('<?= base_url('teacher/exam-drafts/status') ?>', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then((resp) => resp.ok ? resp.json() : null)
|
||||
.then((data) => {
|
||||
if (!data || !Array.isArray(data.drafts)) {
|
||||
return;
|
||||
}
|
||||
data.drafts.forEach((draft) => {
|
||||
const row = rows.get(String(draft.id));
|
||||
if (row) {
|
||||
updateRow(row, draft.status, draft.acceptance_type);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
if (rows.size > 0) {
|
||||
poll();
|
||||
setInterval(poll, 15000);
|
||||
}
|
||||
|
||||
const legacyToggle = document.getElementById('legacyToggle');
|
||||
const legacySection = document.getElementById('legacySection');
|
||||
if (legacyToggle && legacySection) {
|
||||
legacyToggle.addEventListener('click', () => {
|
||||
legacySection.classList.toggle('d-none');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -137,6 +137,9 @@
|
||||
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank">
|
||||
<?= esc($draft['teacher_filename'] ?? 'Download submitted file') ?>
|
||||
</a>
|
||||
<a href="<?= base_url('exam-drafts/files/teacher/' . $draft['teacher_file']) ?>" target="_blank" class="btn btn-sm btn-outline-secondary ms-2">
|
||||
View
|
||||
</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-muted small">No file uploaded</div>
|
||||
@@ -152,6 +155,9 @@
|
||||
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" class="link-success small">
|
||||
<?= esc($draft['final_filename'] ?? 'Download final draft') ?>
|
||||
</a>
|
||||
<a href="<?= base_url('exam-drafts/files/final/' . $draft['final_file']) ?>" target="_blank" class="btn btn-sm btn-outline-secondary ms-2">
|
||||
View
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
@@ -199,6 +205,9 @@
|
||||
<a href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||
Download
|
||||
</a>
|
||||
<a href="<?= base_url('exam-drafts/files/final/' . $item['final_file']) ?>" target="_blank" class="btn btn-sm btn-outline-secondary ms-2">
|
||||
View
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">File missing</span>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -308,6 +308,7 @@
|
||||
id="submitScoresLockBtn"
|
||||
formaction="<?= base_url('/teacher/submit-scores-lock') ?>"
|
||||
formmethod="post"
|
||||
data-confirm-message="Once you submit, you cannot add any scores or comments. Do you want to continue?"
|
||||
<?= $scoresLocked ? 'disabled' : '' ?>>
|
||||
<?= $scoresLocked ? 'Scores Locked' : 'Submit Semester Scores' ?>
|
||||
</button>
|
||||
@@ -430,6 +431,14 @@
|
||||
|
||||
form.addEventListener('submit', function(event) {
|
||||
syncCsrfToken();
|
||||
const submitter = event.submitter;
|
||||
if (submitter && submitter.id === 'submitScoresLockBtn') {
|
||||
const message = submitter.dataset.confirmMessage || 'Submit semester scores?';
|
||||
if (!confirm(message)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const errors = [];
|
||||
form.querySelectorAll('textarea[data-first-name]').forEach(function(field) {
|
||||
const value = field.value.trim();
|
||||
|
||||
@@ -194,22 +194,22 @@
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-1.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-2.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-3.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-4.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-5.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('"assets/images/classes-6.jpg') ?>" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -279,4 +279,4 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -128,22 +128,22 @@
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-1.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-2.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-3.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-4.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-5.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-6.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,4 +200,4 @@
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -180,22 +180,22 @@
|
||||
<h3 class="text-white mb-4">Photo Gallery</h3>
|
||||
<div class="row g-2 pt-2">
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-1.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-1.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-2.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-2.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-3.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-3.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-4.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-4.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-5.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-5.jpg') ?>" alt="">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<img class="img-fluid rounded bg-light p-1" src=""assets/images/classes-6.jpg" alt="">
|
||||
<img class="img-fluid rounded bg-light p-1" src="<?= base_url('assets/images/classes-6.jpg') ?>" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,4 +252,4 @@
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
<?php
|
||||
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
|
||||
if (!function_exists('base_url')) {
|
||||
throw new PageNotFoundException('base_url function not found');
|
||||
}
|
||||
|
||||
$session = session();
|
||||
|
||||
if ($session->get('isLoggedIn')) {
|
||||
header('Location: ' . base_url('/dashboard'));
|
||||
exit();
|
||||
}
|
||||
?>
|
||||
|
||||
<?= $this->extend('layout/register_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<div class="registration-form container mt-5 mb-5">
|
||||
|
||||
<form id="loginForm" method="post" action="<?= base_url('/user/login') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="redirect_to" value="<?= esc(old('redirect_to', $redirect_to ?? '')) ?>">
|
||||
<div class="text-center mb-4">
|
||||
<a href="<?= base_url('/') ?>">
|
||||
<img src="<?= base_url('assets/images/alrahma_logo.png') ?>" alt=""
|
||||
@@ -60,9 +45,9 @@ if ($session->get('isLoggedIn')) {
|
||||
<button type="submit" class="btn btn-success item">Login</button>
|
||||
</div>
|
||||
<!-- Flash Error Message -->
|
||||
<?php if ($session->getFlashdata('error')): ?>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="alert alert-danger mt-3 text-center" role="alert">
|
||||
<?= esc($session->getFlashdata('error')) ?>
|
||||
<?= esc(session()->getFlashdata('error')) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!-- Forgot Password -->
|
||||
@@ -125,4 +110,4 @@ if ($session->get('isLoggedIn')) {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
<div id="collapse<?= $index ?>" class="collapse <?= $index === 0 ? 'show' : '' ?>" aria-labelledby="heading<?= $index ?>" data-bs-parent="#classAccordion">
|
||||
<div class="card-body">
|
||||
|
||||
<!--h5>Semester: <--?= esc($classSection['semester'] ?? '') ?></h5-->
|
||||
<!--h5>School Year: <--?= esc($classSection['school_year'] ?? '') ?></h5-->
|
||||
<!--p><--?= esc($classSection['description'] ?? '') ?></p-->
|
||||
<!-- <h5>Semester: <?= esc($classSection['semester'] ?? '') ?></h5> -->
|
||||
<!-- <h5>School Year: <?= esc($classSection['school_year'] ?? '') ?></h5> -->
|
||||
<!-- <p><?= esc($classSection['description'] ?? '') ?></p> -->
|
||||
|
||||
<?php if (!empty($classSection['parents'])): ?>
|
||||
<div class="table-responsive">
|
||||
@@ -92,13 +92,15 @@
|
||||
<option value="0" <?= ((string)$pm === '0') ? 'selected' : '' ?>>No</option>
|
||||
</select>
|
||||
</td>
|
||||
<!--td>
|
||||
<--?php if (!empty($p['primary_email'])): ?>
|
||||
<a href="mailto:<--?= esc($p['primary_email']) ?>"><--?= esc($p['primary_email']) ?></a>
|
||||
<--?php else: ?>
|
||||
<!--
|
||||
<td>
|
||||
<?php if (!empty($p['primary_email'])): ?>
|
||||
<a href="mailto:<?= esc($p['primary_email']) ?>"><?= esc($p['primary_email']) ?></a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">—</span>
|
||||
<--?php endif; ?>
|
||||
</td-->
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
-->
|
||||
<td><?= esc($p['second_name']) ?></td>
|
||||
<td>
|
||||
<?php if (!empty($p['second_phone'])): ?>
|
||||
@@ -109,13 +111,15 @@
|
||||
<span class="text-muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<!--td>
|
||||
<--?php if (!empty($p['second_email'])): ?>
|
||||
<--a href="mailto:<--?= esc($p['second_email']) ?>"><--?= esc($p['second_email']) ?></a>
|
||||
<--?php else: ?>
|
||||
<--span class="text-muted">—</span>
|
||||
<--?php endif; ?>
|
||||
</td-->
|
||||
<!--
|
||||
<td>
|
||||
<?php if (!empty($p['second_email'])): ?>
|
||||
<a href="mailto:<?= esc($p['second_email']) ?>"><?= esc($p['second_email']) ?></a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
-->
|
||||
|
||||
<td class="text-center">
|
||||
<?php if (!empty($p['second_id'])): ?>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"version":2,"defects":{"Tests\\App\\Controllers\\AuthControllerTest::testLoginReturnsValidResponse":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testDashboardSuccess":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testAbsenceInfoWithValidUser":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testAbsenceInfoUnauthorized":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testSubmitAbsenceRejectsMissingReason":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testSubmitAbsenceValidDateAndReason":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testSearchRequiresQueryParam":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testSearchReturnsStructuredResults":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testEnrollmentWithdrawalPagination":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testEnrollmentWithdrawalDataBuildsStudentList":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testUpdateEnrollmentStatusesRejectsEmptyPayload":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testUpdateEnrollmentStatusesWithValidStatus":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testStudentProfilesReturnsList":8,"Tests\\App\\Controllers\\Api\\AdministratorControllerTest::testParentProfilesWithParentRole":8,"Tests\\App\\Controllers\\Api\\EmergencyContactControllerTest::testIndexFiltersByParent":8,"Tests\\App\\Controllers\\Api\\EmergencyContactControllerTest::testCreateValidContact":8,"Tests\\App\\Controllers\\Api\\EmergencyContactControllerTest::testCreateInvalidPayload":8,"Tests\\App\\Controllers\\Api\\FamilyControllerTest::testGetGuardiansReturnsDataWhenFamilyExists":8,"Tests\\App\\Controllers\\Api\\FamilyControllerTest::testGetByStudentWrapsStudentsAndGuardians":8,"Tests\\App\\Controllers\\Api\\FamilyControllerTest::testIndexUsesPaginatedResult":8,"Tests\\App\\Controllers\\Api\\FamilyControllerTest::testShowReturnsFamily":8,"Tests\\App\\Controllers\\Api\\ParentControllerTest::testShowReturnsParentById":8,"Tests\\App\\Controllers\\Api\\ParentControllerTest::testShowMissingParentReturnsError":8,"Tests\\App\\Controllers\\Api\\ParentControllerTest::testGetStudentsReturnsStudentList":8,"Tests\\App\\Controllers\\Api\\ParentControllerTest::testGetStudentsMissingParent":8,"Tests\\App\\Controllers\\Api\\RolePermissionControllerTest::testRolePermissionsReturnsDataForRole":8,"Tests\\App\\Controllers\\Api\\RolePermissionControllerTest::testUsersListReturnsSummaries":8,"Tests\\App\\Controllers\\Api\\RolePermissionControllerTest::testAssignRoleSuccess":8,"Tests\\App\\Controllers\\Api\\SupplierControllerTest::testShowReturnsNotFound":8,"Tests\\App\\Controllers\\Api\\SupplierControllerTest::testCreateValidSupplier":8,"Tests\\App\\Controllers\\Api\\SupplierControllerTest::testUpdateSupplierPersistsFields":8,"Tests\\App\\Controllers\\Api\\SupplierControllerTest::testDeleteSupplier":8},"times":{"Tests\\App\\Controllers\\AuthControllerTest::testLoginReturnsValidResponse":0.032,"Tests\\App\\Controllers\\Api\\EmergencyContactControllerTest::testIndexFiltersByParent":0.002,"Tests\\App\\Controllers\\Api\\EmergencyContactControllerTest::testShowReturnsNotFound":0.004,"Tests\\App\\Controllers\\Api\\EmergencyContactControllerTest::testCreateValidContact":0.002,"Tests\\App\\Controllers\\Api\\EmergencyContactControllerTest::testCreateInvalidPayload":0.001,"Tests\\App\\Controllers\\Api\\FamilyControllerTest::testGetGuardiansReturnsDataWhenFamilyExists":0.002,"Tests\\App\\Controllers\\Api\\FamilyControllerTest::testGetGuardiansReturnsErrorWhenFamilyMissing":0.001,"Tests\\App\\Controllers\\Api\\FamilyControllerTest::testGetByStudentWrapsStudentsAndGuardians":0.002,"Tests\\App\\Controllers\\Api\\FamilyControllerTest::testIndexUsesPaginatedResult":0.002,"Tests\\App\\Controllers\\Api\\FamilyControllerTest::testShowReturnsFamily":0.002,"Tests\\App\\Controllers\\Api\\RolePermissionControllerTest::testRolesListReturnsData":0.002,"Tests\\App\\Controllers\\Api\\RolePermissionControllerTest::testPermissionsListReturnsData":0.001,"Tests\\App\\Controllers\\Api\\RolePermissionControllerTest::testRolePermissionsReturnsDataForRole":0.002,"Tests\\App\\Controllers\\Api\\RolePermissionControllerTest::testUsersListReturnsSummaries":0.002,"Tests\\App\\Controllers\\Api\\RolePermissionControllerTest::testAssignRoleSuccess":0.003,"Tests\\App\\Controllers\\Api\\BroadcastEmailControllerTest::testSendValidationFails":0.005,"Tests\\App\\Controllers\\Api\\BroadcastEmailControllerTest::testSendStoresSentAndFailedRecipients":0.003,"Tests\\App\\Controllers\\Api\\BroadcastEmailControllerTest::testParentsReturnsParentList":0.001}}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites>
|
||||
<testsuite name="Tests\App\Controllers\Api\BroadcastEmailControllerTest" file="/opt/lampp/htdocs/alrahma_school_sunday/tests/app/Controllers/Api/BroadcastEmailControllerTest.php" tests="3" assertions="15" errors="0" failures="0" skipped="0" time="0.023977">
|
||||
<testcase name="testSendValidationFails" file="/opt/lampp/htdocs/alrahma_school_sunday/tests/app/Controllers/Api/BroadcastEmailControllerTest.php" line="132" class="Tests\App\Controllers\Api\BroadcastEmailControllerTest" classname="Tests.App.Controllers.Api.BroadcastEmailControllerTest" assertions="3" time="0.016935"/>
|
||||
<testcase name="testSendStoresSentAndFailedRecipients" file="/opt/lampp/htdocs/alrahma_school_sunday/tests/app/Controllers/Api/BroadcastEmailControllerTest.php" line="144" class="Tests\App\Controllers\Api\BroadcastEmailControllerTest" classname="Tests.App.Controllers.Api.BroadcastEmailControllerTest" assertions="9" time="0.004336"/>
|
||||
<testcase name="testParentsReturnsParentList" file="/opt/lampp/htdocs/alrahma_school_sunday/tests/app/Controllers/Api/BroadcastEmailControllerTest.php" line="167" class="Tests\App\Controllers\Api\BroadcastEmailControllerTest" classname="Tests.App.Controllers.Api.BroadcastEmailControllerTest" assertions="3" time="0.002707"/>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
@@ -1,61 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Test Documentation</title>
|
||||
<style>
|
||||
body {
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: Source SansSerif Pro, Arial, sans-serif;
|
||||
font-variant-ligatures: common-ligatures;
|
||||
font-kerning: normal;
|
||||
margin-left: 2rem;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
body > ul > li {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: larger;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
margin: 0;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0 0 2rem;
|
||||
padding: 0 0 0 1rem;
|
||||
text-indent: -1rem;
|
||||
}
|
||||
|
||||
.success:before {
|
||||
color: #4e9a06;
|
||||
content: '✓';
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.defect {
|
||||
color: #a40000;
|
||||
}
|
||||
|
||||
.defect:before {
|
||||
color: #a40000;
|
||||
content: '✗';
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Broadcast Email Controller (Tests\App\Controllers\Api\BroadcastEmailController)</h2>
|
||||
<ul>
|
||||
<li class="success">Send validation fails</li>
|
||||
<li class="success">Send stores sent and failed recipients</li>
|
||||
<li class="success">Parents returns parent list</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
Broadcast Email Controller (Tests\App\Controllers\Api\BroadcastEmailController)
|
||||
[x] Send validation fails
|
||||
[x] Send stores sent and failed recipients
|
||||
[x] Parents returns parent list
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
define('LATEST_RELEASE', '^4.0');
|
||||
define('GITHUB_URL', 'https://github.com/codeigniter4/codeigniter4');
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* Stability Toggle
|
||||
* --------------------------------------------------------------------
|
||||
* Use this script to toggle the CodeIgniter dependency between the
|
||||
* latest stable release and the most recent development update.
|
||||
*
|
||||
* Usage: php builds [release|development]
|
||||
*/
|
||||
|
||||
// Determine the requested stability
|
||||
if (empty($argv[1]) || ! in_array($argv[1], ['release', 'development'], true)) {
|
||||
echo 'Usage: php builds [release|development]' . PHP_EOL;
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
$dev = $argv[1] === 'development';
|
||||
|
||||
$modified = [];
|
||||
|
||||
// Locate each file and update it for the requested stability
|
||||
|
||||
$file = __DIR__ . DIRECTORY_SEPARATOR . 'composer.json';
|
||||
|
||||
if (is_file($file)) {
|
||||
$contents = file_get_contents($file);
|
||||
|
||||
if ((string) $contents !== '') {
|
||||
$array = json_decode($contents, true);
|
||||
|
||||
if (is_array($array)) {
|
||||
if ($dev) {
|
||||
$array['minimum-stability'] = 'dev';
|
||||
$array['prefer-stable'] = true;
|
||||
$array['repositories'] ??= [];
|
||||
|
||||
$found = false;
|
||||
|
||||
foreach ($array['repositories'] as $repository) {
|
||||
if ($repository['url'] === GITHUB_URL) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $found) {
|
||||
$array['repositories'][] = [
|
||||
'type' => 'vcs',
|
||||
'url' => GITHUB_URL,
|
||||
];
|
||||
}
|
||||
|
||||
$array['require']['codeigniter4/codeigniter4'] = 'dev-develop';
|
||||
unset($array['require']['codeigniter4/framework']);
|
||||
} else {
|
||||
unset($array['minimum-stability']);
|
||||
|
||||
if (isset($array['repositories'])) {
|
||||
foreach ($array['repositories'] as $i => $repository) {
|
||||
if ($repository['url'] === GITHUB_URL) {
|
||||
unset($array['repositories'][$i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($array['repositories'])) {
|
||||
unset($array['repositories']);
|
||||
}
|
||||
}
|
||||
|
||||
$array['require']['codeigniter4/framework'] = LATEST_RELEASE;
|
||||
unset($array['require']['codeigniter4/codeigniter4']);
|
||||
}
|
||||
|
||||
file_put_contents($file, json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
|
||||
$modified[] = $file;
|
||||
} else {
|
||||
echo 'Warning: Unable to decode composer.json! Skipping...' . PHP_EOL;
|
||||
}
|
||||
} else {
|
||||
echo 'Warning: Unable to read composer.json! Skipping...' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
$files = [
|
||||
__DIR__ . DIRECTORY_SEPARATOR . 'app/Config/Paths.php',
|
||||
__DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml.dist',
|
||||
__DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml',
|
||||
];
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
$contents = file_get_contents($file);
|
||||
|
||||
if ($dev) {
|
||||
$contents = str_replace('vendor/codeigniter4/framework', 'vendor/codeigniter4/codeigniter4', $contents);
|
||||
} else {
|
||||
$contents = str_replace('vendor/codeigniter4/codeigniter4', 'vendor/codeigniter4/framework', $contents);
|
||||
}
|
||||
|
||||
file_put_contents($file, $contents);
|
||||
|
||||
$modified[] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
if ($modified === []) {
|
||||
echo 'No files modified.' . PHP_EOL;
|
||||
} else {
|
||||
echo 'The following files were modified:' . PHP_EOL;
|
||||
|
||||
foreach ($modified as $file) {
|
||||
echo " * {$file}" . PHP_EOL;
|
||||
}
|
||||
|
||||
echo 'Run `composer update` to sync changes with your vendor folder.' . PHP_EOL;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |