Update simulasi-mk.html
update simulasi mk prasyarat
This commit is contained in:
192
simulasi-mk.html
192
simulasi-mk.html
@@ -20,6 +20,11 @@
|
|||||||
.course-card:hover { transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); z-index: 10; }
|
.course-card:hover { transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); z-index: 10; }
|
||||||
.course-card.dragging { opacity: 0.5; border: 2px dashed #a8a29e; background-color: #f5f5f4; }
|
.course-card.dragging { opacity: 0.5; border: 2px dashed #a8a29e; background-color: #f5f5f4; }
|
||||||
|
|
||||||
|
/* Validation Styles */
|
||||||
|
.course-card.violation { border: 2px solid #ef4444; background-color: #fef2f2; }
|
||||||
|
.course-card.violation .cat-bar { background-color: #ef4444 !important; }
|
||||||
|
.violation-badge { position: absolute; right: 0.5rem; bottom: 0.5rem; font-size: 0.65rem; color: #dc2626; font-weight: bold; display: flex; align-items: center; gap: 2px; }
|
||||||
|
|
||||||
/* Grade Badges */
|
/* Grade Badges */
|
||||||
.grade-badge { position: absolute; right: 0.5rem; top: 0.5rem; font-size: 0.7rem; font-weight: bold; padding: 0.1rem 0.4rem; border-radius: 0.25rem; }
|
.grade-badge { position: absolute; right: 0.5rem; top: 0.5rem; font-size: 0.7rem; font-weight: bold; padding: 0.1rem 0.4rem; border-radius: 0.25rem; }
|
||||||
.grade-A { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; }
|
.grade-A { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; }
|
||||||
@@ -68,7 +73,7 @@
|
|||||||
/* Layout Fixes */
|
/* Layout Fixes */
|
||||||
.main-container {
|
.main-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1280px; /* Sedikit lebih kecil dari 1400px agar lebih rapi */
|
max-width: 1280px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
@@ -126,7 +131,7 @@
|
|||||||
<h1 class="text-3xl md:text-4xl font-bold text-slate-900 mb-2">Simulasi Studi Berbasis IPS</h1>
|
<h1 class="text-3xl md:text-4xl font-bold text-slate-900 mb-2">Simulasi Studi Berbasis IPS</h1>
|
||||||
<p class="text-slate-600 max-w-2xl mx-auto text-sm md:text-base">
|
<p class="text-slate-600 max-w-2xl mx-auto text-sm md:text-base">
|
||||||
Simulasi nyata: <strong>IPS Semester lalu menentukan jatah SKS semester depan.</strong><br>
|
Simulasi nyata: <strong>IPS Semester lalu menentukan jatah SKS semester depan.</strong><br>
|
||||||
Jika nilai <strong>E</strong>, gunakan opsi <em>"Ulang di Tahun Depan"</em> (Klik Kanan) untuk memperbaiki.
|
Sistem <strong>Prasyarat Aktif</strong>: Anda tidak bisa mengambil mata kuliah lanjut jika prasyarat belum lulus.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-6 flex flex-wrap justify-center gap-3 text-xs font-semibold opacity-80">
|
<div class="mt-6 flex flex-wrap justify-center gap-3 text-xs font-semibold opacity-80">
|
||||||
@@ -193,13 +198,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// --- UTILS: Fix for HTTP Environments (Coolify) ---
|
// --- UTILS: Fix for HTTP Environments (Coolify) ---
|
||||||
// crypto.randomUUID() only works in Secure Contexts (HTTPS).
|
|
||||||
// This fallback ensures the app works on HTTP or local IPs.
|
|
||||||
function generateUUID() {
|
function generateUUID() {
|
||||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
}
|
}
|
||||||
// Fallback for non-secure contexts
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||||
return v.toString(16);
|
return v.toString(16);
|
||||||
@@ -219,6 +221,20 @@
|
|||||||
RES: { color: 'bg-red-500' }
|
RES: { color: 'bg-red-500' }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Prerequisite Rules ---
|
||||||
|
// M7: Logika Komputasional
|
||||||
|
// M14: Dasar Pemrograman
|
||||||
|
// M19: Struktur Data
|
||||||
|
// M20: Jaringan Komputer
|
||||||
|
// M28: Pemrograman Jaringan
|
||||||
|
// M31: Manajemen Jaringan
|
||||||
|
const prerequisites = {
|
||||||
|
"M14": { reqId: "M7", reqName: "Logika Komputasional" },
|
||||||
|
"M19": { reqId: "M14", reqName: "Dasar Pemrograman" },
|
||||||
|
"M28": { reqId: "M20", reqName: "Jaringan Komputer" },
|
||||||
|
"M31": { reqId: "M28", reqName: "Pemrograman Jaringan" }
|
||||||
|
};
|
||||||
|
|
||||||
const baseCurriculum = {
|
const baseCurriculum = {
|
||||||
1: [
|
1: [
|
||||||
{ id: "M1", name: "Pend. Pancasila", sks: 2, cat: "MKWU" },
|
{ id: "M1", name: "Pend. Pancasila", sks: 2, cat: "MKWU" },
|
||||||
@@ -327,7 +343,7 @@
|
|||||||
// --- State Management ---
|
// --- State Management ---
|
||||||
let currentPlan = {};
|
let currentPlan = {};
|
||||||
let currentTrackId = 'general';
|
let currentTrackId = 'general';
|
||||||
let contextTarget = null; // { semId, index }
|
let contextTarget = null;
|
||||||
|
|
||||||
// --- Core Logic ---
|
// --- Core Logic ---
|
||||||
|
|
||||||
@@ -339,7 +355,6 @@
|
|||||||
for(let i=1; i<=8; i++) {
|
for(let i=1; i<=8; i++) {
|
||||||
currentPlan[i] = [];
|
currentPlan[i] = [];
|
||||||
if(baseCurriculum[i]) {
|
if(baseCurriculum[i]) {
|
||||||
// Use generateUUID instead of crypto.randomUUID
|
|
||||||
currentPlan[i] = baseCurriculum[i].map(c => ({...c, grade: 4, uuid: generateUUID()}));
|
currentPlan[i] = baseCurriculum[i].map(c => ({...c, grade: 4, uuid: generateUUID()}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,15 +370,37 @@
|
|||||||
renderAll();
|
renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Prerequisite Check Function ---
|
||||||
|
function checkPrerequisite(courseId, targetSem) {
|
||||||
|
const rule = prerequisites[courseId];
|
||||||
|
if (!rule) return { allowed: true };
|
||||||
|
|
||||||
|
let passed = false;
|
||||||
|
// Scan only semesters BEFORE the target semester
|
||||||
|
for (let i = 1; i < targetSem; i++) {
|
||||||
|
if (!currentPlan[i]) continue;
|
||||||
|
// Find instance of prerequisite course that is Passed (Grade > 0)
|
||||||
|
const found = currentPlan[i].find(c => c.id === rule.reqId && c.grade > 0);
|
||||||
|
if (found) {
|
||||||
|
passed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passed) {
|
||||||
|
return { allowed: false, msg: `Syarat: Lulus ${rule.reqName} di semester sebelumnya.` };
|
||||||
|
}
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
function calculateSemStats(semId) {
|
function calculateSemStats(semId) {
|
||||||
const courses = currentPlan[semId] || [];
|
const courses = currentPlan[semId] || [];
|
||||||
let totalSKS = 0;
|
let totalSKS = 0;
|
||||||
let totalPoints = 0;
|
let totalPoints = 0;
|
||||||
let loadSKS = 0; // Termasuk yang E
|
let loadSKS = 0;
|
||||||
|
|
||||||
courses.forEach(c => {
|
courses.forEach(c => {
|
||||||
loadSKS += c.sks;
|
loadSKS += c.sks;
|
||||||
// Hanya hitung IP dari sks yang diambil dan bukan yang ditandai mengulang (retake)
|
|
||||||
if(c.grade > 0) {
|
if(c.grade > 0) {
|
||||||
totalSKS += c.sks;
|
totalSKS += c.sks;
|
||||||
totalPoints += (c.sks * c.grade);
|
totalPoints += (c.sks * c.grade);
|
||||||
@@ -386,6 +423,16 @@
|
|||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
|
|
||||||
|
// First Pass: Check Violations (Prerequisites)
|
||||||
|
// Need to run this before rendering to mark cards
|
||||||
|
for(let i=1; i<=8; i++) {
|
||||||
|
currentPlan[i].forEach(course => {
|
||||||
|
const check = checkPrerequisite(course.id, i);
|
||||||
|
course.violation = !check.allowed;
|
||||||
|
course.violationMsg = check.msg;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for(let i=1; i<=8; i++) {
|
for(let i=1; i<=8; i++) {
|
||||||
let maxSKS = 20;
|
let maxSKS = 20;
|
||||||
let prevIPS = 0;
|
let prevIPS = 0;
|
||||||
@@ -459,8 +506,13 @@
|
|||||||
const catData = categories[c.cat] || { color: 'bg-gray-400' };
|
const catData = categories[c.cat] || { color: 'bg-gray-400' };
|
||||||
const isFailed = c.grade === 0;
|
const isFailed = c.grade === 0;
|
||||||
const isRetake = c.isRetake === true;
|
const isRetake = c.isRetake === true;
|
||||||
|
const isViolation = c.violation === true;
|
||||||
|
|
||||||
|
// Add 'violation' class if prereq failed
|
||||||
|
card.className = `course-card bg-white p-2 pl-4 rounded shadow-sm border border-slate-200 relative
|
||||||
|
${isFailed ? 'border-red-300 bg-red-50' : ''}
|
||||||
|
${isViolation ? 'violation' : ''}`;
|
||||||
|
|
||||||
card.className = `course-card bg-white p-2 pl-4 rounded shadow-sm border border-slate-200 relative ${isFailed ? 'border-red-300 bg-red-50' : ''}`;
|
|
||||||
card.draggable = true;
|
card.draggable = true;
|
||||||
card.setAttribute('ondragstart', `dragSemester(event, ${semId}, ${index})`);
|
card.setAttribute('ondragstart', `dragSemester(event, ${semId}, ${index})`);
|
||||||
card.oncontextmenu = (e) => { e.preventDefault(); showContextMenu(e, semId, index); };
|
card.oncontextmenu = (e) => { e.preventDefault(); showContextMenu(e, semId, index); };
|
||||||
@@ -484,10 +536,61 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grade-badge ${gradeClass}">${gradeLabel}</div>
|
<div class="grade-badge ${gradeClass}">${gradeLabel}</div>
|
||||||
|
${isViolation ? `<div class="violation-badge"><i class="fas fa-exclamation-triangle"></i> Prasyarat Invalid</div>` : ''}
|
||||||
`;
|
`;
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Drag & Drop ---
|
||||||
|
let dragSource = null;
|
||||||
|
|
||||||
|
function dragCatalog(ev, id, name, sks, cat) {
|
||||||
|
dragSource = { type: 'catalog', course: { id, name, sks: parseInt(sks), cat, grade: 4, uuid: generateUUID() } };
|
||||||
|
ev.dataTransfer.effectAllowed = "copy";
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragSemester(ev, semId, index) {
|
||||||
|
dragSource = { type: 'semester', semId, index };
|
||||||
|
ev.dataTransfer.effectAllowed = "move";
|
||||||
|
ev.target.classList.add('dragging');
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowDrop(ev) { ev.preventDefault(); }
|
||||||
|
|
||||||
|
function drop(ev, targetSem) {
|
||||||
|
ev.preventDefault();
|
||||||
|
document.querySelectorAll('.course-card').forEach(el => el.classList.remove('dragging'));
|
||||||
|
|
||||||
|
if(!dragSource) return;
|
||||||
|
|
||||||
|
let courseToAdd = null;
|
||||||
|
if(dragSource.type === 'catalog') {
|
||||||
|
courseToAdd = dragSource.course;
|
||||||
|
} else {
|
||||||
|
courseToAdd = currentPlan[dragSource.semId][dragSource.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Prerequisite Check Before Dropping ---
|
||||||
|
const check = checkPrerequisite(courseToAdd.id, targetSem);
|
||||||
|
if (!check.allowed) {
|
||||||
|
showToast("❌ " + check.msg);
|
||||||
|
return; // Block Action
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dragSource.type === 'catalog') {
|
||||||
|
currentPlan[targetSem].push(dragSource.course);
|
||||||
|
} else {
|
||||||
|
const { semId, index } = dragSource;
|
||||||
|
if(semId === targetSem) return;
|
||||||
|
const item = currentPlan[semId][index];
|
||||||
|
currentPlan[semId].splice(index, 1);
|
||||||
|
currentPlan[targetSem].push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAll();
|
||||||
|
dragSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Context Menu Logic ---
|
// --- Context Menu Logic ---
|
||||||
function showContextMenu(e, semId, index) {
|
function showContextMenu(e, semId, index) {
|
||||||
contextTarget = { semId, index };
|
contextTarget = { semId, index };
|
||||||
@@ -495,7 +598,6 @@
|
|||||||
const course = currentPlan[semId][index];
|
const course = currentPlan[semId][index];
|
||||||
const btnRetake = document.getElementById('btn-retake');
|
const btnRetake = document.getElementById('btn-retake');
|
||||||
|
|
||||||
// Show Retake button ONLY if Grade is E (0)
|
|
||||||
if (course.grade === 0) {
|
if (course.grade === 0) {
|
||||||
btnRetake.style.display = 'block';
|
btnRetake.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
@@ -503,12 +605,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
menu.style.display = 'block';
|
menu.style.display = 'block';
|
||||||
|
|
||||||
let x = e.pageX;
|
let x = e.pageX;
|
||||||
let y = e.pageY;
|
let y = e.pageY;
|
||||||
// Prevent menu going offscreen
|
|
||||||
if(x + 200 > window.innerWidth) x = window.innerWidth - 210;
|
if(x + 200 > window.innerWidth) x = window.innerWidth - 210;
|
||||||
|
|
||||||
menu.style.left = `${x}px`;
|
menu.style.left = `${x}px`;
|
||||||
menu.style.top = `${y}px`;
|
menu.style.top = `${y}px`;
|
||||||
}
|
}
|
||||||
@@ -522,12 +621,11 @@
|
|||||||
const { semId, index } = contextTarget;
|
const { semId, index } = contextTarget;
|
||||||
currentPlan[semId][index].grade = grade;
|
currentPlan[semId][index].grade = grade;
|
||||||
|
|
||||||
// Jika E, beri notifikasi untuk opsi ulang
|
|
||||||
if(grade === 0) {
|
if(grade === 0) {
|
||||||
showToast("Mata kuliah Gagal (E). Klik kanan > 'Ulang di Tahun Depan' untuk memperbaiki.");
|
showToast("Mata kuliah Gagal (E). Klik kanan > 'Ulang di Tahun Depan' untuk memperbaiki.");
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAll();
|
renderAll(); // Will trigger re-validation of downstream courses
|
||||||
}
|
}
|
||||||
hideContextMenu();
|
hideContextMenu();
|
||||||
}
|
}
|
||||||
@@ -537,7 +635,6 @@
|
|||||||
const { semId, index } = contextTarget;
|
const { semId, index } = contextTarget;
|
||||||
const course = currentPlan[semId][index];
|
const course = currentPlan[semId][index];
|
||||||
|
|
||||||
// Target: 2 semester berikutnya
|
|
||||||
const targetSem = parseInt(semId) + 2;
|
const targetSem = parseInt(semId) + 2;
|
||||||
|
|
||||||
if (targetSem > 8) {
|
if (targetSem > 8) {
|
||||||
@@ -547,15 +644,21 @@
|
|||||||
if (exists) {
|
if (exists) {
|
||||||
showToast("Mata kuliah ini sudah ada di Semester " + targetSem + ".");
|
showToast("Mata kuliah ini sudah ada di Semester " + targetSem + ".");
|
||||||
} else {
|
} else {
|
||||||
const newCourse = {
|
// Check Prereq again for the target sem (just in case)
|
||||||
...course,
|
const check = checkPrerequisite(course.id, targetSem);
|
||||||
grade: 4,
|
if (!check.allowed) {
|
||||||
uuid: generateUUID(), // Secure UUID
|
showToast("❌ Tidak bisa mengulang: " + check.msg);
|
||||||
isRetake: true
|
} else {
|
||||||
};
|
const newCourse = {
|
||||||
currentPlan[targetSem].push(newCourse);
|
...course,
|
||||||
showToast(`Mata kuliah ditambahkan ke Semester ${targetSem}.`);
|
grade: 4,
|
||||||
renderAll();
|
uuid: generateUUID(),
|
||||||
|
isRetake: true
|
||||||
|
};
|
||||||
|
currentPlan[targetSem].push(newCourse);
|
||||||
|
showToast(`Mata kuliah ditambahkan ke Semester ${targetSem}.`);
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,43 +687,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Drag & Drop ---
|
|
||||||
let dragSource = null;
|
|
||||||
|
|
||||||
function dragCatalog(ev, id, name, sks, cat) {
|
|
||||||
// Use generateUUID
|
|
||||||
dragSource = { type: 'catalog', course: { id, name, sks: parseInt(sks), cat, grade: 4, uuid: generateUUID() } };
|
|
||||||
ev.dataTransfer.effectAllowed = "copy";
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragSemester(ev, semId, index) {
|
|
||||||
dragSource = { type: 'semester', semId, index };
|
|
||||||
ev.dataTransfer.effectAllowed = "move";
|
|
||||||
ev.target.classList.add('dragging');
|
|
||||||
}
|
|
||||||
|
|
||||||
function allowDrop(ev) { ev.preventDefault(); }
|
|
||||||
|
|
||||||
function drop(ev, targetSem) {
|
|
||||||
ev.preventDefault();
|
|
||||||
document.querySelectorAll('.course-card').forEach(el => el.classList.remove('dragging'));
|
|
||||||
|
|
||||||
if(!dragSource) return;
|
|
||||||
|
|
||||||
if(dragSource.type === 'catalog') {
|
|
||||||
currentPlan[targetSem].push(dragSource.course);
|
|
||||||
} else {
|
|
||||||
const { semId, index } = dragSource;
|
|
||||||
if(semId === targetSem) return;
|
|
||||||
const item = currentPlan[semId][index];
|
|
||||||
currentPlan[semId].splice(index, 1);
|
|
||||||
currentPlan[targetSem].push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAll();
|
|
||||||
dragSource = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Sidebar & Catalog ---
|
// --- Sidebar & Catalog ---
|
||||||
function toggleCatalog() { document.getElementById('catalog-sidebar').classList.toggle('closed'); }
|
function toggleCatalog() { document.getElementById('catalog-sidebar').classList.toggle('closed'); }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user