Connecting to KSEF...

Please wait

No Internet Connection

We cannot connect to the KSEF Servers. Please check your network settings.

Logo

KSEF ONLINE JUDGING APP

Select your role to access the portal

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>KSEF 2026 Participant Portal</title> <script src="https://cdn.tailwindcss.com/3.4.1"></script> <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css" /> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.3.0/exceljs.min.js"></script> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f1f5f9; color: #0f172a; } .glass { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.5); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05); } .fade-in { animation: fadeIn 0.3s ease-in-out; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .loader-ring { display: inline-block; width: 24px; height: 24px; } .loader-ring:after { content: " "; display: block; width: 20px; height: 20px; margin: 2px; border-radius: 50%; border: 3px solid #3b82f6; border-color: #3b82f6 transparent #3b82f6 transparent; animation: ring 1.2s linear infinite; } @keyframes ring { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .smart-input { width: 100%; padding: 0.75rem 1rem; border: 1px solid #cbd5e1; border-radius: 0.5rem; transition: all 0.2s; outline: none; background: #f8fafc; } .smart-input:focus { border-color: #2563eb; background: #fff; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); } .btn-primary { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.2); transition: transform 0.1s; } .btn-primary:active { transform: scale(0.98); } .btn-secondary { background: white; color: #475569; border: 1px solid #e2e8f0; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; transition: background 0.2s; } .btn-secondary:hover { background: #f8fafc; } canvas { touch-action: none; background: white; border: 2px dashed #cbd5e1; border-radius: 0.5rem; cursor: crosshair; } .editable-cell { border-bottom: 1px dashed #94a3b8; cursor: text; padding: 4px; } .editable-cell:focus { background: #eff6ff; border-bottom: 2px solid #2563eb; outline: none; } @media print { .no-print { display: none !important; } .print-only { display: block !important; } body { background: white; } .glass { box-shadow: none; border: none; } } </style> </head> <body class="min-h-screen flex flex-col"> <nav class="bg-white border-b border-slate-200 sticky top-0 z-50 no-print"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="flex justify-between h-16"> <div class="flex items-center gap-3"> <img src="https://i.postimg.cc/mgfGYYGq/download-34.jpg" class="h-10 w-10 rounded-lg object-cover shadow-sm"> <div> <h1 class="text-xl font-bold tracking-tight text-slate-900">KSEF 2026</h1> <p class="text-xs text-slate-500 font-medium">Participant & Result Portal</p> </div> </div> <div class="hidden md:flex items-center space-x-4"> <button onclick="router('upload')" class="px-3 py-2 text-sm font-medium rounded-md hover:bg-slate-50 transition text-slate-700" id="nav-upload">Upload Project</button> <button onclick="router('progress')" class="px-3 py-2 text-sm font-medium rounded-md hover:bg-slate-50 transition text-slate-700" id="nav-progress">View Progress</button> <button onclick="router('results')" class="px-3 py-2 text-sm font-medium rounded-md hover:bg-slate-50 transition text-slate-700" id="nav-results">View Results</button> <div class="h-6 w-px bg-slate-300 mx-2"></div> <button onclick="router('admin')" class="px-3 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-md" id="nav-admin">Admin</button> </div> <div class="flex items-center md:hidden"> <button onclick="toggleMobileMenu()" class="text-slate-600 p-2"><i class="ph ph-list text-2xl"></i></button> </div> </div> </div> <div id="mobile-menu" class="hidden md:hidden bg-white border-t border-slate-100 absolute w-full shadow-lg"> <div class="px-2 pt-2 pb-3 space-y-1"> <button onclick="router('upload')" class="block w-full text-left px-3 py-3 text-base font-medium text-slate-700 hover:bg-slate-50 rounded-md">Upload Project</button> <button onclick="router('progress')" class="block w-full text-left px-3 py-3 text-base font-medium text-slate-700 hover:bg-slate-50 rounded-md">View Progress</button> <button onclick="router('results')" class="block w-full text-left px-3 py-3 text-base font-medium text-slate-700 hover:bg-slate-50 rounded-md">View Results</button> <button onclick="router('admin')" class="block w-full text-left px-3 py-3 text-base font-medium text-red-600 hover:bg-red-50 rounded-md">Admin Console</button> </div> </div> </nav> <main class="flex-grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-8 relative"> <section id="view-upload" class="view-section fade-in"> <div class="text-center mb-8"> <h2 class="text-3xl font-extrabold text-slate-900">Project Registration</h2> <p id="system-status-msg" class="mt-2 text-sm text-slate-500">Checking active status...</p> </div> <div id="upload-locked" class="hidden max-w-lg mx-auto text-center py-12 glass rounded-2xl"> <i class="ph ph-lock-key text-6xl text-red-400 mb-4"></i> <h3 class="text-xl font-bold text-slate-800">Registration Closed</h3> <p class="text-slate-500 mt-2">The project upload window is currently not active.</p> </div> <div id="upload-active" class="hidden space-y-8"> <div class="flex justify-center p-1 bg-white border border-slate-200 rounded-lg max-w-md mx-auto shadow-sm"> <button onclick="setUploadMode('manual')" id="btn-mode-manual" class="flex-1 py-2 text-sm font-medium rounded-md transition-all">Manual Entry</button> <button onclick="setUploadMode('excel')" id="btn-mode-excel" class="flex-1 py-2 text-sm font-medium rounded-md transition-all">Automatic (Excel)</button> </div> <div id="mode-manual" class="max-w-3xl mx-auto glass rounded-2xl p-6 md:p-10 shadow-xl"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="space-y-2"> <label class="text-xs font-bold uppercase tracking-wide text-slate-500">Level / Grade</label> <select id="m-grade" class="smart-input" onchange="updateCategories()"> <option value="" disabled selected>Select Grade...</option> <option value="JS">Junior School (JS)</option> <option value="SS">Senior School (SS)</option> </select> </div> <div class="space-y-2"> <label class="text-xs font-bold uppercase tracking-wide text-slate-500">Category</label> <select id="m-category" class="smart-input" disabled> <option>Select Grade First</option> </select> </div> <div class="md:col-span-2 space-y-2"> <label class="text-xs font-bold uppercase tracking-wide text-slate-500">Project Name</label> <input type="text" id="m-name" class="smart-input" placeholder="Type to search existing or add new..." autocomplete="off" oninput="toggleSuggest(this, 'dl-projects')"> <datalist id="dl-projects"></datalist> </div> <div class="md:col-span-2 space-y-2"> <label class="text-xs font-bold uppercase tracking-wide text-slate-500">School Name</label> <input type="text" id="m-school" class="smart-input" placeholder="Search school..." autocomplete="off" oninput="toggleSuggest(this, 'dl-schools')"> <datalist id="dl-schools"></datalist> </div> <div class="space-y-2"> <label class="text-xs font-bold uppercase tracking-wide text-slate-500">Student 1 (Lead)</label> <input type="text" id="m-std1" class="smart-input" placeholder="Full Name"> </div> <div class="space-y-2"> <label class="text-xs font-bold uppercase tracking-wide text-slate-500">Student 2 (Optional)</label> <input type="text" id="m-std2" class="smart-input" placeholder="Full Name"> </div> <div class="md:col-span-2 space-y-2"> <label class="text-xs font-bold uppercase tracking-wide text-slate-500">Level / Region</label> <input type="text" id="m-level" class="smart-input" placeholder="e.g. Regional, County..." autocomplete="off" oninput="toggleSuggest(this, 'dl-levels')"> <datalist id="dl-levels"></datalist> </div> </div> <div class="mt-8 pt-6 border-t border-slate-200"> <button onclick="prepareManualSubmit()" class="w-full btn-primary flex items-center justify-center gap-2"><span>Proceed to Signature</span> <i class="ph ph-arrow-right"></i></button> </div> </div> <div id="mode-excel" class="hidden max-w-5xl mx-auto space-y-6"> <div class="glass p-6 rounded-xl flex flex-col md:flex-row gap-4 items-end justify-between"> <div class="w-full md:w-1/3"> <label class="text-xs font-bold uppercase text-slate-500 mb-1 block">Admin Verification ID</label> <input type="password" id="excel-pass" class="smart-input" placeholder="Enter Admin Password"> </div> <button onclick="downloadTemplate()" class="btn-secondary flex items-center gap-2"><i class="ph ph-download-simple"></i> Download Template</button> </div> <div class="border-2 border-dashed border-slate-300 rounded-xl bg-slate-50 hover:bg-blue-50 transition-colors p-10 text-center cursor-pointer relative"> <input type="file" id="file-excel" accept=".xlsx, .xls" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" onchange="handleExcelFile(this)"> <i class="ph ph-microsoft-excel-logo text-5xl text-green-600 mb-3"></i> <h4 class="font-bold text-slate-700">Click to Upload Excel File</h4> </div> <div id="excel-editor" class="hidden glass rounded-xl overflow-hidden shadow-lg animate-fade-in"> <div class="overflow-x-auto max-h-[500px]"> <table class="w-full text-sm text-left"> <thead class="bg-slate-100 text-xs uppercase text-slate-500 sticky top-0 shadow-sm"> <tr><th class="px-4 py-3">Grade</th><th class="px-4 py-3">Category</th><th class="px-4 py-3">Project Name</th><th class="px-4 py-3">School</th><th class="px-4 py-3">Student 1</th><th class="px-4 py-3">Student 2</th><th class="px-4 py-3">Level</th><th class="px-4 py-3 w-10"></th></tr> </thead> <tbody id="excel-tbody" class="divide-y divide-slate-100"></tbody> </table> </div> <div class="p-6 bg-white border-t border-slate-200 flex flex-col md:flex-row justify-between items-center gap-4"> <label class="flex items-center gap-2 cursor-pointer bg-red-50 px-4 py-2 rounded-lg border border-red-100"> <input type="checkbox" id="chk-overwrite" class="h-4 w-4 text-red-600 rounded"> <span class="text-sm font-bold text-red-600">Overwrite ALL Existing Data</span> </label> <button onclick="prepareExcelSubmit()" class="btn-primary w-full md:w-auto">Confirm & Submit Bulk Data</button> </div> </div> </div> </div> </section> <section id="view-progress" class="view-section hidden fade-in"> <div class="max-w-2xl mx-auto text-center mb-10"> <h2 class="text-3xl font-extrabold text-slate-900">Track Project Status</h2> <p class="text-slate-500 mt-2">Check judging progress for Parts A, B, and C.</p> </div> <div class="max-w-xl mx-auto glass p-8 rounded-2xl shadow-lg"> <div class="flex gap-2"> <input type="text" id="prog-pid" class="smart-input uppercase text-center text-lg font-mono tracking-wider" placeholder="ENTER YOUR PROJECT ID (e.g. A1X9)"> <button onclick="searchProgress()" class="btn-primary"><i class="ph ph-magnifying-glass text-xl"></i></button> </div> <div id="prog-result" class="hidden mt-8 border-t border-slate-200 pt-6 text-left"></div> </div> </section> <section id="view-results" class="view-section hidden fade-in w-full min-h-screen pt-24 pb-12 px-4"> <div class="w-full max-w-md mx-auto glass p-8 rounded-2xl shadow-xl no-print relative z-10" id="res-login-card"> <div class="text-center mb-6"> <h2 class="text-2xl font-bold text-gray-800">Results Portal</h2> <p class="text-sm text-slate-500">Enter Project ID to view Results</p> <p class="text-sm text-red-600 animate-pulse">❗ Use landscape mode for Mobile Phone ❗</p> </div> <div class="space-y-4"> <div> <label class="text-xs font-bold text-slate-500 uppercase">Project ID</label> <input type="text" id="res-pid" class="smart-input w-full uppercase text-center text-lg font-mono tracking-wider p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 outline-none transition-all" placeholder="ENTER YOUR PROJECT ID (e.g. A1X9)"> </div> <button onclick="fetchResults()" class="btn-primary w-full mt-2 py-3 rounded-xl font-bold shadow-lg shadow-blue-200">View Results</button> </div> </div> <div id="res-report" class="hidden w-full max-w-4xl mx-auto glass rounded-2xl overflow-hidden shadow-2xl print-only mt-8 relative z-10"> <div class="bg-slate-900 text-white p-6 md:p-8 flex flex-col md:flex-row justify-between items-start gap-4"> <div class="flex items-center gap-4"> <img src="https://i.postimg.cc/mgfGYYGq/download-34.jpg" class="h-16 w-16 rounded-lg bg-white p-1 object-contain"> <div> <h2 class="text-xl md:text-2xl font-bold">KSEF 2026 RESULTS</h2> <p class="text-slate-400 text-sm">Generated on <span id="res-date"></span></p> </div> </div> <div class="text-left md:text-right w-full md:w-auto"> <div class="text-4xl font-black text-yellow-400" id="res-total-score">0.00</div> <div class="text-xs uppercase tracking-widest text-slate-400">Average Score</div> </div> </div> <div class="p-6 md:p-8 bg-slate-50 border-b border-slate-200 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6"> <div><p class="text-xs text-slate-500 uppercase">Project Name</p><p class="font-bold text-slate-800 leading-tight" id="res-pname">-</p></div> <div><p class="text-xs text-slate-500 uppercase">Category</p><p class="font-bold text-slate-800" id="res-pcat">-</p></div> <div><p class="text-xs text-slate-500 uppercase">School</p><p class="font-bold text-slate-800" id="res-pschool">-</p></div> <div><p class="text-xs text-slate-500 uppercase">Category Rank</p><p class="font-bold text-blue-600 text-xl" id="res-prank">-</p></div> </div> <div id="res-judges-container" class="p-6 md:p-8 space-y-6"></div> <div class="bg-slate-100 p-4 text-center text-xs text-slate-400 no-print"> <button onclick="requestPrint()" class="btn-secondary px-6 py-2 rounded-lg bg-white border border-gray-300 shadow-sm hover:bg-gray-50"><i class="ph ph-printer"></i> Print Official Report</button> </div> </div> </section> <section id="view-admin" class="view-section hidden fade-in"> <div id="admin-login" class="max-w-md mx-auto glass p-8 rounded-2xl text-center"> <i class="ph ph-shield-check text-4xl text-blue-600 mb-4"></i> <h2 class="text-2xl font-bold mb-4">Admin Console</h2> <input type="password" id="admin-pass-main" class="smart-input mb-4 text-center" placeholder="Enter System Password"> <button onclick="adminAccess()" class="btn-primary w-full">Access Dashboard</button> </div> <div id="admin-dash" class="hidden"> <div class="flex justify-between items-center mb-6"> <h2 class="text-2xl font-bold">System Overview</h2> <button onclick="adminAccess()" class="btn-secondary text-sm"> <i class="ph ph-arrows-clockwise"></i> Refresh Data </button> </div> <div class="mb-4"> <div class="relative"> <i class="ph ph-magnifying-glass absolute left-4 top-3.5 text-slate-400 text-lg"></i> <input type="text" id="adminSearchInput" placeholder="Start typing to search projects..." class="w-full pl-11 p-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none shadow-sm text-sm transition"> </div> </div> <div class="glass rounded-xl overflow-hidden shadow-sm border border-slate-200"> <table class="w-full text-sm text-left"> <thead class="bg-slate-50 border-b text-slate-600"> <tr> <th class="p-4 font-bold">ID</th> <th class="p-4 font-bold">Project Name</th> <th class="p-4 font-bold">Category</th> <th class="p-4 text-center font-bold">Judges</th> </tr> </thead> <tbody id="admin-tbody" class="divide-y divide-slate-100 bg-white"> </tbody> </table> </div> <button onclick="location.reload()" class="mt-6 w-full py-3 text-slate-500 font-bold hover:text-slate-700 text-sm">Exit Admin</button> </div> </div> </section> </main> <footer class="bg-white border-t border-slate-200 mt-auto py-8 no-print"> <div class="max-w-7xl mx-auto px-4 text-center"> <p class="text-sm text-slate-500">&copy; 2026 Kenya Science & Engineering Fair. All Rights Reserved.</p> </div> </footer> <div id="modal-sig" class="fixed inset-0 z-50 hidden"> <div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onclick="closeSigModal()"></div> <div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white rounded-2xl shadow-2xl p-6 animate-fade-in"> <div class="text-center mb-4"> <h3 class="text-lg font-bold text-slate-900">Teacher Confirmation</h3> <p class="text-xs text-slate-500">Verify submission with TSC No. and Signature</p> </div> <div class="bg-blue-50 p-3 rounded-lg mb-4 text-sm text-blue-800" id="sig-summary"></div> <label class="block text-xs font-bold uppercase text-slate-500 mb-1">TSC Number (6-8 Digits)</label> <input type="number" id="sig-tsc" class="smart-input mb-4" placeholder="e.g. 123456"> <div class="flex justify-between items-center mb-1"> <label class="text-xs font-bold uppercase text-slate-500">Signature</label> <button onclick="clearCanvas()" class="text-xs text-red-500 hover:underline">Clear</button> </div> <canvas id="sig-canvas" width="350" height="150" class="w-full border-2 border-slate-200 rounded-lg bg-white"></canvas> <div class="grid grid-cols-2 gap-3 mt-6"> <button onclick="closeSigModal()" class="btn-secondary">Cancel</button> <button onclick="submitFinal()" class="btn-primary">Confirm Upload</button> </div> </div> </div> <script> const SCRIPT_URL = "https://script.google.com/macros/s/AKfycbyhHHyfk2WPonCXVmc03Hc0fcBhkGatKfkXDD-f4nDGlFCMo4RZ8QNwOCBd7szw2kyB/exec"; const LOGO_URL = "https://i.postimg.cc/mgfGYYGq/download-34.jpg"; let STATE = { config: {}, categories: [], schools: [], projects: [], levels: [], pendingUpload: null }; let excelData = []; let LOGO_IMG_DATA = null; // HELPER: Loads scripts only when requested function loadLib(url) { return new Promise((resolve, reject) => { if (document.querySelector(`script[src="${url}"]`)) return resolve(); const script = document.createElement('script'); script.src = url; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } // Preload Image for PDF const img = new Image(); img.crossOrigin = "Anonymous"; img.src = LOGO_URL; img.onload = function(){ const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); LOGO_IMG_DATA = canvas.toDataURL("image/jpeg"); }; window.onload = async () => { toggleLoader(true); try { const res = await fetchGAS({ action: 'getParticipantData' }); STATE = { ...STATE, ...res }; setupDatalist('dl-projects', STATE.projectNames); setupDatalist('dl-schools', STATE.schools); setupDatalist('dl-levels', STATE.levels); const status = STATE.config.projectUpload ? STATE.config.projectUpload.toLowerCase() : 'closed'; if(status === 'active') { document.getElementById('upload-active').classList.remove('hidden'); document.getElementById('system-status-msg').innerText = "System Active. Select upload method below."; document.getElementById('system-status-msg').classList.add('text-green-600', 'font-bold'); } else { document.getElementById('upload-locked').classList.remove('hidden'); document.getElementById('system-status-msg').innerText = "System Closed."; document.getElementById('system-status-msg').classList.add('text-red-500'); } router('upload'); } catch (e) { console.error(e); Swal.fire('Error', 'Connection Failed. Check Internet.', 'error'); } finally { toggleLoader(false); } initCanvas(); }; function router(viewId) { document.querySelectorAll('.view-section').forEach(el => el.classList.add('hidden')); document.getElementById(`view-${viewId}`).classList.remove('hidden'); document.querySelectorAll('nav button').forEach(b => b.classList.remove('bg-slate-50', 'text-blue-600')); const navBtn = document.getElementById(`nav-${viewId}`); if(navBtn) navBtn.classList.add('bg-slate-50', 'text-blue-600'); document.getElementById('mobile-menu').classList.add('hidden'); } function toggleMobileMenu() { document.getElementById('mobile-menu').classList.toggle('hidden'); } function setUploadMode(mode) { document.getElementById('mode-manual').classList.add('hidden'); document.getElementById('mode-excel').classList.add('hidden'); document.getElementById('btn-mode-manual').classList.remove('bg-blue-600', 'text-white', 'shadow-md'); document.getElementById('btn-mode-excel').classList.remove('bg-blue-600', 'text-white', 'shadow-md'); if(mode === 'manual') { document.getElementById('mode-manual').classList.remove('hidden'); document.getElementById('btn-mode-manual').classList.add('bg-blue-600', 'text-white', 'shadow-md'); } else { document.getElementById('mode-excel').classList.remove('hidden'); document.getElementById('btn-mode-excel').classList.add('bg-blue-600', 'text-white', 'shadow-md'); } } async function fetchGAS(payload) { const response = await fetch(SCRIPT_URL, { method: "POST", body: JSON.stringify(payload) }); return await response.json(); } function setupDatalist(id, items) { const dl = document.getElementById(id); dl.innerHTML = ""; if(items) items.forEach(item => { const opt = document.createElement('option'); opt.value = item; dl.appendChild(opt); }); } function toggleSuggest(input, listId) { if(input.value.length > 0) input.setAttribute('list', listId); else input.removeAttribute('list'); } function toggleLoader(show) { const l = document.getElementById('global-loader'); show ? l.classList.remove('hidden') : l.classList.add('hidden'); } // --- MANUAL UPLOAD LOGIC --- function updateCategories() { const grade = document.getElementById('m-grade').value; const catSelect = document.getElementById('m-category'); catSelect.innerHTML = '<option value="" disabled selected>Select Category...</option>'; if(!grade) { catSelect.disabled = true; return; } catSelect.disabled = false; STATE.categories.forEach(c => { const opt = document.createElement('option'); opt.value = c; opt.innerText = c; catSelect.appendChild(opt); }); } function prepareManualSubmit() { // 1. CLEAR OLD DATA IMMEDIATELY // This prevents "ghost" uploads if validation fails later STATE.pendingUpload = null; const d = { grade: document.getElementById('m-grade').value, category: document.getElementById('m-category').value, name: document.getElementById('m-name').value.trim(), // Good practice to trim whitespace school: document.getElementById('m-school').value, level: document.getElementById('m-level').value, student1: document.getElementById('m-std1').value, student2: document.getElementById('m-std2').value || "" }; // Validation 1: Missing Fields if(!d.grade || !d.category || !d.name || !d.school || !d.student1 || !d.level) return Swal.fire('Missing Data', 'Fill all fields.', 'warning'); // Validation 2: Duplicate Check // Make sure STATE.projectNames is actually loaded! if(STATE.projectNames && STATE.projectNames.some(n => n.toLowerCase() === d.name.toLowerCase())) { return Swal.fire('Duplicate Project', 'This project name already exists.', 'error'); } // Only reach here if validations pass STATE.pendingUpload = { type: 'manual', data: [d] }; document.getElementById('sig-summary').innerHTML = `<strong>${d.name}</strong><br>${d.category} | ${d.school}`; document.getElementById('modal-sig').classList.remove('hidden'); } // --- EXCEL LOGIC (FIXED) --- async function downloadTemplate() { // 1. Load ExcelJS (Required for Dropdowns) if (typeof ExcelJS === 'undefined') { toggleLoader(true); await loadLib('https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.4.0/exceljs.min.js'); toggleLoader(false); } const workbook = new ExcelJS.Workbook(); // ========================================== // STEP 1: CREATE THE MAIN SHEET (The Form) // ========================================== const sheet = workbook.addWorksheet('KSEF Template'); // Define Columns (Updated Header as requested) sheet.columns = [ { header: 'JS or SS', key: 'grade', width: 10 }, { header: 'Category', key: 'cat', width: 45 }, // WIDER COLUMN { header: 'Project Name', key: 'name', width: 30 }, { header: 'School', key: 'school', width: 25 }, { header: 'Student 1', key: 'std1', width: 20 }, { header: 'Student 2', key: 'std2', width: 20 }, { header: 'Level', key: 'level', width: 15 } ]; // ========================================== // STEP 2: CREATE HIDDEN VALIDATION SHEET // ========================================== // We need this because your list is too long for a standard dropdown string. const setupSheet = workbook.addWorksheet('Setup'); setupSheet.state = 'hidden'; // Hide this sheet from the user const categories = [ "AGRICULTURE (JS)", "AGRICULTURE (SS)", "AUTOMATION & MISSION BASED ROBOTICS (SS)", "BEHAVIOURAL SCIENCE (SS)", "BIOLOGICAL AND ENVIRONMENTAL SCIENCE (JS)", "BIOLOGY (SS)", "CHEMISTRY (SS)", "CLOTHING, TEXTILES AND DECOR (SS)", "COMPUTER SCIENCE (SS)", "ENERGY AND TRANSPORTATION (JS)", "ENERGY AND TRANSPORTATION (SS)", "ENGINEERING (SS)", "ENVIRONMENTAL SCIENCE AND MANAGEMENT (SS)", "FOOD TECHNOLOGY (SS)", "MATHEMATICAL SCIENCE (SS)", "MATHEMATICAL CHEMICAL AND PHYSICAL SCIENCE (JS)", "MICROBIOLOGY AND HEALTH SCIENCE (SS)", "PHYSICS (SS)", "RESEARCH BASED ROBOTICS AND INTELLIGENT MACHINES (SS)", "SOCIAL & BEHAVIOURAL SCIENCE (JS)", "SPACE SCIENCE (SS)", "TECHNOLOGY AND APPLIED TECHNOLOGY (JS)", "TECHNOLOGY AND APPLIED TECHNOLOGY (SS)", "WASTE MANAGEMENT (SS)" ]; // Write categories to the hidden sheet (Column A) categories.forEach((cat, index) => { setupSheet.getCell(`A${index + 1}`).value = cat; }); // ========================================== // STEP 3: APPLY DROPDOWNS TO MAIN SHEET // ========================================== // We apply this to rows 2 through 200 for (let i = 2; i <= 200; i++) { // COLUMN B: Category Dropdown (References the hidden sheet) // 'Setup!$A$1:$A$24' tells Excel to look at the list we just made sheet.getCell(`B${i}`).dataValidation = { type: 'list', allowBlank: true, formulae: [`Setup!$A$1:$A$${categories.length}`] }; // COLUMN A: Grade Dropdown (Simple list) sheet.getCell(`A${i}`).dataValidation = { type: 'list', allowBlank: true, formulae: ['"JS,SS"'] }; } // ========================================== // STEP 4: DOWNLOAD // ========================================== const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'KSEF_Template_V2.xlsx'; link.click(); } function renderExcelTable() { const tbody = document.getElementById('excel-tbody'); tbody.innerHTML = ""; if (excelData.length === 0) { document.getElementById('excel-editor').classList.add('hidden'); return; } excelData.forEach((row, i) => { tbody.innerHTML += ` <tr class="bg-white border-b hover:bg-slate-50 transition"> <td contenteditable class="px-4 py-3 editable-cell" onblur="excelData[${i}].grade=this.innerText">${row.grade}</td> <td contenteditable class="px-4 py-3 editable-cell" onblur="excelData[${i}].category=this.innerText">${row.category}</td> <td contenteditable class="px-4 py-3 editable-cell" onblur="excelData[${i}].name=this.innerText">${row.name}</td> <td contenteditable class="px-4 py-3 editable-cell" onblur="excelData[${i}].school=this.innerText">${row.school}</td> <td contenteditable class="px-4 py-3 editable-cell" onblur="excelData[${i}].student1=this.innerText">${row.student1}</td> <td contenteditable class="px-4 py-3 editable-cell" onblur="excelData[${i}].student2=this.innerText">${row.student2}</td> <td contenteditable class="px-4 py-3 editable-cell" onblur="excelData[${i}].level=this.innerText">${row.level}</td> <td class="px-4 py-3 text-center"> <button onclick="deleteRow(${i})" class="text-red-500 hover:text-red-700 font-bold px-2">X</button> </td> </tr>`; }); document.getElementById('excel-editor').classList.remove('hidden'); } function deleteRow(index) { excelData.splice(index, 1); renderExcelTable(); } async function handleExcelFile(input) { // NEW: Lazy load Excel library if user uploads a file if(typeof XLSX === 'undefined') { toggleLoader(true); await loadLib('https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js'); toggleLoader(false); } const file = input.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = (e) => { const wb = XLSX.read(new Uint8Array(e.target.result), {type: 'array'}); const json = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); excelData = json.map(row => ({ grade: row["JS or SS"] || "UNKNOWN!!", category: row["Categories (Only select from dropdown)"] || row["Category"] || "", name: row["Project Name"] || "", school: row["School"] || "", student1: row["Student 1"] || "", student2: row["Student 2"] || "", level: row["Level"] || "" })); renderExcelTable(); }; reader.readAsArrayBuffer(file); input.value = ''; } function prepareExcelSubmit() { const pass = document.getElementById('excel-pass').value; if(!pass) return Swal.fire('Admin Required', 'Enter Admin Password', 'warning'); if(excelData.length === 0) return Swal.fire('Empty', 'No data.', 'warning'); STATE.pendingUpload = { type: 'excel', data: excelData, overwrite: document.getElementById('chk-overwrite').checked }; document.getElementById('sig-summary').innerHTML = `<strong>Bulk Upload</strong><br>${excelData.length} Projects`; document.getElementById('modal-sig').classList.remove('hidden'); } // --- SIGNATURE LOGIC --- let sigCanvas, sigCtx, isDrawing = false; function initCanvas() { sigCanvas = document.getElementById('sig-canvas'); sigCtx = sigCanvas.getContext('2d'); // NEW: Fill background with white immediately (Prevents black background on JPEG) sigCtx.fillStyle = "#ffffff"; sigCtx.fillRect(0, 0, sigCanvas.width, sigCanvas.height); sigCtx.lineWidth = 2; // Optional: Makes line thicker for better visibility const start = (e) => { e.preventDefault(); isDrawing = true; sigCtx.beginPath(); draw(e); }; const move = (e) => { if(!isDrawing) return; e.preventDefault(); draw(e); }; const end = () => { isDrawing = false; }; sigCanvas.addEventListener('mousedown', start); sigCanvas.addEventListener('mousemove', move); sigCanvas.addEventListener('mouseup', end); sigCanvas.addEventListener('touchstart', start, {passive: false}); sigCanvas.addEventListener('touchmove', move, {passive: false}); sigCanvas.addEventListener('touchend', end); } function draw(e) { const rect = sigCanvas.getBoundingClientRect(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; sigCtx.lineTo(clientX - rect.left, clientY - rect.top); sigCtx.stroke(); } function clearCanvas() { sigCtx.fillStyle = "#ffffff"; sigCtx.fillRect(0, 0, sigCanvas.width, sigCanvas.height); } function closeSigModal() { document.getElementById('modal-sig').classList.add('hidden'); } // --- FINAL SUBMISSION (SECURE) --- async function submitFinal() { const tsc = document.getElementById('sig-tsc').value.trim(); const adminPassInput = document.getElementById('excel-pass'); const adminPass = adminPassInput ? adminPassInput.value.trim() : ""; const blank = document.createElement('canvas'); blank.width = sigCanvas.width; blank.height = sigCanvas.height; if(!tsc || sigCanvas.toDataURL() === blank.toDataURL()) return Swal.fire('Incomplete', 'TSC & Signature Required', 'warning'); if (tsc.length <= 5) return Swal.fire('Invalid TSC', 'TSC Number must be more than 5 digits.', 'warning'); closeSigModal(); toggleLoader(true); try { const res = await fetchGAS({ action: "uploadProject", data: { projects: STATE.pendingUpload.data, tsc: tsc, // Uses WebP format at 50% quality for maximum compression signature: sigCanvas.toDataURL('image/webp', 0.5), overwrite: STATE.pendingUpload.overwrite || false, password: adminPass, type: STATE.pendingUpload.type } }); if(res.success) { if(STATE.pendingUpload.type === 'manual') { Swal.fire('Success', `Project ${res.projects[0].name} Added. ID: ${res.projects[0].id}`, 'success'); // Clear Manual Form ['m-grade','m-category','m-name','m-school','m-level','m-std1','m-std2'].forEach(id => document.getElementById(id).value = ''); } else { generatePDF(res.projects); Swal.fire('Success', `${res.projects.length} Uploaded. PDF Downloaded.`, 'success'); document.getElementById('excel-editor').classList.add('hidden'); if(adminPassInput) adminPassInput.value = ''; } } else { Swal.fire('Error', res.error, 'error'); } } catch(e) { Swal.fire('Error', 'Submission Failed', 'error'); } finally { toggleLoader(false); } } // --- VIEW RESULTS --- async function fetchResults() { const pid = document.getElementById('res-pid').value.trim(); if(!pid) return Swal.fire('Info', 'Enter Project ID', 'info'); toggleLoader(true); try { const res = await fetchGAS({ action: 'getProjectResults', pid: pid }); if(res.success) { document.getElementById('res-login-card').classList.add('hidden'); document.getElementById('res-report').classList.remove('hidden'); const p = res.project; document.getElementById('res-date').innerText = new Date().toLocaleDateString(); document.getElementById('res-total-score').innerText = res.average; document.getElementById('res-pname').innerText = p.name; document.getElementById('res-pcat').innerText = p.category; document.getElementById('res-pschool').innerText = p.school; document.getElementById('res-prank').innerText = `${res.rank} / ${res.totalRanked}`; const container = document.getElementById('res-judges-container'); container.innerHTML = ""; res.results.forEach(j => { container.innerHTML += `<div class="border border-slate-200 rounded-lg p-6 bg-white"><div class="flex justify-between items-center mb-2"><span class="font-bold text-slate-700 bg-slate-100 px-3 py-1 rounded">${j.judgeAlias}</span><span class="font-bold text-blue-600">Total: ${j.total}</span></div><div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm mt-3 border-t border-slate-100 pt-3">${j.marks.map(m=>`<div><span class="text-slate-500 block text-xs uppercase">${m.qText.substring(0,30)}...</span><span class="font-bold">${m.score}</span></div>`).join('')}</div><div class="flex gap-4 mt-4">${j.sigA ? `<div class="text-xs text-slate-400">Sig A:<br><img src="${j.sigA}" class="h-8 border"></div>`:''}${j.sigBC ? `<div class="text-xs text-slate-400">Sig BC:<br><img src="${j.sigBC}" class="h-8 border"></div>`:''}</div></div>`; }); } else Swal.fire('Error', res.error, 'error'); } catch(e) { Swal.fire('Error', 'Failed', 'error'); } finally { toggleLoader(false); } } async function requestPrint() { const { value: password } = await Swal.fire({ title: 'Admin Verification', input: 'password', inputLabel: 'Enter Admin Password', inputPlaceholder: 'Admin Password', showCancelButton: true }); if (password) { toggleLoader(true); const res = await fetchGAS({ action: 'verifyAdmin', data: { password: password } }); toggleLoader(false); if(res.success) window.print(); else Swal.fire('Access Denied', 'Incorrect Password', 'error'); } } // --- PROGRESS CHECK --- async function searchProgress() { const pid = document.getElementById('prog-pid').value.trim(); if(!pid) return; toggleLoader(true); const res = await fetchGAS({ action: 'checkProjectStatus', pid: pid }); toggleLoader(false); if(res.success) { const s = res.status; const badge = (d) => d ? `<span class="bg-green-100 text-green-700 px-3 py-1 rounded text-xs font-bold">Complete</span>` : `<span class="bg-yellow-100 text-yellow-700 px-3 py-1 rounded text-xs font-bold">Pending</span>`; document.getElementById('prog-result').innerHTML = `<h3 class="font-bold">${res.project.name}</h3><div class="mt-4 space-y-2"><div class="flex justify-between p-2 bg-slate-50 border rounded"><span>Part A</span>${badge(s.judgesCount.A>0)}</div><div class="flex justify-between p-2 bg-slate-50 border rounded"><span>Part B/C</span>${badge(s.judgesCount.BC>0)}</div></div>`; document.getElementById('prog-result').classList.remove('hidden'); } else Swal.fire('Error', res.error, 'error'); } /* ========================================= FIXED ADMIN DASHBOARD & SEARCH ========================================= */ // 1. GLOBAL VARIABLE (Must be declared at the top level of your script) let ADMIN_DATA = []; async function adminAccess() { const passInput = document.getElementById('admin-pass-main'); const pass = passInput ? passInput.value : ""; toggleLoader(true); try { const res = await fetchGAS({ action: 'getAdminOverview', pass: pass }); toggleLoader(false); if(res.success) { // A. Store Data ADMIN_DATA = res.data || []; // B. Sort Alphabetically by Category ADMIN_DATA.sort((a, b) => { const c1 = (a.category || "").toLowerCase(); const c2 = (b.category || "").toLowerCase(); return c1.localeCompare(c2); }); // C. Show Dashboard document.getElementById('admin-login').classList.add('hidden'); document.getElementById('admin-dash').classList.remove('hidden'); // D. Render Initial Table renderAdminTable(ADMIN_DATA); // ============================================================ // E. FORCE CONNECT THE SEARCH BAR (The Fix) // ============================================================ const searchInput = document.getElementById('adminSearchInput'); // NEW: Variable to track the timer let debounceTimer; // This function waits 300ms before running searchInput.oninput = function() { clearTimeout(debounceTimer); // Cancel the previous timer if you type again debounceTimer = setTimeout(() => { const query = this.value.toLowerCase().trim(); // Filter the global data const filtered = ADMIN_DATA.filter(p => (p.name || "").toLowerCase().includes(query) || (p.id || "").toString().toLowerCase().includes(query) || (p.category || "").toLowerCase().includes(query) ); // Update the table renderAdminTable(filtered); }, 300); // <--- The 300ms delay is here }; // ============================================================ } else { Swal.fire('Access Denied', 'Invalid Admin Password', 'error'); } } catch(e) { console.error(e); toggleLoader(false); Swal.fire('System Error', 'Could not load data. Check Console.', 'error'); } } // RENDER FUNCTION (Draws the rows) function renderAdminTable(data) { const tbody = document.getElementById('admin-tbody'); if(!tbody) return; if (!data || data.length === 0) { tbody.innerHTML = `<tr><td colspan="4" class="p-8 text-center text-slate-400 italic">No matching projects found.</td></tr>`; return; } tbody.innerHTML = data.map(r => ` <tr class="hover:bg-blue-50 transition duration-150 border-b border-slate-50 last:border-none"> <td class="p-4 font-mono text-xs text-slate-500">${r.id || "-"}</td> <td class="p-4 font-bold text-slate-800">${r.name || "Untitled"}</td> <td class="p-4 text-blue-600 font-medium text-xs uppercase tracking-wide">${r.category || "Uncategorized"}</td> <td class="p-4 text-center"> <span class="bg-slate-100 text-slate-700 px-2 py-1 rounded text-xs font-bold border border-slate-200"> ${r.judges || 0} </span> </td> </tr> `).join(''); } async function generatePDF(projects) { // NEW: Lazy load PDF library only when needed if(!window.jspdf) { toggleLoader(true); await loadLib('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'); toggleLoader(false); } const { jsPDF } = window.jspdf; const doc = new jsPDF(); projects.forEach((p, i) => { if(i>0 && i%4===0) doc.addPage(); let y = (i%4)*70+10; doc.rect(10,y,190,60); if(LOGO_IMG_DATA) { try { doc.addImage(LOGO_IMG_DATA, 'JPEG', 15, y+5, 15, 15); } catch(e) {} } doc.setFontSize(16); doc.text("KSEF 2026 CONFIDENTIAL",105,y+10,{align:"center"}); doc.setFontSize(11); doc.text(`ID: ${p.id}`,35,y+25); doc.text(`Cat: ${p.category}`,35,y+35); doc.text(`School: ${p.school}`,110,y+25); doc.setFontSize(14); doc.text(p.name,105,y+50,{align:"center"}); if((i+1)%4!==0) { doc.setLineDash([5,5],0); doc.line(5,y+65,205,y+65); } }); doc.save(`KSEF_IDs_${new Date().getTime()}.pdf`); } </script> <div id="global-loader" class="fixed inset-0 z-[200] bg-gray-900/60 backdrop-blur-sm flex items-center justify-center hidden transition-all duration-200"> <div class="bg-white p-8 rounded-2xl shadow-2xl flex flex-col items-center transform scale-100 animate-bounce-in border border-gray-100"> <div class="h-14 w-14 border-4 border-indigo-100 border-t-indigo-600 rounded-full animate-spin mb-4"></div> <p class="text-sm font-bold text-gray-700 tracking-wide animate-pulse">Communicating with Server...</p> </div> </div> </body> </html> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>KSEF Judging Portal</title> <script src="https://cdn.tailwindcss.com/3.4.1"></script> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <style> /* ========================================= ORIJI CUSTOM CSS - FULL VERSION ========================================= */ body { font-family: 'Inter', sans-serif; background-color: #f1f5f9; /* Slate-100 */ display: flex; flex-direction: column; min-height: 100vh; color: #1e293b; /* Slate-800 */ -webkit-tap-highlight-color: transparent; } /* --- Glassmorphism & Cards --- */ .glass-panel { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.5); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); } .card-shadow { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } /* --- Inputs --- */ .input-smart { transition: all 0.2s ease; border: 1px solid #cbd5e1; background: #ffffff; appearance: none; } .input-smart:focus { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); outline: none; } /* --- Validation Rings --- */ .error-ring { border-color: #ef4444 !important; box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2) !important; background-color: #fef2f2 !important; } /* --- Radio Buttons (Scale) --- */ .scale-radio { display: none; /* Hide default radio */ } .scale-radio + label { display: inline-block; cursor: pointer; user-select: none; transition: all 0.2s; } .scale-radio:checked + label { background-color: #1e293b; /* Slate-800 */ color: white; border-color: #1e293b; transform: translateY(-2px); box-shadow: 0 4px 6px rgba(0,0,0,0.2); } /* --- Smart Guide Box --- */ .guide-box { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); max-height: 0; opacity: 0; overflow: hidden; margin-top: 0; transform: translateY(-5px); } .guide-box.active { max-height: 200px; opacity: 1; margin-top: 0.75rem; padding: 1rem; transform: translateY(0); border: 1px solid #e0e7ff; } /* --- Loader --- */ .loader { border: 3px solid #f3f3f3; border-top: 3px solid #334155; border-radius: 50%; width: 24px; height: 24px; animation: spin 0.8s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* --- Canvas --- */ canvas { touch-action: none; background-color: #ffffff; background-image: radial-gradient(#cbd5e1 1px, transparent 1px); background-size: 20px 20px; cursor: crosshair; } /* --- Animations --- */ .fade-in { animation: fadeIn 0.5s ease-out forwards; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .slide-up { animation: slideUp 0.3s ease-out forwards; } @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } } /* --- Scrollbar --- */ .custom-scrollbar::-webkit-scrollbar { width: 6px; } .custom-scrollbar::-webkit-scrollbar-track { background: #f1f1f1; } .custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #94a3b8; } </style> </head> <body class="antialiased"> <div id="fatalError" class="fixed inset-0 bg-slate-900 z-[100] flex items-center justify-center p-8 hidden text-center text-white"> <div class="max-w-md w-full"> <div class="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6"> <svg class="w-10 h-10 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg> </div> <h1 class="text-3xl font-bold mb-4">System Locked</h1> <p id="fatalMsg" class="text-slate-300 text-lg font-light leading-relaxed"></p> <button onclick="window.location.reload()" class="mt-8 px-6 py-3 bg-red-600 hover:bg-red-700 rounded-lg font-semibold transition">Try Reloading</button> </div> </div> <nav class="bg-white border-b border-slate-200 z-50 sticky top-0 shadow-sm"> <div class="max-w-5xl mx-auto px-4 h-16 flex justify-between items-center"> <div class="flex items-center space-x-3"> <img src="https://i.postimg.cc/mgfGYYGq/download-(34).jpg" onerror="this.src='https://via.placeholder.com/40'" alt="Logo" class="h-10 w-10 object-contain rounded-md"> <div class="flex flex-col"> <h1 class="font-bold text-slate-800 text-sm md:text-base leading-tight tracking-tight">KSEF JUDGING PORTAL</h1> <div id="userInfo" class="hidden flex items-center text-[11px] md:text-xs text-slate-500 font-medium mt-0.5"> <span class="mr-1">Welcome,</span> <span id="navJudgeName" class="text-slate-900 font-bold mr-2 border-b border-slate-300 pb-0.5">Judge</span> <span class="text-slate-300 mr-2">|</span> <span id="navCategory" class="text-blue-600 font-bold uppercase tracking-wider">Category</span> </div> </div> </div> <button id="logoutBtn" onclick="handleLogout()" class="hidden text-xs font-semibold text-slate-500 hover:text-red-600 px-4 py-2 bg-slate-50 hover:bg-red-50 rounded-md border border-slate-200 hover:border-red-200 transition-all"> Sign Out </button> </div> </nav> <div id="announcementBar" class="hidden bg-red-600 text-white text-sm font-medium px-4 py-3 shadow-md relative z-40 fade-in"> <div class="max-w-5xl mx-auto flex items-start md:items-center"> <svg class="w-5 h-5 mr-3 flex-shrink-0 opacity-80" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"></path> </svg> <span id="announcementText" class="leading-tight"></span> <button onclick="document.getElementById('announcementBar').classList.add('hidden')" class="ml-auto pl-4 hover:text-indigo-200 transition"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> </button> </div> </div> <div id="loginScreen" class="flex-grow flex items-center justify-center p-4"> <div class="glass-panel rounded-2xl w-full max-w-md p-10 fade-in"> <div class="text-center mb-10"> <div class="w-16 h-16 bg-blue-50 text-blue-600 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-sm"> <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg> </div> <h2 class="text-2xl font-bold text-slate-800">Judge Sign In</h2> <p class="text-slate-400 text-sm mt-2">Secure access for KSEF Adjudication</p> </div> <form onsubmit="handleLogin(event)" class="space-y-5"> <div> <label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Username</label> <input type="text" id="username" class="w-full input-smart rounded-xl p-4 text-slate-800" placeholder="Enter your username" required> </div> <div> <label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Password</label> <input type="password" id="password" class="w-full input-smart rounded-xl p-4 text-slate-800" placeholder="••••••••" required> </div> <button type="submit" id="loginBtn" class="w-full bg-slate-900 hover:bg-slate-800 text-white font-bold py-4 rounded-xl shadow-lg active:scale-[0.98] transition-all text-sm mt-4 flex items-center justify-center gap-2"> Access Portal </button> </form> <p id="loginError" class="text-red-600 text-xs text-center mt-6 hidden font-bold bg-red-50 py-3 rounded-lg border border-red-100"></p> </div> </div> <div class="max-w-3xl mx-auto w-full p-4 hidden flex-grow" id="appContainer"> <div id="loading" class="text-center py-32"> <div class="loader mx-auto mb-6"></div> <p class="text-slate-500 text-sm font-medium tracking-widest uppercase">Syncing Database...</p> </div> <div id="mainInterface" class="hidden space-y-8"> <div id="projectSelectorContainer" class="glass-panel rounded-2xl p-8 fade-in"> <div class="flex justify-between items-center mb-6"> <div> <h2 class="text-xl font-bold text-slate-800">Pending Projects</h2> <p class="text-slate-400 text-xs mt-1">Select a project to start judging</p> </div> <button onclick="refreshData()" class="text-blue-600 bg-blue-50 hover:bg-blue-100 p-3 rounded-xl transition shadow-sm" title="Sync Data"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg> </button> </div> <div class="relative"> <select id="projectSelect" class="block w-full input-smart text-slate-700 py-5 px-5 rounded-xl text-base font-medium shadow-sm mb-6 bg-white appearance-none cursor-pointer"> <option>Loading...</option> </select> <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-5 text-slate-500"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg> </div> </div> <div id="noProjectsMsg" class="hidden flex items-center justify-center text-emerald-700 text-sm font-medium bg-emerald-50 p-6 rounded-xl border border-emerald-100 mb-6"> <svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> <span>All assigned projects have been judged or Part A/B/C are locked BY THE ADMIN!</span> </div> <button onclick="startJudging()" id="startBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-4 rounded-xl shadow-lg transition-all text-sm flex justify-center items-center group"> <span>Start Judging</span> <svg class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path></svg> </button> </div> <form id="judgeForm" class="hidden fade-in relative"> <div class="sticky top-16 z-30 bg-white/95 backdrop-blur-md border-b border-slate-200 shadow-sm py-4 px-6 -mx-4 mb-8 flex justify-between items-center rounded-b-xl"> <div class="flex items-center gap-3"> <button type="button" onclick="goBackToSelection()" class="p-2 -ml-2 text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded-full transition-colors" title="Back to Selection"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path> </svg> </button> <div class="flex flex-col"> <span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Currently Judging</span> <div class="text-lg font-bold text-slate-800 leading-none" id="stickyProjId">Project ...</div> </div> </div> <div class="flex flex-col items-end"> <span id="draftStatus" class="mb-1 hidden flex items-center text-emerald-600 text-[10px] font-bold uppercase tracking-wide"> <span class="w-2 h-2 bg-emerald-500 rounded-full mr-1.5 animate-pulse"></span> Auto-Saved </span> <div class="bg-indigo-50 px-3 py-1 rounded text-xs font-bold text-indigo-700 border border-indigo-100"> <span id="stickyPartName">Part A</span> </div> </div> </div> <div id="dynamicQuestions" class="space-y-6 pb-8"> </div> <div class="pt-4 pb-16"> <button type="button" onclick="openSummary()" class="w-full bg-slate-900 hover:bg-slate-800 text-white font-bold py-5 rounded-2xl shadow-xl transition-transform active:scale-[0.99] flex items-center justify-center text-base"> <span>Review & Submit Scores</span> <svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> </button> <p class="text-center text-slate-400 text-xs mt-4">Review your scores before final signature.</p> </div> </form> </div> <div id="successScreen" class="hidden p-8 text-center fade-in flex flex-col items-center justify-center min-h-[60vh]"> <div class="w-24 h-24 bg-green-100 text-green-600 rounded-full flex items-center justify-center mx-auto mb-8 shadow-sm"> <svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> </div> <h2 class="text-3xl font-bold text-slate-800 mb-3">Scores Submitted!</h2> <p class="text-slate-500 text-base mb-10 max-w-xs mx-auto">The project has been successfully recorded and removed from your pending list.</p> <div id="logoutWarning" class="hidden bg-red-50 text-red-700 p-6 rounded-xl mt-2 text-sm font-bold border border-red-200 w-full max-w-sm shadow-sm"> <p class="uppercase tracking-wide text-xs text-red-400 mb-1">System Alert</p> Judging is now CLOSED. Logging out automatically... </div> <button id="nextProjectBtn" onclick="resetUI()" class="px-10 py-4 bg-white border border-slate-300 text-slate-700 font-bold rounded-full hover:bg-slate-50 shadow-md transition hover:shadow-lg w-full md:w-auto"> Judge Next Project </button> </div> </div> <div id="modalOverlay" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm hidden z-[60] flex items-end md:items-center justify-center p-0 md:p-6 transition-all"> <div class="bg-white rounded-t-3xl md:rounded-3xl shadow-2xl w-full max-w-xl max-h-[90vh] flex flex-col animate-slide-up overflow-hidden"> <div class="p-5 border-b border-slate-100 flex justify-between items-center bg-slate-50"> <h3 class="text-lg font-bold text-slate-800">Confirm Scores</h3> <button onclick="closeModal()" class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-200 text-slate-500 transition font-bold text-xl"> &times; </button> </div> <div class="p-6 overflow-y-auto custom-scrollbar bg-white flex-grow"> <div id="summaryList" class="text-sm space-y-4 mb-8"> </div> <div class="bg-slate-50 p-6 rounded-2xl border border-dashed border-slate-300 text-center relative"> <p class="text-[11px] font-bold text-slate-400 uppercase mb-3 tracking-wider">Judge Signature (Required)</p> <div class="relative w-full h-32 bg-white rounded-lg border border-slate-200 shadow-inner overflow-hidden"> <canvas id="sigCanvas" width="400" height="150" class="w-full h-full"></canvas> </div> <button onclick="clearCanvas(); saveDraft();" class="text-xs text-red-500 hover:text-red-700 mt-3 font-bold flex items-center justify-center mx-auto"> <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg> Clear Signature </button> </div> </div> <div class="p-5 border-t border-slate-100 bg-white grid grid-cols-2 gap-4 pb-8 md:pb-5"> <button onclick="closeModal()" class="px-4 py-3.5 text-slate-600 font-bold hover:bg-slate-100 rounded-xl transition text-sm"> Back to Edit </button> <button id="confirmSubmitBtn" onclick="submitData()" class="px-4 py-3.5 bg-blue-600 text-white font-bold rounded-xl shadow-lg hover:bg-blue-700 transition text-sm"> Confirm & Submit </button> </div> </div> </div> <footer class="bg-white border-t border-slate-200 mt-auto z-10 relative"> <div class="max-w-5xl mx-auto px-4 py-6 flex flex-col md:flex-row justify-between items-center"> <div class="flex items-center space-x-2 mb-2 md:mb-0"> <span class="text-slate-400 text-xs">©2026 ALL RIGHT RESERVED</span> </div> <div class="text-slate-400 text-[10px] uppercase tracking-widest font-bold"> Kenya Science & Engineering Fair </div> </div> </footer> <script> // *** CONFIGURATION *** const SCRIPT_URL = "https://script.google.com/macros/s/AKfycbyhHHyfk2WPonCXVmc03Hc0fcBhkGatKfkXDD-f4nDGlFCMo4RZ8QNwOCBd7szw2kyB/exec"; // *** GLOBAL STATE *** let DATA = null; let USER = { name: "", category: "", username: "" }; let ACTIVE_PROJECT = null; /** * 1. HANDLE LOGIN */ async function handleLogin(e) { e.preventDefault(); const btn = document.getElementById('loginBtn'); const originalText = btn.textContent; // === NEW ANIMATION CODE START === // 1. Disable and dim the button btn.disabled = true; btn.classList.add('opacity-75', 'cursor-not-allowed'); // 2. Inject SVG Spinner + Text btn.innerHTML = ` <svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> <span>Signing in...</span> `; // === NEW ANIMATION CODE END === try { const u = document.getElementById('username').value; const p = document.getElementById('password').value; const res = await fetch(SCRIPT_URL, { method: 'POST', body: JSON.stringify({ action: 'login', data: { username: u, password: p } }) }); const result = await res.json(); if (result.fatal) { return showFatal(result.error); } if (result.success) { // STORE USER DATA USER = { name: result.judgeName, category: result.category, username: result.username }; // *** SET USER INFO & UNHIDE *** const nameDisplay = document.getElementById('navJudgeName'); const catDisplay = document.getElementById('navCategory'); const userPanel = document.getElementById('userInfo'); if (nameDisplay) nameDisplay.textContent = USER.name; if (catDisplay) catDisplay.textContent = USER.category; if (userPanel) userPanel.classList.remove('hidden'); document.getElementById('logoutBtn').classList.remove('hidden'); toggleScreens('app'); init(); } else { showError(result.error); // RESET BUTTON STATE ON ERROR btn.textContent = originalText; btn.disabled = false; btn.classList.remove('opacity-75', 'cursor-not-allowed'); // Remove the dimming } } catch (err) { showError("Connection failed. Please check your internet."); console.error(err); // RESET BUTTON STATE ON ERROR btn.textContent = originalText; btn.disabled = false; btn.classList.remove('opacity-75', 'cursor-not-allowed'); // Remove the dimming } } function handleLogout() { // Reset State USER = {}; DATA = null; ACTIVE_PROJECT = null; // Clear Forms document.getElementById('username').value = ""; document.getElementById('password').value = ""; // Hide User Info in Nav document.getElementById('userInfo').classList.add('hidden'); document.getElementById('logoutBtn').classList.add('hidden'); toggleScreens('login'); const btn = document.getElementById('loginBtn'); btn.textContent = "Access Portal"; btn.disabled = false; } /** * 2. INITIALIZATION */ async function init() { const loading = document.getElementById('loading'); const interface = document.getElementById('mainInterface'); loading.classList.remove('hidden'); interface.classList.add('hidden'); try { const res = await fetch(`${SCRIPT_URL}?action=getData`); const json = await res.json(); if(json.fatal) { return showFatal(json.error); } DATA = json; // *** ANNOUNCEMENT LOGIC (FIXED) *** // Improved check to ensure string conversion and visibility if (DATA.config.announcement && String(DATA.config.announcement).trim().length > 0) { const bar = document.getElementById('announcementBar'); const txt = document.getElementById('announcementText'); txt.textContent = DATA.config.announcement; bar.classList.remove('hidden'); // Smooth scroll to ensure visibility window.scrollTo({ top: 0, behavior: 'smooth' }); } // *************************** populateProjects(); loading.classList.add('hidden'); interface.classList.remove('hidden'); if(typeof initCanvas === 'function') initCanvas(); } catch (e) { console.error(e); alert("Data Sync Failed. Please refresh the page."); } } /** * 3. PROJECT SELECTION (FIXED: Hides Completed Projects) */ function populateProjects() { const select = document.getElementById('projectSelect'); select.innerHTML = '<option value="" disabled selected>Select a project to judge...</option>'; // 1. Setup Judge Data const myCat = USER.category ? USER.category.toUpperCase().trim() : ""; const myUser = USER.username.toLowerCase().trim(); const myName = USER.name.toLowerCase().trim(); // We will check this too // 2. Setup Config const configA = (DATA.config.activePartA || []).map(c => c.toUpperCase()); const configBC = (DATA.config.activePartBC || []).map(c => c.toUpperCase()); let count = 0; DATA.projects.forEach(p => { // A. Check Category Match (using Pipes |) const pCats = (p.category || "").toUpperCase().split('|').map(s => s.trim()); if (!pCats.includes(myCat)) return; // B. Find Existing Score (Robust Match) // Checks if EITHER Username OR Name matches the database record const score = DATA.scoreMap.find(s => String(s.pid) === String(p.id) && (String(s.judge).toLowerCase() === myUser || String(s.judge).toLowerCase() === myName) ); const hasA = score ? score.hasA : false; const hasBC = score ? score.hasBC : false; let mode = null; // C. Determine if it should be shown // CASE 1: Part A is required AND I haven't done it yet if (configA.includes(myCat) && !hasA) { mode = 'A'; } // CASE 2: Part B is required AND I haven't done it yet // (Only shows if A is done OR A wasn't required, but B is still pending) else if (configBC.includes(myCat) && !hasBC) { mode = 'BC'; } // If a mode was found, add to list if (mode) { const opt = document.createElement('option'); opt.value = JSON.stringify({ id: p.id, mode: mode }); // Display clearly which part is needed opt.textContent = `Project ${p.id} (${mode === 'A' ? 'Report' : 'Oral'})`; select.appendChild(opt); count++; } }); // D. UI State (Enable/Disable Button) const msg = document.getElementById('noProjectsMsg'); const btn = document.querySelector('#startBtn'); if(count === 0) { msg.classList.remove('hidden'); select.disabled = true; btn.classList.add('opacity-50', 'cursor-not-allowed'); btn.disabled = true; } else { msg.classList.add('hidden'); select.disabled = false; btn.classList.remove('opacity-50', 'cursor-not-allowed'); btn.disabled = false; } } function startJudging() { const val = document.getElementById('projectSelect').value; if(!val || val === "Loading...") { return alert("Please select a project first."); } ACTIVE_PROJECT = JSON.parse(val); document.getElementById('stickyProjId').textContent = `Project ${ACTIVE_PROJECT.id}`; document.getElementById('stickyPartName').textContent = ACTIVE_PROJECT.mode === 'A' ? "Part A (Report)" : "Part B/C (Oral)"; document.getElementById('projectSelectorContainer').classList.add('hidden'); document.getElementById('judgeForm').classList.remove('hidden'); renderQuestions(); restoreDraft(); window.scrollTo(0,0); } /** * 4. RENDER QUESTIONS */ function renderQuestions() { const container = document.getElementById('dynamicQuestions'); container.innerHTML = ''; const mode = ACTIVE_PROJECT.mode; // 1. Get Judge Category (UPPERCASE & TRIMMED) const myCat = USER.category.toUpperCase().trim(); let counter = 1; DATA.questions.forEach((q, idx) => { // 2. LOGIC CHANGE: Split by Pipe (|) and check exact inclusion // Example: q.category = "Physics|Chemistry" -> ["PHYSICS", "CHEMISTRY"] const questionCategories = (q.category || "").toUpperCase().split('|').map(s => s.trim()); // If the Judge's category is NOT in the list, skip this question if (!questionCategories.includes(myCat)) return; const qPart = q.part.toUpperCase(); let show = false; // Part A vs Part B Logic if (mode === 'A') { show = (qPart.includes('A') && !qPart.includes('ORAL')); } else { show = (!qPart.includes('A') || qPart.includes('ORAL')); } if(show) { // Title Logic const isTitle = (q.type || "").toUpperCase().trim() === 'TITLE'; container.appendChild(createCard(q, idx, isTitle ? null : counter)); if (!isTitle) counter++; } }); } function createCard(q, idx, num) { // 1. TITLE TYPE (No Input) if ((q.type || "").toUpperCase().trim() === 'TITLE') { const titleCard = document.createElement('div'); titleCard.className = "mt-8 mb-4 border-l-4 border-slate-800 pl-4 py-2"; titleCard.innerHTML = ` <h3 class="text-xl font-extrabold text-slate-800 uppercase tracking-wide">${q.text}</h3> ${q.guide ? `<p class="text-sm text-slate-500 mt-1 italic">${q.guide}</p>` : ''} `; return titleCard; } // 2. STANDARD QUESTION CARD const card = document.createElement('div'); card.className = "bg-white p-6 rounded-2xl border border-slate-200 shadow-sm question-card transition-all"; card.id = `card-${q.id}`; // Header (Removed static guide to fix the text issue) const header = ` <div class="mb-4"> <div class="flex items-start justify-between"> <span class="text-xs font-bold text-slate-400 uppercase tracking-wider">Metric ${num}</span> ${q.required ? '<span class="text-[10px] bg-red-50 text-red-500 px-2 py-0.5 rounded border border-red-100 font-bold">REQUIRED</span>' : ''} </div> <p class="text-slate-800 font-semibold text-sm mt-1">${q.text}</p> </div> `; card.dataset.id = q.id; card.dataset.required = q.required; card.dataset.text = q.text; const content = document.createElement('div'); // A. Split Question (Feedback) if (q.text.toUpperCase().includes("FEEDBACK") || q.text.toUpperCase().includes("STRENGTH")) { content.innerHTML = ` <div class="space-y-3"> <div> <label class="block text-xs font-bold text-slate-500 mb-1">Strengths (Min 15 chars)</label> <textarea id="str-${q.id}" class="w-full input-smart rounded-xl p-3 text-sm" rows="3" placeholder="Identify strong points..." oninput="clearError(${q.id}); saveDraft()"></textarea> </div> <div> <label class="block text-xs font-bold text-slate-500 mb-1">Recommendations (Min 15 chars)</label> <textarea id="rec-${q.id}" class="w-full input-smart rounded-xl p-3 text-sm" rows="3" placeholder="Suggest improvements..." oninput="clearError(${q.id}); saveDraft()"></textarea> </div> <textarea name="q-${q.id}" id="real-q-${q.id}" class="hidden"></textarea> </div>`; } // B. Scale & Options (Restored updateInterpretation) else if ((q.type || "").toLowerCase().includes('scale') || (q.type || "").toLowerCase().includes('choice')) { const flex = document.createElement('div'); flex.className = "flex flex-wrap gap-3"; const options = q.options ? String(q.options).split(',') : ["1","2","3","4","5","6","7","8","9","10"]; options.forEach(optVal => { const v = optVal.trim(); const uuid = `q${q.id}-${v.replace(/[^a-zA-Z0-9]/g, '')}`; flex.innerHTML += ` <div> <input type="radio" name="q-${q.id}" id="${uuid}" value="${v}" class="scale-radio" onchange="updateInterpretation(this, '${q.id}'); saveDraft(); clearError(${q.id});"> <label for="${uuid}" class="px-4 py-3 rounded-lg border border-slate-200 text-sm font-bold text-slate-600 hover:bg-slate-50 transition-all text-center min-w-[50px] shadow-sm cursor-pointer select-none">${v}</label> </div> `; }); content.appendChild(flex); // Restored Guide Box for Dynamic Text if (q.guide && q.guide.length > 3) { const guideDiv = document.createElement('div'); guideDiv.id = `guide-${q.id}`; // Added 'guide-box' class to ensure animation works guideDiv.className = "guide-box bg-indigo-50 text-indigo-900 text-xs rounded-xl italic font-medium leading-relaxed"; guideDiv.dataset.raw = q.guide; content.appendChild(guideDiv); } } // C. Text Area else { content.innerHTML = `<textarea name="q-${q.id}" class="w-full input-smart rounded-xl p-4 text-sm leading-relaxed" rows="3" placeholder="Enter detailed comments..." oninput="clearError(${q.id}); saveDraft()"></textarea>`; } card.innerHTML = header; card.appendChild(content); return card; } /** * 5. SMART GUIDE LOGIC */ function updateInterpretation(radio, qId) { clearError(qId); const guideDiv = document.getElementById(`guide-${qId}`); if (!guideDiv) return; const val = radio.value.trim(); const raw = guideDiv.dataset.raw; const parts = raw.split('|'); let foundText = ""; for (let part of parts) { const firstColonIndex = part.indexOf(':'); if (firstColonIndex > -1) { const scoreKey = part.substring(0, firstColonIndex).trim(); const explanation = part.substring(firstColonIndex + 1).trim(); if (scoreKey === val) { foundText = explanation; break; } } } if (foundText) { guideDiv.innerHTML = `<span class="block text-indigo-600 font-bold mb-1 uppercase text-[10px]"> rubric Interpretation:</span>${foundText}`; guideDiv.classList.add('active'); } else { guideDiv.classList.remove('active'); } } function clearError(qId) { const card = document.getElementById(`card-${qId}`); if (card) card.classList.remove('error-ring'); } /** * 6. AUTO-SAVE & DRAFTING */ function getDraftKey() { return `ksef_draft_${USER.username}_${ACTIVE_PROJECT.id}_${ACTIVE_PROJECT.mode}`; } // Global variable to hold the timer let saveTimeout; function saveDraft() { if (!ACTIVE_PROJECT) return; // 1. Clear the previous timer (cancels the save if they keep typing) if (saveTimeout) clearTimeout(saveTimeout); // 2. Hide the "Auto-Saved" status while typing/waiting const status = document.getElementById('draftStatus'); status.classList.remove('flex'); status.classList.add('hidden'); // 3. Set a new timer to run the save logic after 2000ms (2 seconds) saveTimeout = setTimeout(() => { performSave(); }, 2000); } // Move the heavy logic into this new separate function function performSave() { // --- EXISTING LOGIC STARTS HERE --- document.querySelectorAll('[id^="str-"]').forEach(el => { const id = el.id.replace('str-', ''); const rec = document.getElementById(`rec-${id}`).value; document.getElementById(`real-q-${id}`).value = `Strength: ${el.value} | Recs: ${rec}`; }); const answers = {}; document.querySelectorAll('[name^="q-"]').forEach(inp => { if(inp.type === 'radio') { if(inp.checked) answers[inp.name] = inp.value; } else { answers[inp.name] = inp.value; } }); const sig = document.getElementById('sigCanvas').toDataURL('image/webp', 0.5); const payload = { answers, sig }; localStorage.setItem(getDraftKey(), JSON.stringify(payload)); // Show "Auto-Saved" status only after it actually saves const status = document.getElementById('draftStatus'); status.classList.remove('hidden'); status.classList.add('flex'); // --- EXISTING LOGIC ENDS HERE --- } function restoreDraft() { const raw = localStorage.getItem(getDraftKey()); if (!raw) return; try { const data = JSON.parse(raw); for (const [name, val] of Object.entries(data.answers)) { const radios = document.querySelectorAll(`input[name="${name}"]`); if (radios.length > 0) { radios.forEach(r => { if(r.value === val) { r.checked = true; updateInterpretation(r, name.replace('q-','')); } }); } else { const txt = document.querySelector(`textarea[name="${name}"]`); if(txt) { txt.value = val; if (val.includes("Strength:") && val.includes("| Recs:")) { const id = name.replace('q-',''); const parts = val.split("| Recs:"); const strVal = parts[0].replace("Strength:", "").trim(); const recVal = parts[1].trim(); const elStr = document.getElementById(`str-${id}`); const elRec = document.getElementById(`rec-${id}`); if(elStr) elStr.value = strVal; if(elRec) elRec.value = recVal; } } } } if (data.sig && data.sig.length > 100) { const img = new Image(); img.onload = function() { ctx.drawImage(img, 0, 0); }; img.src = data.sig; } document.getElementById('draftStatus').classList.remove('hidden'); document.getElementById('draftStatus').classList.add('flex'); } catch(e) { console.log("Draft restore error", e); } } /** * 7. SUBMISSION & VALIDATION */ function openSummary() { document.querySelectorAll('[id^="str-"]').forEach(el => { const id = el.id.replace('str-', ''); document.getElementById(`real-q-${id}`).value = `Strength: ${el.value} | Recs: ${document.getElementById(`rec-${id}`).value}`; }); const list = document.getElementById('summaryList'); list.innerHTML = ''; let hasError = false; document.querySelectorAll('.question-card').forEach(card => { const req = card.dataset.required === "true"; const inp = card.querySelector('input:checked, textarea[name^="q-"]'); let val = ""; if(inp) val = (inp.tagName === 'INPUT') ? inp.value : inp.value.trim(); // *** STRICT VALIDATION LOGIC (15 Chars) *** let isInvalid = false; if (req && !val) { isInvalid = true; } if (req && inp && (inp.tagName === 'TEXTAREA' || inp.type === 'textarea')) { if (val.length < 15) isInvalid = true; } const splitStr = card.querySelector('[id^="str-"]'); const splitRec = card.querySelector('[id^="rec-"]'); if (splitStr && splitRec) { if (splitStr.value.trim().length < 15 || splitRec.value.trim().length < 15) { isInvalid = true; } } if (isInvalid) { card.classList.add('error-ring'); hasError = true; if(!document.querySelector('.error-focus')) { card.scrollIntoView({behavior: "smooth", block: "center"}); card.classList.add('error-focus'); } } else { card.classList.remove('error-ring'); } if(!isInvalid && val) { list.innerHTML += ` <div class="border-b border-slate-100 pb-3 last:border-0"> <div class="text-[10px] text-slate-400 uppercase tracking-wide mb-1">${card.dataset.text}</div> <div class="text-sm font-semibold text-slate-800 leading-relaxed bg-slate-50 p-2 rounded-lg">${val.substring(0,120)}${val.length>120?'...':''}</div> </div>`; } }); document.querySelectorAll('.error-focus').forEach(e => e.classList.remove('error-focus')); if(hasError) { return alert("Please review your answers.\n\n- All red fields are required.\n- Text answers must be at least 15 characters long."); } document.getElementById('modalOverlay').classList.remove('hidden'); } function closeModal() { document.getElementById('modalOverlay').classList.add('hidden'); } async function submitData() { if(isCanvasBlank(document.getElementById('sigCanvas'))) { return alert("You must sign the form before submitting."); } const btn = document.getElementById('confirmSubmitBtn'); const originalText = btn.innerHTML; btn.textContent = "Sending Data..."; btn.disabled = true; const answers = {}; document.querySelectorAll('[name^="q-"]').forEach(inp => { if(inp.type === 'radio' && !inp.checked) return; answers[inp.name.split('-')[1]] = inp.value; }); const payload = { action: ACTIVE_PROJECT.mode === 'A' ? 'submitPartA' : 'submitPartBC', data: { projectId: ACTIVE_PROJECT.id, judgeName: USER.name, // New Line 987: signature: document.getElementById('sigCanvas').toDataURL('image/webp', 0.5), answers: answers } }; try { // Optimistic UI Update (Client Side) const exist = DATA.scoreMap.find(s => s.pid === ACTIVE_PROJECT.id && s.judge === USER.name); if (exist) { if (ACTIVE_PROJECT.mode === 'A') exist.hasA = true; if (ACTIVE_PROJECT.mode === 'BC') exist.hasBC = true; } else { DATA.scoreMap.push({ pid: ACTIVE_PROJECT.id, judge: USER.name, hasA: ACTIVE_PROJECT.mode === 'A', hasBC: ACTIVE_PROJECT.mode === 'BC' }); } const res = await fetch(SCRIPT_URL, { method: 'POST', body: JSON.stringify(payload) }); const json = await res.json(); if (json.success) { localStorage.removeItem(getDraftKey()); closeModal(); document.getElementById('judgeForm').classList.add('hidden'); document.getElementById('successScreen').classList.remove('hidden'); if (json.forcedLogout) { document.getElementById('logoutWarning').classList.remove('hidden'); document.getElementById('nextProjectBtn').classList.add('hidden'); setTimeout(() => { handleLogout(); }, 4000); } } else { alert(json.error); } } catch (e) { alert("Submission Failed. Check internet."); } btn.disabled = false; btn.innerHTML = originalText; } function resetUI() { document.getElementById('successScreen').classList.add('hidden'); document.getElementById('judgeForm').reset(); document.getElementById('projectSelectorContainer').classList.remove('hidden'); populateProjects(); window.scrollTo(0,0); } function goBackToSelection() { // 1. Hide the Judging Form document.getElementById('judgeForm').classList.add('hidden'); // 2. Show the Project Selection Screen document.getElementById('projectSelectorContainer').classList.remove('hidden'); // 3. Scroll to top window.scrollTo(0,0); } // *** HELPER UTILITIES *** function toggleScreens(s) { const login = document.getElementById('loginScreen'); const app = document.getElementById('appContainer'); if(s==='app'){ login.classList.add('hidden'); app.classList.remove('hidden'); } else { login.classList.remove('hidden'); app.classList.add('hidden'); } } function showError(m) { const e = document.getElementById('loginError'); e.textContent = m; e.classList.remove('hidden'); } function showFatal(m) { document.getElementById('fatalMsg').textContent = m; document.getElementById('fatalError').classList.remove('hidden'); document.getElementById('appContainer').classList.add('hidden'); document.getElementById('loginScreen').classList.add('hidden'); } function refreshData() { init(); } // *** CANVAS LOGIC *** const canvas = document.getElementById('sigCanvas'); const ctx = canvas.getContext('2d'); let isDrawing = false; function initCanvas() { ['mousedown', 'touchstart'].forEach(e => canvas.addEventListener(e, start, {passive: false})); ['mousemove', 'touchmove'].forEach(e => canvas.addEventListener(e, draw, {passive: false})); ['mouseup', 'touchend'].forEach(e => canvas.addEventListener(e, stop)); } function start(e) { e.preventDefault(); isDrawing=true; const pt=getPoint(e); ctx.beginPath(); ctx.moveTo(pt.x, pt.y); } function draw(e) { if(!isDrawing)return; e.preventDefault(); const pt=getPoint(e); ctx.lineTo(pt.x, pt.y); ctx.stroke(); } function stop() { isDrawing=false; saveDraft(); } function getPoint(e) { const rect = canvas.getBoundingClientRect(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; return { x: (clientX - rect.left) * (canvas.width / rect.width), y: (clientY - rect.top) * (canvas.height / rect.height) }; } function clearCanvas() { ctx.clearRect(0,0,canvas.width,canvas.height); } function isCanvasBlank(c) { const bl=document.createElement('canvas'); bl.width=c.width; bl.height=c.height; return c.toDataURL()===bl.toDataURL(); } </script> </body> </html> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="description" content="KSEF Science Fair Administration Portal V15 - Enterprise Edition"> <meta name="author" content="System Administrator"> <meta name="version" content="15.0.0-ULTIMATE"> <title>KSEF Admin Console V15 (Protected)</title> <script src="https://cdn.tailwindcss.com/3.4.1"></script> <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.28/jspdf.plugin.autotable.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js"></script> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"> <style> /* ------------------------------------------------------------------ 2.1 CSS VARIABLES & RESET ------------------------------------------------------------------ */ :root { --primary-color: #4f46e5; --primary-dark: #4338ca; --secondary-color: #64748b; --success-color: #22c55e; --danger-color: #ef4444; --warning-color: #f59e0b; --bg-glass: rgba(255, 255, 255, 0.98); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); } body { font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #f1f5f9; /* Slate-100 */ color: #1e293b; /* Slate-800 */ min-height: 100vh; padding-bottom: 70px; /* Accommodate fixed footer */ overflow-x: hidden; /* Prevent horizontal scrollbar */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* ------------------------------------------------------------------ 2.2 GLASSMORPHISM CONTAINERS ------------------------------------------------------------------ */ .panel { background: var(--bg-glass); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); transition: box-shadow 0.3s ease, border-color 0.3s ease; } .panel:hover { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.025); border-color: #cbd5e1; } /* ------------------------------------------------------------------ 2.3 INTERACTIVE INPUTS & FORMS ------------------------------------------------------------------ */ .input-light { background-color: #ffffff; border: 1px solid #cbd5e1; color: #0f172a; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .input-light:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.15); background-color: #f8fafc; } .input-light::placeholder { color: #94a3b8; font-weight: 400; } /* ------------------------------------------------------------------ 2.4 STATEFUL BUTTONS (TOGGLES) ------------------------------------------------------------------ */ .status-btn, .upload-btn, .progress-btn { transition: all 0.2s ease; border: 1px solid transparent; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; } /* Active/Open State */ .status-btn.active[data-val="OPEN"], .upload-btn.active[data-val="Active"], .progress-btn.active[data-val="Allow"] { background-color: #dcfce7; border-color: var(--success-color); color: #15803d; font-weight: 800; transform: scale(1.02); box-shadow: 0 2px 4px rgba(34, 197, 94, 0.1); } /* Closed/Locked State */ .status-btn.active[data-val="CLOSED"], .upload-btn.active[data-val="Closed"], .progress-btn.active[data-val="Locked"] { background-color: #fee2e2; border-color: var(--danger-color); color: #b91c1c; font-weight: 800; transform: scale(1.02); box-shadow: 0 2px 4px rgba(239, 68, 68, 0.1); } /* ------------------------------------------------------------------ 2.5 COMPLEX DATA TABLES (STICKY HEADERS & SCROLLING) ------------------------------------------------------------------ */ /* Container for the table area to allow independent scrolling */ .table-scroll-area { position: relative; overflow: auto; max-height: 100%; width: 100%; } .analysis-table { width: 100%; border-collapse: separate; border-spacing: 0; } /* STICKY HEADER IMPLEMENTATION */ .analysis-table th { background-color: #f8fafc; font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: #475569; padding: 14px 10px; border-bottom: 2px solid #e2e8f0; border-right: 1px solid #f1f5f9; white-space: normal; word-wrap: break-word; /* Sticky Positioning */ position: sticky; top: 0; z-index: 20; text-align: left; vertical-align: bottom; font-weight: 700; box-shadow: 0 2px 4px rgba(0,0,0,0.02); } .analysis-table td { padding: 10px; font-size: 11px; border-bottom: 1px solid #f1f5f9; border-right: 1px solid #f8fafc; color: #334155; vertical-align: middle; line-height: 1.4; } .analysis-table tr:hover td { background-color: #eff6ff; color: #1e40af; cursor: pointer; } /* Qualifier Row Highlighting */ .qualifier-row td { background-color: #f0fdf4 !important; } .qualifier-row td:first-child { border-left: 4px solid var(--success-color); } /* STICKY TAB NAV (Pin buttons to top) */ .sticky-tab-nav { position: sticky; top: 0; z-index: 30; background-color: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-bottom: 1px solid #e2e8f0; } /* ------------------------------------------------------------------ 2.6 LOADING & OVERLAYS ------------------------------------------------------------------ */ #globalLoader { backdrop-filter: blur(12px); background: rgba(255, 255, 255, 0.9); z-index: 99999; } .security-footer { position: fixed; bottom: 0; left: 0; width: 100%; background-color: #ffffff; border-top: 1px solid #e2e8f0; text-align: center; padding: 12px; font-size: 11px; color: #94a3b8; font-weight: 600; z-index: 10000; letter-spacing: 0.05em; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.05); } /* ------------------------------------------------------------------ 2.7 SCROLLBARS (CUSTOM) ------------------------------------------------------------------ */ .custom-scrollbar::-webkit-scrollbar { width: 8px; height: 8px; } .custom-scrollbar::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; } .custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; border: 2px solid #f1f5f9; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #94a3b8; } /* ------------------------------------------------------------------ 2.8 ANIMATIONS ------------------------------------------------------------------ */ .animate-fade-in { animation: fadeIn 0.5s ease-out forwards; } .animate-bounce-in { animation: bounceIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @keyframes bounceIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } /* ------------------------------------------------------------------ 2.9 PRINT STYLES ------------------------------------------------------------------ */ @media print { body * { visibility: hidden; } #printArea, #printArea * { visibility: visible; } #printArea { position: absolute; left: 0; top: 0; width: 100%; margin: 0; padding: 20px; box-sizing: border-box; background: white; } } </style> </head> <body class="min-h-screen flex flex-col items-center p-4 md:p-6 lg:p-10 relative"> <script> // *** SECURITY NOTICE: REPLACE WITH YOUR EXACT WEB APP URL *** // *** ACCESS SETTING MUST BE: "ANYONE" *** const MANUAL_URL = "https://script.google.com/macros/s/AKfycbyhHHyfk2WPonCXVmc03Hc0fcBhkGatKfkXDD-f4nDGlFCMo4RZ8QNwOCBd7szw2kyB/exec"; // SYSTEM LOGO URL const LOGO_URL = "https://i.postimg.cc/mgfGYYGq/download-34.jpg"; </script> <div id="globalLoader" class="fixed inset-0 hidden flex flex-col items-center justify-center transition-opacity duration-300"> <div class="relative"> <div class="animate-spin rounded-full h-24 w-24 border-t-4 border-b-4 border-indigo-600 mb-4 shadow-2xl"></div> <div class="absolute top-0 left-0 h-24 w-24 flex items-center justify-center"> <i class="ph ph-shield-check text-indigo-200 text-3xl"></i> </div> </div> <p id="loaderText" class="text-indigo-900 font-extrabold text-xl animate-pulse tracking-wide mt-6">System Initializing...</p> <p class="text-xs text-gray-400 mt-2 font-mono uppercase">Secure Environment V15</p> </div> <div id="loginOverlay" class="fixed inset-0 z-[100] bg-gray-50 flex items-center justify-center p-4 bg-opacity-95 backdrop-blur-sm"> <div class="bg-white p-12 rounded-3xl shadow-2xl w-full max-w-md text-center border border-gray-100 relative overflow-hidden animate-bounce-in"> <div class="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500"></div> <img src="https://i.postimg.cc/mgfGYYGq/download-34.jpg" class="h-28 mx-auto mb-8 rounded-2xl shadow-lg object-cover ring-4 ring-gray-50"> <h2 class="text-3xl font-extrabold text-gray-900 mb-2 tracking-tight">Admin Access</h2> <p class="text-xs text-gray-500 mb-10 uppercase tracking-widest font-bold">Authorized Personnel Only</p> <div class="relative mb-8 group"> <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400 group-focus-within:text-indigo-500 transition-colors"><i class="ph ph-lock-key text-2xl"></i></div> <input type="password" id="adminPass" class="input-light w-full p-4 pl-12 rounded-xl text-center font-bold text-xl tracking-widest" placeholder="••••••••"> </div> <button onclick="attemptLogin()" id="btnLogin" class="w-full bg-indigo-600 text-white font-bold py-4 rounded-xl hover:bg-indigo-700 hover:shadow-xl transition-all duration-200 transform hover:-translate-y-1 flex items-center justify-center gap-3 text-lg"><i class="ph ph-key text-2xl"></i> Unlock Portal</button> <div class="mt-8 pt-6 border-t border-gray-100"><p class="text-[10px] text-gray-400 flex items-center justify-center gap-2"><i class="ph ph-shield-check text-green-500"></i> Secured by MacAfee</p></div> </div> </div> <div id="mainUI" class="w-full max-w-7xl hidden animate-fade-in pb-16"> <div class="flex flex-col md:flex-row justify-between items-center mb-8 gap-6 bg-white p-5 rounded-2xl shadow-sm border border-gray-200"> <div class="flex items-center gap-5"> <img src="https://i.postimg.cc/mgfGYYGq/download-34.jpg" class="h-16 w-16 rounded-xl shadow-md object-cover ring-2 ring-gray-100"> <div> <h1 class="text-3xl font-extrabold tracking-tight text-gray-900">ADMIN <span class="text-indigo-600">PORTAL</span></h1> <p class="text-xs text-gray-500 font-bold uppercase tracking-[0.3em] mt-1">KSEF Control Center</p> </div> </div> <div class="flex flex-wrap justify-center items-center gap-3 w-full md:w-auto"> <div class="flex flex-wrap justify-center bg-gray-100 p-1.5 rounded-2xl shadow-inner border border-gray-200 w-full md:w-auto"> <button onclick="setPage('config')" id="nav-config" class="nav-item flex-grow whitespace-nowrap px-6 py-3 rounded-xl text-sm font-bold text-indigo-600 bg-white shadow-sm transition-all duration-200 flex items-center justify-center gap-2"> <i class="ph ph-gear text-lg"></i> Config </button> <button onclick="setPage('judges')" id="nav-judges" class="nav-item flex-grow whitespace-nowrap px-6 py-3 rounded-xl text-sm font-bold text-gray-500 hover:bg-white hover:shadow-sm transition-all duration-200 flex items-center justify-center gap-2"> <i class="ph ph-users text-lg"></i> Judges </button> <button onclick="setPage('analysis')" id="nav-analysis" class="nav-item flex-grow whitespace-nowrap px-6 py-3 rounded-xl text-sm font-bold text-gray-500 hover:bg-white hover:shadow-sm transition-all duration-200 flex items-center justify-center gap-2"> <i class="ph ph-chart-pie-slice text-lg"></i> Analysis </button> </div> <div class="hidden md:block h-12 w-px bg-gray-300 mx-2"></div> <button onclick="logout()" class="p-4 bg-red-50 text-red-600 rounded-2xl hover:bg-red-600 hover:text-white font-bold text-xl shadow-sm border border-red-100 transition-all duration-200" title="Secure Logout"> <i class="ph ph-sign-out"></i> </button> </div> </div> <div id="view-config" class="view-section grid grid-cols-1 lg:grid-cols-12 gap-6"> <div class="lg:col-span-4 space-y-6"> <div class="panel p-6 rounded-3xl"> <h3 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-5 flex items-center gap-2 border-b border-gray-100 pb-3"><i class="ph ph-sliders-horizontal text-indigo-500 text-lg"></i> System Controls</h3> <div class="space-y-4"> <div class="flex justify-between items-center bg-gray-50 p-4 rounded-xl hover:bg-gray-100 transition-colors border border-transparent hover:border-gray-200"> <div class="flex flex-col"><span class="text-xs font-bold text-gray-700 ml-2">System Status</span><span class="text-[9px] text-gray-400 ml-2 font-mono">GLOBAL ACCESS</span></div> <div class="flex bg-white rounded-lg p-1 shadow-sm border border-gray-200"><button onclick="setVal('statusVal', 'OPEN')" class="status-btn px-4 py-1.5 rounded-md text-[10px] font-bold" data-target="statusVal" data-val="OPEN">OPEN</button><button onclick="setVal('statusVal', 'CLOSED')" class="status-btn px-4 py-1.5 rounded-md text-[10px] font-bold" data-target="statusVal" data-val="CLOSED">CLOSED</button></div><input type="hidden" id="statusVal"> </div> <div class="flex justify-between items-center bg-gray-50 p-4 rounded-xl hover:bg-gray-100 transition-colors border border-transparent hover:border-gray-200"> <div class="flex flex-col"><span class="text-xs font-bold text-gray-700 ml-2">Project Uploads</span><span class="text-[9px] text-gray-400 ml-2 font-mono">REGISTRATION</span></div> <div class="flex bg-white rounded-lg p-1 shadow-sm border border-gray-200"><button onclick="setVal('uploadVal', 'Active')" class="upload-btn px-4 py-1.5 rounded-md text-[10px] font-bold" data-target="uploadVal" data-val="Active">Active</button><button onclick="setVal('uploadVal', 'Closed')" class="upload-btn px-4 py-1.5 rounded-md text-[10px] font-bold" data-target="uploadVal" data-val="Closed">Closed</button></div><input type="hidden" id="uploadVal"> </div> <div class="flex justify-between items-center bg-gray-50 p-4 rounded-xl hover:bg-gray-100 transition-colors border border-transparent hover:border-gray-200"> <div class="flex flex-col"><span class="text-xs font-bold text-gray-700 ml-2">View Progress</span><span class="text-[9px] text-gray-400 ml-2 font-mono">Allow participants to View Project progress</span></div> <div class="flex bg-white rounded-lg p-1 shadow-sm border border-gray-200"><button onclick="setVal('progressVal', 'Allow')" class="progress-btn px-4 py-1.5 rounded-md text-[10px] font-bold" data-target="progressVal" data-val="Allow">Allow</button><button onclick="setVal('progressVal', 'Locked')" class="progress-btn px-4 py-1.5 rounded-md text-[10px] font-bold" data-target="progressVal" data-val="Locked">Locked</button></div><input type="hidden" id="progressVal"> </div> <div class="flex justify-between items-center bg-indigo-50 p-4 rounded-xl border border-indigo-200 ring-2 ring-indigo-50 ring-offset-2 hover:shadow-md transition-shadow"> <div class="flex flex-col"><span class="text-xs font-bold text-indigo-800 ml-2 flex items-center gap-1"><i class="ph ph-eye text-lg"></i> View Results</span><span class="text-[9px] text-indigo-400 ml-2 font-mono font-bold">Allow Participants to view results</span></div> <div class="flex bg-white rounded-lg p-1 shadow-sm border border-indigo-100"><button onclick="setVal('resultsVal', 'Allow')" class="progress-btn px-4 py-1.5 rounded-md text-[10px] font-bold" data-target="resultsVal" data-val="Allow">Allow</button><button onclick="setVal('resultsVal', 'Locked')" class="progress-btn px-4 py-1.5 rounded-md text-[10px] font-bold" data-target="resultsVal" data-val="Locked">Locked</button></div><input type="hidden" id="resultsVal"> </div> </div> </div> <div class="panel p-6 rounded-3xl"><h3 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-4 flex items-center gap-2 border-b border-gray-100 pb-3"><i class="ph ph-calendar-check text-indigo-500 text-lg"></i> Event Details</h3><div class="space-y-4"><div class="group"><label class="block text-[10px] font-bold text-gray-400 uppercase mb-1">Official Event Name</label><input type="text" id="eventName" class="input-light w-full rounded-xl p-3 text-sm font-bold text-gray-800 focus:ring-2 focus:ring-indigo-500" placeholder="e.g. KSEF Regional Fair 2026"></div><div class="grid grid-cols-2 gap-3"><div><label class="block text-[10px] font-bold text-gray-400 uppercase mb-1">Start Date</label><input type="date" id="startDate" class="input-light w-full rounded-xl p-2 text-xs font-medium text-gray-600"></div><div><label class="block text-[10px] font-bold text-gray-400 uppercase mb-1">End Date</label><input type="date" id="endDate" class="input-light w-full rounded-xl p-2 text-xs font-medium text-gray-600"></div></div><div><label class="block text-[10px] font-bold text-gray-400 uppercase mb-1">Broadcast Message</label><textarea id="broadcast" class="input-light w-full rounded-xl p-3 text-xs font-medium text-gray-600 resize-none border-l-4 border-l-yellow-400 bg-yellow-50/50" rows="3" placeholder="Message for judges..."></textarea></div></div></div> </div> <div class="lg:col-span-8 flex flex-col gap-6"> <div class="panel p-6 rounded-3xl relative h-[500px] flex flex-col shadow-lg"><div class="flex justify-between items-center mb-6 border-b border-gray-100 pb-4"><div><h3 class="text-sm font-bold text-indigo-900 uppercase tracking-widest flex items-center gap-2"><span class="p-2 bg-indigo-100 text-indigo-600 rounded-lg"><i class="ph ph-chart-bar text-xl"></i></span> Detailed Progress</h3><p class="text-[10px] text-gray-400 mt-1 ml-11">Breakdown by Judge & Category</p></div><button onclick="loadGraph()" id="refreshGraphBtn" class="text-[10px] bg-white text-indigo-600 px-4 py-2 rounded-xl font-bold flex items-center gap-2 border border-gray-200 shadow-sm hover:shadow-md hover:bg-gray-50 transition-all"><i class="ph ph-arrows-clockwise text-lg"></i> Refresh Data</button></div><div id="chartContainer" class="relative w-full overflow-y-auto custom-scrollbar flex-grow bg-gray-50/50 rounded-xl border border-dashed border-gray-200 p-2"><canvas id="progressChart"></canvas></div></div> <div class="panel p-6 rounded-3xl flex-grow flex flex-col"><div class="flex justify-between items-center mb-4"><h3 class="text-xs font-bold text-gray-400 uppercase tracking-widest flex items-center gap-2"><i class="ph ph-list-checks text-indigo-500 text-lg"></i> Active Categories</h3><div class="bg-gray-100 p-1 rounded-xl flex shadow-inner"><button onclick="switchTab('A')" id="tabA" class="px-5 py-2 text-xs font-bold rounded-lg bg-white text-indigo-600 shadow-sm transition-all duration-200">Part A</button><button onclick="switchTab('BC')" id="tabBC" class="px-5 py-2 text-xs font-bold rounded-lg text-gray-500 hover:text-gray-700 transition-all duration-200">Part BC</button></div></div><div id="catGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 overflow-y-auto max-h-48 content-start p-1"></div></div> <div class="flex justify-end pt-2"><button onclick="saveConfig()" id="saveBtn" class="bg-indigo-600 text-white text-sm font-bold py-4 px-10 rounded-2xl shadow-xl shadow-indigo-200 hover:bg-indigo-700 hover:scale-[1.02] transition-all flex items-center gap-3"><i class="ph ph-floppy-disk text-xl"></i> Save Configuration</button></div> </div> </div> <div id="view-judges" class="view-section hidden grid grid-cols-1 lg:grid-cols-12 gap-6"> <div class="lg:col-span-4 panel p-6 rounded-3xl h-fit sticky top-6"> <div class="bg-indigo-50 p-4 rounded-2xl mb-6 border border-indigo-100 shadow-inner"><h3 class="text-sm font-bold text-indigo-800 uppercase tracking-widest flex items-center gap-2"><i class="ph ph-user-plus text-xl"></i> Register Judge</h3><p class="text-[10px] text-indigo-400 mt-1">Credentials generated automatically.</p></div> <div class="space-y-5"> <div class="group"><label class="block text-[10px] font-bold text-gray-400 uppercase mb-1 ml-1">Full Name</label><div class="relative"><div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400"><i class="ph ph-identification-card"></i></div><input type="text" id="jName" oninput="autoGenCreds()" class="input-light w-full pl-10 rounded-xl p-3 text-sm font-bold" placeholder="e.g. Dr. Jane Doe"></div></div> <div class="grid grid-cols-2 gap-3"><div><label class="block text-[10px] font-bold text-gray-400 uppercase mb-1 ml-1">Username</label><input id="jUser" readonly class="bg-gray-50 border border-gray-200 w-full rounded-xl p-2.5 text-xs font-mono text-gray-600 text-center select-all"></div><div><label class="block text-[10px] font-bold text-gray-400 uppercase mb-1 ml-1">Password</label><input id="jPass" readonly class="bg-gray-50 border border-gray-200 w-full rounded-xl p-2.5 text-xs font-mono text-gray-600 text-center select-all"></div></div> <div><label class="block text-[10px] font-bold text-gray-400 uppercase mb-1 ml-1">Category Assignment</label><div class="relative"><div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400"><i class="ph ph-tag"></i></div><select id="jCat" class="input-light w-full pl-10 rounded-xl p-3 text-sm font-medium bg-white cursor-pointer appearance-none"></select><div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-gray-400"><i class="ph ph-caret-down"></i></div></div></div> <button onclick="addJudge()" id="btnAddJudge" class="w-full bg-indigo-600 text-white font-bold py-3.5 rounded-xl shadow-lg shadow-indigo-200 hover:bg-indigo-700 transition-all flex items-center justify-center gap-2 mt-4 transform active:scale-95"><i class="ph ph-check-circle text-lg"></i> Create Account</button> <div class="border-t border-dashed border-gray-300 pt-5 mt-4"><button onclick="generateCredentialsPDF()" class="w-full border-2 border-gray-200 py-3 rounded-xl text-xs font-bold hover:bg-gray-50 hover:border-gray-300 text-gray-600 transition-all flex items-center justify-center gap-2 group"><i class="ph ph-file-pdf text-xl text-red-500 group-hover:scale-110 transition-transform"></i> Print Credentials PDF</button></div> </div> </div> <div class="lg:col-span-8 panel p-0 rounded-3xl h-[700px] overflow-hidden flex flex-col shadow-md"> <div class="p-5 border-b border-gray-200 bg-gray-50/80 backdrop-blur flex justify-between items-center"><h3 class="text-xs font-bold text-gray-600 uppercase tracking-widest ml-2 flex items-center gap-2"><i class="ph ph-users-three text-indigo-500 text-lg"></i> Registered Judges</h3><span class="text-[10px] bg-white border border-gray-200 px-3 py-1.5 rounded-full font-bold text-gray-500 shadow-sm" id="judgeCountBadge">0 Total</span></div> <div class="overflow-auto flex-grow custom-scrollbar bg-white"> <table class="w-full text-left min-w-[600px]"><thead class="bg-gray-50 text-[10px] uppercase font-bold text-gray-500 sticky top-0 z-10 shadow-sm"><tr><th class="p-4 border-b">Judge Name</th><th class="p-4 border-b">Assigned Category</th><th class="p-4 border-b">Credentials</th><th class="p-4 border-b text-center">Status</th><th class="p-4 border-b text-right">Actions</th></tr></thead><tbody id="judgeTableBody" class="text-xs divide-y divide-gray-100"></tbody></table> </div> </div> </div> <div id="view-analysis" class="view-section hidden flex flex-col gap-6 pb-32"> <div class="bg-white p-5 rounded-3xl shadow-sm border border-gray-200 flex flex-wrap gap-4 items-center justify-between sticky top-4 z-30"> <div class="flex flex-wrap gap-3 items-center w-full md:w-auto p-1"> <div class="relative group"><div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400"><i class="ph ph-funnel"></i></div><select id="analysisCat" class="input-light pl-9 rounded-xl p-2.5 text-xs font-bold min-w-[160px] cursor-pointer" onchange="toggleRankingTabs()"><option value="ALL">All Categories</option></select></div> <div class="relative group"><div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400"><i class="ph ph-graduation-cap"></i></div><select id="analysisLevel" class="input-light pl-9 rounded-xl p-2.5 text-xs font-bold min-w-[140px] cursor-pointer" onchange="toggleRankingTabs()"><option value="BOTH">Both Grades</option><option value="JS">Junior (JS)</option><option value="SS">Senior (SS)</option></select></div> <div class="relative group"><div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400"><i class="ph ph-map-pin"></i></div><select id="analysisRegion" class="input-light pl-9 rounded-xl p-2.5 text-xs font-bold min-w-[140px] cursor-pointer"><option value="ALL">All Regions</option></select></div> <div class="flex items-center gap-2 bg-gray-50 px-3 py-2 rounded-xl border border-gray-200 ml-2"><span class="text-[10px] font-bold text-gray-500 uppercase">Qualifiers</span><input type="number" id="qualifiersCount" class="input-light rounded-lg p-1 text-center text-xs font-bold w-12 border-gray-300" value="3"></div> </div> <div class="flex gap-3 w-full md:w-auto"> <button onclick="runAnalysis()" id="runAnalysisBtn" class="flex-grow md:flex-grow-0 bg-indigo-600 text-white px-6 py-2.5 rounded-xl text-xs font-bold shadow-lg shadow-indigo-200 hover:bg-indigo-700 hover:scale-[1.02] transition-all flex items-center justify-center gap-2"><i class="ph ph-lightning text-lg"></i> Run Analysis</button> <div class="h-8 w-px bg-gray-300 mx-1"></div> <button onclick="generatePDF()" class="flex-grow md:flex-grow-0 bg-white border border-gray-300 text-gray-700 px-5 py-2.5 rounded-xl text-xs font-bold hover:bg-gray-50 hover:border-gray-400 transition-all flex items-center justify-center gap-2"><i class="ph ph-file-pdf text-lg text-red-500"></i> Export PDF</button> <button onclick="generateExcel()" class="flex-grow md:flex-grow-0 bg-green-50 border border-green-200 text-green-700 px-5 py-2.5 rounded-xl text-xs font-bold hover:bg-green-100 hover:border-green-300 transition-all flex items-center justify-center gap-2 shadow-sm"><i class="ph ph-file-xls text-lg"></i> Print Excel</button> </div> </div> <div id="analysisProgress" class="hidden w-full bg-white p-6 rounded-3xl shadow-lg border border-indigo-100 z-50 animate-fade-in"><div class="flex justify-between text-xs font-bold text-gray-500 mb-2 uppercase tracking-wider"><span><i class="ph ph-cpu animate-spin mr-1"></i> Processing Data...</span><span id="progressText" class="text-indigo-600">0%</span></div><div class="w-full bg-gray-100 rounded-full h-3 overflow-hidden"><div id="progressBar" class="h-full bg-gradient-to-r from-indigo-500 to-purple-500 w-0 transition-all duration-300 ease-out"></div></div></div> <div class="bg-white rounded-3xl shadow-sm border border-gray-200 relative min-h-[500px]"> <div class="sticky-tab-nav flex flex-wrap gap-2 border-b border-gray-200 bg-gray-50/50 p-1 backdrop-blur-md"> <button onclick="showReport('project')" id="rep-project" class="report-tab px-8 py-3 text-xs font-bold bg-white shadow-sm rounded-lg border border-gray-200 whitespace-nowrap transition-all flex items-center gap-2 text-gray-500"> <i class="ph ph-list-numbers text-lg"></i> Project Ranking </button> <button onclick="showReport('school')" id="rep-school" class="report-tab px-8 py-3 text-xs font-bold text-gray-500 hover:text-gray-700 hover:bg-white whitespace-nowrap transition-all flex items-center gap-2"> <i class="ph ph-buildings text-lg"></i> School Ranking </button> <button onclick="showReport('level')" id="rep-level" class="report-tab px-8 py-3 text-xs font-bold text-gray-500 hover:text-gray-700 hover:bg-white whitespace-nowrap transition-all flex items-center gap-2 text-indigo-600 border-indigo-600"> <i class="ph ph-globe text-lg"></i> Region Ranking </button> </div> <div id="analysisResults" class="p-6 bg-gray-50/30 min-h-[400px]"> <div class="flex flex-col items-center justify-center h-64 text-gray-400 opacity-50"> <i class="ph ph-chart-polar text-7xl mb-4 text-gray-300"></i> <p class="text-sm font-bold uppercase tracking-widest">Ready to Analyze</p> <p class="text-[10px] mt-1">Select filters and click Run Analysis</p> </div> </div> <div class="bg-blue-50 border-t border-blue-100 p-2 text-center text-[10px] font-bold text-blue-600 flex items-center justify-center gap-2"> <i class="ph ph-info text-blue-500"></i> Tip: Click on any row in the "Project Ranking" table to open the <span class="underline decoration-dotted cursor-help" title="Allows Admin to edit scores">Moderation Console</span>. </div> </div> </div> <div id="modModal" class="hidden fixed inset-0 z-[150] bg-gray-900/60 backdrop-blur-sm flex items-end md:items-center justify-center p-0 md:p-4 transition-all duration-300"> <div class="bg-white rounded-t-2xl md:rounded-3xl shadow-2xl w-full max-w-5xl h-[95vh] md:h-auto md:max-h-[90vh] flex flex-col overflow-hidden animate-bounce-in transform scale-100 border border-gray-200"> <div class="bg-indigo-600 p-4 md:p-5 text-white flex justify-between items-center shadow-md z-10 shrink-0"> <div> <h3 class="font-bold text-lg flex items-center gap-2"> <i class="ph ph-pencil-simple-line text-indigo-200"></i> <span class="hidden md:inline">Moderation Console</span> <span class="md:hidden">Moderate</span> </h3> <p class="text-xs text-indigo-200 mt-1">Directly editing judge scores.</p> </div> <button onclick="closeModModal()" class="bg-white/10 hover:bg-white/20 p-2 rounded-full transition-all text-white"> <i class="ph ph-x text-xl"></i> </button> </div> <div class="p-3 bg-gray-50 border-b border-gray-200 flex items-center gap-3 shadow-inner shrink-0"> <span class="bg-indigo-100 text-indigo-700 px-3 py-1 rounded-lg text-xs font-bold border border-indigo-200 shadow-sm">PROJECT</span> <span class="font-bold text-sm text-gray-800 truncate" id="modProjInfo">Loading...</span> </div> <div id="modContent" class="flex-grow overflow-y-auto p-4 md:p-6 space-y-6 bg-gray-50/50 custom-scrollbar overscroll-contain"> </div> <div class="p-4 border-t bg-white shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)] z-10 shrink-0"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="col-span-1 md:col-span-2"> <label class="block text-[10px] font-bold text-gray-400 uppercase mb-1">Reason (Required)</label> <div class="relative"> <i class="ph ph-chat-text absolute left-3 top-3 text-gray-400"></i> <input type="text" id="modReason" class="w-full border border-gray-300 rounded-xl p-2.5 pl-10 text-base focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-gray-50 focus:bg-white transition-all" placeholder="e.g. Correction of typo..."> </div> </div> <div class="col-span-1 flex flex-col"> <label class="block text-[10px] font-bold text-gray-400 uppercase mb-1">Sign Here</label> <div class="border border-gray-300 h-20 md:h-16 rounded-xl flex-grow bg-white relative cursor-crosshair overflow-hidden hover:border-indigo-400 transition-colors"> <canvas id="modSigCanvas" class="w-full h-full touch-none"></canvas> <button onclick="clearModCanvas()" class="absolute bottom-1 right-1 text-[9px] text-red-500 font-bold bg-white border border-gray-200 px-2 py-0.5 rounded shadow-sm hover:bg-red-50">Clear</button> </div> </div> </div> <button onclick="submitModeration()" id="btnModSubmit" class="w-full mt-3 bg-gradient-to-r from-red-600 to-red-700 hover:from-red-500 hover:to-red-600 text-white py-3 rounded-xl font-bold shadow-lg shadow-red-200 transition-all flex items-center justify-center gap-2 transform active:scale-95"> <i class="ph ph-floppy-disk text-xl"></i> OVERWRITE SCORES </button> </div> </div> </div> <div id="printArea" class="hidden"></div> <div class="security-footer"> <i class="ph ph-shield-check text-green-500 text-lg"></i> <span>(C)2026 All rights reserved. Protected by MacAfee</span> </div> <script> // --- GLOBAL VARIABLES --- let API_URL = MANUAL_URL; let CONFIG = {}, JUDGES = [], ANALYSIS_DATA = null, COMPUTED_RESULTS = null; let ACTIVE_TAB = 'A', CAT_A = [], CAT_BC = [], ALL_CATS = []; let MODERATION_ENABLED = false, CHART_INSTANCE = null, MOD_CANVAS = null, CURRENT_MOD_PID = null; // PAGINATION STATE let PAGINATION_STATE = {}; // Stores current page for each category: { "Physics": 1, "Chemistry": 1 } const ITEMS_PER_PAGE = 50; // --- CORE UTILITIES --- function showLoader(msg) { document.getElementById('loaderText').innerText = msg; document.getElementById('globalLoader').classList.remove('hidden'); } function hideLoader() { document.getElementById('globalLoader').classList.add('hidden'); } function setPage(p) { document.querySelectorAll('.view-section').forEach(e => e.classList.add('hidden')); document.getElementById('view-'+p).classList.remove('hidden'); setActiveNav(p); } function setActiveNav(p) { ['config','judges','analysis'].forEach(id => { const btn = document.getElementById('nav-'+id); if(id===p) { btn.classList.remove('text-gray-500'); btn.classList.add('text-indigo-600', 'bg-white', 'shadow-sm'); } else { btn.classList.add('text-gray-500'); btn.classList.remove('text-indigo-600', 'bg-white', 'shadow-sm'); } }); } function logout() { if(confirm("Are you sure you want to securely log out?")) { document.getElementById('mainUI').classList.add('hidden'); document.getElementById('loginOverlay').classList.remove('hidden'); document.getElementById('adminPass').value = ''; JUDGES = []; CONFIG = {}; ANALYSIS_DATA = null; } } // --- AUTHENTICATION --- async function attemptLogin() { const pass = document.getElementById('adminPass').value; const btn = document.getElementById('btnLogin'); btn.innerHTML = `<i class="ph ph-spinner animate-spin"></i> Verifying...`; btn.disabled = true; try { const res = await fetch(API_URL, { method: 'POST', body: JSON.stringify({ action: "verifyAdmin", data: { password: pass } }) }).then(r => r.json()); if (res.success) { document.getElementById('loginOverlay').classList.add('hidden'); document.getElementById('mainUI').classList.remove('hidden'); showLoader("Initializing Admin System..."); await loadData(); hideLoader(); } else { alert("Incorrect Password"); } } catch (e) { alert("Connection Failed. Check URL permissions!"); } btn.innerHTML = `<i class="ph ph-key text-xl"></i> Unlock Portal`; btn.disabled = false; } // --- DATA INIT --- async function loadData() { try { const res = await fetch(API_URL + "?action=getAdminData").then(r => r.json()); CONFIG = res.config; JUDGES = res.judges; ALL_CATS = (res.allCategories || []).filter(c => c && c.toString().trim() !== ""); document.getElementById('eventName').value = CONFIG.eventName; if(CONFIG.startDate) document.getElementById('startDate').value = CONFIG.startDate.split('T')[0]; if(CONFIG.endDate) document.getElementById('endDate').value = CONFIG.endDate.split('T')[0]; document.getElementById('broadcast').value = CONFIG.announcement || ""; setToggleState('statusVal', CONFIG.status); setToggleState('uploadVal', CONFIG.projectUpload); setToggleState('progressVal', CONFIG.viewProgress); setToggleState('resultsVal', CONFIG.viewResults || "Locked"); CAT_A = CONFIG.activePartA || []; CAT_BC = CONFIG.activePartBC || []; MODERATION_ENABLED = (CONFIG.moderationStatus === "Available"); const catSel = document.getElementById('analysisCat'); const jCatSel = document.getElementById('jCat'); const html = '<option value="ALL">All Categories</option>' + ALL_CATS.map(c => `<option value="${c}">${c}</option>`).join(''); catSel.innerHTML = html; jCatSel.innerHTML = '<option value="">Select Category...</option>' + ALL_CATS.map(c => `<option value="${c}">${c}</option>`).join(''); renderCategories(); renderJudges(); loadGraph(); } catch(e) { console.error(e); alert("Failed to load data."); } } function setToggleState(id, val) { document.getElementById(id).value = val; document.querySelectorAll(`button[data-target="${id}"]`).forEach(b => { if(b.dataset.val === val) b.classList.add('active'); else b.classList.remove('active'); }); } function setVal(id, val) { setToggleState(id, val); } // --- GRAPH --- async function loadGraph() { const btn = document.getElementById('refreshGraphBtn'); btn.innerHTML = `<i class="ph ph-spinner animate-spin"></i> Loading...`; try { const stats = await fetch(API_URL + "?action=getGraphData").then(r => r.json()); if(!stats || stats.length === 0) { btn.innerHTML = "No Data"; return; } const ctx = document.getElementById('progressChart').getContext('2d'); if (CHART_INSTANCE) CHART_INSTANCE.destroy(); CHART_INSTANCE = new Chart(ctx, { type: 'bar', data: { labels: stats.map(s => `${s.name} (${s.category})`), datasets: [{ label: 'Completion %', data: stats.map(s => s.total > 0 ? (s.judged / s.total * 100) : 0), backgroundColor: stats.map(s => { const p = s.total > 0 ? (s.judged / s.total) : 0; return p >= 1 ? '#22c55e' : (p >= 0.5 ? '#3b82f6' : '#94a3b8'); }), borderRadius: 6, barPercentage: 0.7 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: (c) => { const s = stats[c.dataIndex]; return ` ${s.judged} / ${s.total} Projects (${c.raw.toFixed(1)}%)`; } } } }, scales: { x: { beginAtZero: true, max: 100, title: { display: true, text: 'Percentage Complete' } }, y: { grid: { display: false } } } } }); } catch(e) {} btn.innerHTML = `<i class="ph ph-arrows-clockwise"></i> Refresh Data`; } // --- JUDGE MANAGEMENT --- function autoGenCreds() { const n = document.getElementById('jName').value.trim(); if(!n) return; document.getElementById('jUser').value = n.split(' ')[0].toLowerCase() + Math.floor(Math.random()*100); document.getElementById('jPass').value = Math.random().toString(36).slice(-5).toUpperCase(); } async function addJudge() { const btn = document.getElementById('btnAddJudge'); const data = { name: document.getElementById('jName').value, username: document.getElementById('jUser').value, password: document.getElementById('jPass').value, category: document.getElementById('jCat').value }; if(!data.name || !data.category || data.category === "") { return alert("Please enter Full Name and select a Category."); } btn.innerHTML = `<i class="ph ph-spinner animate-spin"></i> Creating Account...`; btn.disabled = true; await fetch(API_URL, { method: 'POST', mode: 'no-cors', body: JSON.stringify({ action: "addJudge", data: data }) }); document.getElementById('jName').value=""; autoGenCreds(); await loadData(); btn.innerHTML = `<i class="ph ph-check-circle text-lg"></i> Account Created!`; setTimeout(() => { btn.innerHTML = `<i class="ph ph-check-circle text-lg"></i> Create Account`; btn.disabled = false; }, 2000); } function renderJudges() { const tb = document.getElementById('judgeTableBody'); tb.innerHTML = ""; document.getElementById('judgeCountBadge').innerText = `${JUDGES.length} Total`; JUDGES.forEach(j => { tb.innerHTML += `<tr class="border-b hover:bg-gray-50"><td class="p-4 font-bold text-gray-800">${j.name}</td><td class="text-gray-500">${j.category}</td><td class="font-mono text-xs bg-gray-100 p-2 rounded inline-block mt-2">${j.username} / ${j.password}</td><td class="text-center"><span class="px-2 py-1 rounded text-[10px] font-bold uppercase ${j.status==='Active'?'bg-green-100 text-green-700':'bg-red-100 text-red-700'}">${j.status}</span></td><td class="text-right p-4"><div class="flex items-center justify-end gap-2"><button onclick="toggleJudge('${j.username}',${j.status==='Active'})" class="text-indigo-600 font-bold text-xs hover:underline bg-indigo-50 px-2 py-1 rounded">DEACTIVATE</button><button onclick="deleteJudge('${j.username}')" class="text-red-500 hover:bg-red-100 p-2 rounded transition-colors" title="Delete"><i class="ph ph-trash text-lg"></i></button></div></td></tr>`; }); } async function toggleJudge(u, b) { await fetch(API_URL, { method: 'POST', mode: 'no-cors', body: JSON.stringify({ action: "toggleJudge", data: { username: u, block: b } }) }); loadData(); } async function deleteJudge(u) { if(!confirm("Permanently delete this judge? This cannot be undone.")) return; showLoader("Deleting..."); await fetch(API_URL, { method: 'POST', mode: 'no-cors', body: JSON.stringify({ action: "deleteJudge", data: { username: u } }) }); await loadData(); hideLoader(); } // --- PDF CREDENTIALS --- function generateCredentialsPDF() { const { jsPDF } = window.jspdf; const doc = new jsPDF(); const eventName = document.getElementById('eventName').value || "Science Fair"; const confText = doc.splitTextToSize("CONFIDENTIAL - PROPERTY OF KSEF 2026", 80); const cardW=90, cardH=60, startX=10, startY=10, gap=10; let x=startX, y=startY; const img = new Image(); img.src = LOGO_URL; img.crossOrigin = "Anonymous"; img.onload = function() { JUDGES.forEach((j, i) => { doc.setDrawColor(0); doc.setLineWidth(0.5); doc.rect(x, y, cardW, cardH); try { doc.addImage(img, 'JPEG', x+5, y+5, 10, 10); } catch(e){} doc.setFontSize(9); doc.setTextColor(0); doc.setFont("helvetica", "bold"); doc.text(eventName.substring(0, 25), x+20, y+10); doc.setFontSize(7); doc.setTextColor(255,0,0); doc.text(confText, x+20, y+14); doc.setTextColor(0); doc.setFontSize(11); doc.text(j.name, x+10, y+30); doc.setFontSize(9); doc.setFont("helvetica", "normal"); doc.text("Category: "+j.category, x+10, y+36); doc.setFillColor(240); doc.rect(x+10, y+40, cardW-20, 15, 'F'); doc.setFont("courier", "bold"); doc.text(`U: ${j.username}`, x+12, y+45); doc.text(`P: ${j.password}`, x+12, y+50); if(i%2===0) x+=cardW+gap; else { x=startX; y+=cardH+gap+5; } if(y>250) { doc.addPage(); x=startX; y=startY; } }); doc.save("Judge_Credentials.pdf"); }; } // --- CONFIG SAVE --- function switchTab(t) { ACTIVE_TAB = t; renderCategories(); const activeClass = "px-4 py-1.5 text-xs font-bold rounded-md bg-indigo-600 text-white shadow-sm"; const inactiveClass = "px-4 py-1.5 text-xs font-bold rounded-md text-gray-500 hover:bg-gray-100"; document.getElementById('tabA').className = t === 'A' ? activeClass : inactiveClass; document.getElementById('tabBC').className = t === 'BC' ? activeClass : inactiveClass; } function renderCategories() { const g = document.getElementById('catGrid'); const activeList = ACTIVE_TAB === 'A' ? CAT_A : CAT_BC; const html = ALL_CATS.map(c => { if (!c || c.trim() === "") return ""; const isChecked = activeList.some(x => x.toLowerCase() === c.toLowerCase()); const safeCat = c.replace(/'/g, "\\'"); return `<label class="flex items-center p-2 border rounded bg-white hover:bg-indigo-50 transition-colors cursor-pointer select-none"><input type="checkbox" ${isChecked ? 'checked' : ''} onchange="toggleCat('${safeCat}', this.checked)" class="mr-2 h-4 w-4 text-indigo-600 rounded focus:ring-indigo-500 border-gray-300"><span class="text-[10px] font-bold ${isChecked ? 'text-indigo-700' : 'text-gray-500'}">${c}</span></label>`; }).join(''); g.innerHTML = html; } function toggleCat(c, add) { const list = ACTIVE_TAB === 'A' ? CAT_A : CAT_BC; if (add) { if (!list.some(x => x.toLowerCase() === c.toLowerCase())) list.push(c); } else { const idx = list.findIndex(x => x.toLowerCase() === c.toLowerCase()); if (idx > -1) list.splice(idx, 1); } } async function saveConfig() { const btn = document.getElementById('saveBtn'); const originalContent = `<i class="ph ph-floppy-disk text-xl"></i> Save Configuration`; btn.innerHTML = `<i class="ph ph-spinner animate-spin text-xl"></i> Saving Changes...`; btn.disabled = true; try { const d = { eventName: document.getElementById('eventName').value, startDate: document.getElementById('startDate').value, endDate: document.getElementById('endDate').value, status: document.getElementById('statusVal').value, announcement: document.getElementById('broadcast').value, projectUpload: document.getElementById('uploadVal').value, viewProgress: document.getElementById('progressVal').value, viewResults: document.getElementById('resultsVal').value, activePartA: CAT_A.join('|'), activePartBC: CAT_BC.join('|') }; await fetch(API_URL, { method: 'POST', mode: 'no-cors', body: JSON.stringify({action: "updateConfig", data: d}) }); btn.innerHTML = `<i class="ph ph-check text-xl"></i> Configuration Saved!`; btn.classList.remove('bg-indigo-600'); btn.classList.add('bg-green-600'); setTimeout(() => { btn.innerHTML = originalContent; btn.disabled = false; btn.classList.remove('bg-green-600'); btn.classList.add('bg-indigo-600'); }, 2000); } catch (e) { console.error(e); btn.innerHTML = "Error Saving"; setTimeout(() => { btn.innerHTML = originalContent; btn.disabled = false; }, 2000); } } // --- ANALYSIS --- async function runAnalysis() { document.getElementById('analysisProgress').classList.remove('hidden'); document.getElementById('progressBar').style.width="20%"; const res = await fetch(API_URL + "?action=getAnalysisData").then(r=>r.json()); ANALYSIS_DATA = res; document.getElementById('progressBar').style.width="60%"; const lvl=document.getElementById('analysisLevel').value, cat=document.getElementById('analysisCat').value, reg=document.getElementById('analysisRegion').value; const regions = [...new Set(res.projects.map(p=>p.region))]; const rSel=document.getElementById('analysisRegion'); if(rSel.options.length===1) regions.forEach(r=>rSel.innerHTML+=`<option value="${r}">${r}</option>`); const cats={}; res.projects.forEach(p=>{ if((lvl!=='BOTH'&&p.grade!==lvl)||(cat!=='ALL'&&p.category!==cat)||(reg!=='ALL'&&p.region!==reg))return; if(!cats[p.category])cats[p.category]=[]; let names = p.std1; if(p.std2 && p.std2.trim() !== "") names += " and " + p.std2; cats[p.category].push({id:p.id,name:p.name,students:names,school:p.school,region:p.region,grade:p.grade,category:p.category,judges:{},total:0,points:0,rank:0}); }); const clean = (val) => { const match = String(val).match(/-?\d+(\.\d+)?/); return match ? parseFloat(match[0]) : 0; }; res.scores.forEach(s=>{ for(const c in cats){ const p=cats[c].find(x=>x.id===s.pid); if(p){ let sa=0,sb=0; s.qScores.forEach((v,i)=>{ const numVal = clean(v); if(i<res.questions.filter(q=>q.part.includes('A')&&!q.part.includes('ORAL')).length) sa+=numVal; else sb+=numVal; }); p.judges[s.judge]={scoreA:sa,scoreBC:sb,fullScores:s.qScores}; } } }); const schools={}, regs={}; for(const c in cats) { cats[c].forEach(p=>{ const j=Object.values(p.judges); if(j.length>0){ const avgA = j.reduce((a,b)=>a+b.scoreA,0)/j.length; const avgBC = j.reduce((a,b)=>a+b.scoreBC,0)/j.length; p.avgA = avgA; p.avgBC = avgBC; p.total = avgA + avgBC; } else { p.avgA = 0; p.avgBC = 0; p.total = 0; } }); cats[c].sort((a,b)=>b.total-a.total); cats[c].forEach((p,i)=>{ p.rank=i+1; p.points=cats[c].length-i; if(!schools[p.school])schools[p.school]={name:p.school,points:{},total:0}; schools[p.school].total+=p.points; schools[p.school].points[c]=(schools[p.school].points[c]||0)+p.points; if(!regs[p.region])regs[p.region]={name:p.region,points:{},total:0}; regs[p.region].total+=p.points; regs[p.region].points[c]=(regs[p.region].points[c]||0)+p.points; }); // Reset Pagination for this run PAGINATION_STATE[c] = 1; } COMPUTED_RESULTS={rankedCats:cats,schools:schools,regions:regs,allCats:Object.keys(cats).sort()}; document.getElementById('analysisProgress').classList.add('hidden'); document.getElementById('analysisResults').classList.remove('hidden'); showReport('project'); } function showReport(type) { const container = document.getElementById('analysisResults'); container.innerHTML = ""; document.querySelectorAll('.report-tab').forEach(t => { t.classList.remove('text-indigo-600', 'border-indigo-600'); t.classList.add('text-gray-500'); }); document.getElementById(`rep-${type}`).classList.add('text-indigo-600', 'border-indigo-600'); if(type==='project') renderProjectRankings(container); else if(type==='school') renderMatrix(container, COMPUTED_RESULTS.schools, "School"); else renderMatrix(container, COMPUTED_RESULTS.regions, "Region"); } // --- OPTIMIZED RENDERING (PAGINATION) --- function renderProjectRankings(container) { const fmt = (n) => { const num = parseFloat(n); if(isNaN(num)) return "-"; return num.toFixed(3); }; for (const [catName, projs] of Object.entries(COMPUTED_RESULTS.rankedCats)) { let maxJudges = 0; projs.forEach(p => maxJudges = Math.max(maxJudges, Object.keys(p.judges).length)); // PAGINATION LOGIC const page = PAGINATION_STATE[catName] || 1; const totalPages = Math.ceil(projs.length / ITEMS_PER_PAGE); const start = (page - 1) * ITEMS_PER_PAGE; const end = start + ITEMS_PER_PAGE; const pagedProjs = projs.slice(start, end); const wrapper = document.createElement('div'); wrapper.className = "mb-8 bg-white border border-gray-200 rounded-2xl overflow-hidden shadow-sm overflow-x-auto"; let jHeaders = ""; for(let i=1; i<=maxJudges; i++) { jHeaders += `<th class="text-center bg-indigo-50/40 text-indigo-900 border-l border-indigo-100 w-16 text-[9px] p-2 whitespace-normal break-words">J${i}<br>A</th><th class="text-center bg-indigo-50/40 text-indigo-900 w-16 text-[9px] p-2 whitespace-normal break-words">J${i}<br>BC</th>`; } // Build HTML String (Faster) const rowsHtml = pagedProjs.map(p => { let jCells = ""; const judges = Object.values(p.judges); for(let i=0; i<maxJudges; i++) { jCells += judges[i] ? `<td class="text-center text-gray-600 border-l border-gray-50 p-2 font-mono">${fmt(judges[i].scoreA)}</td><td class="text-center text-gray-600 p-2 font-mono">${fmt(judges[i].scoreBC)}</td>` : `<td class="text-center text-gray-300 border-l border-gray-50 p-2">-</td><td class="text-center text-gray-300 p-2">-</td>`; } const modBtn = MODERATION_ENABLED ? `<button onclick="openModeration('${p.id}')" class="text-gray-400 hover:text-indigo-600 transition-colors p-1"><i class="ph ph-pencil-simple text-lg"></i></button>` : `<i class="ph ph-lock text-gray-300"></i>`; return ` <tr class="${p.isQualifier ? 'bg-green-50' : 'hover:bg-gray-50'} transition-colors group"> <td class="p-3 text-center font-bold text-gray-500">${p.rank}</td> <td class="p-3 font-mono text-gray-400 break-words">${p.id}</td> <td class="p-3 font-medium text-gray-700 leading-snug break-words">${p.students}</td> <td class="p-3 font-medium text-gray-800 leading-snug break-words">${p.name}</td> <td class="p-3 text-gray-500 leading-snug break-words">${p.school}</td> <td class="p-3 text-center">${p.grade}</td> <td class="p-3 text-gray-500 leading-snug break-words">${p.region}</td> ${jCells} <td class="p-3 text-center font-bold text-gray-700 bg-gray-50/50 border-l font-mono">${fmt(p.avgA)}</td> <td class="p-3 text-center font-bold text-gray-700 bg-gray-50/50 font-mono">${fmt(p.avgBC)}</td> <td class="p-3 text-right font-bold text-indigo-600 bg-indigo-50/30 text-sm font-mono">${fmt(p.total)}</td> <td class="p-3 text-center">${modBtn}</td> </tr>`; }).join(''); wrapper.innerHTML = ` <div class="bg-gray-50 p-3 font-bold text-sm border-b flex justify-between sticky left-0 items-center"> <span>${catName} <span class="text-xs font-normal text-gray-500 ml-2">(${projs.length} Total)</span></span> <div class="flex items-center gap-2"> <span class="text-xs text-gray-400 mr-2">Page ${page} of ${totalPages}</span> <button onclick="changePage('${catName}', -1)" class="px-2 py-1 bg-white border rounded hover:bg-gray-100 disabled:opacity-50" ${page===1?'disabled':''}>Prev</button> <button onclick="changePage('${catName}', 1)" class="px-2 py-1 bg-white border rounded hover:bg-gray-100 disabled:opacity-50" ${page===totalPages?'disabled':''}>Next</button> </div> </div> <table class="w-full text-left border-collapse"><thead><tr class="bg-gray-50 text-xs text-gray-500 font-bold border-b border-gray-200 uppercase tracking-wider"><th class="p-3 w-12 text-center whitespace-normal">#</th><th class="p-3 w-20 whitespace-normal">ID</th><th class="p-3 w-48 whitespace-normal">Student Names</th><th class="p-3 w-64 whitespace-normal">Project Title</th><th class="p-3 w-32 whitespace-normal">School</th><th class="p-3 w-16 text-center whitespace-normal">Lvl</th><th class="p-3 w-32 whitespace-normal">FROM</th>${jHeaders}<th class="p-3 w-20 text-center bg-gray-100 border-l whitespace-normal">Avg A</th><th class="p-3 w-20 text-center bg-gray-100 whitespace-normal">Avg BC</th><th class="p-3 w-24 text-right bg-indigo-600 text-white whitespace-normal">TOTAL</th><th class="p-3 w-20 text-center whitespace-normal">Action</th></tr></thead> <tbody class="divide-y divide-gray-50 text-xs">${rowsHtml}</tbody></table>`; container.appendChild(wrapper); } } function changePage(catName, direction) { const projs = COMPUTED_RESULTS.rankedCats[catName]; if(!projs) return; const totalPages = Math.ceil(projs.length / ITEMS_PER_PAGE); const currentPage = PAGINATION_STATE[catName] || 1; const newPage = currentPage + direction; if(newPage > 0 && newPage <= totalPages) { PAGINATION_STATE[catName] = newPage; showReport('project'); // Re-render } } function renderMatrix(container, dataMap, label) { const table = document.createElement('table'); table.className = "w-full analysis-table border shadow-sm rounded-lg overflow-hidden"; let thead = `<thead class="bg-gray-50"><tr><th class="w-16 text-center">Rank</th><th class="text-left py-4 pl-4">${label} Name</th>`; COMPUTED_RESULTS.allCats.forEach(c => thead += `<th class="text-center w-12 text-[9px]" title="${c}">${c.substring(0,3)}</th>`); thead += `<th class="text-right bg-indigo-600 text-white font-bold px-4">TOTAL PTS</th></tr></thead>`; let tbody = "<tbody class='divide-y divide-gray-100'>"; Object.values(dataMap).sort((a,b)=>b.total-a.total).forEach((item, idx) => { let row = `<tr class="hover:bg-gray-50"><td class="font-bold text-center text-gray-400">#${idx+1}</td><td class="font-bold text-gray-800 pl-4 py-3">${item.name}</td>`; COMPUTED_RESULTS.allCats.forEach(c => { const pts = item.points[c]||0; row += `<td class="text-center ${pts>0 ? 'font-bold text-indigo-700 bg-indigo-50/30' : 'text-gray-200'}">${pts||'-'}</td>`; }); tbody += row + `<td class="text-right font-bold text-indigo-700 bg-indigo-50 px-4">${item.total}</td></tr>`; }); table.innerHTML = thead + tbody + "</tbody>"; container.appendChild(table); } // --- OPTIMIZED PDF (DATA DRIVEN, NOT DOM DRIVEN) --- function generatePDF() { if(!COMPUTED_RESULTS) return alert("Run Analysis First"); const { jsPDF } = window.jspdf; const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' }); const eventName = document.getElementById('eventName').value || "KSEF Results"; const img = new Image(); img.src = LOGO_URL; img.crossOrigin = "Anonymous"; img.onload = function() { printPdf(doc, img, eventName); }; img.onerror = function() { printPdf(doc, null, eventName); }; } function printPdf(doc, img, eventName) { const drawHeader = () => { if(img) { try { doc.addImage(img, 'JPEG', 14, 10, 15, 15); } catch(e){} } doc.setFontSize(16); doc.setTextColor(0, 0, 0); doc.text(eventName, 35, 20); doc.setFontSize(10); doc.setTextColor(100, 100, 100); doc.text(`Analysis Report | Generated: ${new Date().toLocaleString()}`, 35, 26); return 35; }; let finalY = drawHeader(); // Loop through DATA Object.entries(COMPUTED_RESULTS.rankedCats).forEach(([catName, projs]) => { if (finalY > 180) { doc.addPage(); finalY = drawHeader(); } doc.setFontSize(12); doc.setTextColor(79, 70, 229); doc.setFont(undefined, 'bold'); doc.text(catName, 14, finalY); doc.setFont(undefined, 'normal'); finalY += 5; // 1. UPDATED DATA MAPPING (Added Grade and Region) const bodyData = projs.map(p => [ p.rank, p.id, p.students, p.name, p.school, p.grade || "", // NEW: JS or SS p.region || "", // NEW: Region p.avgA.toFixed(2), p.avgBC.toFixed(2), p.total.toFixed(3) ]); doc.autoTable({ startY: finalY, theme: 'grid', // 2. UPDATED HEADER ROW head: [['#', 'ID', 'Students', 'Title', 'School', 'JS/SS', 'From', 'Avg A', 'Avg BC', 'Total']], body: bodyData, styles: { fontSize: 8, cellPadding: 1.5, overflow: 'linebreak' }, headStyles: { fillColor: [79, 70, 229], textColor: 255, halign: 'center' }, // 3. UPDATED COLUMN WIDTHS (Adjusted to fit 2 new columns) columnStyles: { 0: {cellWidth: 8}, // # Rank 1: {cellWidth: 12}, // ID 2: {cellWidth: 40}, // Students (Reduced from 50) 3: {cellWidth: 45}, // Title (Reduced from 60) 4: {cellWidth: 30}, // School (Reduced from 40) 5: {cellWidth: 12}, // NEW: Grade 6: {cellWidth: 20} // NEW: Region // Remaining space is auto-calculated for scores } }); finalY = doc.lastAutoTable.finalY + 10; }); doc.save('KSEF_Results.pdf'); } // --- EXCEL EXPORT --- function generateExcel() { if(!COMPUTED_RESULTS) return alert("Run Analysis First"); const wb = XLSX.utils.book_new(); const eventName = document.getElementById('eventName').value || "KSEF Results"; const limit = parseInt(document.getElementById('qualifiersCount').value) || 3; const pData = [[`EVENT: ${eventName}`], ["Category", "Rank", "ID", "Student Names", "Project Title", "School", "Grade", "Level", "Avg A", "Avg BC", "Total 3dp"]]; for (const [cat, projs] of Object.entries(COMPUTED_RESULTS.rankedCats)) { projs.forEach(p => { if (p.rank <= limit) pData.push([cat, p.rank, p.id, p.students, p.name, p.school, p.grade, p.region, p.avgA, p.avgBC, p.total]); }); } const ws1 = XLSX.utils.aoa_to_sheet(pData); XLSX.utils.book_append_sheet(wb, ws1, "Top Qualifiers"); const sData = [[`EVENT: ${eventName}`], ["Rank", "School", "Total Points"]]; Object.values(COMPUTED_RESULTS.schools).sort((a,b)=>b.total-a.total).forEach((s,i) => sData.push([i+1, s.name, s.total])); const ws2 = XLSX.utils.aoa_to_sheet(sData); XLSX.utils.book_append_sheet(wb, ws2, "School Rank"); const rData = [[`EVENT: ${eventName}`], ["Rank", "Level/Region", "Total Points"]]; Object.values(COMPUTED_RESULTS.regions).sort((a,b)=>b.total-a.total).forEach((r,i) => rData.push([i+1, r.name, r.total])); const ws3 = XLSX.utils.aoa_to_sheet(rData); XLSX.utils.book_append_sheet(wb, ws3, "Level Rank"); XLSX.writeFile(wb, "KSEF_Official_Results.xlsx"); } // --- MODERATION LOGIC (FIXED FOR MOBILE) --- function openModeration(pid) { CURRENT_MOD_PID = pid; const p = Object.values(COMPUTED_RESULTS.rankedCats).flat().find(x => x.id === pid); if (!p) return; // 1. Setup Text Info document.getElementById('modProjInfo').innerText = `${p.id}: ${p.name}`; document.getElementById('modReason').value = ""; // 2. Show Modal document.getElementById('modModal').classList.remove('hidden'); // 3. Populate Inputs const container = document.getElementById('modContent'); container.innerHTML = ""; Object.entries(p.judges).forEach(([jName, jData]) => { let inputsHtml = ""; jData.fullScores.forEach((score, qIdx) => { const qData = (ANALYSIS_DATA.questions && ANALYSIS_DATA.questions[qIdx]); const qText = qData ? qData.text : `Question ${qIdx + 1}`; const qPart = qData ? qData.part : ""; const qType = (qData && qData.type) ? String(qData.type).toUpperCase().trim() : "SCALE"; const isFeedback = qText.toUpperCase().includes("FEEDBACK"); if (qType === 'TITLE') { inputsHtml += `<div class="bg-indigo-50 p-2 rounded-lg border border-indigo-100 mb-2 mt-4 text-center"><span class="text-xs font-bold text-indigo-800 uppercase tracking-wide">${qText}</span></div>`; } else if (qType === 'TEXT' || isFeedback) { inputsHtml += `<div class="bg-gray-50 p-3 rounded-lg border border-gray-100 mb-2 hover:bg-white transition-colors"><div class="text-[11px] text-gray-600 leading-relaxed font-medium mb-2"><span class="text-indigo-600 font-bold bg-indigo-50 px-1.5 py-0.5 rounded text-[9px] mr-1">${qPart}</span> ${qText}</div><textarea class="mod-input w-full border border-gray-300 rounded-lg p-2 text-xs font-medium bg-white focus:ring-2 focus:ring-indigo-500 shadow-sm" rows="3" data-judge="${jName}" data-qid="${qIdx}">${score || ""}</textarea></div>`; } else { inputsHtml += `<div class="flex items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-100 mb-2 hover:bg-white transition-colors"><div class="text-[11px] text-gray-600 w-3/4 pr-4 leading-relaxed font-medium"><span class="text-indigo-600 font-bold bg-indigo-50 px-1.5 py-0.5 rounded text-[9px] mr-1">${qPart}</span> ${qText}</div><input type="number" class="mod-input border border-gray-300 rounded-lg p-2 text-center text-sm font-bold w-20 bg-white focus:ring-2 focus:ring-indigo-500 shadow-sm" value="${score || 0}" data-judge="${jName}" data-qid="${qIdx}"></div>`; } }); container.innerHTML += `<div class="bg-white border border-gray-200 rounded-xl p-5 shadow-sm mb-4"><div class="flex justify-between items-center mb-4 border-b border-gray-100 pb-3"><h4 class="font-bold text-sm text-indigo-900 flex items-center gap-2"><i class="ph ph-user-circle text-lg text-indigo-500"></i> ${jName}</h4></div><div class="space-y-1">${inputsHtml}</div></div>`; }); // 4. Initialize Canvas (Wait 50ms for modal to animate open) setTimeout(initModCanvas, 50); } function initModCanvas() { const cvs = document.getElementById('modSigCanvas'); MOD_CANVAS = cvs; // Helper: Resize function for responsiveness const resizeCanvas = () => { const parent = cvs.parentElement; const rect = parent.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; // Set High-DPI dimensions cvs.width = rect.width * dpr; cvs.height = rect.height * dpr; // CSS Scale to fit container cvs.style.width = "100%"; cvs.style.height = "100%"; // Re-apply Context Settings const ctx = cvs.getContext('2d'); ctx.scale(dpr, dpr); ctx.lineWidth = 2; ctx.strokeStyle = "#000"; ctx.lineCap = "round"; ctx.lineJoin = "round"; }; // If listeners already attached, just resize and clear if (cvs.dataset.ready) { resizeCanvas(); return; } // --- First Time Setup --- cvs.dataset.ready = "true"; // Mark as ready resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Auto-resize on rotation const ctx = cvs.getContext('2d'); let isDrawing = false; const getPos = (e) => { const rect = cvs.getBoundingClientRect(); let clientX = e.clientX; let clientY = e.clientY; if (e.touches && e.touches.length > 0) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } return { x: clientX - rect.left, y: clientY - rect.top }; }; const start = (e) => { isDrawing = true; ctx.beginPath(); const p = getPos(e); ctx.moveTo(p.x, p.y); }; const move = (e) => { if (!isDrawing) return; const p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); }; const end = () => { isDrawing = false; ctx.closePath(); }; cvs.onmousedown = start; cvs.onmousemove = move; cvs.onmouseup = end; cvs.onmouseleave = end; cvs.addEventListener('touchstart', (e) => { e.preventDefault(); start(e); }, { passive: false }); cvs.addEventListener('touchmove', (e) => { e.preventDefault(); move(e); }, { passive: false }); cvs.addEventListener('touchend', (e) => { e.preventDefault(); end(e); }, { passive: false }); } function closeModModal() { document.getElementById('modModal').classList.add('hidden'); } function clearModCanvas() { const ctx = MOD_CANVAS.getContext('2d'); ctx.clearRect(0, 0, MOD_CANVAS.width, MOD_CANVAS.height); // Note: Uses raw width/height } async function submitModeration() { const reason = document.getElementById('modReason').value; if (!reason) return alert("Error: A reason for modification is required."); const blank = document.createElement('canvas'); blank.width = MOD_CANVAS.width; blank.height = MOD_CANVAS.height; if (MOD_CANVAS.toDataURL() === blank.toDataURL()) return alert("Error: Moderator signature is mandatory."); const btn = document.getElementById('btnModSubmit'); btn.innerHTML = `<i class="ph ph-spinner animate-spin"></i> Saving...`; btn.disabled = true; const inputs = document.querySelectorAll('.mod-input'); const judgeMap = {}; inputs.forEach(inp => { const j = inp.dataset.judge; if (!judgeMap[j]) judgeMap[j] = {}; judgeMap[j][inp.dataset.qid] = inp.value; }); const judgeUpdates = Object.entries(judgeMap).map(([j, s]) => ({ judgeName: j, scores: s })); await fetch(API_URL, { method: 'POST', mode: 'no-cors', body: JSON.stringify({ action: "moderateProject", data: { projectId: CURRENT_MOD_PID, reason: reason, signature: MOD_CANVAS.toDataURL(), judgeUpdates: judgeUpdates } }) }); closeModModal(); runAnalysis(); btn.innerHTML = `<i class="ph ph-floppy-disk text-xl"></i> OVERWRITE SCORES`; btn.disabled = false; } </script> </body> </html>