Files
web-if-dev/simulasi-mk.html
2026-02-04 02:42:04 +00:00

890 lines
41 KiB
HTML

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simulasi KRS Dinamis - Kurikulum Informatika 2025</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
body { font-family: 'Inter', sans-serif; background-color: #f8fafc; }
/* Chart Sizing */
.chart-container { position: relative; width: 100%; height: 250px; }
/* Card Styles */
.course-card { transition: all 0.2s ease; cursor: grab; position: relative; overflow: hidden; user-select: none; }
.course-card:active { cursor: grabbing; }
.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; }
/* 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; }
/* 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; }
.grade-A { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; }
.grade-B { background: #dbeafe; color: #1e40af; border: 1px solid #bfdbfe; }
.grade-C { background: #fef9c3; color: #854d0e; border: 1px solid #fde047; }
.grade-D { background: #ffedd5; color: #9a3412; border: 1px solid #fed7aa; }
.grade-E { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; }
/* Semester Styles */
.semester-container { transition: all 0.3s; }
.semester-container.overload { border-color: #ef4444; background-color: #fff1f2; }
.semester-list { min-height: 120px; transition: background-color 0.2s; border-radius: 0.5rem; border: 2px dashed #e2e8f0; }
.semester-list.drag-over { background-color: #f0fdf4; border-color: #4ade80; }
/* Sidebar */
#catalog-sidebar { transition: transform 0.3s ease-in-out; }
#catalog-sidebar.closed { transform: translateX(-100%); }
.custom-scroll::-webkit-scrollbar { width: 6px; }
.custom-scroll::-webkit-scrollbar-track { background: #f1f5f9; }
.custom-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
/* Context Menu */
#context-menu {
display: none; position: absolute; z-index: 100;
background: white; border: 1px solid #e2e8f0;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
border-radius: 0.5rem; min-width: 180px; overflow: hidden;
}
#context-menu button { display: block; width: 100%; text-align: left; padding: 0.6rem 1rem; font-size: 0.875rem; color: #475569; transition: bg 0.1s; }
#context-menu button:hover { background-color: #f8fafc; color: #0f172a; }
#context-menu .menu-header { padding: 0.5rem 1rem; font-size: 0.7rem; font-weight: bold; color: #94a3b8; background: #f8fafc; letter-spacing: 0.05em; }
/* Retake Button Style */
#btn-retake { background-color: #eff6ff; color: #1d4ed8; font-weight: 600; border-bottom: 1px solid #dbeafe; }
#btn-retake:hover { background-color: #dbeafe; }
.cat-bar { position: absolute; left: 0; top: 0; bottom: 0; width: 6px; }
/* Toast Notification */
#toast {
visibility: hidden; min-width: 250px; background-color: #333; color: #fff; text-align: center; border-radius: 8px; padding: 12px;
position: fixed; z-index: 100; left: 50%; bottom: 30px; transform: translateX(-50%); font-size: 14px; opacity: 0; transition: opacity 0.3s;
}
#toast.show { visibility: visible; opacity: 1; }
/* Layout Fixes */
.main-container {
width: 100%;
max-width: 1280px;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
</style>
</head>
<body class="bg-slate-50 text-slate-800 overflow-x-hidden">
<!-- Catalog Sidebar -->
<div id="catalog-sidebar" class="fixed top-0 left-0 h-full w-80 bg-white shadow-xl z-50 border-r border-slate-200 flex flex-col closed">
<div class="p-4 border-b border-slate-200 bg-slate-50 flex justify-between items-center">
<div>
<h2 class="font-bold text-slate-800">Katalog Mata Kuliah</h2>
<p class="text-xs text-slate-500">Tarik ke semester untuk mengambil.</p>
</div>
<button onclick="toggleCatalog()" class="text-slate-400 hover:text-slate-600"><i class="fas fa-times"></i></button>
</div>
<div class="p-2 border-b border-slate-100">
<input type="text" id="search-catalog" placeholder="Cari mata kuliah..." class="w-full text-sm p-2 border rounded bg-slate-50 focus:outline-none focus:border-blue-400" onkeyup="filterCatalog()">
</div>
<div id="catalog-list" class="flex-1 overflow-y-auto p-3 space-y-2 custom-scroll"></div>
</div>
<!-- Toggle Button (Floating Left) -->
<button onclick="toggleCatalog()" class="fixed left-0 top-24 bg-blue-600 text-white p-3 rounded-r-lg shadow-lg z-40 hover:bg-blue-700 transition-all flex items-center gap-2 font-semibold text-sm group">
<i class="fas fa-book-open"></i> <span class="hidden group-hover:inline">Katalog</span>
</button>
<!-- Toast -->
<div id="toast">Notifikasi</div>
<!-- Context Menu -->
<div id="context-menu">
<div class="menu-header">OPSI AKADEMIK</div>
<!-- Retake Button (Hidden by default) -->
<button id="btn-retake" onclick="retakeCourse()" style="display:none;">
<i class="fas fa-redo-alt mr-2"></i> Ulang di Tahun Depan
</button>
<div class="menu-header mt-1">SET NILAI (SIMULASI)</div>
<button onclick="setCourseGrade(4)"><span class="w-6 inline-block text-center font-bold text-green-600">A</span> (4.0) - Sangat Baik</button>
<button onclick="setCourseGrade(3)"><span class="w-6 inline-block text-center font-bold text-blue-600">B</span> (3.0) - Baik</button>
<button onclick="setCourseGrade(2)"><span class="w-6 inline-block text-center font-bold text-yellow-600">C</span> (2.0) - Cukup</button>
<button onclick="setCourseGrade(1)"><span class="w-6 inline-block text-center font-bold text-orange-600">D</span> (1.0) - Kurang</button>
<button onclick="setCourseGrade(0)" class="text-red-600 bg-red-50 hover:bg-red-100"><span class="w-6 inline-block text-center font-bold">E</span> (0.0) - Gagal/Ulang</button>
<div class="border-t border-slate-100 my-1"></div>
<button onclick="deleteCourse()" class="text-slate-500 hover:text-red-600"><i class="fas fa-trash w-6 mr-2"></i> Hapus dari KRS</button>
</div>
<!-- Main Content -->
<div class="main-container py-8 transition-all duration-300">
<header class="mb-8 text-center">
<h1 class="text-3xl md:text-4xl font-bold text-slate-900 mb-2">Simulasi Studi Berbasis IPS - Kurikulum Informatika 2025</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>
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">
<div class="flex items-center gap-2"><span class="w-3 h-3 bg-pink-400 rounded-sm"></span> MKWU</div>
<div class="flex items-center gap-2"><span class="w-3 h-3 bg-green-500 rounded-sm"></span> Science</div>
<div class="flex items-center gap-2"><span class="w-3 h-3 bg-blue-400 rounded-sm"></span> Fund</div>
<div class="flex items-center gap-2"><span class="w-3 h-3 bg-orange-400 rounded-sm"></span> Core</div>
<div class="flex items-center gap-2"><span class="w-3 h-3 bg-purple-400 rounded-sm"></span> Adv</div>
<div class="flex items-center gap-2"><span class="w-3 h-3 bg-yellow-400 rounded-sm"></span> Pilihan</div>
</div>
<!-- Global Stats -->
<div class="mt-6 grid grid-cols-2 md:grid-cols-4 gap-4 max-w-4xl mx-auto">
<div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
<div class="text-3xl font-bold text-slate-800" id="total-sks-passed">0</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider mt-1">Total SKS Lulus</div>
</div>
<div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
<div class="text-3xl font-bold text-blue-600" id="ipk-global">0.00</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider mt-1">IPK Kumulatif</div>
</div>
<div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
<div class="text-2xl font-bold text-slate-800" id="predikat-ipk">-</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider mt-1">Predikat</div>
</div>
<div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
<div class="text-3xl font-bold text-red-500" id="total-fail">0</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider mt-1">SKS Gagal (E)</div>
</div>
</div>
</header>
<!-- Track Selector -->
<div class="flex flex-wrap justify-center mb-10 gap-2">
<button onclick="switchTrack('general')" class="track-btn active px-4 py-2 rounded-full text-xs font-bold border transition-colors bg-slate-800 text-white" id="btn-general">Umum (Campuran)</button>
<button onclick="switchTrack('ai')" class="track-btn px-4 py-2 rounded-full text-xs font-bold border transition-colors bg-white hover:bg-slate-50" id="btn-ai">AI & Data</button>
<button onclick="switchTrack('rpl')" class="track-btn px-4 py-2 rounded-full text-xs font-bold border transition-colors bg-white hover:bg-slate-50" id="btn-rpl">RPL</button>
<button onclick="switchTrack('net')" class="track-btn px-4 py-2 rounded-full text-xs font-bold border transition-colors bg-white hover:bg-slate-50" id="btn-net">Jaringan</button>
<button onclick="switchTrack('si')" class="track-btn px-4 py-2 rounded-full text-xs font-bold border transition-colors bg-white hover:bg-slate-50" id="btn-si">SI & GIS</button>
</div>
<!-- Semesters Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8 mb-12" id="semester-grid">
<!-- Semesters 1-8 will be injected here -->
</div>
<!-- Charts -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12 bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<div>
<h3 class="text-sm font-bold text-slate-800 mb-4 border-b pb-2">Tren IPS (Indeks Prestasi Semester)</h3>
<div class="chart-container">
<canvas id="ipsChart"></canvas>
</div>
</div>
<div>
<h3 class="text-sm font-bold text-slate-800 mb-4 border-b pb-2">Komposisi Bidang Ilmu</h3>
<div class="chart-container">
<canvas id="domainChart"></canvas>
</div>
</div>
</section>
</div>
<script>
// --- UTILS: Fix for HTTP Environments ---
function generateUUID() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// --- Data Definitions ---
const categories = {
MKWU: { color: 'bg-pink-400' },
BSC: { color: 'bg-green-500' },
FND: { color: 'bg-blue-400' },
CORE: { color: 'bg-orange-400' },
ADV: { color: 'bg-purple-400' },
PROF: { color: 'bg-slate-400' },
CAP: { color: 'bg-fuchsia-600' },
EL: { color: 'bg-yellow-400' },
RES: { color: 'bg-red-500' }
};
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 = {
1: [
{ id: "M1", name: "Pend. Pancasila", sks: 2, cat: "MKWU" },
{ id: "M2", name: "B. Inggris", sks: 2, cat: "MKWU" },
{ id: "M3", name: "Pend. Agama", sks: 3, cat: "MKWU" },
{ id: "M4", name: "Kalkulus", sks: 3, cat: "BSC" },
{ id: "M5", name: "Matematika Diskrit", sks: 3, cat: "BSC" },
{ id: "M6", name: "Pengantar Informatika", sks: 2, cat: "FND" },
{ id: "M7", name: "Logika Komputasional", sks: 2, cat: "FND" },
{ id: "M8", name: "Kom. Profesional", sks: 2, cat: "PROF" }
],
2: [
{ id: "M9", name: "Kewarganegaraan", sks: 2, cat: "MKWU" },
{ id: "M10", name: "B. Indonesia", sks: 2, cat: "MKWU" },
{ id: "M11", name: "Aljabar Linier", sks: 3, cat: "BSC" },
{ id: "M12", name: "Sistem Digital", sks: 2, cat: "FND" },
{ id: "M13", name: "Sistem Operasi", sks: 3, cat: "FND" },
{ id: "M14", name: "Dasar Pemrograman", sks: 3, cat: "FND" },
{ id: "M15", name: "Org. Komputer", sks: 3, cat: "FND" },
{ id: "M16", name: "Literasi Digital", sks: 2, cat: "PROF" }
],
3: [
{ id: "M17", name: "Metode Numerik", sks: 3, cat: "BSC" },
{ id: "M18", name: "Basis Data", sks: 3, cat: "FND" },
{ id: "M19", name: "Struktur Data", sks: 3, cat: "FND" },
{ id: "M20", name: "Jaringan Komputer", sks: 3, cat: "FND" },
{ id: "M21", name: "Sistem Informasi", sks: 2, cat: "CORE" },
{ id: "M22", name: "RPL", sks: 3, cat: "CORE" },
{ id: "M23", name: "Etika Komputasi", sks: 2, cat: "PROF" }
],
4: [
{ id: "M24", name: "Probstat", sks: 3, cat: "BSC" },
{ id: "M25", name: "PBO", sks: 3, cat: "FND" },
{ id: "M26", name: "Analsis SI", sks: 3, cat: "CORE" },
{ id: "M27", name: "Strategi Algoritma", sks: 3, cat: "CORE" },
{ id: "M28", name: "Pemrog. Jaringan", sks: 3, cat: "CORE" },
{ id: "M29", name: "Sis. Terdistribusi", sks: 2, cat: "CORE" },
{ id: "M30", name: "IMK", sks: 2, cat: "CORE" }
],
5: [
{ id: "M31", name: "Manajemen Jaringan", sks: 3, cat: "CORE" },
{ id: "M32", name: "Pemrog. Web", sks: 3, cat: "CORE" },
{ id: "M33", name: "Machine Learning", sks: 3, cat: "ADV" },
{ id: "M34", name: "IoT", sks: 3, cat: "ADV" },
{ id: "M35", name: "Kecerdasan Artifisial", sks: 3, cat: "ADV" },
{ id: "M36", name: "Sistem Enterprise", sks: 3, cat: "ADV" },
{ id: "M37", name: "Metpen", sks: 2, cat: "RES" }
],
6: [
{ id: "M38", name: "Keamanan Info", sks: 3, cat: "CORE" },
{ id: "M39", name: "Computer Vision", sks: 3, cat: "ADV" },
{ id: "M40", name: "NLP", sks: 3, cat: "ADV" },
{ id: "M41", name: "GIS", sks: 3, cat: "ADV" },
{ id: "M42", name: "Proyek PL", sks: 3, cat: "CAP" },
{ id: "M43", name: "Kerja Praktek", sks: 2, cat: "RES" }
],
7: [
{ id: "M44", name: "Testing", sks: 3, cat: "ADV" },
{ id: "M45", name: "Teknopreneur", sks: 3, cat: "PROF" },
{ id: "M46", name: "PMKM", sks: 2, cat: "PROF" },
{ id: "M47", name: "Proposal Skripsi", sks: 2, cat: "RES" }
],
8: [
{ id: "M48", name: "Skripsi", sks: 4, cat: "RES" }
]
};
const electives = {
general: [
{ 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" },
{ id: "AI2", name: "Robotics", sks: 3, cat: "EL" },
{ id: "AI3", name: "Big Data", sks: 3, cat: "EL" },
{ id: "AI4", name: "Info Retrieval", sks: 3, cat: "EL" },
{ id: "AI5", name: "AI Ethics", sks: 3, cat: "EL" }
],
rpl: [
{ id: "SE1", name: "Backend Dev", sks: 3, cat: "EL" },
{ id: "SE2", name: "Mobile Prog", sks: 3, cat: "EL" },
{ id: "SE3", name: "Soft. Arch", sks: 3, cat: "EL" },
{ id: "SE4", name: "DevOps", sks: 3, cat: "EL" },
{ id: "SE5", name: "Game Dev", sks: 3, cat: "EL" }
],
net: [
{ id: "NET1", name: "Net Security", sks: 3, cat: "EL" },
{ id: "NET2", name: "Wireless", sks: 3, cat: "EL" },
{ id: "NET3", name: "Cloud Comp", sks: 3, cat: "EL" },
{ id: "NET4", name: "Blockchain", sks: 3, cat: "EL" },
{ id: "NET5", name: "Cyber Forensics", sks: 3, cat: "EL" }
],
si: [
{ id: "SI1", name: "Business Intel.", sks: 3, cat: "EL" },
{ id: "SI2", name: "E-Business", sks: 3, cat: "EL" },
{ id: "SI3", name: "Ent. Architecture", sks: 3, cat: "EL" },
{ id: "SI4", name: "IT Governance", sks: 3, cat: "EL" },
{ id: "SI5", name: "Smart City", sks: 3, cat: "EL" }
]
};
// --- State Management ---
let currentPlan = {};
let currentTrackId = 'general';
let contextTarget = null;
// --- Core Logic ---
function initPlan(trackId) {
currentTrackId = trackId;
currentPlan = {};
for(let i=1; i<=8; i++) {
currentPlan[i] = [];
if(baseCurriculum[i]) {
currentPlan[i] = baseCurriculum[i].map(c => ({...c, grade: 4, uuid: generateUUID()}));
}
}
const tr = electives[trackId] || electives['general'];
const distribute = [[5,0], [6,1], [7,2], [7,3], [8,4]];
distribute.forEach(([sem, idx]) => {
if(tr[idx]) {
currentPlan[sem].push({
...tr[idx],
grade: 4,
uuid: generateUUID(),
isElective: true
});
}
});
renderCatalog();
renderAll();
}
function checkPrerequisite(courseId, targetSem) {
const rule = prerequisites[courseId];
if (!rule) return { allowed: true };
let passed = false;
for (let i = 1; i < targetSem; i++) {
if (!currentPlan[i]) continue;
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 };
}
// --- Fixed Stat Calculations ---
function calculateSemStats(semId) {
const courses = currentPlan[semId] || [];
let sksAttempted = 0; // Divisor IPS (Termasuk E)
let sksPassed = 0; // Total SKS Lulus (Grade > 0)
let totalPoints = 0; // Numerator IPS
courses.forEach(c => {
sksAttempted += c.sks;
totalPoints += (c.sks * c.grade); // Grade 0 * sks = 0
if (c.grade > 0) {
sksPassed += c.sks;
}
});
// IPS = Total Points / SKS Attempted
const ips = sksAttempted > 0 ? (totalPoints / sksAttempted) : 0;
return { ips, sksAttempted, sksPassed, totalPoints };
}
function getMaxSKS(prevIPS) {
if (prevIPS >= 3.00) return 24;
if (prevIPS >= 2.50) return 22;
if (prevIPS >= 2.00) return 20;
return 18;
}
function renderAll() {
const grid = document.getElementById('semester-grid');
if (!grid) return;
grid.innerHTML = '';
// First Pass: Check Violations
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++) {
let maxSKS = 20;
let prevIPS = 0;
if (i > 1) {
const prevStats = calculateSemStats(i-1);
prevIPS = prevStats.ips;
maxSKS = getMaxSKS(prevIPS);
}
const currentStats = calculateSemStats(i);
// Use sksAttempted for load display
const currentLoad = currentStats.sksAttempted;
const isOverload = currentLoad > maxSKS;
const semDiv = document.createElement('div');
semDiv.className = `semester-container bg-white rounded-xl shadow-sm border ${isOverload ? 'overload' : 'border-slate-200'} overflow-hidden flex flex-col h-full`;
let headerContent = `
<div class="flex justify-between items-center mb-1">
<h3 class="font-bold text-slate-700">Semester ${i}</h3>
<span class="text-xs font-bold ${isOverload ? 'text-red-600' : 'text-slate-500'}">
${currentLoad} / ${maxSKS} SKS
</span>
</div>
`;
if (i > 1) {
headerContent += `
<div class="flex justify-between text-[10px] text-slate-400 border-t border-slate-100 pt-1 mt-1">
<span>IPS Lalu: <strong class="${prevIPS < 2 ? 'text-red-500' : 'text-slate-600'}">${prevIPS.toFixed(2)}</strong></span>
<span>Jatah: <strong>${maxSKS}</strong></span>
</div>
`;
} else {
headerContent += `
<div class="flex justify-between text-[10px] text-slate-400 border-t border-slate-100 pt-1 mt-1">
<span>Mahasiswa Baru</span>
<span>Paket Awal</span>
</div>
`;
}
if (isOverload) {
headerContent += `<div class="mt-1 text-[10px] text-red-600 font-bold bg-red-100 px-2 py-0.5 rounded text-center animate-pulse">OVERLOAD! Kurangi mata kuliah.</div>`;
}
semDiv.innerHTML = `
<div class="bg-slate-50 px-4 py-3 border-b ${isOverload ? 'border-red-200 bg-red-50' : 'border-slate-200'}">
${headerContent}
</div>
<div class="semester-list p-3 space-y-2 flex-grow" ondrop="drop(event, ${i})" ondragover="allowDrop(event)">
</div>
<div class="px-4 py-2 bg-slate-50 border-t ${isOverload ? 'border-red-200' : 'border-slate-100'} text-right">
<span class="text-[10px] uppercase font-bold text-slate-400">IPS: ${currentStats.ips.toFixed(2)}</span>
</div>
`;
const listContainer = semDiv.querySelector('.semester-list');
currentPlan[i].forEach((c, idx) => {
listContainer.appendChild(createCard(c, i, idx));
});
grid.appendChild(semDiv);
}
updateGlobalStats();
}
function createCard(c, semId, index) {
const card = document.createElement('div');
const catData = categories[c.cat] || { color: 'bg-gray-400' };
const isFailed = c.grade === 0;
const isRetake = c.isRetake === true;
const isViolation = c.violation === true;
const isElective = c.isElective === true;
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' : ''}
${isElective ? 'is-elective' : ''}`;
card.draggable = true;
card.setAttribute('ondragstart', `dragSemester(event, ${semId}, ${index})`);
card.oncontextmenu = (e) => { e.preventDefault(); showContextMenu(e, semId, index); };
let gradeClass = 'grade-A';
let gradeLabel = 'A';
if(c.grade === 3) { gradeClass = 'grade-B'; gradeLabel = 'B'; }
if(c.grade === 2) { gradeClass = 'grade-C'; gradeLabel = 'C'; }
if(c.grade === 1) { gradeClass = 'grade-D'; gradeLabel = 'D'; }
if(c.grade === 0) { gradeClass = 'grade-E'; gradeLabel = 'E'; }
card.innerHTML = `
<div class="cat-bar ${catData.color}"></div>
<div class="pr-6">
<div class="text-xs font-bold text-slate-700 leading-tight mb-1 flex items-center">
${c.name}
${isRetake ? '<i class="fas fa-redo-alt text-[10px] text-blue-500 ml-2" title="Mengulang"></i>' : ''}
${isElective ? '<i class="fas fa-star text-[10px] text-yellow-500 ml-2" title="MK Peminatan"></i>' : ''}
</div>
<div class="flex items-center gap-2">
<span class="text-[10px] text-slate-500 bg-slate-100 px-1 rounded border">${c.sks} SKS</span>
</div>
</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;
}
// --- 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];
}
// --- 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 {
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 ---
function showContextMenu(e, semId, index) {
contextTarget = { semId, index };
const menu = document.getElementById('context-menu');
const course = currentPlan[semId][index];
const btnRetake = document.getElementById('btn-retake');
if (course.grade === 0) {
btnRetake.style.display = 'block';
} else {
btnRetake.style.display = 'none';
}
menu.style.display = 'block';
let x = e.pageX;
let y = e.pageY;
if(x + 200 > window.innerWidth) x = window.innerWidth - 210;
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
}
function hideContextMenu() {
document.getElementById('context-menu').style.display = 'none';
}
function setCourseGrade(grade) {
if(contextTarget) {
const { semId, index } = contextTarget;
currentPlan[semId][index].grade = grade;
if(grade === 0) {
showToast("Mata kuliah Gagal (E). Klik kanan > 'Ulang di Tahun Depan' untuk memperbaiki.");
}
renderAll();
}
hideContextMenu();
}
function retakeCourse() {
if(contextTarget) {
const { semId, index } = contextTarget;
const course = currentPlan[semId][index];
const targetSem = parseInt(semId) + 2;
if (targetSem > 8) {
showToast("Maaf, semester tujuan ("+targetSem+") di luar jangkauan simulasi (Max Sem 8).");
} else {
const exists = currentPlan[targetSem].some(c => c.id === course.id);
if (exists) {
showToast("Mata kuliah ini sudah ada di Semester " + targetSem + ".");
} else {
const check = checkPrerequisite(course.id, targetSem);
if (!check.allowed) {
showToast("❌ Tidak bisa mengulang: " + check.msg);
} else {
const newCourse = {
...course,
grade: 4,
uuid: generateUUID(),
isRetake: true
};
currentPlan[targetSem].push(newCourse);
showToast(`Mata kuliah ditambahkan ke Semester ${targetSem}.`);
renderAll();
}
}
}
}
hideContextMenu();
}
function deleteCourse() {
if(contextTarget) {
const { semId, index } = contextTarget;
currentPlan[semId].splice(index, 1);
renderAll();
}
hideContextMenu();
}
function showToast(msg) {
const el = document.getElementById("toast");
el.innerText = msg;
el.className = "show";
setTimeout(function(){ el.className = el.className.replace("show", ""); }, 4000);
}
document.addEventListener('click', (e) => {
if(!e.target.closest('#context-menu') && !e.target.closest('.course-card')) {
hideContextMenu();
}
});
// --- Sidebar & Catalog ---
function toggleCatalog() { document.getElementById('catalog-sidebar').classList.toggle('closed'); }
function renderCatalog() {
const list = document.getElementById('catalog-list');
list.innerHTML = '';
let allCourses = [];
// Base Courses
for(let i=1; i<=8; i++) if(baseCurriculum[i]) allCourses.push(...baseCurriculum[i]);
// Logic Baru: Jika Track == 'general', TAMPILKAN SEMUA PILIHAN
if(currentTrackId === 'general') {
allCourses.push(...electives['ai']);
allCourses.push(...electives['rpl']);
allCourses.push(...electives['net']);
allCourses.push(...electives['si']);
} else {
allCourses.push(...(electives[currentTrackId] || []));
}
const unique = [];
const seen = new Set();
allCourses.forEach(c => { if(!seen.has(c.id)){ seen.add(c.id); unique.push(c); } });
unique.forEach(c => {
const el = document.createElement('div');
const catData = categories[c.cat] || {color: 'bg-gray-400'};
el.className = 'bg-white border border-slate-200 p-2 rounded cursor-grab hover:bg-slate-50 flex items-center justify-between text-sm shadow-sm transition-transform hover:scale-[1.02]';
el.draggable = true;
el.setAttribute('ondragstart', `dragCatalog(event, '${c.id}', '${c.name}', ${c.sks}, '${c.cat}')`);
el.innerHTML = `
<div class="flex items-center gap-2">
<div class="w-1.5 h-8 ${catData.color} rounded-sm"></div>
<div>
<div class="font-bold text-slate-700 text-xs">${c.name}</div>
<div class="text-[10px] text-slate-400">${c.cat} | ${c.sks} SKS</div>
</div>
</div>
<i class="fas fa-plus text-slate-300"></i>
`;
list.appendChild(el);
});
}
function filterCatalog() {
const term = document.getElementById('search-catalog').value.toLowerCase();
document.querySelectorAll('#catalog-list > div').forEach(item => {
item.style.display = item.innerText.toLowerCase().includes(term) ? 'flex' : 'none';
});
}
function switchTrack(trackId) {
document.querySelectorAll('.track-btn').forEach(b => {
b.classList.remove('bg-slate-800', 'text-white');
b.classList.add('bg-white', 'text-slate-800');
});
const btn = document.getElementById(`btn-${trackId}`);
btn.classList.remove('bg-white', 'text-slate-800');
btn.classList.add('bg-slate-800', 'text-white');
// Notification
let trackName = "Umum";
if(trackId === 'ai') trackName = "AI & Data";
if(trackId === 'rpl') trackName = "RPL";
if(trackId === 'net') trackName = "Jaringan";
if(trackId === 'si') trackName = "SI & GIS";
showToast(`Jalur diubah ke ${trackName}.`);
initPlan(trackId);
}
// --- Analytics ---
let ipsChart = null;
let domainChart = null;
function updateGlobalStats() {
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 };
for(let i=1; i<=8; i++) {
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) {
const cat = c.cat === 'CAP' || c.cat === 'RES' ? 'PROF' : c.cat;
if(catCounts[cat] !== undefined) catCounts[cat] += c.sks;
}
});
}
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 = globalFailedSKS;
let predikat = "Memuaskan";
const adaNilaiE = globalFailedSKS > 0;
if (ipk >= 3.51 && !adaNilaiE) {
predikat = "Cum Laude";
} else if (ipk >= 3.01) {
predikat = "Sangat Memuaskan";
} else if (ipk < 2.00) {
predikat = "Nasakom";
}
document.getElementById('predikat-ipk').innerText = predikat;
// Chart 1: IPS Trend
const ctx1 = document.getElementById('ipsChart').getContext('2d');
if(ipsChart) ipsChart.destroy();
ipsChart = new Chart(ctx1, {
type: 'line',
data: {
labels: ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8'],
datasets: [{
label: 'IPS',
data: semIPS,
borderColor: '#2563eb',
backgroundColor: 'rgba(37, 99, 235, 0.1)',
fill: true,
tension: 0.3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { min: 0, max: 4 } },
plugins: { legend: { display: false } }
}
});
// Chart 2: Domain
const ctx2 = document.getElementById('domainChart').getContext('2d');
if(domainChart) domainChart.destroy();
domainChart = new Chart(ctx2, {
type: 'doughnut',
data: {
labels: Object.keys(catCounts),
datasets: [{
data: Object.values(catCounts),
backgroundColor: ['#f472b6', '#22c55e', '#60a5fa', '#fb923c', '#c084fc', '#94a3b8', '#facc15'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'right', labels: { boxWidth: 10, font: {size: 9} } } }
}
});
}
document.addEventListener('DOMContentLoaded', () => {
initPlan('general');
});
</script>
</body>
</html>