Update simulasi-mk.html

versi 7
This commit is contained in:
izu
2026-02-04 02:40:48 +00:00
parent caeb71803c
commit 258ecec880

View File

@@ -25,8 +25,8 @@
.course-card.violation .cat-bar { background-color: #ef4444 !important; } .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; } .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) */ /* Elective Styles */
.course-card.is-elective { background-color: #fffbeb; border-color: #fcd34d; } /* Yellow tint */ .course-card.is-elective { background-color: #fffbeb; border-color: #fcd34d; }
/* 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; }
@@ -134,7 +134,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>
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> </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">
@@ -200,7 +200,7 @@
</div> </div>
<script> <script>
// --- UTILS: Fix for HTTP Environments (Coolify) --- // --- UTILS: Fix for HTTP Environments ---
function generateUUID() { function generateUUID() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) { if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID(); return crypto.randomUUID();
@@ -224,7 +224,6 @@
RES: { color: 'bg-red-500' } RES: { color: 'bg-red-500' }
}; };
// --- Prerequisite Rules ---
const prerequisites = { const prerequisites = {
"M14": { reqId: "M7", reqName: "Logika Komputasional" }, "M14": { reqId: "M7", reqName: "Logika Komputasional" },
"M19": { reqId: "M14", reqName: "Dasar Pemrograman" }, "M19": { reqId: "M14", reqName: "Dasar Pemrograman" },
@@ -300,13 +299,12 @@
}; };
const electives = { const electives = {
// General track now has specific courses as a "Sample Mix"
general: [ general: [
{ id: "GEN1", name: "Cross-Platform Dev", sks: 3, cat: "EL" }, // RPL flavor { id: "GEN1", name: "Cross-Platform Dev", sks: 3, cat: "EL" },
{ id: "GEN2", name: "Data Mining", sks: 3, cat: "EL" }, // AI flavor { id: "GEN2", name: "Data Mining", sks: 3, cat: "EL" },
{ id: "GEN3", name: "Cloud Computing", sks: 3, cat: "EL" }, // Net flavor { id: "GEN3", name: "Cloud Computing", sks: 3, cat: "EL" },
{ id: "GEN4", name: "E-Business", sks: 3, cat: "EL" }, // SI flavor { id: "GEN4", name: "E-Business", sks: 3, cat: "EL" },
{ id: "GEN5", name: "Blockchain Tech", sks: 3, cat: "EL" } // Advanced { id: "GEN5", name: "Blockchain Tech", sks: 3, cat: "EL" }
], ],
ai: [ ai: [
{ id: "AI1", name: "Deep Learning", sks: 3, cat: "EL" }, { id: "AI1", name: "Deep Learning", sks: 3, cat: "EL" },
@@ -349,7 +347,6 @@
currentTrackId = trackId; currentTrackId = trackId;
currentPlan = {}; currentPlan = {};
// Populate Plan
for(let i=1; i<=8; i++) { for(let i=1; i<=8; i++) {
currentPlan[i] = []; currentPlan[i] = [];
if(baseCurriculum[i]) { if(baseCurriculum[i]) {
@@ -357,7 +354,6 @@
} }
} }
// Add Electives with Flag
const tr = electives[trackId] || electives['general']; const tr = electives[trackId] || electives['general'];
const distribute = [[5,0], [6,1], [7,2], [7,3], [8,4]]; const distribute = [[5,0], [6,1], [7,2], [7,3], [8,4]];
distribute.forEach(([sem, idx]) => { distribute.forEach(([sem, idx]) => {
@@ -366,7 +362,7 @@
...tr[idx], ...tr[idx],
grade: 4, grade: 4,
uuid: generateUUID(), uuid: generateUUID(),
isElective: true // Tag as elective isElective: true
}); });
} }
}); });
@@ -375,16 +371,13 @@
renderAll(); renderAll();
} }
// --- Prerequisite Check Function ---
function checkPrerequisite(courseId, targetSem) { function checkPrerequisite(courseId, targetSem) {
const rule = prerequisites[courseId]; const rule = prerequisites[courseId];
if (!rule) return { allowed: true }; if (!rule) return { allowed: true };
let passed = false; let passed = false;
// Scan only semesters BEFORE the target semester
for (let i = 1; i < targetSem; i++) { for (let i = 1; i < targetSem; i++) {
if (!currentPlan[i]) continue; 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); const found = currentPlan[i].find(c => c.id === rule.reqId && c.grade > 0);
if (found) { if (found) {
passed = true; passed = true;
@@ -398,22 +391,25 @@
return { allowed: true }; return { allowed: true };
} }
// --- Fixed Stat Calculations ---
function calculateSemStats(semId) { function calculateSemStats(semId) {
const courses = currentPlan[semId] || []; const courses = currentPlan[semId] || [];
let totalSKS = 0; let sksAttempted = 0; // Divisor IPS (Termasuk E)
let totalPoints = 0; let sksPassed = 0; // Total SKS Lulus (Grade > 0)
let loadSKS = 0; let totalPoints = 0; // Numerator IPS
courses.forEach(c => { courses.forEach(c => {
loadSKS += c.sks; sksAttempted += c.sks;
totalPoints += (c.sks * c.grade); // Grade 0 * sks = 0
if (c.grade > 0) { if (c.grade > 0) {
totalSKS += c.sks; sksPassed += c.sks;
totalPoints += (c.sks * c.grade);
} }
}); });
const ips = totalSKS > 0 ? (totalPoints / totalSKS) : 0; // IPS = Total Points / SKS Attempted
return { ips, loadSKS }; const ips = sksAttempted > 0 ? (totalPoints / sksAttempted) : 0;
return { ips, sksAttempted, sksPassed, totalPoints };
} }
function getMaxSKS(prevIPS) { function getMaxSKS(prevIPS) {
@@ -428,7 +424,7 @@
if (!grid) return; if (!grid) return;
grid.innerHTML = ''; grid.innerHTML = '';
// First Pass: Check Violations (Prerequisites) // First Pass: Check Violations
for(let i=1; i<=8; i++) { for(let i=1; i<=8; i++) {
currentPlan[i].forEach(course => { currentPlan[i].forEach(course => {
const check = checkPrerequisite(course.id, i); const check = checkPrerequisite(course.id, i);
@@ -448,7 +444,8 @@
} }
const currentStats = calculateSemStats(i); const currentStats = calculateSemStats(i);
const currentLoad = currentStats.loadSKS; // Use sksAttempted for load display
const currentLoad = currentStats.sksAttempted;
const isOverload = currentLoad > maxSKS; const isOverload = currentLoad > maxSKS;
const semDiv = document.createElement('div'); const semDiv = document.createElement('div');
@@ -576,12 +573,38 @@
courseToAdd = currentPlan[dragSource.semId][dragSource.index]; courseToAdd = currentPlan[dragSource.semId][dragSource.index];
} }
// --- 1. Prerequisite Check ---
const check = checkPrerequisite(courseToAdd.id, targetSem); const check = checkPrerequisite(courseToAdd.id, targetSem);
if (!check.allowed) { if (!check.allowed) {
showToast("❌ " + check.msg); showToast("❌ " + check.msg);
return; 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') { if(dragSource.type === 'catalog') {
currentPlan[targetSem].push(dragSource.course); currentPlan[targetSem].push(dragSource.course);
} else { } else {
@@ -768,9 +791,11 @@
let domainChart = null; let domainChart = null;
function updateGlobalStats() { function updateGlobalStats() {
let totalSKS = 0; let globalAttempted = 0;
let totalPoints = 0; let globalPassed = 0;
let failedSKS = 0; let globalPoints = 0;
let globalFailedSKS = 0;
let semIPS = []; let semIPS = [];
let catCounts = { MKWU:0, BSC:0, FND:0, CORE:0, ADV:0, PROF:0, EL:0 }; let catCounts = { MKWU:0, BSC:0, FND:0, CORE:0, ADV:0, PROF:0, EL:0 };
@@ -778,26 +803,28 @@
const stats = calculateSemStats(i); const stats = calculateSemStats(i);
semIPS.push(stats.ips); semIPS.push(stats.ips);
globalAttempted += stats.sksAttempted;
globalPassed += stats.sksPassed;
globalPoints += stats.totalPoints;
currentPlan[i].forEach(c => { currentPlan[i].forEach(c => {
if (c.grade === 0) globalFailedSKS += c.sks;
// Count categories for passed/active courses
if (c.grade > 0) { if (c.grade > 0) {
totalSKS += c.sks;
totalPoints += (c.sks * c.grade);
const cat = c.cat === 'CAP' || c.cat === 'RES' ? 'PROF' : c.cat; const cat = c.cat === 'CAP' || c.cat === 'RES' ? 'PROF' : c.cat;
if(catCounts[cat] !== undefined) catCounts[cat] += c.sks; if(catCounts[cat] !== undefined) catCounts[cat] += c.sks;
} else {
failedSKS += c.sks;
} }
}); });
} }
const ipk = totalSKS > 0 ? (totalPoints / totalSKS) : 0; const ipk = globalAttempted > 0 ? (globalPoints / globalAttempted) : 0;
document.getElementById('total-sks-passed').innerText = totalSKS;
document.getElementById('total-sks-passed').innerText = globalPassed;
document.getElementById('ipk-global').innerText = ipk.toFixed(2); document.getElementById('ipk-global').innerText = ipk.toFixed(2);
document.getElementById('total-fail').innerText = failedSKS; document.getElementById('total-fail').innerText = globalFailedSKS;
let predikat = "Memuaskan"; let predikat = "Memuaskan";
// NEW LOGIC const adaNilaiE = globalFailedSKS > 0;
const adaNilaiE = failedSKS > 0;
if (ipk >= 3.51 && !adaNilaiE) { if (ipk >= 3.51 && !adaNilaiE) {
predikat = "Cum Laude"; predikat = "Cum Laude";