Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f51be91e4 | |||
| 92016f90d0 | |||
| 45d7895182 | |||
| 596d368b1d | |||
| 35988e9ce1 | |||
| 5fc4d8be30 | |||
| dcfd1f9542 | |||
| b99f816261 | |||
| 931e30b6f1 | |||
| 189bb1a628 | |||
| f04934039e | |||
| c13dcb07d1 | |||
| dd1d492c30 | |||
| a448288878 | |||
| 821e9cdc81 |
@@ -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
|
||||
|
||||
@@ -535,9 +535,12 @@ $routes->post('payment/event_charges', 'View\EventController::eventUpdate');
|
||||
|
||||
// Parent event participation
|
||||
$routes->get('administrator/event-charges', 'View\EventController::eventShow');
|
||||
$routes->post('administrator/event-charges/remove/(:num)', 'View\EventController::removeCharge/$1');
|
||||
$routes->post('administrator/event-charges/payment/(:num)', 'View\EventController::toggleEventPayment/$1');
|
||||
$routes->post('administrator/event-charges/waiver/(:num)', 'View\EventController::toggleWaiverStatus/$1');
|
||||
$routes->get('administrator/get-students-with-charges', 'View\EventController::getStudentsWithCharges');
|
||||
|
||||
$routes->get('parent/events', 'View\ParentController::parentEventPage');
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -102,11 +102,9 @@ class ExamDraftController extends BaseController
|
||||
session()->set('class_section_id', $selectedClass);
|
||||
}
|
||||
|
||||
$allDrafts = $this->examDraftModel
|
||||
->select($this->draftSelectColumns())
|
||||
->select('cs.class_section_name')
|
||||
->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left')
|
||||
->where($this->authorIdColumn, $teacherId)
|
||||
$classSectionIds = array_map(static fn($a) => (int) $a['class_section_id'], $assignments);
|
||||
|
||||
$allDrafts = $this->visibleTeacherDraftsQuery($teacherId, $classSectionIds)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
@@ -117,10 +115,10 @@ class ExamDraftController extends BaseController
|
||||
$row['final_pdf_file'] = $pdf;
|
||||
}
|
||||
}
|
||||
$row = $this->attachTeacherDraftContext($row, $teacherId);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$classSectionIds = array_map(static fn($a) => (int) $a['class_section_id'], $assignments);
|
||||
$legacyExams = [];
|
||||
// Guard against missing schema column in older databases
|
||||
if ($this->hasIsLegacyColumn) {
|
||||
@@ -129,8 +127,9 @@ class ExamDraftController extends BaseController
|
||||
|
||||
$legacyQuery = $this->examDraftModel
|
||||
->select($this->draftSelectColumns())
|
||||
->select('cs.class_section_name')
|
||||
->select('cs.class_section_name, u.firstname AS teacher_first, u.lastname AS teacher_last')
|
||||
->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left')
|
||||
->join('users u', 'u.id = exam_drafts.' . $this->authorIdColumn, 'left')
|
||||
->where('exam_drafts.is_legacy', 1)
|
||||
->where('exam_drafts.status', 'legacy')
|
||||
->where('exam_drafts.final_file IS NOT NULL', null, false);
|
||||
@@ -143,6 +142,11 @@ class ExamDraftController extends BaseController
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->orderBy('exam_drafts.created_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
foreach ($legacyExams as &$row) {
|
||||
$row = $this->attachTeacherDraftContext($row, $teacherId);
|
||||
}
|
||||
unset($row);
|
||||
} else {
|
||||
// Legacy column absent, show all drafts and skip legacy tab query
|
||||
$drafts = $allDrafts;
|
||||
@@ -182,7 +186,7 @@ class ExamDraftController extends BaseController
|
||||
}
|
||||
|
||||
$classSectionId = (int) ($this->request->getPost('class_section_id') ?? session()->get('class_section_id') ?? 0);
|
||||
$examType = trim((string) $this->request->getPost('exam_type'));
|
||||
$examType = $this->normalizeExamType($this->request->getPost('exam_type'));
|
||||
$authorComment = trim((string) $this->request->getPost('author_comment'));
|
||||
|
||||
if ($classSectionId <= 0) {
|
||||
@@ -226,7 +230,11 @@ class ExamDraftController extends BaseController
|
||||
->orderBy('version', 'DESC')
|
||||
->first();
|
||||
|
||||
$title = $examType ?: 'Exam Draft';
|
||||
if ($examType === '') {
|
||||
return redirect()->back()->withInput()->with('error', 'Select an exam type before submitting.');
|
||||
}
|
||||
|
||||
$title = $examType;
|
||||
|
||||
if ($saveAsDraft) {
|
||||
if ($teacherFile === null && (empty($existingDraft) || empty($this->draftTeacherFile($existingDraft)))) {
|
||||
@@ -235,7 +243,7 @@ class ExamDraftController extends BaseController
|
||||
if (!empty($existingDraft) && strtolower((string) ($existingDraft['status'] ?? '')) === 'draft') {
|
||||
$draftRowId = (int) ($existingDraft['id'] ?? 0);
|
||||
$updateDraft = [
|
||||
'exam_type' => $examType ?: null,
|
||||
'exam_type' => $examType,
|
||||
'draft_title' => $title,
|
||||
'author_comment' => $authorComment === '' ? null : $authorComment,
|
||||
'status' => 'draft',
|
||||
@@ -263,7 +271,7 @@ class ExamDraftController extends BaseController
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $this->semester,
|
||||
'school_year' => $this->schoolYear,
|
||||
'exam_type' => $examType ?: null,
|
||||
'exam_type' => $examType,
|
||||
'draft_title' => $title,
|
||||
'author_comment' => $authorComment === '' ? null : $authorComment,
|
||||
'status' => 'draft',
|
||||
@@ -280,9 +288,6 @@ class ExamDraftController extends BaseController
|
||||
}
|
||||
|
||||
$existingStatus = strtolower((string) ($existingDraft['status'] ?? ''));
|
||||
if ($examType === '') {
|
||||
return redirect()->back()->withInput()->with('error', 'Select an exam type before submitting.');
|
||||
}
|
||||
if ($teacherFile === null) {
|
||||
return redirect()->back()->withInput()->with('error', 'Upload a DOC/DOCX file to submit the exam draft.');
|
||||
}
|
||||
@@ -291,7 +296,7 @@ class ExamDraftController extends BaseController
|
||||
if (!empty($existingDraft) && $existingStatus === 'submitted') {
|
||||
$draftRowId = (int) ($existingDraft['id'] ?? 0);
|
||||
$updateSubmit = [
|
||||
'exam_type' => $examType ?: null,
|
||||
'exam_type' => $examType,
|
||||
'draft_title' => $title,
|
||||
'author_comment' => $authorComment === '' ? null : $authorComment,
|
||||
'status' => 'submitted',
|
||||
@@ -322,7 +327,7 @@ class ExamDraftController extends BaseController
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $this->semester,
|
||||
'school_year' => $this->schoolYear,
|
||||
'exam_type' => $examType ?: null,
|
||||
'exam_type' => $examType,
|
||||
'draft_title' => $title,
|
||||
'author_comment' => $authorComment === '' ? null : $authorComment,
|
||||
'status' => 'submitted',
|
||||
@@ -499,7 +504,7 @@ class ExamDraftController extends BaseController
|
||||
$classSectionIds = array_values(array_filter(array_map('intval', $classSectionIds)));
|
||||
$schoolYear = trim((string) ($this->request->getPost('school_year') ?? $this->schoolYear));
|
||||
$semester = trim((string) ($this->request->getPost('semester') ?? $this->semester));
|
||||
$examType = trim((string) $this->request->getPost('exam_type'));
|
||||
$examType = $this->normalizeExamType($this->request->getPost('exam_type'));
|
||||
|
||||
if (empty($classSectionIds)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Select at least one class section.');
|
||||
@@ -510,6 +515,9 @@ class ExamDraftController extends BaseController
|
||||
if ($semester === '') {
|
||||
return redirect()->back()->withInput()->with('error', 'Semester is required.');
|
||||
}
|
||||
if ($examType === '') {
|
||||
return redirect()->back()->withInput()->with('error', 'Exam type is required.');
|
||||
}
|
||||
|
||||
$file = $this->request->getFile('old_exam_file');
|
||||
if (!$file || !$file->isValid()) {
|
||||
@@ -534,8 +542,8 @@ class ExamDraftController extends BaseController
|
||||
$this->authorIdColumn => $adminId, // store under admin user since legacy uploads are admin-only
|
||||
'semester' => ucfirst(strtolower($semester)),
|
||||
'school_year' => $schoolYear,
|
||||
'exam_type' => $examType === '' ? null : $examType,
|
||||
'draft_title' => $examType === '' ? 'Legacy Exam' : $examType,
|
||||
'exam_type' => $examType,
|
||||
'draft_title' => $examType,
|
||||
'author_comment' => null,
|
||||
'final_file' => $stored,
|
||||
'final_filename' => $file->getClientName(),
|
||||
@@ -646,13 +654,18 @@ class ExamDraftController extends BaseController
|
||||
$baseVersion = 1;
|
||||
}
|
||||
$reviewRevision = $this->nextReviewRevision($draft, $baseVersion);
|
||||
$reviewExamType = $this->normalizeExamType($draft['exam_type'] ?? $draft['draft_title'] ?? '');
|
||||
if ($reviewExamType === '') {
|
||||
return redirect()->back()->with('error', 'This draft is missing an exam type. Update the exam type before reviewing.');
|
||||
}
|
||||
|
||||
$newRow = [
|
||||
$this->authorIdColumn => $this->draftTeacherId($draft),
|
||||
'class_section_id' => (int) ($draft['class_section_id'] ?? 0),
|
||||
'semester' => (string) ($draft['semester'] ?? ''),
|
||||
'school_year' => (string) ($draft['school_year'] ?? ''),
|
||||
'exam_type' => $draft['exam_type'] ?? null,
|
||||
'draft_title' => $draft['draft_title'] ?? $draft['exam_type'] ?? 'Exam Draft',
|
||||
'exam_type' => $reviewExamType,
|
||||
'draft_title' => $draft['draft_title'] ?? $reviewExamType,
|
||||
'author_comment' => $draft['author_comment'] ?? null,
|
||||
'status' => $status,
|
||||
'reviewed_at' => $update['reviewed_at'],
|
||||
@@ -759,10 +772,11 @@ class ExamDraftController extends BaseController
|
||||
return $this->response->setStatusCode(401);
|
||||
}
|
||||
|
||||
$drafts = $this->examDraftModel
|
||||
->select($this->draftSelectColumns())
|
||||
->select('id, status, acceptance_type, updated_at, reviewed_at')
|
||||
->where($this->authorIdColumn, $teacherId)
|
||||
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId($teacherId, $this->schoolYear);
|
||||
$classSectionIds = array_map(static fn($a) => (int) $a['class_section_id'], $assignments);
|
||||
|
||||
$drafts = $this->visibleTeacherDraftsQuery($teacherId, $classSectionIds)
|
||||
->select('exam_drafts.id, exam_drafts.status, exam_drafts.acceptance_type, exam_drafts.updated_at, exam_drafts.reviewed_at')
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
@@ -808,6 +822,58 @@ class ExamDraftController extends BaseController
|
||||
return $validIds[0] ?? 0;
|
||||
}
|
||||
|
||||
private function visibleTeacherDraftsQuery(int $teacherId, array $classSectionIds)
|
||||
{
|
||||
$query = $this->examDraftModel
|
||||
->select($this->draftSelectColumns())
|
||||
->select('cs.class_section_name, u.firstname AS teacher_first, u.lastname AS teacher_last')
|
||||
->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left')
|
||||
->join('users u', 'u.id = exam_drafts.' . $this->authorIdColumn, 'left');
|
||||
|
||||
if (empty($classSectionIds)) {
|
||||
return $query->where('exam_drafts.' . $this->authorIdColumn, $teacherId);
|
||||
}
|
||||
|
||||
$query->groupStart()
|
||||
->where('exam_drafts.' . $this->authorIdColumn, $teacherId)
|
||||
->orGroupStart()
|
||||
->whereIn('exam_drafts.class_section_id', $classSectionIds)
|
||||
->where('exam_drafts.' . $this->authorIdColumn . ' !=', $teacherId)
|
||||
->where('exam_drafts.status !=', 'draft');
|
||||
|
||||
if ($this->schoolYear !== '') {
|
||||
$query->where('exam_drafts.school_year', $this->schoolYear);
|
||||
}
|
||||
if ($this->semester !== '') {
|
||||
$query->where('exam_drafts.semester', $this->semester);
|
||||
}
|
||||
|
||||
$query->groupEnd()
|
||||
->groupEnd();
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function attachTeacherDraftContext(array $row, int $viewerId): array
|
||||
{
|
||||
$isOwn = $this->draftTeacherId($row) === $viewerId;
|
||||
$row['is_own_submission'] = $isOwn;
|
||||
$row['teacher_display_name'] = $isOwn ? 'You' : $this->draftTeacherName($row);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function draftTeacherName(array $draft): string
|
||||
{
|
||||
$name = trim(((string) ($draft['teacher_first'] ?? '')) . ' ' . ((string) ($draft['teacher_last'] ?? '')));
|
||||
if ($name !== '') {
|
||||
return $name;
|
||||
}
|
||||
|
||||
$teacherId = $this->draftTeacherId($draft);
|
||||
return $teacherId > 0 ? 'Teacher #' . $teacherId : 'Teacher';
|
||||
}
|
||||
|
||||
private function statusBadgeMap(): array
|
||||
{
|
||||
return [
|
||||
@@ -1309,7 +1375,7 @@ class ExamDraftController extends BaseController
|
||||
|
||||
// Attempt conversion via LibreOffice if available
|
||||
$cmd = 'soffice --headless --convert-to pdf --outdir ' . escapeshellarg($targetDir) . ' ' . escapeshellarg($sourcePath) . ' 2>/dev/null';
|
||||
@exec($cmd);
|
||||
@\exec($cmd);
|
||||
|
||||
return is_file($targetPath) ? basename($targetPath) : null;
|
||||
}
|
||||
@@ -1358,6 +1424,11 @@ class ExamDraftController extends BaseController
|
||||
);
|
||||
}
|
||||
|
||||
private function normalizeExamType($value): string
|
||||
{
|
||||
return trim((string) $value);
|
||||
}
|
||||
|
||||
private function copyDraftToFinal(string $draftFilename): ?string
|
||||
{
|
||||
$source = $this->fullUploadPath(self::TEACHER_UPLOAD_DIR, $draftFilename);
|
||||
|
||||
@@ -224,6 +224,10 @@ class FilesController extends Controller
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
if (!$this->canAccessExamDraftFile($name, $subdir)) {
|
||||
return $this->response->setStatusCode(403, 'You are not allowed to access this file.');
|
||||
}
|
||||
|
||||
$mime = 'application/octet-stream';
|
||||
if (function_exists('finfo_open')) {
|
||||
$fi = finfo_open(FILEINFO_MIME_TYPE);
|
||||
@@ -317,4 +321,53 @@ class FilesController extends Controller
|
||||
}
|
||||
return 'teacher_file';
|
||||
}
|
||||
|
||||
private function canAccessExamDraftFile(string $filename, string $subdir): bool
|
||||
{
|
||||
$userId = (int) (session()->get('user_id') ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$role = strtolower((string) (session()->get('role') ?? ''));
|
||||
if ($role === 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$db = Database::connect();
|
||||
$fileColumn = $subdir === 'finals' ? 'final_file' : $this->resolveExamDraftFileColumn($db);
|
||||
|
||||
$draft = $db->table('exam_drafts ed')
|
||||
->select('ed.class_section_id, ed.school_year, ed.teacher_id, ed.author_id')
|
||||
->where('ed.' . $fileColumn, $filename)
|
||||
->limit(1)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (empty($draft)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authorId = (int) ($draft['teacher_id'] ?? $draft['author_id'] ?? 0);
|
||||
if ($authorId > 0 && $authorId === $userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$classSectionId = (int) ($draft['class_section_id'] ?? 0);
|
||||
if ($classSectionId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$assignmentQuery = $db->table('teacher_class')
|
||||
->select('id')
|
||||
->where('teacher_id', $userId)
|
||||
->where('class_section_id', $classSectionId);
|
||||
|
||||
$schoolYear = trim((string) ($draft['school_year'] ?? ''));
|
||||
if ($schoolYear !== '') {
|
||||
$assignmentQuery->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $assignmentQuery->limit(1)->countAllResults() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1797,16 +1797,34 @@ $existing = $this->studentModel
|
||||
$activeEventCount = is_array($activeEvents) ? count($activeEvents) : 0;
|
||||
|
||||
// Get charges (participation info)
|
||||
$chargesList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear);
|
||||
$chargesList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear, $semester);
|
||||
|
||||
// Build a map: "studentId:eventId" => [ 'participation' => ..., 'date' => ... ]
|
||||
$charges = [];
|
||||
$externalParticipantsByEvent = [];
|
||||
foreach ($chargesList as $charge) {
|
||||
$key = $charge['student_id'] . ':' . $charge['event_id'];
|
||||
$charges[$key] = [
|
||||
'participation' => $charge['participation'],
|
||||
'date' => $charge['updated_at'] ?? $charge['created_at'] // Use updated_at if available
|
||||
];
|
||||
$studentId = $charge['student_id'] ?? null;
|
||||
$eventId = (int) ($charge['event_id'] ?? 0);
|
||||
|
||||
if (!empty($studentId)) {
|
||||
$key = $studentId . ':' . $eventId;
|
||||
$charges[$key] = [
|
||||
'participation' => $charge['participation'],
|
||||
'date' => $charge['updated_at'] ?? $charge['created_at'], // Use updated_at if available
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$externalName = trim((string) ($charge['external_firstname'] ?? '') . ' ' . (string) ($charge['external_lastname'] ?? ''));
|
||||
if ($eventId > 0 && $externalName !== '') {
|
||||
$externalParticipantsByEvent[$eventId][] = [
|
||||
'name' => $externalName,
|
||||
'note' => (string) ($charge['external_note'] ?? ''),
|
||||
'participation' => (string) ($charge['participation'] ?? ''),
|
||||
'event_paid' => !empty($charge['event_paid']),
|
||||
'charged' => (float) ($charge['charged'] ?? ($charge['event_amount'] ?? 0)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Get enrolled students
|
||||
@@ -1815,6 +1833,7 @@ $existing = $this->studentModel
|
||||
return view('parent/event_participation', [
|
||||
'activeEvents' => $activeEvents,
|
||||
'charges' => $charges,
|
||||
'externalParticipantsByEvent' => $externalParticipantsByEvent,
|
||||
'yourStudents' => $students,
|
||||
'activeEventCount' => $activeEventCount,
|
||||
]);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddWaiverSignedToEventCharges extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$fields = [
|
||||
'waiver_signed' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
'after' => 'charged',
|
||||
],
|
||||
];
|
||||
|
||||
if (! $this->db->fieldExists('waiver_signed', 'event_charges')) {
|
||||
$this->forge->addColumn('event_charges', $fields);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->db->fieldExists('waiver_signed', 'event_charges')) {
|
||||
$this->forge->dropColumn('event_charges', 'waiver_signed');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -15,8 +15,20 @@ class EventChargesModel extends Model
|
||||
'student_id',
|
||||
'participation',
|
||||
'charged',
|
||||
'waiver_signed',
|
||||
'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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-control"></textarea>
|
||||
<textarea id="event_description" name="description" class="form-control"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -56,9 +56,89 @@
|
||||
<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 src="<?= base_url('assets/tinymce/tinymce.min.js') ?>"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const toggle = document.getElementById('add_to_calendar');
|
||||
const recipients = document.getElementById('calendar_recipients');
|
||||
const description = document.getElementById('event_description');
|
||||
|
||||
if (toggle && recipients) {
|
||||
function sync() {
|
||||
recipients.style.display = toggle.checked ? '' : 'none';
|
||||
}
|
||||
toggle.addEventListener('change', sync);
|
||||
sync();
|
||||
}
|
||||
|
||||
if (description && window.tinymce) {
|
||||
tinymce.init({
|
||||
selector: '#event_description',
|
||||
base_url: '<?= base_url('assets/tinymce') ?>',
|
||||
suffix: '.min',
|
||||
license_key: 'gpl',
|
||||
height: 320,
|
||||
menubar: true,
|
||||
branding: false,
|
||||
promotion: false,
|
||||
plugins: 'advlist autolink lists link charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime table help wordcount',
|
||||
toolbar: 'undo redo | blocks | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link table | removeformat | preview code',
|
||||
convert_urls: false,
|
||||
content_style: 'body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size: 14px; }'
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
<div class="container mt-4">
|
||||
<h2>Edit 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));
|
||||
?>
|
||||
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
|
||||
<?php endif; ?>
|
||||
@@ -23,7 +29,7 @@
|
||||
<label class="form-label">Category</label>
|
||||
<select name="event_category" class="form-control" required>
|
||||
<option value="" disabled <?= empty($event['event_category']) ? 'selected' : '' ?>>Select category</option>
|
||||
<?php foreach (($categories ?? []) as $category): ?>
|
||||
<?php foreach ($allCategories as $category): ?>
|
||||
<option value="<?= esc($category) ?>" <?= (string)($event['event_category'] ?? '') === (string)$category ? 'selected' : '' ?>>
|
||||
<?= esc(ucwords($category)) ?>
|
||||
</option>
|
||||
@@ -33,7 +39,7 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-control"><?= esc($event['description']) ?></textarea>
|
||||
<textarea id="event_description" name="description" class="form-control"><?= esc($event['description']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -44,7 +50,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Current Flyer</label><br>
|
||||
<?php if ($event['flyer']): ?>
|
||||
<img src="<?= base_url('writable/uploads/' . $event['flyer']) ?>" width="150" class="mb-2">
|
||||
<img src="<?= base_url('uploads/' . ltrim((string) $event['flyer'], '/')) ?>" width="150" class="mb-2">
|
||||
<?php else: ?>
|
||||
<div class="text-muted">No flyer uploaded.</div>
|
||||
<?php endif; ?>
|
||||
@@ -70,9 +76,89 @@
|
||||
<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">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">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>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script src="<?= base_url('assets/tinymce/tinymce.min.js') ?>"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const toggle = document.getElementById('add_to_calendar');
|
||||
const recipients = document.getElementById('calendar_recipients');
|
||||
const description = document.getElementById('event_description');
|
||||
|
||||
if (toggle && recipients) {
|
||||
function sync() {
|
||||
recipients.style.display = toggle.checked ? '' : 'none';
|
||||
}
|
||||
toggle.addEventListener('change', sync);
|
||||
sync();
|
||||
}
|
||||
|
||||
if (description && window.tinymce) {
|
||||
tinymce.init({
|
||||
selector: '#event_description',
|
||||
base_url: '<?= base_url('assets/tinymce') ?>',
|
||||
suffix: '.min',
|
||||
license_key: 'gpl',
|
||||
height: 320,
|
||||
menubar: true,
|
||||
branding: false,
|
||||
promotion: false,
|
||||
plugins: 'advlist autolink lists link charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime table help wordcount',
|
||||
toolbar: 'undo redo | blocks | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link table | removeformat | preview code',
|
||||
convert_urls: false,
|
||||
content_style: 'body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size: 14px; }'
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -3,6 +3,54 @@
|
||||
|
||||
<div class="container mt-5">
|
||||
<h3>Event Charges</h3>
|
||||
<style>
|
||||
.event-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.event-card-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sortable-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.sortable-header:hover,
|
||||
.sortable-header:focus {
|
||||
color: var(--bs-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.sortable-header::after {
|
||||
content: '↕';
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.45;
|
||||
}
|
||||
.sortable-header.sorted-asc::after {
|
||||
content: '↑';
|
||||
opacity: 1;
|
||||
}
|
||||
.sortable-header.sorted-desc::after {
|
||||
content: '↓';
|
||||
opacity: 1;
|
||||
}
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="alert alert-success"><?= session()->getFlashdata('success') ?></div>
|
||||
@@ -12,6 +60,14 @@
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$classSectionNames = $classSectionNames ?? [];
|
||||
$semesterOptions = $semesterOptions ?? [];
|
||||
$schoolYearOptions = $schoolYearOptions ?? [];
|
||||
?>
|
||||
|
||||
<?php
|
||||
$filterParentId = $filterParentId ?? 0;
|
||||
$parentBalances = $parentBalances ?? [];
|
||||
$selectedEvent = null;
|
||||
if (!empty($filterEventId)) {
|
||||
foreach ($events as $event) {
|
||||
@@ -22,18 +78,58 @@
|
||||
}
|
||||
}
|
||||
?>
|
||||
<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>
|
||||
|
||||
<!-- Add New Charge Form -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form action="<?= site_url('payment/event_charges') ?>" method="post">
|
||||
<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']) ?>" <?= isset($filterEventId) && $filterEventId == $event['id'] ? 'selected' : '' ?>>
|
||||
<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; ?>
|
||||
@@ -60,16 +156,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-2">
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="parent_id" class="form-label">Parents List</label>
|
||||
<select id="parent_id" name="parent_id" class="form-select" required>
|
||||
<select id="parent_id" name="parent_id" class="form-select">
|
||||
<option value="">-- Select Parent --</option>
|
||||
<?php foreach ($parents as $parent): ?>
|
||||
<option value="<?= $parent['id'] ?>">
|
||||
<option value="<?= esc($parent['id']) ?>" <?= $filterParentId && $filterParentId == $parent['id'] ? 'selected' : '' ?>>
|
||||
<?= esc($parent['firstname'] . ' ' . $parent['lastname']) ?> (<?= esc($parent['school_id']) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
@@ -82,6 +175,15 @@
|
||||
<div id="studentList" class="row g-3"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<?php if (!in_array(strtolower((string)(session()->get('role') ?? '')), ['parent', 'teacher', 'teacher_assistant'], true)): ?>
|
||||
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#externalParticipantModal">
|
||||
Add non-school participant
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<div class="form-text">Enrolled students are saved when you click Submit. Non-school participants are saved immediately and will then appear in all browsers.</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>
|
||||
@@ -89,73 +191,272 @@
|
||||
</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">
|
||||
<div class="card-header bg-light fw-semibold">
|
||||
<?= esc($eventLabel) ?>
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3 no-print">
|
||||
<p class="text-muted mb-0">Click a column header to sort the participants list.</p>
|
||||
<button type="button" class="btn btn-outline-secondary" id="printAllEventCharges">
|
||||
Print All Participant Lists
|
||||
</button>
|
||||
</div>
|
||||
<div id="eventChargeCards">
|
||||
<?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 event-card-header">
|
||||
<span><?= esc($eventLabel) ?></span>
|
||||
<div class="event-card-header-actions no-print">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm print-event-card" data-event-id="<?= esc($eventId) ?>">
|
||||
Print This List
|
||||
</button>
|
||||
</div>
|
||||
</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 event-charges-table">
|
||||
<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>
|
||||
<th>Description</th>
|
||||
<th>Event Fees</th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="0" data-sort-type="number">ID</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="1" data-sort-type="text">Parent Name</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="2" data-sort-type="text">Student Name</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="3" data-sort-type="text">External Info</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="4" data-sort-type="text">Class Section</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="5" data-sort-type="number">Charged Amount</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="6" data-sort-type="date">Created</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="7" data-sort-type="text">Waiver</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="8" data-sort-type="text">Fees Paid</button></th>
|
||||
<th><button type="button" class="sortable-header" data-sort-index="9" data-sort-type="text">Payment</button></th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $charge): ?>
|
||||
<?php
|
||||
$isParticipating = ($charge['participation'] === 'yes');
|
||||
$isEventPaid = !empty($charge['event_paid']);
|
||||
$returnParams = [];
|
||||
if (!empty($semester)) {
|
||||
$returnParams['semester'] = $semester;
|
||||
}
|
||||
if (!empty($school_year)) {
|
||||
$returnParams['school_year'] = $school_year;
|
||||
}
|
||||
if (!empty($filterEventId)) {
|
||||
$returnParams['event_id'] = $filterEventId;
|
||||
}
|
||||
if (!empty($filterParentId)) {
|
||||
$returnParams['parent_id'] = $filterParentId;
|
||||
}
|
||||
$returnTo = site_url('administrator/event-charges')
|
||||
. (!empty($returnParams) ? '?' . http_build_query($returnParams) : '')
|
||||
. '#eventTable_' . (int) ($charge['event_id'] ?? 0);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($charge['id']) ?></td>
|
||||
<td><?= esc($charge['parent_firstname'] . ' ' . $charge['parent_lastname']) ?></td>
|
||||
<td>
|
||||
<?= $charge['student_firstname']
|
||||
? esc($charge['student_firstname'] . ' ' . $charge['student_lastname'])
|
||||
: '-' ?>
|
||||
<td data-sort-value="<?= esc((string) ($charge['id'] ?? '')) ?>"><?= esc($charge['id']) ?></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'] ?? '';
|
||||
$externalParentEmail = $charge['external_parent_email'] ?? '';
|
||||
$standardParentName = trim($charge['parent_firstname'] . ' ' . $charge['parent_lastname']);
|
||||
$parentColumnName = $externalParentLabel ?: ($standardParentName ?: '—');
|
||||
$externalInfoValue = trim(implode(' ', array_filter([
|
||||
$externalParentPhone,
|
||||
$externalParentEmail,
|
||||
$externalNote,
|
||||
])));
|
||||
?>
|
||||
<td data-sort-value="<?= esc(strtolower($parentColumnName)) ?>"><?= esc($parentColumnName) ?></td>
|
||||
<td data-sort-value="<?= esc(strtolower($displayName)) ?>">
|
||||
<?= 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>' ?>
|
||||
<td data-sort-value="<?= esc(strtolower($externalInfoValue)) ?>">
|
||||
<?php if ($externalParentPhone): ?>
|
||||
<div class="text-muted small"><?= esc($externalParentPhone) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($externalParentEmail): ?>
|
||||
<div class="text-muted small"><?= esc($externalParentEmail) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!$externalParentPhone && !$externalParentEmail): ?>
|
||||
<span class="text-muted small">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td data-sort-value="<?= esc(strtolower((string) ($classSectionNames[$charge['class_section_id'] ?? ''] ?? ''))) ?>">
|
||||
<?= esc($classSectionNames[$charge['class_section_id'] ?? ''] ?? '—') ?>
|
||||
</td>
|
||||
<td data-sort-value="<?= esc(number_format((float) ($charge['event_amount'] ?? 0), 2, '.', '')) ?>">$<?= esc(number_format($charge['event_amount'] ?? 0, 2)) ?></td>
|
||||
<td data-sort-value="<?= esc((string) ($charge['created_at'] ?? '')) ?>"><?= 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;
|
||||
$waiverSigned = !empty($charge['waiver_signed']);
|
||||
$feeIsPaid = $isParticipating && ($isEventPaid || ($hasBalanceRecord && $parentBalance <= 0));
|
||||
?>
|
||||
<td class="text-center" data-sort-value="<?= $waiverSigned ? 'signed' : 'unsigned' ?>">
|
||||
<form method="post" action="<?= site_url('administrator/event-charges/waiver/' . esc($charge['id'])) ?>" class="d-inline-flex align-items-center">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="return_to" value="<?= esc($returnTo) ?>">
|
||||
<input type="hidden" name="waiver_signed" value="<?= $waiverSigned ? '1' : '0' ?>">
|
||||
<input type="checkbox" class="form-check-input" id="eventWaiver_<?= esc($charge['id']) ?>"
|
||||
<?= $waiverSigned ? 'checked' : '' ?>
|
||||
onchange="this.form.waiver_signed.value = this.checked ? 1 : 0; this.form.submit();">
|
||||
<label class="form-check-label ms-2" for="eventWaiver_<?= esc($charge['id']) ?>" aria-hidden="true">
|
||||
<?= $waiverSigned ? 'Signed' : 'Unsigned' ?>
|
||||
</label>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-center" data-sort-value="<?= $feeIsPaid ? 'paid' : 'unpaid' ?>">
|
||||
<?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" data-sort-value="<?= $isEventPaid ? 'paid' : 'unpaid' ?>">
|
||||
<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="return_to" value="<?= esc($returnTo) ?>">
|
||||
<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() ?>
|
||||
<input type="hidden" name="return_to" value="<?= esc($returnTo) ?>">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
<td><?= esc($charge['semester'] ?? '-') ?></td>
|
||||
<td><?= esc($charge['school_year'] ?? '-') ?></td>
|
||||
<td><?= esc(!empty($charge['created_at']) ? local_datetime($charge['created_at'], 'm-d-Y H:i') : '') ?></td>
|
||||
<td><?= esc($charge['event_description'] ?? '—') ?></td>
|
||||
<td>$<?= esc(number_format($charge['event_amount'] ?? 0, 2)) ?></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="6">
|
||||
<span class="badge bg-secondary">
|
||||
<?= esc($totalParticipants) ?> participating
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!in_array(strtolower((string)(session()->get('role') ?? '')), ['parent', 'teacher', 'teacher_assistant'], true)): ?>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
@@ -201,7 +502,20 @@ function loadStudentsWithCharges() {
|
||||
}
|
||||
}
|
||||
|
||||
$(function() {
|
||||
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() {
|
||||
@@ -214,6 +528,283 @@ $(function() {
|
||||
}
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
|
||||
if ($('#event_id').val() && $('#parent_id').val()) {
|
||||
loadStudentsWithCharges();
|
||||
}
|
||||
|
||||
const $externalSaveButton = $('#externalParticipantSave');
|
||||
|
||||
const modalElement = document.getElementById('externalParticipantModal');
|
||||
const modalInstance = modalElement ? new bootstrap.Modal(modalElement) : null;
|
||||
|
||||
function clearModalInputs() {
|
||||
$('#modalExternalFirstName').val('');
|
||||
$('#modalExternalLastName').val('');
|
||||
$('#modalExternalNote').val('');
|
||||
$('#modalParentFirstName').val('');
|
||||
$('#modalParentLastName').val('');
|
||||
$('#modalParentPhone').val('');
|
||||
$('#modalParentEmail').val('');
|
||||
}
|
||||
|
||||
function appendHiddenField(form, name, value) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = name;
|
||||
input.value = value;
|
||||
form.appendChild(input);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
const s = String(value ?? '');
|
||||
return s.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function submitExternalParticipant(payload) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'post';
|
||||
form.action = '<?= site_url('payment/event_charges') ?>';
|
||||
|
||||
appendHiddenField(form, '<?= csrf_token() ?>', '<?= csrf_hash() ?>');
|
||||
appendHiddenField(form, 'school_year', '<?= esc($school_year) ?>');
|
||||
appendHiddenField(form, 'semester', '<?= esc($semester) ?>');
|
||||
appendHiddenField(form, 'event_id', payload.eventId);
|
||||
appendHiddenField(form, 'external_participants[new][firstname]', payload.firstname);
|
||||
appendHiddenField(form, 'external_participants[new][lastname]', payload.lastname);
|
||||
appendHiddenField(form, 'external_participants[new][note]', payload.note);
|
||||
appendHiddenField(form, 'external_participants[new][parent_firstname]', payload.parentFirstname);
|
||||
appendHiddenField(form, 'external_participants[new][parent_lastname]', payload.parentLastname);
|
||||
appendHiddenField(form, 'external_participants[new][parent_phone]', payload.parentPhone);
|
||||
appendHiddenField(form, 'external_participants[new][parent_email]', payload.parentEmail);
|
||||
appendHiddenField(form, 'external_participants[new][waiver_signed]', '0');
|
||||
appendHiddenField(form, 'external_participants[new][paid]', '0');
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function getSortValue(row, columnIndex, sortType) {
|
||||
const cell = row.children[columnIndex];
|
||||
if (!cell) {
|
||||
return sortType === 'number' ? 0 : '';
|
||||
}
|
||||
const rawValue = cell.getAttribute('data-sort-value');
|
||||
const fallbackValue = (cell.textContent || '').trim();
|
||||
const value = rawValue !== null ? rawValue : fallbackValue;
|
||||
if (sortType === 'number') {
|
||||
const parsed = parseFloat(String(value).replace(/[^0-9.-]/g, ''));
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
if (sortType === 'date') {
|
||||
const time = Date.parse(value);
|
||||
return Number.isNaN(time) ? 0 : time;
|
||||
}
|
||||
return String(value).toLowerCase();
|
||||
}
|
||||
|
||||
function updateSortIndicators(table, columnIndex, direction) {
|
||||
$(table).find('.sortable-header').each(function() {
|
||||
const isCurrent = Number($(this).data('sortIndex')) === Number(columnIndex);
|
||||
$(this)
|
||||
.toggleClass('sorted-asc', isCurrent && direction === 'asc')
|
||||
.toggleClass('sorted-desc', isCurrent && direction === 'desc')
|
||||
.attr('aria-sort', isCurrent ? direction : 'none');
|
||||
});
|
||||
}
|
||||
|
||||
function sortEventTable(table, columnIndex, sortType, direction) {
|
||||
if (!table || !table.tBodies.length) {
|
||||
return;
|
||||
}
|
||||
const tbody = table.tBodies[0];
|
||||
const rows = Array.from(tbody.rows);
|
||||
rows.sort((a, b) => {
|
||||
const first = getSortValue(a, columnIndex, sortType);
|
||||
const second = getSortValue(b, columnIndex, sortType);
|
||||
if (first < second) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (first > second) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
rows.forEach((row) => tbody.appendChild(row));
|
||||
table.dataset.sortColumn = String(columnIndex);
|
||||
table.dataset.sortType = sortType;
|
||||
table.dataset.sortDirection = direction;
|
||||
updateSortIndicators(table, columnIndex, direction);
|
||||
}
|
||||
|
||||
function applyActiveSort(table) {
|
||||
if (!table || !table.dataset || !table.dataset.sortColumn) {
|
||||
return;
|
||||
}
|
||||
sortEventTable(
|
||||
table,
|
||||
Number(table.dataset.sortColumn),
|
||||
table.dataset.sortType || 'text',
|
||||
table.dataset.sortDirection || 'asc'
|
||||
);
|
||||
}
|
||||
|
||||
function buildPrintableCard(card) {
|
||||
const clone = card.cloneNode(true);
|
||||
clone.querySelectorAll('.no-print').forEach((node) => node.remove());
|
||||
clone.querySelectorAll('.sortable-header').forEach((button) => {
|
||||
const headerText = button.textContent || '';
|
||||
const textNode = document.createTextNode(headerText);
|
||||
button.parentNode.replaceChild(textNode, button);
|
||||
});
|
||||
clone.querySelectorAll('form').forEach((form) => {
|
||||
const cell = form.closest('td');
|
||||
if (!cell) {
|
||||
form.remove();
|
||||
return;
|
||||
}
|
||||
const label = form.querySelector('.form-check-label');
|
||||
const checkbox = form.querySelector('input[type="checkbox"]');
|
||||
const text = label
|
||||
? label.textContent.trim()
|
||||
: (checkbox && checkbox.checked ? 'Paid' : 'Unpaid');
|
||||
cell.textContent = text || '—';
|
||||
});
|
||||
clone.querySelectorAll('tr').forEach((row) => {
|
||||
if (row.children.length > 10) {
|
||||
row.removeChild(row.lastElementChild);
|
||||
}
|
||||
});
|
||||
return clone.outerHTML;
|
||||
}
|
||||
|
||||
function openPrintWindow(title, contentHtml) {
|
||||
const printWindow = window.open('', '_blank', 'noopener,noreferrer,width=1200,height=900');
|
||||
if (!printWindow) {
|
||||
alert('Unable to open the print preview window. Please allow pop-ups for this site.');
|
||||
return;
|
||||
}
|
||||
const style = `
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 24px; color: #212529; }
|
||||
h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
|
||||
p { margin-top: 0; color: #6c757d; }
|
||||
.card { border: 1px solid #dee2e6; border-radius: 0.5rem; margin-bottom: 1.5rem; overflow: hidden; }
|
||||
.card-header { background: #f8f9fa; padding: 0.9rem 1rem; font-weight: 700; }
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th, .table td { border: 1px solid #dee2e6; padding: 0.55rem 0.7rem; text-align: left; vertical-align: top; }
|
||||
.table thead th, .table tfoot th { background: #f8f9fa; }
|
||||
.badge { display: inline-block; padding: 0.2rem 0.45rem; border-radius: 999px; font-size: 0.8rem; font-weight: 700; }
|
||||
.bg-success { background: #198754; color: #fff; }
|
||||
.bg-danger { background: #dc3545; color: #fff; }
|
||||
.bg-secondary { background: #6c757d; color: #fff; }
|
||||
.text-muted { color: #6c757d; }
|
||||
.small { font-size: 0.875rem; }
|
||||
@media print {
|
||||
body { margin: 0; }
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
printWindow.document.open();
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${escapeHtml(title)}</title>
|
||||
${style}
|
||||
</head>
|
||||
<body>
|
||||
<h1>${escapeHtml(title)}</h1>
|
||||
<p>Generated on ${escapeHtml(new Date().toLocaleString())}</p>
|
||||
${contentHtml}
|
||||
<script>
|
||||
window.onload = function() {
|
||||
window.print();
|
||||
};
|
||||
<\/script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
}
|
||||
|
||||
function printEventCards(cards, title) {
|
||||
const printable = cards.map((card) => buildPrintableCard(card)).join('');
|
||||
openPrintWindow(title, printable);
|
||||
}
|
||||
|
||||
$('.sortable-header').on('click', function() {
|
||||
const $button = $(this);
|
||||
const table = $button.closest('table').get(0);
|
||||
const columnIndex = Number($button.data('sortIndex'));
|
||||
const sortType = String($button.data('sortType') || 'text');
|
||||
const currentColumn = Number(table.dataset.sortColumn);
|
||||
const currentDirection = table.dataset.sortDirection || 'asc';
|
||||
const direction = currentColumn === columnIndex && currentDirection === 'asc' ? 'desc' : 'asc';
|
||||
sortEventTable(table, columnIndex, sortType, direction);
|
||||
});
|
||||
|
||||
$('.print-event-card').on('click', function() {
|
||||
const eventId = String($(this).data('eventId'));
|
||||
const card = document.querySelector(`.event-card[data-event-id="${eventId}"]`);
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
const title = $(card).find('.card-header span').first().text().trim() || 'Event Participants';
|
||||
printEventCards([card], `${title} Participant List`);
|
||||
});
|
||||
|
||||
$('#printAllEventCharges').on('click', function() {
|
||||
const cards = Array.from(document.querySelectorAll('.event-card'));
|
||||
if (!cards.length) {
|
||||
return;
|
||||
}
|
||||
printEventCards(cards, 'All Event Participant Lists');
|
||||
});
|
||||
|
||||
$('#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 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;
|
||||
}
|
||||
submitExternalParticipant({
|
||||
firstname: firstName,
|
||||
lastname: lastName,
|
||||
note: note,
|
||||
parentFirstname: parentFirst,
|
||||
parentLastname: parentLast,
|
||||
parentPhone: parentPhone,
|
||||
parentEmail: parentEmail,
|
||||
eventId: eventId
|
||||
});
|
||||
clearModalInputs();
|
||||
});
|
||||
});
|
||||
</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: ?>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'])): ?>
|
||||
<div style="margin:0.75rem 0;"><?= $event['description'] ?></div>
|
||||
<?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>
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($activeEvents as $event): ?>
|
||||
<?php $externalParticipants = $externalParticipantsByEvent[$event['id']] ?? []; ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
|
||||
@@ -42,6 +43,53 @@
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h6 class="mb-1">Description</h6>
|
||||
<?php if (!empty($event['description'])): ?>
|
||||
<div class="mb-0"><?= $event['description'] ?></div>
|
||||
<?php else: ?>
|
||||
<p class="mb-0">No description</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($externalParticipants)): ?>
|
||||
<div class="mb-3">
|
||||
<h6 class="mb-2">External Participants</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Student Name</th>
|
||||
<th>Status</th>
|
||||
<th>Fee</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($externalParticipants as $participant): ?>
|
||||
<?php
|
||||
$isPaid = !empty($participant['event_paid']) || ((float) ($participant['charged'] ?? 0) <= 0);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<?= esc($participant['name']) ?> <span class="text-muted">(external)</span>
|
||||
<?php if (!empty($participant['note'])): ?>
|
||||
<small class="text-muted d-block"><?= esc($participant['note']) ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-<?= $isPaid ? 'success' : 'danger' ?>">
|
||||
<?= $isPaid ? 'Paid' : 'Unpaid' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-nowrap">$<?= esc(number_format((float) ($participant['charged'] ?? 0), 2)) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Participation Table -->
|
||||
<form method="post" action="<?= site_url('parent/updateParticipation') ?>">
|
||||
<?= csrf_field() ?>
|
||||
@@ -51,11 +99,8 @@
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Student First Name</th>
|
||||
<th>Student Last Name</th>
|
||||
<th>Student Name</th>
|
||||
<th class="text-center">Participate</th>
|
||||
<th>Description</th>
|
||||
<th>Event Fees</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -65,8 +110,7 @@
|
||||
$current = $charges[$key]['participation'] ?? '';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($student['firstname']) ?></td>
|
||||
<td><?= esc($student['lastname']) ?></td>
|
||||
<td><?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?></td>
|
||||
<td class="text-center align-middle">
|
||||
<div class="d-inline-flex align-items-center gap-3">
|
||||
<div class="form-check">
|
||||
@@ -83,8 +127,6 @@
|
||||
</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">
|
||||
|
||||
@@ -100,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>
|
||||
@@ -376,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>` :
|
||||
|
||||
@@ -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%">
|
||||
|
||||
@@ -89,12 +89,14 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
};
|
||||
?>
|
||||
|
||||
<h2 class="h5 mb-3">Your submissions</h2>
|
||||
<h2 class="h5 mb-1">Visible submissions</h2>
|
||||
<p class="text-muted small">Includes your uploads and submitted exam drafts from teachers assigned to the same class-section.</p>
|
||||
<?php if (!empty($drafts)): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped align-middle teacher-drafts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Teacher</th>
|
||||
<th>Class</th>
|
||||
<th>Title / type</th>
|
||||
<th>Ver.</th>
|
||||
@@ -111,6 +113,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
$badgeHtml = $renderBadge($st, $statusBadges);
|
||||
?>
|
||||
<tr data-draft-id="<?= (int) ($d['id'] ?? 0) ?>">
|
||||
<td><?= esc($d['teacher_display_name'] ?? 'Teacher') ?></td>
|
||||
<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>
|
||||
@@ -168,6 +171,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
<table class="table table-sm table-striped align-middle teacher-drafts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Teacher</th>
|
||||
<th>Class</th>
|
||||
<th>Title</th>
|
||||
<th>Ver.</th>
|
||||
@@ -183,6 +187,7 @@ $maxUploadBytes = $maxUploadBytes ?? (12 * 1024 * 1024);
|
||||
<?php foreach ($legacyExams as $d): ?>
|
||||
<?php $st = strtolower((string) ($d['status'] ?? '')); ?>
|
||||
<tr>
|
||||
<td><?= esc($d['teacher_display_name'] ?? 'Teacher') ?></td>
|
||||
<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>
|
||||
|
||||
@@ -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: 74 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Directory access is forbidden.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
a:3:{s:4:"time";i:1772401149;s:3:"ttl";i:300;s:4:"data";a:3:{i:0;a:12:{s:2:"id";s:2:"83";s:14:"menu_parent_id";N;s:5:"label";s:15:"TimeOff Request";s:3:"url";s:22:"/administrator/absence";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-11-01 20:58:55";s:10:"updated_at";s:19:"2025-11-01 20:58:55";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"85";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:17:"Print for teacher";s:3:"url";s:20:"admin/print-requests";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-07 04:29:16";s:10:"updated_at";s:19:"2026-01-07 04:29:16";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"86";s:14:"menu_parent_id";N;s:5:"label";s:11:"Competition";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:40:32";s:10:"updated_at";s:19:"2026-01-11 07:40:32";s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"87";s:14:"menu_parent_id";s:2:"86";s:5:"label";s:17:"Competition Setup";s:3:"url";s:19:"competition-winners";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:41:19";s:10:"updated_at";s:19:"2026-01-11 07:41:19";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}}}
|
||||
@@ -1 +0,0 @@
|
||||
a:3:{s:4:"time";i:1770699659;s:3:"ttl";i:300;s:4:"data";a:11:{i:0;a:12:{s:2:"id";s:2:"71";s:14:"menu_parent_id";s:1:"4";s:5:"label";s:16:"Staff Attendance";s:3:"url";s:30:"admin/teacher-attendance/month";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-27 04:29:24";s:10:"updated_at";s:19:"2025-09-28 05:39:08";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"73";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:24:"Whatsapp Link Management";s:3:"url";s:9:"whatsapp/";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-11 22:58:29";s:10:"updated_at";s:19:"2025-10-11 22:58:29";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"79";s:14:"menu_parent_id";s:1:"7";s:5:"label";s:16:"Class Distribute";s:3:"url";s:38:"administrator/sections/auto-distribute";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:41:13";s:10:"updated_at";s:19:"2025-10-26 06:41:13";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"83";s:14:"menu_parent_id";N;s:5:"label";s:15:"TimeOff Request";s:3:"url";s:22:"/administrator/absence";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-11-01 20:58:55";s:10:"updated_at";s:19:"2025-11-01 20:58:55";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"85";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:17:"Print for teacher";s:3:"url";s:20:"admin/print-requests";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-07 04:29:16";s:10:"updated_at";s:19:"2026-01-07 04:29:16";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"86";s:14:"menu_parent_id";N;s:5:"label";s:11:"Competition";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:40:32";s:10:"updated_at";s:19:"2026-01-11 07:40:32";s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"87";s:14:"menu_parent_id";s:2:"86";s:5:"label";s:17:"Competition Setup";s:3:"url";s:19:"competition-winners";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:41:19";s:10:"updated_at";s:19:"2026-01-11 07:41:19";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:6;a:12:{s:2:"id";s:2:"80";s:14:"menu_parent_id";s:1:"7";s:5:"label";s:22:"Section Auto Generator";s:3:"url";s:39:"administrator/sections/promotion-totals";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:42:02";s:10:"updated_at";s:19:"2025-10-26 06:42:02";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:7;a:12:{s:2:"id";s:2:"81";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:16:"Grading Tracking";s:3:"url";s:25:"grading/homework-tracking";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:43:11";s:10:"updated_at";s:19:"2025-10-26 14:35:52";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:8;a:12:{s:2:"id";s:2:"78";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:16:"Early Dismissals";s:3:"url";s:27:"attendance/early-dismissals";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:40:01";s:10:"updated_at";s:19:"2025-10-26 14:57:41";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:9;a:12:{s:2:"id";s:2:"77";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:18:"Late Slips History";s:3:"url";s:13:"slips/preview";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"8";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:38:42";s:10:"updated_at";s:19:"2025-10-26 06:38:42";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:10;a:12:{s:2:"id";s:2:"31";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:16:"Score Management";s:3:"url";s:7:"grading";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"10";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2026-01-26 05:11:48";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Directory access is forbidden.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1768344244;_ci_previous_url|s:58:"http://localhost:8080/administrator/administratordashboard";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1768323639;roles|a:4:{i:0;s:13:"administrator";i:1;s:12:"head of itpd";i:2;s:6:"parent";i:3;s:7:"teacher";}semester|s:4:"Fall";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|i:60;class_section_ids|a:1:{i:0;i:10;}
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1770267016;_ci_previous_url|s:44:"http://localhost:8080/parent/invoice_payment";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1770240160;roles|a:5:{i:0;s:13:"administrator";i:1;s:12:"head of itpd";i:2;s:6:"parent";i:3;s:9:"principal";i:4;s:7:"teacher";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";grading_selected_semester|s:6:"Spring";class_section_id|i:90;class_section_ids|a:1:{i:0;i:90;}
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1764570469;_ci_previous_url|s:72:"http://localhost:8080/teacher/showupdate_attendance?class_section_id=140";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1764550237;roles|a:4:{i:0;s:13:"administrator";i:1;s:12:"head of itpd";i:2;s:6:"parent";i:3;s:7:"teacher";}semester|s:4:"Fall";school_year|s:9:"2025-2026";role|s:7:"teacher";class_section_id|i:140;class_section_ids|a:2:{i:0;i:80;i:1;i:140;}
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1768450648;_ci_previous_url|s:44:"http://localhost:8080/teacher/print-requests";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1768447087;roles|a:13:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:11:"head of csm";i:6;s:10:"head of fa";i:7;s:11:"head of fes";i:8;s:10:"head of fs";i:9;s:12:"head of itpd";i:10;s:6:"parent";i:11;s:9:"principal";i:12;s:7:"teacher";}semester|s:4:"Fall";school_year|s:9:"2025-2026";role|s:7:"teacher";class_section_id|i:70;class_section_ids|a:1:{i:0;i:70;}
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1762298991;_ci_previous_url|s:53:"http://localhost:8080/reimbursements/under-processing";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;roles|a:11:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:11:"head of csm";i:6;s:10:"head of fa";i:7;s:11:"head of fes";i:8;s:10:"head of fs";i:9;s:12:"head of itpd";i:10;s:7:"teacher";}semester|s:4:"Fall";school_year|s:9:"2025-2026";role|s:13:"administrator";
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1761874246;_ci_previous_url|s:52:"http://localhost:8080/admin/teacher-attendance/month";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;roles|a:11:{i:0;s:12:"head of itpd";i:1;s:10:"head of fs";i:2;s:11:"head of fes";i:3;s:10:"head of fa";i:4;s:11:"head of csm";i:5;s:21:"financial_contributor";i:6;s:15:"fes_contributor";i:7;s:15:"csm_contributor";i:8;s:13:"administrator";i:9;s:5:"admin";i:10;s:7:"teacher";}semester|s:4:"Fall";school_year|s:9:"2025-2026";role|s:13:"administrator";style_color|s:6:"orange";menu_color|s:5:"white";
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1768965918;_ci_previous_url|s:60:"http://localhost:8080/administrator/teacher_class_assignment";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1768964969;roles|a:5:{i:0;s:13:"administrator";i:1;s:12:"head of itpd";i:2;s:6:"parent";i:3;s:9:"principal";i:4;s:7:"teacher";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}teacher_scores_selected_semester|s:6:"Spring";
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1761104379;_ci_previous_url|s:52:"http://localhost:8080/administrator/daily_attendance";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;roles|a:9:{i:0;s:21:"financial_contributor";i:1;s:10:"head of fs";i:2;s:15:"fes_contributor";i:3;s:11:"head of fes";i:4;s:15:"csm_contributor";i:5;s:13:"administrator";i:6;s:5:"admin";i:7;s:11:"head of csm";i:8;s:10:"head of fa";}semester|s:4:"Fall";school_year|s:9:"2025-2026";role|s:13:"administrator";flag_form_token|s:32:"0b12f89247258d8ed031b65cb4bb04ba";class_section_id|i:70;
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1769745875;_ci_previous_url|s:52:"http://localhost:8080/printables_reports/report_card";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1769743645;roles|a:5:{i:0;s:13:"administrator";i:1;s:12:"head of itpd";i:2;s:6:"parent";i:3;s:9:"principal";i:4;s:7:"teacher";}semester|s:4:"Fall";school_year|s:9:"2025-2026";role|s:13:"administrator";flag_form_token|s:32:"b8a56916890deca17bafa969fe0a957d";
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1761802220;_ci_previous_url|s:58:"http://localhost:8080/administrator/administratordashboard";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;roles|a:11:{i:0;s:12:"head of itpd";i:1;s:10:"head of fs";i:2;s:11:"head of fes";i:3;s:10:"head of fa";i:4;s:11:"head of csm";i:5;s:21:"financial_contributor";i:6;s:15:"fes_contributor";i:7;s:15:"csm_contributor";i:8;s:13:"administrator";i:9;s:5:"admin";i:10;s:7:"teacher";}semester|s:4:"Fall";school_year|s:9:"2025-2026";role|s:13:"administrator";style_color|s:4:"rose";menu_color|s:5:"white";
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1768448111;_ci_previous_url|s:43:"http://localhost:8080/parent/child_register";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1768424053;roles|a:4:{i:0;s:13:"administrator";i:1;s:12:"head of itpd";i:2;s:6:"parent";i:3;s:7:"teacher";}semester|s:4:"Fall";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|N;class_section_ids|a:1:{i:0;i:120;}__ci_vars|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
__ci_last_regenerate|i:1766871009;_ci_previous_url|s:58:"http://localhost:8080/grading/homework?class_section_id=10";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1766866262;roles|a:4:{i:0;s:13:"administrator";i:1;s:12:"head of itpd";i:2;s:6:"parent";i:3;s:7:"teacher";}semester|s:4:"Fall";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|i:10;
|
||||