add assessment feature
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 49s
Tests / PHPUnit (push) Successful in 1m36s

This commit is contained in:
root
2026-09-10 06:44:24 -04:00
parent ac19226bf0
commit b5e719382d
31 changed files with 1712 additions and 65 deletions
@@ -91,6 +91,7 @@ $selectedYear = trim((string)($selectedYear ?? ''));
<th>Allergies</th>
<th>Photo Consent</th>
<th>Registration Date</th>
<th style="min-width: 170px;">Assessment</th>
<th style="min-width: 220px;">Action</th>
</tr>
</thead>
@@ -179,6 +180,20 @@ $selectedYear = trim((string)($selectedYear ?? ''));
<td><?= esc($student['allergies'] ?? '') ?></td>
<td><?= !empty($student['photo_consent']) ? 'Yes' : 'No' ?></td>
<td><?= esc($regDisp) ?></td>
<td>
<?php $studentAssessment = ($assessmentByStudent ?? [])[(int)$student['id']] ?? null; ?>
<?php if (!$studentAssessment): ?>
<form method="post" action="<?= site_url('administrator/assessments/students/' . $student['id'] . '/start') ?>"><?= csrf_field() ?><button class="btn btn-sm btn-outline-primary">Assign Assessment</button></form>
<?php elseif (in_array($studentAssessment['status'], ['not_started', 'in_progress'], true)): ?>
<a class="btn btn-sm btn-outline-secondary" href="<?= site_url('administrator/assessments/students/' . $student['id']) ?>">View Status</a>
<div class="small text-muted mt-1"><?= esc(ucwords(str_replace('_', ' ', $studentAssessment['status']))) ?></div>
<?php elseif ($studentAssessment['status'] === 'completed'): ?>
<a class="btn btn-sm btn-warning" href="<?= site_url('administrator/assessments/review/' . $studentAssessment['id']) ?>">Review</a>
<?php else: ?>
<a class="btn btn-sm btn-success" href="<?= site_url('administrator/assessments/results/' . $studentAssessment['id']) ?>">View Results</a>
<div class="small text-muted mt-1">Reviewed</div>
<?php endif; ?>
</td>
<td class="d-flex flex-wrap gap-2">
<!-- Contact Modal Trigger -->
<button class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#<?= $modalIdContact ?>">
+11
View File
@@ -0,0 +1,11 @@
<?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; ?>
<?php if (session()->getFlashdata('errors')): ?>
<div class="alert alert-danger"><ul class="mb-0">
<?php foreach ((array) session()->getFlashdata('errors') as $error): ?><li><?= esc($error) ?></li><?php endforeach; ?>
</ul></div>
<?php endif; ?>
+21
View File
@@ -0,0 +1,21 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container py-4" style="max-width:900px">
<div class="d-flex justify-content-between align-items-start mb-3"><div><h2><?= esc($assessment['form_name']) ?></h2><p class="text-muted mb-0">Interviewing <?= esc($assessment['firstname'].' '.$assessment['lastname']) ?> · <?= esc($assessment['school_year']) ?></p></div><a class="btn btn-outline-secondary" href="<?= site_url('administrator/assessments/students') ?>">New Students</a></div>
<?= $this->include('assessments/_alerts') ?>
<?php if(!empty($assessment['form_education_committee_note'])): ?><div class="alert alert-info"><strong>Form instructions</strong><div class="mt-1" style="white-space:pre-wrap"><?= esc($assessment['form_education_committee_note']) ?></div></div><?php endif; ?>
<div id="saveNotice" class="alert alert-light border py-2">Responses save automatically as you type.</div>
<form id="adminAssessmentForm" method="post" action="<?= site_url('administrator/assessments/attempts/'.$assessment['id'].'/complete') ?>"><?= csrf_field() ?>
<?php foreach($questions as $index=>$q): ?><div class="card shadow-sm mb-3"><div class="card-body"><label class="form-label fw-semibold" for="answer-<?= (int)$q['id'] ?>"><?= $index+1 ?>. <?= esc($q['text']) ?></label>
<?php if($q['type']==='short_answer'): ?><input id="answer-<?= (int)$q['id'] ?>" class="form-control assessment-answer" name="answers[<?= (int)$q['id'] ?>]" value="<?= esc($q['answer_value']??'') ?>" autocomplete="off">
<?php else: ?><textarea id="answer-<?= (int)$q['id'] ?>" class="form-control assessment-answer" name="answers[<?= (int)$q['id'] ?>]" rows="4"><?= esc($q['answer_value']??'') ?></textarea><?php endif; ?>
</div></div><?php endforeach; ?>
<div class="card shadow-sm mb-3"><div class="card-body"><label class="form-label fw-semibold" for="educationCommitteeNote">Education Committee Note</label><textarea id="educationCommitteeNote" class="form-control assessment-answer" name="education_committee_note" rows="5" maxlength="10000" placeholder="Add the Education Committee's note for this student."><?= esc($assessment['education_committee_note'] ?? '') ?></textarea><div class="form-text">This note is saved automatically with the responses.</div></div></div>
<button class="btn btn-success" onclick="return confirm('Complete this assessment and review the responses?')">Complete Assessment</button>
</form>
</div>
<script>(function(){var form=document.getElementById('adminAssessmentForm'),notice=document.getElementById('saveNotice'),timer=null,savePromise=null,pending=false,completing=false;
async function save(){if(savePromise){pending=true;return savePromise}savePromise=(async function(){notice.textContent='Saving…';try{var response=await fetch('<?= site_url('administrator/assessments/attempts/'.$assessment['id'].'/progress') ?>',{method:'POST',body:new FormData(form),headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'}}),data=await response.json();if(!response.ok)throw new Error(data.message||'Save failed');if(data.csrfName&&data.csrfHash){var token=form.querySelector('input[name="'+data.csrfName+'"]');if(token)token.value=data.csrfHash}notice.textContent='All responses saved.'}catch(e){notice.textContent='Autosave failed. Keep this page open and continue typing while it retries.'}})();await savePromise;savePromise=null;if(pending){pending=false;return save()}}
document.querySelectorAll('.assessment-answer').forEach(function(field){field.addEventListener('input',function(){clearTimeout(timer);timer=setTimeout(save,700)});field.addEventListener('change',save)});
form.addEventListener('submit',async function(event){if(completing)return;event.preventDefault();clearTimeout(timer);await save();completing=true;form.submit()});})();</script>
<?= $this->endSection() ?>
+32
View File
@@ -0,0 +1,32 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
$isEdit = !empty($form);
$singlePool = count($pools) === 1;
$action = $isEdit ? site_url('administrator/assessments/forms/' . $form['id']) : site_url('administrator/assessments/forms');
$yearNames = array_column($schoolYears ?? [], 'name');
if ($selectedSchoolYear !== '' && !in_array($selectedSchoolYear, $yearNames, true)) $schoolYears[] = ['name' => $selectedSchoolYear, 'status' => ''];
?>
<div class="container py-3" style="max-width:1000px">
<div class="d-flex justify-content-between align-items-center mb-3"><div><h2><?= $isEdit ? 'Edit' : 'Create' ?> Assessment Form</h2><p class="text-muted mb-0">Choose questions from one shared pool and set their order.</p></div><a class="btn btn-outline-secondary" href="<?= site_url('administrator/assessments/forms') ?>">Back</a></div>
<?= $this->include('assessments/_alerts') ?>
<?php if ($locked): ?><div class="alert alert-info">This form has student assignments. Its school year, pool, and question list are locked, but its name, note, and status can still be changed.</div><?php endif; ?>
<form method="post" action="<?= $action ?>"><?= csrf_field() ?>
<div class="card shadow-sm mb-3"><div class="card-body"><div class="row g-3">
<div class="col-md-4"><label class="form-label">Form name</label><input class="form-control" name="name" maxlength="150" required value="<?= set_value('name', $form['name'] ?? '') ?>"></div>
<div class="col-md-3"><label class="form-label">Question pool</label><select id="poolSelect" class="form-select" name="pool_id" required <?= ($isEdit || $singlePool) ? 'disabled' : '' ?>><option value="">Select a pool</option><?php foreach ($pools as $pool): ?><option value="<?= (int)$pool['id'] ?>" <?= ($singlePool || (int)($form['pool_id'] ?? 0)===(int)$pool['id']) ? 'selected' : '' ?>><?= esc($pool['name']) ?></option><?php endforeach; ?></select><?php if ($isEdit || $singlePool): ?><input type="hidden" name="pool_id" value="<?= (int)($form['pool_id'] ?? $pools[0]['id'] ?? 0) ?>"><?php endif; ?></div>
<div class="col-md-3"><label class="form-label">School year</label><select class="form-select" name="school_year" required <?= $locked ? 'disabled' : '' ?>><option value="">Select school year</option><?php foreach (($schoolYears ?? []) as $year): ?><option value="<?= esc($year['name']) ?>" <?= set_select('school_year', $year['name'], $selectedSchoolYear === $year['name']) ?>><?= esc($year['name']) ?><?= !empty($year['status']) ? ' — '.esc(ucfirst($year['status'])) : '' ?></option><?php endforeach; ?></select><?php if($locked): ?><input type="hidden" name="school_year" value="<?= esc($selectedSchoolYear) ?>"><?php endif; ?></div>
<div class="col-md-2"><label class="form-label">Status</label><select class="form-select" name="status"><option value="draft" <?= ($form['status'] ?? 'draft')==='draft'?'selected':'' ?>>Draft</option><option value="published" <?= ($form['status'] ?? '')==='published'?'selected':'' ?>>Published</option><option value="archived" <?= ($form['status'] ?? '')==='archived'?'selected':'' ?>>Archived</option></select></div>
<div class="col-12"><label class="form-label">Education Committee Note <span class="text-muted">(optional, admin only)</span></label><textarea class="form-control" name="education_committee_note" rows="3" maxlength="10000" placeholder="Add context or instructions for the Education Committee."><?= set_value('education_committee_note', $form['education_committee_note'] ?? '') ?></textarea></div>
</div></div></div>
<?php if (!$isEdit): ?><div id="newFormHint" class="alert alert-secondary">Pick individual questions or select “Use all questions.”</div><?php endif; ?>
<div class="card shadow-sm"><div class="card-header d-flex justify-content-between"><strong>Questions</strong><?php if (!$locked): ?><label><input type="checkbox" name="use_all" value="1"> Use all questions</label><?php endif; ?></div><div class="card-body">
<?php if (empty($questions)): ?><p class="text-muted">There are no questions available. Add questions to a pool first.</p><?php endif; ?>
<p id="poolQuestionHint" class="text-muted <?= $isEdit ? 'd-none' : '' ?>">Select a pool to see its questions.</p>
<?php foreach ($questions as $index => $question): ?><div class="border rounded p-3 mb-2 question-choice" data-pool-id="<?= (int)$question['pool_id'] ?>"><div class="row align-items-start"><div class="col-auto"><input class="form-check-input" type="checkbox" name="question_ids[]" value="<?= (int)$question['id'] ?>" <?= in_array((int)$question['id'], $selectedIds, true)?'checked':'' ?> <?= $locked?'disabled':'' ?>></div><div class="col"><div><?= esc($question['text']) ?></div><small class="text-muted"><?= esc(str_replace('_',' ',ucfirst($question['type']))) ?></small></div><div class="col-auto"><label class="small">Order</label><input style="width:80px" class="form-control form-control-sm" type="number" min="1" name="order_<?= (int)$question['id'] ?>" value="<?= ($pos=array_search((int)$question['id'],$selectedIds,true))!==false ? $pos+1 : $index+1 ?>" <?= $locked?'disabled':'' ?>></div></div></div><?php endforeach; ?>
</div></div>
<button class="btn btn-primary mt-3" <?= $isEdit && empty($questions) && !$locked ? 'disabled' : '' ?>>Save Form</button>
</form>
</div>
<?php if (!$isEdit): ?><script>(function(){var select=document.getElementById('poolSelect'),hint=document.getElementById('poolQuestionHint');function sync(){var pool=select.value,shown=0;document.querySelectorAll('.question-choice').forEach(function(row){var active=pool!==''&&row.dataset.poolId===pool;row.classList.toggle('d-none',!active);row.querySelectorAll('input').forEach(function(input){input.disabled=!active;if(!active&&input.type==='checkbox')input.checked=false});if(active)shown++});hint.textContent=pool===''?'Select a pool to see its questions.':(shown?'':'This pool has no questions.');hint.classList.toggle('d-none',shown>0)}select.addEventListener('change',sync);sync()})();</script><?php endif; ?>
<?= $this->endSection() ?>
+43
View File
@@ -0,0 +1,43 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid py-3">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div><h2 class="mb-1">Assessment Forms</h2><p class="text-muted mb-0">Build, publish, preview, and track individual assessments.</p></div>
<div class="d-flex gap-2">
<a class="btn btn-outline-secondary" href="<?= site_url('administrator/assessments/students') ?>">New Students</a>
<a class="btn btn-outline-primary" href="<?= site_url('administrator/assessments/pools') ?>">Question Pool</a>
<a class="btn btn-primary" href="<?= site_url('administrator/assessments/forms/new') ?>">Create Form</a>
</div>
</div>
<?= $this->include('assessments/_alerts') ?>
<div class="card shadow-sm"><div class="card-body"><div class="table-responsive">
<table class="table align-middle no-mgmt-sticky" data-no-mgmt-sticky>
<thead><tr><th>Name</th><th>School Year</th><th>Pool</th><th>Education Committee Note</th><th>Status</th><th>Questions</th><th>Assignments</th><th>Actions</th></tr></thead>
<tbody>
<?php if (empty($forms)): ?><tr><td colspan="8" class="text-muted text-center">No assessment forms yet.</td></tr><?php endif; ?>
<?php foreach ($forms as $form): ?>
<tr>
<td><?= esc($form['name']) ?></td>
<td><?= esc($form['school_year'] ?: '—') ?></td>
<td><?= esc($form['pool_name']) ?></td>
<td style="max-width:280px;white-space:pre-wrap"><?= esc($form['education_committee_note'] ?: '—') ?></td>
<td><span class="badge <?= $form['status'] === 'published' ? 'bg-success' : ($form['status'] === 'archived' ? 'bg-secondary' : 'bg-warning text-dark') ?>"><?= esc(ucfirst($form['status'])) ?></span></td>
<td><?= (int) $form['question_count'] ?></td>
<td><?= (int) $form['assignment_count'] ?></td>
<td><div class="d-flex flex-wrap gap-1">
<a class="btn btn-sm btn-outline-primary" href="<?= site_url('administrator/assessments/forms/' . $form['id'] . '/edit') ?>">Edit</a>
<a class="btn btn-sm btn-outline-secondary" href="<?= site_url('administrator/assessments/forms/' . $form['id'] . '/preview') ?>">Preview</a>
<?php if ($form['status'] !== 'published'): ?>
<form method="post" action="<?= site_url('administrator/assessments/forms/' . $form['id'] . '/publish') ?>" onsubmit="return confirm('Publish this assessment form?')">
<?= csrf_field() ?>
<button class="btn btn-sm btn-success" <?= (int)$form['question_count'] === 0 ? 'disabled title="Add questions before publishing"' : '' ?>>Publish</button>
</form>
<?php endif; ?>
</div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div></div></div>
</div>
<?= $this->endSection() ?>
+9
View File
@@ -0,0 +1,9 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container py-4" style="max-width:950px"><div class="d-flex justify-content-between"><div><h2>Review: <?= esc($assessment['form_name']) ?></h2><p class="text-muted"><?= esc($assessment['firstname'].' '.$assessment['lastname']) ?> · <?= esc($assessment['school_year']) ?></p></div><a class="btn btn-outline-secondary align-self-start" href="<?= site_url('administrator/assessments/students/'.$assessment['student_id']) ?>">Back</a></div><?= $this->include('assessments/_alerts') ?>
<form method="post" action="<?= site_url('administrator/assessments/review/'.$assessment['id']) ?>"><?= csrf_field() ?>
<?php foreach($questions as $index=>$q): ?><div class="card shadow-sm mb-3"><div class="card-body"><h5><?= $index+1 ?>. <?= esc($q['text']) ?></h5><div class="p-3 bg-light rounded"><strong>Response</strong><div class="mt-1" style="white-space:pre-wrap"><?= esc($q['answer_value'] ?? 'No answer') ?></div></div></div></div><?php endforeach; ?>
<div class="card shadow-sm mb-3"><div class="card-body"><label class="form-label fw-semibold" for="reviewCommitteeNote">Education Committee Note</label><textarea id="reviewCommitteeNote" class="form-control" name="education_committee_note" rows="5" maxlength="10000"><?= esc($assessment['education_committee_note'] ?? '') ?></textarea><div id="noteSaveNotice" class="form-text">This note remains editable and saves automatically.</div></div></div>
<button class="btn btn-success">Complete Review</button></form></div>
<script>(function(){var form=document.querySelector('form[action*="/review/"]'),field=document.getElementById('reviewCommitteeNote'),notice=document.getElementById('noteSaveNotice'),timer;if(!form||!field)return;async function save(){notice.textContent='Saving note…';try{var data=new FormData();var token=form.querySelector('input[type="hidden"]');if(token)data.append(token.name,token.value);data.append('education_committee_note',field.value);var response=await fetch('<?= site_url('administrator/assessments/attempts/'.$assessment['id'].'/note') ?>',{method:'POST',body:data,headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'}}),json=await response.json();if(!response.ok)throw new Error(json.message||'Save failed');if(json.csrfName&&json.csrfHash){var csrf=form.querySelector('input[name="'+json.csrfName+'"]');if(csrf)csrf.value=json.csrfHash}notice.textContent='Note saved.'}catch(e){notice.textContent='Note could not be saved. Keep this page open and try typing again.'}}field.addEventListener('input',function(){clearTimeout(timer);timer=setTimeout(save,700)});field.addEventListener('change',save)})();</script>
<?= $this->endSection() ?>
+9
View File
@@ -0,0 +1,9 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container py-4" style="max-width:900px"><h2>My Assessments</h2><p class="text-muted">Open assigned assessments, continue saved work, or review completed responses.</p><?= $this->include('assessments/_alerts') ?>
<div class="row g-3"><?php if(empty($assignments)): ?><div class="col-12"><div class="alert alert-info">There are no assigned assessments.</div></div><?php endif; ?>
<?php foreach($assignments as $a): ?><div class="col-md-6"><div class="card h-100 shadow-sm"><div class="card-body"><h5><?= esc($a['form_name']) ?></h5><p class="mb-1"><?= esc($a['firstname'].' '.$a['lastname']) ?></p><p class="text-muted mb-2"><?= esc($a['school_year'] ?? '—') ?></p><p><span class="badge <?= $a['status']==='graded'?'bg-success':($a['status']==='completed'?'bg-warning text-dark':'bg-primary') ?>"><?= $a['status']==='graded' ? 'Reviewed' : esc(ucwords(str_replace('_',' ',$a['status']))) ?></span></p>
<?php if(in_array($a['status'],['not_started','in_progress'],true)): ?><a class="btn btn-primary" href="<?= site_url('student/assessments/'.$a['id']) ?>"><?= $a['status']==='in_progress'?'Continue':'Start' ?></a><?php elseif($a['status']==='graded'): ?><a class="btn btn-success" href="<?= site_url('student/assessments/'.$a['id'].'/results') ?>">View Responses</a><?php else: ?><span class="text-muted">Awaiting review</span><?php endif; ?>
</div></div></div><?php endforeach; ?></div>
</div>
<?= $this->endSection() ?>
+25
View File
@@ -0,0 +1,25 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid py-3">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div><h2 class="mb-1">New Student Assessments</h2><p class="text-muted mb-0">New students from the <?= esc($schoolYear) ?> enrollment roster.</p></div>
<a class="btn btn-outline-secondary" href="<?= site_url('administrator/assessments/forms') ?>">Assessment Forms</a>
</div>
<?= $this->include('assessments/_alerts') ?>
<div class="card shadow-sm"><div class="card-body"><div class="table-responsive">
<table id="assessmentStudentTable" class="table table-striped align-middle no-mgmt-sticky" data-no-mgmt-sticky><thead><tr><th>Student</th><th>School ID</th><th>Age</th><th>Current Class</th><th>Enrollment Status</th><th>Assessment</th></tr></thead><tbody>
<?php if(empty($new_students)): ?><tr><td colspan="6" class="text-center text-muted">No new students found for this school year.</td></tr><?php endif; ?>
<?php foreach($new_students as $student): $sid=(int)$student['id']; $a=($assessmentByStudent??[])[$sid]??null; ?><tr>
<td><?= esc($student['firstname'].' '.$student['lastname']) ?></td><td><?= esc($student['school_id']??'—') ?></td><td><?= esc($student['age']??'—') ?></td><td><?= esc($student['class_section']??'Class not Assigned') ?></td><td><?= esc(ucwords((string)($student['enrollment_status']??'unknown'))) ?></td><td>
<?php if(!$a): ?><form method="post" action="<?= site_url('administrator/assessments/students/'.$sid.'/start') ?>"><?= csrf_field() ?><button class="btn btn-sm btn-primary">Assign Assessment</button></form>
<?php elseif(in_array($a['status'],['not_started','in_progress'],true)): ?><a class="btn btn-sm btn-outline-secondary" href="<?= site_url('administrator/assessments/students/'.$sid) ?>">View Status</a><div class="small text-muted"><?= esc(ucwords(str_replace('_',' ',$a['status']))) ?></div>
<?php elseif($a['status']==='completed'): ?><a class="btn btn-sm btn-warning" href="<?= site_url('administrator/assessments/review/'.$a['id']) ?>">Review</a>
<?php else: ?><a class="btn btn-sm btn-success" href="<?= site_url('administrator/assessments/results/'.$a['id']) ?>">View Results</a><?php endif; ?>
</td></tr><?php endforeach; ?>
</tbody></table>
</div></div></div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>$(function(){if($.fn.DataTable)$('#assessmentStudentTable').DataTable({pageLength:100,order:[[0,'asc']],fixedHeader:false})});</script>
<?= $this->endSection() ?>
+35
View File
@@ -0,0 +1,35 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php $typeLabels = ['multiple_choice' => 'Multiple choice', 'short_answer' => 'Short answer', 'true_false' => 'True / False', 'essay' => 'Essay']; ?>
<div class="container-fluid py-3">
<div class="d-flex justify-content-between align-items-center mb-3"><div><h2><?= esc($pool['name']) ?> Question Pool</h2><p class="text-muted mb-0">Add, edit, delete, or reorder questions in the shared assessment pool.</p></div><a class="btn btn-outline-secondary" href="<?= site_url('administrator/assessments/forms') ?>">Assessment Forms</a></div>
<?= $this->include('assessments/_alerts') ?>
<div class="card shadow-sm mb-4"><div class="card-header"><strong>Add a question</strong></div><div class="card-body">
<form method="post" action="<?= site_url('administrator/assessments/pools/' . $pool['id'] . '/questions') ?>"><?= csrf_field() ?>
<div class="row g-3"><div class="col-md-4"><label class="form-label">Type</label><select class="form-select question-type" name="type" required><?php foreach ($typeLabels as $value => $label): ?><option value="<?= $value ?>"><?= esc($label) ?></option><?php endforeach; ?></select></div>
<div class="col-md-8"><label class="form-label">Question</label><textarea class="form-control" name="text" rows="2" required><?= set_value('text') ?></textarea></div>
<div class="col-md-6 options-wrap"><label class="form-label">Choices <span class="text-muted">(one per line)</span></label><textarea class="form-control" name="options_text" rows="3"></textarea></div>
<div class="col-md-6"><label class="form-label">Correct answer <span class="text-muted">(reference only)</span></label><textarea class="form-control" name="correct_answer" rows="3"></textarea></div></div>
<button class="btn btn-primary mt-3">Add Question</button>
</form>
</div></div>
<?php if (empty($questions)): ?><div class="alert alert-info">This pool has no questions yet.</div><?php endif; ?>
<?php foreach ($questions as $index => $question): ?>
<?php $opts = implode("\n", (array) json_decode((string) ($question['options'] ?? '[]'), true)); ?>
<div class="card shadow-sm mb-3"><div class="card-header d-flex justify-content-between"><span><span class="badge bg-secondary me-2">#<?= $index + 1 ?></span><?= esc($typeLabels[$question['type']] ?? $question['type']) ?></span><span class="d-flex gap-1">
<form method="post" action="<?= site_url('administrator/assessments/questions/' . $question['id'] . '/move') ?>"><?= csrf_field() ?><input type="hidden" name="direction" value="up"><button class="btn btn-sm btn-outline-secondary" aria-label="Move up" <?= $index === 0 ? 'disabled' : '' ?>>↑</button></form>
<form method="post" action="<?= site_url('administrator/assessments/questions/' . $question['id'] . '/move') ?>"><?= csrf_field() ?><input type="hidden" name="direction" value="down"><button class="btn btn-sm btn-outline-secondary" aria-label="Move down" <?= $index === count($questions)-1 ? 'disabled' : '' ?>>↓</button></form>
</span></div><div class="card-body">
<form method="post" action="<?= site_url('administrator/assessments/questions/' . $question['id'] . '/update') ?>"><?= csrf_field() ?>
<div class="row g-3"><div class="col-md-4"><label class="form-label">Type</label><select class="form-select question-type" name="type"><?php foreach ($typeLabels as $value => $label): ?><option value="<?= $value ?>" <?= $question['type'] === $value ? 'selected' : '' ?>><?= esc($label) ?></option><?php endforeach; ?></select></div>
<div class="col-md-8"><label class="form-label">Question</label><textarea class="form-control" name="text" rows="2" required><?= esc($question['text']) ?></textarea></div>
<div class="col-md-6 options-wrap"><label class="form-label">Choices (one per line)</label><textarea class="form-control" name="options_text" rows="3"><?= esc($opts) ?></textarea></div>
<div class="col-md-6"><label class="form-label">Correct answer (reference only)</label><textarea class="form-control" name="correct_answer" rows="3"><?= esc($question['correct_answer'] ?? '') ?></textarea></div></div>
<button class="btn btn-sm btn-primary mt-3">Save Changes</button>
</form>
<form class="d-inline" method="post" action="<?= site_url('administrator/assessments/questions/' . $question['id'] . '/delete') ?>" onsubmit="return confirm('Delete this question?')"><?= csrf_field() ?><button class="btn btn-sm btn-outline-danger mt-2">Delete</button></form>
</div></div>
<?php endforeach; ?>
</div>
<script>document.querySelectorAll('.question-type').forEach(function(s){function sync(){var w=s.closest('form').querySelector('.options-wrap');if(w)w.style.display=s.value==='multiple_choice'?'block':'none'}s.addEventListener('change',sync);sync()});</script>
<?= $this->endSection() ?>
+9
View File
@@ -0,0 +1,9 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container py-4" style="max-width:950px">
<div class="d-flex justify-content-end mb-3"><a class="btn btn-outline-secondary" href="<?= !empty($adminView) ? site_url('administrator/assessments/students') : site_url('student/assessments') ?>"><?= !empty($adminView) ? 'Back to Students' : 'Back to My Assessments' ?></a></div>
<div class="card shadow-sm mb-4"><div class="card-body text-center"><h2><?= esc($assessment['form_name']) ?></h2><p class="mb-1"><?= esc($assessment['firstname'].' '.$assessment['lastname']) ?></p><p class="text-muted mb-2"><?= esc($assessment['school_year'] ?? '—') ?></p><span class="badge bg-success">Reviewed</span></div></div><?= $this->include('assessments/_alerts') ?>
<?php foreach($questions as $index=>$q): ?><div class="card mb-3"><div class="card-body"><h5><?= $index+1 ?>. <?= esc($q['text']) ?></h5><p class="mb-1"><strong>Response:</strong></p><div style="white-space:pre-wrap"><?= esc($q['answer_value'] ?? 'No answer') ?></div></div></div><?php endforeach; ?>
<?php if(!empty($adminView)): ?><div class="card mb-3"><div class="card-body"><form id="resultsCommitteeNoteForm"><?= csrf_field() ?><label class="form-label fw-semibold" for="resultsCommitteeNote">Education Committee Note</label><textarea id="resultsCommitteeNote" class="form-control" name="education_committee_note" rows="5" maxlength="10000"><?= esc($assessment['education_committee_note'] ?? '') ?></textarea><div id="resultsNoteSaveNotice" class="form-text">This note remains editable and saves automatically.</div></form></div></div><?php endif; ?></div>
<?php if(!empty($adminView)): ?><script>(function(){var form=document.getElementById('resultsCommitteeNoteForm'),field=document.getElementById('resultsCommitteeNote'),notice=document.getElementById('resultsNoteSaveNotice'),timer;async function save(){notice.textContent='Saving note…';try{var response=await fetch('<?= site_url('administrator/assessments/attempts/'.$assessment['id'].'/note') ?>',{method:'POST',body:new FormData(form),headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'}}),json=await response.json();if(!response.ok)throw new Error(json.message||'Save failed');if(json.csrfName&&json.csrfHash){var csrf=form.querySelector('input[name="'+json.csrfName+'"]');if(csrf)csrf.value=json.csrfHash}notice.textContent='Note saved.'}catch(e){notice.textContent='Note could not be saved. Keep this page open and try typing again.'}}field.addEventListener('input',function(){clearTimeout(timer);timer=setTimeout(save,700)});field.addEventListener('change',save)})();</script><?php endif; ?>
<?= $this->endSection() ?>
@@ -0,0 +1,17 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php $byForm=[]; foreach($assignments as $a)$byForm[(int)$a['form_id']]=$a; ?>
<div class="container py-3" style="max-width:1000px">
<div class="d-flex justify-content-between align-items-center mb-3"><div><h2>Assessments: <?= esc($student['firstname'].' '.$student['lastname']) ?></h2><p class="text-muted mb-0"><?= esc($student['school_id'] ?? '') ?></p></div><a class="btn btn-outline-secondary" href="<?= site_url('administrator/assessments/students') ?>">Back to Students</a></div>
<?= $this->include('assessments/_alerts') ?>
<div class="card shadow-sm mb-4"><div class="card-header"><strong>Assign a published form</strong></div><div class="card-body">
<?php if(empty($forms)): ?><p class="text-muted mb-0">No published assessment forms are available.</p><?php else: ?><div class="table-responsive"><table class="table align-middle no-mgmt-sticky" data-no-mgmt-sticky><thead><tr><th>Form</th><th>Pool</th><th>Questions</th><th>Status / Action</th></tr></thead><tbody>
<?php foreach($forms as $form): $existing=$byForm[(int)$form['id']]??null; ?><tr><td><?= esc($form['name']) ?></td><td><?= esc($form['pool_name']) ?></td><td><?= (int)$form['question_count'] ?></td><td><?php if($existing): ?><span class="badge bg-secondary"><?= esc(ucwords(str_replace('_',' ',$existing['status']))) ?></span><?php else: ?><form method="post" action="<?= site_url('administrator/assessments/students/'.$student['id'].'/assign') ?>"><?= csrf_field() ?><input type="hidden" name="form_id" value="<?= (int)$form['id'] ?>"><button class="btn btn-sm btn-primary">Assign</button></form><?php endif; ?></td></tr><?php endforeach; ?>
</tbody></table></div><?php endif; ?>
</div></div>
<div class="card shadow-sm"><div class="card-header"><strong>Assignment history</strong></div><div class="card-body"><div class="table-responsive"><table class="table align-middle no-mgmt-sticky" data-no-mgmt-sticky><thead><tr><th>Form</th><th>School Year</th><th>Assigned</th><th>Status</th><th>Action</th></tr></thead><tbody>
<?php if(empty($assignments)): ?><tr><td colspan="5" class="text-center text-muted">Nothing assigned yet.</td></tr><?php endif; ?>
<?php foreach($assignments as $a): ?><tr><td><?= esc($a['form_name']) ?></td><td><?= esc($a['school_year'] ?? '—') ?></td><td><?= esc(local_datetime($a['assigned_at'],'m-d-Y H:i')) ?></td><td><?= $a['status']==='graded' ? 'Reviewed' : esc(ucwords(str_replace('_',' ',$a['status']))) ?></td><td><?php if($a['status']==='completed'): ?><a class="btn btn-sm btn-warning" href="<?= site_url('administrator/assessments/review/'.$a['id']) ?>">Review</a><?php elseif($a['status']==='graded'): ?><a class="btn btn-sm btn-success" href="<?= site_url('administrator/assessments/results/'.$a['id']) ?>">View Results</a><?php else: ?><span class="text-muted">View Status</span><?php endif; ?></td></tr><?php endforeach; ?>
</tbody></table></div></div></div>
</div>
<?= $this->endSection() ?>
+22
View File
@@ -0,0 +1,22 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php $preview=$preview??false; $assessment=$assessment??null; $answers=$answers??[]; ?>
<div class="container py-4" style="max-width:900px"><div class="d-flex justify-content-between align-items-center mb-3"><div><h2><?= esc($form['name']) ?></h2><p class="text-muted mb-0"><?= esc($form['school_year'] ?? '—') ?> · <?= count($questions) ?> questions</p></div><?php if($preview): ?><a class="btn btn-outline-secondary" href="<?= site_url('administrator/assessments/forms') ?>">Exit Preview</a><?php endif; ?></div>
<?php if($preview): ?><div class="alert alert-info">Preview mode — answers cannot be saved or submitted.</div><?php else: ?><div id="saveNotice" class="alert alert-light border py-2">Changes are saved as you work.</div><?php endif; ?>
<form id="assessmentForm" method="post" action="<?= !$preview ? site_url('student/assessments/'.$assessment['id'].'/submit') : '#' ?>"><?= csrf_field() ?>
<?php foreach($questions as $index=>$q): $options=(array)json_decode((string)($q['options']??'[]'),true); $value=(string)($answers[$q['id']]??''); ?>
<fieldset class="card shadow-sm mb-3" <?= $preview?'disabled':'' ?>><div class="card-body"><legend class="fs-6 mb-3"><span class="badge bg-secondary me-2"><?= $index+1 ?></span><?= esc($q['text']) ?></legend>
<?php if(in_array($q['type'],['multiple_choice','true_false'],true)): foreach($options as $option): ?><div class="form-check mb-2"><input class="form-check-input assessment-answer" type="radio" name="answers[<?= (int)$q['id'] ?>]" value="<?= esc($option) ?>" <?= $value===(string)$option?'checked':'' ?>><label class="form-check-label"><?= esc($option) ?></label></div><?php endforeach; ?>
<?php elseif($q['type']==='short_answer'): ?><input class="form-control assessment-answer" name="answers[<?= (int)$q['id'] ?>]" value="<?= esc($value) ?>">
<?php else: ?><textarea class="form-control assessment-answer" rows="6" name="answers[<?= (int)$q['id'] ?>]"><?= esc($value) ?></textarea><?php endif; ?>
</div></fieldset>
<?php endforeach; ?>
<?php if(!$preview): ?><div class="d-flex gap-2"><button type="button" id="saveButton" class="btn btn-outline-primary">Save Progress</button><button class="btn btn-success" onclick="return confirm('Submit this assessment? You cannot change answers after submission.')">Submit Assessment</button></div><?php endif; ?>
</form>
</div>
<?php if(!$preview): ?><script>
(function(){var form=document.getElementById('assessmentForm'),notice=document.getElementById('saveNotice'),timer;
async function save(){var fd=new FormData(form);notice.textContent='Saving…';try{var r=await fetch('<?= site_url('student/assessments/'.$assessment['id'].'/progress') ?>',{method:'POST',body:fd,headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'}});var j=await r.json();if(!r.ok)throw new Error(j.message||'Save failed');if(j.csrfName&&j.csrfHash){var token=form.querySelector('input[name="'+j.csrfName+'"]');if(token)token.value=j.csrfHash}notice.textContent='Progress saved.'}catch(e){notice.textContent='Could not save automatically. Use Save Progress to retry.'}}
document.querySelectorAll('.assessment-answer').forEach(function(el){el.addEventListener('change',function(){clearTimeout(timer);timer=setTimeout(save,500)});if(el.tagName==='TEXTAREA'||el.type==='text')el.addEventListener('input',function(){clearTimeout(timer);timer=setTimeout(save,1200)})});document.getElementById('saveButton').addEventListener('click',save);
})();</script><?php endif; ?>
<?= $this->endSection() ?>
+20 -60
View File
@@ -19,7 +19,7 @@
<?php endif; ?>
<div class="table-responsive">
<table id="enrollmentTable" class="table table-bordered table-striped mt-4 align-middle w-100">
<table id="enrollmentTable" class="table table-bordered table-striped mt-4 align-middle w-100 no-mgmt-sticky" data-no-mgmt-sticky>
<thead>
<tr>
@@ -30,6 +30,7 @@
<th>Actual Status</th>
<th>Age</th> <!-- NEW -->
<th>Registration Date</th> <!-- NEW -->
<th>Assessment</th>
<th>Action</th>
</tr>
@@ -107,6 +108,20 @@
<td><?= esc($student['age'] ?? '-') ?></td>
<td><?= esc(!empty($student['registration_date']) ? local_date($student['registration_date'], 'm-d-Y') : '-') ?></td>
<td>
<?php $studentAssessment = ($assessmentByStudent ?? [])[$sid] ?? null; ?>
<?php if (!$studentAssessment): ?>
<form method="post" action="<?= site_url('administrator/assessments/students/' . $sid . '/start') ?>"><?= csrf_field() ?><button class="btn btn-sm btn-outline-primary">Assign Assessment</button></form>
<?php elseif (in_array($studentAssessment['status'], ['not_started', 'in_progress'], true)): ?>
<a class="btn btn-sm btn-outline-secondary" href="<?= site_url('administrator/assessments/students/' . $sid) ?>">View Status</a>
<div class="small text-muted mt-1"><?= esc(ucwords(str_replace('_', ' ', $studentAssessment['status']))) ?></div>
<?php elseif ($studentAssessment['status'] === 'completed'): ?>
<a class="btn btn-sm btn-warning" href="<?= site_url('administrator/assessments/review/' . $studentAssessment['id']) ?>">Review</a>
<?php else: ?>
<a class="btn btn-sm btn-success" href="<?= site_url('administrator/assessments/results/' . $studentAssessment['id']) ?>">View Results</a>
<?php endif; ?>
</td>
<td class="d-flex gap-2">
<!-- Contact info modal trigger ONLY -->
<button class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#<?= esc($modalIdContact) ?>">
@@ -156,7 +171,7 @@
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="8" class="text-center">No students found.</td>
<td colspan="9" class="text-center">No students found.</td>
</tr>
<?php endif; ?>
</tbody>
@@ -169,52 +184,7 @@
<?= $this->section('scripts') ?>
<script>
$(function() {
function getFixedHeaderOffset() {
let total = 0;
const stack = [];
const header = document.querySelector('header.navbar.sticky-top, header.navbar.fixed-top');
if (header) stack.push(header);
const mgmt = document.getElementById('navbarManagement');
if (mgmt && (mgmt.classList.contains('sticky-top') || mgmt.classList.contains('fixed-top'))) stack.push(mgmt);
document.querySelectorAll('.navbar.sticky-top, .navbar.fixed-top').forEach(el => { if (!stack.includes(el)) stack.push(el); });
stack.forEach(el => { const h = el.offsetHeight || el.getBoundingClientRect().height || 0; total += Math.max(0, Math.round(h)); });
return total;
}
function loadScript(src, id) {
return new Promise((resolve, reject) => {
if (id && document.getElementById(id)) return resolve();
const s = document.createElement('script');
if (id) s.id = id;
s.src = src;
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
});
}
function loadCss(href, id) {
return new Promise((resolve) => {
if (id && document.getElementById(id)) return resolve();
const l = document.createElement('link');
if (id) l.id = id;
l.rel = 'stylesheet';
l.href = href;
l.onload = resolve;
document.head.appendChild(l);
});
}
function ensureFixedHeaderAssets() {
const hasFH = !!($.fn.dataTable && $.fn.dataTable.FixedHeader);
if (hasFH) return Promise.resolve();
return Promise.all([
loadScript('https://cdn.jsdelivr.net/npm/datatables.net-fixedheader@3.4.0/js/dataTables.fixedHeader.min.js', 'dt-fixedheader'),
loadCss('https://cdn.jsdelivr.net/npm/datatables.net-fixedheader-bs5@3.4.0/css/fixedHeader.bootstrap5.min.css', 'dt-fixedheader-css')
]).catch(() => {});
}
function initTable() {
$('#enrollmentTable').DataTable({
$('#enrollmentTable').DataTable({
pageLength: 100,
lengthMenu: [10, 25, 50, 100],
stateSave: true,
@@ -222,18 +192,8 @@
columnDefs: [
{ targets: [1, 2, 3], searchable: false }
],
fixedHeader: {
header: true,
headerOffset: getFixedHeaderOffset()
}
});
}
if ($.fn.dataTable && $.fn.dataTable.FixedHeader) {
initTable();
} else {
ensureFixedHeaderAssets().then(() => initTable()).catch(() => initTable());
}
fixedHeader: false
});
});
</script>
<?= $this->endSection() ?>