<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SISTEM ANALISIS PEPERIKSAAN</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#F8FAFC; /* Latar belakang utama (kelabu sangat cerah) */
--bg2:#F1F5F9; /* Latar belakang sekunder (sidebar/header) */
--card:#FFFFFF; /* Latar belakang kad (putih tulen) */
--card2:#F8FAFC;
--border:#E2E8F0; /* Border standard */
--border2:#CBD5E1; /* Border gelap untuk hover */
--text:#0F172A; /* Teks utama (kelabu gelap / hampir hitam) */
--text2:#475569; /* Teks sekunder */
--muted:#94A3B8; /* Teks pudar */
--accent:#10B981; /* Hijau profesional (Emerald) */
--accent2:#059669; /* Hijau gelap untuk hover */
--gold:#F59E0B; /* Oren keemasan */
--gold2:#D97706; /* Oren gelap untuk hover */
--danger:#EF4444; /* Merah */
--danger2:#DC2626; /* Merah gelap untuk hover */
--info:#3B82F6; /* Biru */
--radius:12px;
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
h1,h2,h3,h4,h5{font-family:'Outfit',sans-serif}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-track{background:var(--bg2)}
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--muted)}
/* Login Page */
#loginPage{min-height:100vh;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}
.login-bg{position:absolute;inset:0;background:radial-gradient(ellipse at 20% 50%,rgba(16,185,129,.1) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(245,158,11,.08) 0%,transparent 50%),radial-gradient(ellipse at 50% 80%,rgba(59,130,246,.05) 0%,transparent 50%),var(--bg)}
.orb{position:absolute;border-radius:50%;filter:blur(80px);animation:orbFloat 8s ease-in-out infinite}
.orb-1{width:400px;height:400px;background:rgba(16,185,129,.15);top:-100px;left:-100px;animation-delay:0s}
.orb-2{width:300px;height:300px;background:rgba(245,158,11,.12);bottom:-50px;right:-50px;animation-delay:3s}
.orb-3{width:200px;height:200px;background:rgba(59,130,246,.1);top:50%;left:60%;animation-delay:5s}
@keyframes orbFloat{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(30px,-20px) scale(1.05)}66%{transform:translate(-20px,30px) scale(.95)}}
.login-card{background:rgba(255,255,255,.9);backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:20px;padding:48px 40px;width:420px;max-width:90vw;position:relative;z-index:1;box-shadow:0 25px 60px rgba(0,0,0,.08)}
.login-card::before{content:'';position:absolute;top:-1px;left:50%;transform:translateX(-50%);width:60%;height:2px;background:linear-gradient(90deg,transparent,var(--accent),transparent)}
.input-group{position:relative;margin-bottom:20px}
.input-group i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:14px;transition:.3s;pointer-events:none}
.input-group input{width:100%;padding:14px 14px 14px 42px;background:var(--bg);border:1px solid var(--border);border-radius:10px;color:var(--text);font-size:15px;font-family:'DM Sans',sans-serif;transition:.3s;outline:none}
.input-group input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(16,185,129,.1)}
.input-group input:focus+i,.input-group input:focus~i{color:var(--accent)}
.btn{padding:12px 24px;border:none;border-radius:10px;font-family:'DM Sans',sans-serif;font-weight:600;font-size:15px;cursor:pointer;transition:all .25s;display:inline-flex;align-items:center;gap:8px}
.btn-primary{background:var(--accent);color:#fff}.btn-primary:hover{background:var(--accent2);transform:translateY(-1px);box-shadow:0 6px 20px rgba(16,185,129,.25)}
.btn-gold{background:var(--gold);color:#fff}.btn-gold:hover{background:var(--gold2);transform:translateY(-1px)}
.btn-danger{background:var(--danger);color:#fff}.btn-danger:hover{background:var(--danger2);transform:translateY(-1px)}
.btn-outline{background:transparent;border:1px solid var(--border2);color:var(--text2)}.btn-outline:hover{border-color:var(--accent);color:var(--accent)}
.btn-sm{padding:8px 16px;font-size:13px;border-radius:8px}
.btn-xs{padding:5px 10px;font-size:12px;border-radius:6px}
/* Year/Exam Page */
#yearExamPage{min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--bg);position:relative}
.year-card{background:var(--card);border:1px solid var(--border);border-radius:20px;padding:48px 40px;width:560px;max-width:90vw;position:relative;box-shadow:0 25px 60px rgba(0,0,0,.05)}
.year-card::before{content:'';position:absolute;top:-1px;left:50%;transform:translateX(-50%);width:50%;height:2px;background:linear-gradient(90deg,transparent,var(--gold),transparent)}
select{
width:100%;
height:48px;
padding:0 40px 0 16px;
background:var(--bg);
border:1px solid var(--border);
border-radius:10px;
color:var(--text);
font-size:15px;
font-family:'DM Sans',sans-serif;
line-height:48px;
transition:.3s;
outline:none;
appearance:none;
-webkit-appearance:none;
-moz-appearance:none;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23475569' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E");
background-repeat:no-repeat;
background-position:right 16px center;
cursor:pointer;
text-indent:1px;
}
select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(16,185,129,.1)}
.select-group{position:relative;margin-bottom:20px}
/* Main App */
#mainApp{display:none;min-height:100vh}
.sidebar{position:fixed;left:0;top:0;bottom:0;width:260px;background:var(--card);border-right:1px solid var(--border);z-index:50;display:flex;flex-direction:column;transition:.3s;box-shadow:2px 0 10px rgba(0,0,0,.02)}
.sidebar-logo{padding:24px 20px;border-bottom:1px solid var(--border)}
.sidebar-nav{flex:1;padding:16px 12px;overflow-y:auto}
.nav-item{display:flex;align-items:center;gap:12px;padding:11px 16px;border-radius:10px;color:var(--text2);cursor:pointer;transition:.2s;margin-bottom:4px;font-size:14px;font-weight:500}
.nav-item:hover{background:rgba(16,185,129,.06);color:var(--text)}
.nav-item.active{background:rgba(16,185,129,.1);color:var(--accent);font-weight:600}
.nav-item i{width:20px;text-align:center;font-size:15px}
.sidebar-footer{padding:16px 20px;border-top:1px solid var(--border)}
.content{margin-left:260px;padding:32px;min-height:100vh}
.page{display:none}
.page.active{display:block}
/* Cards */
.stat-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:24px;transition:.3s;position:relative;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.02)}
.stat-card:hover{border-color:var(--border2);transform:translateY(-2px);box-shadow:0 8px 30px rgba(0,0,0,.06)}
.stat-card .stat-icon{width:48px;height:48px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:20px;margin-bottom:16px}
.stat-card .stat-value{font-family:'Outfit',sans-serif;font-size:32px;font-weight:800;margin-bottom:4px}
.stat-card .stat-label{color:var(--text2);font-size:13px;font-weight:500}
.data-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.02)}
.data-card .card-header{padding:20px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:between;gap:16px}
.data-card .card-body{padding:24px}
/* Table */
.table-wrap{overflow-x:auto;border-radius:8px}
table{width:100%;border-collapse:collapse}
thead{background:var(--bg2)}
th{padding:12px 16px;text-align:left;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text2);border-bottom:1px solid var(--border);white-space:nowrap}
td{padding:12px 16px;border-bottom:1px solid var(--border);font-size:14px;vertical-align:middle}
tbody tr{transition:.15s}
tbody tr:hover{background:rgba(16,185,129,.03)}
tfoot{background:var(--bg2);font-weight:700}
tfoot td{border-bottom:none;border-top:2px solid var(--border2)}
/* Progress Bar */
.progress-wrap{width:100%;height:6px;background:var(--bg2);border-radius:3px;overflow:hidden;margin:16px 0}
.progress-bar{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--accent),var(--gold));transition:width .3s ease;width:0%}
/* Toast */
.toast-container{position:fixed;top:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px}
.toast{padding:14px 20px;border-radius:10px;font-size:14px;font-weight:500;animation:toastIn .4s ease;display:flex;align-items:center;gap:10px;min-width:280px;box-shadow:0 10px 30px rgba(0,0,0,.08)}
.toast-success{background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.3);color:var(--accent2)}
.toast-error{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:var(--danger2)}
.toast-info{background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.3);color:var(--info)}
.toast-out{animation:toastOut .3s ease forwards}
@keyframes toastIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}}
@keyframes toastOut{to{opacity:0;transform:translateX(40px)}}
/* Modal */
.modal-overlay{position:fixed;inset:0;background:rgba(15,23,42,.4);backdrop-filter:blur(4px);z-index:100;display:none;align-items:center;justify-content:center}
.modal-overlay.show{display:flex}
.modal{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:32px;width:440px;max-width:90vw;box-shadow:0 25px 60px rgba(0,0,0,.1);animation:modalIn .3s ease}
@keyframes modalIn{from{opacity:0;transform:scale(.95) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}
/* Tingkatan Cards */
.tingkatan-card{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:32px;text-align:center;transition:.3s;cursor:default;position:relative;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.02)}
.tingkatan-card:hover{border-color:var(--accent);transform:translateY(-4px);box-shadow:0 12px 40px rgba(16,185,129,.1)}
.tingkatan-card .tk-num{font-family:'Outfit',sans-serif;font-size:48px;font-weight:900;background:linear-gradient(135deg,var(--accent),var(--gold));-webkit-background-clip:text;-webkit-text-fill-color:transparent;line-height:1.1}
.tingkatan-card .tk-count{font-family:'Outfit',sans-serif;font-size:28px;font-weight:700;margin-top:12px}
/* Badge */
.badge{display:inline-flex;align-items:center;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600}
.badge-success{background:rgba(16,185,129,.15);color:var(--accent2)}
.badge-warning{background:rgba(245,158,11,.15);color:var(--gold2)}
.badge-danger{background:rgba(239,68,68,.15);color:var(--danger2)}
.badge-muted{background:rgba(148,163,184,.15);color:var(--text2)}
/* Mark Input - Border Hitam */
.mark-input{
width:80px;
padding:8px 10px;
background:#fff;
border:1px solid #000000;
border-radius:8px;
color:var(--text);
font-size:14px;
text-align:center;
font-family:'DM Sans',sans-serif;
outline:none;
transition:.3s
}
.mark-input:focus{
border-color:#000000;
box-shadow:0 0 0 3px rgba(0,0,0,.1)
}
/* Subject column in results */
.subj-cell{text-align:center;white-space:normal;word-wrap:break-word;vertical-align:middle;max-width:140px}
.subj-mark{font-weight:600;font-size:14px}
.subj-grade{font-size:12px;font-weight:600;margin-top:2px}
/* Pagination */
.pagination-btn{min-width:36px;text-align:center;justify-content:center}
/* Exam Slip Styles */
.slip-container{max-width:800px;margin:0 auto;background:#fff;padding:40px;border:1px solid #ddd;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.05)}
.slip-header{display:flex;align-items:center;justify-content:space-between;border-bottom:3px solid #000;padding-bottom:16px;margin-bottom:24px}
.slip-school-info{text-align:center;flex:1}
.slip-school-name{font-size:20px;font-weight:800;text-transform:uppercase}
.slip-exam-name{font-size:14px;font-weight:600;text-transform:uppercase;color:var(--text2)}
.slip-logo{width:80px;height:80px;border-radius:50%;object-fit:contain}
.slip-section{margin-bottom:24px}
.slip-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 24px}
.slip-grid-item{display:flex;gap:8px}
.slip-grid-item span:first-child{font-weight:600;color:var(--text2);min-width:120px}
.slip-table{width:100%;border:1px solid #ccc;margin-bottom:24px}
.slip-table th{background:var(--bg2);border:1px solid #ccc;padding:8px;text-align:left}
.slip-table td{border:1px solid #ccc;padding:8px}
.slip-summary{display:grid;grid-template-columns:1fr 1fr;gap:8px 24px;margin-bottom:24px}
.slip-summary-item{display:flex;gap:8px}
.slip-summary-item span:first-child{font-weight:600;color:var(--text2);min-width:150px}
.slip-ulasan{width:100%;border:1px solid #ccc;border-radius:6px;padding:12px;font-size:14px;outline:none;min-height:80px;margin-bottom:24px}
.slip-signature{display:flex;justify-content:space-between;margin-top:40px}
.slip-sign-box{text-align:center;width:48%}
.slip-sign-line{border-top:1px solid #000;margin-bottom:4px}
/* Print CSS for A4 Fit Width */
@media print {
body * { visibility: hidden; }
.slip-container, .slip-container * { visibility: visible; }
.slip-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
border: none;
box-shadow: none;
padding: 0;
margin: 0;
}
.no-print { display: none !important; }
@page {
size: A4 portrait;
margin: 10mm;
}
table { width: 100% !important; }
.slip-logo {
border-radius: 0;
object-fit: contain;
width: 80px;
height: 80px;
}
}
/* Responsive */
@media(max-width:768px){
.sidebar{width:60px}.sidebar .nav-text,.sidebar .logo-text,.sidebar .footer-text,.sidebar .session-info{display:none}
.sidebar .nav-item{justify-content:center;padding:12px}
.sidebar .nav-item i{width:auto;font-size:18px}
.content{margin-left:60px;padding:16px}
.stat-card .stat-value{font-size:24px}
}
/* Animations */
@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
.fade-in{animation:fadeIn .4s ease}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
.loading-pulse{animation:pulse 1.5s ease-in-out infinite}
/* Edit input inline */
.edit-input{padding:6px 10px;background:#fff;border:1px solid var(--accent);border-radius:6px;color:var(--text);font-size:14px;font-family:'DM Sans',sans-serif;outline:none;width:100%}
</style>
</head>
<body>
<!-- Toast Container -->
<div class="toast-container" id="toastContainer"></div>
<!-- Confirmation Modal -->
<div class="modal-overlay" id="confirmModal">
<div class="modal">
<h3 class="text-lg font-bold mb-3" id="confirmTitle">Sahkan</h3>
<p class="text-sm mb-6" style="color:var(--text2)" id="confirmMsg">Adakah anda pasti?</p>
<div class="flex gap-3 justify-end">
<button class="btn btn-outline btn-sm" onclick="closeConfirm()">Batal</button>
<button class="btn btn-danger btn-sm" id="confirmBtn" onclick="confirmAction()">Sahkan</button>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal-overlay" id="editModal">
<div class="modal">
<h3 class="text-lg font-bold mb-4" id="editTitle">Kemaskini</h3>
<div id="editBody"></div>
<div class="flex gap-3 justify-end mt-6">
<button class="btn btn-outline btn-sm" onclick="closeEdit()">Batal</button>
<button class="btn btn-primary btn-sm" id="editSaveBtn">Simpan</button>
</div>
</div>
</div>
<!-- Login Page -->
<div id="loginPage">
<div class="login-bg"></div>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="orb orb-3"></div>
<div class="login-card">
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl mb-4" style="background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.2)">
<i class="fas fa-chart-line text-2xl" style="color:var(--accent)"></i>
</div>
<h1 class="text-2xl font-extrabold" style="font-family:'Outfit'">SISTEM ANALISIS<br>PEPERIKSAAN</h1>
<p class="text-sm mt-2" style="color:var(--text2)">Sila log masuk untuk meneruskan</p>
</div>
<div class="input-group">
<i class="fas fa-envelope"></i>
<input type="email" id="loginEmail" placeholder="Alamat e-mel" autocomplete="off">
</div>
<div class="input-group">
<i class="fas fa-lock"></i>
<input type="password" id="loginPassword" placeholder="Kata laluan">
</div>
<button class="btn btn-primary w-full justify-center text-base py-3" onclick="handleLogin()" id="loginBtn">
<i class="fas fa-sign-in-alt"></i> Log Masuk
</button>
<p class="text-center text-xs mt-6" style="color:var(--muted)">Hak cipta terpelihara © 2025</p>
</div>
</div>
<!-- Year & Exam Type Page -->
<div id="yearExamPage" style="display:none">
<div class="year-card">
<div class="text-center mb-8">
<h2 class="text-xl font-extrabold" style="font-family:'Outfit'">Pilih Sesi Peperiksaan</h2>
<p class="text-sm mt-2" style="color:var(--text2)">Pilih tahun dan jenis peperiksaan</p>
</div>
<!-- Admin Only: Tambah Tahun -->
<div id="adminYearSection" style="display:none" class="mb-6">
<label class="text-xs font-semibold uppercase tracking-wider mb-2 block" style="color:var(--text2)">Tambah Tahun Baru</label>
<div class="flex gap-2">
<div class="input-group flex-1 mb-0">
<i class="fas fa-calendar"></i>
<input type="text" id="newYearInput" placeholder="Cth: 2025">
</div>
<button class="btn btn-primary btn-sm" onclick="addYear()"><i class="fas fa-plus"></i> Tambah</button>
</div>
</div>
<!-- Admin Only: Tambah Jenis Peperiksaan -->
<div id="adminExamSection" style="display:none" class="mb-6">
<label class="text-xs font-semibold uppercase tracking-wider mb-2 block" style="color:var(--text2)">Tambah Jenis Peperiksaan</label>
<div class="flex gap-2">
<div class="input-group flex-1 mb-0">
<i class="fas fa-file-alt"></i>
<input type="text" id="newExamInput" placeholder="Cth: UASA">
</div>
<button class="btn btn-primary btn-sm" onclick="addExamType()"><i class="fas fa-plus"></i> Tambah</button>
</div>
</div>
<div class="select-group">
<select id="yearSelect"><option value="">-- Pilih Tahun --</option></select>
</div>
<div class="select-group">
<select id="examSelect"><option value="">-- Pilih Jenis Peperiksaan --</option></select>
</div>
<button class="btn btn-gold w-full justify-center text-base py-3 mt-4" onclick="enterSession()">
<i class="fas fa-arrow-right"></i> Masuk
</button>
<div class="text-center mt-4">
<button class="btn btn-outline btn-sm" onclick="backToLogin()"><i class="fas fa-arrow-left"></i> Kembali</button>
</div>
</div>
</div>
<!-- Main App -->
<div id="mainApp">
<aside class="sidebar">
<div class="sidebar-logo">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-lg flex items-center justify-center" style="background:rgba(16,185,129,.12)">
<i class="fas fa-chart-line text-sm" style="color:var(--accent)"></i>
</div>
<div class="logo-text">
<div class="text-sm font-bold" style="font-family:'Outfit';color:var(--text)">SAP</div>
<div class="text-xs" style="color:var(--muted)">Analisis Peperiksaan</div>
</div>
</div>
</div>
<nav class="sidebar-nav">
<div class="nav-item active" data-page="dashboard" onclick="navigateTo('dashboard')">
<i class="fas fa-th-large"></i><span class="nav-text">Dashboard</span>
</div>
<div class="nav-item" data-page="students" onclick="navigateTo('students')">
<i class="fas fa-user-graduate"></i><span class="nav-text">Daftar Murid</span>
</div>
<div class="nav-item" data-page="classes" onclick="navigateTo('classes')">
<i class="fas fa-chalkboard"></i><span class="nav-text">Kelas</span>
</div>
<div class="nav-item" data-page="tingkatan" onclick="navigateTo('tingkatan')">
<i class="fas fa-layer-group"></i><span class="nav-text">Tingkatan</span>
</div>
<div class="nav-item" data-page="subjects" onclick="navigateTo('subjects')">
<i class="fas fa-book"></i><span class="nav-text">Mata Pelajaran</span>
</div>
<div class="nav-item" data-page="marks" onclick="navigateTo('marks')">
<i class="fas fa-pen-alt"></i><span class="nav-text">Masuk Markah</span>
</div>
<div class="nav-item" data-page="results" onclick="navigateTo('results')">
<i class="fas fa-poll"></i><span class="nav-text">Keputusan Kelas</span>
</div>
<div class="nav-item" data-page="tingkatan-results" onclick="navigateTo('tingkatan-results')">
<i class="fas fa-trophy"></i><span class="nav-text">Keputusan Tingkatan</span>
</div>
<div class="nav-item" data-page="ranking" onclick="navigateTo('ranking')">
<i class="fas fa-ranking-star"></i><span class="nav-text">Ranking Kelas</span>
</div>
<div class="nav-item" data-page="ranking-tingkatan" onclick="navigateTo('ranking-tingkatan')">
<i class="fas fa-medal"></i><span class="nav-text">Ranking Tingkatan</span>
</div>
<div class="nav-item" data-page="ranking-subject" onclick="navigateTo('ranking-subject')">
<i class="fas fa-book-bookmark"></i><span class="nav-text">Ranking Mata Pelajaran</span>
</div>
<div class="nav-item" data-page="analisis-subject" onclick="navigateTo('analisis-subject')">
<i class="fas fa-chart-pie"></i><span class="nav-text">Analisis Mata Pelajaran</span>
</div>
<div class="nav-item" data-page="analisis-tingkatan" onclick="navigateTo('analisis-tingkatan')">
<i class="fas fa-chart-bar"></i><span class="nav-text">Analisis Tingkatan</span>
</div>
<div class="nav-item" data-page="miss-a" onclick="navigateTo('miss-a')">
<i class="fas fa-tasks"></i><span class="nav-text">Miss 1-3 A</span>
</div>
<div class="nav-item" data-page="slip" onclick="navigateTo('slip')">
<i class="fas fa-file-alt"></i><span class="nav-text">Slip Peperiksaan</span>
</div>
</nav>
<div class="sidebar-footer">
<div class="session-info mb-3">
<div class="text-xs mb-1" style="color:var(--muted)">Sesi Semasa</div>
<div class="text-sm font-semibold" style="color:var(--gold2)" id="sidebarSession">-</div>
<div class="text-xs mt-1" style="color:var(--muted)">Log masuk sebagai: <span style="color:var(--text2)" id="sidebarRole">-</span></div>
</div>
<button class="btn btn-outline btn-sm w-full justify-center" onclick="handleLogout()">
<i class="fas fa-sign-out-alt"></i><span class="nav-text">Log Keluar</span>
</button>
</div>
</aside>
<div class="content">
<!-- Dashboard -->
<div class="page active" id="page-dashboard">
<div class="mb-8">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Dashboard</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Ringkasan sesi peperiksaan semasa</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 mb-8" id="dashStats"></div>
<div class="data-card" id="dashActivityCard">
<div class="card-header">
<h3 class="font-bold text-base" style="font-family:'Outfit'">Aktiviti Terkini</h3>
</div>
<div class="card-body" id="dashActivity"></div>
</div>
</div>
<!-- Daftar Murid / Kelas -->
<div class="page" id="page-students">
<div class="flex items-center justify-between flex-wrap gap-4 mb-6">
<div>
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Daftar Murid / Kelas</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Import, tambah, kemaskini atau padam murid</p>
</div>
<div class="flex gap-2 flex-wrap">
<label class="btn btn-primary btn-sm cursor-pointer"><i class="fas fa-file-excel"></i> Import Excel<input type="file" accept=".xlsx,.xls,.csv" onchange="importExcel(event)" class="hidden"></label>
<button class="btn btn-gold btn-sm" onclick="showAddStudentModal()"><i class="fas fa-user-plus"></i> Tambah Murid</button>
<button class="btn btn-danger btn-sm" onclick="confirmDeleteAllStudents()"><i class="fas fa-trash-alt"></i> Padam Semua</button>
</div>
</div>
<div class="progress-wrap" id="studentsProgress" style="display:none"><div class="progress-bar" id="studentsProgressBar"></div></div>
<div class="data-card">
<div class="card-body p-0">
<div class="table-wrap">
<table>
<thead><tr><th>Bil</th><th>Nama Murid</th><th>Kelas</th><th>Tindakan</th></tr></thead>
<tbody id="studentsTableBody"></tbody>
</table>
</div>
<div class="p-4 text-center text-sm" style="color:var(--muted);display:none" id="studentsEmpty">Tiada murid berdaftar</div>
</div>
</div>
</div>
<!-- Kelas -->
<div class="page" id="page-classes">
<div class="flex items-center justify-between flex-wrap gap-4 mb-6">
<div>
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Senarai Kelas</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Kelas yang telah didaftar bersama bilangan murid</p>
</div>
<button class="btn btn-danger btn-sm" onclick="confirmDeleteAllClasses()"><i class="fas fa-trash-alt"></i> Padam Semua Kelas</button>
</div>
<div class="progress-wrap" id="classesProgress" style="display:none"><div class="progress-bar" id="classesProgressBar"></div></div>
<div class="data-card">
<div class="card-body p-0">
<div class="table-wrap">
<table>
<thead><tr><th>Bil</th><th>Nama Kelas</th><th>Bilangan Murid</th></tr></thead>
<tbody id="classesTableBody"></tbody>
</table>
</div>
<div class="p-4 text-center text-sm" style="color:var(--muted);display:none" id="classesEmpty">Tiada kelas berdaftar</div>
</div>
</div>
</div>
<!-- Tingkatan -->
<div class="page" id="page-tingkatan">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Tingkatan</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Paparan kad mengikut tingkatan</p>
</div>
<div class="progress-wrap" id="tingkatanProgress" style="display:none"><div class="progress-bar" id="tingkatanProgressBar"></div></div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6" id="tingkatanCards"></div>
</div>
<!-- Daftar Mata Pelajaran -->
<div class="page" id="page-subjects">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Daftar Mata Pelajaran</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Daftar, kemaskini atau padam mata pelajaran</p>
</div>
<div class="data-card mb-6">
<div class="card-header"><h3 class="font-bold text-sm" style="font-family:'Outfit'">Tambah Mata Pelajaran Baru</h3></div>
<div class="card-body">
<div class="flex gap-3 flex-wrap">
<div class="flex-1" style="min-width:200px">
<input type="text" id="newSubjectName" placeholder="Nama Mata Pelajaran" class="edit-input w-full" style="padding:12px 14px;border-radius:10px">
</div>
<div style="width:180px">
<input type="text" id="newSubjectCode" placeholder="Kod (opcional)" class="edit-input w-full" style="padding:12px 14px;border-radius:10px">
</div>
<button class="btn btn-primary btn-sm" onclick="addSubject()"><i class="fas fa-plus"></i> Tambah</button>
</div>
</div>
</div>
<div class="data-card">
<div class="card-body p-0">
<div class="table-wrap">
<table>
<thead><tr><th>Bil</th><th>Nama Mata Pelajaran</th><th>Kod</th><th>Tindakan</th></tr></thead>
<tbody id="subjectsTableBody"></tbody>
</table>
</div>
<div class="p-4 text-center text-sm" style="color:var(--muted);display:none" id="subjectsEmpty">Tiada mata pelajaran berdaftar</div>
</div>
</div>
</div>
<!-- Masuk Markah -->
<div class="page" id="page-marks">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Masuk Markah</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Input markah murid mengikut kelas dan mata pelajaran (Tip: Taip 'TH' untuk Tidak Hadir)</p>
</div>
<div class="data-card mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="select-group mb-0">
<select id="marksTingkatan" onchange="onMarksTingkatanChange()"><option value="">-- Pilih Tingkatan --</option><option value="1">Tingkatan 1</option><option value="2">Tingkatan 2</option><option value="3">Tingkatan 3</option></select>
</div>
<div class="select-group mb-0">
<select id="marksKelas" onchange="onMarksKelasChange()"><option value="">-- Pilih Kelas --</option></select>
</div>
<div class="select-group mb-0">
<select id="marksSubject" onchange="onMarksSubjectChange()"><option value="">-- Pilih Mata Pelajaran --</option></select>
</div>
</div>
</div>
</div>
<div id="marksTableArea" style="display:none">
<div class="flex justify-between flex-wrap gap-2 mb-4">
<div class="flex gap-2 flex-wrap" id="marksAdminActions" style="display:none;">
<button class="btn btn-danger btn-sm" onclick="generateRandomMarks()"><i class="fas fa-dice"></i> Jana Markah Rawak</button>
<button class="btn btn-danger btn-sm" onclick="deleteAllMarks()"><i class="fas fa-trash-alt"></i> Padam Semua Markah</button>
</div>
<button class="btn btn-primary" onclick="saveMarks()"><i class="fas fa-save"></i> Simpan Markah</button>
</div>
<div class="data-card">
<div class="card-body p-0">
<div class="table-wrap">
<table>
<thead><tr><th>Bil</th><th>Nama Murid</th><th>Markah</th><th>Gred</th><th>Status</th></tr></thead>
<tbody id="marksTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="text-center py-12" style="color:var(--muted)" id="marksPlaceholder">
<i class="fas fa-pen-alt text-4xl mb-4 block" style="opacity:.3"></i>
<p>Tiada Pilihan Dibuat</p>
<p style="font-size: 13px; margin-top: 4px;">Pilih kelas dan mata pelajaran di atas untuk mula memasukkan markah.</p>
</div>
</div>
<!-- Keputusan Kelas -->
<div class="page" id="page-results">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Keputusan Kelas</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Keputusan peperiksaan mengikut kelas</p>
</div>
<div class="data-card mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="select-group mb-0">
<select id="resultsTingkatan" onchange="onResultsTingkatanChange()"><option value="">-- Pilih Tingkatan --</option><option value="1">Tingkatan 1</option><option value="2">Tingkatan 2</option><option value="3">Tingkatan 3</option></select>
</div>
<div class="select-group mb-0">
<select id="resultsKelas" onchange="onResultsKelasChange()"><option value="">-- Pilih Kelas --</option></select>
</div>
</div>
</div>
</div>
<div class="progress-wrap" id="resultsProgress" style="display:none"><div class="progress-bar" id="resultsProgressBar"></div></div>
<div id="resultsTableArea" style="display:none">
<div class="data-card">
<div class="card-header">
<h3 class="font-bold text-sm" style="font-family:'Outfit'" id="resultsTitle">Senarai Murid — Markah & Gred</h3>
</div>
<div class="card-body p-0">
<div class="table-wrap">
<table id="resultsTable">
<thead id="resultsThead"></thead>
<tbody id="resultsTbody"></tbody>
</table>
</div>
<div class="p-4 text-sm" style="color:var(--text2);border-top:1px solid var(--border)" id="resultsFooter"></div>
</div>
</div>
</div>
<div class="text-center py-12" style="color:var(--muted)" id="resultsPlaceholder">
<i class="fas fa-poll text-4xl mb-4 block" style="opacity:.3"></i>
<p>Sila pilih tingkatan dan kelas</p>
</div>
</div>
<!-- Keputusan Tingkatan -->
<div class="page" id="page-tingkatan-results">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Keputusan Tingkatan</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Keputusan peperiksaan keseluruhan murid mengikut tingkatan</p>
</div>
<div class="data-card mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="select-group mb-0">
<select id="trTingkatan" onchange="onTingkatanResultsChange()">
<option value="">-- Pilih Tingkatan --</option>
<option value="1">Tingkatan 1</option>
<option value="2">Tingkatan 2</option>
<option value="3">Tingkatan 3</option>
</select>
</div>
</div>
</div>
</div>
<div class="progress-wrap" id="trProgress" style="display:none"><div class="progress-bar" id="trProgressBar"></div></div>
<div id="trTableArea" style="display:none">
<div class="data-card">
<div class="card-header">
<h3 class="font-bold text-sm" style="font-family:'Outfit'" id="trTitle">Senarai Murid — Markah & Gred</h3>
</div>
<div class="card-body p-0">
<div class="table-wrap">
<table id="trTable">
<thead id="trThead"></thead>
<tbody id="trTbody"></tbody>
</table>
</div>
<div class="p-4 text-sm" style="color:var(--text2);border-top:1px solid var(--border)" id="trFooter"></div>
</div>
</div>
<div class="flex justify-center items-center gap-2 mt-6 flex-wrap" id="trPagination"></div>
</div>
<div class="text-center py-12" style="color:var(--muted)" id="trPlaceholder">
<i class="fas fa-trophy text-4xl mb-4 block" style="opacity:.3"></i>
<p>Sila pilih tingkatan</p>
</div>
</div>
<!-- Ranking Kelas -->
<div class="page" id="page-ranking">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Ranking Kelas</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Kedudukan murid berdasarkan Gred Purata Murid (GPM)</p>
</div>
<div class="data-card mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="select-group mb-0">
<select id="rankingTingkatan" onchange="onRankingKelasTingkatanChange()"><option value="">-- Pilih Tingkatan --</option><option value="1">Tingkatan 1</option><option value="2">Tingkatan 2</option><option value="3">Tingkatan 3</option></select>
</div>
<div class="select-group mb-0">
<select id="rankingKelas" onchange="onRankingKelasChange()"><option value="">-- Pilih Kelas --</option></select>
</div>
</div>
</div>
</div>
<div class="progress-wrap" id="rankingProgress" style="display:none"><div class="progress-bar" id="rankingProgressBar"></div></div>
<div id="rankingTableArea" style="display:none">
<div class="data-card">
<div class="card-header">
<h3 class="font-bold text-sm" style="font-family:'Outfit'" id="rankingTitle">Kedudukan Murid</h3>
</div>
<div class="card-body p-0">
<div class="table-wrap">
<table id="rankingTable">
<thead>
<tr>
<th>Kedudukan</th>
<th>Nama Murid</th>
<th class="subj-cell">GPM</th>
<th class="subj-cell">Jumlah Markah</th>
<th>Ringkasan Gred</th>
</tr>
</thead>
<tbody id="rankingTbody"></tbody>
</table>
</div>
<div class="p-4 text-sm" style="color:var(--text2);border-top:1px solid var(--border)" id="rankingFooter"></div>
</div>
</div>
</div>
<div class="text-center py-12" style="color:var(--muted)" id="rankingPlaceholder">
<i class="fas fa-ranking-star text-4xl mb-4 block" style="opacity:.3"></i>
<p>Sila pilih tingkatan dan kelas</p>
</div>
</div>
<!-- Ranking Tingkatan -->
<div class="page" id="page-ranking-tingkatan">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Ranking Tingkatan</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Kedudukan keseluruhan murid dalam tingkatan berdasarkan GPM</p>
</div>
<div class="data-card mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="select-group mb-0">
<select id="rtTingkatan" onchange="onRankingTingkatanChange()">
<option value="">-- Pilih Tingkatan --</option>
<option value="1">Tingkatan 1</option>
<option value="2">Tingkatan 2</option>
<option value="3">Tingkatan 3</option>
</select>
</div>
</div>
</div>
</div>
<div class="progress-wrap" id="rtProgress" style="display:none"><div class="progress-bar" id="rtProgressBar"></div></div>
<div id="rtTableArea" style="display:none">
<div class="data-card">
<div class="card-header">
<h3 class="font-bold text-sm" style="font-family:'Outfit'" id="rtTitle">Kedudukan Murid Tingkatan</h3>
</div>
<div class="card-body p-0">
<div class="table-wrap">
<table id="rtTable">
<thead>
<tr>
<th>Kedudukan</th>
<th>Nama Murid</th>
<th>Kelas</th>
<th class="subj-cell">GPM</th>
<th class="subj-cell">Jumlah Markah</th>
<th>Ringkasan Gred</th>
</tr>
</thead>
<tbody id="rtTbody"></tbody>
</table>
</div>
<div class="p-4 text-sm" style="color:var(--text2);border-top:1px solid var(--border)" id="rtFooter"></div>
</div>
</div>
</div>
<div class="text-center py-12" style="color:var(--muted)" id="rtPlaceholder">
<i class="fas fa-medal text-4xl mb-4 block" style="opacity:.3"></i>
<p>Sila pilih tingkatan</p>
</div>
</div>
<!-- Ranking Mata Pelajaran -->
<div class="page" id="page-ranking-subject">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Ranking Mata Pelajaran</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Kedudukan murid dalam tingkatan berdasarkan markah mata pelajaran</p>
</div>
<div class="data-card mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="select-group mb-0">
<select id="rsTingkatan" onchange="onRankingSubjectTingkatanChange()">
<option value="">-- Pilih Tingkatan --</option>
<option value="1">Tingkatan 1</option>
<option value="2">Tingkatan 2</option>
<option value="3">Tingkatan 3</option>
</select>
</div>
<div class="select-group mb-0">
<select id="rsSubject" onchange="onRankingSubjectChange()">
<option value="">-- Pilih Mata Pelajaran --</option>
</select>
</div>
</div>
</div>
</div>
<div class="progress-wrap" id="rsProgress" style="display:none"><div class="progress-bar" id="rsProgressBar"></div></div>
<div id="rsTableArea" style="display:none">
<div class="data-card">
<div class="card-header">
<h3 class="font-bold text-sm" style="font-family:'Outfit'" id="rsTitle">Kedudukan Murid</h3>
</div>
<div class="card-body p-0">
<div class="table-wrap">
<table id="rsTable">
<thead>
<tr>
<th>Kedudukan</th>
<th>Nama Murid</th>
<th>Kelas</th>
<th class="subj-cell">Markah</th>
<th class="subj-cell">Gred</th>
</tr>
</thead>
<tbody id="rsTbody"></tbody>
</table>
</div>
<div class="p-4 text-sm" style="color:var(--text2);border-top:1px solid var(--border)" id="rsFooter"></div>
</div>
</div>
</div>
<div class="text-center py-12" style="color:var(--muted)" id="rsPlaceholder">
<i class="fas fa-book-bookmark text-4xl mb-4 block" style="opacity:.3"></i>
<p>Sila pilih tingkatan dan mata pelajaran</p>
</div>
</div>
<!-- Analisis Mata Pelajaran -->
<div class="page" id="page-analisis-subject">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Analisis Mata Pelajaran</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Analisis pencapaian murid mengikut kelas bagi setiap mata pelajaran</p>
</div>
<div class="data-card mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="select-group mb-0">
<select id="asTingkatan" onchange="onAnalisisSubjectTingkatanChange()">
<option value="">-- Pilih Tingkatan --</option>
<option value="1">Tingkatan 1</option>
<option value="2">Tingkatan 2</option>
<option value="3">Tingkatan 3</option>
</select>
</div>
<div class="select-group mb-0">
<select id="asSubject" onchange="onAnalisisSubjectChange()">
<option value="">-- Pilih Mata Pelajaran --</option>
</select>
</div>
</div>
</div>
</div>
<div class="progress-wrap" id="asProgress" style="display:none"><div class="progress-bar" id="asProgressBar"></div></div>
<div id="asTableArea" style="display:none">
<div class="data-card">
<div class="card-header">
<h3 class="font-bold text-sm" style="font-family:'Outfit'" id="asTitle">Analisis Pencapaian</h3>
</div>
<div class="card-body p-0">
<div class="table-wrap">
<table id="asTable" style="min-width: 1000px;">
<thead>
<tr>
<th rowspan="2">Kelas</th>
<th colspan="2" class="subj-cell">A</th>
<th colspan="2" class="subj-cell">B</th>
<th colspan="2" class="subj-cell">C</th>
<th colspan="2" class="subj-cell">D</th>
<th colspan="2" class="subj-cell">E</th>
<th colspan="2" class="subj-cell">F</th>
<th colspan="2" class="subj-cell">TH</th>
<th rowspan="2" class="subj-cell">Bil. Hadir</th>
<th rowspan="2" class="subj-cell">Gred Purata</th>
</tr>
<tr>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
</tr>
</thead>
<tbody id="asTbody"></tbody>
<tfoot id="asTfoot"></tfoot>
</table>
</div>
</div>
</div>
</div>
<div class="text-center py-12" style="color:var(--muted)" id="asPlaceholder">
<i class="fas fa-chart-pie text-4xl mb-4 block" style="opacity:.3"></i>
<p>Sila pilih tingkatan dan mata pelajaran</p>
</div>
</div>
<!-- Analisis Tingkatan -->
<div class="page" id="page-analisis-tingkatan">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Analisis Tingkatan</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Analisis pencapaian murid mengikut mata pelajaran bagi setiap tingkatan</p>
</div>
<div class="data-card mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="select-group mb-0">
<select id="atTingkatan" onchange="onAnalisisTingkatanChange()">
<option value="">-- Pilih Tingkatan --</option>
<option value="1">Tingkatan 1</option>
<option value="2">Tingkatan 2</option>
<option value="3">Tingkatan 3</option>
</select>
</div>
</div>
</div>
</div>
<div class="progress-wrap" id="atProgress" style="display:none"><div class="progress-bar" id="atProgressBar"></div></div>
<div id="atTableArea" style="display:none">
<div class="data-card">
<div class="card-header">
<h3 class="font-bold text-sm" style="font-family:'Outfit'" id="atTitle">Analisis Pencapaian</h3>
</div>
<div class="card-body p-0">
<div class="table-wrap">
<table id="atTable" style="min-width: 1000px;">
<thead>
<tr>
<th rowspan="2">Mata Pelajaran</th>
<th colspan="2" class="subj-cell">A</th>
<th colspan="2" class="subj-cell">B</th>
<th colspan="2" class="subj-cell">C</th>
<th colspan="2" class="subj-cell">D</th>
<th colspan="2" class="subj-cell">E</th>
<th colspan="2" class="subj-cell">F</th>
<th colspan="2" class="subj-cell">TH</th>
<th rowspan="2" class="subj-cell">Bil. Hadir</th>
<th rowspan="2" class="subj-cell">Gred Purata</th>
</tr>
<tr>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
<th class="subj-cell">Bil</th><th class="subj-cell">%</th>
</tr>
</thead>
<tbody id="atTbody"></tbody>
<tfoot id="atTfoot"></tfoot>
</table>
</div>
</div>
</div>
</div>
<div class="text-center py-12" style="color:var(--muted)" id="atPlaceholder">
<i class="fas fa-chart-bar text-4xl mb-4 block" style="opacity:.3"></i>
<p>Sila pilih tingkatan</p>
</div>
</div>
<!-- Miss 1-3 A -->
<div class="page" id="page-miss-a">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Miss 1-3 A</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Senarai murid yang mempunyai 1 hingga 3 gred bukan A</p>
</div>
<div class="data-card mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="select-group mb-0">
<select id="maTingkatan" onchange="onMissATingkatanChange()">
<option value="">-- Pilih Tingkatan --</option>
<option value="1">Tingkatan 1</option>
<option value="2">Tingkatan 2</option>
<option value="3">Tingkatan 3</option>
</select>
</div>
</div>
</div>
</div>
<div class="progress-wrap" id="maProgress" style="display:none"><div class="progress-bar" id="maProgressBar"></div></div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6" id="maStats" style="display:none"></div>
<div id="maTableArea" style="display:none">
<div class="data-card">
<div class="card-header">
<h3 class="font-bold text-sm" style="font-family:'Outfit'" id="maTitle">Senarai Murid Miss 1-3 A</h3>
</div>
<div class="card-body p-0">
<div class="table-wrap">
<table id="maTable">
<thead>
<tr>
<th>Bil</th>
<th>Nama Murid</th>
<th>Kelas</th>
<th class="subj-cell">Bil. Bukan A</th>
<th>Ringkasan Gred</th>
<th>Subjek Bukan A</th>
</tr>
</thead>
<tbody id="maTbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="text-center py-12" style="color:var(--muted)" id="maPlaceholder">
<i class="fas fa-tasks text-4xl mb-4 block" style="opacity:.3"></i>
<p>Sila pilih tingkatan</p>
</div>
</div>
<!-- Slip Peperiksaan -->
<div class="page" id="page-slip">
<div class="mb-6">
<h2 class="text-2xl font-extrabold" style="font-family:'Outfit'">Slip Peperiksaan</h2>
<p class="text-sm mt-1" style="color:var(--text2)">Pilih murid untuk menjana slip peperiksaan</p>
</div>
<div class="data-card mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="select-group mb-0">
<select id="slipTingkatan" onchange="onSlipTingkatanChange()"><option value="">-- Pilih Tingkatan --</option><option value="1">Tingkatan 1</option><option value="2">Tingkatan 2</option><option value="3">Tingkatan 3</option></select>
</div>
<div class="select-group mb-0">
<select id="slipKelas" onchange="onSlipKelasChange()"><option value="">-- Pilih Kelas --</option></select>
</div>
<div class="select-group mb-0">
<select id="slipStudent" onchange="onSlipStudentChange()"><option value="">-- Pilih Nama Murid --</option></select>
</div>
</div>
</div>
</div>
<div class="flex justify-end mb-4 no-print" id="slipActions" style="display:none">
<button class="btn btn-primary" onclick="window.print()"><i class="fas fa-print"></i> Cetak Slip</button>
</div>
<div id="slipArea" style="display:none"></div>
<div class="text-center py-12" style="color:var(--muted)" id="slipPlaceholder">
<i class="fas fa-file-alt text-4xl mb-4 block" style="opacity:.3"></i>
<p>Sila pilih tingkatan, kelas dan nama murid</p>
</div>
</div>
</div>
</div>
<script>
/* ===== CONSTANTS ===== */
const SUPABASE_URL='https://bnvejvqgdmjkdffcmvyv.supabase.co';
const SUPABASE_KEY='sb_publishable_blJ9sQA_0B9-zo2-dtcQww_Y2H-6gZP';
const EXCLUDED_SUBJECTS=['Pendidikan Seni Visual','Pendidikan Jasmani dan Kesihatan'];
const GRADE_SCALE={A:1,B:2,C:3,D:4,E:5,F:6,TH:7};
/* ===== STATE ===== */
let currentUser=null;
let selectedYear=null;
let selectedExamType=null;
let currentPage='dashboard';
let marksInputData={};
let existingMarksMap={};
let pendingConfirmCallback=null;
// State for Tingkatan Results Pagination
let trCurrentPage = 1;
const trPageSize = 15;
let trAllStudents = [];
let trActiveSubjects = [];
let trMarksMap = {};
// State for Ranking Tingkatan
let rtAllRankedStudents = [];
/* ===== SUPABASE ===== */
const db=window.supabase.createClient(SUPABASE_URL,SUPABASE_KEY);
/* ===== UTILITI ===== */
function getGrade(mark){
const mStr = String(mark).trim().toUpperCase();
if(mStr === 'TH') return 'TH';
const m=parseInt(mark);
if(isNaN(m)||m<0)return'F';
if(m>=82)return'A';if(m>=66)return'B';if(m>=50)return'C';
if(m>=35)return'D';if(m>=20)return'E';return'F';
}
function getGradeValue(g){return GRADE_SCALE[g]||7;}
function getTingkatan(cn){
if(!cn)return null;
const f=cn.trim().charAt(0);
if(f==='1')return'1';if(f==='2')return'2';if(f==='3')return'3';
return null;
}
function isExcluded(name){return EXCLUDED_SUBJECTS.some(e=>name.trim().toLowerCase()===e.toLowerCase());}
/* Helper for Student Stats */
function calculateStats(studentId, marksByStudent, subjectMap) {
const studentMarks = marksByStudent[studentId] || [];
let totalGradeVal = 0, totalMark = 0, subjCount = 0;
const gradeSummary = { A:0, B:0, C:0, D:0, E:0, F:0, TH:0 };
studentMarks.forEach(m => {
const subj = subjectMap[m.subject_id];
if (!subj) return;
// FIX: Skip excluded subjects entirely for summary calculations
if (!isExcluded(subj.name)) {
if (gradeSummary.hasOwnProperty(m.grade)) gradeSummary[m.grade]++;
if (m.grade !== 'TH') {
totalGradeVal += getGradeValue(m.grade);
totalMark += m.mark;
subjCount++;
}
}
});
const gpm = subjCount > 0 ? (totalGradeVal / subjCount) : 99.99;
const summary = Object.entries(gradeSummary).filter(([_,c])=>c>0).map(([g,c])=>c+g).join(' ')||'-';
return { gpm, totalMark, summary, subjCount, gradeSummary, totalSubjects: studentMarks.length };
}
/* Toast */
function showToast(msg,type='success'){
const c=document.getElementById('toastContainer');
const t=document.createElement('div');
const icons={success:'fa-check-circle',error:'fa-exclamation-circle',info:'fa-info-circle'};
t.className=`toast toast-${type}`;
t.innerHTML=`<i class="fas ${icons[type]||icons.info}"></i><span>${msg}</span>`;
c.appendChild(t);
setTimeout(()=>{t.classList.add('toast-out');setTimeout(()=>t.remove(),300);},3000);
}
/* Progress bar animasi */
function animateProgress(barId,duration=800){
return new Promise(resolve=>{
const bar=document.getElementById(barId);
if(!bar){resolve();return;}
bar.style.width='0%';
let start=null;
function step(ts){
if(!start)start=ts;
const p=Math.min(((ts-start)/duration)*100,100);
bar.style.width=p+'%';
if(p<100)requestAnimationFrame(step);else resolve();
}
requestAnimationFrame(step);
});
}
function showProgress(id){const el=document.getElementById(id);if(el)el.style.display='block';}
function hideProgress(id){const el=document.getElementById(id);if(el)el.style.display='none';}
/* Confirm modal */
function showConfirm(title,msg,callback){
document.getElementById('confirmTitle').textContent=title;
document.getElementById('confirmMsg').textContent=msg;
document.getElementById('confirmModal').classList.add('show');
pendingConfirmCallback=callback;
}
function closeConfirm(){document.getElementById('confirmModal').classList.remove('show');pendingConfirmCallback=null;}
function confirmAction(){if(pendingConfirmCallback)pendingConfirmCallback();closeConfirm();}
/* Edit modal */
function showEdit(title,bodyHtml,saveCallback){
document.getElementById('editTitle').textContent=title;
document.getElementById('editBody').innerHTML=bodyHtml;
document.getElementById('editModal').classList.add('show');
document.getElementById('editSaveBtn').onclick=saveCallback;
}
function closeEdit(){document.getElementById('editModal').classList.remove('show');}
/* ===== AUTH ===== */
async function handleLogin(){
const email=document.getElementById('loginEmail').value.trim();
const pass=document.getElementById('loginPassword').value.trim();
if(!email||!pass){showToast('Sila masuk e-mel dan kata laluan','error');return;}
const btn=document.getElementById('loginBtn');
btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Menyemak...';
try{
const{data,error}=await db.from('users').select('*').eq('email',email).eq('password',pass).single();
if(error||!data){
if(email==='admin@sekolah.com'&&pass==='admin123'){currentUser={email,role:'admin'};}
else if(email==='user@sekolah.com'&&pass==='user123'){currentUser={email,role:'user'};}
else{throw new Error('Kredensial tidak sah');}
}else{currentUser={email:data.email,role:data.role};}
showToast('Log masuk berjaya!');
document.getElementById('loginPage').style.display='none';
document.getElementById('yearExamPage').style.display='flex';
if(currentUser.role==='admin'){
document.getElementById('adminYearSection').style.display='block';
document.getElementById('adminExamSection').style.display='block';
} else {
document.getElementById('adminYearSection').style.display='none';
document.getElementById('adminExamSection').style.display='none';
}
await loadYearsAndExamTypes();
}catch(e){showToast(e.message||'Log masuk gagal','error');}
btn.disabled=false;btn.innerHTML='<i class="fas fa-sign-in-alt"></i> Log Masuk';
}
function backToLogin(){
document.getElementById('yearExamPage').style.display='none';
document.getElementById('loginPage').style.display='flex';
document.getElementById('loginEmail').value='';document.getElementById('loginPassword').value='';
document.getElementById('adminYearSection').style.display='none';
document.getElementById('adminExamSection').style.display='none';
}
function handleLogout(){
currentUser=null;selectedYear=null;selectedExamType=null;
document.getElementById('mainApp').style.display='none';
document.getElementById('yearExamPage').style.display='none';
document.getElementById('loginPage').style.display='flex';
document.getElementById('loginEmail').value='';document.getElementById('loginPassword').value='';
document.getElementById('adminYearSection').style.display='none';
document.getElementById('adminExamSection').style.display='none';
showToast('Telah log keluar','info');
}
/* ===== YEAR & EXAM TYPE ===== */
async function loadYearsAndExamTypes(){
try{
const{data:yData}=await db.from('exam_years').select('*').order('year');
const ys=document.getElementById('yearSelect');
ys.innerHTML='<option value="">-- Pilih Tahun --</option>';
if(yData)yData.forEach(y=>{const o=document.createElement('option');o.value=y.year;o.textContent=y.year;ys.appendChild(o);});
const{data:eData}=await db.from('exam_types').select('*').order('name');
const es=document.getElementById('examSelect');
es.innerHTML='<option value="">-- Pilih Jenis Peperiksaan --</option>';
if(eData)eData.forEach(e=>{const o=document.createElement('option');o.value=e.name;o.textContent=e.name;es.appendChild(o);});
}catch(e){console.error(e);}
}
async function addYear(){
const v=document.getElementById('newYearInput').value.trim();
if(!v){showToast('Sila masuk tahun','error');return;}
try{
const{error}=await db.from('exam_years').insert({year:v});
if(error)throw error;
showToast('Tahun berjaya ditambah!');
document.getElementById('newYearInput').value='';
await loadYearsAndExamTypes();
}catch(e){showToast(e.message||'Gagal menambah tahun','error');}
}
async function addExamType(){
const v=document.getElementById('newExamInput').value.trim();
if(!v){showToast('Sila masuk jenis peperiksaan','error');return;}
try{
const{error}=await db.from('exam_types').insert({name:v});
if(error)throw error;
showToast('Jenis peperiksaan berjaya ditambah!');
document.getElementById('newExamInput').value='';
await loadYearsAndExamTypes();
}catch(e){showToast(e.message||'Gagal menambah jenis peperiksaan','error');}
}
async function enterSession(){
selectedYear=document.getElementById('yearSelect').value;
selectedExamType=document.getElementById('examSelect').value;
if(!selectedYear||!selectedExamType){showToast('Sila pilih tahun dan jenis peperiksaan','error');return;}
document.getElementById('yearExamPage').style.display='none';
document.getElementById('mainApp').style.display='block';
document.getElementById('sidebarSession').textContent=selectedYear+' — '+selectedExamType;
document.getElementById('sidebarRole').textContent=currentUser.role==='admin'?'Admin':'User';
navigateTo('dashboard');
}
/* ===== NAVIGATION ===== */
function navigateTo(page){
currentPage=page;
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
document.getElementById('page-'+page).classList.add('active');
document.querySelectorAll('.nav-item').forEach(n=>{
n.classList.toggle('active',n.dataset.page===page);
});
if(page==='dashboard')loadDashboard();
else if(page==='students')loadStudents();
else if(page==='classes')loadClasses();
else if(page==='tingkatan')loadTingkatan();
else if(page==='subjects')loadSubjects();
else if(page==='marks') {
resetMarksPage();
if (currentUser.role === 'admin') {
document.getElementById('marksAdminActions').style.display = 'flex';
} else {
document.getElementById('marksAdminActions').style.display = 'none';
}
}
else if(page==='results')resetResultsPage();
else if(page==='tingkatan-results')resetTingkatanResultsPage();
else if(page==='ranking')resetRankingPage();
else if(page==='ranking-tingkatan')resetRankingTingkatanPage();
else if(page==='ranking-subject')resetRankingSubjectPage();
else if(page==='analisis-subject')resetAnalisisSubjectPage();
else if(page==='analisis-tingkatan')resetAnalisisTingkatanPage();
else if(page==='miss-a')resetMissAPage();
else if(page==='slip')resetSlipPage();
}
/* ===== DASHBOARD ===== */
async function loadDashboard(){
const statsEl=document.getElementById('dashStats');
statsEl.innerHTML='<div class="col-span-full text-center py-8 loading-pulse" style="color:var(--muted)">Memuatkan data...</div>';
try{
const[sRes,cRes,sjRes,mRes]=await Promise.all([
db.from('students').select('id',{count:'exact',head:true}).eq('exam_year',selectedYear).eq('exam_type',selectedExamType),
db.from('students').select('class_name').eq('exam_year',selectedYear).eq('exam_type',selectedExamType),
db.from('subjects').select('id',{count:'exact',head:true}).eq('exam_year',selectedYear).eq('exam_type',selectedExamType),
db.from('marks').select('id',{count:'exact',head:true}).eq('exam_year',selectedYear).eq('exam_type',selectedExamType)
]);
const sCount=sRes.count||0;
const uniqueClasses=[...new Set((cRes.data||[]).map(s=>s.class_name))];
const sjCount=sjRes.count||0;
const mCount=mRes.count||0;
statsEl.innerHTML=`
<div class="stat-card fade-in" style="animation-delay:.05s">
<div class="stat-icon" style="background:rgba(16,185,129,.1);color:var(--accent)"><i class="fas fa-user-graduate"></i></div>
<div class="stat-value">${sCount}</div>
<div class="stat-label">Jumlah Murid</div>
</div>
<div class="stat-card fade-in" style="animation-delay:.1s">
<div class="stat-icon" style="background:rgba(245,158,11,.1);color:var(--gold)"><i class="fas fa-chalkboard"></i></div>
<div class="stat-value">${uniqueClasses.length}</div>
<div class="stat-label">Jumlah Kelas</div>
</div>
<div class="stat-card fade-in" style="animation-delay:.15s">
<div class="stat-icon" style="background:rgba(59,130,246,.1);color:var(--info)"><i class="fas fa-book"></i></div>
<div class="stat-value">${sjCount}</div>
<div class="stat-label">Mata Pelajaran</div>
</div>
<div class="stat-card fade-in" style="animation-delay:.2s">
<div class="stat-icon" style="background:rgba(239,68,68,.1);color:var(--danger)"><i class="fas fa-pen-alt"></i></div>
<div class="stat-value">${mCount}</div>
<div class="stat-label">Markah Dimasukkan</div>
</div>`;
const actEl=document.getElementById('dashActivity');
const { data: recentMarks, error: mErr } = await db.from('marks').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('updated_at', { ascending: false })
.limit(8);
if (mErr) console.error("Error fetching activities:", mErr);
if (!recentMarks || recentMarks.length === 0) {
actEl.innerHTML = '<p class="text-sm text-center py-6" style="color:var(--muted)">Tiada aktiviti terkini</p>';
} else {
const studentIds = [...new Set(recentMarks.map(m => m.student_id))];
const subjectIds = [...new Set(recentMarks.map(m => m.subject_id))];
const { data: studentsData } = await db.from('students').select('id,name,class_name')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.in('id', studentIds);
const { data: subjectsData } = await db.from('subjects').select('id,name')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.in('id', subjectIds);
const studentMap = {};
(studentsData || []).forEach(s => studentMap[s.id] = s);
const subjectMap = {};
(subjectsData || []).forEach(s => subjectMap[s.id] = s);
actEl.innerHTML = recentMarks.map(m => {
const student = studentMap[m.student_id] || { name: '?' };
const subject = subjectMap[m.subject_id] || { name: '?' };
const markDisplay = m.grade === 'TH' ? 'TH' : `${m.mark}% (${m.grade})`;
return `
<div class="flex items-center gap-3 py-3" style="border-bottom:1px solid var(--border)">
<div class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0" style="background:rgba(16,185,129,.1)">
<i class="fas fa-pen text-xs" style="color:var(--accent)"></i>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium truncate">Markah ${escHtml(subject.name)} untuk ${escHtml(student.name)}: ${markDisplay}</div>
</div>
<div class="text-xs flex-shrink-0" style="color:var(--muted)">${m.updated_at ? new Date(m.updated_at).toLocaleString('ms-MY') : '-'}</div>
</div>`;
}).join('');
}
}catch(e){
statsEl.innerHTML=`<div class="col-span-full text-center py-8"><p style="color:var(--danger)">${e.message||'Gagal memuatkan data'}</p></div>`;
}
}
/* ===== STUDENTS ===== */
async function loadStudents(){
const tbody=document.getElementById('studentsTableBody');
const empty=document.getElementById('studentsEmpty');
tbody.innerHTML='';empty.style.display='none';
showProgress('studentsProgress');
await animateProgress('studentsProgressBar',600);
try{
const{data,error}=await db.from('students').select('*').eq('exam_year',selectedYear).eq('exam_type',selectedExamType).order('class_name').order('name');
hideProgress('studentsProgress');
if(error)throw error;
if(!data||data.length===0){empty.style.display='block';return;}
data.forEach((s,i)=>{
const tr=document.createElement('tr');
tr.innerHTML=`<td>${i+1}</td><td>${escHtml(s.name)}</td><td><span class="badge badge-success">${escHtml(s.class_name)}</span></td>
<td><div class="flex gap-2">
<button class="btn btn-outline btn-xs" onclick="showEditStudent(${s.id},'${escAttr(s.name)}','${escAttr(s.class_name)}')"><i class="fas fa-edit"></i></button>
<button class="btn btn-danger btn-xs" onclick="confirmDeleteStudent(${s.id},'${escAttr(s.name)}')"><i class="fas fa-trash"></i></button>
</div></td>`;
tbody.appendChild(tr);
});
}catch(e){hideProgress('studentsProgress');showToast(e.message||'Gagal memuatkan murid','error');}
}
function escHtml(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
function escAttr(s){return s.replace(/'/g,"\\'").replace(/"/g,'"');}
async function importExcel(event){
const file=event.target.files[0];if(!file)return;
event.target.value='';
showProgress('studentsProgress');
const bar=document.getElementById('studentsProgressBar');
bar.style.width='10%';
try{
const data=await file.arrayBuffer();
const wb=XLSX.read(data,{type:'array'});
const ws=wb.Sheets[wb.SheetNames[0]];
const rows=XLSX.utils.sheet_to_json(ws,{defval:''});
bar.style.width='30%';
if(!rows.length){throw new Error('Fail kosong');}
let nameKey=null,classKey=null;
const keys=Object.keys(rows[0]);
keys.forEach(k=>{
const kl=k.toLowerCase().trim();
if(kl.includes('nama')&&kl.includes('murid'))nameKey=k;
else if(kl.includes('kelas'))classKey=k;
});
if(!nameKey)nameKey=keys[0];
if(!classKey)classKey=keys[1]||keys[0];
bar.style.width='50%';
const inserts=rows.filter(r=>r[nameKey]&&r[nameKey].toString().trim()).map(r=>({
exam_year:selectedYear,exam_type:selectedExamType,
name:r[nameKey].toString().trim(),
class_name:(r[classKey]||'').toString().trim()||'Tiada Kelas'
}));
bar.style.width='70%';
if(!inserts.length){throw new Error('Tiada data untuk diimport');}
const batchSize=100;
for(let i=0;i<inserts.length;i+=batchSize){
const batch=inserts.slice(i,i+batchSize);
const{error}=await db.from('students').insert(batch);
if(error)throw error;
bar.style.width=(70+Math.round((i+batchSize)/inserts.length*30))+'%';
}
bar.style.width='100%';
showToast(`${inserts.length} rekod berjaya diimport!`);
setTimeout(()=>loadStudents(),400);
}catch(e){hideProgress('studentsProgress');showToast(e.message||'Gagal mengimport','error');}
}
function showAddStudentModal(){
showEdit('Tambah Murid Baru',`
<div class="mb-4"><label class="text-xs font-semibold block mb-2" style="color:var(--text2)">Nama Murid</label>
<input type="text" id="addStudentName" class="edit-input" placeholder="Masukkan nama murid"></div>
<div><label class="text-xs font-semibold block mb-2" style="color:var(--text2)">Kelas</label>
<input type="text" id="addStudentClass" class="edit-input" placeholder="Cth: 1 Ibnu Sina"></div>`,
async()=>{
const name=document.getElementById('addStudentName').value.trim();
const cls=document.getElementById('addStudentClass').value.trim();
if(!name){showToast('Sila masuk nama murid','error');return;}
try{
const{error}=await db.from('students').insert({exam_year:selectedYear,exam_type:selectedExamType,name,class_name:cls||'Tiada Kelas'});
if(error)throw error;
showToast('Murid berjaya ditambah!');closeEdit();loadStudents();
}catch(e){showToast(e.message||'Gagal menambah','error');}
});
}
function showEditStudent(id,name,cls){
showEdit('Kemaskini Murid',`
<div class="mb-4"><label class="text-xs font-semibold block mb-2" style="color:var(--text2)">Nama Murid</label>
<input type="text" id="editStudentName" class="edit-input" value="${escHtml(name)}"></div>
<div><label class="text-xs font-semibold block mb-2" style="color:var(--text2)">Kelas</label>
<input type="text" id="editStudentClass" class="edit-input" value="${escHtml(cls)}"></div>`,
async()=>{
const n=document.getElementById('editStudentName').value.trim();
const c=document.getElementById('editStudentClass').value.trim();
if(!n){showToast('Sila masuk nama','error');return;}
try{
const{error}=await db.from('students').update({name:n,class_name:c||'Tiada Kelas'}).eq('id',id);
if(error)throw error;showToast('Murid berjaya dikemaskini!');closeEdit();loadStudents();
}catch(e){showToast(e.message||'Gagal mengemaskini','error');}
});
}
function confirmDeleteStudent(id,name){showConfirm('Padam Murid',`Adakah anda pasti ingin memadam "${name}"?`,async()=>{try{await db.from('marks').delete().eq('student_id',id).eq('exam_year',selectedYear).eq('exam_type',selectedExamType);await db.from('students').delete().eq('id',id);showToast('Murid berjaya dipadam');loadStudents();}catch(e){showToast(e.message||'Gagal memadam','error');}});}
function confirmDeleteAllStudents(){showConfirm('Padam Semua Murid','Adakah anda pasti ingin memadam SEMUA murid dalam sesi ini? Tindakan ini tidak boleh dibatalkan.',async()=>{try{const{data:students}=await db.from('students').select('id').eq('exam_year',selectedYear).eq('exam_type',selectedExamType);if(students&&students.length){const ids=students.map(s=>s.id);await db.from('marks').delete().in('student_id',ids).eq('exam_year',selectedYear).eq('exam_type',selectedExamType);}await db.from('students').delete().eq('exam_year',selectedYear).eq('exam_type',selectedExamType);showToast('Semua murid berjaya dipadam');loadStudents();}catch(e){showToast(e.message||'Gagal memadam','error');}});}
/* ===== CLASSES ===== */
async function loadClasses(){
const tbody=document.getElementById('classesTableBody');
const empty=document.getElementById('classesEmpty');
tbody.innerHTML='';empty.style.display='none';
showProgress('classesProgress');
await animateProgress('classesProgressBar',700);
try{
const{data,error}=await db.from('students').select('class_name').eq('exam_year',selectedYear).eq('exam_type',selectedExamType);
hideProgress('classesProgress');
if(error)throw error;
const classMap={};
(data||[]).forEach(s=>{classMap[s.class_name]=(classMap[s.class_name]||0)+1;});
const entries=Object.entries(classMap).sort((a,b)=>a[0].localeCompare(b[0]));
if(!entries.length){empty.style.display='block';return;}
entries.forEach(([name,count],i)=>{
const tr=document.createElement('tr');
tr.innerHTML=`<td>${i+1}</td><td class="font-semibold">${escHtml(name)}</td><td><span class="badge badge-success">${count} murid</span></td>`;
tbody.appendChild(tr);
});
}catch(e){hideProgress('classesProgress');showToast(e.message||'Gagal memuatkan kelas','error');}
}
function confirmDeleteAllClasses(){showConfirm('Padam Semua Kelas','Adakah anda pasti ingin memadam SEMUA kelas (dan murid) dalam sesi ini?',async()=>{try{const{data:students}=await db.from('students').select('id').eq('exam_year',selectedYear).eq('exam_type',selectedExamType);if(students&&students.length){const ids=students.map(s=>s.id);await db.from('marks').delete().in('student_id',ids).eq('exam_year',selectedYear).eq('exam_type',selectedExamType);}await db.from('students').delete().eq('exam_year',selectedYear).eq('exam_type',selectedExamType);showToast('Semua kelas berjaya dipadam');loadClasses();}catch(e){showToast(e.message||'Gagal memadam','error');}});}
/* ===== TINGKATAN ===== */
async function loadTingkatan(){
const container=document.getElementById('tingkatanCards');
container.innerHTML='';showProgress('tingkatanProgress');
await animateProgress('tingkatanProgressBar',800);
try{
const{data,error}=await db.from('students').select('class_name').eq('exam_year',selectedYear).eq('exam_type',selectedExamType);
hideProgress('tingkatanProgress');
if(error)throw error;
const tkCounts={1:new Set(),2:new Set(),3:new Set()};
(data||[]).forEach(s=>{
const tk=getTingkatan(s.class_name);
if(tk)tkCounts[tk].add(s.class_name);
});
const colors=['var(--accent)','var(--gold)','var(--info)'];
const bgs=['rgba(16,185,129,.08)','rgba(245,158,11,.08)','rgba(59,130,246,.08)'];
['1','2','3'].forEach((tk,i)=>{
const card=document.createElement('div');
card.className='tingkatan-card fade-in';
card.style.animationDelay=(i*0.1)+'s';
card.style.borderColor=colors[i]+'22';
card.innerHTML=`
<div style="position:absolute;top:0;right:0;width:80px;height:80px;border-radius:0 0 0 80px;background:${bgs[i]}"></div>
<div class="tk-num" style="color:${colors[i]}">T${tk}</div>
<div style="color:var(--text2);font-size:13px;margin-top:4px">Tingkatan ${tk}</div>
<div class="tk-count" style="color:${colors[i]}">${tkCounts[tk].size}</div>
<div style="color:var(--text2);font-size:12px;margin-top:4px">kelas</div>`;
container.appendChild(card);
});
}catch(e){hideProgress('tingkatanProgress');showToast(e.message||'Gagal memuatkan','error');}
}
/* ===== SUBJECTS ===== */
async function loadSubjects(){
const tbody=document.getElementById('subjectsTableBody');
const empty=document.getElementById('subjectsEmpty');
tbody.innerHTML='';empty.style.display='none';
try{
const{data,error}=await db.from('subjects').select('*').eq('exam_year',selectedYear).eq('exam_type',selectedExamType).order('name');
if(error)throw error;
if(!data||data.length===0){empty.style.display='block';return;}
data.forEach((s,i)=>{
const tr=document.createElement('tr');
tr.innerHTML=`<td>${i+1}</td><td class="font-semibold">${escHtml(s.name)}</td><td><span class="badge badge-muted">${escHtml(s.code||'-')}</span></td>
<td><div class="flex gap-2">
<button class="btn btn-outline btn-xs" onclick="showEditSubject(${s.id},'${escAttr(s.name)}','${escAttr(s.code||'')}')"><i class="fas fa-edit"></i></button>
<button class="btn btn-danger btn-xs" onclick="confirmDeleteSubject(${s.id},'${escAttr(s.name)}')"><i class="fas fa-trash"></i></button>
</div></td>`;
tbody.appendChild(tr);
});
}catch(e){showToast(e.message||'Gagal memuatkan mata pelajaran','error');}
}
async function addSubject(){
const name=document.getElementById('newSubjectName').value.trim();
const code=document.getElementById('newSubjectCode').value.trim();
if(!name){showToast('Sila masuk nama mata pelajaran','error');return;}
try{
const{error}=await db.from('subjects').insert({exam_year:selectedYear,exam_type:selectedExamType,name,code:code||null});
if(error)throw error;
showToast('Mata pelajaran berjaya didaftar!');
document.getElementById('newSubjectName').value='';document.getElementById('newSubjectCode').value='';
loadSubjects();
}catch(e){showToast(e.message||'Gagal menambah','error');}
}
function showEditSubject(id,name,code){
showEdit('Kemaskini Mata Pelajaran',`
<div class="mb-4"><label class="text-xs font-semibold block mb-2" style="color:var(--text2)">Nama Mata Pelajaran</label>
<input type="text" id="editSubjName" class="edit-input" value="${escHtml(name)}"></div>
<div><label class="text-xs font-semibold block mb-2" style="color:var(--text2)">Kod</label>
<input type="text" id="editSubjCode" class="edit-input" value="${escHtml(code)}"></div>`,
async()=>{
const n=document.getElementById('editSubjName').value.trim();
const c=document.getElementById('editSubjCode').value.trim();
if(!n){showToast('Sila masuk nama','error');return;}
try{
const{error}=await db.from('subjects').update({name:n,code:c||null}).eq('id',id);
if(error)throw error;showToast('Berjaya dikemaskini!');closeEdit();loadSubjects();
}catch(e){showToast(e.message||'Gagal mengemaskini','error');}
});
}
function confirmDeleteSubject(id,name){showConfirm('Padam Mata Pelajaran',`Adakah anda pasti ingin memadam "${name}"?`,async()=>{try{await db.from('marks').delete().eq('subject_id',id).eq('exam_year',selectedYear).eq('exam_type',selectedExamType);await db.from('subjects').delete().eq('id',id);showToast('Mata pelajaran berjaya dipadam');loadSubjects();}catch(e){showToast(e.message||'Gagal memadam','error');}});}
/* ===== MARKS ===== */
function resetMarksPage(){
document.getElementById('marksTingkatan').value='';
document.getElementById('marksKelas').innerHTML='<option value="">-- Pilih Kelas --</option>';
document.getElementById('marksSubject').innerHTML='<option value="">-- Pilih Mata Pelajaran --</option>';
document.getElementById('marksTableArea').style.display='none';
document.getElementById('marksPlaceholder').style.display='block';
marksInputData={};existingMarksMap={};
}
async function onMarksTingkatanChange(){
const tk=document.getElementById('marksTingkatan').value;
const ks=document.getElementById('marksKelas');
ks.innerHTML='<option value="">-- Pilih Kelas --</option>';
document.getElementById('marksSubject').innerHTML='<option value="">-- Pilih Mata Pelajaran --</option>';
document.getElementById('marksTableArea').style.display='none';
document.getElementById('marksPlaceholder').style.display='block';
marksInputData={};existingMarksMap={};
if(!tk)return;
try{
const{data}=await db.from('students').select('class_name').eq('exam_year',selectedYear).eq('exam_type',selectedExamType);
const classes=[...new Set((data||[]).filter(s=>getTingkatan(s.class_name)===tk).map(s=>s.class_name))].sort();
classes.forEach(c=>{const o=document.createElement('option');o.value=c;o.textContent=c;ks.appendChild(o);});
}catch(e){console.error(e);}
}
async function onMarksKelasChange(){
const cls=document.getElementById('marksKelas').value;
const ss=document.getElementById('marksSubject');
ss.innerHTML='<option value="">-- Pilih Mata Pelajaran --</option>';
document.getElementById('marksTableArea').style.display='none';
document.getElementById('marksPlaceholder').style.display='block';
marksInputData={};existingMarksMap={};
if(!cls)return;
try{
const{data}=await db.from('subjects').select('*').eq('exam_year',selectedYear).eq('exam_type',selectedExamType).order('name');
(data||[]).forEach(s=>{const o=document.createElement('option');o.value=s.id;o.textContent=s.code?s.code+' — '+s.name:s.name;ss.appendChild(o);});
}catch(e){console.error(e);}
}
async function onMarksSubjectChange(){
const cls=document.getElementById('marksKelas').value;
const subjId=document.getElementById('marksSubject').value;
document.getElementById('marksTableArea').style.display='none';
document.getElementById('marksPlaceholder').style.display='block';
marksInputData={};existingMarksMap={};
if(!cls||!subjId)return;
try{
const[studentsRes,marksRes]=await Promise.all([
db.from('students').select('*').eq('exam_year',selectedYear).eq('exam_type',selectedExamType).eq('class_name',cls).order('name'),
db.from('marks').select('*').eq('exam_year',selectedYear).eq('exam_type',selectedExamType).eq('subject_id',subjId)
]);
const students=studentsRes.data||[];
const marks=marksRes.data||[];
const marksById={};marks.forEach(m=>{marksById[m.student_id]=m;});
students.forEach(s=>{
const existing=marksById[s.id];
if(existing){
existingMarksMap[s.id]={mark:existing.mark,grade:existing.grade};
const displayMark = existing.grade === 'TH' ? 'TH' : existing.mark.toString();
marksInputData[s.id]={mark:displayMark,grade:existing.grade,status:'Tersimpan'};
}else{
existingMarksMap[s.id]=null;
marksInputData[s.id]={mark:'',grade:'',status:'-'};
}
});
const tbody=document.getElementById('marksTableBody');
tbody.innerHTML='';
students.forEach((s,i)=>{
const d=marksInputData[s.id];
const tr=document.createElement('tr');
tr.innerHTML=`<td>${i+1}</td><td>${escHtml(s.name)}</td>
<td><input type="text" class="mark-input" value="${d.mark}" data-sid="${s.id}" oninput="onMarkInput(this)" placeholder="0-100 / TH"></td>
<td><span class="badge ${d.grade?getGradeBadgeClass(d.grade):'badge-muted'}" id="grade-${s.id}">${d.grade||'-'}</span></td>
<td id="status-${s.id}"><span class="badge ${d.status==='Tersimpan'?'badge-success':d.status==='Belum disimpan'?'badge-warning':'badge-muted'}">${d.status}</span></td>`;
tbody.appendChild(tr);
});
document.getElementById('marksTableArea').style.display='block';
document.getElementById('marksPlaceholder').style.display='none';
}catch(e){showToast(e.message||'Gagal memuatkan data','error');}
}
function getGradeBadgeClass(g){
if(g==='A')return'badge-success';if(g==='B')return'badge-success';
if(g==='C')return'badge-warning';if(g==='D')return'badge-warning';
if(g==='E')return'badge-danger';if(g==='TH')return'badge-muted';
return'badge-danger';
}
function onMarkInput(input){
const sid=input.dataset.sid;
const val=input.value.trim().toUpperCase();
const grade=getGrade(val);
marksInputData[sid].mark=val;
marksInputData[sid].grade= val===''?'':grade;
const gradeEl=document.getElementById('grade-'+sid);
gradeEl.className='badge '+(grade?getGradeBadgeClass(grade):'badge-muted');
gradeEl.textContent=val===''?'-':grade;
const existing=existingMarksMap[sid];
let newStatus='-';
if(val!==''){
if(existing){
const existingVal = existing.grade === 'TH' ? 'TH' : existing.mark.toString();
if(val === existingVal && grade === existing.grade){
newStatus='Tersimpan';
} else {
newStatus='Belum disimpan';
}
} else {
newStatus='Belum disimpan';
}
}
marksInputData[sid].status=newStatus;
const statusEl=document.getElementById('status-'+sid);
statusEl.innerHTML=`<span class="badge ${newStatus==='Tersimpan'?'badge-success':newStatus==='Belum disimpan'?'badge-warning':'badge-muted'}">${newStatus}</span>`;
}
async function saveMarks(){
const subjId=parseInt(document.getElementById('marksSubject').value);
if(!subjId){showToast('Sila pilih mata pelajaran','error');return;}
const toUpsert=[];
for(const[sid,d]of Object.entries(marksInputData)){
if(d.mark!==''&&d.status!=='Tersimpan'){
const val = d.mark;
const isTH = val.toUpperCase() === 'TH';
toUpsert.push({
exam_year:selectedYear,exam_type:selectedExamType,
student_id:parseInt(sid),subject_id:subjId,
mark: isTH ? 0 : parseInt(val),
grade: d.grade
});
}
}
if(!toUpsert.length){showToast('Tiada markah baru untuk disimpan','info');return;}
try{
const{error}=await db.from('marks').upsert(toUpsert,{onConflict:'exam_year,exam_type,student_id,subject_id'});
if(error)throw error;
toUpsert.forEach(r=>{
const sid=r.student_id.toString();
existingMarksMap[sid]={mark:r.mark,grade:r.grade};
marksInputData[sid].status='Tersimpan';
const statusEl=document.getElementById('status-'+sid);
if(statusEl)statusEl.innerHTML='<span class="badge badge-success">Tersimpan</span>';
});
showToast(`${toUpsert.length} markah berjaya disimpan!`);
}catch(e){showToast(e.message||'Gagal menyimpan markah','error');}
}
/* JANA MARKAH RAWAK (Admin Only) - Jana untuk SEMUA kelas dan subjek */
async function generateRandomMarks() {
showConfirm('Jana Markah Secara Rawak',
'Ini akan memadamkan markah sedia ada dan menjana markah rawak untuk SEMUA kelas dan mata pelajaran dalam sesi ini. Adakah anda pasti?',
async () => {
try {
showToast('Memproses penjanaan markah...', 'info');
// 1. Dapatkan semua pelajar dalam sesi ini
const { data: studentsData, error: sErr } = await db.from('students').select('id')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType);
if (sErr) throw sErr;
// 2. Dapatkan semua subjek dalam sesi ini
const { data: subjectsData, error: sjErr } = await db.from('subjects').select('id')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType);
if (sjErr) throw sjErr;
if (!studentsData || studentsData.length === 0 || !subjectsData || subjectsData.length === 0) {
showToast('Tiada murid atau mata pelajaran untuk dijana markah.', 'error');
return;
}
// 3. Padam semua markah sedia ada bagi sesi ini sahaja
const { error: delErr } = await db.from('marks').delete()
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType);
if (delErr) throw delErr;
// 4. Jana markah baru untuk semua kombinasi murid & subjek
const newMarks = [];
studentsData.forEach(student => {
subjectsData.forEach(subject => {
const isTH = Math.random() < 0.05; // 5% peluang TH
let mark, grade;
if (isTH) {
mark = 0;
grade = 'TH';
} else {
mark = Math.floor(Math.random() * 101); // 0 hingga 100
grade = getGrade(mark);
}
newMarks.push({
exam_year: selectedYear,
exam_type: selectedExamType,
student_id: student.id,
subject_id: subject.id,
mark: mark,
grade: grade
});
});
});
// 5. Masukkan secara batch
const batchSize = 500;
for (let i = 0; i < newMarks.length; i += batchSize) {
const batch = newMarks.slice(i, i + batchSize);
const { error: insertErr } = await db.from('marks').upsert(batch, {
onConflict: 'exam_year,exam_type,student_id,subject_id'
});
if (insertErr) throw insertErr;
}
showToast(`${newMarks.length} markah berjaya dijana secara rawak!`);
// Reload jadual markah jika ada subjek dipilih
const subjId = document.getElementById('marksSubject').value;
if (subjId) {
onMarksSubjectChange();
}
} catch (e) {
showToast(e.message || 'Gagal menjana markah', 'error');
}
});
}
/* PADAM SEMUA MARKAH (Admin Only) */
function deleteAllMarks() {
showConfirm('Padam Semua Markah',
'Adakah anda pasti ingin memadam SEMUA markah bagi sesi peperiksaan ini? Tindakan ini tidak boleh dibatalkan.',
async () => {
try {
const { error } = await db.from('marks').delete()
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType);
if (error) throw error;
showToast('Semua markah bagi sesi ini berjaya dipadam.');
resetMarksPage(); // Clear the table
} catch (e) {
showToast(e.message || 'Gagal memadam markah', 'error');
}
});
}
/* ===== RESULTS (KELAS) ===== */
function resetResultsPage(){
document.getElementById('resultsTingkatan').value='';
document.getElementById('resultsKelas').innerHTML='<option value="">-- Pilih Kelas --</option>';
document.getElementById('resultsTableArea').style.display='none';
document.getElementById('resultsPlaceholder').style.display='block';
hideProgress('resultsProgress');
}
async function onResultsTingkatanChange(){
const tk=document.getElementById('resultsTingkatan').value;
const ks=document.getElementById('resultsKelas');
ks.innerHTML='<option value="">-- Pilih Kelas --</option>';
document.getElementById('resultsTableArea').style.display='none';
document.getElementById('resultsPlaceholder').style.display='block';
if(!tk)return;
try{
const{data}=await db.from('students').select('class_name').eq('exam_year',selectedYear).eq('exam_type',selectedExamType);
const classes=[...new Set((data||[]).filter(s=>getTingkatan(s.class_name)===tk).map(s=>s.class_name))].sort();
classes.forEach(c=>{const o=document.createElement('option');o.value=c;o.textContent=c;ks.appendChild(o);});
}catch(e){console.error(e);}
}
async function onResultsKelasChange(){
const cls=document.getElementById('resultsKelas').value;
if(!cls){document.getElementById('resultsTableArea').style.display='none';document.getElementById('resultsPlaceholder').style.display='block';return;}
document.getElementById('resultsPlaceholder').style.display='none';
showProgress('resultsProgress');
const bar=document.getElementById('resultsProgressBar');
bar.style.width='0%';
try{
bar.style.width='15%';
const{data:students,error:sErr}=await db.from('students').select('*').eq('exam_year',selectedYear).eq('exam_type',selectedExamType).eq('class_name',cls).order('name');
if(sErr)throw sErr;
if(!students||!students.length){hideProgress('resultsProgress');document.getElementById('resultsPlaceholder').style.display='block';document.getElementById('resultsPlaceholder').querySelector('p').textContent='Tiada murid dalam kelas this';return;}
bar.style.width='30%';
const{data:subjects,error:sjErr}=await db.from('subjects').select('*').eq('exam_year',selectedYear).eq('exam_type',selectedExamType).order('name');
if(sjErr)throw sjErr;
bar.style.width='50%';
const studentIds=students.map(s=>s.id);
const{data:allMarks,error:mErr}=await db.from('marks').select('*').eq('exam_year',selectedYear).eq('exam_type',selectedExamType).in('student_id',studentIds);
if(mErr)throw mErr;
bar.style.width='70%';
const subjectIdsWithMarks=new Set((allMarks||[]).map(m=>m.subject_id));
const activeSubjects=(subjects||[]).filter(s=>subjectIdsWithMarks.has(s.id));
const marksMap={};
(allMarks||[]).forEach(m=>{
const key=m.student_id+'_'+m.subject_id;
marksMap[key]={mark:m.mark,grade:m.grade};
});
bar.style.width='85%';
const thead=document.getElementById('resultsThead');
const tbody=document.getElementById('resultsTbody');
thead.innerHTML='';tbody.innerHTML='';
let headerHtml='<tr><th>Bil</th><th>Nama Murid</th>';
activeSubjects.forEach(s=>{
const subjDisplay = s.code ? s.code + '<br>' + escHtml(s.name) : escHtml(s.name);
headerHtml+=`<th class="subj-cell">${subjDisplay}</th>`;
});
headerHtml+='<th class="subj-cell">GPM</th><th class="subj-cell">Jumlah Markah</th><th>Ringkasan Gred</th></tr>';
thead.innerHTML=headerHtml;
students.forEach((s,i)=>{
let totalGradeVal=0,totalMark=0,subjCount=0;
const gradeSummary={A:0,B:0,C:0,D:0,E:0,F:0,TH:0};
let cells='<td>'+escHtml(s.name)+'</td>';
activeSubjects.forEach(sub=>{
const key=s.id+'_'+sub.id;
const md=marksMap[key];
if(md){
const displayMark = md.grade === 'TH' ? 'TH' : md.mark;
cells+=`<td class="subj-cell"><div class="subj-mark">${displayMark}</div><div class="subj-grade">${md.grade}</div></td>`;
// FIX: Exclude PSV & PJK from summary calculations
if(!isExcluded(sub.name)){
if(gradeSummary.hasOwnProperty(md.grade))gradeSummary[md.grade]++;
if(md.grade !== 'TH'){
totalGradeVal+=getGradeValue(md.grade);
totalMark+=md.mark;
subjCount++;
}
}
}else{
cells+=`<td class="subj-cell"><div class="subj-mark" style="color:var(--muted)">-</div><div class="subj-grade" style="color:var(--muted)">-</div></td>`;
}
});
const gpm=subjCount>0?(totalGradeVal/subjCount).toFixed(2):'-';
const summary=Object.entries(gradeSummary).filter(([_,c])=>c>0).map(([g,c])=>c+g).join(' ')||'-';
cells+=`<td class="subj-cell"><span class="font-bold" style="color:var(--gold2)">${gpm}</span></td>`;
cells+=`<td class="subj-cell"><span class="font-bold">${totalMark>0?totalMark:'-'}</span></td>`;
cells+=`<td><span class="text-sm font-semibold">${summary}</span></td>`;
const tr=document.createElement('tr');
tr.innerHTML=`<td>${i+1}</td>${cells}`;
tbody.appendChild(tr);
});
document.getElementById('resultsTitle').textContent='Senarai Murid — Markah & Gred — '+cls.toUpperCase();
document.getElementById('resultsFooter').textContent=`Jumlah: ${students.length} murid`;
bar.style.width='100%';
setTimeout(()=>{
hideProgress('resultsProgress');
document.getElementById('resultsTableArea').style.display='block';
},300);
}catch(e){hideProgress('resultsProgress');showToast(e.message||'Gagal memuatkan keputusan','error');document.getElementById('resultsPlaceholder').style.display='block';}
}
/* ===== RESULTS (TINGKATAN) - WITH PAGINATION ===== */
function resetTingkatanResultsPage() {
document.getElementById('trTingkatan').value = '';
document.getElementById('trTableArea').style.display = 'none';
document.getElementById('trPlaceholder').style.display = 'block';
document.getElementById('trPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan';
hideProgress('trProgress');
trCurrentPage = 1;
trAllStudents = [];
}
async function onTingkatanResultsChange() {
const tk = document.getElementById('trTingkatan').value;
if (!tk) {
resetTingkatanResultsPage();
return;
}
document.getElementById('trPlaceholder').style.display = 'none';
showProgress('trProgress');
const bar = document.getElementById('trProgressBar');
bar.style.width = '0%';
try {
bar.style.width = '15%';
const { data: students, error: sErr } = await db.from('students').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('class_name')
.order('name');
if (sErr) throw sErr;
trAllStudents = (students || []).filter(s => getTingkatan(s.class_name) === tk);
if (!trAllStudents.length) {
hideProgress('trProgress');
document.getElementById('trPlaceholder').style.display = 'block';
document.getElementById('trPlaceholder').querySelector('p').textContent = 'Tiada murid dalam tingkatan this';
document.getElementById('trTableArea').style.display = 'none';
return;
}
bar.style.width = '30%';
const { data: subjects, error: sjErr } = await db.from('subjects').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('name');
if (sjErr) throw sjErr;
bar.style.width = '50%';
const studentIds = trAllStudents.map(s => s.id);
const { data: allMarks, error: mErr } = await db.from('marks').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.in('student_id', studentIds);
if (mErr) throw mErr;
bar.style.width = '70%';
const subjectIdsWithMarks = new Set((allMarks || []).map(m => m.subject_id));
trActiveSubjects = (subjects || []).filter(s => subjectIdsWithMarks.has(s.id));
trMarksMap = {};
(allMarks || []).forEach(m => {
const key = m.student_id + '_' + m.subject_id;
trMarksMap[key] = { mark: m.mark, grade: m.grade };
});
trCurrentPage = 1;
bar.style.width = '100%';
setTimeout(() => {
hideProgress('trProgress');
document.getElementById('trTableArea').style.display = 'block';
renderTingkatanResultsTable();
}, 300);
} catch (e) {
hideProgress('trProgress');
showToast(e.message || 'Gagal memuatkan keputusan tingkatan', 'error');
document.getElementById('trPlaceholder').style.display = 'block';
}
}
function renderTingkatanResultsTable() {
const thead = document.getElementById('trThead');
const tbody = document.getElementById('trTbody');
thead.innerHTML = '';
tbody.innerHTML = '';
let headerHtml = '<tr><th>Bil</th><th>Nama Murid</th><th>Kelas</th>';
trActiveSubjects.forEach(s => {
const subjDisplay = s.code ? s.code + '<br>' + escHtml(s.name) : escHtml(s.name);
headerHtml += `<th class="subj-cell">${subjDisplay}</th>`;
});
headerHtml += '<th class="subj-cell">GPM</th><th class="subj-cell">Jumlah Markah</th><th>Ringkasan Gred</th></tr>';
thead.innerHTML = headerHtml;
const totalPages = Math.ceil(trAllStudents.length / trPageSize);
if (trCurrentPage > totalPages && totalPages > 0) trCurrentPage = totalPages;
const startIndex = (trCurrentPage - 1) * trPageSize;
const endIndex = startIndex + trPageSize;
const pageStudents = trAllStudents.slice(startIndex, endIndex);
pageStudents.forEach((s, i) => {
let totalGradeVal = 0, totalMark = 0, subjCount = 0;
const gradeSummary = { A: 0, B: 0, C: 0, D: 0, E: 0, F: 0, TH:0 };
let cells = `<td>${startIndex + i + 1}</td><td>${escHtml(s.name)}</td><td><span class="badge badge-success">${escHtml(s.class_name)}</span></td>`;
trActiveSubjects.forEach(sub => {
const key = s.id + '_' + sub.id;
const md = trMarksMap[key];
if (md) {
const displayMark = md.grade === 'TH' ? 'TH' : md.mark;
cells += `<td class="subj-cell"><div class="subj-mark">${displayMark}</div><div class="subj-grade">${md.grade}</div></td>`;
// FIX: Exclude PSV & PJK from summary calculations
if (!isExcluded(sub.name)) {
if(gradeSummary.hasOwnProperty(md.grade))gradeSummary[md.grade]++;
if (md.grade !== 'TH') {
totalGradeVal += getGradeValue(md.grade);
totalMark += md.mark;
subjCount++;
}
}
} else {
cells += `<td class="subj-cell"><div class="subj-mark" style="color:var(--muted)">-</div><div class="subj-grade" style="color:var(--muted)">-</div></td>`;
}
});
const gpm = subjCount > 0 ? (totalGradeVal / subjCount).toFixed(2) : '-';
const summary = Object.entries(gradeSummary).filter(([_, c]) => c > 0).map(([g, c]) => c + g).join(' ') || '-';
cells += `<td class="subj-cell"><span class="font-bold" style="color:var(--gold2)">${gpm}</span></td>`;
cells += `<td class="subj-cell"><span class="font-bold">${totalMark > 0 ? totalMark : '-'}</span></td>`;
cells += `<td><span class="text-sm font-semibold">${summary}</span></td>`;
const tr = document.createElement('tr');
tr.innerHTML = cells;
tbody.appendChild(tr);
});
document.getElementById('trTitle').textContent = `Senarai Murid — Tingkatan ${document.getElementById('trTingkatan').value} — Markah & Gred`;
document.getElementById('trFooter').textContent = `Jumlah: ${trAllStudents.length} murid`;
renderTingkatanPagination(totalPages);
}
function renderTingkatanPagination(totalPages) {
const pagContainer = document.getElementById('trPagination');
pagContainer.innerHTML = '';
if (totalPages <= 1) return;
const createBtn = (text, page, disabled, active) => {
const btn = document.createElement('button');
btn.className = `btn btn-sm pagination-btn ${active ? 'btn-primary' : 'btn-outline'}`;
btn.innerHTML = text;
btn.disabled = disabled;
if (disabled) btn.style.opacity = '0.5';
btn.onclick = () => {
trCurrentPage = page;
renderTingkatanResultsTable();
};
return btn;
};
pagContainer.appendChild(createBtn('<i class="fas fa-chevron-left"></i>', trCurrentPage - 1, trCurrentPage === 1, false));
let startPage = Math.max(1, trCurrentPage - 2);
let endPage = Math.min(totalPages, trCurrentPage + 2);
if (startPage > 1) {
pagContainer.appendChild(createBtn('1', 1, false, trCurrentPage === 1));
if (startPage > 2) {
const span = document.createElement('span');
span.textContent = '...';
span.style.color = 'var(--text2)';
span.style.padding = '0 8px';
pagContainer.appendChild(span);
}
}
for (let i = startPage; i <= endPage; i++) {
pagContainer.appendChild(createBtn(i.toString(), i, false, trCurrentPage === i));
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
const span = document.createElement('span');
span.textContent = '...';
span.style.color = 'var(--text2)';
span.style.padding = '0 8px';
pagContainer.appendChild(span);
}
pagContainer.appendChild(createBtn(totalPages.toString(), totalPages, false, trCurrentPage === totalPages));
}
pagContainer.appendChild(createBtn('<i class="fas fa-chevron-right"></i>', trCurrentPage + 1, trCurrentPage === totalPages, false));
}
/* ===== RANKING KELAS ===== */
function resetRankingPage(){
document.getElementById('rankingTingkatan').value='';
document.getElementById('rankingKelas').innerHTML='<option value="">-- Pilih Kelas --</option>';
document.getElementById('rankingTableArea').style.display='none';
document.getElementById('rankingPlaceholder').style.display='block';
document.getElementById('rankingPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan dan kelas';
hideProgress('rankingProgress');
}
async function onRankingKelasTingkatanChange(){
const tk=document.getElementById('rankingTingkatan').value;
const ks=document.getElementById('rankingKelas');
ks.innerHTML='<option value="">-- Pilih Kelas --</option>';
document.getElementById('rankingTableArea').style.display='none';
document.getElementById('rankingPlaceholder').style.display='block';
document.getElementById('rankingPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan dan kelas';
hideProgress('rankingProgress');
if(!tk)return;
try{
const{data}=await db.from('students').select('class_name').eq('exam_year',selectedYear).eq('exam_type',selectedExamType);
const classes=[...new Set((data||[]).filter(s=>getTingkatan(s.class_name)===tk).map(s=>s.class_name))].sort();
classes.forEach(c=>{const o=document.createElement('option');o.value=c;o.textContent=c;ks.appendChild(o);});
}catch(e){console.error(e);}
}
async function onRankingKelasChange(){
const cls=document.getElementById('rankingKelas').value;
if(!cls){document.getElementById('rankingTableArea').style.display='none';document.getElementById('rankingPlaceholder').style.display='block';return;}
document.getElementById('rankingPlaceholder').style.display='none';
showProgress('rankingProgress');
const bar=document.getElementById('rankingProgressBar');
bar.style.width='0%';
try{
bar.style.width='15%';
const{data:students,error:sErr}=await db.from('students').select('*')
.eq('exam_year',selectedYear)
.eq('exam_type',selectedExamType)
.eq('class_name',cls)
.order('name');
if(sErr)throw sErr;
if(!students||!students.length){
hideProgress('rankingProgress');
document.getElementById('rankingPlaceholder').style.display='block';
document.getElementById('rankingPlaceholder').querySelector('p').textContent='Tiada murid dalam kelas this';
return;
}
bar.style.width='30%';
const{data:subjects,error:sjErr}=await db.from('subjects').select('*')
.eq('exam_year',selectedYear)
.eq('exam_type',selectedExamType)
.order('name');
if(sjErr)throw sjErr;
bar.style.width='50%';
const studentIds=students.map(s=>s.id);
const{data:allMarks,error:mErr}=await db.from('marks').select('*')
.eq('exam_year',selectedYear)
.eq('exam_type',selectedExamType)
.in('student_id',studentIds);
if(mErr)throw mErr;
bar.style.width='70%';
const marksMap={};
(allMarks||[]).forEach(m=>{
const key=m.student_id+'_'+m.subject_id;
marksMap[key]={mark:m.mark,grade:m.grade};
});
const subjectIdsWithMarks=new Set((allMarks||[]).map(m=>m.subject_id));
const activeSubjects=(subjects||[]).filter(s=>subjectIdsWithMarks.has(s.id));
const rankedStudents = students.map(s => {
let totalGradeVal=0, totalMark=0, subjCount=0;
const gradeSummary={A:0,B:0,C:0,D:0,E:0,F:0,TH:0};
activeSubjects.forEach(sub=>{
const key=s.id+'_'+sub.id;
const md=marksMap[key];
if(md){
// FIX: Exclude PSV & PJK from summary
if(!isExcluded(sub.name) && md.grade !== 'TH'){
totalGradeVal+=getGradeValue(md.grade);
totalMark+=md.mark;
subjCount++;
}
if(!isExcluded(sub.name)){
if(gradeSummary.hasOwnProperty(md.grade))gradeSummary[md.grade]++;
}
}
});
const gpm = subjCount>0 ? (totalGradeVal/subjCount) : 99.99;
const summary = Object.entries(gradeSummary).filter(([_,c])=>c>0).map(([g,c])=>c+g).join(' ')||'-';
return {
id: s.id,
name: s.name,
gpm: gpm,
totalMark: totalMark,
summary: summary,
subjCount: subjCount
};
});
rankedStudents.sort((a, b) => {
if (a.gpm !== b.gpm) {
return a.gpm - b.gpm;
}
if (a.totalMark !== b.totalMark) {
return b.totalMark - a.totalMark;
}
return a.name.localeCompare(b.name);
});
bar.style.width='85%';
const tbody=document.getElementById('rankingTbody');
tbody.innerHTML='';
let rankCounter = 1;
let prevGpm = null;
let prevTotalMark = null;
let prevName = null;
let actualRank = 1;
rankedStudents.forEach((s, i) => {
if (prevGpm !== null && s.gpm === prevGpm && s.totalMark === prevTotalMark && s.name === prevName) {
} else {
actualRank = rankCounter;
}
let medalIcon = '';
if(actualRank === 1) medalIcon = '🥇 ';
else if(actualRank === 2) medalIcon = '🥈 ';
else if(actualRank === 3) medalIcon = '🥉 ';
const tr=document.createElement('tr');
tr.innerHTML=`
<td class="font-bold text-center" style="font-size:16px">${medalIcon}${actualRank}</td>
<td class="font-semibold">${escHtml(s.name)}</td>
<td class="subj-cell"><span class="font-bold" style="color:var(--gold2)">${s.subjCount > 0 ? s.gpm.toFixed(2) : '-'}</span></td>
<td class="subj-cell"><span class="font-bold">${s.totalMark>0?s.totalMark:'-'}</span></td>
<td><span class="text-sm font-semibold">${s.summary}</span></td>`;
tbody.appendChild(tr);
prevGpm = s.gpm;
prevTotalMark = s.totalMark;
prevName = s.name;
rankCounter++;
});
document.getElementById('rankingTitle').textContent='Kedudukan Murid — '+cls.toUpperCase();
document.getElementById('rankingFooter').textContent=`Jumlah: ${students.length} murid`;
bar.style.width='100%';
setTimeout(()=>{
hideProgress('rankingProgress');
document.getElementById('rankingTableArea').style.display='block';
},300);
}catch(e){
hideProgress('rankingProgress');
showToast(e.message||'Gagal memuatkan ranking','error');
document.getElementById('rankingPlaceholder').style.display='block';
}
}
/* ===== RANKING TINGKATAN ===== */
function resetRankingTingkatanPage() {
document.getElementById('rtTingkatan').value = '';
document.getElementById('rtTableArea').style.display = 'none';
document.getElementById('rtPlaceholder').style.display = 'block';
document.getElementById('rtPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan';
hideProgress('rtProgress');
rtAllRankedStudents = [];
}
async function onRankingTingkatanChange() {
const tk = document.getElementById('rtTingkatan').value;
if (!tk) {
resetRankingTingkatanPage();
return;
}
document.getElementById('rtPlaceholder').style.display = 'none';
showProgress('rtProgress');
const bar = document.getElementById('rtProgressBar');
bar.style.width = '0%';
try {
bar.style.width = '15%';
const { data: students, error: sErr } = await db.from('students').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('class_name')
.order('name');
if (sErr) throw sErr;
const tingkatanStudents = (students || []).filter(s => getTingkatan(s.class_name) === tk);
if (!tingkatanStudents.length) {
hideProgress('rtProgress');
document.getElementById('rtPlaceholder').style.display = 'block';
document.getElementById('rtPlaceholder').querySelector('p').textContent = 'Tiada murid dalam tingkatan this';
document.getElementById('rtTableArea').style.display = 'none';
return;
}
bar.style.width = '30%';
const { data: subjects, error: sjErr } = await db.from('subjects').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('name');
if (sjErr) throw sjErr;
bar.style.width = '50%';
const studentIds = tingkatanStudents.map(s => s.id);
const { data: allMarks, error: mErr } = await db.from('marks').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.in('student_id', studentIds);
if (mErr) throw mErr;
bar.style.width = '70%';
const marksMap = {};
(allMarks || []).forEach(m => {
const key = m.student_id + '_' + m.subject_id;
marksMap[key] = { mark: m.mark, grade: m.grade };
});
const subjectIdsWithMarks = new Set((allMarks || []).map(m => m.subject_id));
const activeSubjects = (subjects || []).filter(s => subjectIdsWithMarks.has(s.id));
rtAllRankedStudents = tingkatanStudents.map(s => {
let totalGradeVal = 0, totalMark = 0, subjCount = 0;
const gradeSummary = { A: 0, B: 0, C: 0, D: 0, E: 0, F: 0, TH:0 };
activeSubjects.forEach(sub => {
const key = s.id + '_' + sub.id;
const md = marksMap[key];
if (md) {
// FIX: Exclude PSV & PJK from summary
if (!isExcluded(sub.name) && md.grade !== 'TH') {
totalGradeVal += getGradeValue(md.grade);
totalMark += md.mark;
subjCount++;
}
if (!isExcluded(sub.name)) {
if(gradeSummary.hasOwnProperty(md.grade))gradeSummary[md.grade]++;
}
}
});
const gpm = subjCount > 0 ? (totalGradeVal / subjCount) : 99.99;
const summary = Object.entries(gradeSummary).filter(([_, c]) => c > 0).map(([g, c]) => c + g).join(' ') || '-';
return {
id: s.id,
name: s.name,
class_name: s.class_name,
gpm: gpm,
totalMark: totalMark,
summary: summary,
subjCount: subjCount
};
});
rtAllRankedStudents.sort((a, b) => {
if (a.gpm !== b.gpm) {
return a.gpm - b.gpm;
}
if (a.totalMark !== b.totalMark) {
return b.totalMark - a.totalMark;
}
return a.name.localeCompare(b.name);
});
bar.style.width = '100%';
setTimeout(() => {
hideProgress('rtProgress');
document.getElementById('rtTableArea').style.display = 'block';
renderRankingTingkatanTable();
}, 300);
} catch (e) {
hideProgress('rtProgress');
showToast(e.message || 'Gagal memuatkan ranking tingkatan', 'error');
document.getElementById('rtPlaceholder').style.display = 'block';
}
}
function renderRankingTingkatanTable() {
const tbody = document.getElementById('rtTbody');
tbody.innerHTML = '';
let rankCounter = 1;
let prevGpm = null;
let prevTotalMark = null;
let prevName = null;
let actualRank = 1;
rtAllRankedStudents.forEach((s, i) => {
if (prevGpm !== null && s.gpm === prevGpm && s.totalMark === prevTotalMark && s.name === prevName) {
} else {
actualRank = rankCounter;
}
let medalIcon = '';
if(actualRank === 1) medalIcon = '🥇 ';
else if(actualRank === 2) medalIcon = '🥈 ';
else if(actualRank === 3) medalIcon = '🥉 ';
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="font-bold text-center" style="font-size:16px">${medalIcon}${actualRank}</td>
<td class="font-semibold">${escHtml(s.name)}</td>
<td><span class="badge badge-success">${escHtml(s.class_name)}</span></td>
<td class="subj-cell"><span class="font-bold" style="color:var(--gold2)">${s.subjCount > 0 ? s.gpm.toFixed(2) : '-'}</span></td>
<td class="subj-cell"><span class="font-bold">${s.totalMark > 0 ? s.totalMark : '-'}</span></td>
<td><span class="text-sm font-semibold">${s.summary}</span></td>`;
tbody.appendChild(tr);
prevGpm = s.gpm;
prevTotalMark = s.totalMark;
prevName = s.name;
rankCounter++;
});
document.getElementById('rtTitle').textContent = `Kedudukan Murid — Tingkatan ${document.getElementById('rtTingkatan').value}`;
document.getElementById('rtFooter').textContent = `Jumlah: ${rtAllRankedStudents.length} murid`;
}
/* ===== RANKING MATA PELAJARAN ===== */
function resetRankingSubjectPage() {
document.getElementById('rsTingkatan').value = '';
document.getElementById('rsSubject').innerHTML = '<option value="">-- Pilih Mata Pelajaran --</option>';
document.getElementById('rsTableArea').style.display = 'none';
document.getElementById('rsPlaceholder').style.display = 'block';
document.getElementById('rsPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan dan mata pelajaran';
hideProgress('rsProgress');
}
async function onRankingSubjectTingkatanChange() {
const tk = document.getElementById('rsTingkatan').value;
const ss = document.getElementById('rsSubject');
ss.innerHTML = '<option value="">-- Pilih Mata Pelajaran --</option>';
document.getElementById('rsTableArea').style.display = 'none';
document.getElementById('rsPlaceholder').style.display = 'block';
document.getElementById('rsPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan dan mata pelajaran';
if (!tk) return;
try {
const { data } = await db.from('subjects').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('name');
(data || []).forEach(s => {
const o = document.createElement('option');
o.value = s.id;
o.textContent = s.code ? s.code + ' — ' + s.name : s.name;
ss.appendChild(o);
});
} catch (e) { console.error(e); }
}
async function onRankingSubjectChange() {
const tk = document.getElementById('rsTingkatan').value;
const subjId = document.getElementById('rsSubject').value;
if (!tk || !subjId) {
document.getElementById('rsTableArea').style.display = 'none';
document.getElementById('rsPlaceholder').style.display = 'block';
document.getElementById('rsPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan dan mata pelajaran';
return;
}
document.getElementById('rsPlaceholder').style.display = 'none';
showProgress('rsProgress');
const bar = document.getElementById('rsProgressBar');
bar.style.width = '0%';
try {
bar.style.width = '15%';
const { data: studentsData, error: sErr } = await db.from('students').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('class_name')
.order('name');
if (sErr) throw sErr;
const tingkatanStudents = (studentsData || []).filter(s => getTingkatan(s.class_name) === tk);
if (!tingkatanStudents.length) {
hideProgress('rsProgress');
document.getElementById('rsPlaceholder').style.display = 'block';
document.getElementById('rsPlaceholder').querySelector('p').textContent = 'Tiada murid dalam tingkatan this';
return;
}
bar.style.width = '50%';
const { data: marksData, error: mErr } = await db.from('marks').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.eq('subject_id', subjId);
if (mErr) throw mErr;
const marksMap = {};
(marksData || []).forEach(m => {
marksMap[m.student_id] = m;
});
bar.style.width = '70%';
const rankedStudents = tingkatanStudents
.filter(s => marksMap[s.id])
.map(s => {
const markRecord = marksMap[s.id];
return {
id: s.id,
name: s.name,
class_name: s.class_name,
mark: markRecord.mark,
grade: markRecord.grade
};
});
if (rankedStudents.length === 0) {
hideProgress('rsProgress');
document.getElementById('rsTableArea').style.display = 'none';
document.getElementById('rsPlaceholder').style.display = 'block';
document.getElementById('rsPlaceholder').querySelector('p').textContent = 'Tiada murid mempunyai markah untuk mata pelajaran this.';
return;
}
rankedStudents.sort((a, b) => {
if (a.mark !== b.mark) {
return b.mark - a.mark;
}
return a.name.localeCompare(b.name);
});
bar.style.width = '100%';
setTimeout(() => {
hideProgress('rsProgress');
document.getElementById('rsTableArea').style.display = 'block';
renderRankingSubjectTable(rankedStudents, tk);
}, 300);
} catch (e) {
hideProgress('rsProgress');
showToast(e.message || 'Gagal memuatkan ranking mata pelajaran', 'error');
document.getElementById('rsPlaceholder').style.display = 'block';
}
}
function renderRankingSubjectTable(rankedStudents, tk) {
const tbody = document.getElementById('rsTbody');
tbody.innerHTML = '';
rankedStudents.forEach((s, i) => {
const actualRank = i + 1;
let medalIcon = '';
if(actualRank === 1) medalIcon = '🥇 ';
else if(actualRank === 2) medalIcon = '🥈 ';
else if(actualRank === 3) medalIcon = '🥉 ';
const displayMark = s.grade === 'TH' ? 'TH' : s.mark;
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="font-bold text-center" style="font-size:16px">${medalIcon}${actualRank}</td>
<td class="font-semibold">${escHtml(s.name)}</td>
<td><span class="badge badge-success">${escHtml(s.class_name)}</span></td>
<td class="subj-cell"><span class="font-bold">${displayMark}</span></td>
<td class="subj-cell"><span class="text-sm font-semibold">${s.grade}</span></td>`;
tbody.appendChild(tr);
});
const subjSelect = document.getElementById('rsSubject');
const subjName = subjSelect.options[subjSelect.selectedIndex].text;
document.getElementById('rsTitle').textContent = `Kedudukan Murid — Tingkatan ${tk} — ${subjName}`;
document.getElementById('rsFooter').textContent = `Jumlah: ${rankedStudents.length} murid`;
}
/* ===== ANALISIS MATA PELAJARAN ===== */
function resetAnalisisSubjectPage() {
document.getElementById('asTingkatan').value = '';
document.getElementById('asSubject').innerHTML = '<option value="">-- Pilih Mata Pelajaran --</option>';
document.getElementById('asTableArea').style.display = 'none';
document.getElementById('asPlaceholder').style.display = 'block';
document.getElementById('asPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan dan mata pelajaran';
hideProgress('asProgress');
}
async function onAnalisisSubjectTingkatanChange() {
const tk = document.getElementById('asTingkatan').value;
const ss = document.getElementById('asSubject');
ss.innerHTML = '<option value="">-- Pilih Mata Pelajaran --</option>';
document.getElementById('asTableArea').style.display = 'none';
document.getElementById('asPlaceholder').style.display = 'block';
document.getElementById('asPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan dan mata pelajaran';
if (!tk) return;
try {
const { data } = await db.from('subjects').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('name');
(data || []).forEach(s => {
const o = document.createElement('option');
o.value = s.id;
o.textContent = s.code ? s.code + ' — ' + s.name : s.name;
ss.appendChild(o);
});
} catch (e) { console.error(e); }
}
async function onAnalisisSubjectChange() {
const tk = document.getElementById('asTingkatan').value;
const subjId = document.getElementById('asSubject').value;
if (!tk || !subjId) {
document.getElementById('asTableArea').style.display = 'none';
document.getElementById('asPlaceholder').style.display = 'block';
document.getElementById('asPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan dan mata pelajaran';
return;
}
document.getElementById('asTableArea').style.display = 'none';
document.getElementById('asPlaceholder').style.display = 'none';
showProgress('asProgress');
const bar = document.getElementById('asProgressBar');
bar.style.width = '0%';
try {
bar.style.width = '15%';
const { data: studentsData, error: sErr } = await db.from('students').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('class_name')
.order('name');
if (sErr) throw sErr;
const tingkatanStudents = (studentsData || []).filter(s => getTingkatan(s.class_name) === tk);
if (!tingkatanStudents.length) {
hideProgress('asProgress');
document.getElementById('asPlaceholder').style.display = 'block';
document.getElementById('asPlaceholder').querySelector('p').textContent = 'Tiada murid dalam tingkatan this';
document.getElementById('asTableArea').style.display = 'none';
return;
}
bar.style.width = '50%';
const { data: marksData, error: mErr } = await db.from('marks').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.eq('subject_id', subjId);
if (mErr) throw mErr;
const marksMap = {};
(marksData || []).forEach(m => {
marksMap[m.student_id] = m;
});
bar.style.width = '70%';
const classGroups = {};
tingkatanStudents.forEach(s => {
if (!classGroups[s.class_name]) {
classGroups[s.class_name] = [];
}
classGroups[s.class_name].push(s);
});
const classes = Object.keys(classGroups).sort();
const analysisData = [];
const totals = { A:0, B:0, C:0, D:0, E:0, F:0, TH:0, totalHadir:0, totalGradeSum:0, totalStudents:0 };
classes.forEach(className => {
const studentsInClass = classGroups[className];
const counts = { A:0, B:0, C:0, D:0, E:0, F:0, TH:0 };
let gradeSum = 0;
let bilHadir = 0;
const totalStudents = studentsInClass.length;
studentsInClass.forEach(st => {
const markRecord = marksMap[st.id];
if (markRecord) {
const grade = markRecord.grade;
if (counts.hasOwnProperty(grade)) {
counts[grade]++;
if (grade !== 'TH') {
bilHadir++;
gradeSum += GRADE_SCALE[grade];
}
}
}
});
const gredPurata = bilHadir > 0 ? (gradeSum / bilHadir).toFixed(2) : '-';
analysisData.push({
className,
counts,
totalStudents,
bilHadir,
gredPurata,
gradeSum
});
totals.A += counts.A; totals.B += counts.B; totals.C += counts.C;
totals.D += counts.D; totals.E += counts.E; totals.F += counts.F; totals.TH += counts.TH;
totals.totalHadir += bilHadir;
totals.totalGradeSum += gradeSum;
totals.totalStudents += totalStudents;
});
totals.gredPurata = totals.totalHadir > 0 ? (totals.totalGradeSum / totals.totalHadir).toFixed(2) : '-';
bar.style.width = '100%';
setTimeout(() => {
hideProgress('asProgress');
document.getElementById('asTableArea').style.display = 'block';
renderAnalisisTable(analysisData, totals, tk);
}, 300);
} catch (e) {
hideProgress('asProgress');
showToast(e.message || 'Gagal memuatkan analisis mata pelajaran', 'error');
document.getElementById('asPlaceholder').style.display = 'block';
}
}
function renderAnalisisTable(data, totals, tk) {
const tbody = document.getElementById('asTbody');
const tfoot = document.getElementById('asTfoot');
tbody.innerHTML = '';
tfoot.innerHTML = '';
const grades = ['A', 'B', 'C', 'D', 'E', 'F', 'TH'];
data.forEach(row => {
let cells = `<td class="font-semibold">${escHtml(row.className)}</td>`;
grades.forEach(g => {
const count = row.counts[g];
const percentage = row.totalStudents > 0 ? ((count / row.totalStudents) * 100).toFixed(1) : '0.0';
cells += `<td class="subj-cell font-semibold">${count}</td>`;
cells += `<td class="subj-cell" style="color:var(--muted);font-size:12px">${percentage}%</td>`;
});
cells += `<td class="subj-cell font-bold">${row.bilHadir}</td>`;
cells += `<td class="subj-cell font-bold" style="color:var(--gold2)">${row.gredPurata}</td>`;
const tr = document.createElement('tr');
tr.innerHTML = cells;
tbody.appendChild(tr);
});
let footerCells = `<td class="font-bold">Jumlah</td>`;
grades.forEach(g => {
const count = totals[g];
const percentage = totals.totalStudents > 0 ? ((count / totals.totalStudents) * 100).toFixed(1) : '0.0';
footerCells += `<td class="subj-cell font-bold">${count}</td>`;
footerCells += `<td class="subj-cell" style="color:var(--muted);font-size:12px">${percentage}%</td>`;
});
footerCells += `<td class="subj-cell font-bold">${totals.totalHadir}</td>`;
footerCells += `<td class="subj-cell font-bold" style="color:var(--gold2)">${totals.gredPurata}</td>`;
tfoot.innerHTML = footerCells;
const subjSelect = document.getElementById('asSubject');
const subjName = subjSelect.options[subjSelect.selectedIndex].text;
document.getElementById('asTitle').textContent = `Analisis Mata Pelajaran — Tingkatan ${tk} — ${subjName}`;
}
/* ===== ANALISIS TINGKATAN ===== */
function resetAnalisisTingkatanPage() {
document.getElementById('atTingkatan').value = '';
document.getElementById('atTableArea').style.display = 'none';
document.getElementById('atPlaceholder').style.display = 'block';
document.getElementById('atPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan';
hideProgress('atProgress');
}
async function onAnalisisTingkatanChange() {
const tk = document.getElementById('atTingkatan').value;
if (!tk) {
resetAnalisisTingkatanPage();
return;
}
document.getElementById('atTableArea').style.display = 'none';
document.getElementById('atPlaceholder').style.display = 'none';
showProgress('atProgress');
const bar = document.getElementById('atProgressBar');
bar.style.width = '0%';
try {
bar.style.width = '15%';
const { data: subjectsData, error: sjErr } = await db.from('subjects').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('name');
if (sjErr) throw sjErr;
bar.style.width = '30%';
const { data: studentsData, error: sErr } = await db.from('students').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('class_name')
.order('name');
if (sErr) throw sErr;
const tingkatanStudents = (studentsData || []).filter(s => getTingkatan(s.class_name) === tk);
if (!tingkatanStudents.length) {
hideProgress('atProgress');
document.getElementById('atPlaceholder').style.display = 'block';
document.getElementById('atPlaceholder').querySelector('p').textContent = 'Tiada murid dalam tingkatan this';
document.getElementById('atTableArea').style.display = 'none';
return;
}
bar.style.width = '50%';
const studentIds = tingkatanStudents.map(s => s.id);
const { data: allMarks, error: mErr } = await db.from('marks').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.in('student_id', studentIds);
if (mErr) throw mErr;
bar.style.width = '70%';
const totalStudentsInTingkatan = tingkatanStudents.length;
const totals = { A:0, B:0, C:0, D:0, E:0, F:0, TH:0, totalHadir:0, totalGradeSum:0 };
const analysisData = [];
const subjectGroups = {};
(allMarks || []).forEach(m => {
if (!subjectGroups[m.subject_id]) {
subjectGroups[m.subject_id] = [];
}
subjectGroups[m.subject_id].push(m);
});
subjectsData.forEach(sub => {
const marksForSubject = subjectGroups[sub.id];
if (!marksForSubject || marksForSubject.length === 0) return;
// FIX: Skip excluded subjects so they don't appear in the table
if (isExcluded(sub.name)) return;
const counts = { A:0, B:0, C:0, D:0, E:0, F:0, TH:0 };
let gradeSum = 0;
let bilHadir = 0;
marksForSubject.forEach(m => {
const grade = m.grade;
if (counts.hasOwnProperty(grade)) {
counts[grade]++;
if (grade !== 'TH') {
bilHadir++;
gradeSum += GRADE_SCALE[grade];
}
}
});
const gredPurata = bilHadir > 0 ? (gradeSum / bilHadir).toFixed(2) : '-';
analysisData.push({
subjectName: sub.code ? `${sub.code} - ${sub.name}` : sub.name,
counts,
bilHadir,
gredPurata,
gradeSum
});
totals.A += counts.A; totals.B += counts.B; totals.C += counts.C;
totals.D += counts.D; totals.E += counts.E; totals.F += counts.F; totals.TH += counts.TH;
totals.totalHadir += bilHadir;
totals.totalGradeSum += gradeSum;
});
totals.gredPurata = totals.totalHadir > 0 ? (totals.totalGradeSum / totals.totalHadir).toFixed(2) : '-';
if (analysisData.length === 0) {
hideProgress('atProgress');
document.getElementById('atTableArea').style.display = 'none';
document.getElementById('atPlaceholder').style.display = 'block';
document.getElementById('atPlaceholder').querySelector('p').textContent = 'Tiada markah dimasukkan untuk tingkatan ini.';
return;
}
bar.style.width = '100%';
setTimeout(() => {
hideProgress('atProgress');
document.getElementById('atTableArea').style.display = 'block';
renderAnalisisTingkatanTable(analysisData, totals, tk, totalStudentsInTingkatan);
}, 300);
} catch (e) {
hideProgress('atProgress');
showToast(e.message || 'Gagal memuatkan analisis tingkatan', 'error');
document.getElementById('atPlaceholder').style.display = 'block';
}
}
function renderAnalisisTingkatanTable(data, totals, tk, totalStudents) {
const tbody = document.getElementById('atTbody');
const tfoot = document.getElementById('atTfoot');
tbody.innerHTML = '';
tfoot.innerHTML = '';
const grades = ['A', 'B', 'C', 'D', 'E', 'F', 'TH'];
data.forEach(row => {
let cells = `<td class="font-semibold">${escHtml(row.subjectName)}</td>`;
grades.forEach(g => {
const count = row.counts[g];
const percentage = totalStudents > 0 ? ((count / totalStudents) * 100).toFixed(1) : '0.0';
cells += `<td class="subj-cell font-semibold">${count}</td>`;
cells += `<td class="subj-cell" style="color:var(--muted);font-size:12px">${percentage}%</td>`;
});
cells += `<td class="subj-cell font-bold">${row.bilHadir}</td>`;
cells += `<td class="subj-cell font-bold" style="color:var(--gold2)">${row.gredPurata}</td>`;
const tr = document.createElement('tr');
tr.innerHTML = cells;
tbody.appendChild(tr);
});
let footerCells = `<td class="font-bold">Jumlah</td>`;
grades.forEach(g => {
const count = totals[g];
const percentage = totalStudents > 0 ? ((count / totalStudents) * 100).toFixed(1) : '0.0';
footerCells += `<td class="subj-cell font-bold">${count}</td>`;
footerCells += `<td class="subj-cell" style="color:var(--muted);font-size:12px">${percentage}%</td>`;
});
footerCells += `<td class="subj-cell font-bold">${totals.totalHadir}</td>`;
footerCells += `<td class="subj-cell font-bold" style="color:var(--gold2)">${totals.gredPurata}</td>`;
tfoot.innerHTML = footerCells;
document.getElementById('atTitle').textContent = `Analisis Pencapaian — Tingkatan ${tk}`;
}
/* ===== MISS 1-3 A ===== */
function resetMissAPage() {
document.getElementById('maTingkatan').value = '';
document.getElementById('maTableArea').style.display = 'none';
document.getElementById('maStats').style.display = 'none';
document.getElementById('maPlaceholder').style.display = 'block';
document.getElementById('maPlaceholder').querySelector('p').textContent = 'Sila pilih tingkatan';
hideProgress('maProgress');
}
async function onMissATingkatanChange() {
const tk = document.getElementById('maTingkatan').value;
if (!tk) {
resetMissAPage();
return;
}
document.getElementById('maTableArea').style.display = 'none';
document.getElementById('maStats').style.display = 'none';
document.getElementById('maPlaceholder').style.display = 'none';
showProgress('maProgress');
const bar = document.getElementById('maProgressBar');
bar.style.width = '0%';
try {
bar.style.width = '15%';
const { data: studentsData, error: sErr } = await db.from('students').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('class_name')
.order('name');
if (sErr) throw sErr;
const tingkatanStudents = (studentsData || []).filter(s => getTingkatan(s.class_name) === tk);
if (!tingkatanStudents.length) {
hideProgress('maProgress');
document.getElementById('maPlaceholder').style.display = 'block';
document.getElementById('maPlaceholder').querySelector('p').textContent = 'Tiada murid dalam tingkatan ini.';
return;
}
bar.style.width = '30%';
const { data: subjectsData, error: sjErr } = await db.from('subjects').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('name');
if (sjErr) throw sjErr;
bar.style.width = '50%';
const studentIds = tingkatanStudents.map(s => s.id);
const { data: allMarks, error: mErr } = await db.from('marks').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.in('student_id', studentIds);
if (mErr) throw mErr;
bar.style.width = '70%';
const subjectMap = {};
subjectsData.forEach(s => subjectMap[s.id] = s);
const marksByStudent = {};
allMarks.forEach(m => {
if (!marksByStudent[m.student_id]) marksByStudent[m.student_id] = [];
marksByStudent[m.student_id].push(m);
});
const missAStudents = [];
let totalStudentsWithMarks = 0;
let miss1 = 0, miss2 = 0, miss3 = 0;
tingkatanStudents.forEach(s => {
const studentMarks = marksByStudent[s.id];
if (!studentMarks || studentMarks.length === 0) return;
totalStudentsWithMarks++;
let nonACount = 0;
const nonASubjects = [];
const gradeSummary = { A:0, B:0, C:0, D:0, E:0, F:0, TH:0 };
studentMarks.forEach(m => {
const subj = subjectMap[m.subject_id];
if (!subj) return;
// FIX: Skip excluded subjects completely
if (isExcluded(subj.name)) return;
if (gradeSummary.hasOwnProperty(m.grade)) gradeSummary[m.grade]++;
if (m.grade !== 'A') {
nonACount++;
const displayMark = m.grade === 'TH' ? 'TH' : `${m.mark} (${m.grade})`;
nonASubjects.push(`${subj.name} [${displayMark}]`);
}
});
if (nonACount >= 1 && nonACount <= 3) {
const summary = Object.entries(gradeSummary).filter(([_,c])=>c>0).map(([g,c])=>c+g).join(' ')||'-';
missAStudents.push({
name: s.name,
class_name: s.class_name,
nonACount,
nonASubjects: nonASubjects.join(', '),
summary
});
if (nonACount === 1) miss1++;
else if (nonACount === 2) miss2++;
else if (nonACount === 3) miss3++;
}
});
bar.style.width = '100%';
setTimeout(() => {
hideProgress('maProgress');
const statsEl = document.getElementById('maStats');
statsEl.innerHTML = `
<div class="stat-card fade-in">
<div class="stat-icon" style="background:rgba(59,130,246,.1);color:var(--info)"><i class="fas fa-users"></i></div>
<div class="stat-value">${totalStudentsWithMarks}</div>
<div class="stat-label">Jumlah Murid Bertanding</div>
</div>
<div class="stat-card fade-in" style="animation-delay:.1s">
<div class="stat-icon" style="background:rgba(245,158,11,.1);color:var(--gold)"><i class="fas fa-medal"></i></div>
<div class="stat-value">${miss1}</div>
<div class="stat-label">Miss 1 A</div>
</div>
<div class="stat-card fade-in" style="animation-delay:.2s">
<div class="stat-icon" style="background:rgba(245,158,11,.1);color:var(--gold2)"><i class="fas fa-medal"></i></div>
<div class="stat-value">${miss2}</div>
<div class="stat-label">Miss 2 A</div>
</div>
<div class="stat-card fade-in" style="animation-delay:.3s">
<div class="stat-icon" style="background:rgba(239,68,68,.1);color:var(--danger)"><i class="fas fa-medal"></i></div>
<div class="stat-value">${miss3}</div>
<div class="stat-label">Miss 3 A</div>
</div>
`;
statsEl.style.display = 'grid';
renderMissATable(missAStudents, tk);
}, 300);
} catch (e) {
hideProgress('maProgress');
showToast(e.message || 'Gagal memuatkan data Miss A', 'error');
document.getElementById('maPlaceholder').style.display = 'block';
}
}
function renderMissATable(data, tk) {
const tbody = document.getElementById('maTbody');
tbody.innerHTML = '';
if (data.length === 0) {
document.getElementById('maTableArea').style.display = 'none';
document.getElementById('maPlaceholder').style.display = 'block';
document.getElementById('maPlaceholder').querySelector('p').textContent = 'Tiada murid yang miss 1-3 A.';
return;
}
data.forEach((s, i) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${i + 1}</td>
<td class="font-semibold">${escHtml(s.name)}</td>
<td><span class="badge badge-success">${escHtml(s.class_name)}</span></td>
<td class="subj-cell"><span class="badge ${s.nonACount === 1 ? 'badge-success' : s.nonACount === 2 ? 'badge-warning' : 'badge-danger'}">${s.nonACount}</span></td>
<td style="font-size:13px">${escHtml(s.summary)}</td>
<td style="font-size:13px">${escHtml(s.nonASubjects)}</td>`;
tbody.appendChild(tr);
});
document.getElementById('maTableArea').style.display = 'block';
document.getElementById('maTitle').textContent = `Senarai Murid Miss 1-3 A — Tingkatan ${tk}`;
}
/* ===== SLIP PEPERIKSAAN ===== */
function resetSlipPage() {
document.getElementById('slipTingkatan').value = '';
document.getElementById('slipKelas').innerHTML = '<option value="">-- Pilih Kelas --</option>';
document.getElementById('slipStudent').innerHTML = '<option value="">-- Pilih Nama Murid --</option>';
document.getElementById('slipArea').style.display = 'none';
document.getElementById('slipActions').style.display = 'none';
document.getElementById('slipPlaceholder').style.display = 'block';
}
async function onSlipTingkatanChange() {
const tk = document.getElementById('slipTingkatan').value;
const ks = document.getElementById('slipKelas');
ks.innerHTML = '<option value="">-- Pilih Kelas --</option>';
document.getElementById('slipStudent').innerHTML = '<option value="">-- Pilih Nama Murid --</option>';
document.getElementById('slipArea').style.display = 'none';
document.getElementById('slipActions').style.display = 'none';
document.getElementById('slipPlaceholder').style.display = 'block';
if (!tk) return;
try {
const { data } = await db.from('students').select('class_name')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType);
const classes = [...new Set((data || []).filter(s => getTingkatan(s.class_name) === tk).map(s => s.class_name))].sort();
classes.forEach(c => {
const o = document.createElement('option');
o.value = c;
o.textContent = c;
ks.appendChild(o);
});
} catch (e) { console.error(e); }
}
async function onSlipKelasChange() {
const cls = document.getElementById('slipKelas').value;
const ss = document.getElementById('slipStudent');
ss.innerHTML = '<option value="">-- Pilih Nama Murid --</option>';
document.getElementById('slipArea').style.display = 'none';
document.getElementById('slipActions').style.display = 'none';
document.getElementById('slipPlaceholder').style.display = 'block';
if (!cls) return;
try {
const { data } = await db.from('students').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.eq('class_name', cls)
.order('name');
(data || []).forEach(s => {
const o = document.createElement('option');
o.value = s.id;
o.textContent = s.name;
ss.appendChild(o);
});
} catch (e) { console.error(e); }
}
async function onSlipStudentChange() {
const studentId = document.getElementById('slipStudent').value;
if (!studentId) {
document.getElementById('slipArea').style.display = 'none';
document.getElementById('slipActions').style.display = 'none';
document.getElementById('slipPlaceholder').style.display = 'block';
return;
}
document.getElementById('slipPlaceholder').style.display = 'none';
const slipArea = document.getElementById('slipArea');
slipArea.innerHTML = '<div class="text-center py-8 loading-pulse" style="color:var(--muted)">Memuatkan slip...</div>';
slipArea.style.display = 'block';
try {
// Fetch student details
const { data: studentData } = await db.from('students').select('*')
.eq('id', studentId)
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.single();
if (!studentData) {
slipArea.innerHTML = '<p class="text-center text-red-500">Murid tidak dijumpai.</p>';
return;
}
// Fetch subjects
const { data: subjectsData } = await db.from('subjects').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.order('name');
const subjectMap = {};
subjectsData.forEach(s => subjectMap[s.id] = s);
// Fetch all students in the tingkatan (to calculate ranking)
const tk = getTingkatan(studentData.class_name);
const { data: allTingkatanStudents } = await db.from('students').select('id, name, class_name')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType);
const tingkatanStudents = (allTingkatanStudents || []).filter(s => getTingkatan(s.class_name) === tk);
const classStudents = tingkatanStudents.filter(s => s.class_name === studentData.class_name);
// Fetch marks for all tingkatan students
const studentIds = tingkatanStudents.map(s => s.id);
const { data: allMarks } = await db.from('marks').select('*')
.eq('exam_year', selectedYear)
.eq('exam_type', selectedExamType)
.in('student_id', studentIds);
// Organize marks by student
const marksByStudent = {};
(allMarks || []).forEach(m => {
if (!marksByStudent[m.student_id]) marksByStudent[m.student_id] = [];
marksByStudent[m.student_id].push(m);
});
// Calculate stats for all tingkatan students
const tingkatanStats = tingkatanStudents.map(s => ({
id: s.id,
name: s.name,
class_name: s.class_name,
...calculateStats(s.id, marksByStudent, subjectMap)
}));
// Sort to get rankings
tingkatanStats.sort((a, b) => {
if (a.gpm !== b.gpm) return a.gpm - b.gpm;
if (a.totalMark !== b.totalMark) return b.totalMark - a.totalMark;
return a.name.localeCompare(b.name);
});
const classStats = tingkatanStats.filter(s => s.class_name === studentData.class_name);
// Find ranks
let classRank = '-';
let tingkatanRank = '-';
classStats.forEach((s, i) => {
if (s.id === parseInt(studentId)) {
classRank = `${i + 1}/${classStats.length}`;
}
});
tingkatanStats.forEach((s, i) => {
if (s.id === parseInt(studentId)) {
tingkatanRank = `${i + 1}/${tingkatanStats.length}`;
}
});
// Get selected student's stats
const studentStats = tingkatanStats.find(s => s.id === parseInt(studentId)) || { gpm: 99.99, totalMark: 0, summary: '-', subjCount: 0, totalSubjects: 0 };
const studentMarks = marksByStudent[studentId] || [];
// Build table rows
let tableRows = '';
studentMarks.forEach(m => {
const subj = subjectMap[m.subject_id];
if (!subj) return;
const displayMark = m.grade === 'TH' ? 'TH' : m.mark;
tableRows += `
<tr>
<td style="border:1px solid #ccc;padding:8px;">${escHtml(subj.name)}</td>
<td style="border:1px solid #ccc;padding:8px;text-align:center;">${displayMark}</td>
<td style="border:1px solid #ccc;padding:8px;text-align:center;">${m.grade}</td>
</tr>`;
});
const gpmDisplay = studentStats.subjCount > 0 ? studentStats.gpm.toFixed(2) : '-';
const examSession = `${selectedExamType} ${selectedYear}`;
// Render Slip
slipArea.innerHTML = `
<div class="slip-container">
<div class="slip-header">
<img src="https://i.ibb.co/Y74tqDVV/logo-web.jpg" alt="Logo" class="slip-logo">
<div class="slip-school-info">
<div class="slip-school-name">SMK ABU BAKAR AL-BAQIR</div>
<div class="slip-exam-name">${examSession}</div>
<div style="font-size:12px;color:var(--text2)">SLIP PEPERIKSAAN</div>
</div>
<div style="width:80px"></div>
</div>
<div class="slip-section">
<div class="slip-grid">
<div class="slip-grid-item"><span>Nama Murid:</span> <span style="font-weight:600">${escHtml(studentData.name)}</span></div>
<div class="slip-grid-item"><span>Kelas:</span> <span style="font-weight:600">${escHtml(studentData.class_name)}</span></div>
</div>
</div>
<div class="slip-section">
<table class="slip-table">
<thead>
<tr>
<th style="width:60%">Mata Pelajaran</th>
<th style="width:20%">Markah</th>
<th style="width:20%">Gred</th>
</tr>
</thead>
<tbody>
${tableRows || '<tr><td colspan="3" style="text-align:center;color:var(--muted)">Tiada markah</td></tr>'}
</tbody>
</table>
</div>
<div class="slip-section">
<div class="slip-summary">
<div class="slip-summary-item"><span>Bil. Mata Pelajaran:</span> <span style="font-weight:600">${studentStats.totalSubjects}</span></div>
<div class="slip-summary-item"><span>Gred Purata Murid (GPM):</span> <span style="font-weight:600">${gpmDisplay}</span></div>
<div class="slip-summary-item"><span>Jumlah Markah:</span> <span style="font-weight:600">${studentStats.totalMark > 0 ? studentStats.totalMark : '-'}</span></div>
<div class="slip-summary-item"><span>Ringkasan Gred:</span> <span style="font-weight:600">${studentStats.summary}</span></div>
<div class="slip-summary-item"><span>Ranking Kelas:</span> <span style="font-weight:600">${classRank}</span></div>
<div class="slip-summary-item"><span>Ranking Tingkatan:</span> <span style="font-weight:600">${tingkatanRank}</span></div>
</div>
</div>
<div class="slip-section">
<label style="font-weight:600;display:block;margin-bottom:8px">Ulasan Guru:</label>
<textarea class="slip-ulasan" placeholder="Taip ulasan guru kelas di sini..."></textarea>
</div>
<div class="slip-signature">
<div class="slip-sign-box">
<div class="slip-sign-line"></div>
<p style="font-weight:600;margin-top:4px;">PN MASDA BINTI OTHMAN</p>
<p style="font-size:12px;color:var(--text2)">Pengetua SMK Abu Bakar Al-Baqir</p>
</div>
</div>
</div>
`;
document.getElementById('slipActions').style.display = 'flex';
} catch (e) {
console.error(e);
slipArea.innerHTML = '<p class="text-center text-red-500">Gagal memuatkan slip peperiksaan.</p>';
}
}
/* ===== KEYBOARD: Enter pada login ===== */
document.getElementById('loginPassword').addEventListener('keydown',e=>{if(e.key==='Enter')handleLogin();});
document.getElementById('loginEmail').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('loginPassword').focus();});
/* ===== INIT ===== */
document.getElementById('loginEmail').focus();
</script>
</body>
</html>