Update simulasi-mk.html
versi 7
This commit is contained in:
107
simulasi-mk.html
107
simulasi-mk.html
@@ -25,8 +25,8 @@
|
||||
.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; }
|
||||
|
||||
/* Elective Styles (New) */
|
||||
.course-card.is-elective { background-color: #fffbeb; border-color: #fcd34d; } /* Yellow tint */
|
||||
/* Elective Styles */
|
||||
.course-card.is-elective { background-color: #fffbeb; border-color: #fcd34d; }
|
||||
|
||||
/* 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; }
|
||||
@@ -134,7 +134,7 @@
|
||||
<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">
|
||||
Simulasi nyata: <strong>IPS Semester lalu menentukan jatah SKS semester depan.</strong><br>
|
||||
Jalur <strong>UMUM (Campuran)</strong> sekarang menampilkan mata kuliah spesifik di semester akhir. Buka Katalog untuk mengganti sesuai minat Anda.
|
||||
Jika IPS rendah, Anda <strong>wajib mengurangi SKS</strong> di semester berikutnya.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 flex flex-wrap justify-center gap-3 text-xs font-semibold opacity-80">
|
||||
@@ -200,7 +200,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- UTILS: Fix for HTTP Environments (Coolify) ---
|
||||
// --- UTILS: Fix for HTTP Environments ---
|
||||
function generateUUID() {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
@@ -224,7 +224,6 @@
|
||||
RES: { color: 'bg-red-500' }
|
||||
};
|
||||
|
||||
// --- Prerequisite Rules ---
|
||||
const prerequisites = {
|
||||
"M14": { reqId: "M7", reqName: "Logika Komputasional" },
|
||||
"M19": { reqId: "M14", reqName: "Dasar Pemrograman" },
|
||||
@@ -300,13 +299,12 @@
|
||||
};
|
||||
|
||||
const electives = {
|
||||
// General track now has specific courses as a "Sample Mix"
|
||||
general: [
|
||||
{ id: "GEN1", name: "Cross-Platform Dev", sks: 3, cat: "EL" }, // RPL flavor
|
||||
{ id: "GEN2", name: "Data Mining", sks: 3, cat: "EL" }, // AI flavor
|
||||
{ id: "GEN3", name: "Cloud Computing", sks: 3, cat: "EL" }, // Net flavor
|
||||
{ id: "GEN4", name: "E-Business", sks: 3, cat: "EL" }, // SI flavor
|
||||
{ id: "GEN5", name: "Blockchain Tech", sks: 3, cat: "EL" } // Advanced
|
||||
{ id: "GEN1", name: "Cross-Platform Dev", sks: 3, cat: "EL" },
|
||||
{ id: "GEN2", name: "Data Mining", sks: 3, cat: "EL" },
|
||||
{ id: "GEN3", name: "Cloud Computing", sks: 3, cat: "EL" },
|
||||
{ id: "GEN4", name: "E-Business", sks: 3, cat: "EL" },
|
||||
{ id: "GEN5", name: "Blockchain Tech", sks: 3, cat: "EL" }
|
||||
],
|
||||
ai: [
|
||||
{ id: "AI1", name: "Deep Learning", sks: 3, cat: "EL" },
|
||||
@@ -349,7 +347,6 @@
|
||||
currentTrackId = trackId;
|
||||
currentPlan = {};
|
||||
|
||||
// Populate Plan
|
||||
for(let i=1; i<=8; i++) {
|
||||
currentPlan[i] = [];
|
||||
if(baseCurriculum[i]) {
|
||||
@@ -357,7 +354,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Add Electives with Flag
|
||||
const tr = electives[trackId] || electives['general'];
|
||||
const distribute = [[5,0], [6,1], [7,2], [7,3], [8,4]];
|
||||
distribute.forEach(([sem, idx]) => {
|
||||
@@ -366,7 +362,7 @@
|
||||
...tr[idx],
|
||||
grade: 4,
|
||||
uuid: generateUUID(),
|
||||
isElective: true // Tag as elective
|
||||
isElective: true
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -375,16 +371,13 @@
|
||||
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;
|
||||
@@ -398,22 +391,25 @@
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
// --- Fixed Stat Calculations ---
|
||||
function calculateSemStats(semId) {
|
||||
const courses = currentPlan[semId] || [];
|
||||
let totalSKS = 0;
|
||||
let totalPoints = 0;
|
||||
let loadSKS = 0;
|
||||
let sksAttempted = 0; // Divisor IPS (Termasuk E)
|
||||
let sksPassed = 0; // Total SKS Lulus (Grade > 0)
|
||||
let totalPoints = 0; // Numerator IPS
|
||||
|
||||
courses.forEach(c => {
|
||||
loadSKS += c.sks;
|
||||
if(c.grade > 0) {
|
||||
totalSKS += c.sks;
|
||||
totalPoints += (c.sks * c.grade);
|
||||
sksAttempted += c.sks;
|
||||
totalPoints += (c.sks * c.grade); // Grade 0 * sks = 0
|
||||
if (c.grade > 0) {
|
||||
sksPassed += c.sks;
|
||||
}
|
||||
});
|
||||
|
||||
const ips = totalSKS > 0 ? (totalPoints / totalSKS) : 0;
|
||||
return { ips, loadSKS };
|
||||
// IPS = Total Points / SKS Attempted
|
||||
const ips = sksAttempted > 0 ? (totalPoints / sksAttempted) : 0;
|
||||
|
||||
return { ips, sksAttempted, sksPassed, totalPoints };
|
||||
}
|
||||
|
||||
function getMaxSKS(prevIPS) {
|
||||
@@ -428,7 +424,7 @@
|
||||
if (!grid) return;
|
||||
grid.innerHTML = '';
|
||||
|
||||
// First Pass: Check Violations (Prerequisites)
|
||||
// First Pass: Check Violations
|
||||
for(let i=1; i<=8; i++) {
|
||||
currentPlan[i].forEach(course => {
|
||||
const check = checkPrerequisite(course.id, i);
|
||||
@@ -448,7 +444,8 @@
|
||||
}
|
||||
|
||||
const currentStats = calculateSemStats(i);
|
||||
const currentLoad = currentStats.loadSKS;
|
||||
// Use sksAttempted for load display
|
||||
const currentLoad = currentStats.sksAttempted;
|
||||
const isOverload = currentLoad > maxSKS;
|
||||
|
||||
const semDiv = document.createElement('div');
|
||||
@@ -576,12 +573,38 @@
|
||||
courseToAdd = currentPlan[dragSource.semId][dragSource.index];
|
||||
}
|
||||
|
||||
// --- 1. Prerequisite Check ---
|
||||
const check = checkPrerequisite(courseToAdd.id, targetSem);
|
||||
if (!check.allowed) {
|
||||
showToast("❌ " + check.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 2. SKS Limit Check (NEW) ---
|
||||
let maxSKS = 20; // Default for Sem 1
|
||||
let prevIPS = 0;
|
||||
|
||||
if (targetSem > 1) {
|
||||
// Get IPS from previous semester
|
||||
const prevStats = calculateSemStats(targetSem - 1);
|
||||
prevIPS = prevStats.ips;
|
||||
maxSKS = getMaxSKS(prevIPS);
|
||||
}
|
||||
|
||||
// Current load in target semester
|
||||
const currentStats = calculateSemStats(targetSem);
|
||||
const currentLoad = currentStats.sksAttempted;
|
||||
|
||||
// Check if adding this course exceeds limit
|
||||
if (currentLoad + courseToAdd.sks > maxSKS) {
|
||||
let msg = `⚠️ Gagal! Maksimal ${maxSKS} SKS.`;
|
||||
if(targetSem > 1) {
|
||||
msg += ` (IPS Sem ${targetSem-1}: ${prevIPS.toFixed(2)})`;
|
||||
}
|
||||
showToast(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if(dragSource.type === 'catalog') {
|
||||
currentPlan[targetSem].push(dragSource.course);
|
||||
} else {
|
||||
@@ -768,9 +791,11 @@
|
||||
let domainChart = null;
|
||||
|
||||
function updateGlobalStats() {
|
||||
let totalSKS = 0;
|
||||
let totalPoints = 0;
|
||||
let failedSKS = 0;
|
||||
let globalAttempted = 0;
|
||||
let globalPassed = 0;
|
||||
let globalPoints = 0;
|
||||
let globalFailedSKS = 0;
|
||||
|
||||
let semIPS = [];
|
||||
let catCounts = { MKWU:0, BSC:0, FND:0, CORE:0, ADV:0, PROF:0, EL:0 };
|
||||
|
||||
@@ -778,26 +803,28 @@
|
||||
const stats = calculateSemStats(i);
|
||||
semIPS.push(stats.ips);
|
||||
|
||||
globalAttempted += stats.sksAttempted;
|
||||
globalPassed += stats.sksPassed;
|
||||
globalPoints += stats.totalPoints;
|
||||
|
||||
currentPlan[i].forEach(c => {
|
||||
if (c.grade === 0) globalFailedSKS += c.sks;
|
||||
// Count categories for passed/active courses
|
||||
if (c.grade > 0) {
|
||||
totalSKS += c.sks;
|
||||
totalPoints += (c.sks * c.grade);
|
||||
const cat = c.cat === 'CAP' || c.cat === 'RES' ? 'PROF' : c.cat;
|
||||
if(catCounts[cat] !== undefined) catCounts[cat] += c.sks;
|
||||
} else {
|
||||
failedSKS += c.sks;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const ipk = totalSKS > 0 ? (totalPoints / totalSKS) : 0;
|
||||
document.getElementById('total-sks-passed').innerText = totalSKS;
|
||||
const ipk = globalAttempted > 0 ? (globalPoints / globalAttempted) : 0;
|
||||
|
||||
document.getElementById('total-sks-passed').innerText = globalPassed;
|
||||
document.getElementById('ipk-global').innerText = ipk.toFixed(2);
|
||||
document.getElementById('total-fail').innerText = failedSKS;
|
||||
document.getElementById('total-fail').innerText = globalFailedSKS;
|
||||
|
||||
let predikat = "Memuaskan";
|
||||
// NEW LOGIC
|
||||
const adaNilaiE = failedSKS > 0;
|
||||
const adaNilaiE = globalFailedSKS > 0;
|
||||
|
||||
if (ipk >= 3.51 && !adaNilaiE) {
|
||||
predikat = "Cum Laude";
|
||||
|
||||
Reference in New Issue
Block a user