add feature do not show again job for parent portal
This commit is contained in:
@@ -1290,6 +1290,7 @@ $routes->get('/landing_page/admin_dashboard', 'View\LandingPageController::admin
|
||||
$routes->get('/teacher_dashboard', 'View\LandingPageController::teacher', ['filter' => 'auth:teacher_dashboard,read']);
|
||||
$routes->get('/landing_page/student_dashboard', 'View\LandingPageController::student', ['filter' => 'auth:student_dashboard,read']);
|
||||
$routes->get('/parent_dashboard', 'View\LandingPageController::parentDashboard', ['filter' => 'auth:parent_dashboard,read']);
|
||||
$routes->post('/parent_dashboard/job-openings-popup', 'View\LandingPageController::hideJobOpeningsPopup', ['filter' => 'auth:parent_dashboard,read']);
|
||||
$routes->get('/landing_page/guest_dashboard', 'View\LandingPageController::guest', ['filter' => 'auth:guest_dashboard,read']);
|
||||
$routes->get('/dashboard', 'View\LandingPageController::index');
|
||||
$routes->get('/access_denied', 'ErrorController::accessDenied');
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Models\AttendanceRecordModel;
|
||||
use App\Models\AttendanceDayModel;
|
||||
use App\Models\CalendarModel;
|
||||
use App\Models\JobPositionModel;
|
||||
use App\Models\PreferencesModel;
|
||||
use \Config\Database;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
@@ -775,39 +776,6 @@ class LandingPageController extends BaseController
|
||||
}
|
||||
|
||||
|
||||
// Fetch Notifications (only active, non-expired, non-deleted)
|
||||
$notifications = $this->db->table('notifications')
|
||||
->select([
|
||||
'notifications.id',
|
||||
'notifications.title',
|
||||
'notifications.message',
|
||||
'notifications.target_group',
|
||||
'notifications.created_at',
|
||||
'notifications.expires_at',
|
||||
'user_notifications.user_id',
|
||||
"CASE
|
||||
WHEN user_notifications.user_id IS NOT NULL THEN 'personal'
|
||||
ELSE 'broadcast'
|
||||
END as notification_type"
|
||||
])
|
||||
->join(
|
||||
'user_notifications',
|
||||
'user_notifications.notification_id = notifications.id AND user_notifications.user_id = ' . (int) $parentId,
|
||||
'left'
|
||||
)
|
||||
->groupStart()
|
||||
->where('notifications.target_group', 'parent')
|
||||
->orWhere('user_notifications.user_id', $parentId)
|
||||
->groupEnd()
|
||||
->where('notifications.deleted_at IS NULL') // Exclude soft-deleted notifications
|
||||
->groupStart()
|
||||
->where('notifications.expires_at IS NULL')
|
||||
->orWhere('notifications.expires_at > NOW()') // Exclude expired
|
||||
->groupEnd()
|
||||
->orderBy('notifications.created_at', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
// Fetch Student Information (no filtering needed by school year or semester)
|
||||
|
||||
$students = $this->db->table('students')
|
||||
@@ -823,6 +791,8 @@ class LandingPageController extends BaseController
|
||||
}
|
||||
unset($student);
|
||||
|
||||
$notifications = $this->parentDashboardNotifications((int) $parentId, (int) session()->get('user_id'));
|
||||
|
||||
|
||||
// Fetch Attendance Records (filtered by most recent school year and semester)
|
||||
$attendanceData = $this->db->table('attendance_data')
|
||||
@@ -870,12 +840,14 @@ class LandingPageController extends BaseController
|
||||
$paymentBalance = (float) ($paymentRow['account_balance'] ?? 0);
|
||||
$openPositions = [];
|
||||
|
||||
try {
|
||||
if ($this->db->tableExists('job_positions')) {
|
||||
$openPositions = (new JobPositionModel())->openPositions();
|
||||
if (! $this->parentHidesJobOpeningsPopup($parentId)) {
|
||||
try {
|
||||
if ($this->db->tableExists('job_positions')) {
|
||||
$openPositions = (new JobPositionModel())->openPositions();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Unable to load parent dashboard job positions: ' . $e->getMessage());
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Unable to load parent dashboard job positions: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Pass data to the view, including the deadlines
|
||||
@@ -892,6 +864,255 @@ class LandingPageController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
public function hideJobOpeningsPopup()
|
||||
{
|
||||
$userId = (int) (session()->get('user_id') ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return $this->response->setStatusCode(401)->setJSON([
|
||||
'ok' => false,
|
||||
'error' => 'Please log in to update this setting.',
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
! $this->db->tableExists('user_preferences')
|
||||
|| ! $this->db->fieldExists('hide_job_openings_popup', 'user_preferences')
|
||||
) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => false,
|
||||
'error' => 'Preference storage is not ready.',
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
$hide = (string) $this->request->getPost('hide_job_openings_popup') === '1' ? 1 : 0;
|
||||
$preferencesModel = new PreferencesModel();
|
||||
$existing = $preferencesModel->where('user_id', $userId)->first();
|
||||
|
||||
if ($existing) {
|
||||
$preferencesModel->update((int) $existing['id'], [
|
||||
'hide_job_openings_popup' => $hide,
|
||||
]);
|
||||
} else {
|
||||
$preferencesModel->insert([
|
||||
'user_id' => $userId,
|
||||
'hide_job_openings_popup' => $hide,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => true,
|
||||
'hide_job_openings_popup' => $hide,
|
||||
'csrf_token' => csrf_token(),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function parentHidesJobOpeningsPopup(int $parentId): bool
|
||||
{
|
||||
if ($parentId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
! $this->db->tableExists('user_preferences')
|
||||
|| ! $this->db->fieldExists('hide_job_openings_popup', 'user_preferences')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$preferences = (new PreferencesModel())->where('user_id', $parentId)->first();
|
||||
|
||||
return ! empty($preferences['hide_job_openings_popup']);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Unable to load parent job openings popup preference: ' . $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function parentDashboardNotifications(int $parentId, int $userId): array
|
||||
{
|
||||
$notifications = array_merge(
|
||||
$this->parentAttendanceNotifications($parentId),
|
||||
$this->activeParentBroadcastNotifications($parentId, $userId)
|
||||
);
|
||||
|
||||
usort($notifications, static function (array $a, array $b): int {
|
||||
return strtotime((string) ($b['created_at'] ?? '')) <=> strtotime((string) ($a['created_at'] ?? ''));
|
||||
});
|
||||
|
||||
return array_slice($notifications, 0, 25);
|
||||
}
|
||||
|
||||
private function parentAttendanceNotifications(int $parentId): array
|
||||
{
|
||||
if (
|
||||
$parentId <= 0
|
||||
|| ! $this->db->tableExists('parent_notifications')
|
||||
|| ! $this->db->tableExists('students')
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->db->table('parent_notifications pn')
|
||||
->select([
|
||||
'pn.id',
|
||||
'pn.student_id',
|
||||
'pn.code',
|
||||
'pn.incident_date',
|
||||
'pn.channel',
|
||||
'pn.subject',
|
||||
'pn.status',
|
||||
'pn.response',
|
||||
'pn.semester',
|
||||
'pn.school_year',
|
||||
'pn.created_at',
|
||||
'pn.updated_at',
|
||||
'students.firstname',
|
||||
'students.lastname',
|
||||
])
|
||||
->join('students', 'students.id = pn.student_id')
|
||||
->where('students.parent_id', $parentId)
|
||||
->where('pn.school_year', (string) $this->schoolYear)
|
||||
->where('pn.semester', (string) $this->semester)
|
||||
->orderBy('COALESCE(pn.updated_at, pn.created_at)', 'DESC', false)
|
||||
->orderBy('pn.id', 'DESC')
|
||||
->limit(100)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$notifications = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$key = implode('|', [
|
||||
(string) ($row['student_id'] ?? ''),
|
||||
(string) ($row['code'] ?? ''),
|
||||
(string) ($row['incident_date'] ?? ''),
|
||||
(string) ($row['subject'] ?? ''),
|
||||
]);
|
||||
|
||||
if (isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$key] = true;
|
||||
$studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? ''));
|
||||
$code = strtoupper((string) ($row['code'] ?? ''));
|
||||
$incidentDate = (string) ($row['incident_date'] ?? '');
|
||||
$subject = trim((string) ($row['subject'] ?? ''));
|
||||
|
||||
$title = $subject !== '' ? $subject : $this->parentNotificationCodeLabel($code);
|
||||
if ($studentName !== '') {
|
||||
$title .= ' - ' . $studentName;
|
||||
}
|
||||
|
||||
$messageParts = [];
|
||||
if ($incidentDate !== '') {
|
||||
$messageParts[] = 'Incident date: ' . $incidentDate;
|
||||
}
|
||||
if (!empty($row['status'])) {
|
||||
$messageParts[] = 'Status: ' . ucfirst((string) $row['status']);
|
||||
}
|
||||
|
||||
$notifications[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'title' => $title,
|
||||
'message' => implode(' | ', $messageParts),
|
||||
'created_at' => $row['updated_at'] ?: ($row['created_at'] ?? null),
|
||||
'notification_type' => $this->parentNotificationCodeLabel($code),
|
||||
'status' => $row['status'] ?? null,
|
||||
'code' => $code,
|
||||
'incident_date' => $incidentDate,
|
||||
'student_name' => $studentName,
|
||||
];
|
||||
|
||||
if (count($notifications) >= 25) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
private function activeParentBroadcastNotifications(int $parentId, int $userId): array
|
||||
{
|
||||
if ($parentId <= 0 || ! $this->db->tableExists('notifications')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$builder = $this->db->table('notifications')
|
||||
->select([
|
||||
'notifications.id',
|
||||
'notifications.title',
|
||||
'notifications.message',
|
||||
'notifications.target_group',
|
||||
'notifications.created_at',
|
||||
'notifications.expires_at',
|
||||
"CASE
|
||||
WHEN user_notifications.user_id IS NOT NULL THEN 'personal'
|
||||
ELSE 'broadcast'
|
||||
END as notification_type",
|
||||
])
|
||||
->join(
|
||||
'user_notifications',
|
||||
'user_notifications.notification_id = notifications.id AND user_notifications.user_id IN (' . implode(',', array_unique([$parentId, $userId])) . ')',
|
||||
'left'
|
||||
)
|
||||
->groupStart()
|
||||
->whereIn('notifications.target_group', ['parent', 'everyone'])
|
||||
->orWhere('user_notifications.user_id IS NOT NULL')
|
||||
->groupEnd()
|
||||
->where('notifications.deleted_at IS NULL')
|
||||
->groupStart()
|
||||
->where('notifications.scheduled_at IS NULL')
|
||||
->orWhere('notifications.scheduled_at <=', utc_now())
|
||||
->groupEnd()
|
||||
->groupStart()
|
||||
->where('notifications.expires_at IS NULL')
|
||||
->orWhere('notifications.expires_at >', utc_now())
|
||||
->groupEnd();
|
||||
|
||||
if ($this->db->fieldExists('school_year', 'notifications')) {
|
||||
$builder->where('notifications.school_year', (string) $this->schoolYear);
|
||||
}
|
||||
|
||||
if ($this->db->fieldExists('semester', 'notifications')) {
|
||||
$builder->groupStart()
|
||||
->where('notifications.semester', null)
|
||||
->orWhere('notifications.semester', '')
|
||||
->orWhere('notifications.semester', (string) $this->semester)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
return $builder
|
||||
->groupBy('notifications.id')
|
||||
->orderBy('notifications.created_at', 'DESC')
|
||||
->limit(25)
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
private function parentNotificationCodeLabel(string $code): string
|
||||
{
|
||||
return match ($code) {
|
||||
'ABS_1' => 'Unreported absence',
|
||||
'ABS_2' => 'Repeated absences',
|
||||
'ABS_3' => 'Attendance warning',
|
||||
'ABS_4' => 'Attendance review',
|
||||
'LATE_2' => 'Repeated lateness',
|
||||
'LATE_3' => 'Lateness warning',
|
||||
'LATE_4' => 'Lateness review',
|
||||
'MIX_L2A1' => 'Attendance warning',
|
||||
default => 'Parent notice',
|
||||
};
|
||||
}
|
||||
|
||||
public function guest()
|
||||
{
|
||||
return view('/landing_page/guest_dashboard');
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddHideJobOpeningsPopupToPreferences extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (! $this->db->tableExists('user_preferences')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->db->fieldExists('hide_job_openings_popup', 'user_preferences')) {
|
||||
$definition = [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
'null' => false,
|
||||
];
|
||||
|
||||
if ($this->db->fieldExists('menu_custom_mode', 'user_preferences')) {
|
||||
$definition['after'] = 'menu_custom_mode';
|
||||
}
|
||||
|
||||
$this->forge->addColumn('user_preferences', [
|
||||
'hide_job_openings_popup' => $definition,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if (
|
||||
$this->db->tableExists('user_preferences')
|
||||
&& $this->db->fieldExists('hide_job_openings_popup', 'user_preferences')
|
||||
) {
|
||||
$this->forge->dropColumn('user_preferences', 'hide_job_openings_popup');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ final class SchoolYearWritableFilter implements FilterInterface
|
||||
'api/register',
|
||||
'user/select_role',
|
||||
'set-role',
|
||||
'parent_dashboard/job-openings-popup',
|
||||
'processForgotPassword',
|
||||
'user/forgot_password',
|
||||
'user/processResetPassword',
|
||||
|
||||
@@ -22,6 +22,7 @@ class PreferencesModel extends Model
|
||||
'menu_custom_bg', // Custom menu background color
|
||||
'menu_custom_text', // Custom menu text color
|
||||
'menu_custom_mode', // Custom menu mode: light|dark
|
||||
'hide_job_openings_popup', // Parent dismissed volunteer openings popup
|
||||
'created_at', // Timestamp of when the record was created
|
||||
'updated_at' // Timestamp of when the record was last updated
|
||||
];
|
||||
|
||||
@@ -71,14 +71,18 @@
|
||||
<ul class="mb-0 ps-3">
|
||||
<?php foreach ($notifications as $n): ?>
|
||||
<li class="mb-2">
|
||||
<strong><?= esc($n['title']) ?></strong><br>
|
||||
<strong><?= esc($n['title'] ?? 'Notification') ?></strong><br>
|
||||
<small class="text-muted">
|
||||
<?= isset($n['created_at']) && strtotime($n['created_at'])
|
||||
? esc(local_datetime($n['created_at'], 'm-d-Y H:i'))
|
||||
: 'No date' ?>
|
||||
</small>
|
||||
<?php if (!empty($n['message'] ?? '')): ?>
|
||||
<br>
|
||||
<span><?= esc($n['message']) ?></span>
|
||||
<?php endif; ?>
|
||||
<br>
|
||||
<span class="badge bg-light text-dark"><?= ucfirst($n['notification_type'] ?? 'broadcast') ?></span>
|
||||
<span class="badge bg-light text-dark"><?= esc(ucfirst((string) ($n['notification_type'] ?? 'notice'))) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
@@ -256,6 +260,20 @@
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-check me-auto text-start">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
value="1"
|
||||
id="hideJobOpeningsPopup"
|
||||
data-save-url="<?= esc(site_url('parent_dashboard/job-openings-popup')) ?>"
|
||||
data-csrf-name="<?= esc(csrf_token()) ?>"
|
||||
data-csrf-value="<?= esc(csrf_hash()) ?>">
|
||||
<label class="form-check-label" for="hideJobOpeningsPopup">
|
||||
Do not show again
|
||||
</label>
|
||||
<div class="small text-muted d-none" id="hideJobOpeningsPopupStatus"></div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="<?= site_url('careers') ?>">View all openings</a>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
@@ -276,6 +294,68 @@
|
||||
}
|
||||
|
||||
bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
||||
|
||||
const checkbox = document.getElementById('hideJobOpeningsPopup');
|
||||
const statusEl = document.getElementById('hideJobOpeningsPopupStatus');
|
||||
if (!checkbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkbox.addEventListener('change', async function () {
|
||||
if (!checkbox.checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkbox.disabled = true;
|
||||
if (statusEl) {
|
||||
statusEl.classList.remove('d-none', 'text-danger');
|
||||
statusEl.classList.add('text-muted');
|
||||
statusEl.textContent = 'Saving preference...';
|
||||
}
|
||||
|
||||
const body = new FormData();
|
||||
let csrfName = checkbox.dataset.csrfName || '';
|
||||
let csrfValue = checkbox.dataset.csrfValue || '';
|
||||
body.append('hide_job_openings_popup', '1');
|
||||
if (csrfName && csrfValue) {
|
||||
body.append(csrfName, csrfValue);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(checkbox.dataset.saveUrl || '', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
...(csrfValue ? { 'X-CSRF-TOKEN': csrfValue } : {})
|
||||
},
|
||||
body
|
||||
});
|
||||
const data = await response.json().catch(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
if (data.csrf_token && data.csrf_hash) {
|
||||
checkbox.dataset.csrfName = data.csrf_token;
|
||||
checkbox.dataset.csrfValue = data.csrf_hash;
|
||||
}
|
||||
|
||||
if (!response.ok || !data.ok) {
|
||||
throw new Error(data.error || 'Unable to save preference.');
|
||||
}
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'Saved.';
|
||||
}
|
||||
} catch (error) {
|
||||
checkbox.checked = false;
|
||||
checkbox.disabled = false;
|
||||
if (statusEl) {
|
||||
statusEl.classList.remove('d-none', 'text-muted');
|
||||
statusEl.classList.add('text-danger');
|
||||
statusEl.textContent = error.message || 'Unable to save preference.';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
Reference in New Issue
Block a user