Update simulasi-mk.html

update 2
This commit is contained in:
izu
2026-02-03 08:35:02 +00:00
parent 6090b28bfa
commit f9719df1b5

View File

@@ -9,7 +9,7 @@
<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; }
body { font-family: 'Inter', sans-serif; background-color: #f8fafc; }
/* Chart Sizing */
.chart-container { position: relative; width: 100%; height: 250px; }
@@ -64,12 +64,22 @@
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; /* Sedikit lebih kecil dari 1400px agar lebih rapi */
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-40 border-r border-slate-200 flex flex-col closed">
<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>
@@ -83,9 +93,9 @@
<div id="catalog-list" class="flex-1 overflow-y-auto p-3 space-y-2 custom-scroll"></div>
</div>
<!-- Toggle Button -->
<button onclick="toggleCatalog()" class="fixed left-0 top-24 bg-blue-600 text-white p-2 rounded-r-lg shadow-lg z-30 hover:bg-blue-700 transition-all flex items-center gap-2 pr-4 font-semibold text-sm">
<i class="fas fa-book-open"></i> Katalog
<!-- 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 -->
@@ -110,9 +120,9 @@
</div>
<!-- Main Content -->
<div class="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 py-8 transition-all duration-300 ml-0 md:ml-12">
<div class="main-container py-8 transition-all duration-300">
<header class="mb-8 text-center pl-8">
<header class="mb-8 text-center">
<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>
@@ -130,36 +140,36 @@
<!-- 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-3 rounded-xl shadow-sm border border-slate-200">
<div class="text-2xl font-bold text-slate-800" id="total-sks-passed">0</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider">Total SKS Lulus</div>
<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-3 rounded-xl shadow-sm border border-slate-200">
<div class="text-2xl font-bold text-blue-600" id="ipk-global">0.00</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider">IPK Kumulatif</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-3 rounded-xl shadow-sm border border-slate-200">
<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">Predikat</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider mt-1">Predikat</div>
</div>
<div class="bg-white p-3 rounded-xl shadow-sm border border-slate-200">
<div class="text-2xl font-bold text-red-500" id="total-fail">0</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider">SKS Gagal (E)</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-1.5 rounded-full text-xs font-bold border transition-colors bg-slate-800 text-white" id="btn-general">Umum</button>
<button onclick="switchTrack('ai')" class="track-btn px-4 py-1.5 rounded-full text-xs font-bold border transition-colors bg-white hover:bg-slate-50" id="btn-ai">AI</button>
<button onclick="switchTrack('rpl')" class="track-btn px-4 py-1.5 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-1.5 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-1.5 rounded-full text-xs font-bold border transition-colors bg-white hover:bg-slate-50" id="btn-si">SI & GIS</button>
<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</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</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 lg:grid-cols-2 gap-x-8 gap-y-10 mb-12" id="semester-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>
@@ -182,6 +192,20 @@
</div>
<script>
// --- UTILS: Fix for HTTP Environments (Coolify) ---
// crypto.randomUUID() only works in Secure Contexts (HTTPS).
// This fallback ensures the app works on HTTP or local IPs.
function generateUUID() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback for non-secure contexts
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' },
@@ -315,8 +339,8 @@
for(let i=1; i<=8; i++) {
currentPlan[i] = [];
if(baseCurriculum[i]) {
// Default Grade: 4.0 (A) to encourage positive simulation
currentPlan[i] = baseCurriculum[i].map(c => ({...c, grade: 4, uuid: crypto.randomUUID()}));
// Use generateUUID instead of crypto.randomUUID
currentPlan[i] = baseCurriculum[i].map(c => ({...c, grade: 4, uuid: generateUUID()}));
}
}
@@ -324,7 +348,7 @@
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: crypto.randomUUID()});
if(tr[idx]) currentPlan[sem].push({...tr[idx], grade: 4, uuid: generateUUID()});
});
renderCatalog();
@@ -340,7 +364,6 @@
courses.forEach(c => {
loadSKS += c.sks;
// Hanya hitung IP dari sks yang diambil dan bukan yang ditandai mengulang (retake)
// Note: Jika mengulang di semester baru, grade awal akan fresh.
if(c.grade > 0) {
totalSKS += c.sks;
totalPoints += (c.sks * c.grade);
@@ -360,6 +383,7 @@
function renderAll() {
const grid = document.getElementById('semester-grid');
if (!grid) return;
grid.innerHTML = '';
for(let i=1; i<=8; i++) {
@@ -377,7 +401,7 @@
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`;
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">
@@ -482,7 +506,8 @@
let x = e.pageX;
let y = e.pageY;
if(x + 200 > window.innerWidth) x -= 200;
// Prevent menu going offscreen
if(x + 200 > window.innerWidth) x = window.innerWidth - 210;
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
@@ -512,22 +537,20 @@
const { semId, index } = contextTarget;
const course = currentPlan[semId][index];
// Target: 2 semester berikutnya (Tahun depan, semester ganjil/genap yang sama)
// Target: 2 semester berikutnya
const targetSem = parseInt(semId) + 2;
if (targetSem > 8) {
showToast("Maaf, semester tujuan ("+targetSem+") di luar jangkauan simulasi (Max Sem 8).");
} else {
// Cek apakah course sudah ada di target
const exists = currentPlan[targetSem].some(c => c.id === course.id);
if (exists) {
showToast("Mata kuliah ini sudah ada di Semester " + targetSem + ".");
} else {
// Copy course
const newCourse = {
...course,
grade: 4, // Default A saat mengambil ulang
uuid: crypto.randomUUID(),
grade: 4,
uuid: generateUUID(), // Secure UUID
isRetake: true
};
currentPlan[targetSem].push(newCourse);
@@ -556,7 +579,6 @@
}
document.addEventListener('click', (e) => {
// Close context menu if clicking outside
if(!e.target.closest('#context-menu') && !e.target.closest('.course-card')) {
hideContextMenu();
}
@@ -566,7 +588,8 @@
let dragSource = null;
function dragCatalog(ev, id, name, sks, cat) {
dragSource = { type: 'catalog', course: { id, name, sks: parseInt(sks), cat, grade: 4, uuid: crypto.randomUUID() } };
// Use generateUUID
dragSource = { type: 'catalog', course: { id, name, sks: parseInt(sks), cat, grade: 4, uuid: generateUUID() } };
ev.dataTransfer.effectAllowed = "copy";
}
@@ -668,7 +691,6 @@
semIPS.push(stats.ips);
currentPlan[i].forEach(c => {
// SKS yang dihitung di Global Stats adalah yang LULUS (Grade > 0)
if (c.grade > 0) {
totalSKS += c.sks;
totalPoints += (c.sks * c.grade);