Files
alrahma_sunday_school/app/Views/administrator/enrollment_admin_dashboard.php
T
root 0a31aa1393
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Successful in 1m16s
fix is_active student flag logic and make moving with enrollment status
2026-08-20 03:11:37 -04:00

910 lines
57 KiB
PHP

<?= $this->extend('layout/management_layout') ?>
<?= $this->section('styles') ?>
<style>
.enrollment-admin .nav-tabs .nav-link {
font-weight: 600;
}
.enrollment-admin .nav-tabs .nav-link .tab-count {
font-weight: 600;
}
.enrollment-admin .step-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.45rem;
height: 1.45rem;
border-radius: 50%;
background: #0d6efd;
color: #fff;
font-size: .75rem;
font-weight: 700;
margin-right: .4rem;
}
.enrollment-admin .step-badge.done {
background: #198754;
}
.enrollment-admin .step-badge.blocked {
background: #dc3545;
}
.enrollment-admin .action-stack .form-control,
.enrollment-admin .action-stack .form-select {
max-width: 18rem;
}
.enrollment-admin .email-check {
display: flex;
gap: 1rem;
align-items: flex-start;
padding: 1rem 0;
border-bottom: 1px solid #dee2e6;
}
.enrollment-admin .email-check:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.enrollment-admin .email-check .email-check-body {
flex: 1 1 auto;
}
</style>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?php
if (!function_exists('enrollment_admin_flag_label')) {
function enrollment_admin_flag_label(string $type): string
{
return match ($type) {
'CLASS_REASSIGNMENT_REQUIRED' => 'Assign a class',
'PENDING_MAKE_UP_EXAM_PROMOTION' => 'Makeup exam',
'AGE_EXCEPTION_REQUIRED' => 'Age exception',
'LATE_REGISTRATION_EXCEPTION' => 'Late registration',
'FINANCIAL_REVIEW_REQUIRED' => 'Finance review',
'CLASS_CAPACITY_EXCEPTION_REQUIRED' => 'Class capacity',
'DEFERRED_DELIBERATION' => 'Missing decision',
'RESTRICTED_ADMINISTRATIVE_REVIEW' => 'Restricted review',
'WITHDRAWAL_REVIEW_REQUIRED' => 'Withdrawal review',
'COMPLETION_OR_EXIT_PROCESS_REQUIRED' => 'Exit / completion',
'ADULT_STUDENT_ACTION_REQUIRED' => 'Adult student',
'SIBLING_LAST_NAME_MISMATCH' => 'Sibling last names',
default => $type !== '' ? ucwords(strtolower(str_replace('_', ' ', $type))) : 'Unknown',
};
}
}
if (!function_exists('enrollment_admin_placement_label')) {
function enrollment_admin_placement_label(string $status): string
{
return match ($status) {
'automatic_distribution_pending' => 'Awaiting class assignment',
'manual_class_required' => 'Needs a class assigned',
'temporary_manual_class_required' => 'Needs a temporary class',
'same_class_assigned' => 'Same class assigned',
'temporary_same_grade' => 'Temporary same grade',
'exit_required' => 'Exit / completion',
'not_created' => 'Not created',
default => $status !== '' ? ucwords(str_replace('_', ' ', $status)) : 'Pending',
};
}
}
if (!function_exists('enrollment_admin_rule_label')) {
function enrollment_admin_rule_label(string $code): string
{
return match ($code) {
'STUDENT_NOT_LINKED' => 'Student is not linked to this parent',
'SOURCE_YEAR_NOT_FOUND' => 'Student was not in the previous year',
'TARGET_YEAR_NOT_FOUND' => 'School year was not found',
'ALREADY_ENROLLED' => 'Already enrolled',
'SIBLING_LAST_NAME_MISMATCH' => 'Sibling last names do not match',
'OUTSTANDING_BALANCE_BLOCKED' => 'Previous-year balance must be paid',
'FINANCE_APPROVAL_REQUIRED' => 'Finance approval required',
'AGE_RULE_BLOCKED' => 'Age rule not met',
'REGISTRATION_CLOSED' => 'Registration is closed',
'REGISTRATION_NOT_OPEN' => 'Registration is not open yet',
'ADULT_STUDENT_PARENT_BLOCKED' => 'Adult student cannot be enrolled by a parent',
'EXIT_REQUIRED' => 'Exit / completion required',
'EXPELLED' => 'Expelled — administrative review',
'WITHDRAWN' => 'Withdrawn — review required',
'NO_FINAL_DECISION' => 'No final academic decision',
'UNRECOGNIZED_DECISION' => 'Unrecognized academic decision',
'DEFERRED_DECISION' => 'Deferred academic decision',
'REVIEW_REQUIRED' => 'Staff review required',
'LAST_NAME_EXCEPTION_CARRIED_FORWARD' => 'Last-name exception carried forward',
'PASSED' => 'Passed',
'REPEAT_CLASS' => 'Repeat class',
'MAKE_UP_EXAM' => 'Makeup exam',
default => enrollment_admin_flag_label($code),
};
}
}
if (!function_exists('enrollment_admin_decision_label')) {
function enrollment_admin_decision_label(string $decision): string
{
return match ($decision) {
'ELIGIBLE' => 'Can enroll',
'ELIGIBLE_WITH_WARNING' => 'Can enroll, with follow-up',
'INELIGIBLE' => 'Cannot enroll',
'REVIEW_REQUIRED' => 'Needs staff review',
'EXCEPTION_ELIGIBLE' => 'Can enroll with exception',
'ALREADY_ENROLLED' => 'Already enrolled',
default => $decision !== '' ? ucwords(strtolower(str_replace('_', ' ', $decision))) : 'Pending',
};
}
}
if (!function_exists('enrollment_admin_priority_label')) {
function enrollment_admin_priority_label(string $priority): string
{
return match (strtolower($priority)) {
'high' => 'High',
'low' => 'Low',
default => 'Normal',
};
}
}
if (!function_exists('enrollment_admin_exception_status_label')) {
function enrollment_admin_exception_status_label(string $status): string
{
return match ($status) {
'active' => 'Active',
'used' => 'Used',
'revoked' => 'Revoked',
'expired' => 'Expired',
default => $status !== '' ? ucfirst($status) : 'Unknown',
};
}
}
if (!function_exists('enrollment_admin_audit_action_label')) {
function enrollment_admin_audit_action_label(string $action): string
{
return match ($action) {
'flag_resolved' => 'Closed a work item',
'manual_class_assignment' => 'Assigned a class',
'make_up_exam_promotion_completed' => 'Recorded a passing makeup exam',
'make_up_exam_no_promotion_required' => 'Recorded a failed makeup exam',
'enrollment_exception_created' => 'Granted an exception',
'enrollment_exception_revoked' => 'Revoked an exception',
'initial_transition_applied' => 'Applied year transition',
'transition_evaluation_blocked' => 'Transition blocked',
default => $action !== '' ? ucwords(str_replace('_', ' ', $action)) : '',
};
}
}
if (!function_exists('enrollment_admin_reason_label')) {
function enrollment_admin_reason_label(string $code, array $reasonCodes): string
{
if (isset($reasonCodes[$code]) && is_string($reasonCodes[$code]) && $reasonCodes[$code] !== '') {
return $reasonCodes[$code];
}
return enrollment_admin_rule_label($code);
}
}
$selectedParentId = (int) ($exceptionSelectedParentId ?? 0);
$preview = $exceptionPreview ?? null;
$nonOverridableCodes = ['STUDENT_NOT_LINKED', 'SOURCE_YEAR_NOT_FOUND', 'TARGET_YEAR_NOT_FOUND', 'ALREADY_ENROLLED'];
$previewStudents = is_array($preview['students'] ?? null) ? $preview['students'] : [];
$hasGrantableStudent = false;
$openFlagCount = (int) ($openFlagCount ?? 0);
$followupCount = count($enrollmentFollowups ?? []);
$activeExceptionCount = 0;
foreach (($activeExceptions ?? []) as $exceptionRow) {
if (($exceptionRow['status'] ?? '') === 'active') {
$activeExceptionCount++;
}
}
$emailExampleCount = count($emailExamples ?? []);
$launchReady = empty($launchState['missing']);
$launchApproved = !empty($launchState['approved']);
$defaultTab = 'work';
$emailStepLabel = $launchApproved ? 'Ready' : ($launchReady ? 'Review' : 'Blocked');
$reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes : [];
?>
<div class="container-fluid py-4 enrollment-admin">
<div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-3">
<div>
<h2 class="mb-1">Enrollment Administration</h2>
<p class="text-muted mb-0">Work these steps in order: clear today's queue, grant any family exceptions, then send parent emails.</p>
</div>
<form method="get" action="<?= site_url('administrator/enrollment-admin') ?>" class="d-flex align-items-end gap-2">
<?php if ($selectedParentId > 0): ?>
<input type="hidden" name="exception_parent_id" value="<?= $selectedParentId ?>">
<?php endif; ?>
<div>
<label class="form-label mb-1" for="school_year">School year</label>
<select class="form-select" id="school_year" name="school_year" onchange="this.form.submit()">
<?php foreach (($schoolYears ?? []) as $year): ?>
<?php $name = (string) ($year['name'] ?? ''); ?>
<option value="<?= esc($name) ?>" <?= $name === ($schoolYear ?? '') ? 'selected' : '' ?>><?= esc($name) ?></option>
<?php endforeach; ?>
</select>
</div>
</form>
</div>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<ul class="nav nav-tabs mb-3" id="enrollmentAdminTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link <?= $defaultTab === 'work' ? 'active' : '' ?>" id="work-tab" data-bs-toggle="tab" data-bs-target="#work-pane" type="button" role="tab">
1. Work queue
<span class="badge rounded-pill <?= $openFlagCount > 0 ? 'bg-primary' : 'bg-secondary' ?> tab-count"><?= $openFlagCount ?></span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="followup-tab" data-bs-toggle="tab" data-bs-target="#followup-pane" type="button" role="tab">
2. Follow-up
<span class="badge rounded-pill bg-secondary tab-count"><?= $followupCount ?></span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="emails-tab" data-bs-toggle="tab" data-bs-target="#emails-pane" type="button" role="tab">
3. Parent emails
<span class="badge rounded-pill <?= $launchApproved ? 'bg-success' : ($launchReady ? 'bg-info text-dark' : 'bg-danger') ?> tab-count"><?= esc($emailStepLabel) ?></span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="history-tab" data-bs-toggle="tab" data-bs-target="#history-pane" type="button" role="tab">History</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade <?= $defaultTab === 'work' ? 'show active' : '' ?>" id="work-pane" role="tabpanel">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2 mb-3">
<div>
<h3 class="h5 mb-1">Today's work queue</h3>
<p class="text-muted mb-0">Start here. These are students who currently cannot complete re-enrollment, plus makeup and class follow-ups.</p>
</div>
</div>
<form method="get" action="<?= site_url('administrator/enrollment-admin') ?>" class="row g-2 align-items-end mb-3">
<input type="hidden" name="school_year" value="<?= esc($schoolYear ?? '') ?>">
<div class="col-md-3">
<label class="form-label" for="status">Status</label>
<select class="form-select" id="status" name="status">
<?php foreach (['open' => 'Open', 'resolved' => 'Resolved'] as $value => $label): ?>
<option value="<?= esc($value) ?>" <?= $value === ($status ?? '') ? 'selected' : '' ?>><?= esc($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="flag_type">Issue type</label>
<select class="form-select" id="flag_type" name="flag_type">
<option value="">All issue types</option>
<?php foreach (($flagTypes ?? []) as $type): ?>
<option value="<?= esc($type) ?>" <?= $type === ($flagType ?? '') ? 'selected' : '' ?>><?= esc(enrollment_admin_flag_label((string) $type)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label" for="assigned_to">Assigned to</label>
<select class="form-select" id="assigned_to" name="assigned_to">
<option value="">Anyone</option>
<?php foreach (($admins ?? []) as $admin): ?>
<?php
$adminId = (int) ($admin['id'] ?? 0);
$adminName = trim((string) ($admin['firstname'] ?? '') . ' ' . (string) ($admin['lastname'] ?? '')) ?: 'User #' . $adminId;
?>
<option value="<?= $adminId ?>" <?= (string) $adminId === (string) ($assignedTo ?? '') ? 'selected' : '' ?>><?= esc($adminName) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-outline-primary w-100">Apply filters</button>
</div>
</form>
<?php if (($status ?? '') === 'resolved'): ?>
<div class="alert alert-info">Showing closed items. Switch Status back to Open to see today's queue.</div>
<?php endif; ?>
<div class="table-responsive mb-4">
<table id="enrollmentFlagsTable" class="table table-bordered table-striped align-middle enrollment-admin-datatable no-mgmt-sticky no-dt-fixedheader" data-no-mgmt-sticky data-no-dt-fixedheader>
<thead>
<tr>
<th>Student</th>
<th>School ID</th>
<th>Issue</th>
<th>Priority</th>
<th>Details</th>
<th>Assigned</th>
<th>Created</th>
<th class="no-sort" style="min-width: 280px;">What to do</th>
</tr>
</thead>
<tbody>
<?php if (!empty($flags)): ?>
<?php foreach ($flags as $flag): ?>
<?php
$flagId = (int) ($flag['id'] ?? 0);
$flagTypeValue = (string) ($flag['flag_type'] ?? '');
$details = is_array($flag['details'] ?? null) ? $flag['details'] : [];
$priorityValue = (string) ($flag['priority'] ?? 'normal');
$priorityClass = strtolower($priorityValue) === 'high' ? 'bg-danger' : 'bg-secondary';
?>
<tr>
<td><?= esc($flag['student_name'] ?? '') ?></td>
<td><?= esc($flag['school_id'] ?? '') ?></td>
<td><span class="badge bg-secondary"><?= esc(enrollment_admin_flag_label($flagTypeValue)) ?></span></td>
<td><span class="badge <?= esc($priorityClass) ?>"><?= esc(enrollment_admin_priority_label($priorityValue)) ?></span></td>
<td>
<?php if ($details !== []): ?>
<?php foreach ($details as $key => $value): ?>
<div><span class="text-muted"><?= esc(ucwords(str_replace('_', ' ', (string) $key))) ?>:</span> <?= esc(is_scalar($value) ? (string) $value : json_encode($value)) ?></div>
<?php endforeach; ?>
<?php else: ?>
<span class="text-muted">No extra details</span>
<?php endif; ?>
</td>
<td><?= esc($flag['assignee_name'] ?: '') ?></td>
<td><?= esc(!empty($flag['created_at']) ? local_datetime($flag['created_at'], 'm-d-Y H:i') : '') ?></td>
<td class="action-stack">
<?php if (($flag['status'] ?? '') === 'open'): ?>
<?php if ($flagTypeValue === 'CLASS_REASSIGNMENT_REQUIRED'): ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/flags/' . $flagId . '/assign-class') ?>" class="mb-2">
<?= csrf_field() ?>
<div class="small fw-semibold mb-1">Assign this student a class</div>
<select class="form-select form-select-sm mb-2" name="class_section_id" required>
<option value="">Select class section</option>
<?php foreach (($classSections ?? []) as $section): ?>
<option value="<?= (int) ($section['class_section_id'] ?? 0) ?>"><?= esc($section['class_section_name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
<input class="form-control form-control-sm mb-2" name="resolution_notes" placeholder="Reason / notes" required>
<button class="btn btn-sm btn-primary" type="submit">Assign class</button>
</form>
<?php elseif ($flagTypeValue === 'PENDING_MAKE_UP_EXAM_PROMOTION'): ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/flags/' . $flagId . '/makeup-promotion') ?>" class="mb-2">
<?= csrf_field() ?>
<div class="small fw-semibold mb-1">Record the makeup exam</div>
<select class="form-select form-select-sm mb-2" name="exam_result" required>
<option value="">Exam result</option>
<option value="passed">Passed</option>
<option value="failed">Failed</option>
</select>
<select class="form-select form-select-sm mb-2" name="class_section_id">
<option value="">Promoted class if passed</option>
<?php foreach (($classSections ?? []) as $section): ?>
<option value="<?= (int) ($section['class_section_id'] ?? 0) ?>"><?= esc($section['class_section_name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
<input class="form-control form-control-sm mb-2" name="resolution_notes" placeholder="Resolution notes" required>
<button class="btn btn-sm btn-primary" type="submit">Save exam result</button>
</form>
<?php elseif (in_array($flagTypeValue, ['AGE_EXCEPTION_REQUIRED', 'LATE_REGISTRATION_EXCEPTION', 'FINANCIAL_REVIEW_REQUIRED', 'CLASS_CAPACITY_EXCEPTION_REQUIRED', 'SIBLING_LAST_NAME_MISMATCH', 'DEFERRED_DELIBERATION', 'WITHDRAWAL_REVIEW_REQUIRED', 'RESTRICTED_ADMINISTRATIVE_REVIEW'], true) && !empty($canManageEnrollmentExceptions)): ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/flags/' . $flagId . '/approve-exception') ?>" class="mb-2">
<?= csrf_field() ?>
<div class="small fw-semibold mb-1">Approve an exception for this student</div>
<input class="form-control form-control-sm mb-2" name="reason" placeholder="Approval reason" required>
<button class="btn btn-sm btn-warning" type="submit">Approve exception</button>
</form>
<?php endif; ?>
<?php else: ?>
<div class="text-muted small"><?= esc($flag['resolution_notes'] ?? '') ?></div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="followup-pane" role="tabpanel">
<h3 class="h5 mb-1">Enrollments still waiting on staff</h3>
<p class="text-muted">These students already have an enrollment record. If a class or exception is needed, handle it in Work queue.</p>
<div class="table-responsive mb-4">
<table id="enrollmentFollowupTable" class="table table-bordered table-striped align-middle enrollment-admin-datatable no-mgmt-sticky no-dt-fixedheader" data-no-mgmt-sticky data-no-dt-fixedheader>
<thead>
<tr>
<th>Student</th>
<th>School ID</th>
<th>Enrollment status</th>
<th>Decision</th>
<th>Placement</th>
<th>Class</th>
<th>Exception / reason</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
<?php if (!empty($enrollmentFollowups)): ?>
<?php foreach ($enrollmentFollowups as $row): ?>
<?php
$statusValue = (string) ($row['enrollment_status'] ?? '');
$placementValue = (string) ($row['placement_status'] ?? '');
$decisionValue = (string) ($row['deliberation_decision'] ?? '');
$statusClass = match (strtolower($statusValue)) {
'review & decision' => 'bg-warning text-dark',
'waitlist' => 'bg-info text-dark',
'denied' => 'bg-danger',
default => 'bg-secondary',
};
?>
<tr>
<td><?= esc($row['student_name'] ?? '') ?></td>
<td><?= esc($row['school_id'] ?? '') ?></td>
<td><span class="badge <?= esc($statusClass) ?>"><?= esc($statusValue) ?></span></td>
<td><?= esc($decisionValue !== '' ? ucwords(str_replace('_', ' ', $decisionValue)) : 'Pending') ?></td>
<td><?= esc(enrollment_admin_placement_label($placementValue)) ?></td>
<td><?= esc($row['class_section_name'] ?? '') ?></td>
<td>
<?php if (!empty($row['exception_required'])): ?>
<span class="badge bg-warning text-dark">Exception</span>
<?php endif; ?>
<?php
$exceptionReason = (string) ($row['exception_reason'] ?? '');
$reasonParts = array_values(array_filter(array_map('trim', explode(',', $exceptionReason))));
$looksLikeCodes = $reasonParts !== [] && count(array_filter($reasonParts, static fn (string $part): bool => (bool) preg_match('/^[A-Z0-9_]+$/', $part))) === count($reasonParts);
?>
<div class="small"><?= esc($looksLikeCodes ? implode(', ', array_map('enrollment_admin_flag_label', $reasonParts)) : $exceptionReason) ?></div>
</td>
<td><?= esc(!empty($row['updated_at']) ? local_datetime($row['updated_at'], 'm-d-Y H:i') : '') ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="exceptions-pane" role="tabpanel">
<h3 class="h5 mb-1">Family exception tools</h3>
<p class="text-muted">Use the lookup to grant a family exception after reviewing the matching work queue item. Saved exceptions stay here for audit and revocation.</p>
<div class="row g-4">
<div class="col-xl-6">
<div class="border rounded p-3 h-100">
<h4 class="h6 mb-3">Look up a family</h4>
<?php if (empty($canManageEnrollmentExceptions)): ?>
<div class="alert alert-warning mb-0">You do not have permission to manage enrollment exceptions.</div>
<?php else: ?>
<form method="get" action="<?= site_url('administrator/enrollment-admin') ?>" class="row g-2 align-items-end mb-3">
<input type="hidden" name="school_year" value="<?= esc($schoolYear ?? '') ?>">
<input type="hidden" name="status" value="<?= esc($status ?? '') ?>">
<input type="hidden" name="flag_type" value="<?= esc($flagType ?? '') ?>">
<input type="hidden" name="assigned_to" value="<?= esc($assignedTo ?? '') ?>">
<div class="col-lg-8">
<label class="form-label" for="exception_parent_id">Parent</label>
<select class="form-select" id="exception_parent_id" name="exception_parent_id" required>
<option value="">Select parent</option>
<?php foreach (($exceptionFamilies ?? []) as $family): ?>
<?php $familyParentId = (int) ($family['parent_id'] ?? 0); ?>
<option value="<?= $familyParentId ?>" <?= $familyParentId === $selectedParentId ? 'selected' : '' ?>>
<?= esc($family['parent_name'] ?? ('Parent #' . $familyParentId)) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-lg-4">
<button type="submit" class="btn btn-primary w-100">Review family</button>
</div>
</form>
<?php if (is_array($preview) && !empty($preview['error'])): ?>
<div class="alert alert-danger mb-0"><?= esc($preview['error']) ?></div>
<?php elseif (is_array($preview) && $previewStudents !== []): ?>
<div class="mb-3">
<div class="fw-semibold"><?= esc($preview['parent_name'] ?? '') ?></div>
<div class="small text-muted">
<?= esc(($preview['source_school_year'] ?? '') . ' to ' . ($preview['school_year'] ?? '')) ?>
<?php if (!empty($preview['parent_email'])): ?>
&middot; <?= esc($preview['parent_email']) ?>
<?php endif; ?>
</div>
</div>
<form method="post" action="<?= site_url('administrator/enrollment-admin/exceptions/create') ?>">
<?= csrf_field() ?>
<input type="hidden" name="parent_id" value="<?= (int) ($preview['parent_id'] ?? 0) ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear ?? '') ?>">
<div class="table-responsive mb-3">
<table id="enrollmentFamilyEligibilityTable" class="table table-sm table-bordered align-middle enrollment-admin-datatable no-mgmt-sticky no-dt-fixedheader" data-no-mgmt-sticky data-no-dt-fixedheader>
<thead>
<tr>
<th class="no-sort">Select</th>
<th>Student</th>
<th>Decision</th>
<th>Why they are blocked</th>
<th class="no-sort">Rules to bypass</th>
<th>Warnings</th>
</tr>
</thead>
<tbody>
<?php foreach ($previewStudents as $studentPreview): ?>
<?php
$studentId = (int) ($studentPreview['student_id'] ?? 0);
$evaluation = is_array($studentPreview['evaluation'] ?? null) ? $studentPreview['evaluation'] : [];
$blockingCodes = array_values(array_filter(array_map('strval', $evaluation['blocking_rule_codes'] ?? [])));
$reviewCodes = array_values(array_filter(array_map('strval', $evaluation['review_rule_codes'] ?? [])));
$warningCodes = array_values(array_filter(array_map('strval', $evaluation['warning_rule_codes'] ?? [])));
$failedCodes = array_values(array_unique(array_merge($blockingCodes, $reviewCodes)));
$overridableCodes = array_values(array_diff($failedCodes, $nonOverridableCodes));
$hasGrantableStudent = $hasGrantableStudent || $overridableCodes !== [];
$decision = (string) ($evaluation['decision'] ?? '');
$canEnroll = !empty($evaluation['can_enroll']);
?>
<tr>
<td>
<?php if ($overridableCodes !== []): ?>
<input class="form-check-input" type="checkbox" name="student_ids[]" value="<?= $studentId ?>">
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<?= esc($studentPreview['student_name'] ?? '') ?>
<div class="small text-muted"><?= esc($studentPreview['school_id'] ?? '') ?></div>
</td>
<td><span class="badge <?= $canEnroll ? 'bg-success' : 'bg-warning text-dark' ?>"><?= esc(enrollment_admin_decision_label($decision)) ?></span></td>
<td>
<?php foreach (($evaluation['blockers'] ?? []) as $message): ?>
<div class="small"><?= esc($message) ?></div>
<?php endforeach; ?>
</td>
<td>
<?php if ($failedCodes !== []): ?>
<?php foreach ($failedCodes as $code): ?>
<?php $isBypassable = !in_array($code, $nonOverridableCodes, true); ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="bypassed_rule_codes_by_student[<?= $studentId ?>][]" id="bypass_<?= $studentId ?>_<?= esc($code) ?>" value="<?= esc($code) ?>" <?= $isBypassable ? 'checked' : 'disabled' ?>>
<label class="form-check-label small" for="bypass_<?= $studentId ?>_<?= esc($code) ?>">
<?= esc(enrollment_admin_rule_label($code)) ?><?= $isBypassable ? '' : ' (cannot bypass)' ?>
</label>
</div>
<?php endforeach; ?>
<?php else: ?>
<span class="text-muted small">None</span>
<?php endif; ?>
</td>
<td>
<?php if ($warningCodes !== []): ?>
<?php foreach ($warningCodes as $code): ?>
<div class="small"><?= esc(enrollment_admin_rule_label($code)) ?></div>
<?php endforeach; ?>
<?php else: ?>
<span class="text-muted small">None</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($hasGrantableStudent): ?>
<h4 class="h6 mb-3">Grant selected exceptions</h4>
<div class="row g-2">
<div class="col-md-5">
<label class="form-label" for="reason_code">Reason</label>
<select class="form-select form-select-sm" id="reason_code" name="reason_code" required>
<?php foreach ($reasonCodes as $code => $label): ?>
<option value="<?= esc($code) ?>"><?= esc($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-7">
<label class="form-label" for="expires_at">Expires</label>
<input class="form-control form-control-sm" id="expires_at" name="expires_at" type="datetime-local">
</div>
<div class="col-12">
<label class="form-label" for="reason_note">Approval note</label>
<input class="form-control form-control-sm" id="reason_note" name="reason_note" required>
</div>
<div class="col-12">
<button type="submit" class="btn btn-warning">Save selected exceptions</button>
</div>
</div>
<?php else: ?>
<div class="alert alert-info mb-0">No linked students currently have rules that can be bypassed.</div>
<?php endif; ?>
</form>
<?php else: ?>
<div class="text-muted">Select a parent to see their students and whether they can enroll.</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<div class="col-xl-6">
<div class="border rounded p-3 h-100">
<h4 class="h6 mb-3">Saved exceptions</h4>
<?php if (empty($canManageEnrollmentExceptions)): ?>
<div class="alert alert-warning mb-0">You do not have permission to view enrollment exceptions.</div>
<?php else: ?>
<div class="table-responsive">
<table id="enrollmentExceptionsTable" class="table table-sm table-bordered align-middle enrollment-admin-datatable no-mgmt-sticky no-dt-fixedheader" data-no-mgmt-sticky data-no-dt-fixedheader>
<thead>
<tr>
<th>Student</th>
<th>Parent</th>
<th>Status</th>
<th>Reason</th>
<th>Bypassed rules</th>
<th>Created by</th>
<th>Expires</th>
<th class="no-sort">Action</th>
</tr>
</thead>
<tbody>
<?php if (!empty($activeExceptions)): ?>
<?php foreach ($activeExceptions as $exception): ?>
<?php
$exceptionId = (int) ($exception['id'] ?? 0);
$exceptionStatus = (string) ($exception['status'] ?? '');
$badgeClass = match ($exceptionStatus) {
'active' => 'bg-success',
'used' => 'bg-info text-dark',
'revoked', 'expired' => 'bg-secondary',
default => 'bg-light text-dark',
};
$bypassedLabels = array_map('enrollment_admin_rule_label', $exception['bypassed_rule_codes'] ?? []);
?>
<tr>
<td>
<?= esc($exception['student_name'] ?? '') ?>
<div class="small text-muted"><?= esc($exception['school_id'] ?? '') ?></div>
</td>
<td><?= esc($exception['parent_name'] ?? '') ?></td>
<td><span class="badge <?= esc($badgeClass) ?>"><?= esc(enrollment_admin_exception_status_label($exceptionStatus)) ?></span></td>
<td>
<div><?= esc(enrollment_admin_reason_label((string) ($exception['reason_code'] ?? ''), $reasonCodes)) ?></div>
<div class="small text-muted"><?= esc($exception['reason_note'] ?? '') ?></div>
</td>
<td><?= esc($bypassedLabels !== [] ? implode(', ', $bypassedLabels) : 'None') ?></td>
<td><?= esc($exception['created_by_name'] ?? '') ?></td>
<td><?= esc(!empty($exception['expires_at']) ? local_datetime($exception['expires_at'], 'm-d-Y H:i') : 'No expiry') ?></td>
<td>
<?php if ($exceptionStatus === 'active' && !empty($canManageEnrollmentExceptions)): ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/exceptions/' . $exceptionId . '/revoke') ?>">
<?= csrf_field() ?>
<input class="form-control form-control-sm mb-2" name="revocation_reason" placeholder="Revocation reason" required>
<button class="btn btn-sm btn-outline-danger" type="submit">Revoke</button>
</form>
<?php elseif (!empty($exception['revocation_reason'])): ?>
<div class="small text-muted"><?= esc($exception['revocation_reason']) ?></div>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="emails-pane" role="tabpanel">
<h3 class="h5 mb-1">Launch parent emails</h3>
<p class="text-muted">Preview one email, approve the launch, then send. Do this after the work queue and exceptions are in good shape.</p>
<?php if (!empty($launchState['missing'])): ?>
<div class="alert alert-danger">Finish this setup before you can approve or send: <?= esc(implode(', ', $launchState['missing'])) ?>.</div>
<?php endif; ?>
<div class="border rounded px-3 mb-4">
<div class="email-check">
<span class="step-badge">1</span>
<div class="email-check-body">
<div class="fw-semibold mb-1">Preview a sample email</div>
<p class="small text-muted mb-2">Open one parent email and confirm the wording, dates, and fees look right.</p>
<?php if (!empty($previewParentId)): ?>
<a class="btn btn-outline-primary" target="_blank" href="<?= site_url('administrator/enrollment-admin/email-preview?school_year=' . rawurlencode((string) $schoolYear) . '&parent_id=' . (int) $previewParentId) ?>">Preview sample</a>
<?php else: ?>
<span class="text-muted small">No parent with linked students is available for a sample preview.</span>
<?php endif; ?>
</div>
</div>
<div class="email-check">
<span class="step-badge <?= $launchApproved ? 'done' : (!$launchReady ? 'blocked' : '') ?>">2</span>
<div class="email-check-body">
<div class="fw-semibold mb-1">Approve the launch</div>
<?php if ($launchApproved): ?>
<p class="small text-success mb-2">Approved <?= esc(local_datetime($launchState['approved_at'], 'm-d-Y H:i')) ?>.</p>
<?php else: ?>
<p class="small text-muted mb-2">Approve only after the year dates and fees are correct.</p>
<?php endif; ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/approve-launch') ?>">
<?= csrf_field() ?>
<input type="hidden" name="school_year" value="<?= esc($schoolYear ?? '') ?>">
<button type="submit" class="btn btn-success" <?= !$launchReady || $launchApproved ? 'disabled' : '' ?>>
<?= $launchApproved ? 'Already approved' : 'Approve launch' ?>
</button>
</form>
</div>
</div>
<div class="email-check">
<span class="step-badge <?= $launchApproved && $launchReady && $emailExampleCount > 0 ? '' : 'blocked' ?>">3</span>
<div class="email-check-body">
<div class="fw-semibold mb-1">Send real emails</div>
<p class="small text-muted mb-2">This emails parents for <?= esc((string) ($schoolYear ?? '')) ?>. Use resend only if you need to send again.</p>
<form method="post" action="<?= site_url('administrator/enrollment-admin/send-registration-emails') ?>" onsubmit="return confirm('Send real registration emails to parents for <?= esc((string) ($schoolYear ?? ''), 'js') ?>?');">
<?= csrf_field() ?>
<input type="hidden" name="school_year" value="<?= esc($schoolYear ?? '') ?>">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="force_resend" name="force_resend" value="1">
<label class="form-check-label small" for="force_resend">Also resend emails already sent</label>
</div>
<button type="submit" class="btn btn-danger" <?= !$launchApproved || !$launchReady || $emailExampleCount === 0 ? 'disabled' : '' ?>>Send real emails</button>
</form>
</div>
</div>
</div>
<h4 class="h6 mb-3">Email examples (<?= (int) $emailExampleCount ?>)</h4>
<div class="table-responsive">
<table id="enrollmentEmailExamplesTable" class="table table-sm table-bordered align-middle enrollment-admin-datatable no-mgmt-sticky no-dt-fixedheader" data-no-mgmt-sticky data-no-dt-fixedheader>
<thead>
<tr>
<th>Parent</th>
<th>Recipient</th>
<th>Students</th>
<th>Subject</th>
<th>Delivery</th>
<th class="no-sort">Example</th>
</tr>
</thead>
<tbody>
<?php if (!empty($emailExamples)): ?>
<?php foreach ($emailExamples as $example): ?>
<?php
$deliveryStatus = (string) ($example['delivery_status'] ?? 'not sent');
$badgeClass = match ($deliveryStatus) {
'sent' => 'bg-success',
'failed' => 'bg-danger',
'generated' => 'bg-info text-dark',
default => 'bg-secondary',
};
$deliveryLabel = match ($deliveryStatus) {
'sent' => 'Sent',
'failed' => 'Failed',
'generated' => 'Generated',
'not sent' => 'Not sent',
default => ucwords(str_replace('_', ' ', $deliveryStatus)),
};
?>
<tr>
<td><?= esc($example['parent_name'] ?? '') ?></td>
<td><?= esc(implode(', ', $example['recipients'] ?? [])) ?></td>
<td><?= esc(implode(', ', $example['student_names'] ?? [])) ?></td>
<td><?= esc($example['subject'] ?? '') ?></td>
<td>
<span class="badge <?= esc($badgeClass) ?>"><?= esc($deliveryLabel) ?></span>
<?php if (!empty($example['sent_at'])): ?>
<div class="small text-muted"><?= esc(local_datetime($example['sent_at'], 'm-d-Y H:i')) ?></div>
<?php endif; ?>
<?php if (!empty($example['failure_reason'])): ?>
<div class="small text-danger"><?= esc($example['failure_reason']) ?></div>
<?php endif; ?>
</td>
<td>
<a class="btn btn-sm btn-outline-secondary" target="_blank" href="<?= site_url('administrator/enrollment-admin/email-preview?school_year=' . rawurlencode((string) $schoolYear) . '&parent_id=' . (int) ($example['parent_user_id'] ?? 0)) ?>">View example</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="history-pane" role="tabpanel">
<h3 class="h5 mb-1">History</h3>
<p class="text-muted">A record of enrollment decisions and exception actions for this school year.</p>
<div class="table-responsive">
<table id="enrollmentAuditTable" class="table table-sm table-bordered align-middle enrollment-admin-datatable no-mgmt-sticky no-dt-fixedheader" data-no-mgmt-sticky data-no-dt-fixedheader>
<thead>
<tr>
<th>When</th>
<th>Student</th>
<th>Action</th>
<th>Administrator</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
<?php if (!empty($auditRows)): ?>
<?php foreach ($auditRows as $row): ?>
<tr>
<td><?= esc(!empty($row['created_at']) ? local_datetime($row['created_at'], 'm-d-Y H:i') : '') ?></td>
<td><?= esc($row['student_name'] ?? '') ?></td>
<td><?= esc(enrollment_admin_audit_action_label((string) ($row['action'] ?? ''))) ?></td>
<td><?= esc($row['performed_by_name'] ?? '') ?></td>
<td><?= esc($row['reason'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
(function () {
function mergeExceptionPaneIntoWorkQueue() {
var workPane = document.getElementById('work-pane');
var exceptionsPane = document.getElementById('exceptions-pane');
if (!workPane || !exceptionsPane || exceptionsPane.parentElement === workPane) {
return;
}
exceptionsPane.classList.remove('tab-pane', 'fade', 'show', 'active');
exceptionsPane.classList.add('mt-4');
exceptionsPane.removeAttribute('role');
workPane.appendChild(exceptionsPane);
}
function initEnrollmentAdminTables() {
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
return;
}
var tableMessages = {
enrollmentEmailExamplesTable: 'No parent decision email examples found for this school year.',
enrollmentFollowupTable: 'No enrollment records need follow-up for this school year.',
enrollmentFlagsTable: 'No students with enrollment blockages were found for these filters.',
enrollmentFamilyEligibilityTable: 'No linked students found for this parent.',
enrollmentExceptionsTable: 'No enrollment exceptions found for this school year.',
enrollmentAuditTable: 'No enrollment audit records found.'
};
window.jQuery('.enrollment-admin-datatable').each(function () {
var table = this;
var $table = window.jQuery(table);
if (window.jQuery.fn.DataTable.isDataTable(table)) {
return;
}
var emptyText = tableMessages[table.id] || 'No records found.';
var $emptyCell = $table.find('tbody tr:first-child td[colspan]');
if ($emptyCell.length === 1 && $table.find('tbody tr').length === 1) {
emptyText = $emptyCell.text().trim() || emptyText;
$table.find('tbody').empty();
}
$table.DataTable({
pageLength: 10,
lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, 'All']],
order: [],
autoWidth: false,
fixedHeader: false,
columnDefs: [
{
targets: 'no-sort',
orderable: false,
searchable: false
}
],
language: {
emptyTable: emptyText
}
});
});
}
mergeExceptionPaneIntoWorkQueue();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initEnrollmentAdminTables);
} else {
initEnrollmentAdminTables();
}
})();
</script>
<?= $this->endSection() ?>