document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('login-form'); const submitBtn = document.getElementById('submit-btn'); const btnText = submitBtn.querySelector('.btn-text'); const loader = submitBtn.querySelector('.loader'); const loginView = document.getElementById('login-view-container'); const dashboardView = document.getElementById('dashboard-view'); const errorMessage = document.getElementById('error-message'); const errorText = document.getElementById('error-text'); const logoutBtn = document.getElementById('logout-btn'); form.addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('username').value.trim(); const password = document.getElementById('password').value; if (!username || !password) return; setLoading(true); errorMessage.classList.add('hidden'); try { const formData = new FormData(); formData.append('username', username); formData.append('password', password); const response = await fetch('/api/login', { method: 'POST', body: formData }); const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data.detail || `Server error: ${response.status}`); } if (data.success && data['ASP.NET_SessionId']) { // Hide login, show dashboard loginView.classList.add('hidden'); dashboardView.classList.remove('hidden'); // Start fetch marks process fetchMarks(username, data['ASP.NET_SessionId']); } else { throw new Error("Login succeeded but no session cookie was returned."); } } catch (error) { console.error("Login error:", error); errorText.textContent = error.message || "Failed to login. Please try again."; errorMessage.classList.remove('hidden'); // Trigger shake animation again by cloning and replacing const newError = errorMessage.cloneNode(true); errorMessage.parentNode.replaceChild(newError, errorMessage); } finally { setLoading(false); } }); logoutBtn.addEventListener('click', () => { dashboardView.classList.add('hidden'); loginView.classList.remove('hidden'); form.reset(); document.getElementById('username').focus(); }); async function fetchMarks(username, sessionId) { const loading = document.getElementById('dashboard-loading'); const container = document.getElementById('cards-container'); const errorMsg = document.getElementById('dashboard-error'); const userDisplay = document.getElementById('user-display'); const circle = document.querySelector('.progress-ring__circle'); const percentageText = document.getElementById('progress-percentage'); const progressStatus = document.getElementById('progress-text'); const radius = circle.r.baseVal.value; const circumference = radius * 2 * Math.PI; circle.style.strokeDasharray = `${circumference} ${circumference}`; circle.style.strokeDashoffset = circumference; userDisplay.textContent = `${username}`; loading.classList.remove('hidden'); container.classList.add('hidden'); errorMsg.classList.add('hidden'); function setProgress(percent) { const offset = circumference - percent / 100 * circumference; circle.style.strokeDashoffset = offset; percentageText.textContent = Math.round(percent); } setProgress(0); progressStatus.textContent = 'Connecting to SIMATS...'; let progress = 0; const progressInterval = setInterval(() => { const remaining = 99 - progress; progress += (Math.random() * 0.1 + 0.05) * remaining; // Add 5-15% of remaining distance setProgress(progress); if (progress > 30 && progress < 60) { progressStatus.textContent = 'Fetching course enrollments...'; } else if (progress >= 60 && progress < 85) { progressStatus.textContent = 'Aggregating detailed mark splits...'; } else if (progress >= 85) { progressStatus.textContent = 'Finalizing data format...'; } }, 500); try { const formData = new FormData(); formData.append('username', username); formData.append('session_id', sessionId); const response = await fetch('/api/fetch-marks', { method: 'POST', body: formData }); const data = await response.json().catch(() => ({})); clearInterval(progressInterval); if (!response.ok) throw new Error(data.detail || 'Failed to fetch marks'); setProgress(100); progressStatus.textContent = 'Data loaded successfully!'; setTimeout(() => { loading.classList.add('hidden'); renderCards(data.data); container.classList.remove('hidden'); }, 800); } catch (e) { clearInterval(progressInterval); loading.classList.add('hidden'); errorMsg.textContent = e.message; errorMsg.classList.remove('hidden'); // Render basic fallback if UI needs resetting container.innerHTML = ''; } } function renderCards(data) { const container = document.getElementById('cards-container'); container.innerHTML = ''; if (!data || data.length === 0) { container.innerHTML = '
No course enrollments found for this period.
'; return; } let totalGradePoints = 0; let totalCredits = 0; let validCourses = 0; const gradeValues = { 'S': 10, 'A': 9, 'B': 8, 'C': 7, 'D': 6, 'E': 5 }; let gradeCounts = { 'S': 0, 'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0 }; data.forEach((item, index) => { const card = document.createElement('div'); card.className = 'course-card'; card.style.opacity = '0'; card.animate([ { opacity: 0, transform: 'translateY(30px) scale(0.95)' }, { opacity: 1, transform: 'translateY(0) scale(1)' } ], { duration: 500, easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)', fill: 'forwards', delay: index * 100 }); const c = item.course; const marks = item.marks; const error = item.error; const grade = c.FinalGrade ? c.FinalGrade.trim().toUpperCase() : ''; if (gradeValues[grade] !== undefined) { const credit = parseFloat(c.Credit || c.Credits || 1); totalGradePoints += gradeValues[grade] * credit; totalCredits += credit; validCourses++; if (gradeCounts[grade] !== undefined) { gradeCounts[grade]++; } } let marksHtml = ''; let totalObtained = 0; if (error) { marksHtml = `
${error}
`; } else if (marks && marks.length > 0) { const targetCategories = ["Class Test (IA)", "Research", "Class Practical", "University Theory", "University Practical"]; marks.forEach(m => { const category = m.RubricCategory ? m.RubricCategory.trim() : ""; if (targetCategories.includes(category)) { const val = parseFloat(m.OrginalConvertedMark); if (!isNaN(val)) { totalObtained += val; } } }); totalObtained = Math.round(totalObtained * 100) / 100; const list = marks.map(m => { const isPass = m.IsPassed; const statusClass = isPass ? 'mark-pass' : 'mark-fail'; const statusText = isPass ? 'PASS' : 'FAIL'; return `
  • ${m.RubricCategory} ${m.Type}
    ${m.OrginalConvertedMark} / ${m.RubricsMaxMark}
    ${statusText}
  • `; }).join(''); marksHtml = ``; } else { marksHtml = `
    No marks available for this course
    `; } card.innerHTML = `
    ${c.CourseCode || '-'}

    ${c.CourseName || 'Unknown Course'}

    ${c.MonthYearValue || '-'}
    ${marks && !error ? `Total: ${totalObtained}/500` : ''} Grade: ${c.FinalGrade || 'N/A'}
    ${marksHtml}
    `; container.appendChild(card); }); if (validCourses > 0 && totalCredits > 0) { const cgpa = (totalGradePoints / totalCredits).toFixed(2); const cgpaCard = document.createElement('div'); cgpaCard.className = 'course-card'; cgpaCard.style.opacity = '0'; cgpaCard.style.gridColumn = '1 / -1'; cgpaCard.style.background = 'var(--card-bg)'; cgpaCard.style.border = '2px solid rgba(212, 175, 55, 0.4)'; cgpaCard.style.textAlign = 'center'; cgpaCard.style.marginTop = '10px'; cgpaCard.animate([ { opacity: 0, transform: 'translateY(30px) scale(0.95)' }, { opacity: 1, transform: 'translateY(0) scale(1)' } ], { duration: 500, easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)', fill: 'forwards', delay: data.length * 100 }); cgpaCard.innerHTML = `

    Overall CGPA

    ${cgpa}

    Target CGPA Planner

    `; container.appendChild(cgpaCard); const updateCalc = () => { const target = parseFloat(document.getElementById('target-cgpa-input').value); const futureGp = parseFloat(document.getElementById('future-grade-select').value); const resultDiv = document.getElementById('calculator-result'); const curr = totalGradePoints / totalCredits; if (isNaN(target) || isNaN(futureGp)) return; if (curr >= target) { resultDiv.innerHTML = `You already have a ${target.toFixed(1)}+ CGPA!`; return; } if (futureGp <= target) { resultDiv.innerHTML = `Impossible to reach a ${target.toFixed(1)} CGPA by scoring ${futureGp} points or less.`; return; } const reqCredits = (target * totalCredits - totalGradePoints) / (futureGp - target); resultDiv.innerHTML = `You need ${Math.ceil(reqCredits)} more credits of this grade to reach a ${target.toFixed(1)} CGPA.`; }; document.getElementById('target-cgpa-input').addEventListener('input', updateCalc); document.getElementById('future-grade-select').addEventListener('change', updateCalc); updateCalc(); const totalGradesCount = Object.values(gradeCounts).reduce((a, b) => a + b, 0); if (totalGradesCount > 0) { const graphCard = document.createElement('div'); graphCard.className = 'course-card'; graphCard.style.opacity = '0'; graphCard.style.gridColumn = '1 / -1'; graphCard.style.background = 'var(--card-bg)'; graphCard.style.border = '1px solid rgba(212, 175, 55, 0.2)'; graphCard.style.padding = '30px'; graphCard.style.marginTop = '20px'; graphCard.animate([ { opacity: 0, transform: 'translateY(30px) scale(0.95)' }, { opacity: 1, transform: 'translateY(0) scale(1)' } ], { duration: 500, easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)', fill: 'forwards', delay: (data.length + 1) * 100 }); graphCard.innerHTML = `

    Grade Distribution

    `; container.appendChild(graphCard); const pieLabels = []; const pieData = []; const pieColors = []; const colorPalette = { 'S': 'rgba(212, 175, 55, 0.9)', // Gold 'A': 'rgba(52, 211, 153, 0.9)', // Emerald 'B': 'rgba(96, 165, 250, 0.9)', // Blue 'C': 'rgba(167, 139, 250, 0.9)', // Purple 'D': 'rgba(251, 146, 60, 0.9)', // Orange 'E': 'rgba(248, 113, 113, 0.9)' // Red }; for (const g in gradeCounts) { if (gradeCounts[g] > 0) { pieLabels.push(g); pieData.push(gradeCounts[g]); pieColors.push(colorPalette[g] || '#888'); } } const ctx = document.getElementById('performanceChart').getContext('2d'); new Chart(ctx, { type: 'pie', data: { labels: pieLabels, datasets: [{ data: pieData, backgroundColor: pieColors, borderColor: '#0a0a0a', borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#a1a1aa', font: { size: 14 } } } } } }); } } } function setLoading(isLoading) { submitBtn.disabled = isLoading; if (isLoading) { btnText.classList.add('hidden'); loader.classList.remove('hidden'); } else { btnText.classList.remove('hidden'); loader.classList.add('hidden'); } } });