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