/** * 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