Compare commits

..

1 Commits

Author SHA1 Message Date
root b32fb853f6 merge prod to main 2026-05-16 13:44:12 -04:00
7278 changed files with 1002620 additions and 647 deletions
Vendored
BIN
View File
Binary file not shown.
+5 -9
View File
@@ -26,7 +26,7 @@ SMTP_HOST = smtp.gmail.com
SMTP_USER = alrahma.sunday.school@gmail.com
SMTP_PASS = "psnp emdq dykw ypul"
SMTP_PORT = 587
SMTP_ENCRYPTION = tls # was ssl; use tls for port 587
SMTP_ENCRYPTION = tls # was SSL; set to ssl to match 587
# Reply-To for all mail profiles
MAIL_DEFAULT_REPLY_TO=alrahma.isgl@gmail.com
@@ -35,6 +35,7 @@ MAIL_COMMUNICATION_REPLY_TO=alrahma.isgl@gmail.com
MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications"
MAIL_PAYMENT_REPLY_TO=alrahma.isgl@gmail.com
MAIL_PAYMENT_REPLY_TO_NAME="School Payments"
MAIL_DEFAULT_REPLY_TO="alrahma.isgl@gmail.com"
# Optional sender directory you already use elsewhere
MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma No-Reply\"},\
@@ -43,11 +44,8 @@ MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al R
\"finance\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Finance Office\"}}"
# Principal notification recipient for TimeOff requests (comma-separated allowed)
PRINCIPAL_EMAIL="melabidi@alrahmaisgl.org"
MAIL_PARENT_REPORT_TO="alrahma.isgl@gmail.com"
MAIL_FROM_ADDRESS = "alrahma.sunday.school@gmail.com"
PRINCIPAL_EMAIL="principal@alrahmaisgl.org"
session.expiration = 43200
#--------------------------------------------------------------------
# APP
#--------------------------------------------------------------------
@@ -63,7 +61,7 @@ session.expiration = 43200
database.default.hostname = 127.0.0.1
database.default.database = school
database.default.username = root
database.default.password = rootpassword
database.default.password = root
database.default.DBDriver = MySQLi
database.default.DBPrefix =
database.default.port = 3306
@@ -157,9 +155,7 @@ database.default.port = 3306
# LOGGER
#--------------------------------------------------------------------
logger.threshold = 4
MAIL_DEBUG = true
MAIL_TIMEOUT = 15
# logger.threshold = 4
#--------------------------------------------------------------------
# CURLRequest
-39
View File
@@ -1,39 +0,0 @@
.env
.env.*
!.env.example
/vendor/
/node_modules/
/writable/cache/*
/writable/debugbar/*
/writable/logs/*
/writable/session/*
/writable/uploads/*
/writable/tmp/*
/writable/*.log
!/writable/.gitkeep
!/writable/cache/.gitkeep
!/writable/debugbar/.gitkeep
!/writable/logs/.gitkeep
!/writable/session/.gitkeep
!/writable/uploads/.gitkeep
!/writable/tmp/.gitkeep
/public/build/
/public/hot/
docker-compose.override.yml
docker-compose.local.yml
*.log
.DS_Store
Thumbs.db
.idea/
.vscode/
/coverage/
/build/
/phpunit.xml.cache
/.phpunit.result.cache
BIN
View File
Binary file not shown.
View File
View File
View File
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
View File
View File
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
+9 -3
View File
@@ -393,8 +393,8 @@ $routes->get('print-requests/file/(:segment)', 'PrintRequests::serveFile/$1', ['
$routes->get('print-requests/file/(:segment)/(:alpha)', 'PrintRequests::serveFile/$1/$2', ['filter' => 'auth:teacher,teacher_assistant,admin']);
$routes->get('uploads/print_requests/(:segment)', 'PrintRequests::serveFile/$1', ['filter' => 'auth:teacher,teacher_assistant,admin']);
$routes->get('exam-drafts/files/teacher/(:segment)', 'View\FilesController::examDraftTeacher/$1', ['filter' => 'auth:teacher,teacher_assistant,admin']);
$routes->get('exam-drafts/files/final/(:segment)', 'View\FilesController::examDraftFinal/$1', ['filter' => 'auth:teacher,teacher_assistant,admin']);
$routes->get('exam-drafts/files/teacher/(:segment)', 'View\FilesController::examDraftTeacher/$1', ['filter' => 'auth:teacher,teacher_assistant,admin,administrator,principal']);
$routes->get('exam-drafts/files/final/(:segment)', 'View\FilesController::examDraftFinal/$1', ['filter' => 'auth:teacher,teacher_assistant,admin,administrator,principal']);
@@ -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
@@ -742,6 +745,9 @@ $routes->post('/administrator/teacher-submissions/notify', 'View\AdministratorCo
$routes->get('/administrator/exam-drafts', 'View\ExamDraftController::adminIndex', ['filter' => 'auth:admin']);
$routes->post('/administrator/exam-drafts/review', 'View\ExamDraftController::adminReview', ['filter' => 'auth:admin']);
$routes->post('/administrator/exam-drafts/upload-legacy', 'View\ExamDraftController::adminUploadLegacy', ['filter' => 'auth:admin']);
$routes->get('/principal/exam-drafts', 'View\ExamDraftController::principalIndex', ['filter' => 'auth:admin']);
$routes->post('/principal/exam-drafts/review', 'View\ExamDraftController::principalReview', ['filter' => 'auth:admin']);
$routes->post('/principal/exam-drafts/upload-legacy', 'View\ExamDraftController::principalUploadLegacy', ['filter' => 'auth:admin']);
/*
* --------------------------------------------------------------------
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
View File
+74 -5
View File
@@ -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;
}
}
View File
View File
View File
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
+54 -2
View File
@@ -2003,6 +2003,58 @@ class AttendanceTrackingController extends BaseController
return [$subject, $body];
}
private function buildFallbackTemplate(string $code, string $variant, array $context): array
{
$studentName = (string) ($context['{{student_name}}'] ?? 'the student');
$incident = (string) ($context['{{incident_date}}'] ?? date('Y-m-d'));
$parentName = (string) ($context['{{parent_name}}'] ?? 'Parent/Guardian');
$phone = (string) ($context['{{school_phone}}'] ?? 'the school office');
$voicemail = (string) ($context['{{voicemail_phone}}'] ?? 'your voicemail');
$subject = match (true) {
str_starts_with($code, 'ABS_1') => 'Attendance Notice: Unreported Absence',
str_starts_with($code, 'ABS_2') => 'Attendance Follow-Up: Two Consecutive Absences',
str_starts_with($code, 'ABS_3') => 'Attendance Follow-Up: Three Absences',
str_starts_with($code, 'LATE_2') => 'Attendance Notice: Repeated Lateness',
str_starts_with($code, 'LATE_3'),
str_starts_with($code, 'LATE_4'),
str_starts_with($code, 'MIX') => 'Attendance Follow-Up: Repeated Lateness and Absence',
default => 'Attendance Notice',
};
$intro = "<p>Insha Allah this email finds you well";
if ($variant === 'answered') {
$intro .= ", <strong>{$parentName}</strong>. Jazakum Allahu khayran for taking the time to speak with us today.</p>";
} elseif ($variant === 'no_answer') {
$intro .= ".</p><p>We tried to reach you but could not connect and left a voice message at <strong>{$voicemail}</strong>.</p>";
} else {
$intro .= ".</p>";
}
$details = match (true) {
str_starts_with($code, 'ABS_1')
=> "<p>This is to let you know that <strong>{$studentName}</strong> was absent on <strong>{$incident}</strong> without prior notice.</p>",
str_starts_with($code, 'ABS_2')
=> "<p>This is a reminder that <strong>{$studentName}</strong> has had repeated absences, most recently on <strong>{$incident}</strong>, without prior notice.</p>",
str_starts_with($code, 'ABS_3')
=> "<p>This is a reminder that <strong>{$studentName}</strong> has been absent three times, most recently on <strong>{$incident}</strong>, without prior notice.</p>",
str_starts_with($code, 'LATE_2')
=> "<p>This is a reminder that <strong>{$studentName}</strong> has been late multiple times, most recently on <strong>{$incident}</strong>.</p>",
str_starts_with($code, 'LATE_3'),
str_starts_with($code, 'LATE_4')
=> "<p>This is a follow-up that <strong>{$studentName}</strong> has had repeated lateness concerns, most recently on <strong>{$incident}</strong>.</p>",
str_starts_with($code, 'MIX')
=> "<p>This is a follow-up that <strong>{$studentName}</strong> has had a mix of repeated lateness and absence concerns, most recently on <strong>{$incident}</strong>.</p>",
default
=> "<p>We'd like to inform you about <strong>{$studentName}</strong>'s recent attendance concern dated <strong>{$incident}</strong>.</p>",
};
$closing = "<p>If your child will be absent or late due to illness or family commitments, please let us know ahead of time.</p>"
. "<p>You can email/call/text us at <strong>{$phone}</strong>.</p>";
return [$subject, $intro . $details . $closing];
}
public function compose()
{
@@ -2033,8 +2085,8 @@ class AttendanceTrackingController extends BaseController
$rendered = $this->renderTemplate($code, $variant, $ctx);
if (!$rendered) {
return redirect()->to(site_url('attendance/violations'))
->with('error', 'Template not found for ' . $code . ' (' . $variant . ').');
log_message('warning', 'Attendance email template missing; using fallback compose body. code=' . $code . ' variant=' . $variant);
$rendered = $this->buildFallbackTemplate($code, $variant, $ctx);
}
[$subject, $body] = $rendered;
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
+5 -2
View File
@@ -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 = [
View File

Some files were not shown because too many files have changed in this diff Show More