455 lines
26 KiB
PHP
455 lines
26 KiB
PHP
<?= $this->extend('layout/management_layout') ?>
|
|
<?= $this->section('content') ?>
|
|
<div class="container-fluid">
|
|
<h2 class="text-center mt-4 mb-3">Incidents Management</h2>
|
|
<?= $this->include('partials/academic_filter') ?>
|
|
<!-- Flash Success Message -->
|
|
<?php if (session()->getFlashdata('success')): ?>
|
|
<div class="alert alert-success">
|
|
<?= session()->getFlashdata('success'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php
|
|
// Generate a unique token and store it in the session
|
|
$token = bin2hex(random_bytes(16));
|
|
session()->set('flag_form_token', $token);
|
|
|
|
$incidentLabels = [
|
|
'payment' => 'Payment',
|
|
'attendance' => 'Attendance',
|
|
'grade' => 'Grade',
|
|
'behavior' => 'Behavior (General)',
|
|
'talking_off_task' => 'Talking/off-task',
|
|
'calling_out' => 'Calling out',
|
|
'minor_disruption' => 'Minor disruption',
|
|
'minor_dress_code' => 'Minor dress code',
|
|
'forgetting_materials' => 'Forgetting materials',
|
|
'repeated_defiance' => 'Repeated defiance',
|
|
'disrespectful_tone' => 'Disrespectful tone',
|
|
'minor_profanity' => 'Minor profanity',
|
|
'repeated_tardiness' => 'Repeated tardiness',
|
|
'device_misuse' => 'Device misuse',
|
|
'minor_conflict' => 'Minor conflict/horseplay',
|
|
'bullying_harassment' => 'Bullying/harassment',
|
|
'fighting_aggression' => 'Fighting/aggression',
|
|
'threats' => 'Threats',
|
|
'theft' => 'Theft',
|
|
'vandalism' => 'Vandalism',
|
|
'sexual_harassment' => 'Sexual harassment',
|
|
'serious_cyber_incident' => 'Serious cyber incident',
|
|
'contraband' => 'Contraband',
|
|
'weapons_drugs' => 'Weapons/drugs',
|
|
'other' => 'Other',
|
|
];
|
|
?>
|
|
<h4 class="mb-3">Pending Incidents</h4>
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered" data-no-mgmt-sticky>
|
|
<thead>
|
|
<tr>
|
|
<th>Last Updater</th>
|
|
<th>Student Name</th>
|
|
<th>Grade</th>
|
|
<th>Incident Type</th>
|
|
<th>Description</th>
|
|
<th>Datetime</th>
|
|
<th>Incident State</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (!empty($flags) && is_array($flags)): ?>
|
|
<?php foreach ($flags as $flag): ?>
|
|
<tr>
|
|
<td>
|
|
<?php
|
|
if ($flag['flag_state'] === 'Open') {
|
|
$name = $flag['updated_by_open_name'] ?? null;
|
|
$id = $flag['updated_by_open'] ?? null;
|
|
echo esc($name ?: ($id ?? 'N/A'));
|
|
} elseif ($flag['flag_state'] === 'Closed') {
|
|
$name = $flag['updated_by_closed_name'] ?? null;
|
|
$id = $flag['updated_by_closed'] ?? null;
|
|
echo esc($name ?: ($id ?? 'N/A'));
|
|
} elseif ($flag['flag_state'] === 'Canceled') {
|
|
$name = $flag['updated_by_canceled_name'] ?? null;
|
|
$id = $flag['updated_by_canceled'] ?? null;
|
|
echo esc($name ?: ($id ?? 'N/A'));
|
|
} else {
|
|
echo 'N/A';
|
|
}
|
|
?>
|
|
</td>
|
|
<td><?= esc($flag['student_name']) ?></td>
|
|
<td><?= esc($flag['grade']) ?></td>
|
|
<?php
|
|
$incidentValue = trim((string)($flag['flag'] ?? ''));
|
|
$incidentLabel = $incidentValue !== ''
|
|
? ($incidentLabels[$incidentValue] ?? $incidentValue)
|
|
: 'N/A';
|
|
?>
|
|
<td><?= esc($incidentLabel) ?></td>
|
|
<td>
|
|
<?php
|
|
if ($flag['flag_state'] === 'Open') {
|
|
echo isset($flag['open_description']) ? esc($flag['open_description']) : 'N/A';
|
|
} elseif ($flag['flag_state'] === 'Closed') {
|
|
echo isset($flag['close_description']) ? esc($flag['close_description']) : 'N/A';
|
|
} elseif ($flag['flag_state'] === 'Canceled') {
|
|
echo isset($flag['cancel_description']) ? esc($flag['cancel_description']) : 'N/A';
|
|
} else {
|
|
echo 'N/A';
|
|
}
|
|
?>
|
|
</td>
|
|
<td><?= esc(!empty($flag['flag_datetime']) ? local_datetime($flag['flag_datetime'], 'm-d-Y H:i') : '') ?></td>
|
|
<td><?= esc($flag['flag_state']) ?></td>
|
|
|
|
<td>
|
|
<form id="flagForm_<?= $flag['id'] ?>" method="post">
|
|
<?= csrf_field() ?>
|
|
|
|
<select name="flag_state" class="form-select" id="flag_state_<?= $flag['id'] ?>"
|
|
data-incident="<?= esc($flag['flag']) ?>"
|
|
data-incident-key="<?=
|
|
esc(trim(strtolower(preg_replace('/[^a-z0-9]+/', '_', (string)($flag['flag'] ?? ''))), '_'))
|
|
?>"
|
|
onchange="handleFlagStateChange(<?= $flag['id'] ?>)">
|
|
<option value="">Select Action</option>
|
|
<option value="Closed" <?= $flag['flag_state'] === 'Closed' ? 'selected' : '' ?>>Closed
|
|
</option>
|
|
<option value="Canceled" <?= $flag['flag_state'] === 'Canceled' ? 'selected' : '' ?>>
|
|
Canceled</option>
|
|
</select>
|
|
|
|
<input type="hidden" name="state_description" id="state_description_<?= $flag['id'] ?>" />
|
|
<input type="hidden" name="action_taken" id="action_taken_<?= $flag['id'] ?>" />
|
|
<button type="submit" class="btn btn-sm btn-primary mt-2">Update</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<tr>
|
|
<td colspan="8">No incidents found.</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<!-- Modal for Flag State Description -->
|
|
<div class="modal fade" id="descriptionModal" tabindex="-1" aria-labelledby="descriptionModalLabel"
|
|
aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="descriptionModalLabel">Add Description</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<textarea id="flagStateDescription" class="form-control" rows="3"
|
|
placeholder="Enter description here"></textarea>
|
|
<div class="mt-3">
|
|
<label for="flagActionTaken" class="form-label">Action Taken</label>
|
|
<textarea id="flagActionTaken" class="form-control" rows="3"
|
|
placeholder="Enter action taken here"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-primary" onclick="saveDescription()">Save</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Display Flag Entries -->
|
|
<div class="d-flex gap-2 mb-3 justify-content-end">
|
|
<button id="addFlagButton" class="btn btn-success d-inline-block" onclick="toggleForm()">Add New Incident</button>
|
|
<a class="btn btn-info d-inline-block" href="/flags/incident_analysis">Incident Analysis</a>
|
|
<a id="displayFlagButton" class="btn btn-secondary d-inline-block" href="/flags/processed_flags">View Processed Incidents</a>
|
|
</div>
|
|
|
|
<!-- Flag Entry Form (Initially Hidden) -->
|
|
<div id="flagForm" class="container my-5" style="display: none;">
|
|
<form action="/flags/add" method="post" class="row g-3">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="flag_form_token" value="<?= $token ?>">
|
|
|
|
<div class="col-md-12">
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<label for="grade" class="form-label">Grade</label>
|
|
<select class="form-select" name="grade" id="grade" required>
|
|
<option value="">Select Grade</option>
|
|
<?php foreach ($grades as $grade): ?>
|
|
<option value="<?= esc($grade['id']) ?>"><?= esc($grade['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-6">
|
|
<label for="student_name" class="form-label">Student Name</label>
|
|
<select name="student" id="student_name" class="form-select" required>
|
|
<option value="">Select Student</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label for="flag" class="form-label">Incident</label>
|
|
<select name="flag" id="flag" class="form-select" required>
|
|
<option value="">Select Incident</option>
|
|
<option value="payment">Payment</option>
|
|
<option value="attendance">Attendance</option>
|
|
<option value="grade">Grade</option>
|
|
<option value="behavior">Behavior (General)</option>
|
|
<optgroup label="Level 1 - Minor (Teacher-managed)">
|
|
<option value="talking_off_task">Talking/off-task</option>
|
|
<option value="calling_out">Calling out</option>
|
|
<option value="minor_disruption">Minor disruption</option>
|
|
<option value="minor_dress_code">Minor dress code</option>
|
|
<option value="forgetting_materials">Forgetting materials</option>
|
|
</optgroup>
|
|
<optgroup label="Level 2 - Moderate / Repeated">
|
|
<option value="repeated_defiance">Repeated defiance</option>
|
|
<option value="disrespectful_tone">Disrespectful tone</option>
|
|
<option value="minor_profanity">Minor profanity</option>
|
|
<option value="repeated_tardiness">Repeated tardiness</option>
|
|
<option value="device_misuse">Device misuse</option>
|
|
<option value="minor_conflict">Minor conflict/horseplay</option>
|
|
</optgroup>
|
|
<optgroup label="Level 3 - Serious (Immediate admin)">
|
|
<option value="bullying_harassment">Bullying/harassment</option>
|
|
<option value="fighting_aggression">Fighting/aggression</option>
|
|
<option value="threats">Threats</option>
|
|
<option value="theft">Theft</option>
|
|
<option value="vandalism">Vandalism</option>
|
|
<option value="sexual_harassment">Sexual harassment</option>
|
|
<option value="serious_cyber_incident">Serious cyber incident</option>
|
|
<option value="contraband">Contraband</option>
|
|
<option value="weapons_drugs">Weapons/drugs</option>
|
|
</optgroup>
|
|
<option value="other">Other</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label for="description" class="form-label">Description</label>
|
|
<textarea name="description" id="description" class="form-control" rows="3" required></textarea>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary">Save</button>
|
|
<button type="button" class="btn btn-secondary" onclick="cancelForm()">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('scripts') ?>
|
|
<script>
|
|
let currentFlagId = null;
|
|
|
|
const incidentActionTemplates = {
|
|
payment: "Reminder/clarify expectations → parent contact if needed → admin follow-up if unresolved.",
|
|
attendance: "Log + warning → lunch detention/supervised study → parent contact → admin conference + attendance plan.",
|
|
grade: "Clarify expectations → support/reteach → parent contact if pattern → admin referral if unresolved.",
|
|
behavior: "Start small → reteach expectations → repair harm → escalate only for repeat or safety issues.",
|
|
talking_off_task: "Reminder → seat change/separation → reflection + reteach → parent contact + lunch detention/supervised study → admin referral (pattern).",
|
|
calling_out: "Reminder → seat change → reflection + reteach → parent contact + lunch detention/supervised study → admin referral (pattern).",
|
|
minor_disruption: "Reminder → seat change/separation → reflection + reteach → parent contact + lunch detention/supervised study → admin referral (pattern).",
|
|
minor_dress_code: "Reminder/clarify → reflection + reteach → parent contact + lunch detention/supervised study → admin referral (pattern).",
|
|
forgetting_materials: "Reminder + reteach routines → reflection → parent contact + lunch detention/supervised study → admin referral (pattern).",
|
|
repeated_defiance: "Reflection + restorative chat → parent contact + lunch detention → behavior contract + counselor check-in → admin referral (ISS if needed).",
|
|
disrespectful_tone: "Warning + rephrase → reflection + apology plan → parent contact + lunch detention → admin referral if repeated.",
|
|
minor_profanity: "Warning + rephrase → reflection + apology plan → parent contact + lunch detention → admin referral if repeated.",
|
|
repeated_tardiness: "Log + warning → lunch detention/supervised study → parent contact → admin conference + attendance plan.",
|
|
device_misuse: "Warning → device away/confiscate (end of day) → loss of privilege + lunch detention/supervised study → admin referral if repeated.",
|
|
minor_conflict: "Reflection + restorative chat → parent contact + lunch detention → behavior contract + counselor check-in → admin referral if repeated.",
|
|
bullying_harassment: "Immediate removal + admin referral → parent contact + investigation → safety/restorative plan → consequence based on severity (lunch detention/ISS/OSS) + restitution when applicable.",
|
|
fighting_aggression: "Immediate removal + admin referral → parent contact + investigation → safety/restorative plan → consequence based on severity (lunch detention/ISS/OSS) + restitution when applicable.",
|
|
threats: "Immediate removal + admin referral → parent contact + investigation → safety/restorative plan → consequence based on severity (lunch detention/ISS/OSS) + restitution when applicable.",
|
|
theft: "Immediate removal + admin referral → parent contact + investigation → safety/restorative plan → consequence based on severity (lunch detention/ISS/OSS) + restitution when applicable.",
|
|
vandalism: "Immediate removal + admin referral → parent contact + investigation → safety/restorative plan → consequence based on severity (lunch detention/ISS/OSS) + restitution when applicable.",
|
|
sexual_harassment: "Immediate removal + admin referral → parent contact + investigation → safety/restorative plan → consequence based on severity (lunch detention/ISS/OSS) + restitution when applicable.",
|
|
serious_cyber_incident: "Immediate removal + admin referral → parent contact + investigation → safety/restorative plan → consequence based on severity (lunch detention/ISS/OSS) + restitution when applicable.",
|
|
contraband: "Immediate removal + admin referral → parent contact + investigation → safety/restorative plan → consequence based on severity (lunch detention/ISS/OSS) + restitution when applicable.",
|
|
weapons_drugs: "Immediate removal + admin referral → parent contact + investigation → safety/restorative plan → consequence based on severity (lunch detention/ISS/OSS) + restitution when applicable.",
|
|
other: ""
|
|
};
|
|
|
|
function normalizeIncidentKey(value) {
|
|
return (value || "")
|
|
.toString()
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[()]/g, "")
|
|
.replace(/[^a-z0-9]+/g, "_")
|
|
.replace(/^_+|_+$/g, "");
|
|
}
|
|
|
|
function handleFlagStateChange(flagId) {
|
|
const flagState = document.getElementById(`flag_state_${flagId}`).value;
|
|
if (flagState === "Closed" || flagState === "Canceled") {
|
|
currentFlagId = flagId;
|
|
const select = document.getElementById(`flag_state_${flagId}`);
|
|
const incidentKeyRaw = select?.dataset?.incident || "";
|
|
const incidentKey =
|
|
select?.dataset?.incidentKey ||
|
|
normalizeIncidentKey(incidentKeyRaw);
|
|
const textarea = document.getElementById("flagStateDescription");
|
|
|
|
if (flagState === "Closed") {
|
|
if (!textarea.value || textarea.dataset.autofilled === "1") {
|
|
const text =
|
|
incidentActionTemplates[incidentKey] ??
|
|
incidentActionTemplates[incidentKeyRaw] ??
|
|
"";
|
|
textarea.value = text;
|
|
textarea.dataset.autofilled = text ? "1" : "0";
|
|
}
|
|
} else {
|
|
textarea.value = "";
|
|
textarea.dataset.autofilled = "0";
|
|
}
|
|
|
|
const actionTextarea = document.getElementById("flagActionTaken");
|
|
actionTextarea.value = "";
|
|
actionTextarea.dataset.autofilled = "0";
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('descriptionModal'));
|
|
modal.show();
|
|
}
|
|
}
|
|
|
|
function cancelForm() {
|
|
//location.reload();
|
|
var form = document.getElementById("flagForm");
|
|
var buttonadd = document.getElementById("addFlagButton");
|
|
var buttondisplay = document.getElementById("displayFlagButton");
|
|
form.style.display = "none";
|
|
buttonadd.style.display = "block";
|
|
buttondisplay.style.display = "block";
|
|
}
|
|
|
|
function saveDescription() {
|
|
const description = document.getElementById('flagStateDescription').value;
|
|
document.getElementById(`state_description_${currentFlagId}`).value = description;
|
|
const actionTaken = document.getElementById('flagActionTaken').value;
|
|
document.getElementById(`action_taken_${currentFlagId}`).value = actionTaken;
|
|
|
|
const form = document.getElementById(`flagForm_${currentFlagId}`);
|
|
const flagState = document.getElementById(`flag_state_${currentFlagId}`).value;
|
|
|
|
// Set form action based on flag state
|
|
if (flagState === "Closed") {
|
|
form.action = `/flags/closeFlag/${currentFlagId}`;
|
|
} else if (flagState === "Canceled") {
|
|
form.action = `/flags/cancelFlag/${currentFlagId}`;
|
|
}
|
|
|
|
console.log("Description set for form submission:", description); // For debugging
|
|
console.log("Form action set to:", form.action); // For debugging
|
|
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('descriptionModal'));
|
|
modal.hide();
|
|
}
|
|
|
|
document.getElementById('flagStateDescription').addEventListener('input', function() {
|
|
this.dataset.autofilled = "0";
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
document.getElementById('grade').addEventListener('change', function() {
|
|
var gradeId = this.value;
|
|
var studentDropdown = document.getElementById('student_name');
|
|
studentDropdown.innerHTML = '<option value="">Loading...</option>';
|
|
|
|
if (gradeId) {
|
|
fetch(`/flags/getStudentsByGrade/${gradeId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
studentDropdown.innerHTML = '<option value="">Select Student</option>';
|
|
data.forEach(student => {
|
|
var option = document.createElement('option');
|
|
option.value = student.id;
|
|
option.textContent = student.name;
|
|
studentDropdown.appendChild(option);
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching students:', error);
|
|
studentDropdown.innerHTML = '<option value="">Error loading students</option>';
|
|
});
|
|
} else {
|
|
studentDropdown.innerHTML = '<option value="">Select Student</option>';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
const incidentDescriptions = {
|
|
payment: "Payment-related concern, such as an overdue balance, missing tuition installment, or a fee that was not submitted on time.",
|
|
attendance: "Attendance-related concern involving absences, patterns of late arrival, or missed instructional time affecting progress.",
|
|
grade: "Grade-related concern where current performance is below expected level, missing work is affecting progress, or a trend needs attention.",
|
|
behavior: "General behavior concern that does not fit a specific category but needs documentation for follow-up and support.",
|
|
talking_off_task: "Student was talking or off-task during instruction or independent work, impacting focus or the learning environment.",
|
|
calling_out: "Student called out without permission or interrupted instruction, affecting classroom flow.",
|
|
minor_disruption: "Student caused a minor disruption (noise, movement, or distractions) that interrupted learning briefly.",
|
|
minor_dress_code: "Student was out of dress code in a minor way (e.g., small uniform issue) needing a reminder.",
|
|
forgetting_materials: "Student did not bring required materials (books, supplies, device) needed for class activities.",
|
|
repeated_defiance: "Student repeatedly refused to follow directions after being prompted, impacting class routines.",
|
|
disrespectful_tone: "Student used a disrespectful tone or language toward staff or peers, requiring documentation.",
|
|
minor_profanity: "Student used mild or inappropriate language that is not threatening but is still unacceptable.",
|
|
repeated_tardiness: "Student has a pattern of arriving late to class or school, disrupting routines.",
|
|
device_misuse: "Student used a phone or device inappropriately (off-task apps, messaging, or during instruction).",
|
|
minor_conflict: "Student engaged in minor conflict or horseplay that could escalate if not addressed.",
|
|
bullying_harassment: "Bullying or harassment behavior reported, including repeated targeted actions or intimidation.",
|
|
fighting_aggression: "Physical fighting or aggressive contact occurred or was attempted.",
|
|
threats: "Verbal or written threats made toward a person or group that raise safety concerns.",
|
|
theft: "Theft or attempted theft of property was reported.",
|
|
vandalism: "Intentional damage to property or school materials was reported.",
|
|
sexual_harassment: "Sexual harassment reported, including inappropriate comments, gestures, or contact.",
|
|
serious_cyber_incident: "Serious online incident reported (harassment, threats, or harmful digital behavior).",
|
|
contraband: "Possession of unauthorized or prohibited items on campus.",
|
|
weapons_drugs: "Possession or involvement with weapons or drugs reported.",
|
|
other: ""
|
|
};
|
|
|
|
function setIncidentDescription() {
|
|
const select = document.getElementById("flag");
|
|
const description = document.getElementById("description");
|
|
const value = select.value;
|
|
|
|
if (!description.value || description.dataset.autofilled === "1") {
|
|
const text = incidentDescriptions[value] ?? "";
|
|
description.value = text;
|
|
description.dataset.autofilled = text ? "1" : "0";
|
|
}
|
|
}
|
|
|
|
document.getElementById("flag").addEventListener("change", setIncidentDescription);
|
|
document.getElementById("description").addEventListener("input", function() {
|
|
this.dataset.autofilled = "0";
|
|
});
|
|
|
|
function toggleForm() {
|
|
var form = document.getElementById("flagForm");
|
|
var button = document.getElementById("addFlagButton");
|
|
if (form.style.display === "none") {
|
|
form.style.display = "block";
|
|
button.style.display = "none";
|
|
} else {
|
|
form.style.display = "none";
|
|
button.style.display = "block";
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<?= $this->endSection() ?>
|