init backend presensi
This commit is contained in:
279
app/Views/dashboard/schedule_builder.php
Normal file
279
app/Views/dashboard/schedule_builder.php
Normal 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>
|
||||
Reference in New Issue
Block a user