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; }
|
.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;
|
||||||
if(c.grade > 0) {
|
totalPoints += (c.sks * c.grade); // Grade 0 * sks = 0
|
||||||
totalSKS += c.sks;
|
if (c.grade > 0) {
|
||||||
totalPoints += (c.sks * c.grade);
|
sksPassed += c.sks;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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";
|
||||||
|
|||||||
Reference in New Issue
Block a user