Add simulasi-mk.html
This commit is contained in:
651
simulasi-mk.html
Normal file
651
simulasi-mk.html
Normal file
@@ -0,0 +1,651 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Simulasi KRS & Student Journey</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.chart-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 300px;
|
||||
max-height: 400px;
|
||||
}
|
||||
@media (min-width: 768px) { .chart-container { height: 350px; } }
|
||||
.course-card { transition: all 0.2s ease; cursor: grab; user-select: none; }
|
||||
.course-card:active { cursor: grabbing; }
|
||||
.course-card:hover { transform: translateY(-2px); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
|
||||
.course-card.dragging { opacity: 0.5; border: 2px dashed #a8a29e; background-color: #f5f5f4; }
|
||||
|
||||
/* Drop Zone Styles */
|
||||
.semester-list { min-height: 120px; transition: background-color 0.2s; }
|
||||
.semester-list.drag-over { background-color: #f0fdf4; border: 2px dashed #4ade80; }
|
||||
|
||||
/* Warning States */
|
||||
.semester-container { transition: all 0.3s; }
|
||||
.semester-container.overload { border-color: #ef4444; background-color: #fef2f2; }
|
||||
.semester-container.overload .header-bg { background-color: #fee2e2; border-bottom-color: #fca5a5; }
|
||||
.semester-container.overload .sks-badge { background-color: #ef4444; color: white; border-color: #ef4444; }
|
||||
|
||||
.track-btn.active { background-color: #44403c; color: white; border-color: #44403c; }
|
||||
.track-btn { background-color: #f5f5f4; color: #78716c; border: 1px solid #e7e5e4; }
|
||||
|
||||
.fade-in { animation: fadeIn 0.5s ease-in-out; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
/* Range Slider Styling */
|
||||
input[type=range] {
|
||||
-webkit-appearance: none; width: 100%; background: transparent;
|
||||
}
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; height: 16px; width: 16px; border-radius: 50%; background: #44403c; cursor: pointer; margin-top: -6px;
|
||||
}
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
width: 100%; height: 4px; cursor: pointer; background: #d6d3d1; border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-stone-50 text-stone-800">
|
||||
|
||||
<!-- Chosen Palette: Warm Neutrals (Stone) with Logic-based Alerts (Red/Green) -->
|
||||
<!-- Application Structure Plan:
|
||||
- Added "GPA Simulation" controls to each semester header.
|
||||
- Semester containers now react visually to SKS Overload based on previous semester's GPA.
|
||||
- Drag & Drop enables users to "Fail/Postpone" courses to future semesters to fit the SKS limit.
|
||||
-->
|
||||
<!-- CONFIRMATION: NO SVG graphics used. NO Mermaid JS used. -->
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="mb-10 text-center">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-stone-900 mb-4">Simulasi Perjalanan Mahasiswa & KRS</h1>
|
||||
<p class="text-lg text-stone-600 max-w-3xl mx-auto">
|
||||
Atur strategi studimu. <span class="font-bold text-orange-600">Simulasikan IP rendah</span>, lihat dampak batas SKS, dan geser mata kuliah untuk menyusun ulang rencana (mengulang/menunda).
|
||||
</p>
|
||||
|
||||
<div class="mt-6 flex flex-wrap justify-center gap-4 text-sm">
|
||||
<div class="flex items-center gap-2 bg-white px-3 py-1.5 rounded border border-stone-200 shadow-sm">
|
||||
<div class="w-3 h-3 rounded-full bg-green-500"></div>
|
||||
<span>IP ≥ 3.00 (Max 24 SKS)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 bg-white px-3 py-1.5 rounded border border-stone-200 shadow-sm">
|
||||
<div class="w-3 h-3 rounded-full bg-yellow-400"></div>
|
||||
<span>2.50 ≤ IP < 3.00 (Max 21 SKS)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 bg-white px-3 py-1.5 rounded border border-stone-200 shadow-sm">
|
||||
<div class="w-3 h-3 rounded-full bg-orange-400"></div>
|
||||
<span>2.00 ≤ IP < 2.50 (Max 18 SKS)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 bg-white px-3 py-1.5 rounded border border-stone-200 shadow-sm">
|
||||
<div class="w-3 h-3 rounded-full bg-red-500"></div>
|
||||
<span>IP < 2.00 (Max 15 SKS)</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Phase 1: Generalist (Sem 1-4) -->
|
||||
<section class="mb-12">
|
||||
<div class="flex items-center justify-between mb-6 border-b border-stone-200 pb-2">
|
||||
<h2 class="text-2xl font-bold text-stone-800">Tahun 1 & 2 (Generalist)</h2>
|
||||
<div class="text-xs text-stone-500 italic">Geser slider IP untuk melihat perubahan Max SKS semester depan</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Generate Semester Containers dynamically to handle logic easier -->
|
||||
<div id="container-sem-1"></div>
|
||||
<div id="container-sem-2"></div>
|
||||
<div id="container-sem-3"></div>
|
||||
<div id="container-sem-4"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Phase 2: Specialist (Sem 5-8) -->
|
||||
<section class="mb-16">
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center justify-between mb-6">
|
||||
<div class="mb-4 md:mb-0">
|
||||
<h2 class="text-2xl font-bold text-stone-800">Tahun 3 & 4 (Specialist)</h2>
|
||||
<p class="text-stone-500 text-sm">Pilih jalur, lalu atur beban studi jika terkena dampak IP rendah.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button onclick="switchTrack('ai')" id="btn-ai" class="track-btn active px-4 py-2 rounded-lg text-sm font-semibold transition-colors">Komputasi & AI</button>
|
||||
<button onclick="switchTrack('rpl')" id="btn-rpl" class="track-btn px-4 py-2 rounded-lg text-sm font-semibold transition-colors">RPL</button>
|
||||
<button onclick="switchTrack('net')" id="btn-net" class="track-btn px-4 py-2 rounded-lg text-sm font-semibold transition-colors">Jaringan</button>
|
||||
<button onclick="switchTrack('si')" id="btn-si" class="track-btn px-4 py-2 rounded-lg text-sm font-semibold transition-colors">Sistem Informasi</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="specialist-content" class="bg-stone-100 p-6 rounded-2xl border border-stone-200">
|
||||
<!-- Injected via JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Analytics -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
|
||||
<div class="bg-white p-6 rounded-xl border border-stone-200 shadow-sm">
|
||||
<h3 class="text-lg font-bold text-stone-800 mb-2">Monitor Beban SKS</h3>
|
||||
<p class="text-xs text-stone-500 mb-4">Batang merah menandakan beban melebihi jatah SKS akibat IP semester sebelumnya.</p>
|
||||
<div class="chart-container">
|
||||
<canvas id="sksChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-6 rounded-xl border border-stone-200 shadow-sm flex flex-col justify-center items-center text-center">
|
||||
<h3 class="text-lg font-bold text-stone-800 mb-2">Total SKS Terambil</h3>
|
||||
<div class="text-5xl font-bold text-stone-800 my-4" id="grand-total-sks">146</div>
|
||||
<p class="text-sm text-stone-500">Target Kelulusan: 144 SKS</p>
|
||||
<div id="graduation-status" class="mt-4 px-4 py-2 rounded-full text-sm font-bold bg-green-100 text-green-700">Siap Lulus</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="text-center text-stone-400 text-sm pt-8 border-t border-stone-200 pb-8">
|
||||
<p>© 2026 Curriculum Simulator. Based on Verified Student Journey Map.</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TEMPLATE FOR SEMESTER CARD -->
|
||||
<template id="semester-template">
|
||||
<div class="semester-container bg-white rounded-xl shadow-sm border border-stone-200 overflow-hidden flex flex-col h-full">
|
||||
<div class="header-bg bg-stone-100 px-4 py-3 border-b border-stone-200">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 class="font-bold text-stone-800 text-lg sem-title">Semester X</h3>
|
||||
<span class="sks-badge text-xs bg-white px-2 py-1 rounded border border-stone-300 font-bold">0 / 24 SKS</span>
|
||||
</div>
|
||||
|
||||
<!-- GPA Simulator Control -->
|
||||
<div class="bg-white/50 rounded p-2 border border-stone-200/50">
|
||||
<div class="flex justify-between text-xs text-stone-600 mb-1">
|
||||
<span>Simulasi IP: <strong class="gpa-value">3.50</strong></span>
|
||||
<span class="text-[10px] text-stone-400">Efek ke Sem Selanjutnya</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="4" step="0.01" value="3.50" class="gpa-slider w-full h-1 bg-stone-200 rounded-lg appearance-none cursor-pointer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="semester-list p-3 space-y-2 flex-grow" ondrop="drop(event)" ondragover="allowDrop(event)" ondragenter="dragEnter(event)" ondragleave="dragLeave(event)">
|
||||
<!-- Course Cards go here -->
|
||||
</div>
|
||||
|
||||
<div class="status-footer px-4 py-2 bg-stone-50 text-[10px] text-stone-400 border-t border-stone-100 italic text-right">
|
||||
Max SKS Next: <span class="next-max-sks">24</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// --- DATA ---
|
||||
const generalistSemestersBlueprint = {
|
||||
1: [
|
||||
{code: "MK06", name: "Dasar Pemrograman*", sks: 3},
|
||||
{code: "MK07", name: "Algoritma & Logika", sks: 2},
|
||||
{code: "MK04", name: "Kalkulus", sks: 3},
|
||||
{code: "MK05", name: "Pengantar Informatika", sks: 2},
|
||||
{code: "MK01", name: "Pendidikan Pancasila", sks: 2},
|
||||
{code: "MK02", name: "Pendidikan Agama", sks: 3},
|
||||
{code: "MK03", name: "Bahasa Inggris", sks: 2},
|
||||
{code: "MK08", name: "Kom. Profesional", sks: 2}
|
||||
],
|
||||
2: [
|
||||
{code: "MK15", name: "Struktur Data & Algo*", sks: 3},
|
||||
{code: "MK12", name: "Matematika Diskrit", sks: 3},
|
||||
{code: "MK11", name: "Sistem Digital", sks: 2},
|
||||
{code: "MK13", name: "Org. & Ars. Komputer", sks: 3},
|
||||
{code: "MK14", name: "Sistem Operasi*", sks: 3},
|
||||
{code: "MK16", name: "Literasi Digital", sks: 2},
|
||||
{code: "MK10", name: "Bahasa Indonesia", sks: 2},
|
||||
{code: "MK09", name: "Kewarganegaraan", sks: 2}
|
||||
],
|
||||
3: [
|
||||
{code: "MK20", name: "Basis Data*", sks: 3},
|
||||
{code: "MK21", name: "Jaringan Komputer*", sks: 3},
|
||||
{code: "MK27", name: "PBO*", sks: 3},
|
||||
{code: "MK24", name: "IMK", sks: 2},
|
||||
{code: "MK22", name: "Strategi Algoritma*", sks: 3},
|
||||
{code: "MK17", name: "Aljabar Linier", sks: 3},
|
||||
{code: "MK19", name: "Sistem Informasi", sks: 2},
|
||||
{code: "MK18", name: "Etika & Keberlanjutan", sks: 2}
|
||||
],
|
||||
4: [
|
||||
{code: "MK23", name: "RPL*", sks: 3},
|
||||
{code: "MK32", name: "Probstat", sks: 3},
|
||||
{code: "MK31", name: "Pemrograman Web*", sks: 3},
|
||||
{code: "MK30", name: "Kecerdasan Buatan", sks: 3},
|
||||
{code: "MK25", name: "APSI", sks: 3},
|
||||
{code: "MK29", name: "Pemrog. Jaringan*", sks: 3},
|
||||
{code: "MK26", name: "Sis. Paralel", sks: 2}
|
||||
]
|
||||
};
|
||||
|
||||
const tracksBlueprint = {
|
||||
ai: {
|
||||
name: "Komputasi & AI",
|
||||
color: "text-blue-800",
|
||||
desc: "Fokus: Data, ML, Vision.",
|
||||
sem5: [
|
||||
{code: "MK35", name: "Machine Learning", sks: 3},
|
||||
{code: "MK28", name: "Metode Numerik*", sks: 3},
|
||||
{code: "MK36", name: "Proyek PL*", sks: 3},
|
||||
{code: "MK37", name: "Proposal TA", sks: 2},
|
||||
{code: "MKP", name: "Data Mining", sks: 3},
|
||||
{code: "MKP", name: "Big Data", sks: 3},
|
||||
{code: "MK33", name: "IoT", sks: 3}
|
||||
],
|
||||
sem6: [
|
||||
{code: "MK38", name: "Comp. Vision*", sks: 3},
|
||||
{code: "MK43", name: "NLP", sks: 3},
|
||||
{code: "MK44", name: "Kerja Praktik", sks: 2},
|
||||
{code: "MKP", name: "Deep Learning", sks: 3},
|
||||
{code: "MKP", name: "AI Ethics", sks: 2},
|
||||
{code: "MKP", name: "Pilihan Lintas KK", sks: 3},
|
||||
{code: "MK42", name: "Keamanan Info*", sks: 2}
|
||||
],
|
||||
sem7: [
|
||||
{code: "MK48", name: "TA 1", sks: 2},
|
||||
{code: "MK45", name: "Teknopreneur [K]", sks: 3},
|
||||
{code: "MK47", name: "Uji PL", sks: 3},
|
||||
{code: "MK42", name: "Keamanan Info", sks: 3},
|
||||
{code: "MKP", name: "Health Info", sks: 3},
|
||||
{code: "MKP", name: "Reinforcement L.", sks: 3},
|
||||
{code: "MKP", name: "Quantum/Bebas", sks: 5}
|
||||
]
|
||||
},
|
||||
rpl: {
|
||||
name: "RPL",
|
||||
color: "text-orange-800",
|
||||
desc: "Fokus: Apps, Architecture, UX.",
|
||||
sem5: [
|
||||
{code: "MK36", name: "Proyek PL*", sks: 3},
|
||||
{code: "MKP", name: "UX/UI Design", sks: 3},
|
||||
{code: "MKP", name: "Secure SE", sks: 3},
|
||||
{code: "MKP", name: "Web Lanjut", sks: 3},
|
||||
{code: "MK28", name: "Metode Numerik*", sks: 3},
|
||||
{code: "MK37", name: "Proposal TA", sks: 2},
|
||||
{code: "MK33", name: "IoT", sks: 3}
|
||||
],
|
||||
sem6: [
|
||||
{code: "MK39", name: "Pemrog. Mobile*", sks: 3},
|
||||
{code: "MKP", name: "Microservices", sks: 3},
|
||||
{code: "MKP", name: "Game Dev", sks: 3},
|
||||
{code: "MK44", name: "Kerja Praktik", sks: 2},
|
||||
{code: "MK41", name: "Sis. Enterprise", sks: 3},
|
||||
{code: "MKP", name: "Pil. Lintas KK", sks: 4}
|
||||
],
|
||||
sem7: [
|
||||
{code: "MK48", name: "TA 1", sks: 2},
|
||||
{code: "MK45", name: "Teknopreneur [K]", sks: 3},
|
||||
{code: "MK47", name: "Uji PL", sks: 3},
|
||||
{code: "MK42", name: "Keamanan Info", sks: 3},
|
||||
{code: "MKP", name: "DevOps", sks: 3},
|
||||
{code: "MKP", name: "Soft. Arch", sks: 3},
|
||||
{code: "MKP", name: "Pil. Bebas", sks: 5}
|
||||
]
|
||||
},
|
||||
net: {
|
||||
name: "Jaringan",
|
||||
color: "text-emerald-800",
|
||||
desc: "Fokus: Infra, Security, IoT.",
|
||||
sem5: [
|
||||
{code: "MK33", name: "IoT", sks: 3},
|
||||
{code: "MK34", name: "Manajemen Jar.*", sks: 3},
|
||||
{code: "MKP", name: "Embedded Sys", sks: 3},
|
||||
{code: "MKP", name: "Ethical Hack", sks: 3},
|
||||
{code: "MK36", name: "Proyek PL*", sks: 3},
|
||||
{code: "MK28", name: "Metode Numerik*", sks: 3},
|
||||
{code: "MK37", name: "Proposal TA", sks: 2}
|
||||
],
|
||||
sem6: [
|
||||
{code: "MK42", name: "Keamanan Info*", sks: 3},
|
||||
{code: "MKP", name: "Wireless Sensor", sks: 3},
|
||||
{code: "MKP", name: "Cryptography", sks: 3},
|
||||
{code: "MK44", name: "Kerja Praktik", sks: 2},
|
||||
{code: "MK39", name: "Mobile (IoT)*", sks: 3},
|
||||
{code: "MKP", name: "Pil. Lintas KK", sks: 3}
|
||||
],
|
||||
sem7: [
|
||||
{code: "MK48", name: "TA 1", sks: 2},
|
||||
{code: "MK45", name: "Teknopreneur [K]", sks: 3},
|
||||
{code: "MK47", name: "Uji PL", sks: 3},
|
||||
{code: "MKP", name: "Cloud IoT", sks: 3},
|
||||
{code: "MKP", name: "Blockchain", sks: 3},
|
||||
{code: "MKP", name: "Pil. Bebas 1", sks: 3},
|
||||
{code: "MKP", name: "Pil. Bebas 2", sks: 6}
|
||||
]
|
||||
},
|
||||
si: {
|
||||
name: "Sistem Informasi",
|
||||
color: "text-purple-800",
|
||||
desc: "Fokus: Business, GIS, Governance.",
|
||||
sem5: [
|
||||
{code: "MKP", name: "Business Intel", sks: 3},
|
||||
{code: "MKP", name: "GIS II", sks: 3},
|
||||
{code: "MK36", name: "Proyek PL*", sks: 3},
|
||||
{code: "MK28", name: "Metode Numerik*", sks: 3},
|
||||
{code: "MK37", name: "Proposal TA", sks: 2},
|
||||
{code: "MKP", name: "E-Business", sks: 3},
|
||||
{code: "MK33", name: "IoT", sks: 3}
|
||||
],
|
||||
sem6: [
|
||||
{code: "MK41", name: "Sis. Enterprise", sks: 3},
|
||||
{code: "MK40", name: "SIG", sks: 3},
|
||||
{code: "MKP", name: "Enterprise Arch", sks: 3},
|
||||
{code: "MKP", name: "Smart City", sks: 3},
|
||||
{code: "MK44", name: "Kerja Praktik", sks: 2},
|
||||
{code: "MKP", name: "Pil. Lintas KK", sks: 3}
|
||||
],
|
||||
sem7: [
|
||||
{code: "MK48", name: "TA 1", sks: 2},
|
||||
{code: "MK45", name: "Teknopreneur [K]", sks: 3},
|
||||
{code: "MK47", name: "Uji PL", sks: 3},
|
||||
{code: "MK42", name: "Keamanan Info", sks: 3},
|
||||
{code: "MKP", name: "SCM", sks: 3},
|
||||
{code: "MKP", name: "IT Governance", sks: 3},
|
||||
{code: "MKP", name: "Pil. Bebas", sks: 6}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const mbkmData = [
|
||||
{code: "MK46", name: "PMKM (Wajib)", sks: 2},
|
||||
{code: "MK45", name: "Teknopreneur (Konversi)", sks: 3},
|
||||
{code: "MKP", name: "Pilihan Bebas (Ops)", sks: 3}
|
||||
];
|
||||
|
||||
// --- STATE ---
|
||||
let currentPlan = {};
|
||||
let currentTrack = 'ai';
|
||||
let semesterStats = {
|
||||
1: { gpa: 3.50, maxSKSNext: 24 }, // Sem 1 Max is fixed (usually paket)
|
||||
2: { gpa: 3.50, maxSKSNext: 24 },
|
||||
3: { gpa: 3.50, maxSKSNext: 24 },
|
||||
4: { gpa: 3.50, maxSKSNext: 24 },
|
||||
5: { gpa: 3.50, maxSKSNext: 24 },
|
||||
6: { gpa: 3.50, maxSKSNext: 24 },
|
||||
7: { gpa: 3.50, maxSKSNext: 24 },
|
||||
8: { gpa: 4.00, maxSKSNext: 24 }
|
||||
};
|
||||
|
||||
// --- LOGIC ---
|
||||
|
||||
function calculateMaxSKS(gpa) {
|
||||
if (gpa >= 3.00) return 24;
|
||||
if (gpa >= 2.50) return 21;
|
||||
if (gpa >= 2.00) return 18;
|
||||
return 15;
|
||||
}
|
||||
|
||||
function initPlan(trackKey) {
|
||||
currentTrack = trackKey;
|
||||
currentPlan = {};
|
||||
|
||||
// Deep Copy Gen
|
||||
for(let i=1; i<=4; i++) currentPlan[i] = JSON.parse(JSON.stringify(generalistSemestersBlueprint[i]));
|
||||
// Deep Copy Spec
|
||||
const track = tracksBlueprint[trackKey];
|
||||
currentPlan[5] = JSON.parse(JSON.stringify(track.sem5));
|
||||
currentPlan[6] = JSON.parse(JSON.stringify(track.sem6));
|
||||
currentPlan[7] = JSON.parse(JSON.stringify(track.sem7));
|
||||
currentPlan[8] = [{code: "MK49", name: "TA 2 (Sidang)", sks: 4}];
|
||||
}
|
||||
|
||||
function renderSem(semId, containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
// Determine Max SKS for THIS semester based on PREV semester GPA
|
||||
let maxSKS = 24;
|
||||
if (semId > 1) {
|
||||
const prevGPA = semesterStats[semId-1].gpa;
|
||||
maxSKS = calculateMaxSKS(prevGPA);
|
||||
}
|
||||
|
||||
// Get Current SKS Load
|
||||
const courses = currentPlan[semId];
|
||||
const currentSKS = courses.reduce((a,b)=>a+b.sks, 0);
|
||||
|
||||
// Warning State
|
||||
const isOverload = currentSKS > maxSKS;
|
||||
|
||||
// Template Cloning
|
||||
const template = document.getElementById('semester-template');
|
||||
const clone = template.content.cloneNode(true);
|
||||
|
||||
// Populate Header
|
||||
const titleEl = clone.querySelector('.sem-title');
|
||||
titleEl.textContent = `Semester ${semId}`;
|
||||
|
||||
const badgeEl = clone.querySelector('.sks-badge');
|
||||
badgeEl.textContent = `${currentSKS} / ${maxSKS} SKS`;
|
||||
|
||||
const containerDiv = clone.querySelector('.semester-container');
|
||||
if (isOverload) {
|
||||
containerDiv.classList.add('overload');
|
||||
badgeEl.textContent += " (OVER!)";
|
||||
}
|
||||
|
||||
// GPA Control
|
||||
const gpaValEl = clone.querySelector('.gpa-value');
|
||||
const gpaSlider = clone.querySelector('.gpa-slider');
|
||||
const nextMaxEl = clone.querySelector('.next-max-sks');
|
||||
|
||||
gpaSlider.value = semesterStats[semId].gpa;
|
||||
gpaValEl.textContent = parseFloat(semesterStats[semId].gpa).toFixed(2);
|
||||
|
||||
// Calc next sem limit based on current slider
|
||||
const nextLimit = calculateMaxSKS(semesterStats[semId].gpa);
|
||||
if (semId < 8) {
|
||||
nextMaxEl.textContent = nextLimit;
|
||||
} else {
|
||||
nextMaxEl.parentElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Event Listener for Slider
|
||||
gpaSlider.addEventListener('input', (e) => {
|
||||
const val = parseFloat(e.target.value);
|
||||
semesterStats[semId].gpa = val;
|
||||
gpaValEl.textContent = val.toFixed(2);
|
||||
|
||||
// Update display of next limit immediately
|
||||
if(semId < 8) nextMaxEl.textContent = calculateMaxSKS(val);
|
||||
|
||||
// Trigger re-render of NEXT semester (to show overload warning)
|
||||
// We re-render everything to be safe and simple
|
||||
// Debounce slighty could be good but for now direct call
|
||||
});
|
||||
gpaSlider.addEventListener('change', () => {
|
||||
renderAll(); // Re-render all to propagate red status to next semester
|
||||
});
|
||||
|
||||
// Populate List
|
||||
const listContainer = clone.querySelector('.semester-list');
|
||||
listContainer.id = `list-sem-${semId}`; // For drag logic ref
|
||||
listContainer.setAttribute('ondrop', `drop(event, ${semId})`); // Update drop target ID
|
||||
|
||||
courses.forEach((c, idx) => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'course-card bg-white p-2 rounded border border-stone-200 shadow-sm flex justify-between items-center';
|
||||
el.setAttribute('draggable', 'true');
|
||||
el.setAttribute('ondragstart', `drag(event, ${semId}, ${idx})`);
|
||||
|
||||
// Styling for special items
|
||||
if(c.name.includes('KONVERSI')) el.classList.add('bg-green-50', 'border-green-200');
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="overflow-hidden">
|
||||
<div class="text-[10px] text-stone-400 font-mono">${c.code}</div>
|
||||
<div class="text-xs font-bold text-stone-700 truncate w-32" title="${c.name}">${c.name}</div>
|
||||
</div>
|
||||
<div class="text-xs font-bold bg-stone-100 text-stone-600 px-1.5 py-0.5 rounded border border-stone-200 ml-2">${c.sks}</div>
|
||||
`;
|
||||
listContainer.appendChild(el);
|
||||
});
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(clone);
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
// Render 1-4
|
||||
renderSem(1, 'container-sem-1');
|
||||
renderSem(2, 'container-sem-2');
|
||||
renderSem(3, 'container-sem-3');
|
||||
renderSem(4, 'container-sem-4');
|
||||
|
||||
// Render 5-8 (Inside Specialist Section)
|
||||
const track = tracksBlueprint[currentTrack];
|
||||
const contentDiv = document.getElementById('specialist-content');
|
||||
|
||||
// We recreate the grid structure for 5-8 inside the content div
|
||||
// This is a bit brute force but keeps logic simple
|
||||
contentDiv.innerHTML = `
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-bold ${track.color}">${track.name}</h3>
|
||||
<p class="text-sm text-stone-500">${track.desc}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div id="container-sem-5"></div>
|
||||
<!-- MBKM Slot Visual -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="bg-gradient-to-br from-green-50 to-emerald-100 rounded-xl border border-green-200 p-3 shadow-sm h-auto relative">
|
||||
<div class="absolute top-0 right-0 bg-green-200 text-green-800 text-[9px] font-bold px-2 py-1 rounded-bl">LIBURAN</div>
|
||||
<h4 class="font-bold text-green-900 text-sm mb-2">MBKM (Sem Antara)</h4>
|
||||
<div class="space-y-2">
|
||||
${mbkmData.map(c => `
|
||||
<div class="bg-white/60 p-2 rounded border border-green-100 flex justify-between items-center">
|
||||
<span class="text-[10px] font-bold text-green-800">${c.name}</span>
|
||||
<span class="text-[9px] bg-green-200 text-green-800 px-1 rounded">${c.sks}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div id="container-sem-6" class="flex-grow"></div>
|
||||
</div>
|
||||
<div id="container-sem-7"></div>
|
||||
<div id="container-sem-8"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
renderSem(5, 'container-sem-5');
|
||||
renderSem(6, 'container-sem-6');
|
||||
renderSem(7, 'container-sem-7');
|
||||
renderSem(8, 'container-sem-8');
|
||||
|
||||
updateCharts();
|
||||
}
|
||||
|
||||
function switchTrack(key) {
|
||||
document.querySelectorAll('.track-btn').forEach(b => b.classList.remove('active'));
|
||||
document.getElementById(`btn-${key}`).classList.add('active');
|
||||
initPlan(key);
|
||||
renderAll();
|
||||
}
|
||||
|
||||
// --- DRAG & DROP ---
|
||||
let draggedItem = null;
|
||||
|
||||
function drag(ev, semId, idx) {
|
||||
draggedItem = { semId, idx };
|
||||
ev.dataTransfer.effectAllowed = "move";
|
||||
ev.target.style.opacity = '0.4';
|
||||
}
|
||||
|
||||
function allowDrop(ev) { ev.preventDefault(); }
|
||||
function dragEnter(ev) {
|
||||
ev.preventDefault();
|
||||
ev.currentTarget.classList.add('drag-over');
|
||||
}
|
||||
function dragLeave(ev) {
|
||||
ev.currentTarget.classList.remove('drag-over');
|
||||
}
|
||||
|
||||
function drop(ev, targetSem) {
|
||||
ev.preventDefault();
|
||||
document.querySelectorAll('.semester-list').forEach(el => el.classList.remove('drag-over'));
|
||||
|
||||
if (!draggedItem) return;
|
||||
const { semId, idx } = draggedItem;
|
||||
|
||||
if (semId !== targetSem) {
|
||||
// Move logic
|
||||
const course = currentPlan[semId][idx];
|
||||
currentPlan[semId].splice(idx, 1);
|
||||
currentPlan[targetSem].push(course);
|
||||
|
||||
renderAll(); // Re-calc status
|
||||
} else {
|
||||
renderAll(); // Just reset opacity
|
||||
}
|
||||
draggedItem = null;
|
||||
}
|
||||
|
||||
// --- CHARTS ---
|
||||
let sksChart = null;
|
||||
|
||||
function updateCharts() {
|
||||
// Calc SKS
|
||||
let dataSKS = [];
|
||||
let bgColors = [];
|
||||
let totalSKS = 0;
|
||||
|
||||
for(let i=1; i<=8; i++) {
|
||||
const s = currentPlan[i].reduce((a,b)=>a+b.sks, 0);
|
||||
dataSKS.push(s);
|
||||
totalSKS += s;
|
||||
|
||||
// Color logic: Red if overload
|
||||
let limit = 24;
|
||||
if(i > 1) limit = calculateMaxSKS(semesterStats[i-1].gpa);
|
||||
|
||||
if (s > limit) bgColors.push('#ef4444');
|
||||
else bgColors.push('#d6d3d1');
|
||||
}
|
||||
|
||||
// Update Total Display
|
||||
document.getElementById('grand-total-sks').textContent = totalSKS;
|
||||
const statusEl = document.getElementById('graduation-status');
|
||||
if(totalSKS >= 144) {
|
||||
statusEl.textContent = "Siap Lulus";
|
||||
statusEl.className = "mt-4 px-4 py-2 rounded-full text-sm font-bold bg-green-100 text-green-700";
|
||||
} else {
|
||||
statusEl.textContent = `Kurang ${144 - totalSKS} SKS`;
|
||||
statusEl.className = "mt-4 px-4 py-2 rounded-full text-sm font-bold bg-red-100 text-red-700";
|
||||
}
|
||||
|
||||
// Chart
|
||||
const ctx = document.getElementById('sksChart').getContext('2d');
|
||||
if(sksChart) sksChart.destroy();
|
||||
|
||||
sksChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8'],
|
||||
datasets: [{
|
||||
label: 'SKS',
|
||||
data: dataSKS,
|
||||
backgroundColor: bgColors,
|
||||
borderRadius: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: { y: { beginAtZero: true, max: 28 } },
|
||||
plugins: { legend: { display: false } }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Init
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
switchTrack('ai');
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user