/** * Learn Module - JavaScript Controller * Learning Management System for General Bots */ // State management const LearnState = { courses: [], myCourses: [], mandatoryAssignments: [], certificates: [], categories: [], currentCourse: null, currentLesson: null, currentQuiz: null, quizAnswers: {}, quizTimer: null, quizTimeRemaining: 0, currentQuestionIndex: 0, filters: { category: "all", difficulty: ["beginner", "intermediate", "advanced"], search: "", sort: "recent", }, pagination: { offset: 0, limit: 12, hasMore: true, }, userStats: { coursesCompleted: 0, coursesInProgress: 0, certificates: 0, timeSpent: 0, }, }; // API Base URL const LEARN_API = "/api/learn"; // ============================================================================ // INITIALIZATION // ============================================================================ document.addEventListener("DOMContentLoaded", () => { initLearn(); }); function initLearn() { loadUserStats(); loadCategories(); loadCourses(); loadMyCourses(); loadMandatoryAssignments(); loadCertificates(); loadRecommendations(); bindEvents(); } function bindEvents() { // Search input const searchInput = document.getElementById("searchCourses"); if (searchInput) { let searchTimeout; searchInput.addEventListener("input", (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { LearnState.filters.search = e.target.value; LearnState.pagination.offset = 0; loadCourses(); }, 300); }); } // Sort select const sortSelect = document.getElementById("sortCourses"); if (sortSelect) { sortSelect.addEventListener("change", (e) => { LearnState.filters.sort = e.target.value; LearnState.pagination.offset = 0; loadCourses(); }); } // Category filters document.querySelectorAll(".category-item").forEach((item) => { item.addEventListener("click", () => { document .querySelectorAll(".category-item") .forEach((i) => i.classList.remove("active")); item.classList.add("active"); LearnState.filters.category = item.dataset.category; LearnState.pagination.offset = 0; loadCourses(); }); }); // Difficulty filters document.querySelectorAll("[data-difficulty]").forEach((checkbox) => { checkbox.addEventListener("change", () => { LearnState.filters.difficulty = Array.from( document.querySelectorAll("[data-difficulty]:checked"), ).map((cb) => cb.dataset.difficulty); LearnState.pagination.offset = 0; loadCourses(); }); }); // Close modals on background click document.querySelectorAll(".modal").forEach((modal) => { modal.addEventListener("click", (e) => { if (e.target === modal) { modal.classList.add("hidden"); } }); }); // Keyboard shortcuts document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeAllModals(); } }); } // ============================================================================ // API CALLS // ============================================================================ async function apiCall(endpoint, options = {}) { try { const response = await fetch(`${LEARN_API}${endpoint}`, { headers: { "Content-Type": "application/json", ...options.headers, }, ...options, }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "API request failed"); } return data; } catch (error) { console.error("API Error:", error); showNotification("error", "Erro ao carregar dados. Tente novamente."); throw error; } } async function loadUserStats() { try { const response = await apiCall("/stats/user"); if (response.success) { LearnState.userStats = response.data; updateUserStatsUI(); } } catch (error) { // Use mock data if API fails updateUserStatsUI(); } } async function loadCategories() { try { const response = await apiCall("/categories"); if (response.success) { LearnState.categories = response.data; updateCategoryCounts(); } } catch (error) { // Categories loaded from HTML } } async function loadCourses() { try { const params = new URLSearchParams({ limit: LearnState.pagination.limit, offset: LearnState.pagination.offset, }); if (LearnState.filters.category !== "all") { params.append("category", LearnState.filters.category); } if (LearnState.filters.search) { params.append("search", LearnState.filters.search); } if (LearnState.filters.difficulty.length < 3) { params.append("difficulty", LearnState.filters.difficulty.join(",")); } const response = await apiCall(`/courses?${params}`); if (response.success) { if (LearnState.pagination.offset === 0) { LearnState.courses = response.data; } else { LearnState.courses = [...LearnState.courses, ...response.data]; } LearnState.pagination.hasMore = response.data.length >= LearnState.pagination.limit; renderCourses(); } } catch (error) { // Render mock courses for demo renderMockCourses(); } } async function loadMyCourses() { try { const response = await apiCall("/progress"); if (response.success) { LearnState.myCourses = response.data; renderMyCourses(); } } catch (error) { renderMockMyCourses(); } } async function loadMandatoryAssignments() { try { const response = await apiCall("/assignments/pending"); if (response.success) { LearnState.mandatoryAssignments = response.data; renderMandatoryAssignments(); updateMandatoryAlert(); } } catch (error) { renderMockMandatory(); } } async function loadCertificates() { try { const response = await apiCall("/certificates"); if (response.success) { LearnState.certificates = response.data; renderCertificates(); } } catch (error) { renderMockCertificates(); } } async function loadRecommendations() { try { const response = await apiCall("/recommendations"); if (response.success) { renderRecommendations(response.data); } } catch (error) { renderMockRecommendations(); } } async function loadCourseDetail(courseId) { try { const response = await apiCall(`/courses/${courseId}`); if (response.success) { LearnState.currentCourse = response.data; renderCourseModal(response.data); } } catch (error) { showMockCourseDetail(courseId); } } async function startCourseAPI(courseId) { try { const response = await apiCall(`/progress/${courseId}/start`, { method: "POST", }); if (response.success) { showNotification("success", "Curso iniciado com sucesso!"); loadMyCourses(); return response.data; } } catch (error) { showNotification("error", "Erro ao iniciar o curso."); } } async function completeLessonAPI(lessonId) { try { const response = await apiCall(`/progress/${lessonId}/complete`, { method: "POST", }); if (response.success) { showNotification("success", "Aula concluída!"); return response.data; } } catch (error) { showNotification("error", "Erro ao marcar aula como concluída."); } } async function submitQuizAPI(courseId, answers) { try { const response = await apiCall(`/courses/${courseId}/quiz`, { method: "POST", body: JSON.stringify({ answers }), }); if (response.success) { return response.data; } } catch (error) { showNotification("error", "Erro ao enviar respostas."); } } // ============================================================================ // UI RENDERING // ============================================================================ function updateUserStatsUI() { document.getElementById("statCoursesCompleted").textContent = LearnState.userStats.courses_completed || 0; document.getElementById("statCoursesInProgress").textContent = LearnState.userStats.courses_in_progress || 0; document.getElementById("statCertificates").textContent = LearnState.userStats.certificates_earned || 0; document.getElementById("statTimeSpent").textContent = `${LearnState.userStats.total_time_spent_hours || 0}h`; } function updateCategoryCounts() { // Update category counts based on courses const counts = {}; LearnState.courses.forEach((course) => { counts[course.category] = (counts[course.category] || 0) + 1; }); document.getElementById("countAll").textContent = LearnState.courses.length; document.getElementById("countMandatory").textContent = LearnState.mandatoryAssignments.length; } function renderCourses() { const grid = document.getElementById("coursesGrid"); const countLabel = document.getElementById("coursesCountLabel"); if (!grid) return; if (LearnState.pagination.offset === 0) { grid.innerHTML = ""; } if (LearnState.courses.length === 0) { grid.innerHTML = `
📚

Nenhum curso encontrado

Tente ajustar os filtros de busca.

`; countLabel.textContent = "0 cursos"; return; } LearnState.courses.forEach((course) => { grid.appendChild(createCourseCard(course)); }); countLabel.textContent = `${LearnState.courses.length} cursos`; // Show/hide load more button const loadMore = document.getElementById("loadMore"); if (loadMore) { loadMore.style.display = LearnState.pagination.hasMore ? "block" : "none"; } } function createCourseCard(course) { const card = document.createElement("div"); card.className = "course-card"; card.onclick = () => openCourseModal(course.id); const difficultyClass = (course.difficulty || "beginner").toLowerCase(); const progress = course.user_progress || 0; card.innerHTML = `
${ course.thumbnail_url ? `${course.title}` : `📖` } ${course.is_mandatory ? 'Obrigatório' : ""} ${progress > 0 ? `${progress}%` : ""}

${escapeHtml(course.title)}

${formatDifficulty(course.difficulty)} ${formatDuration(course.duration_minutes)}
${ progress > 0 ? `
${progress}% completo
` : "" }
`; return card; } function renderMyCourses() { const continueLearning = document.getElementById("continueLearning"); const completedCourses = document.getElementById("completedCourses"); if (!continueLearning || !completedCourses) return; const inProgress = LearnState.myCourses.filter( (c) => c.status === "in_progress", ); const completed = LearnState.myCourses.filter( (c) => c.status === "completed", ); // Update badge document.getElementById("myCoursesCount").textContent = inProgress.length; // Render in-progress courses if (inProgress.length === 0) { continueLearning.innerHTML = `
📚

Nenhum curso em andamento

`; } else { continueLearning.innerHTML = inProgress .map((course) => createCourseListItem(course)) .join(""); } // Render completed courses if (completed.length === 0) { completedCourses.innerHTML = `

Nenhum curso concluído ainda

`; } else { completedCourses.innerHTML = completed .map((course) => createCourseListItem(course, true)) .join(""); } } function createCourseListItem(course, isCompleted = false) { return `
📖

${escapeHtml(course.course_title || course.title || "Curso")}

${formatDuration(course.duration_minutes || 30)}
${course.completion_percentage || (isCompleted ? 100 : 0)}% completo
`; } function renderMandatoryAssignments() { const list = document.getElementById("mandatoryList"); const badge = document.getElementById("mandatoryCount"); if (!list) return; badge.textContent = LearnState.mandatoryAssignments.length; if (LearnState.mandatoryAssignments.length === 0) { list.innerHTML = `
🎉

Tudo em dia!

Você não possui treinamentos obrigatórios pendentes.

`; return; } list.innerHTML = LearnState.mandatoryAssignments .map((assignment) => { const isOverdue = assignment.due_date && new Date(assignment.due_date) < new Date(); const daysUntilDue = assignment.due_date ? Math.ceil( (new Date(assignment.due_date) - new Date()) / (1000 * 60 * 60 * 24), ) : null; const isUrgent = daysUntilDue !== null && daysUntilDue <= 7 && daysUntilDue > 0; return `
${isOverdue ? "⚠️" : isUrgent ? "⏰" : "📋"}

${escapeHtml(assignment.course_title || "Treinamento Obrigatório")}

${ isOverdue ? "⚠️ Prazo vencido!" : daysUntilDue !== null ? `Prazo: ${daysUntilDue} dias` : "Sem prazo definido" }
`; }) .join(""); } function updateMandatoryAlert() { const alert = document.getElementById("mandatoryAlert"); const alertText = document.getElementById("mandatoryAlertText"); if (!alert) return; const overdueCount = LearnState.mandatoryAssignments.filter( (a) => a.due_date && new Date(a.due_date) < new Date(), ).length; const urgentCount = LearnState.mandatoryAssignments.filter((a) => { if (!a.due_date) return false; const days = Math.ceil( (new Date(a.due_date) - new Date()) / (1000 * 60 * 60 * 24), ); return days > 0 && days <= 7; }).length; if (overdueCount > 0 || urgentCount > 0) { alert.style.display = "flex"; if (overdueCount > 0) { alertText.textContent = `Você possui ${overdueCount} treinamento(s) com prazo vencido!`; } else { alertText.textContent = `Você possui ${urgentCount} treinamento(s) com prazo próximo.`; } } else { alert.style.display = "none"; } } function renderCertificates() { const grid = document.getElementById("certificatesGrid"); const preview = document.getElementById("certificatesPreview"); if (!grid) return; if (LearnState.certificates.length === 0) { grid.innerHTML = `
🏆

Nenhum certificado ainda

Complete seus cursos para ganhar certificados.

`; if (preview) { preview.innerHTML = `
🏆

Nenhum certificado ainda

`; } return; } grid.innerHTML = LearnState.certificates .map( (cert) => `
🎓

${escapeHtml(cert.course_title || "Curso Concluído")}

${cert.score}% ${formatDate(cert.issued_at)}
`, ) .join(""); // Update sidebar preview if (preview) { preview.innerHTML = LearnState.certificates .slice(0, 3) .map( (cert) => `
🎓
${escapeHtml(cert.course_title || "Curso")}
${formatDate(cert.issued_at)}
`, ) .join(""); } } function renderRecommendations(courses) { const carousel = document.getElementById("recommendedCourses"); if (!carousel) return; if (!courses || courses.length === 0) { carousel.innerHTML = '

Explore o catálogo para encontrar cursos.

'; return; } carousel.innerHTML = courses .slice(0, 6) .map((course) => { const card = createCourseCard(course); card.style.minWidth = "280px"; return card.outerHTML; }) .join(""); } // ============================================================================ // MODALS // ============================================================================ function openCourseModal(courseId) { loadCourseDetail(courseId); document.getElementById("courseModal").classList.remove("hidden"); } function closeCourseModal() { document.getElementById("courseModal").classList.add("hidden"); LearnState.currentCourse = null; } function renderCourseModal(data) { const { course, lessons, quiz } = data; document.getElementById("modalCourseTitle").textContent = course.title; document.getElementById("modalDescription").textContent = course.description || "Sem descrição disponível."; document.getElementById("modalDifficulty").textContent = formatDifficulty( course.difficulty, ); document.getElementById("modalDifficulty").className = `difficulty-badge ${(course.difficulty || "beginner").toLowerCase()}`; document.getElementById("modalDuration").querySelector("span").textContent = formatDuration(course.duration_minutes); document .getElementById("modalLessonsCount") .querySelector("span").textContent = `${lessons?.length || 0} aulas`; // Render lessons const lessonsList = document.getElementById("modalLessonsList"); if (lessons && lessons.length > 0) { lessonsList.innerHTML = lessons .map( (lesson, index) => `
${lesson.is_completed ? "✓" : index + 1}
${escapeHtml(lesson.title)}
${formatDuration(lesson.duration_minutes)}
`, ) .join(""); } else { lessonsList.innerHTML = '

Nenhuma aula disponível.

'; } // Render quiz section const quizSection = document.getElementById("modalQuizSection"); if (quiz) { quizSection.style.display = "block"; const questions = typeof quiz.questions === "string" ? JSON.parse(quiz.questions) : quiz.questions || []; document.getElementById("modalQuizQuestions").textContent = `${questions.length} questões`; document.getElementById("modalQuizTime").textContent = quiz.time_limit_minutes ? `${quiz.time_limit_minutes} min` : "Sem limite"; document.getElementById("modalQuizPassing").textContent = `${quiz.passing_score}% para aprovação`; LearnState.currentQuiz = quiz; } else { quizSection.style.display = "none"; } // Update button text based on progress const startBtn = document.getElementById("startCourseBtn"); if (data.user_progress) { const progress = data.user_progress; if (progress.status === "completed") { startBtn.innerHTML = ` Revisar Curso `; } else if (progress.status === "in_progress") { startBtn.innerHTML = ` Continuar `; } // Show progress document.getElementById("modalProgress").style.display = "block"; document.getElementById("modalProgressFill").style.width = `${progress.completion_percentage || 0}%`; document.getElementById("modalProgressText").textContent = `${progress.completion_percentage || 0}% completo`; } else { document.getElementById("modalProgress").style.display = "none"; startBtn.innerHTML = ` Iniciar Curso `; } } function startCourse() { if (!LearnState.currentCourse) return; const course = LearnState.currentCourse.course || LearnState.currentCourse; const lessons = LearnState.currentCourse.lessons || []; // Start course via API startCourseAPI(course.id); // Open first lesson if (lessons.length > 0) { openLesson(lessons[0].id, 0); } else { showNotification("info", "Este curso ainda não possui aulas."); } } function openLesson(lessonId, index) { const lessons = LearnState.currentCourse?.lessons || []; const lesson = lessons.find((l) => l.id === lessonId) || lessons[index]; if (!lesson) { showNotification("error", "Aula não encontrada."); return; } LearnState.currentLesson = lesson; LearnState.currentLessonIndex = index; // Close course modal, open lesson modal closeCourseModal(); document.getElementById("lessonModal").classList.remove("hidden"); // Update lesson UI document.getElementById("lessonTitle").textContent = lesson.title; document.getElementById("lessonNavTitle").textContent = `Aula ${index + 1} de ${lessons.length}`; // Render content based on type const contentDiv = document.getElementById("lessonContent"); if (lesson.video_url) { contentDiv.innerHTML = `
${lesson.content || ""}
`; } else { contentDiv.innerHTML = `
${lesson.content || "

Conteúdo da aula será exibido aqui.

"}
`; } // Update sidebar list const sidebar = document.getElementById("lessonListSidebar"); sidebar.innerHTML = lessons .map( (l, i) => `
${l.is_completed ? "✓" : i + 1}
${escapeHtml(l.title)}
`, ) .join(""); // Update navigation buttons document.getElementById("prevLessonBtn").disabled = index === 0; document.getElementById("nextLessonBtn").disabled = index >= lessons.length - 1; // Update progress const progress = ((index + 1) / lessons.length) * 100; document.getElementById("lessonProgressFill").style.width = `${progress}%`; } function closeLessonModal() { document.getElementById("lessonModal").classList.add("hidden"); LearnState.currentLesson = null; // Reopen course modal if (LearnState.currentCourse) { openCourseModal(LearnState.currentCourse.id); } } function toggleLearnSidebar() { const sidebar = document.querySelector(".learn-sidebar"); if (sidebar) { sidebar.classList.toggle("collapsed"); } } function switchTab(tabId) { document.querySelectorAll(".tab-btn, .learn-tab-btn").forEach((btn) => { btn.classList.remove("active"); if ( btn.dataset.tab === tabId || btn.getAttribute("onclick")?.includes(tabId) ) { btn.classList.add("active"); } }); document .querySelectorAll(".tab-content, .learn-tab-content") .forEach((content) => { content.classList.add("hidden"); if (content.id === tabId || content.id === `${tabId}-tab`) { content.classList.remove("hidden"); } }); } function showAllCertificates() { switchTab("certificates"); } function loadMoreCourses() { const currentCount = document.querySelectorAll(".course-card").length; fetch(`/api/learn/courses?offset=${currentCount}&limit=12`) .then((r) => r.json()) .then((data) => { const grid = document.querySelector(".courses-grid"); if (grid && data.courses) { data.courses.forEach((course) => { grid.insertAdjacentHTML("beforeend", createCourseCard(course)); }); } if (!data.hasMore) { const btn = document.querySelector('[onclick="loadMoreCourses()"]'); if (btn) btn.style.display = "none"; } }) .catch((err) => console.error("Error loading more courses:", err)); } function startQuiz() { if (!LearnState.currentCourse) return; LearnState.quizState = { questions: LearnState.currentCourse.quiz || [], currentIndex: 0, answers: {}, startTime: Date.now(), }; document.getElementById("courseModal").classList.add("hidden"); document.getElementById("quizModal").classList.remove("hidden"); renderQuizQuestion(); } function renderQuizQuestion() { const { questions, currentIndex } = LearnState.quizState; if (!questions || questions.length === 0) return; const question = questions[currentIndex]; const container = document.getElementById("quizQuestionContainer"); container.innerHTML = `
Question ${currentIndex + 1} of ${questions.length}

${question.text}

${question.options .map( (opt, i) => ` `, ) .join("")}
`; document.getElementById("quizProgress").textContent = `${currentIndex + 1}/${questions.length}`; document.getElementById("quizProgressFill").style.width = `${((currentIndex + 1) / questions.length) * 100}%`; } function selectAnswer(index) { LearnState.quizState.answers[LearnState.quizState.currentIndex] = index; } function prevQuestion() { if (LearnState.quizState.currentIndex > 0) { LearnState.quizState.currentIndex--; renderQuizQuestion(); } } function nextQuestion() { if ( LearnState.quizState.currentIndex < LearnState.quizState.questions.length - 1 ) { LearnState.quizState.currentIndex++; renderQuizQuestion(); } } function submitQuiz() { const { questions, answers, startTime } = LearnState.quizState; let correct = 0; questions.forEach((q, i) => { if (answers[i] === q.correctIndex) correct++; }); const score = Math.round((correct / questions.length) * 100); const passed = score >= 70; const duration = Math.round((Date.now() - startTime) / 1000); LearnState.quizResult = { score, correct, total: questions.length, passed, duration, }; document.getElementById("quizModal").classList.add("hidden"); document.getElementById("quizResultModal").classList.remove("hidden"); document.getElementById("quizScore").textContent = `${score}%`; document.getElementById("quizCorrect").textContent = `${correct}/${questions.length}`; document.getElementById("quizStatus").textContent = passed ? "Passed!" : "Not Passed"; document.getElementById("quizStatus").className = passed ? "status-passed" : "status-failed"; if (passed && LearnState.currentCourse) { fetch(`/api/learn/courses/${LearnState.currentCourse.id}/complete`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ score, duration }), }).catch((err) => console.error("Error completing course:", err)); } } function closeQuizResult() { document.getElementById("quizResultModal").classList.add("hidden"); LearnState.quizState = null; LearnState.quizResult = null; renderMyCourses(); renderCertificates(); } function reviewAnswers() { document.getElementById("quizResultModal").classList.add("hidden"); document.getElementById("quizModal").classList.remove("hidden"); LearnState.quizState.currentIndex = 0; renderQuizQuestion(); } function confirmExitQuiz() { if (confirm("Are you sure you want to exit? Your progress will be lost.")) { document.getElementById("quizModal").classList.add("hidden"); LearnState.quizState = null; if (LearnState.currentCourse) { openCourseModal(LearnState.currentCourse.id); } } } function downloadCertificate() { if (!LearnState.currentCourse) return; window.open( `/api/learn/certificates/${LearnState.currentCourse.id}/download`, "_blank", ); } function downloadCertificateById(certId) { window.open(`/api/learn/certificates/${certId}/download`, "_blank"); } function closeCertificateModal() { document.getElementById("certificateModal").classList.add("hidden"); } function prevLesson() { if (!LearnState.currentCourse || !LearnState.currentLesson) return; const lessons = LearnState.currentCourse.lessons || []; const currentIndex = lessons.findIndex( (l) => l.id === LearnState.currentLesson.id, ); if (currentIndex > 0) { openLesson(lessons[currentIndex - 1].id, currentIndex - 1); } } function nextLesson() { if (!LearnState.currentCourse || !LearnState.currentLesson) return; const lessons = LearnState.currentCourse.lessons || []; const currentIndex = lessons.findIndex( (l) => l.id === LearnState.currentLesson.id, ); if (currentIndex < lessons.length - 1) { openLesson(lessons[currentIndex + 1].id, currentIndex + 1); } } function completeLesson() { if (!LearnState.currentLesson || !LearnState.currentCourse) return; fetch(`/api/learn/lessons/${LearnState.currentLesson.id}/complete`, { method: "POST", }) .then(() => { LearnState.currentLesson.completed = true; closeLessonModal(); }) .catch((err) => console.error("Error completing lesson:", err)); } // Export functions to window window.toggleLearnSidebar = toggleLearnSidebar; window.showAllCertificates = showAllCertificates; window.loadMoreCourses = loadMoreCourses; window.startQuiz = startQuiz; window.prevQuestion = prevQuestion; window.nextQuestion = nextQuestion; window.submitQuiz = submitQuiz; window.closeQuizResult = closeQuizResult; window.reviewAnswers = reviewAnswers; window.confirmExitQuiz = confirmExitQuiz; window.downloadCertificate = downloadCertificate; window.downloadCertificateById = downloadCertificateById; window.closeCertificateModal = closeCertificateModal; window.prevLesson = prevLesson; window.nextLesson = nextLesson; window.completeLesson = completeLesson; window.selectAnswer = selectAnswer; window.switchTab = switchTab; window.openCourseModal = openCourseModal; window.closeCourseModal = closeCourseModal; window.startCourse = startCourse; window.openLesson = openLesson; window.closeLessonModal = closeLessonModal;