Files
presensi/app/Views/dashboard/discipline.php
2026-03-05 14:37:36 +07:00

508 lines
24 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<div class="space-y-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 class="text-xl font-semibold">Poin Pelanggaran Siswa</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1 text-sm">
Guru/Wali Kelas dapat mencatat pelanggaran dan melihat rekap poin perilaku siswa.
</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Filter & Form -->
<div class="space-y-6 lg:col-span-1">
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm space-y-3">
<h2 class="font-semibold text-gray-900 dark:text-gray-100 text-sm">Filter</h2>
<div class="space-y-3 text-sm">
<div>
<label class="block mb-1 text-gray-600 dark:text-gray-300">Kelas</label>
<select id="filter-class" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm">
<option value="">Semua kelas</option>
</select>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block mb-1 text-gray-600 dark:text-gray-300">Dari tanggal</label>
<input type="date" id="filter-from" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm">
</div>
<div>
<label class="block mb-1 text-gray-600 dark:text-gray-300">Sampai tanggal</label>
<input type="date" id="filter-to" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm">
</div>
</div>
<button type="button" id="btn-filter" class="w-full mt-1 inline-flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover">
Terapkan Filter
</button>
</div>
</div>
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm space-y-3">
<h2 class="font-semibold text-gray-900 dark:text-gray-100 text-sm">Catat Pelanggaran Baru</h2>
<div class="space-y-3 text-sm">
<div>
<label class="block mb-1 text-gray-600 dark:text-gray-300">Kelas</label>
<select id="form-class" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm">
<option value="">Pilih kelas</option>
</select>
</div>
<div>
<label class="block mb-1 text-gray-600 dark:text-gray-300">Siswa</label>
<select id="form-student" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm">
<option value="">Pilih siswa</option>
</select>
</div>
<div>
<label class="block mb-1 text-gray-600 dark:text-gray-300">Kategori Pelanggaran</label>
<select id="form-violation-category" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm">
<option value="">Pilih kategori</option>
</select>
</div>
<div>
<label class="block mb-1 text-gray-600 dark:text-gray-300">Jenis Pelanggaran</label>
<select id="form-violation" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm">
<option value="">Pilih jenis pelanggaran</option>
</select>
</div>
<div>
<label class="block mb-1 text-gray-600 dark:text-gray-300">Tanggal & Waktu</label>
<input type="datetime-local" id="form-occurred-at" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm">
</div>
<div>
<label class="block mb-1 text-gray-600 dark:text-gray-300">Catatan (opsional)</label>
<textarea id="form-notes" rows="2" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm"></textarea>
</div>
<button type="button" id="btn-save-violation" class="w-full inline-flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg bg-red-600 text-white text-sm font-medium hover:bg-red-700">
Simpan Pelanggaran
</button>
</div>
</div>
</div>
<!-- Rekap / List -->
<div class="lg:col-span-2 space-y-4">
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm">
<div class="flex items-center justify-between mb-3">
<h2 class="font-semibold text-gray-900 dark:text-gray-100 text-sm">Rekap Pelanggaran</h2>
<span id="recap-range" class="text-xs text-gray-500 dark:text-gray-400"></span>
</div>
<div id="recap-content" class="text-sm text-gray-600 dark:text-gray-300">
<p id="recap-empty" class="text-gray-500 dark:text-gray-400">Belum ada data untuk filter ini.</p>
<div id="recap-table-wrap" class="hidden overflow-x-auto">
<table class="w-full text-left text-xs sm:text-sm">
<thead class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-400">
<tr>
<th class="px-3 py-2 font-medium">Siswa</th>
<th class="px-3 py-2 font-medium">Kelas</th>
<th class="px-3 py-2 font-medium text-right">Total Poin</th>
<th class="px-3 py-2 font-medium text-right">Jumlah Kasus</th>
<th class="px-3 py-2 font-medium">Level / Tindakan</th>
</tr>
</thead>
<tbody id="recap-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
</table>
</div>
</div>
</div>
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm">
<div class="flex items-center justify-between mb-3">
<h2 class="font-semibold text-gray-900 dark:text-gray-100 text-sm">Riwayat Pelanggaran</h2>
</div>
<div id="violations-loading" class="py-10 text-center text-gray-500 dark:text-gray-400 hidden">Memuat…</div>
<div id="violations-error" class="hidden py-3 text-sm text-red-600 dark:text-red-400"></div>
<div id="violations-content" class="hidden overflow-x-auto">
<table class="w-full text-left text-xs sm:text-sm">
<thead class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-400">
<tr>
<th class="px-3 py-2 font-medium">Tanggal</th>
<th class="px-3 py-2 font-medium">Siswa</th>
<th class="px-3 py-2 font-medium">Kelas</th>
<th class="px-3 py-2 font-medium">Pelanggaran</th>
<th class="px-3 py-2 font-medium text-right">Skor</th>
<th class="px-3 py-2 font-medium">Guru/Wali</th>
</tr>
</thead>
<tbody id="violations-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div id="discipline-toast-container" class="fixed bottom-6 right-6 z-[60] flex flex-col gap-2 pointer-events-none"></div>
<script>
(function() {
var baseUrl = '<?= base_url() ?>'.replace(/\/$/, '');
var apiClasses = baseUrl + '/api/academic/classes';
var apiStudents = baseUrl + '/api/academic/students';
var apiViolationsMaster = baseUrl + '/api/discipline/violations';
var apiDisciplineLevels = baseUrl + '/api/discipline/levels';
var apiStudentViolations = baseUrl + '/api/discipline/student-violations';
var filterClass = document.getElementById('filter-class');
var filterFrom = document.getElementById('filter-from');
var filterTo = document.getElementById('filter-to');
var btnFilter = document.getElementById('btn-filter');
var formClass = document.getElementById('form-class');
var formStudent = document.getElementById('form-student');
var formViolationCategory = document.getElementById('form-violation-category');
var formViolation = document.getElementById('form-violation');
var formOccurredAt = document.getElementById('form-occurred-at');
var formNotes = document.getElementById('form-notes');
var btnSave = document.getElementById('btn-save-violation');
var recapRange = document.getElementById('recap-range');
var recapEmpty = document.getElementById('recap-empty');
var recapTableWrap = document.getElementById('recap-table-wrap');
var recapTbody = document.getElementById('recap-tbody');
var vLoading = document.getElementById('violations-loading');
var vError = document.getElementById('violations-error');
var vContent = document.getElementById('violations-content');
var vTbody = document.getElementById('violations-tbody');
var toastContainer = document.getElementById('discipline-toast-container');
var classesList = [];
var violationsMaster = [];
var violationItemsByCategory = {};
var disciplineLevels = [];
function showToast(message, type) {
type = type || 'success';
var el = document.createElement('div');
el.className = 'pointer-events-auto px-4 py-3 rounded-lg shadow-lg text-sm font-medium ' + (type === 'success' ? 'bg-green-600 text-white' : 'bg-red-600 text-white');
el.textContent = message;
toastContainer.appendChild(el);
setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 4000);
}
function fetchJson(url) {
return fetch(url, { method: 'GET', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } })
.then(function(r) { return r.json().then(function(j) { return { ok: r.ok, data: j }; }); });
}
function escapeHtml(str) {
if (str == null) return '';
var d = document.createElement('div'); d.textContent = str; return d.innerHTML;
}
function loadClasses(callback) {
fetchJson(apiClasses).then(function(r) {
var list = (r.ok && r.data && r.data.data) ? r.data.data : [];
classesList = list;
[filterClass, formClass].forEach(function(sel, idx) {
if (!sel) return;
var keepFirst = sel.options.length > 0 ? sel.options[0] : null;
sel.innerHTML = '';
if (keepFirst) sel.appendChild(keepFirst);
list.forEach(function(c) {
var parts = [];
if (c.grade) parts.push(c.grade);
if (c.major) parts.push(c.major);
if (c.name) parts.push(c.name);
var label = c.full_label || parts.join(' ').trim() || ('Kelas ' + c.id);
var opt = document.createElement('option');
opt.value = c.id;
opt.textContent = label;
sel.appendChild(opt);
});
});
if (callback) callback();
}).catch(function() {
showToast('Gagal memuat kelas', 'error');
});
}
function loadViolationsMaster() {
fetchJson(apiViolationsMaster).then(function(r) {
if (!r.ok) { showToast('Gagal memuat daftar pelanggaran', 'error'); return; }
violationsMaster = (r.data && r.data.data) ? r.data.data : [];
violationItemsByCategory = {};
// Isi dropdown kategori
if (formViolationCategory) {
var firstCat = formViolationCategory.options[0];
formViolationCategory.innerHTML = '';
if (firstCat) formViolationCategory.appendChild(firstCat);
violationsMaster.forEach(function(cat) {
if (!Array.isArray(cat.items)) return;
violationItemsByCategory[cat.id] = cat.items;
var opt = document.createElement('option');
opt.value = cat.id;
opt.textContent = '[' + (cat.code || '') + '] ' + (cat.name || '');
formViolationCategory.appendChild(opt);
});
}
// Reset dropdown jenis pelanggaran sampai kategori dipilih
if (formViolation) {
var firstV = formViolation.options[0];
formViolation.innerHTML = '';
if (firstV) formViolation.appendChild(firstV);
}
}).catch(function() {
showToast('Gagal memuat daftar pelanggaran', 'error');
});
}
function loadDisciplineLevels() {
fetchJson(apiDisciplineLevels).then(function(r) {
if (!r.ok) { showToast('Gagal memuat level disiplin', 'error'); return; }
disciplineLevels = (r.data && r.data.data) ? r.data.data : [];
}).catch(function() {
showToast('Gagal memuat level disiplin', 'error');
});
}
function loadStudentsForClass(classId, callback) {
if (!classId) {
if (formStudent) {
var first = formStudent.options[0];
formStudent.innerHTML = '';
if (first) formStudent.appendChild(first);
}
if (callback) callback([]);
return;
}
var url = apiStudents + '?class_id=' + encodeURIComponent(classId) + '&per_page=200';
fetchJson(url).then(function(r) {
var list = (r.ok && r.data && r.data.data) ? r.data.data : [];
if (formStudent) {
var first = formStudent.options[0];
formStudent.innerHTML = '';
if (first) formStudent.appendChild(first);
list.forEach(function(s) {
var opt = document.createElement('option');
opt.value = s.id;
opt.textContent = s.name + (s.nisn ? ' (' + s.nisn + ')' : '');
formStudent.appendChild(opt);
});
}
if (callback) callback(list);
}).catch(function() {
showToast('Gagal memuat siswa', 'error');
});
}
function formatDateTime(dt) {
if (!dt) return '';
var d = new Date(dt);
if (isNaN(d.getTime())) return dt;
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
}
function findLevelForScore(score) {
if (!Array.isArray(disciplineLevels) || !disciplineLevels.length) return null;
for (var i = 0; i < disciplineLevels.length; i++) {
var lvl = disciplineLevels[i];
var min = lvl.min_score || 0;
var max = (lvl.max_score != null) ? lvl.max_score : null;
if (score >= min && (max === null || score <= max)) {
return lvl;
}
}
return null;
}
function applyRecap(list) {
if (!Array.isArray(list) || list.length === 0) {
recapEmpty.classList.remove('hidden');
recapTableWrap.classList.add('hidden');
recapTbody.innerHTML = '';
return;
}
var byStudent = {};
list.forEach(function(r) {
var sid = r.student_id;
if (!byStudent[sid]) {
byStudent[sid] = {
student_name: r.student_name,
class_label: r.class_label || '-',
total_score: 0,
count: 0
};
}
byStudent[sid].total_score += (r.violation_score || 0);
byStudent[sid].count += 1;
});
var rows = Object.keys(byStudent).map(function(id) {
var x = byStudent[id];
return {
student_name: x.student_name,
class_label: x.class_label,
total_score: x.total_score,
count: x.count
};
});
rows.sort(function(a, b) { return b.total_score - a.total_score; });
recapTbody.innerHTML = '';
rows.forEach(function(x) {
var lvl = findLevelForScore(x.total_score);
var levelText = lvl ? (lvl.title + ' — ' + (lvl.school_action || '')) : '-';
var tr = document.createElement('tr');
tr.innerHTML =
'<td class="px-3 py-2 font-medium">' + escapeHtml(x.student_name) + '</td>' +
'<td class="px-3 py-2">' + escapeHtml(x.class_label || '-') + '</td>' +
'<td class="px-3 py-2 text-right text-red-600 dark:text-red-400 font-semibold">' + x.total_score + '</td>' +
'<td class="px-3 py-2 text-right text-gray-600 dark:text-gray-400">' + x.count + '</td>' +
'<td class="px-3 py-2 text-xs text-gray-700 dark:text-gray-300 max-w-xs whitespace-pre-line">' + escapeHtml(levelText) + '</td>';
recapTbody.appendChild(tr);
});
recapEmpty.classList.add('hidden');
recapTableWrap.classList.remove('hidden');
}
function loadStudentViolations() {
vLoading.classList.remove('hidden');
vError.classList.add('hidden');
vContent.classList.add('hidden');
var params = [];
var classVal = filterClass.value;
if (classVal) params.push('class_id=' + encodeURIComponent(classVal));
if (filterFrom.value) params.push('from_date=' + encodeURIComponent(filterFrom.value));
if (filterTo.value) params.push('to_date=' + encodeURIComponent(filterTo.value));
var url = apiStudentViolations;
if (params.length) url += '?' + params.join('&');
fetchJson(url).then(function(r) {
vLoading.classList.add('hidden');
if (!r.ok) {
vError.textContent = (r.data && r.data.message) ? r.data.message : 'Gagal memuat data';
vError.classList.remove('hidden');
return;
}
var list = (r.data && r.data.data) ? r.data.data : [];
// Recap
applyRecap(list);
vTbody.innerHTML = '';
if (!list.length) {
vContent.classList.remove('hidden');
return;
}
list.forEach(function(row) {
var tr = document.createElement('tr');
tr.innerHTML =
'<td class="px-3 py-2 whitespace-nowrap">' + escapeHtml(formatDateTime(row.occurred_at)) + '</td>' +
'<td class="px-3 py-2">' + escapeHtml(row.student_name) + '</td>' +
'<td class="px-3 py-2">' + escapeHtml(row.class_label || '-') + '</td>' +
'<td class="px-3 py-2">' + '[' + escapeHtml(row.category_code || '') + '] ' + escapeHtml(row.violation_title || '') + '</td>' +
'<td class="px-3 py-2 text-right text-red-600 dark:text-red-400">' + (row.violation_score || 0) + '</td>' +
'<td class="px-3 py-2">' + escapeHtml(row.reported_by_name || '-') + '</td>';
vTbody.appendChild(tr);
});
vContent.classList.remove('hidden');
}).catch(function() {
vLoading.classList.add('hidden');
vError.textContent = 'Gagal memuat data';
vError.classList.remove('hidden');
});
var label = '';
if (filterFrom.value || filterTo.value) {
label = (filterFrom.value || '') + ' s/d ' + (filterTo.value || '');
}
recapRange.textContent = label;
}
btnFilter.addEventListener('click', loadStudentViolations);
if (formViolationCategory) {
formViolationCategory.addEventListener('change', function() {
var catId = formViolationCategory.value ? parseInt(formViolationCategory.value, 10) : 0;
if (!formViolation) return;
var first = formViolation.options[0];
formViolation.innerHTML = '';
if (first) formViolation.appendChild(first);
if (!catId || !violationItemsByCategory[catId]) {
return;
}
violationItemsByCategory[catId].forEach(function(v) {
var opt = document.createElement('option');
opt.value = v.id;
opt.textContent = v.title + ' (' + v.score + ' poin)';
formViolation.appendChild(opt);
});
});
}
formClass.addEventListener('change', function() {
var cid = formClass.value;
loadStudentsForClass(cid);
});
btnSave.addEventListener('click', function() {
var studentId = formStudent.value ? parseInt(formStudent.value, 10) : 0;
var violationId = formViolation.value ? parseInt(formViolation.value, 10) : 0;
var occurredAt = formOccurredAt.value;
var notes = formNotes.value.trim() || null;
if (!formClass.value) {
showToast('Pilih kelas terlebih dahulu', 'error');
return;
}
if (!studentId) {
showToast('Pilih siswa terlebih dahulu', 'error');
return;
}
if (!violationId) {
showToast('Pilih jenis pelanggaran', 'error');
return;
}
var payload = {
student_id: studentId,
violation_id: violationId
};
if (occurredAt) {
payload.occurred_at = occurredAt.replace('T', ' ') + ':00';
}
if (notes) payload.notes = notes;
btnSave.disabled = true;
fetch(apiStudentViolations, {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify(payload)
}).then(function(r) { return r.json().then(function(j) { return { ok: r.ok, data: j }; }); })
.then(function(r) {
btnSave.disabled = false;
if (!r.ok) {
showToast(r.data && r.data.message ? r.data.message : 'Gagal menyimpan pelanggaran', 'error');
return;
}
showToast('Pelanggaran berhasil dicatat');
formViolation.value = '';
formOccurredAt.value = '';
formNotes.value = '';
loadStudentViolations();
}).catch(function() {
btnSave.disabled = false;
showToast('Gagal menyimpan pelanggaran', 'error');
});
});
// Init defaults
var today = new Date();
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
var todayStr = today.getFullYear() + '-' + pad(today.getMonth() + 1) + '-' + pad(today.getDate());
filterFrom.value = todayStr;
filterTo.value = todayStr;
loadClasses(function() {
loadViolationsMaster();
loadDisciplineLevels();
loadStudentViolations();
});
})();
</script>