Update simulasi-mk.html
update 2
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user