671 lines
44 KiB
PHP
671 lines
44 KiB
PHP
<?php
|
||
// Options (can be moved to config or controller)
|
||
$medicalOptions = $medicalOptions ?? [
|
||
"None",
|
||
"ADHD (Attention-Deficit/Hyperactivity Disorder)",
|
||
"Anxiety or Emotional Disorders",
|
||
"Asthma",
|
||
"Autism Spectrum Disorder (ASD)",
|
||
"Behavioral or Conduct Disorders",
|
||
"Blindness / Vision Impairment",
|
||
"Celiac Disease (Gluten Intolerance)",
|
||
"Cerebral Palsy",
|
||
"Cystic Fibrosis",
|
||
"Depression",
|
||
"Diabetes (Type 1 or Type 2)",
|
||
"Down Syndrome",
|
||
"Dyslexia or Learning Disabilities",
|
||
"Eating Disorders",
|
||
"Eczema / Severe Skin Conditions",
|
||
"Epilepsy / Seizure Disorders",
|
||
"Hearing Impairments / Deafness",
|
||
"Heart Conditions (congenital or acquired)",
|
||
"Hemophilia / Bleeding Disorders",
|
||
"Kidney Disease",
|
||
"Migraines / Chronic Headaches",
|
||
"Obsessive-Compulsive Disorder (OCD)",
|
||
"Physical Disabilities / Mobility Impairments",
|
||
"PTSD (Post-Traumatic Stress Disorder)",
|
||
"Sickle Cell Anemia",
|
||
"Speech and Language Disorders",
|
||
"Thyroid Disorders",
|
||
"Tourette Syndrome",
|
||
"Traumatic Brain Injury (TBI)",
|
||
"Rheumatic diseases",
|
||
"Ulcerative Colitis / Crohn’s Disease",
|
||
"Other"
|
||
];
|
||
|
||
$allergyOptions = $allergyOptions ?? [
|
||
"None",
|
||
"Animal Dander (cats, dogs, etc.)",
|
||
"Antibiotics",
|
||
"Bee stings",
|
||
"Cockroach",
|
||
"Corn",
|
||
"Dust Mites",
|
||
"Egg",
|
||
"Fire ant stings",
|
||
"Fish",
|
||
"Fragrances / Perfumes",
|
||
"Latex",
|
||
"Milk / Dairy",
|
||
"Mold",
|
||
"Mosquito bites",
|
||
"Peanut",
|
||
"Pollen (grass, tree, weed)",
|
||
"Sesame",
|
||
"Shellfish (shrimp, crab, lobster, etc.)",
|
||
"Soy",
|
||
"Tree Nuts (almond, cashew, walnut, etc.)",
|
||
"Wasp stings",
|
||
"Wheat / Gluten",
|
||
"Other"
|
||
];
|
||
|
||
$gradeOptions = $gradeOptions ?? ['KG', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', 'Youth'];
|
||
?>
|
||
|
||
<?= $this->extend('layout/management_layout') ?>
|
||
<?= $this->section('content') ?>
|
||
|
||
<div class="container-fluid">
|
||
<div class="wrapper">
|
||
<div class="content">
|
||
<h2 class="text-center mt-4 mb-3">Student Profiles</h2>
|
||
|
||
<div class="table-responsive">
|
||
<table id="myTable" class="table table-striped table-hover align-middle">
|
||
<thead class="table-dark">
|
||
<tr>
|
||
<th>School ID</th>
|
||
<th>First Name</th>
|
||
<th>Last Name</th>
|
||
<th>DOB</th>
|
||
<th>Age</th>
|
||
<th>Gender</th>
|
||
<th>Assigned Grade</th>
|
||
<th>Active</th>
|
||
<th>Medical Conditions</th>
|
||
<th>Allergies</th>
|
||
<th>Photo Consent</th>
|
||
<th>Registration Date</th>
|
||
<th style="min-width: 220px;">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (!empty($students)): ?>
|
||
<?php foreach ($students as $student): ?>
|
||
<?php
|
||
$modalIdContact = 'contactModal' . (int)$student['id'];
|
||
$modalIdEdit = 'editStudentModal' . (int)$student['id'];
|
||
$updateUrl = base_url('administrator/students/update'); // POST target
|
||
|
||
// Format DOB for display
|
||
$dobDisp = '';
|
||
if (!empty($student['dob'])) {
|
||
try {
|
||
$dobDisp = (new DateTime($student['dob']))->format('m-d-Y');
|
||
} catch (\Throwable $e) {
|
||
$dobDisp = esc($student['dob']);
|
||
}
|
||
}
|
||
|
||
// Format Reg Date for display (as local mm-dd-YYYY)
|
||
$regDisp = '';
|
||
if (!empty($student['registration_date'])) {
|
||
try {
|
||
$dt = new \DateTime($student['registration_date'], new \DateTimeZone('UTC'));
|
||
$dt->setTimezone(new \DateTimeZone(user_timezone()));
|
||
$regDisp = local_datetime($student['registration_date'], 'm-d-Y H:i');
|
||
} catch (\Throwable $e) {
|
||
$regDisp = esc($student['registration_date']);
|
||
}
|
||
}
|
||
|
||
// Input-safe values
|
||
$dobVal = $dobDisp;
|
||
$regDateLocal = '';
|
||
if (!empty($student['registration_date'])) {
|
||
try {
|
||
$dt2 = new \DateTime($student['registration_date'], new \DateTimeZone('UTC'));
|
||
$dt2->setTimezone(new \DateTimeZone(user_timezone()));
|
||
$regDateLocal = local_datetime($student['registration_date'], 'Y-m-d\TH:i');
|
||
} catch (\Throwable $e) {
|
||
$regDateLocal = '';
|
||
}
|
||
}
|
||
$isActive = (int)($student['is_active'] ?? 1);
|
||
|
||
// Preselected lists (server-computed strings or joined lists)
|
||
$medSelected = array_filter(array_map('trim', preg_split('/[,\n;]+/', (string)($student['medical_conditions'] ?? ''), -1, PREG_SPLIT_NO_EMPTY)));
|
||
$allSelected = array_filter(array_map('trim', preg_split('/[,\n;]+/', (string)($student['allergies'] ?? ''), -1, PREG_SPLIT_NO_EMPTY)));
|
||
|
||
$medOther = implode(', ', array_diff($medSelected, $medicalOptions));
|
||
$allOther = implode(', ', array_diff($allSelected, $allergyOptions));
|
||
|
||
$medId = 'medical_conditions_' . (int)$student['id'];
|
||
$allId = 'allergies_' . (int)$student['id'];
|
||
$medOtherId = 'medical_other_' . (int)$student['id'];
|
||
$allOtherId = 'allergy_other_' . (int)$student['id'];
|
||
?>
|
||
<tr>
|
||
<td><?= esc($student['school_id']) ?></td>
|
||
<td>
|
||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['id'] ?? 0) ?>">
|
||
<?= esc($student['firstname']) ?>
|
||
</a>
|
||
</td>
|
||
<td>
|
||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['id'] ?? 0) ?>">
|
||
<?= esc($student['lastname']) ?>
|
||
</a>
|
||
</td>
|
||
<td><?= esc($dobDisp) ?></td>
|
||
<td><?= esc((string)($student['age'] ?? '')) ?></td>
|
||
<td><?= esc($student['gender']) ?></td>
|
||
<td><?= esc($student['class_section_name']) ?></td>
|
||
<td><?= $isActive ? 'Yes' : 'No' ?></td>
|
||
<td><?= esc($student['medical_conditions'] ?? '') ?></td>
|
||
<td><?= esc($student['allergies'] ?? '') ?></td>
|
||
<td><?= !empty($student['photo_consent']) ? 'Yes' : 'No' ?></td>
|
||
<td><?= esc($regDisp) ?></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 ?>">
|
||
Contact Information
|
||
</button>
|
||
<!-- Edit Modal Trigger -->
|
||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#<?= $modalIdEdit ?>">
|
||
Edit Student
|
||
</button>
|
||
|
||
<!-- Contact Modal -->
|
||
<div class="modal fade" id="<?= $modalIdContact ?>" tabindex="-1" aria-labelledby="<?= $modalIdContact ?>Label" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content shadow">
|
||
<div class="modal-header bg-primary text-white">
|
||
<h5 class="modal-title" id="<?= $modalIdContact ?>Label">
|
||
Contact Info - <?= esc($student['firstname']) ?> <?= esc($student['lastname']) ?>
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<h5 class="mb-3 border-bottom pb-1">👨👩👧 Parent Information</h5>
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<p><strong>Name:</strong> <?= esc(($student['parent_firstname'] ?? '') . ' ' . ($student['parent_lastname'] ?? '')) ?></p>
|
||
<p><strong>Phone:</strong> <?= esc($student['parent_phone'] ?? 'N/A') ?></p>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<p><strong>Email:</strong> <?= esc($student['parent_email'] ?? 'N/A') ?></p>
|
||
</div>
|
||
</div>
|
||
<h5 class="mb-3 border-bottom pb-1">🚨 Emergency Contact</h5>
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<p><strong>Name:</strong> <?= esc($student['emergency_name'] ?? 'N/A') ?></p>
|
||
<p><strong>Relationship:</strong> <?= esc($student['emergency_relationship'] ?? 'N/A') ?></p>
|
||
<p><strong>Phone:</strong> <?= esc($student['emergency_phone'] ?? 'N/A') ?></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit Student Modal -->
|
||
<div class="modal fade" id="<?= $modalIdEdit ?>" tabindex="-1" aria-labelledby="<?= $modalIdEdit ?>Label" aria-hidden="true">
|
||
<div class="modal-dialog modal-xl">
|
||
<div class="modal-content shadow">
|
||
<div class="modal-header bg-success text-white">
|
||
<h5 class="modal-title" id="<?= $modalIdEdit ?>Label">
|
||
Edit Student - <?= esc($student['firstname']) ?> <?= esc($student['lastname']) ?>
|
||
</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
|
||
<form method="post" action="<?= $updateUrl ?>" class="needs-validation" novalidate>
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="id" value="<?= (int)$student['id'] ?>">
|
||
|
||
<div class="modal-body">
|
||
<div class="row g-3">
|
||
<!-- school_id -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">School ID</label>
|
||
<input type="text" name="school_id" class="form-control" value="<?= esc($student['school_id'] ?? '') ?>" maxlength="100">
|
||
</div>
|
||
|
||
<!-- firstname -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">First Name</label>
|
||
<input type="text" name="firstname" class="form-control"
|
||
value="<?= esc($student['firstname'] ?? '') ?>"
|
||
pattern="^[A-Za-z\s\-]{2,30}$" required>
|
||
<div class="invalid-feedback">Only letters, spaces, or dashes (2–30 chars).</div>
|
||
</div>
|
||
|
||
<!-- lastname -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">Last Name</label>
|
||
<input type="text" name="lastname" class="form-control"
|
||
value="<?= esc($student['lastname'] ?? '') ?>"
|
||
pattern="^[A-Za-z\s\-]{2,30}$" required>
|
||
<div class="invalid-feedback">Only letters, spaces, or dashes (2–30 chars).</div>
|
||
</div>
|
||
|
||
<!-- gender -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">Gender</label>
|
||
<select name="gender" class="form-select" required>
|
||
<option value="" disabled <?= empty($student['gender']) ? 'selected' : '' ?>>Select…</option>
|
||
<option value="Male" <?= (strcasecmp($student['gender'] ?? '', 'Male') === 0) ? 'selected' : '' ?>>Male</option>
|
||
<option value="Female" <?= (strcasecmp($student['gender'] ?? '', 'Female') === 0) ? 'selected' : '' ?>>Female</option>
|
||
</select>
|
||
<div class="invalid-feedback">Please choose a gender.</div>
|
||
</div>
|
||
|
||
<!-- dob -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">DOB</label>
|
||
<input type="date" name="dob" class="form-control js-dob" value="<?= esc($dobVal) ?>" required>
|
||
<div class="invalid-feedback">Please provide a valid date of birth.</div>
|
||
</div>
|
||
|
||
<!-- age (readonly) -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">Age</label>
|
||
<input type="number" name="age" class="form-control js-age" value="<?= (int)($student['age'] ?? 0) ?>" min="0" step="1" readonly>
|
||
<div class="form-text">Auto-calculated from DOB.</div>
|
||
</div>
|
||
|
||
<!-- registration_grade -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">Grade</label>
|
||
<select name="registration_grade" class="form-select" required>
|
||
<option value="" disabled <?= empty($student['registration_grade']) ? 'selected' : '' ?>>Select…</option>
|
||
<?php foreach ($gradeOptions as $opt): ?>
|
||
<option value="<?= esc($opt) ?>" <?= (strcasecmp((string)($student['registration_grade'] ?? ''), (string)$opt) === 0) ? 'selected' : '' ?>>
|
||
<?= esc($opt) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<div class="invalid-feedback">Please choose a grade.</div>
|
||
</div>
|
||
|
||
<!-- registration_date (datetime-local) -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">Registration Date & Time</label>
|
||
<input type="datetime-local" name="registration_date" class="form-control" value="<?= esc($regDateLocal) ?>">
|
||
<div class="form-text">Local time (<?= esc(user_timezone()) ?>).</div>
|
||
</div>
|
||
|
||
<!-- parent_id -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">Parent ID</label>
|
||
<input type="number" name="parent_id" class="form-control" value="<?= (int)($student['parent_id'] ?? 0) ?>" min="1" step="1" required>
|
||
<div class="invalid-feedback">Parent ID is required.</div>
|
||
</div>
|
||
|
||
<!-- year_of_registration -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">Year of Registration</label>
|
||
<input type="text" name="year_of_registration" class="form-control"
|
||
value="<?= esc($student['year_of_registration'] ?? '') ?>"
|
||
pattern="^\d{4}(-\d{4})?$" maxlength="25" placeholder="e.g., 2025 or 2025-2026">
|
||
<div class="invalid-feedback">Use YYYY or YYYY-YYYY.</div>
|
||
</div>
|
||
|
||
<!-- school_year -->
|
||
<?php
|
||
$syDatalistId = 'datalist-school-years-' . (int)$student['id'];
|
||
$nowY = (int)date('Y');
|
||
$syOptions = [];
|
||
for ($y = $nowY - 2; $y <= $nowY + 3; $y++) {
|
||
$syOptions[] = $y . '-' . ($y + 1);
|
||
}
|
||
?>
|
||
<div class="col-md-3">
|
||
<label class="form-label">School Year</label>
|
||
<input list="<?= $syDatalistId ?>" name="school_year" class="form-control"
|
||
value="<?= esc($student['school_year'] ?? '') ?>"
|
||
pattern="^\d{4}-\d{4}$" maxlength="9" placeholder="YYYY-YYYY">
|
||
<datalist id="<?= $syDatalistId ?>">
|
||
<?php foreach ($syOptions as $sy): ?>
|
||
<option value="<?= esc($sy) ?>"></option>
|
||
<?php endforeach; ?>
|
||
</datalist>
|
||
<div class="invalid-feedback">Format: YYYY-YYYY.</div>
|
||
</div>
|
||
|
||
<!-- rfid_tag -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">RFID Tag</label>
|
||
<input type="text" name="rfid_tag" class="form-control" value="<?= esc($student['rfid_tag'] ?? '') ?>" maxlength="100" placeholder="Optional">
|
||
</div>
|
||
|
||
<!-- semester -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">Semester</label>
|
||
<?php $sem = trim((string)($student['semester'] ?? '')); ?>
|
||
<select name="semester" class="form-select">
|
||
<option value="" <?= $sem === '' ? 'selected' : '' ?>>—</option>
|
||
<option value="Fall" <?= strcasecmp($sem, 'Fall') === 0 ? 'selected' : '' ?>>Fall</option>
|
||
<option value="Spring" <?= strcasecmp($sem, 'Spring') === 0 ? 'selected' : '' ?>>Spring</option>
|
||
<option value="Summer" <?= strcasecmp($sem, 'Summer') === 0 ? 'selected' : '' ?>>Summer</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- tuition_paid -->
|
||
<div class="col-md-3">
|
||
<label class="form-label d-block">Tuition Paid</label>
|
||
<input type="hidden" name="tuition_paid" value="0">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" value="1" id="tuition_paid_<?= (int)$student['id'] ?>" name="tuition_paid" <?= !empty($student['tuition_paid']) ? 'checked' : '' ?>>
|
||
<label class="form-check-label" for="tuition_paid_<?= (int)$student['id'] ?>">Mark as paid</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- is_new -->
|
||
<div class="col-md-3">
|
||
<label class="form-label">Is New Student?</label>
|
||
<?php $isNewVal = (string)($student['is_new'] ?? '1'); ?>
|
||
<select name="is_new" class="form-select" required>
|
||
<option value="1" <?= $isNewVal === '1' ? 'selected' : '' ?>>Yes (New)</option>
|
||
<option value="0" <?= $isNewVal === '0' ? 'selected' : '' ?>>No (Returning)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- is_active -->
|
||
<div class="col-md-3">
|
||
<label class="form-label d-block">Active Status</label>
|
||
<input type="hidden" name="is_active" value="0">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" value="1" id="is_active_<?= (int)$student['id'] ?>" name="is_active" <?= $isActive ? 'checked' : '' ?>>
|
||
<label class="form-check-label" for="is_active_<?= (int)$student['id'] ?>">Show in classes/attendance</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- photo_consent -->
|
||
<div class="col-md-12">
|
||
<div class="form-check mt-2">
|
||
<input type="hidden" name="photo_consent" value="0">
|
||
<input class="form-check-input" type="checkbox" value="1" id="photo_consent_<?= (int)$student['id'] ?>" name="photo_consent" <?= !empty($student['photo_consent']) ? 'checked' : '' ?>>
|
||
<label class="form-check-label" for="photo_consent_<?= (int)$student['id'] ?>">Photo Consent</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Medical Conditions -->
|
||
<div class="col-md-6">
|
||
<label class="form-label">Medical Conditions</label>
|
||
<!-- Hidden payload + touched flag -->
|
||
<input type="hidden" name="medical_conditions" id="<?= $medId ?>_hidden">
|
||
<input type="hidden" name="medical_touched" id="<?= $medId ?>_touched" value="0">
|
||
<select class="form-select js-multi"
|
||
id="<?= $medId ?>"
|
||
multiple
|
||
data-none="None"
|
||
data-hidden="#<?= $medId ?>_hidden"
|
||
data-other="#<?= $medOtherId ?>"
|
||
data-touched="#<?= $medId ?>_touched">
|
||
<?php foreach ($medicalOptions as $opt): ?>
|
||
<option value="<?= esc($opt) ?>" <?= in_array($opt, $medSelected, true) ? 'selected' : '' ?>>
|
||
<?= esc($opt) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<div class="form-text">Hold Ctrl/⌘ to select multiple. Choose <b>None</b> if no conditions.</div>
|
||
|
||
<label class="form-label mt-2">Other (free text, comma-separated)</label>
|
||
<input type="text" class="form-control" id="<?= $medOtherId ?>" placeholder="e.g., Seasonal rhinitis" value="<?= esc($medOther) ?>">
|
||
</div>
|
||
|
||
<!-- Allergies -->
|
||
<div class="col-md-6">
|
||
<label class="form-label">Allergies</label>
|
||
<!-- Hidden payload + touched flag -->
|
||
<input type="hidden" name="allergies" id="<?= $allId ?>_hidden">
|
||
<input type="hidden" name="allergies_touched" id="<?= $allId ?>_touched" value="0">
|
||
<select class="form-select js-multi"
|
||
id="<?= $allId ?>"
|
||
multiple
|
||
data-none="None"
|
||
data-hidden="#<?= $allId ?>_hidden"
|
||
data-other="#<?= $allOtherId ?>"
|
||
data-touched="#<?= $allId ?>_touched">
|
||
<?php foreach ($allergyOptions as $opt): ?>
|
||
<option value="<?= esc($opt) ?>" <?= in_array($opt, $allSelected, true) ? 'selected' : '' ?>>
|
||
<?= esc($opt) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<div class="form-text">Hold Ctrl/⌘ to select multiple. Choose <b>None</b> if no allergies.</div>
|
||
|
||
<label class="form-label mt-2">Other (free text, comma-separated)</label>
|
||
<input type="text" class="form-control" id="<?= $allOtherId ?>" placeholder="e.g., Penicillin" value="<?= esc($allOther) ?>">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||
<button type="submit" class="btn btn-success">Save Changes</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- /Edit Student Modal -->
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php else: ?>
|
||
<tr>
|
||
<td colspan="13" class="text-center">No students found.</td>
|
||
</tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<?= $this->endSection() ?>
|
||
|
||
<?= $this->section('scripts') ?>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
/* ============== DataTables (guarded) ============== */
|
||
if (window.jQuery && typeof jQuery.fn.DataTable === 'function' && document.getElementById('myTable')) {
|
||
jQuery('#myTable').DataTable(); // Use your own options if needed
|
||
}
|
||
|
||
/* ============== Bootstrap 5 validation ============== */
|
||
(function() {
|
||
const forms = document.querySelectorAll('.needs-validation');
|
||
Array.prototype.forEach.call(forms, (form) => {
|
||
form.addEventListener('submit', (evt) => {
|
||
if (!form.checkValidity()) {
|
||
evt.preventDefault();
|
||
evt.stopPropagation();
|
||
}
|
||
form.classList.add('was-validated');
|
||
}, false);
|
||
});
|
||
})();
|
||
|
||
/* ============== Helpers ============== */
|
||
function splitCSV(s) {
|
||
if (!s) return [];
|
||
return s.split(',').map(x => x.trim()).filter(Boolean);
|
||
}
|
||
|
||
function uniq(arr) {
|
||
const seen = new Set();
|
||
const out = [];
|
||
for (const v of arr) {
|
||
const k = v.toLowerCase();
|
||
if (!seen.has(k)) {
|
||
seen.add(k);
|
||
out.push(v);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function setTouched(sel) {
|
||
const tSel = sel.dataset.touched ? document.querySelector(sel.dataset.touched) : null;
|
||
if (tSel) tSel.value = '1';
|
||
}
|
||
|
||
function updateHiddenFromSelect(sel) {
|
||
const hiddenSel = sel.dataset.hidden ? document.querySelector(sel.dataset.hidden) : null;
|
||
if (!hiddenSel) return;
|
||
const noneLabel = (sel.dataset.none || 'None').toLowerCase();
|
||
const otherInp = sel.dataset.other ? document.querySelector(sel.dataset.other) : null;
|
||
|
||
const selected = Array.from(sel.selectedOptions).map(o => o.value).filter(Boolean);
|
||
const others = otherInp && !otherInp.disabled ? splitCSV(otherInp.value) : [];
|
||
let final = uniq([...selected, ...others]);
|
||
|
||
// Remove "None" token from payload; exclusivity handled elsewhere
|
||
final = final.filter(v => v.toLowerCase() !== noneLabel);
|
||
|
||
hiddenSel.value = final.length ? final.join(', ') : '';
|
||
}
|
||
|
||
function enforceNone(sel) {
|
||
const noneLabel = sel.dataset.none || 'None';
|
||
const values = Array.from(sel.selectedOptions).map(o => o.value);
|
||
const otherInp = sel.dataset.other ? document.querySelector(sel.dataset.other) : null;
|
||
|
||
if (values.includes(noneLabel)) {
|
||
// Deselect everything except "None"
|
||
Array.from(sel.options).forEach(opt => {
|
||
if (opt.value !== noneLabel) opt.selected = false;
|
||
});
|
||
if (otherInp) {
|
||
otherInp.value = '';
|
||
otherInp.disabled = true;
|
||
}
|
||
} else {
|
||
// Ensure "None" is off if other items chosen
|
||
Array.from(sel.options).forEach(opt => {
|
||
if (opt.value === noneLabel) opt.selected = false;
|
||
});
|
||
if (otherInp) {
|
||
otherInp.disabled = false;
|
||
}
|
||
}
|
||
updateHiddenFromSelect(sel);
|
||
}
|
||
|
||
function bindSelect(sel) {
|
||
if (sel.__bound) return;
|
||
sel.__bound = true;
|
||
|
||
// Initial normalize + pack based on server preselects
|
||
enforceNone(sel);
|
||
|
||
sel.addEventListener('change', () => {
|
||
enforceNone(sel);
|
||
setTouched(sel);
|
||
});
|
||
const otherInp = sel.dataset.other ? document.querySelector(sel.dataset.other) : null;
|
||
if (otherInp) {
|
||
otherInp.addEventListener('input', () => {
|
||
updateHiddenFromSelect(sel);
|
||
setTouched(sel);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Bind all multi-selects present on the page (all modals rendered server-side)
|
||
document.querySelectorAll('select.js-multi').forEach(bindSelect);
|
||
|
||
// Modal lifecycle: wire age + (re)bind selects inside shown modal
|
||
document.querySelectorAll('.modal').forEach((modal) => {
|
||
modal.addEventListener('shown.bs.modal', () => {
|
||
// Age from DOB
|
||
const dob = modal.querySelector('.js-dob');
|
||
const age = modal.querySelector('.js-age');
|
||
if (dob && age) {
|
||
const update = () => {
|
||
if (!dob.value) {
|
||
age.value = '';
|
||
return;
|
||
}
|
||
const d = new Date(dob.value + 'T00:00:00');
|
||
if (isNaN(d.getTime())) {
|
||
age.value = '';
|
||
return;
|
||
}
|
||
const t = new Date();
|
||
let a = t.getFullYear() - d.getFullYear();
|
||
const m = t.getMonth() - d.getMonth();
|
||
if (m < 0 || (m === 0 && t.getDate() < d.getDate())) a--;
|
||
age.value = a >= 0 ? a : '';
|
||
};
|
||
update();
|
||
dob.addEventListener('change', update);
|
||
dob.addEventListener('input', update);
|
||
}
|
||
|
||
modal.querySelectorAll('select.js-multi').forEach(bindSelect);
|
||
|
||
// Ensure hidden fields packed and flags set on submit
|
||
modal.querySelectorAll('form').forEach((form) => {
|
||
if (form.__syncBound) return;
|
||
form.__syncBound = true;
|
||
form.addEventListener('submit', () => {
|
||
form.querySelectorAll('select.js-multi').forEach((sel) => {
|
||
enforceNone(sel);
|
||
updateHiddenFromSelect(sel);
|
||
setTouched(sel);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
// --- CSRF sync: ensure latest token is sent on submit ---
|
||
(function attachCsrfSync() {
|
||
const TOKEN_NAME = '<?= csrf_token() ?>';
|
||
const COOKIE_NAME = <?= json_encode(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>;
|
||
|
||
function getCookie(name) {
|
||
const parts = document.cookie.split(';');
|
||
for (let i = 0; i < parts.length; i++) {
|
||
const p = parts[i].trim();
|
||
if (p.startsWith(name + '=')) return decodeURIComponent(p.substring(name.length + 1));
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function syncFormToken(form) {
|
||
try {
|
||
const cookieVal = getCookie(COOKIE_NAME);
|
||
if (!cookieVal) return; // nothing to sync
|
||
const hidden = form.querySelector('input[name="' + TOKEN_NAME + '"]');
|
||
if (hidden) hidden.value = cookieVal;
|
||
} catch (_) {
|
||
// no-op
|
||
}
|
||
}
|
||
|
||
// Bind to all edit forms on this page
|
||
document.querySelectorAll('form').forEach((form) => {
|
||
if (form.__csrfSyncBound) return;
|
||
form.__csrfSyncBound = true;
|
||
form.addEventListener('submit', () => syncFormToken(form));
|
||
});
|
||
})();
|
||
});
|
||
</script>
|
||
<?= $this->endSection() ?>
|