init backend presensi

This commit is contained in:
mwpn
2026-03-05 14:37:36 +07:00
commit b4fda6b9c9
319 changed files with 27261 additions and 0 deletions

View File

@@ -0,0 +1,279 @@
<?php
$classId = (int) ($classId ?? 0);
$className = $className ?? 'Kelas';
?>
<div class="space-y-6" id="schedule-builder-app">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-3">
<a href="<?= base_url('dashboard') ?>" class="inline-flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-primary">
<i class="bx bx-arrow-back text-xl"></i>
<span class="text-sm font-medium">Dashboard</span>
</a>
<h1 class="text-xl font-semibold">Schedule Builder — <?= esc($className) ?></h1>
</div>
<button type="button" id="btn-save" disabled class="inline-flex items-center gap-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-hover focus:ring-2 focus:ring-primary/50 disabled:opacity-50 disabled:cursor-not-allowed">
<i class="bx bx-save text-lg"></i>
<span>Simpan</span>
</button>
</div>
<div id="builder-loading" class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-12 text-center text-gray-500 dark:text-gray-400">
Loading…
</div>
<div id="builder-error" class="hidden rounded-2xl border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 p-6 text-red-700 dark:text-red-300"></div>
<div id="builder-content" class="hidden rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead class="bg-gray-50 dark:bg-gray-700/50 text-sm text-gray-600 dark:text-gray-400">
<tr>
<th class="px-4 py-3 font-medium border-b border-gray-200 dark:border-gray-700 w-40">Slot / Waktu</th>
<th class="px-4 py-3 font-medium border-b border-gray-200 dark:border-gray-700">Senin (1)</th>
<th class="px-4 py-3 font-medium border-b border-gray-200 dark:border-gray-700">Selasa (2)</th>
<th class="px-4 py-3 font-medium border-b border-gray-200 dark:border-gray-700">Rabu (3)</th>
<th class="px-4 py-3 font-medium border-b border-gray-200 dark:border-gray-700">Kamis (4)</th>
<th class="px-4 py-3 font-medium border-b border-gray-200 dark:border-gray-700">Jumat (5)</th>
</tr>
</thead>
<tbody id="builder-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
</table>
</div>
</div>
<!-- Success toast (TailAdmin-style) -->
<div id="builder-toast" role="alert" class="hidden fixed top-4 right-4 z-50 flex items-center gap-3 rounded-lg border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/30 px-4 py-3 text-green-800 dark:text-green-200 shadow-lg transition-opacity duration-300">
<i class="bx bx-check-circle text-2xl text-green-600 dark:text-green-400"></i>
<span id="builder-toast-message">Saved.</span>
</div>
</div>
<script>
(function() {
var classId = <?= $classId ?>;
var baseUrl = '<?= base_url() ?>'.replace(/\/$/, '');
var apiBase = baseUrl + '/api/academic';
var loading = document.getElementById('builder-loading');
var errorEl = document.getElementById('builder-error');
var content = document.getElementById('builder-content');
var tbody = document.getElementById('builder-tbody');
var btnSave = document.getElementById('btn-save');
var lessonSlots = [];
var subjects = [];
var teachers = [];
var subjectTeacherMap = {};
var scheduleGrid = [];
function showLoading() {
loading.classList.remove('hidden');
errorEl.classList.add('hidden');
content.classList.add('hidden');
btnSave.disabled = true;
}
function showError(msg) {
loading.classList.add('hidden');
content.classList.add('hidden');
errorEl.classList.remove('hidden');
errorEl.textContent = msg;
btnSave.disabled = true;
}
function showContent() {
loading.classList.add('hidden');
errorEl.classList.add('hidden');
content.classList.remove('hidden');
btnSave.disabled = false;
}
function fetchJson(url) {
return fetch(url, { method: 'GET', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function(r) { return r.json().then(function(j) { return { ok: r.ok, data: j }; }); });
}
function escapeHtml(str) {
if (str == null) return '';
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function showToast(message) {
var toast = document.getElementById('builder-toast');
var msgEl = document.getElementById('builder-toast-message');
if (!toast || !msgEl) return;
msgEl.textContent = message || 'Saved.';
toast.classList.remove('hidden');
clearTimeout(window._builderToastTimer);
window._builderToastTimer = setTimeout(function() {
toast.classList.add('opacity-0');
setTimeout(function() {
toast.classList.add('hidden');
toast.classList.remove('opacity-0');
}, 300);
}, 3000);
}
/** One dropdown per cell: options = "{subject_name} — {teacher_name}", value = subjectId_teacherId. Empty = no schedule. */
function renderCell(slotId, day, existing) {
var selectedValue = '';
if (existing && existing.subject_id) {
var tid = (existing.teacher_user_id != null && existing.teacher_user_id !== '') ? existing.teacher_user_id : '0';
selectedValue = existing.subject_id + '_' + tid;
}
var room = (existing && existing.room != null) ? escapeHtml(existing.room) : '';
var opts = '<option value="">— Tidak ada jadwal —</option>';
subjects.forEach(function(s) {
var labelNoGuru = escapeHtml(s.name) + ' — (Tidak ada guru)';
opts += '<option value="' + s.id + '_0"' + (selectedValue === s.id + '_0' ? ' selected' : '') + '>' + labelNoGuru + '</option>';
var allowedTeacherIds = subjectTeacherMap[s.id] || [];
if (!Array.isArray(allowedTeacherIds) || allowedTeacherIds.length === 0) {
return;
}
teachers.forEach(function(t) {
if (allowedTeacherIds.indexOf(t.id) === -1) return;
var val = s.id + '_' + t.id;
var label = escapeHtml(s.name) + ' — ' + escapeHtml(t.name);
var sel = selectedValue === val ? ' selected' : '';
opts += '<option value="' + val + '"' + sel + '>' + label + '</option>';
});
});
return '<div class="p-2 space-y-2 border border-gray-200 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-800/50">' +
'<select class="cell-schedule w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm" data-slot="' + slotId + '" data-day="' + day + '">' + opts + '</select>' +
'<input type="text" class="cell-room w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm placeholder-gray-400" placeholder="Ruang (opsional)" value="' + room + '" data-slot="' + slotId + '" data-day="' + day + '">' +
'</div>';
}
function renderGrid() {
tbody.innerHTML = '';
lessonSlots.forEach(function(slot) {
var tr = document.createElement('tr');
tr.className = 'align-top';
var slotLabel = 'Slot ' + slot.slot_number + '<br><span class="text-xs text-gray-500">' + escapeHtml(slot.start_time) + ' ' + escapeHtml(slot.end_time) + '</span>';
var td0 = document.createElement('td');
td0.className = 'px-4 py-3 border-b border-gray-200 dark:border-gray-700 font-medium text-sm';
td0.innerHTML = slotLabel;
tr.appendChild(td0);
for (var day = 1; day <= 5; day++) {
var daySchedule = (slot.days && slot.days[day]) ? slot.days[day] : null;
var td = document.createElement('td');
td.className = 'px-4 py-3 border-b border-gray-200 dark:border-gray-700 min-w-[200px]';
td.innerHTML = renderCell(slot.id, day, daySchedule);
tr.appendChild(td);
}
tbody.appendChild(tr);
});
showContent();
}
function collectSchedules() {
var list = [];
lessonSlots.forEach(function(slot) {
for (var day = 1; day <= 5; day++) {
var sel = document.querySelector('.cell-schedule[data-slot="' + slot.id + '"][data-day="' + day + '"]');
var roomInp = document.querySelector('.cell-room[data-slot="' + slot.id + '"][data-day="' + day + '"]');
var val = sel ? (sel.value || '').trim() : '';
if (!val) continue;
var parts = val.split('_');
var subjectId = parseInt(parts[0], 10);
if (!subjectId) continue;
var teacherId = parseInt(parts[1], 10);
var room = roomInp && roomInp.value ? roomInp.value.trim() : null;
list.push({
day_of_week: day,
lesson_slot_id: slot.id,
subject_id: subjectId,
teacher_user_id: (teacherId === 0 || isNaN(teacherId)) ? undefined : teacherId,
room: room || undefined
});
}
});
return list;
}
function doSave() {
var payload = { class_id: classId, schedules: collectSchedules() };
btnSave.disabled = true;
fetch(apiBase + '/schedules/bulk-save', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
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 && r.data && r.data.success) {
showToast('Jadwal berhasil disimpan.');
} else {
var msg = (r.data && r.data.message) ? r.data.message : 'Gagal menyimpan';
showError(msg);
}
})
.catch(function() {
btnSave.disabled = false;
showError('Network error');
});
}
btnSave.addEventListener('click', doSave);
showLoading();
Promise.all([
fetchJson(apiBase + '/schedules/class/' + classId),
fetchJson(apiBase + '/lesson-slots'),
fetchJson(apiBase + '/subjects'),
fetchJson(baseUrl + '/api/users?role=GURU_MAPEL'),
fetchJson(apiBase + '/teacher-subjects/map')
]).then(function(results) {
var sched = results[0];
var slots = results[1];
var subj = results[2];
var users = results[3];
var mapRes = results[4];
if (!sched.ok || !sched.data || !sched.data.success) {
showError(sched.data && sched.data.message ? sched.data.message : 'Gagal memuat jadwal');
return;
}
if (!slots.ok || !slots.data || !slots.data.success) {
showError(slots.data && slots.data.message ? slots.data.message : 'Gagal memuat slot');
return;
}
if (!subj.ok || !subj.data || !subj.data.success) {
showError(subj.data && subj.data.message ? subj.data.message : 'Gagal memuat mapel');
return;
}
if (!users.ok || !users.data || !users.data.success) {
showError(users.data && users.data.message ? users.data.message : 'Gagal memuat guru');
return;
}
if (!mapRes.ok || !mapRes.data || !mapRes.data.success) {
showError(mapRes.data && mapRes.data.message ? mapRes.data.message : 'Gagal memuat relasi guru-mapel');
return;
}
scheduleGrid = (sched.data.data != null) ? sched.data.data : [];
lessonSlots = (slots.data.data != null) ? slots.data.data : [];
lessonSlots.sort(function(a, b) { return (a.slot_number || 0) - (b.slot_number || 0); });
subjects = (subj.data.data != null) ? subj.data.data : [];
teachers = (users.data.data != null) ? users.data.data : [];
subjectTeacherMap = (mapRes.data.data != null) ? mapRes.data.data : {};
lessonSlots.forEach(function(slot) {
var row = scheduleGrid.find(function(r) { return r.lesson_slot_id === slot.id; });
slot.days = (row && row.days) ? row.days : {};
});
renderGrid();
}).catch(function() {
showError('Network error');
});
})();
</script>