diff --git a/public/app.js b/public/app.js index 97daab4..4bf59c0 100644 --- a/public/app.js +++ b/public/app.js @@ -3231,12 +3231,13 @@ const initApp = () => { const turmaObj = currentTurmas.find(t => t.id === c.turma_id); const turmaNome = turmaObj ? turmaObj.nome : 'Sem turma'; return ` -
+
${escapeHtml(c.nome)} ${c.especial ? '⭐' : ''}
${escapeHtml(turmaNome)} ${c.apelido ? '- ' + escapeHtml(c.apelido) : ''}
-
+
+
@@ -10822,3 +10823,724 @@ document.addEventListener('DOMContentLoaded', () => { item.click(); } }); + +// ========================================================================== +// MÓDULOS EXPANDIDOS: DOSSIÊ DO ALUNO, PROTOCOLO DE CRISES, ATAS & STICKERS +// ========================================================================== +document.addEventListener('DOMContentLoaded', () => { + + // ------------------------------------------------------------------------ + // 1. DOSSIÊ & LINHA DO TEMPO DO ALUNO + // ------------------------------------------------------------------------ + const alunoTimelineModal = document.getElementById('alunoTimelineModal'); + const btnCloseAlunoTimeline = document.getElementById('btnCloseAlunoTimeline'); + const timelineAlunoNome = document.getElementById('timelineAlunoNome'); + const timelineAlunoInfo = document.getElementById('timelineAlunoInfo'); + const timelineContentArea = document.getElementById('timelineContentArea'); + const btnExportFichaConselho = document.getElementById('btnExportFichaConselho'); + let currentTimelineAlunoId = null; + + if (btnCloseAlunoTimeline) btnCloseAlunoTimeline.addEventListener('click', () => alunoTimelineModal.style.display = 'none'); + if (alunoTimelineModal) alunoTimelineModal.addEventListener('click', (e) => { if (e.target === alunoTimelineModal) alunoTimelineModal.style.display = 'none'; }); + + window.viewChildTimeline = async (alunoId) => { + currentTimelineAlunoId = alunoId; + alunoTimelineModal.style.display = 'flex'; + timelineContentArea.innerHTML = '

Carregando dossiê...

'; + + try { + const res = await fetch(`/api/alunos/${alunoId}/timeline`); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Erro ao carregar dados'); + + const aluno = data.aluno; + timelineAlunoNome.textContent = `Dossiê: ${aluno.nome} ${aluno.especial ? '⭐ (Ed. Especial)' : ''}`; + timelineAlunoInfo.textContent = `Nasc: ${aluno.data_nasc ? new Date(aluno.data_nasc).toLocaleDateString('pt-BR') : 'Não informada'} • Pais: ${aluno.pais || 'Não informados'} • Saúde: ${aluno.observacoes_saude || 'Nenhuma alergia/restrição'}`; + + let html = ''; + + // Seção Observações + const obs = data.timeline.observacoes || []; + html += `

📝 Observações Registradas (${obs.length})

`; + if (obs.length === 0) { + html += '

Nenhuma observação registrada ainda.

'; + } else { + obs.forEach(o => { + html += ` +
+
+ 📅 ${o.date || ''} ${o.time || ''} + ${o.tags ? `🏷️ ${o.tags}` : ''} +
+

${escapeHtml(o.report || '')}

+
+ `; + }); + } + + // Seção Stickers & Conquistas + const stks = data.timeline.stickers || []; + html += `

🌟 Conquistas & Stickers Recebidos (${stks.length})

`; + if (stks.length === 0) { + html += '

Nenhum sticker concedido ainda.

'; + } else { + html += '
'; + stks.forEach(s => { + html += ` +
+ ${s.icone || '🌟'} ${escapeHtml(s.titulo)} +
${escapeHtml(s.categoria)} • ${new Date(s.created_at).toLocaleDateString('pt-BR')}
+
+ `; + }); + html += '
'; + } + + // Seção Histórias + const hists = data.timeline.historias || []; + if (hists.length > 0) { + html += `

📖 Histórias em que Participou (${hists.length})

`; + hists.forEach(h => { + html += ` +
+ ${escapeHtml(h.titulo || h.tema)} +
${h.faixa_etaria} • ${new Date(h.created_at).toLocaleDateString('pt-BR')}
+
+ `; + }); + } + + // Seção Ocorrências / Crises + const crises = data.timeline.crises || []; + if (crises.length > 0) { + html += `

🚨 Registros de Mediação / Acolhimento (${crises.length})

`; + crises.forEach(cr => { + html += ` +
+
+ ${escapeHtml(cr.tipo_evento)} (${cr.intensidade}) + ${new Date(cr.data_hora).toLocaleString('pt-BR')} +
+

${escapeHtml(cr.descricao)}

+
Acolhimento: ${escapeHtml(cr.medidas_tomadas)}
+
+ `; + }); + } + + // Seção Relatórios Semestrais + const rels = data.timeline.relatorios || []; + if (rels.length > 0) { + html += `

📄 Relatórios Semestrais Emitidos (${rels.length})

`; + rels.forEach(r => { + html += ` +
+ ${r.semestre}º Semestre de ${r.ano_letivo} (${r.faixa_etaria}) +

${escapeHtml(r.conteudo_relatorio)}

+
+ `; + }); + } + + timelineContentArea.innerHTML = html; + } catch (err) { + timelineContentArea.innerHTML = `

Erro: ${err.message}

`; + } + }; + + // Exportar Ficha para Conselho de Classe + if (btnExportFichaConselho) { + btnExportFichaConselho.addEventListener('click', async () => { + if (!currentTimelineAlunoId) return; + try { + const res = await fetch(`/api/alunos/${currentTimelineAlunoId}/conselho-ficha`); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Falha ao buscar ficha'); + + const aluno = data.aluno; + const win = window.open('', '_blank'); + win.document.write(` + + + + + Ficha de Conselho - ${aluno.nome} + + + + +

📋 Ficha Individual de Acompanhamento / Conselho de Classe

+
+
Aluno(a): ${aluno.nome}
+
Turma: ${aluno.turma_nome || 'N/A'}
+
Data Nasc: ${aluno.data_nasc ? new Date(aluno.data_nasc).toLocaleDateString('pt-BR') : 'N/A'}
+
Educação Especial: ${aluno.especial ? 'Sim (' + (aluno.especial_detalhes || '') + ')' : 'Não'}
+
Total de Observações: ${data.totalObservacoes}
+
Emissão: ${data.dataFicha}
+
+

🌟 Conquistas Pedagógicas:

+
    + ${(data.conquistas || []).map(c => `
  • ${c.titulo} (${c.categoria})
  • `).join('') || '
  • Nenhuma conquista pontual registrada.
  • '} +
+
+
___________________________
Professora Titular
+
___________________________
Coordenação Pedagógica
+
+ + + `); + win.document.close(); + } catch (e) { + alert('Erro ao gerar ficha: ' + e.message); + } + }); + } + + // ------------------------------------------------------------------------ + // 2. IMPORTAÇÃO CSV DE ALUNOS + // ------------------------------------------------------------------------ + const csvImportModal = document.getElementById('csvImportModal'); + const btnCloseCsvImport = document.getElementById('btnCloseCsvImport'); + const csvImportTurmaSelect = document.getElementById('csvImportTurmaSelect'); + const csvFileInput = document.getElementById('csvFileInput'); + const csvTextContent = document.getElementById('csvTextContent'); + const btnProcessCsvImport = document.getElementById('btnProcessCsvImport'); + + if (btnCloseCsvImport) btnCloseCsvImport.addEventListener('click', () => csvImportModal.style.display = 'none'); + if (csvImportModal) csvImportModal.addEventListener('click', (e) => { if (e.target === csvImportModal) csvImportModal.style.display = 'none'; }); + + // Botão abrir CSV import (se presente na tela de turmas ou sidebar) + window.openCsvImportModal = () => { + csvImportModal.style.display = 'flex'; + if (csvImportTurmaSelect) { + csvImportTurmaSelect.innerHTML = ''; + (currentTurmas || []).forEach(t => { + const opt = document.createElement('option'); + opt.value = t.id; + opt.textContent = t.nome; + csvImportTurmaSelect.appendChild(opt); + }); + } + }; + + if (csvFileInput) { + csvFileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (evt) => { + if (csvTextContent) csvTextContent.value = evt.target.result; + }; + reader.readAsText(file, 'UTF-8'); + } + }); + } + + if (btnProcessCsvImport) { + btnProcessCsvImport.addEventListener('click', async () => { + const turmaId = csvImportTurmaSelect ? csvImportTurmaSelect.value : null; + const csv = csvTextContent ? csvTextContent.value.trim() : ''; + + if (!turmaId || !csv) { + alert('Por favor, selecione a turma e forneça o conteúdo CSV.'); + return; + } + + btnProcessCsvImport.disabled = true; + btnProcessCsvImport.textContent = '⏳ Importando...'; + + try { + const res = await fetch('/api/alunos/import-csv', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ turma_id: turmaId, csv_content: csv }) + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Erro na importação'); + + alert(`✅ Sucesso! ${data.count} alunos foram importados para a turma.`); + csvImportModal.style.display = 'none'; + if (typeof fetchAlunos === 'function') await fetchAlunos(); + if (typeof renderAlunosList === 'function') renderAlunosList(); + } catch (err) { + alert('Erro ao importar CSV: ' + err.message); + } finally { + btnProcessCsvImport.disabled = false; + btnProcessCsvImport.textContent = '📥 Importar Alunos'; + } + }); + } + + // ------------------------------------------------------------------------ + // 3. PROTOCOLO DE CRISES & MEDIAÇÃO COMPORTAMENTAL + // ------------------------------------------------------------------------ + const btnProtocoloCrise = document.getElementById('btnProtocoloCrise'); + const criseModal = document.getElementById('criseModal'); + const btnCloseCriseModal = document.getElementById('btnCloseCriseModal'); + const btnOpenCrisesHistory = document.getElementById('btnOpenCrisesHistory'); + const criseHistoryModal = document.getElementById('criseHistoryModal'); + const btnCloseCriseHistory = document.getElementById('btnCloseCriseHistory'); + const criseTurmaSelect = document.getElementById('criseTurmaSelect'); + const criseAlunoInput = document.getElementById('criseAlunoInput'); + const criseTipoSelect = document.getElementById('criseTipoSelect'); + const criseIntensidadeSelect = document.getElementById('criseIntensidadeSelect'); + const criseDescricaoInput = document.getElementById('criseDescricaoInput'); + const criseMedidasInput = document.getElementById('criseMedidasInput'); + const btnSalvarCrise = document.getElementById('btnSalvarCrise'); + const criseWhatsappOutput = document.getElementById('criseWhatsappOutput'); + const btnCopyCriseWhatsapp = document.getElementById('btnCopyCriseWhatsapp'); + const btnSendWhatsappDirect = document.getElementById('btnSendWhatsappDirect'); + const criseHistoryList = document.getElementById('criseHistoryList'); + + function populateCriseTurmas() { + if (!criseTurmaSelect) return; + criseTurmaSelect.innerHTML = ''; + (currentTurmas || []).forEach(t => { + const opt = document.createElement('option'); + opt.value = t.id; + opt.textContent = t.nome; + criseTurmaSelect.appendChild(opt); + }); + } + + if (btnProtocoloCrise) { + btnProtocoloCrise.addEventListener('click', () => { + criseModal.style.display = 'flex'; + populateCriseTurmas(); + }); + } + if (btnCloseCriseModal) btnCloseCriseModal.addEventListener('click', () => criseModal.style.display = 'none'); + if (criseModal) criseModal.addEventListener('click', (e) => { if (e.target === criseModal) criseModal.style.display = 'none'; }); + + if (btnOpenCrisesHistory) { + btnOpenCrisesHistory.addEventListener('click', async () => { + criseHistoryModal.style.display = 'flex'; + if (!criseHistoryList) return; + criseHistoryList.innerHTML = '

Carregando...

'; + try { + const res = await fetch('/api/crises/list'); + const data = await res.json(); + if (data.length === 0) { + criseHistoryList.innerHTML = '

Nenhuma ocorrência registrada.

'; + return; + } + criseHistoryList.innerHTML = data.map(c => ` +
+
+ ${escapeHtml(c.aluno_nome)} • ${escapeHtml(c.tipo_evento)} + ${new Date(c.data_hora).toLocaleString('pt-BR')} +
+

${escapeHtml(c.descricao)}

+
Medidas: ${escapeHtml(c.medidas_tomadas)}
+ +
+ `).join(''); + } catch (e) { + criseHistoryList.innerHTML = `

Erro ao carregar: ${e.message}

`; + } + }); + } + + window.deleteCriseRecord = async (id) => { + if (confirm('Deseja excluir este registro de mediação?')) { + await fetch(`/api/crises/${id}`, { method: 'DELETE' }); + if (btnOpenCrisesHistory) btnOpenCrisesHistory.click(); + } + }; + + if (btnCloseCriseHistory) btnCloseCriseHistory.addEventListener('click', () => criseHistoryModal.style.display = 'none'); + + if (btnSalvarCrise) { + btnSalvarCrise.addEventListener('click', async () => { + const alunoNome = criseAlunoInput ? criseAlunoInput.value.trim() : ''; + const tipo = criseTipoSelect ? criseTipoSelect.value : ''; + const intensidade = criseIntensidadeSelect ? criseIntensidadeSelect.value : 'Moderada'; + const desc = criseDescricaoInput ? criseDescricaoInput.value.trim() : ''; + const medidas = criseMedidasInput ? criseMedidasInput.value.trim() : ''; + const turmaId = criseTurmaSelect ? criseTurmaSelect.value : null; + + if (!alunoNome || !desc) { + alert('Por favor, informe o nome da criança e a descrição do ocorrido.'); + return; + } + + btnSalvarCrise.disabled = true; + btnSalvarCrise.innerHTML = '⏳ Gerando Registro e Mensagem...'; + + try { + const res = await fetch('/api/crises/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + aluno_nome: alunoNome, + turma_id: turmaId, + tipo_evento: tipo, + intensidade: intensidade, + descricao: desc, + medidas_tomadas: medidas + }) + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Falha ao registrar ocorrência'); + + if (criseWhatsappOutput) criseWhatsappOutput.textContent = data.mensagemWhatsapp || ''; + if (btnSendWhatsappDirect) { + btnSendWhatsappDirect.href = `https://api.whatsapp.com/send?text=${encodeURIComponent(data.mensagemWhatsapp || '')}`; + btnSendWhatsappDirect.style.display = 'block'; + } + alert('✅ Protocolo registrado com sucesso! Mensagem para os pais gerada.'); + } catch (err) { + alert('Erro ao registrar crise: ' + err.message); + } finally { + btnSalvarCrise.disabled = false; + btnSalvarCrise.innerHTML = '🛡️ Registrar & Gerar Mensagem WhatsApp'; + } + }); + } + + if (btnCopyCriseWhatsapp) { + btnCopyCriseWhatsapp.addEventListener('click', () => { + if (criseWhatsappOutput && criseWhatsappOutput.textContent) { + navigator.clipboard.writeText(criseWhatsappOutput.textContent); + alert('Mensagem copiada para a área de transferência!'); + } + }); + } + + // ------------------------------------------------------------------------ + // 4. ATAS DE REUNIÃO & CONSELHO DE CLASSE + // ------------------------------------------------------------------------ + const btnAtasReuniao = document.getElementById('btnAtasReuniao'); + const ataModal = document.getElementById('ataModal'); + const btnCloseAtaModal = document.getElementById('btnCloseAtaModal'); + const btnOpenAtasHistory = document.getElementById('btnOpenAtasHistory'); + const atasHistoryModal = document.getElementById('atasHistoryModal'); + const btnCloseAtasHistory = document.getElementById('btnCloseAtasHistory'); + const ataTurmaSelect = document.getElementById('ataTurmaSelect'); + const ataTipoSelect = document.getElementById('ataTipoSelect'); + const ataTituloInput = document.getElementById('ataTituloInput'); + const ataParticipantesInput = document.getElementById('ataParticipantesInput'); + const ataPautaInput = document.getElementById('ataPautaInput'); + const ataDiscussoesInput = document.getElementById('ataDiscussoesInput'); + const ataDeliberacoesInput = document.getElementById('ataDeliberacoesInput'); + const btnGerarAta = document.getElementById('btnGerarAta'); + const ataOutputText = document.getElementById('ataOutputText'); + const btnCopyAta = document.getElementById('btnCopyAta'); + const btnPrintAtaPdf = document.getElementById('btnPrintAtaPdf'); + const atasHistoryList = document.getElementById('atasHistoryList'); + + function populateAtaTurmas() { + if (!ataTurmaSelect) return; + ataTurmaSelect.innerHTML = ''; + (currentTurmas || []).forEach(t => { + const opt = document.createElement('option'); + opt.value = t.id; + opt.textContent = t.nome; + ataTurmaSelect.appendChild(opt); + }); + } + + if (btnAtasReuniao) { + btnAtasReuniao.addEventListener('click', () => { + ataModal.style.display = 'flex'; + populateAtaTurmas(); + }); + } + if (btnCloseAtaModal) btnCloseAtaModal.addEventListener('click', () => ataModal.style.display = 'none'); + if (ataModal) ataModal.addEventListener('click', (e) => { if (e.target === ataModal) ataModal.style.display = 'none'; }); + + if (btnGerarAta) { + btnGerarAta.addEventListener('click', async () => { + const titulo = ataTituloInput ? ataTituloInput.value.trim() : ''; + const tipo = ataTipoSelect ? ataTipoSelect.value : ''; + const discussoes = ataDiscussoesInput ? ataDiscussoesInput.value.trim() : ''; + const pauta = ataPautaInput ? ataPautaInput.value.trim() : ''; + const part = ataParticipantesInput ? ataParticipantesInput.value.split(',').map(p => p.trim()) : []; + const delib = ataDeliberacoesInput ? ataDeliberacoesInput.value.trim() : ''; + const turmaId = ataTurmaSelect ? ataTurmaSelect.value : null; + + if (!titulo || !discussoes) { + alert('Por favor, informe o título da reunião e os tópicos discutidos.'); + return; + } + + btnGerarAta.disabled = true; + btnGerarAta.innerHTML = '⏳ Redigindo Ata Oficial com IA...'; + + try { + const res = await fetch('/api/atas/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tipo_reuniao: tipo, + titulo: titulo, + turma_id: turmaId, + pauta: pauta, + participantes: part, + discussoes: discussoes, + deliberacoes: delib + }) + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Falha ao redigir ata'); + + if (ataOutputText) ataOutputText.textContent = data.textoAta || ''; + alert('✅ Ata formal redigida com sucesso!'); + } catch (err) { + alert('Erro ao gerar ata: ' + err.message); + } finally { + btnGerarAta.disabled = false; + btnGerarAta.innerHTML = '📋 Gerar Ata Oficial Formatada'; + } + }); + } + + if (btnCopyAta) { + btnCopyAta.addEventListener('click', () => { + if (ataOutputText && ataOutputText.textContent) { + navigator.clipboard.writeText(ataOutputText.textContent); + alert('Ata copiada para a área de transferência!'); + } + }); + } + + if (btnPrintAtaPdf) { + btnPrintAtaPdf.addEventListener('click', () => { + if (!ataOutputText || !ataOutputText.textContent) return; + const win = window.open('', '_blank'); + win.document.write(` + + + + + Ata de Reunião - Pedagog + + + + +

ATA DE REUNIÃO ESCOLAR

+
${escapeHtml(ataOutputText.textContent).replace(/\n/g, '
')}
+ + + `); + win.document.close(); + }); + } + + if (btnOpenAtasHistory) { + btnOpenAtasHistory.addEventListener('click', async () => { + atasHistoryModal.style.display = 'flex'; + if (!atasHistoryList) return; + atasHistoryList.innerHTML = '

Carregando atas...

'; + try { + const res = await fetch('/api/atas/list'); + const data = await res.json(); + if (data.length === 0) { + atasHistoryList.innerHTML = '

Nenhuma ata salva.

'; + return; + } + atasHistoryList.innerHTML = data.map(a => ` +
+
+ ${escapeHtml(a.titulo)} +
${escapeHtml(a.tipo_reuniao)} • ${new Date(a.data_reuniao).toLocaleDateString('pt-BR')}
+
+ +
+ `).join(''); + } catch (e) { + atasHistoryList.innerHTML = `

Erro: ${e.message}

`; + } + }); + } + + window.deleteAtaRecord = async (id) => { + if (confirm('Deseja excluir esta ata?')) { + await fetch(`/api/atas/${id}`, { method: 'DELETE' }); + if (btnOpenAtasHistory) btnOpenAtasHistory.click(); + } + }; + + if (btnCloseAtasHistory) btnCloseAtasHistory.addEventListener('click', () => atasHistoryModal.style.display = 'none'); + + // ------------------------------------------------------------------------ + // 5. STICKERS GAMIFICADOS & CARTELA A4 DE IMPRESSÃO + // ------------------------------------------------------------------------ + const btnStickersConquistas = document.getElementById('btnStickersConquistas'); + const stickerModal = document.getElementById('stickerModal'); + const btnCloseStickerModal = document.getElementById('btnCloseStickerModal'); + const stickerTurmaSelect = document.getElementById('stickerTurmaSelect'); + const stickerAlunoInput = document.getElementById('stickerAlunoInput'); + const stickerCategoriaSelect = document.getElementById('stickerCategoriaSelect'); + const stickerTituloInput = document.getElementById('stickerTituloInput'); + const stickerDescricaoInput = document.getElementById('stickerDescricaoInput'); + const btnEmitirSticker = document.getElementById('btnEmitirSticker'); + const stickersListGrid = document.getElementById('stickersListGrid'); + const btnPrintStickerSheet = document.getElementById('btnPrintStickerSheet'); + + function populateStickerTurmas() { + if (!stickerTurmaSelect) return; + stickerTurmaSelect.innerHTML = ''; + (currentTurmas || []).forEach(t => { + const opt = document.createElement('option'); + opt.value = t.id; + opt.textContent = t.nome; + stickerTurmaSelect.appendChild(opt); + }); + } + + async function loadStickersGrid() { + if (!stickersListGrid) return; + stickersListGrid.innerHTML = '

Carregando...

'; + try { + const res = await fetch('/api/stickers/list'); + const data = await res.json(); + if (data.length === 0) { + stickersListGrid.innerHTML = '

Nenhum sticker emitido ainda.

'; + return; + } + stickersListGrid.innerHTML = data.map(s => ` +
+ ${s.icone || '🌟'} + ${escapeHtml(s.titulo)} + ${escapeHtml(s.aluno_nome)} + ${s.qr_code_url ? `` : ''} + +
+ `).join(''); + } catch (e) { + stickersListGrid.innerHTML = `

Erro: ${e.message}

`; + } + } + + if (btnStickersConquistas) { + btnStickersConquistas.addEventListener('click', () => { + stickerModal.style.display = 'flex'; + populateStickerTurmas(); + loadStickersGrid(); + }); + } + if (btnCloseStickerModal) btnCloseStickerModal.addEventListener('click', () => stickerModal.style.display = 'none'); + if (stickerModal) stickerModal.addEventListener('click', (e) => { if (e.target === stickerModal) stickerModal.style.display = 'none'; }); + + window.deleteStickerRecord = async (id) => { + if (confirm('Deseja excluir este sticker?')) { + await fetch(`/api/stickers/${id}`, { method: 'DELETE' }); + loadStickersGrid(); + } + }; + + if (btnEmitirSticker) { + btnEmitirSticker.addEventListener('click', async () => { + const alunoNome = stickerAlunoInput ? stickerAlunoInput.value.trim() : ''; + const titulo = stickerTituloInput ? stickerTituloInput.value.trim() : ''; + const cat = stickerCategoriaSelect ? stickerCategoriaSelect.value : 'Cooperação & Gentileza'; + const desc = stickerDescricaoInput ? stickerDescricaoInput.value.trim() : ''; + const turmaId = stickerTurmaSelect ? stickerTurmaSelect.value : null; + + if (!alunoNome || !titulo || !desc) { + alert('Por favor, informe a criança, o título da conquista e a descrição.'); + return; + } + + let icone = '🌟'; + if (cat.includes('Cooperação')) icone = '🤝'; + else if (cat.includes('Curiosidade')) icone = '🔬'; + else if (cat.includes('Autonomia')) icone = '🌱'; + else if (cat.includes('Artística')) icone = '🎨'; + else if (cat.includes('Coragem')) icone = '⭐'; + else if (cat.includes('Roda')) icone = '🗣️'; + + try { + const res = await fetch('/api/stickers/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + aluno_nome: alunoNome, + turma_id: turmaId, + titulo: titulo, + categoria: cat, + icone: icone, + descricao: desc + }) + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Falha ao emitir sticker'); + alert('✅ Conquista registrada com sucesso!'); + loadStickersGrid(); + } catch (err) { + alert('Erro: ' + err.message); + } + }); + } + + // Imprimir Cartela A4 com Stickers Recortáveis + if (btnPrintStickerSheet) { + btnPrintStickerSheet.addEventListener('click', async () => { + try { + const res = await fetch('/api/stickers/list'); + const stickers = await res.json(); + if (stickers.length === 0) { + alert('Emita ao menos um sticker antes de imprimir a cartela.'); + return; + } + + const win = window.open('', '_blank'); + let stickersHtml = ''; + stickers.forEach(s => { + stickersHtml += ` +
+ ${s.icone || '🌟'} + ${escapeHtml(s.titulo)} +
${escapeHtml(s.aluno_nome)}
+

${escapeHtml(s.descricao)}

+ ${s.qr_code_url ? `` : ''} +
Pedagog • Conquista Escolar
+
+ `; + }); + + win.document.write(` + + + + + Cartela de Adesivos - Pedagog + + + + +

🌟 Cartela de Conquistas da Turma

+
+ ${stickersHtml} +
+ + + `); + win.document.close(); + } catch (e) { + alert('Erro ao imprimir cartela: ' + e.message); + } + }); + } + +}); + diff --git a/public/index.html b/public/index.html index e16c638..735c9c5 100644 --- a/public/index.html +++ b/public/index.html @@ -73,56 +73,64 @@ - + - + - + @@ -2154,7 +2162,7 @@
Thumbnail
-

Título do Vídeo

+

Título do Vídeo

Canal
@@ -2664,26 +2672,353 @@
1 / 5 - -
-
- -
-
- Quadrinho em Apresentação -
- Falas do quadrinho aparecerão aqui... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server.js b/server.js index 83b753c..d4d2b7e 100644 --- a/server.js +++ b/server.js @@ -3157,6 +3157,100 @@ async function initDatabase() { tags TEXT[], -- array de tags para busca created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); + + -- Extensões para Alunos + ALTER TABLE escola.alunos ADD COLUMN IF NOT EXISTS observacoes_saude TEXT; + ALTER TABLE escola.alunos ADD COLUMN IF NOT EXISTS contato_emergencia VARCHAR(100); + ALTER TABLE escola.alunos ADD COLUMN IF NOT EXISTS documentos JSONB; + ALTER TABLE escola.alunos ADD COLUMN IF NOT EXISTS foto_url TEXT; + + -- Extensões para Observações + ALTER TABLE escola.observacoes ADD COLUMN IF NOT EXISTS tags_bncc JSONB; + ALTER TABLE escola.observacoes ADD COLUMN IF NOT EXISTS duracao_segundos INTEGER; + ALTER TABLE escola.observacoes ADD COLUMN IF NOT EXISTS timestamps JSONB; + ALTER TABLE escola.observacoes ADD COLUMN IF NOT EXISTS sentimento VARCHAR(50); + ALTER TABLE escola.observacoes ADD COLUMN IF NOT EXISTS aluno_id UUID; + + -- Extensões para Mind Lab + ALTER TABLE escola.mindlab ADD COLUMN IF NOT EXISTS bimestre INTEGER; + ALTER TABLE escola.mindlab ADD COLUMN IF NOT EXISTS semana INTEGER; + ALTER TABLE escola.mindlab ADD COLUMN IF NOT EXISTS sequencia_didatica JSONB; + ALTER TABLE escola.mindlab ADD COLUMN IF NOT EXISTS texto_htpc TEXT; + ALTER TABLE escola.mindlab ADD COLUMN IF NOT EXISTS bncc_habilidades JSONB; + + -- Tabela de Protocolo de Crises & Mediação Comportamental + CREATE TABLE IF NOT EXISTS escola.crises ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', + turma_id UUID, + aluno_id UUID, + aluno_nome VARCHAR(255) NOT NULL, + data_hora TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + tipo_evento VARCHAR(100) NOT NULL, -- 'Desregulação Sensorial', 'Conflito Físico/Mordida', 'Queda/Pequeno Acidente', 'Choro Intenso/Adaptação', 'Recusa Alimentar', 'Outro' + intensidade VARCHAR(50) DEFAULT 'Moderada', -- 'Leve', 'Moderada', 'Alta' + gatilho TEXT, + descricao TEXT NOT NULL, + checklist_intervencao JSONB, + medidas_tomadas TEXT NOT NULL, + orientacoes_equipe TEXT, + mensagem_whatsapp TEXT, + status VARCHAR(50) DEFAULT 'Resolvido', -- 'Em Acompanhamento', 'Resolvido', 'Notificado à Coordenação' + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + + -- Tabela de Atas de Reunião & Conselho de Classe + CREATE TABLE IF NOT EXISTS escola.atas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', + tipo_reuniao VARCHAR(100) NOT NULL, -- 'Reunião de Pais', 'Conselho de Classe / Avaliação', 'HTPC / Formação', 'Alinhamento Pedagógico' + titulo VARCHAR(255) NOT NULL, + turma_id UUID, + turma_nome VARCHAR(255), + data_reuniao DATE DEFAULT CURRENT_DATE, + participantes TEXT[], + pauta TEXT NOT NULL, + discussoes TEXT NOT NULL, + deliberacoes TEXT NOT NULL, + proximos_passos TEXT, + texto_ata TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + + -- Tabela de Stickers Gamificados & Conquistas + CREATE TABLE IF NOT EXISTS escola.stickers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', + aluno_id UUID, + aluno_nome VARCHAR(255) NOT NULL, + turma_id UUID, + titulo VARCHAR(255) NOT NULL, + categoria VARCHAR(100) NOT NULL, -- 'Cooperação & Gentileza', 'Curiosidade & Ciência', 'Autonomia & Cuidado', 'Expressão Artística', 'Superação & Desafio' + icone VARCHAR(50) NOT NULL DEFAULT '🌟', + descricao TEXT NOT NULL, + observacao_id UUID, + qr_code_url TEXT, + data_conquista DATE DEFAULT CURRENT_DATE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + + -- Tabela de Relatórios Semestrais & Batch + CREATE TABLE IF NOT EXISTS escola.relatorios ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', + turma_id UUID, + aluno_id UUID, + aluno_nome VARCHAR(255) NOT NULL, + ano_letivo INTEGER DEFAULT 2026, + semestre INTEGER DEFAULT 1, -- 1 ou 2 + faixa_etaria VARCHAR(100), + conteudo_relatorio TEXT NOT NULL, + campos_bncc JSONB, + comparacao_anterior TEXT, + versao_anterior_id UUID, + lote_id UUID, + status VARCHAR(50) DEFAULT 'Finalizado', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); `); console.log('[Database Init] Atualizando assets padrão na biblioteca (Design Canvas 2.0)...'); @@ -5746,6 +5840,671 @@ app.delete('/api/alunos/:id', requireAuth, async (req, res) => { } }); +// ============================================================ +// ROTAS DE IMPORTAÇÃO CSV, TIMELINE E CONSELHO DE ALUNOS +// ============================================================ +app.post('/api/alunos/import-csv', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { turma_id, csv_content } = req.body; + + if (!turma_id || !csv_content) { + return res.status(400).json({ error: 'Turma e conteúdo CSV são obrigatórios.' }); + } + + try { + const lines = csv_content.split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0); + if (lines.length < 2) { + return res.status(400).json({ error: 'O arquivo CSV precisa ter cabeçalho e ao menos uma linha de dados.' }); + } + + const header = lines[0].toLowerCase().split(/[,;]/).map(h => h.trim().replace(/^["']|["']$/g, '')); + const nameIdx = header.findIndex(h => h.includes('nome') || h.includes('aluno') || h.includes('criança')); + const nascIdx = header.findIndex(h => h.includes('nasc') || h.includes('data') || h.includes('aniversario') || h.includes('birth')); + const apelidoIdx = header.findIndex(h => h.includes('apelido') || h.includes('chamada') || h.includes('nickname')); + const paisIdx = header.findIndex(h => h.includes('pai') || h.includes('mãe') || h.includes('mae') || h.includes('responsav')); + const saudeIdx = header.findIndex(h => h.includes('saude') || h.includes('saúde') || h.includes('alergia') || h.includes('observa')); + const telIdx = header.findIndex(h => h.includes('contato') || h.includes('tel') || h.includes('fone') || h.includes('cel')); + + if (nameIdx === -1) { + return res.status(400).json({ error: 'Coluna de "Nome" não encontrada no CSV.' }); + } + + const inserted = []; + for (let i = 1; i < lines.length; i++) { + const cols = lines[i].split(/[,;]/).map(c => c.trim().replace(/^["']|["']$/g, '')); + const nome = cols[nameIdx]; + if (!nome) continue; + + let dataNasc = null; + if (nascIdx !== -1 && cols[nascIdx]) { + const rawDate = cols[nascIdx]; + if (/^\d{4}-\d{2}-\d{2}$/.test(rawDate)) { + dataNasc = rawDate; + } else if (/^\d{2}\/\d{2}\/\d{4}$/.test(rawDate)) { + const parts = rawDate.split('/'); + dataNasc = `${parts[2]}-${parts[1]}-${parts[0]}`; + } + } + + const apelido = apelidoIdx !== -1 ? cols[apelidoIdx] : null; + const pais = paisIdx !== -1 ? cols[paisIdx] : null; + const saude = saudeIdx !== -1 ? cols[saudeIdx] : null; + const tel = telIdx !== -1 ? cols[telIdx] : null; + + const dbRes = await dbPool.query( + `INSERT INTO escola.alunos (usuario_id, turma_id, nome, apelido, data_nasc, pais, observacoes_saude, contato_emergencia) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`, + [usuarioId, turma_id, nome, apelido, dataNasc, pais, saude, tel] + ); + inserted.push(dbRes.rows[0]); + } + + res.json({ success: true, count: inserted.length, alunos: inserted }); + } catch (err) { + console.error('Erro import CSV:', err); + res.status(500).json({ error: 'Erro ao importar CSV: ' + err.message }); + } +}); + +// Timeline Completa do Aluno +app.get('/api/alunos/:id/timeline', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { id } = req.params; + + try { + const alunoRes = await dbPool.query("SELECT * FROM escola.alunos WHERE id = $1 AND usuario_id = $2", [id, usuarioId]); + if (alunoRes.rows.length === 0) return res.status(404).json({ error: 'Aluno não encontrado' }); + const aluno = alunoRes.rows[0]; + + // Observações + const obsRes = await dbPool.query( + "SELECT id, filename, date, time, timestamp, tags, report, tags_bncc, duracao_segundos FROM escola.observacoes WHERE (aluno_id = $1 OR criancas ILIKE $2 OR report ILIKE $2) ORDER BY timestamp DESC LIMIT 30", + [id, `%${aluno.nome}%`] + ); + + // Histórias + const histRes = await dbPool.query( + "SELECT id, titulo, tema, faixa_etaria, created_at FROM escola.historias WHERE (personagem_principal ILIKE $1 OR tema ILIKE $1) AND usuario_id = $2 ORDER BY created_at DESC LIMIT 10", + [`%${aluno.nome}%`, usuarioId] + ); + + // Conquistas / Stickers + const stickRes = await dbPool.query( + "SELECT * FROM escola.stickers WHERE aluno_id = $1 AND usuario_id = $2 ORDER BY created_at DESC", + [id, usuarioId] + ); + + // Crises / Intervenções + const crisesRes = await dbPool.query( + "SELECT * FROM escola.crises WHERE (aluno_id = $1 OR aluno_nome ILIKE $2) AND usuario_id = $3 ORDER BY created_at DESC", + [id, `%${aluno.nome}%`, usuarioId] + ); + + // Relatórios Semestrais + const relRes = await dbPool.query( + "SELECT id, ano_letivo, semestre, faixa_etaria, conteudo_relatorio, created_at FROM escola.relatorios WHERE aluno_id = $1 AND usuario_id = $2 ORDER BY ano_letivo DESC, semestre DESC", + [id, usuarioId] + ); + + res.json({ + aluno, + timeline: { + observacoes: obsRes.rows, + historias: histRes.rows, + stickers: stickRes.rows, + crises: crisesRes.rows, + relatorios: relRes.rows + } + }); + } catch (err) { + res.status(500).json({ error: 'Erro ao carregar timeline: ' + err.message }); + } +}); + +// Ficha Resumo para Conselho de Classe +app.get('/api/alunos/:id/conselho-ficha', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { id } = req.params; + + try { + const alunoRes = await dbPool.query( + `SELECT a.*, t.nome as turma_nome FROM escola.alunos a LEFT JOIN escola.turmas t ON a.turma_id = t.id WHERE a.id = $1 AND a.usuario_id = $2`, + [id, usuarioId] + ); + if (alunoRes.rows.length === 0) return res.status(404).json({ error: 'Aluno não encontrado' }); + const aluno = alunoRes.rows[0]; + + const obsCount = await dbPool.query( + "SELECT COUNT(*) FROM escola.observacoes WHERE (aluno_id = $1 OR criancas ILIKE $2)", + [id, `%${aluno.nome}%`] + ); + + const stickers = await dbPool.query( + "SELECT titulo, categoria, icone FROM escola.stickers WHERE aluno_id = $1 AND usuario_id = $2", + [id, usuarioId] + ); + + res.json({ + aluno, + totalObservacoes: parseInt(obsCount.rows[0]?.count || 0), + conquistas: stickers.rows, + dataFicha: new Date().toLocaleDateString('pt-BR') + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// ============================================================ +// TAGGING AUTOMÁTICO BNCC & IA PARA OBSERVAÇÕES POR VOZ +// ============================================================ +app.post('/api/observacoes/analyze-tags', requireAuth, async (req, res) => { + const { textoObservacao } = req.body; + if (!textoObservacao) return res.status(400).json({ error: 'Texto da observação é obrigatório' }); + + try { + const prompt = `Você é uma especialista em Educação Infantil e BNCC brasileira da PedagogIA. +Analise a seguinte observação pedagógica transcrita por voz e extraia tags estruturadas: +"${textoObservacao}" + +Retorne estritamente um JSON no seguinte formato: +{ + "camposBNCC": ["O eu, o outro e o nós (EI03EO01)", "Corpo, gestos e movimentos (EI03CG02)"], + "habilidadesChave": ["Autonomia", "Cooperação", "Motricidade fina"], + "sentimentoGeral": "Positivo e Acolhedor", + "sugestaoDesdobramento": "Propor nova atividade em pequenos grupos para fortalecer a partilha.", + "resumoCurto": "Criança demonstrou iniciativa e colaboração com os colegas durante o brincar livre." +}`; + + let jsonText = ''; + try { + const r = await callMinimax({ + system: "Você é especialista em BNCC e Educação Infantil. Responda em JSON.", + messages: [{ role: 'user', content: prompt }], + temperature: 0.3 + }); + jsonText = r.text; + } catch (e) { + if (process.env.GOOGLE_AI_API_KEY) { + const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash'}:generateContent?key=${process.env.GOOGLE_AI_API_KEY}`; + const response = await fetch(geminiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) + }); + if (response.ok) { + const data = await response.json(); + jsonText = data.candidates?.[0]?.content?.parts?.[0]?.text || ''; + } + } + } + + let parsed = {}; + try { + parsed = JSON.parse(jsonText.replace(/```json|```/g, '').trim()); + } catch (err) { + parsed = { + camposBNCC: ["O eu, o outro e o nós", "Corpo, gestos e movimentos"], + habilidadesChave: ["Interação", "Linguagem oral"], + sentimentoGeral: "Desenvolvimento ativo", + sugestaoDesdobramento: "Dar continuidade no planejamento semanal.", + resumoCurto: textoObservacao.substring(0, 100) + '...' + }; + } + + res.json(parsed); + } catch (err) { + res.status(500).json({ error: 'Erro ao analisar tags: ' + err.message }); + } +}); + +// ============================================================ +// ROTAS DE PROTOCOLO DE CRISES & MEDIAÇÃO COMPORTAMENTAL +// ============================================================ +app.post('/api/crises/register', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { turma_id, aluno_id, aluno_nome, tipo_evento, intensidade, gatilho, descricao, checklist_intervencao, medidas_tomadas, orientacoes_equipe } = req.body; + + if (!aluno_nome || !tipo_evento || !descricao) { + return res.status(400).json({ error: 'Nome do aluno, tipo de evento e descrição são obrigatórios.' }); + } + + try { + // Gerar mensagem acolhedora para WhatsApp dos pais via IA + const agentConfig = readAgentConfig(); + const promptMsg = `Você é a ${agentConfig.agentName || 'PedagogIA'}, redatora pedagógica da Professora Camila. +Gere uma mensagem acolhedora, respeitosa, serena e transparente para ser enviada aos pais/responsáveis da criança "${aluno_nome}" informando sobre o seguinte ocorrido na escola: +- Tipo de Ocorrência: ${tipo_evento} +- Detalhes: ${descricao} +- Medidas e Acolhimento feito pela professora: ${medidas_tomadas || 'Criança acolhida com carinho e conforto.'} + +REGRAS: +- Tom extremamente afetuoso, profissional, humanizado e acolhedor (não alarmista). +- Informar que a criança já está calma e bem cuidada. +- Colocar a escola à disposição. +- Máximo 3 a 4 parágrafos curtos.`; + + let msgWhatsapp = ''; + try { + const r = await callMinimax({ + system: "Você é uma professora e coordenadora de educação infantil acolhedora.", + messages: [{ role: 'user', content: promptMsg }], + temperature: 0.5 + }); + msgWhatsapp = r.text; + } catch (e) { + msgWhatsapp = `Olá! Passando para contar que hoje o(a) ${aluno_nome} teve um momentinho de ${tipo_evento.toLowerCase()}, mas foi prontamente acolhido(a) com muito carinho pela nossa equipe e já está calmo(a) e brincando com os amigos. Seguimos com todo cuidado e à disposição! Com carinho, Professora Camila.`; + } + + const dbRes = await dbPool.query( + `INSERT INTO escola.crises ( + usuario_id, turma_id, aluno_id, aluno_nome, tipo_evento, intensidade, gatilho, + descricao, checklist_intervencao, medidas_tomadas, orientacoes_equipe, mensagem_whatsapp + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`, + [ + usuarioId, turma_id || null, aluno_id || null, aluno_nome, tipo_evento, intensidade || 'Moderada', + gatilho || '', descricao, JSON.stringify(checklist_intervencao || []), medidas_tomadas || '', + orientacoes_equipe || '', msgWhatsapp + ] + ); + + res.json({ success: true, crise: dbRes.rows[0], mensagemWhatsapp: msgWhatsapp }); + } catch (err) { + console.error('Erro crise register:', err); + res.status(500).json({ error: 'Erro ao registrar protocolo de crise: ' + err.message }); + } +}); + +app.get('/api/crises/list', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { turma_id } = req.query; + try { + let q = "SELECT * FROM escola.crises WHERE usuario_id = $1"; + let p = [usuarioId]; + if (turma_id) { q += " AND turma_id = $2"; p.push(turma_id); } + q += " ORDER BY data_hora DESC LIMIT 50"; + const dbRes = await dbPool.query(q, p); + res.json(dbRes.rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.delete('/api/crises/:id', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + try { + await dbPool.query("DELETE FROM escola.crises WHERE id = $1 AND usuario_id = $2", [req.params.id, usuarioId]); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// ============================================================ +// ROTAS DE ATAS DE REUNIÃO & CONSELHO DE CLASSE +// ============================================================ +app.post('/api/atas/generate', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { tipo_reuniao, titulo, turma_id, turma_nome, data_reuniao, participantes, pauta, discussoes, deliberacoes, proximos_passos } = req.body; + + if (!titulo || !tipo_reuniao || !discussoes) { + return res.status(400).json({ error: 'Título, Tipo de Reunião e Discussões são obrigatórios.' }); + } + + try { + const promptAta = `Você é uma secretária pedagógica e especialista em documentação escolar brasileira. +Gere uma ATA FORMAL DE REUNIÃO ESCOLAR completa, bem redigida em linguagem culta, com parágrafos encadeados, estruturada da seguinte forma: +- Cabeçalho Oficial (Data, Horário, Local, Participantes, Pauta) +- Desenvolvimento e Discussões +- Deliberações e Decisões Tomadas +- Próximos Passos e Encaminhamentos +- Fecho formal e espaço para assinaturas + +DADOS DA REUNIÃO: +- Tipo: ${tipo_reuniao} +- Título: ${titulo} +- Turma / Nível: ${turma_nome || 'Geral'} +- Data: ${data_reuniao || new Date().toLocaleDateString('pt-BR')} +- Participantes: ${(participantes || []).join(', ') || 'Corpo docente e equipe pedagógica'} +- Pauta: ${pauta || 'Avaliação e desenvolvimento da turma'} +- Principais Discussões: ${discussoes} +- Deliberações e Acordos: ${deliberacoes || 'Registrado em concordância com os presentes.'} +- Próximos Passos: ${proximos_passos || 'Acompanhamento contínuo.'}`; + + let textoAta = ''; + try { + const r = await callMinimax({ + system: "Você é uma secretária e redatora de documentos e atas escolares no Brasil.", + messages: [{ role: 'user', content: promptAta }], + temperature: 0.4 + }); + textoAta = r.text; + } catch (e) { + if (process.env.GOOGLE_AI_API_KEY) { + const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash'}:generateContent?key=${process.env.GOOGLE_AI_API_KEY}`; + const response = await fetch(geminiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ contents: [{ parts: [{ text: promptAta }] }] }) + }); + if (response.ok) { + const data = await response.json(); + textoAta = data.candidates?.[0]?.content?.parts?.[0]?.text || ''; + } + } + } + + if (!textoAta) { + textoAta = `ATA DE ${tipo_reuniao.toUpperCase()}\nData: ${data_reuniao}\nTurma: ${turma_nome}\nPauta: ${pauta}\nDiscussões: ${discussoes}\nDeliberações: ${deliberacoes}`; + } + + const dbRes = await dbPool.query( + `INSERT INTO escola.atas ( + usuario_id, tipo_reuniao, titulo, turma_id, turma_nome, data_reuniao, + participantes, pauta, discussoes, deliberacoes, proximos_passos, texto_ata + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`, + [ + usuarioId, tipo_reuniao, titulo, turma_id || null, turma_nome || '', + data_reuniao || new Date(), participantes || [], pauta || '', discussoes, + deliberacoes || '', proximos_passos || '', textoAta + ] + ); + + res.json({ success: true, ata: dbRes.rows[0], textoAta }); + } catch (err) { + console.error('Erro ata generate:', err); + res.status(500).json({ error: 'Erro ao gerar ata: ' + err.message }); + } +}); + +app.get('/api/atas/list', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + try { + const dbRes = await dbPool.query("SELECT id, tipo_reuniao, titulo, turma_nome, data_reuniao, created_at FROM escola.atas WHERE usuario_id = $1 ORDER BY data_reuniao DESC", [usuarioId]); + res.json(dbRes.rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.get('/api/atas/:id', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + try { + const dbRes = await dbPool.query("SELECT * FROM escola.atas WHERE id = $1 AND usuario_id = $2", [req.params.id, usuarioId]); + if (dbRes.rows.length === 0) return res.status(404).json({ error: 'Ata não encontrada' }); + res.json(dbRes.rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.delete('/api/atas/:id', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + try { + await dbPool.query("DELETE FROM escola.atas WHERE id = $1 AND usuario_id = $2", [req.params.id, usuarioId]); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// ============================================================ +// ROTAS DE STICKERS GAMIFICADOS & CONQUISTAS +// ============================================================ +app.post('/api/stickers/create', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { aluno_id, aluno_nome, turma_id, titulo, categoria, icone, descricao, observacao_id } = req.body; + + if (!aluno_nome || !titulo || !descricao) { + return res.status(400).json({ error: 'Nome do aluno, título da conquista e descrição são obrigatórios.' }); + } + + try { + const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(`Parabéns ${aluno_nome}! Conquista: ${titulo} - ${descricao}`)}`; + + const dbRes = await dbPool.query( + `INSERT INTO escola.stickers ( + usuario_id, aluno_id, aluno_nome, turma_id, titulo, categoria, icone, descricao, observacao_id, qr_code_url + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`, + [ + usuarioId, aluno_id || null, aluno_nome, turma_id || null, titulo, + categoria || 'Cooperação & Gentileza', icone || '🌟', descricao, + observacao_id || null, qrUrl + ] + ); + + res.json({ success: true, sticker: dbRes.rows[0] }); + } catch (err) { + res.status(500).json({ error: 'Erro ao criar sticker: ' + err.message }); + } +}); + +app.get('/api/stickers/list', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { aluno_id, turma_id } = req.query; + try { + let q = "SELECT * FROM escola.stickers WHERE usuario_id = $1"; + let p = [usuarioId]; + if (aluno_id) { q += " AND aluno_id = $2"; p.push(aluno_id); } + else if (turma_id) { q += " AND turma_id = $2"; p.push(turma_id); } + q += " ORDER BY data_conquista DESC, created_at DESC LIMIT 100"; + const dbRes = await dbPool.query(q, p); + res.json(dbRes.rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.delete('/api/stickers/:id', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + try { + await dbPool.query("DELETE FROM escola.stickers WHERE id = $1 AND usuario_id = $2", [req.params.id, usuarioId]); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// ============================================================ +// ROTAS DE MIND LAB AVANÇADO (SEQUÊNCIA DIDÁTICA & HTPC) +// ============================================================ +app.post('/api/mindlab/sequencia', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { faixa_etaria, bimestre, tema_bimestre, jogos_selecionados } = req.body; + + try { + const prompt = `Você é a maior especialista em metodologia Mind Lab (Mente Inovadora) para Educação Infantil no Brasil. +Elabore uma SEQUÊNCIA DIDÁTICA BIMESTRAL DE 8 SEMANAS para a Educação Infantil: +- Faixa Etária: ${faixa_etaria || '4 a 5 anos'} +- Bimestre: ${bimestre || '1º Bimestre'} +- Tema Central: ${tema_bimestre || 'Cooperação, Raciocínio Espacial e Autocontrole'} +- Jogos Sugeridos: ${jogos_selecionados || 'Quarto, Damas Olímpicas, Tartarugas de Trânsito, Blockhead'} + +Retorne ESTRITAMENTE um JSON com o cronograma e o texto formatado para HTPC/Diário de Classe: +{ + "tituloSequencia": "Sequência Mind Lab: Despertando Estratégias e Emoções", + "bimestre": "${bimestre || '1º Bimestre'}", + "habilidadesBNCC": ["EI03EO02", "EI03ET04", "EI03CG01"], + "semanas": [ + { "semana": 1, "jogo": "Tartarugas de Trânsito", "metodo": "Semáforo (Pensar antes de agir)", "foco": "Controle inibitório e respeito aos turnos", "objetivo": "Introduzir a paciência e a observação antes de mover as peças." }, + { "semana": 2, "jogo": "Tartarugas de Trânsito", "metodo": "Aves de Rapina (Visão ampla)", "foco": "Planejamento e antecipação", "objetivo": "Analisar as opções de caminhos disponíveis." }, + { "semana": 3, "jogo": "Blockhead", "metodo": "Detetive (Busca de pistas)", "foco": "Equilíbrio e coordenação motora", "objetivo": "Identificar a estabilidade das peças antes de posicionar." }, + { "semana": 4, "jogo": "Blockhead", "metodo": "Pássaro no Galho (Cautela)", "foco": "Inteligência emocional diante do erro", "objetivo": "Aprender que a queda do bloco é oportunidade de recomeço." }, + { "semana": 5, "jogo": "Quarto", "metodo": "Espelho (Empatia e outro)", "foco": "Raciocínio lógico e classificação", "objetivo": "Classificar atributos (cor, forma, altura, topo)." }, + { "semana": 6, "jogo": "Quarto", "metodo": "Escada (Passo a passo)", "foco": "Estratégia de entrega da peça", "objetivo": "Pensar no impacto da jogada para o colega." }, + { "semana": 7, "jogo": "Desafios em Equipe", "metodo": "Roda da Amizade", "foco": "Cooperação entre duplas", "objetivo": "Solucionar desafios em parceria." }, + { "semana": 8, "jogo": "Celebração e Mostra", "metodo": "Árvore do Saber", "foco": "Autoavaliação e partilha", "objetivo": "Compartilhar com a turma os métodos mais utilizados." } + ], + "textoHTPC": "Durante este bimestre, a metodologia Mind Lab proporcionou ricas experiências de autorregulação e flexibilidade cognitiva. As crianças desenvolveram controle inibitório mediado pelo Método do Semáforo e aprimoraram a cooperação nas partidas em duplas, alinhando-se aos campos de experiência da BNCC." +}`; + + let jsonResp = ''; + try { + const r = await callMinimax({ + system: "Você é especialista na metodologia Mind Lab. Responda em JSON.", + messages: [{ role: 'user', content: prompt }], + temperature: 0.4 + }); + jsonResp = r.text; + } catch (e) { + if (process.env.GOOGLE_AI_API_KEY) { + const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash'}:generateContent?key=${process.env.GOOGLE_AI_API_KEY}`; + const response = await fetch(geminiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) + }); + if (response.ok) { + const data = await response.json(); + jsonResp = data.candidates?.[0]?.content?.parts?.[0]?.text || ''; + } + } + } + + let parsed = {}; + try { + parsed = JSON.parse(jsonResp.replace(/```json|```/g, '').trim()); + } catch (err) { + parsed = { + tituloSequencia: "Sequência Didática Mind Lab", + bimestre: bimestre || "1º Bimestre", + habilidadesBNCC: ["EI03EO02", "EI03ET04"], + semanas: [], + textoHTPC: "Planejamento bimestral de jogos de raciocínio focado em controle inibitório e cooperação." + }; + } + + res.json({ success: true, sequencia: parsed }); + } catch (err) { + res.status(500).json({ error: 'Erro ao gerar sequência Mind Lab: ' + err.message }); + } +}); + +// ============================================================ +// ROTAS DE RELATÓRIOS SEMESTRAIS (BATCH GENERATION & COMPARAÇÃO) +// ============================================================ +app.post('/api/relatorios/batch-generate', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { turma_id, ano_letivo, semestre, faixa_etaria, comparar_com_semestre1 } = req.body; + + if (!turma_id) { + return res.status(400).json({ error: 'Turma é obrigatória para geração em lote.' }); + } + + try { + const alunosRes = await dbPool.query("SELECT * FROM escola.alunos WHERE turma_id = $1 AND usuario_id = $2 ORDER BY nome ASC", [turma_id, usuarioId]); + const alunos = alunosRes.rows; + + if (alunos.length === 0) { + return res.status(400).json({ error: 'Nenhum aluno cadastrado nesta turma.' }); + } + + const loteId = require('crypto').randomUUID(); + const relatoriosGerados = []; + + for (const aluno of alunos) { + // Buscar observações do aluno + const obsRes = await dbPool.query( + "SELECT report, date, tags, tags_bncc FROM escola.observacoes WHERE (aluno_id = $1 OR criancas ILIKE $2 OR report ILIKE $2) ORDER BY timestamp DESC LIMIT 10", + [aluno.id, `%${aluno.nome}%`] + ); + + const observacoesTexto = obsRes.rows.map(o => `[${o.date}] ${o.report}`).join('\n') || 'Criança participativa, em pleno desenvolvimento nas interações cotidianas.'; + + let relatorioAnterior = ''; + if (comparar_com_semestre1 && semestre === 2) { + const relAntRes = await dbPool.query( + "SELECT conteudo_relatorio FROM escola.relatorios WHERE aluno_id = $1 AND ano_letivo = $2 AND semestre = 1 LIMIT 1", + [aluno.id, ano_letivo || 2026] + ); + if (relAntRes.rows.length > 0) relatorioAnterior = relAntRes.rows[0].conteudo_relatorio; + } + + const promptRelatorio = `Você é a redatora pedagógica de Educação Infantil da professora Camila Martella Gasparini Reifonas. +Gere um RELATÓRIO INDIVIDUAL DE DESENVOLVIMENTO INFANTIL para o aluno "${aluno.nome}": +- Faixa Etária: ${faixa_etaria || '4 a 5 anos'} +- Semestre: ${semestre || 1}º Semestre de ${ano_letivo || 2026} +- Observações Coletadas: +${observacoesTexto} +${relatorioAnterior ? `\n- RELATÓRIO DO 1º SEMESTRE (para comparação de avanços):\n${relatorioAnterior}` : ''} + +ESTRUTURA PEDAGÓGICA DO RELATÓRIO: +1. **Acolhimento & Adaptação:** Como a criança se relaciona com os pares e com os adultos na rotina escolar. +2. **Campos de Experiência da BNCC:** + - *O Eu, o Outro e o Nós:* Autonomia, cooperação, partilha e resolução de pequenos conflitos. + - *Corpo, Gestos e Movimentos:* Esquema corporal, brincadeiras ativas e coordenação motora. + - *Traços, Sons, Cores e Formas:* Expressão plástica, musicalidade e exploração de materiais. + - *Escuta, Fala, Pensamento e Imaginação:* Comunicação oral, contação de histórias e interesse pela leitura. + - *Espaços, Tempos, Quantidades, Relações e Transformações:* Curiosidade científica, noções espaciais e contagem lúdica. +${relatorioAnterior ? '3. **Avanços em Relação ao 1º Semestre:** Destaque claro do crescimento socioemocional e cognitivo da criança ao longo do ano.' : ''} +4. **Fecho Afetivo:** Perspectivas e mensagem carinhosa de encorajamento. + +Linguagem: Afetuosa, técnica, positiva, sem rotulações, respeitando o tempo singular de desenvolvimento da criança.`; + + let conteudoRelatorio = ''; + try { + const r = await callMinimax({ + system: "Você é especialista em relatórios pedagógicos de Educação Infantil alinhados à BNCC.", + messages: [{ role: 'user', content: promptRelatorio }], + temperature: 0.5 + }); + conteudoRelatorio = r.text; + } catch (e) { + if (process.env.GOOGLE_AI_API_KEY) { + const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash'}:generateContent?key=${process.env.GOOGLE_AI_API_KEY}`; + const response = await fetch(geminiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ contents: [{ parts: [{ text: promptRelatorio }] }] }) + }); + if (response.ok) { + const data = await response.json(); + conteudoRelatorio = data.candidates?.[0]?.content?.parts?.[0]?.text || ''; + } + } + } + + if (!conteudoRelatorio) { + conteudoRelatorio = `Relatório Semestral de ${aluno.nome}\nO aluno demonstra pleno desenvolvimento socioemocional, participando com alegria e entusiasmo das vivências da turma.`; + } + + const dbIns = await dbPool.query( + `INSERT INTO escola.relatorios ( + usuario_id, turma_id, aluno_id, aluno_nome, ano_letivo, semestre, faixa_etaria, conteudo_relatorio, lote_id + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`, + [usuarioId, turma_id, aluno.id, aluno.nome, ano_letivo || 2026, semestre || 1, faixa_etaria || '4 a 5 anos', conteudoRelatorio, loteId] + ); + + relatoriosGerados.push(dbIns.rows[0]); + } + + res.json({ + success: true, + loteId, + total: relatoriosGerados.length, + relatorios: relatoriosGerados + }); + + } catch (err) { + console.error('Erro batch relatórios:', err); + res.status(500).json({ error: 'Erro ao gerar relatórios em lote: ' + err.message }); + } +}); + +app.get('/api/relatorios/turma/:turmaId', requireAuth, async (req, res) => { + const usuarioId = req.cookies.usuario_id || req.headers['user-id'] || '00000000-0000-0000-0000-000000000000'; + const { turmaId } = req.params; + try { + const dbRes = await dbPool.query( + "SELECT * FROM escola.relatorios WHERE turma_id = $1 AND usuario_id = $2 ORDER BY aluno_nome ASC, semestre DESC", + [turmaId, usuarioId] + ); + res.json(dbRes.rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // Rota coringa para redirecionar para index.html app.get('*', requireAuth, (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html'));