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 = `
${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 = `
`;
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');
}
}
});