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

280 lines
13 KiB
PHP
Raw 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.

<?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>