Files
homeschool/public/js/admin.js
T
2025-08-13 11:25:12 -04:00

1050 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Ensure tab event listeners are attached after everything is defined
window.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
const tabButtons = document.querySelectorAll('#adminTabs button');
tabButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const tabId = button.getAttribute('data-tab');
if (tabId && typeof window.switchTab === 'function') {
window.switchTab(tabId);
}
});
});
}, 0);
});
// Global variables for admin functionality
// --- Weeks input logic ---
// Populate active week dropdown from weeksContainer
window.populateActiveWeekDropdown = function() {
const select = document.getElementById('activeWeekSelect');
select.innerHTML = '';
document.querySelectorAll('#weeksContainer .week-input-card').forEach(card => {
const date = card.querySelector('.week-date').value;
if (date) {
const option = document.createElement('option');
option.value = date;
option.textContent = date;
select.appendChild(option);
}
});
};
// Call populateActiveWeekDropdown whenever weeks change
const observer = new MutationObserver(() => populateActiveWeekDropdown());
observer.observe(document.getElementById('weeksContainer'), { childList: true, subtree: true });
window.setActiveWeek = async function() {
const selectedUser = document.getElementById('wordUserSelect')?.value;
const activeWeek = document.getElementById('activeWeekSelect')?.value;
if (!selectedUser || !activeWeek) {
alert('⚠️ Please select a user and week.');
return;
}
await fetch('/setActiveWeek', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: selectedUser, activeWeek })
});
alert(`✅ Active week set for ${selectedUser}: ${activeWeek}`);
}
window.addWeekInput = function() {
const container = document.getElementById('weeksContainer');
const weekIdx = container.children.length;
const weekDiv = document.createElement('div');
weekDiv.className = 'week-input-card';
weekDiv.style = 'border:1px solid #ccc; padding:1rem; margin-bottom:1rem;';
weekDiv.innerHTML = `
<label>Date: <input type="date" class="week-date" /></label><br/>
<textarea class="week-words" placeholder="Enter one word per line..."></textarea><br/>
<button type="button" onclick="this.parentElement.remove()" class="btn-error" style="margin-top:0.5rem;">Remove Week</button>
`;
container.appendChild(weekDiv);
}
window.saveWeeks = async function() {
const selectedUser = document.getElementById('wordUserSelect')?.value;
if (!selectedUser) {
alert('⚠️ Please select a user before saving.');
return;
}
const weeks = [];
document.querySelectorAll('#weeksContainer .week-input-card').forEach(card => {
const date = card.querySelector('.week-date').value;
const words = card.querySelector('.week-words').value.split('\n').map(w => w.trim()).filter(Boolean);
if (date && words.length) {
weeks.push({ date, words });
}
});
if (!weeks.length) {
alert('⚠️ Please enter at least one week with a date and words.');
return;
}
await fetch('/saveWeeksWordList', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: selectedUser, weeks })
});
alert(`✅ Weeks saved for ${selectedUser}`);
}
window.clearWeeks = function() {
document.getElementById('weeksContainer').innerHTML = '';
}
let allWords = [];
let adminUsers = [];
// 🧭 Admin tab switcher (make it globally accessible)
window.switchTab = function switchTab(targetId) {
console.log('🔄 Switching to tab:', targetId);
// Hide all tabs
document.querySelectorAll('.adminTab').forEach(tab => {
tab.classList.add('hidden');
});
// Remove active class from all buttons
document.querySelectorAll('#adminTabs button').forEach(btn => {
btn.classList.remove('active');
});
// Show the selected tab
const targetTab = document.getElementById(targetId);
if (targetTab) {
targetTab.classList.remove('hidden');
console.log('✅ Showing tab:', targetId);
} else {
console.error('❌ Tab not found:', targetId);
}
// Highlight the active button
document.querySelectorAll('#adminTabs button').forEach(btn => {
if (btn.getAttribute('data-tab') === targetId) {
btn.classList.add('active');
}
});
// 🔁 Trigger tab-specific logic
switch(targetId) {
case 'tabSessions':
if (typeof populateSessionUserDropdown === 'function') {
populateSessionUserDropdown();
}
const sessionList = document.getElementById("userSessionList");
if (sessionList) sessionList.innerHTML = "";
break;
case 'tabReports':
if (typeof loadStudentNamesForReport === 'function') {
loadStudentNamesForReport();
}
break;
case 'tabPasswords':
populatePasswordUserDropdown();
break;
case 'tabBadges':
if (typeof loadBadgeViewer === 'function') {
loadBadgeViewer();
}
break;
case 'tabAnalytics':
if (typeof refreshAnalytics === 'function') {
refreshAnalytics();
}
break;
case 'tabResults':
if (typeof loadStudentResults === 'function') {
loadStudentResults();
}
break;
case 'tabWords':
loadUserDropdowns();
break;
}
// Attach admin tab event listeners after switchTab is defined
window.addEventListener('DOMContentLoaded', () => {
const tabButtons = document.querySelectorAll('#adminTabs button');
tabButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const tabId = button.getAttribute('data-tab');
if (tabId) {
window.switchTab(tabId);
}
});
});
});
};
// Load users from server
async function loadUsers() {
try {
const res = await fetch('/getUsers');
adminUsers = await res.json();
} catch (err) {
console.error("Failed to load users:", err);
adminUsers = [];
}
}
// ➕ Add a new user
async function addNewUser() {
const username = document.getElementById("newUsername").value.trim();
const password = document.getElementById("newPassword").value;
const role = document.getElementById("newRole").value;
const msg = document.getElementById("userAddMessage");
const hashPreview = document.getElementById("hashPreview");
if (!username || !password) {
msg.textContent = "Please fill out all fields.";
return;
}
const hash = hashPassword(password);
if (hashPreview) hashPreview.textContent = hash;
try {
await fetch('/addUser', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, hash, role })
});
msg.textContent = `✅ User "${username}" added to server.`;
document.getElementById("newUsername").value = '';
document.getElementById("newPassword").value = '';
if (hashPreview) hashPreview.textContent = '[auto]';
await loadUsers();
displayUserDropdown();
populateWordUserDropdown();
} catch (e) {
msg.textContent = "❌ Failed to add user.";
console.error("Add user error:", e);
}
}
// 🗑 Populate and delete users
function displayUserDropdown() {
const dropdown = document.getElementById("userDropdown");
dropdown.innerHTML = `<option value="">-- Select a user --</option>`;
adminUsers.forEach(user => {
dropdown.innerHTML += `<option value="${user.username}">${user.username} (${user.role})</option>`;
});
}
function selectUserAction(username) {
if (!username) return;
if (!confirm(`Are you sure you want to delete user "${username}"?`)) {
document.getElementById("userDropdown").value = "";
return;
}
fetch("/deleteUser", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username })
})
.then(res => res.text())
.then(msg => {
alert(msg);
loadUsers().then(displayUserDropdown);
});
}
// 📚 Save or clear word list for current user
async function saveWords() {
const selectedUser = document.getElementById('wordUserSelect')?.value;
if (!selectedUser) {
alert("⚠️ Please select a user before saving.");
return;
}
const text = document.getElementById('wordInput').value.trim();
allWords = text.split('\n').map(w => w.trim()).filter(Boolean);
await fetch('/saveWordList', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: selectedUser, words: allWords })
});
displayWordList();
alert(`✅ Word list saved to server for ${selectedUser}`);
}
async function clearWords() {
const selectedUser = document.getElementById('wordUserSelect')?.value;
if (!selectedUser) return;
allWords = [];
await fetch('/saveWordList', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: selectedUser, words: [] })
});
displayWordList();
alert(`🗑 Word list cleared for ${selectedUser}`);
}
// 🗂 Load and display word lists
async function loadWordsForSelectedUser() {
const selectedUser = document.getElementById("wordUserSelect").value;
if (!selectedUser) return;
// Fetch the user's word list object (could be array or {weeks, activeWeek})
const res = await fetch(`/getWordList?username=${encodeURIComponent(selectedUser)}`);
const data = await res.json();
allWords = data.words || [];
// Now also fetch the raw wordlists object to get weeks/activeWeek
const wordlistsRes = await fetch('/getWordlistsRaw');
const wordlists = await wordlistsRes.json();
const userList = wordlists[selectedUser];
// If userList is an object with weeks, populate week cards and dropdown
if (userList && typeof userList === 'object' && Array.isArray(userList.weeks)) {
// Clear current week cards
document.getElementById('weeksContainer').innerHTML = '';
userList.weeks.forEach(week => {
window.addWeekInput();
const cards = document.querySelectorAll('#weeksContainer .week-input-card');
const lastCard = cards[cards.length - 1];
if (lastCard) {
lastCard.querySelector('.week-date').value = week.date;
lastCard.querySelector('.week-words').value = (week.words || []).join('\n');
}
});
// Repopulate the active week dropdown
window.populateActiveWeekDropdown();
// Set the dropdown to the saved activeWeek if present
if (userList.activeWeek) {
const select = document.getElementById('activeWeekSelect');
select.value = userList.activeWeek;
}
}
displayWordList();
}
function displayWordList() {
const ul = document.getElementById('wordListDisplay');
ul.innerHTML = allWords.map(word => `<li>${word}</li>`).join('');
}
function populateWordUserDropdown() {
const selects = document.querySelectorAll(".word-user-dropdown");
selects.forEach(select => {
select.innerHTML = '<option value="">-- Select a user --</option>';
adminUsers.forEach(user => {
const option = document.createElement("option");
option.value = user.username;
option.textContent = user.username;
select.appendChild(option);
});
});
}
async function displayWordListForSelectedUser() {
const select = document.getElementById("viewUserSelect");
const username = select.value;
const list = document.getElementById("userWordListDisplay");
list.innerHTML = '';
if (!username) return;
try {
const res = await fetch(`/getWordList?username=${username}`);
const data = await res.json();
const words = data.words || [];
if (!words.length) {
list.innerHTML = "<li><em>No words found for this user.</em></li>";
return;
}
words.forEach(word => {
const li = document.createElement("li");
li.textContent = word;
list.appendChild(li);
});
} catch (err) {
console.error("Failed to load word list for user:", username, err);
list.innerHTML = `<li style="color:red">❌ Error loading words</li>`;
}
}
// 📊 Student result viewer
window.loadStudentResults = async function() {
const list = document.getElementById("resultsList");
list.innerHTML = '';
try {
const res = await fetch('/getResults');
const results = await res.json();
if (!Object.keys(results).length) {
list.innerHTML = '<li><em>No results available yet.</em></li>';
return;
}
for (const [username, userResults] of Object.entries(results)) {
// Handle array of results per user
if (Array.isArray(userResults) && userResults.length > 0) {
userResults.forEach((result, index) => {
const li = document.createElement("li");
let answersHTML = '';
// Check if result and answers exist and are valid
if (result && result.answers && Array.isArray(result.answers)) {
result.answers.forEach(entry => {
const status = entry.correct ? '✅' : '❌';
answersHTML += `${status} ${entry.word}<br>`;
});
} else {
answersHTML = '<em>No answer data available</em>';
}
const timestamp = result.timestamp ? new Date(result.timestamp).toLocaleString() : 'Unknown time';
// Label result type
let typeLabel = '';
if (result.type === 'typing') {
typeLabel = '<span style="color: #007bff; font-weight: bold;">[Typing Practice]</span> ';
} else {
typeLabel = '<span style="color: #28a745; font-weight: bold;">[Game]</span> ';
}
li.innerHTML = `
${typeLabel}<strong>${username}</strong> (Session ${index + 1}) — Score: ${result?.score || 'N/A'}, Completed: ${result?.completed ? '✅' : '❌'}<br>
<em>Date:</em> ${timestamp}<br>
<em>Answers:</em><br>${answersHTML}
<hr style="margin: 1rem 0; opacity: 0.3;">
`;
list.appendChild(li);
});
} else {
// Fallback for single result format
const li = document.createElement("li");
li.innerHTML = `<strong>${username}</strong> — No valid results found`;
list.appendChild(li);
}
}
} catch (err) {
console.error("🔴 Failed to load results:", err);
list.innerHTML = '<li style="color:red">Error loading results.</li>';
}
}
async function loadUserDropdowns() {
try {
const res = await fetch('/getUsers');
const users = await res.json();
const dropdowns = [
document.getElementById('userDropdown'),
document.getElementById('wordUserSelect'),
document.getElementById('viewUserSelect')
];
dropdowns.forEach(drop => {
if (!drop) return;
drop.innerHTML = '<option value="">-- Select a user --</option>';
users.forEach(u => {
const opt = document.createElement('option');
opt.value = u.username;
opt.textContent = u.username;
drop.appendChild(opt);
});
});
console.log('✅ User dropdowns loaded');
} catch (err) {
console.error("⚠️ Failed to load user list:", err);
}
}
async function refreshAnalytics() {
const summary = document.getElementById("analyticsSummary");
const breakdown = document.getElementById("studentBreakdown");
summary.innerHTML = "<li>Loading...</li>";
breakdown.innerHTML = "";
try {
const res = await fetch('/getResults');
const data = await res.json();
let totalScore = 0, sessionCount = 0;
const missMap = {};
for (const username in data) {
const { score, completed, answers } = data[username];
if (!completed || !Array.isArray(answers)) continue;
totalScore += score;
sessionCount++;
// Count missed words
answers.forEach(({ word, correct }) => {
if (!correct) {
missMap[word] = (missMap[word] || 0) + 1;
}
});
// Per-student breakdown
const li = document.createElement("li");
li.innerHTML = `<strong>${username}</strong>: ${score}/${answers.length} correct`;
breakdown.appendChild(li);
}
const avgScore = sessionCount ? (totalScore / sessionCount).toFixed(2) : "0";
let mostMissed = "-", highestMisses = 0;
for (const word in missMap) {
if (missMap[word] > highestMisses) {
highestMisses = missMap[word];
mostMissed = word;
}
}
summary.innerHTML = `
<li><strong>Average Score:</strong> ${avgScore}</li>
<li><strong>Total Completed Sessions:</strong> ${sessionCount}</li>
<li><strong>Most Missed Word:</strong> ${mostMissed} (${highestMisses} misses)</li>
`;
} catch (err) {
console.error("📉 Failed to refresh analytics:", err);
summary.innerHTML = '<li style="color:red">Error loading analytics.</li>';
}
}
async function loadBadgeViewer() {
const res = await fetch('/getBadges');
const badgeData = await res.json();
const userList = document.getElementById("badgeUserList");
userList.innerHTML = "";
// Keep track of badge statistics
let totalBadges = 0;
let mostBadgesStudent = { name: '-', count: 0 };
let badgeCounts = {};
let badgesThisWeek = 0;
let currentWeekStart = new Date();
currentWeekStart.setDate(currentWeekStart.getDate() - 7);
// Process badge data
for (const username in badgeData) {
// Create student list items
const li = document.createElement("li");
li.innerHTML = `<button onclick="showUserBadges('${username}')">${username}</button>`;
userList.appendChild(li);
// Calculate badge statistics
let userBadgeCount = 0;
if (Array.isArray(badgeData[username])) {
// Old format
userBadgeCount = badgeData[username].length;
// Track badge types
badgeData[username].forEach(badge => {
if (!badgeCounts[badge]) badgeCounts[badge] = 0;
badgeCounts[badge]++;
});
} else if (badgeData[username] && badgeData[username].earned) {
// New format
userBadgeCount = badgeData[username].earned.length;
// Track badge types
badgeData[username].earned.forEach(badge => {
// Count total badges
totalBadges++;
// Count by badge name
const badgeName = badge.name;
if (!badgeCounts[badgeName]) badgeCounts[badgeName] = 0;
badgeCounts[badgeName]++;
// Count recent badges
if (badge.earnedAt) {
const earnedDate = new Date(badge.earnedAt);
if (earnedDate > currentWeekStart) {
badgesThisWeek++;
}
}
});
}
// Track user with most badges
if (userBadgeCount > mostBadgesStudent.count) {
mostBadgesStudent = { name: username, count: userBadgeCount };
}
}
// Find most common badge
let mostCommonBadge = { name: '-', count: 0 };
for (const badge in badgeCounts) {
if (badgeCounts[badge] > mostCommonBadge.count) {
mostCommonBadge = { name: badge, count: badgeCounts[badge] };
}
}
// Update badge statistics display
document.getElementById('totalBadgesAwarded').textContent = totalBadges;
document.getElementById('mostBadgesStudent').textContent = mostBadgesStudent.name !== '-' ?
`${mostBadgesStudent.name} (${mostBadgesStudent.count})` : '-';
document.getElementById('mostCommonBadge').textContent = mostCommonBadge.name !== '-' ?
`${mostCommonBadge.name} (${mostCommonBadge.count})` : '-';
document.getElementById('badgesThisWeek').textContent = badgesThisWeek;
// Add search functionality
const searchInput = document.getElementById('badgeStudentSearch');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
document.querySelectorAll('#badgeUserList li').forEach(li => {
const username = li.textContent.toLowerCase();
if (username.includes(searchTerm)) {
li.style.display = '';
} else {
li.style.display = 'none';
}
});
});
// Add filter functionality
document.querySelectorAll('.badge-filters button').forEach(button => {
button.addEventListener('click', function() {
// Update active state
document.querySelectorAll('.badge-filters button').forEach(btn => {
btn.classList.remove('active');
});
this.classList.add('active');
// Apply filter
const filter = this.getAttribute('data-filter');
const badgeCards = document.querySelectorAll('.admin-badge-card');
badgeCards.forEach(card => {
if (filter === 'all' || card.getAttribute('data-category') === filter) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
});
});
// Initialize print certificates functionality
document.getElementById('printBadges').addEventListener('click', function() {
const username = document.getElementById('badgeUserTitle').textContent.replace('Badges for ', '');
printBadgeCertificate(username);
});
// Initialize custom badge award functionality
document.getElementById('awardCustomBadge').addEventListener('click', function() {
const username = document.getElementById('badgeUserTitle').textContent.replace('Badges for ', '');
showCustomBadgeModal(username);
});
}
function showUserBadges(username) {
fetch('/getBadges')
.then(res => res.json())
.then(data => {
const userBadgeData = data[username];
const title = document.getElementById("badgeUserTitle");
const badgeList = document.getElementById("badgeUserBadges");
const details = document.getElementById("badgeDetails");
let badges = [];
// Handle different badge data formats
if (!userBadgeData) {
badges = [];
} else if (Array.isArray(userBadgeData)) {
// Old format
badges = userBadgeData.map(name => ({
name,
icon: "🏅",
color: "#4a5568"
}));
} else if (userBadgeData.earned) {
// New format
badges = userBadgeData.earned;
}
title.textContent = `Badges for ${username}`;
badgeList.innerHTML = "";
// Create badge stats summary
const badgeStats = document.createElement("div");
badgeStats.className = "badge-stats";
// Calculate badge counts by category
const categoryStats = {};
if (userBadgeData && userBadgeData.counts) {
Object.entries(userBadgeData.counts).forEach(([category, count]) => {
categoryStats[category] = count;
});
} else {
// Calculate from badges array
badges.forEach(badge => {
const category = badge.category || 'other';
if (!categoryStats[category]) categoryStats[category] = 0;
categoryStats[category]++;
});
}
// Create badge summary
badgeStats.innerHTML = `
<div class="badge-summary">
<h3>Badge Summary</h3>
<div class="badge-total">
<span class="badge-count">${badges.length}</span>
<span class="badge-label">Total Badges</span>
</div>
<div class="badge-categories">
${Object.entries(categoryStats).map(([category, count]) => `
<div class="badge-category">
<span class="category-name">${category.charAt(0).toUpperCase() + category.slice(1)}</span>
<span class="category-count">${count}</span>
</div>
`).join('')}
</div>
</div>
`;
badgeList.appendChild(badgeStats);
if (!badges.length) {
const emptyState = document.createElement("div");
emptyState.className = "empty-badges";
emptyState.innerHTML = `
<div class="empty-badge-icon">🏆</div>
<h3>No badges earned yet</h3>
<p>Complete spelling activities to earn badges!</p>
`;
badgeList.appendChild(emptyState);
} else {
// Create badge grid for displaying badges
const badgeGrid = document.createElement("div");
badgeGrid.className = "badge-grid";
badges.forEach(badge => {
const badgeCard = document.createElement("div");
badgeCard.className = "admin-badge-card";
const badgeIcon = badge.icon || "🏅";
const badgeColor = badge.color || "#4a5568";
const badgeName = badge.name;
const badgeDesc = badge.description || "";
const badgeDate = badge.earnedAt ? new Date(badge.earnedAt).toLocaleDateString() : "";
badgeCard.style.borderColor = badgeColor;
badgeCard.innerHTML = `
<div class="admin-badge-icon" style="background-color: ${badgeColor}40">
<span>${badgeIcon}</span>
</div>
<div class="admin-badge-info">
<h4>${badgeName}</h4>
${badgeDesc ? `<p class="admin-badge-desc">${badgeDesc}</p>` : ''}
${badgeDate ? `<div class="admin-badge-date">Earned: ${badgeDate}</div>` : ''}
</div>
`;
badgeGrid.appendChild(badgeCard);
});
badgeList.appendChild(badgeGrid);
}
details.classList.remove("hidden");
});
}
async function loadStudentNamesForReport() {
const res = await fetch('/getResults');
const data = await res.json();
const select = document.getElementById("reportUser");
select.innerHTML = `<option value="">-- Select --</option>`;
Object.keys(data).forEach(username => {
const opt = document.createElement("option");
opt.value = username;
opt.textContent = username;
select.appendChild(opt);
});
}
window.loadStudentReport = async function(username) {
if (!username) return;
try {
const res1 = await fetch('/getResults');
const results = await res1.json();
const res2 = await fetch('/getBadges');
const badges = await res2.json();
const userResults = results[username];
const badgeList = badges[username] || [];
// Handle case where userResults might be an array or undefined
let latestReport = null;
let allUserResults = [];
if (Array.isArray(userResults) && userResults.length > 0) {
// Get the most recent result
latestReport = userResults[userResults.length - 1];
allUserResults = userResults;
} else if (userResults && typeof userResults === 'object') {
// Handle single result object
latestReport = userResults;
allUserResults = [userResults];
}
if (!latestReport) {
document.getElementById("reportContent").classList.add("hidden");
alert(`No results found for ${username}`);
return;
}
document.getElementById("reportTitle").textContent = `Report for ${username}`;
// Safe access to score and answers
const score = latestReport.score || 0;
const answers = latestReport.answers || [];
document.getElementById("reportScore").textContent = `${score}/${answers.length} correct`;
const wordList = document.getElementById("reportWords");
wordList.innerHTML = "";
if (answers.length > 0) {
answers.forEach(({ word, correct }) => {
const li = document.createElement("li");
li.textContent = `${word} — ${correct ? '✅' : '❌'}`;
wordList.appendChild(li);
});
} else {
wordList.innerHTML = "<li><em>No word data available.</em></li>";
}
const badgeContainer = document.getElementById("reportBadges");
badgeContainer.innerHTML = "";
if (badgeList.length) {
badgeList.forEach(b => {
const li = document.createElement("li");
li.textContent = `🏅 ${b}`;
badgeContainer.appendChild(li);
});
} else {
badgeContainer.innerHTML = "<li><em>No badges earned yet.</em></li>";
}
// Generate progress chart if function exists
if (typeof window.createScoreChart === 'function') {
createScoreChart(username, allUserResults);
}
// Generate word difficulty chart if function exists
if (typeof window.createWordDifficultyChart === 'function') {
createWordDifficultyChart(username, allUserResults);
}
document.getElementById("reportContent").classList.remove("hidden");
} catch (error) {
console.error("Error loading student report:", error);
alert("Error loading student report. Please try again.");
}
}
window.copyReportToClipboard = function() {
const name = document.getElementById("reportTitle").textContent;
const score = document.getElementById("reportScore").textContent;
const words = Array.from(document.querySelectorAll("#reportWords li")).map(li => li.textContent).join("\n");
const badges = Array.from(document.querySelectorAll("#reportBadges li")).map(li => li.textContent).join("\n");
const summary = `${name}\nScore: ${score}\n\nWords:\n${words}\n\nBadges:\n${badges}`;
navigator.clipboard.writeText(summary);
alert("✅ Report copied to clipboard");
}
async function loadSessionHistory() {
const res = await fetch('/getResults');
const allResults = await res.json();
const list = document.getElementById("sessionSummaryList");
list.innerHTML = "";
for (const username in allResults) {
const sessions = allResults[username];
const header = document.createElement("li");
header.innerHTML = `<strong>${username}</strong>`;
list.appendChild(header);
sessions.forEach(({ score, answers, timestamp }, index) => {
const li = document.createElement("li");
const dateStr = new Date(timestamp).toLocaleString();
const total = answers.length;
li.textContent = `🗓️ ${dateStr} — ${score}/${total} correct`;
list.appendChild(li);
});
}
}
async function populateSessionUserDropdown() {
const res = await fetch('/getResults');
const allResults = await res.json();
const select = document.getElementById("sessionUserSelect");
select.innerHTML = `<option value="">-- Select --</option>`;
Object.keys(allResults).forEach(username => {
const opt = document.createElement("option");
opt.value = username;
opt.textContent = username;
select.appendChild(opt);
});
}
async function loadUserSessions(username) {
const res = await fetch('/getResults');
const allResults = await res.json();
const userSessions = allResults[username] || [];
const list = document.getElementById("userSessionList");
list.innerHTML = "";
userSessions.forEach(({ score, answers, timestamp }) => {
const sessionItem = document.createElement("li");
const dateStr = new Date(timestamp).toLocaleString();
const total = answers.length;
let html = `<p>🗓️ <strong>${dateStr}</strong> — ${score}/${total} correct</p><ul>`;
answers.forEach(({ word, correct }) => {
html += `<li>${correct ? '✅' : '❌'} ${word}</li>`;
});
html += "</ul>";
sessionItem.innerHTML = html;
list.appendChild(sessionItem);
});
}
// File upload handler for word lists
function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const content = e.target.result;
const words = content.split(/\r?\n/).map(w => w.trim()).filter(Boolean);
document.getElementById('wordInput').value = words.join('\n');
showToast(`📁 Loaded ${words.length} words from file`);
};
reader.readAsText(file);
}
// 🔐 Password Management Functions
window.changeUserPassword = async function() {
const username = document.getElementById('passwordUsername').value;
const newPassword = document.getElementById('newUserPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (!username) {
alert('Please select a user');
return;
}
if (!newPassword || newPassword.length < 3) {
alert('Password must be at least 3 characters long');
return;
}
if (newPassword !== confirmPassword) {
alert('Passwords do not match');
return;
}
try {
// Hash the new password (same way as registration)
const encoder = new TextEncoder();
const data = encoder.encode(newPassword);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const newPasswordHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// Send password change request
const response = await fetch('/changePassword', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, newPasswordHash })
});
const message = await response.text();
if (response.ok) {
alert(message);
logPasswordChange(username);
// Clear form
document.getElementById('newUserPassword').value = '';
document.getElementById('confirmPassword').value = '';
document.getElementById('passwordUsername').value = '';
} else {
alert('Error: ' + message);
}
} catch (error) {
console.error('Error changing password:', error);
alert('Failed to change password');
}
}
// Log password changes
function logPasswordChange(username) {
const passwordLog = document.getElementById('passwordLog');
const timestamp = new Date().toLocaleString();
const logEntry = document.createElement('div');
logEntry.style.padding = '0.5rem';
logEntry.style.marginBottom = '0.5rem';
logEntry.style.backgroundColor = 'var(--bg-primary)';
logEntry.style.borderRadius = 'var(--radius)';
logEntry.style.borderLeft = '4px solid var(--warning)';
logEntry.innerHTML = `
<strong>🔐 Password Changed</strong><br>
<span style="color: var(--text-secondary);">User: ${username}</span><br>
<span style="color: var(--text-secondary); font-size: 0.9rem;">${timestamp}</span>
`;
// Remove "no changes" message if it exists
if (passwordLog.querySelector('p')) {
passwordLog.innerHTML = '';
}
// Add new entry at the top
passwordLog.insertBefore(logEntry, passwordLog.firstChild);
}
// Populate password username dropdown
function populatePasswordUserDropdown() {
const dropdown = document.getElementById('passwordUsername');
if (!dropdown || !adminUsers) return;
dropdown.innerHTML = '<option value="">-- Select a user --</option>';
adminUsers.forEach(user => {
const option = document.createElement('option');
option.value = user.username;
option.textContent = `${user.username} (${user.role})`;
dropdown.appendChild(option);
});
}
// Add event listeners for admin tabs as backup to onclick
window.addEventListener('DOMContentLoaded', () => {
const tabButtons = document.querySelectorAll('#adminTabs button');
tabButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const tabId = button.getAttribute('data-tab');
if (tabId) {
console.log('Tab clicked:', tabId);
if (typeof window.switchTab === 'function') {
window.switchTab(tabId);
} else {
console.error('switchTab function not available');
}
}
});
});
console.log('✅ Tab event listeners added to', tabButtons.length, 'buttons');
});