diff --git a/public/app.js b/public/app.js index 4049a0f..77ac27c 100644 --- a/public/app.js +++ b/public/app.js @@ -2481,6 +2481,494 @@ const initApp = () => { if (e.target === modal) modal.remove(); }); }; + + // ============================================================ + // CRUD DE MODELOS DE RELATÓRIO + // ============================================================ + const modelosRelatorioModal = document.getElementById('modelosRelatorioModal'); + const btnModelosRelatorio = document.getElementById('btnModelosRelatorio'); + const btnCloseModelosModal = document.getElementById('btnCloseModelosModal'); + const templatesList = document.getElementById('templatesList'); + const btnCreateTemplate = document.getElementById('btnCreateTemplate'); + const btnCancelTemplate = document.getElementById('btnCancelTemplate'); + const btnSaveTemplate = document.getElementById('btnSaveTemplate'); + + const templateFormId = document.getElementById('templateFormId'); + const templateFormNome = document.getElementById('templateFormNome'); + const templateFormFinalidade = document.getElementById('templateFormFinalidade'); + const templateFormPeriodicidade = document.getElementById('templateFormPeriodicidade'); + const templateFormEstrutura = document.getElementById('templateFormEstrutura'); + const templateFormTitle = document.getElementById('templateFormTitle'); + + let allTemplates = []; + + // Abrir modal de modelos + if (btnModelosRelatorio) { + btnModelosRelatorio.addEventListener('click', async () => { + modelosRelatorioModal.style.display = 'flex'; + clearTemplateForm(); + await fetchTemplates(); + }); + } + + // Fechar modal de modelos + if (btnCloseModelosModal) { + btnCloseModelosModal.addEventListener('click', () => { + modelosRelatorioModal.style.display = 'none'; + }); + } + if (modelosRelatorioModal) { + modelosRelatorioModal.addEventListener('click', (e) => { + if (e.target === modelosRelatorioModal) modelosRelatorioModal.style.display = 'none'; + }); + } + + // Buscar modelos do backend + async function fetchTemplates() { + templatesList.innerHTML = '
Carregando...
'; + try { + const res = await fetch('/api/modelos'); + allTemplates = await res.json(); + renderTemplatesList(); + } catch (err) { + console.error('Erro ao buscar modelos:', err); + templatesList.innerHTML = '
Erro ao carregar modelos.
'; + } + } + + // Renderizar a lista de modelos na esquerda + function renderTemplatesList() { + if (allTemplates.length === 0) { + templatesList.innerHTML = '
Nenhum modelo cadastrado.
'; + return; + } + templatesList.innerHTML = allTemplates.map(t => ` +
+
${t.nome}
+
${t.periodicidade} • ${t.finalidade || 'Sem finalidade'}
+
+ + +
+
+ `).join(''); + + // Eventos de clique na lista para editar/deletar + templatesList.querySelectorAll('.obs-item').forEach(item => { + item.addEventListener('click', (e) => { + if (e.target.tagName === 'BUTTON') return; + const id = item.dataset.id; + const template = allTemplates.find(t => t.id == id); + if (template) fillTemplateForm(template); + }); + }); + + templatesList.querySelectorAll('.template-edit-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const id = btn.dataset.id; + const template = allTemplates.find(t => t.id == id); + if (template) fillTemplateForm(template); + }); + }); + + templatesList.querySelectorAll('.template-delete-btn').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const id = btn.dataset.id; + const template = allTemplates.find(t => t.id == id); + if (!template) return; + if (confirm(`Tem certeza que deseja excluir o modelo "${template.nome}"?`)) { + try { + const res = await fetch(`/api/modelos/${id}`, { method: 'DELETE' }); + if (res.ok) { + alert('Modelo excluído com sucesso!'); + clearTemplateForm(); + await fetchTemplates(); + } else { + const err = await res.json(); + alert('Erro ao excluir: ' + err.error); + } + } catch (err) { + console.error('Erro ao excluir modelo:', err); + } + } + }); + }); + } + + // Preencher formulário de edição + function fillTemplateForm(template) { + templateFormId.value = template.id; + templateFormNome.value = template.nome; + templateFormFinalidade.value = template.finalidade || ''; + templateFormPeriodicidade.value = template.periodicidade || 'Semanal'; + templateFormEstrutura.value = template.estrutura || ''; + templateFormTitle.textContent = 'Editar Modelo'; + } + + // Limpar formulário + function clearTemplateForm() { + templateFormId.value = ''; + templateFormNome.value = ''; + templateFormFinalidade.value = ''; + templateFormPeriodicidade.value = 'Semanal'; + templateFormEstrutura.value = ''; + templateFormTitle.textContent = 'Novo Modelo'; + } + + // Botão "Novo Modelo" + if (btnCreateTemplate) { + btnCreateTemplate.addEventListener('click', clearTemplateForm); + } + + // Botão "Cancelar" + if (btnCancelTemplate) { + btnCancelTemplate.addEventListener('click', clearTemplateForm); + } + + // Botão "Salvar Modelo" + if (btnSaveTemplate) { + btnSaveTemplate.addEventListener('click', async () => { + const id = templateFormId.value; + const nome = templateFormNome.value.trim(); + const finalidade = templateFormFinalidade.value.trim(); + const periodicidade = templateFormPeriodicidade.value; + const estrutura = templateFormEstrutura.value.trim(); + + if (!nome || !estrutura) { + alert('Nome e estrutura do modelo são obrigatórios.'); + return; + } + + const body = { nome, finalidade, periodicidade, estrutura }; + const url = id ? `/api/modelos/${id}` : '/api/modelos'; + const method = id ? 'PUT' : 'POST'; + + try { + const res = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + if (res.ok) { + alert('Modelo salvo com sucesso!'); + clearTemplateForm(); + await fetchTemplates(); + } else { + const err = await res.json(); + alert('Erro ao salvar modelo: ' + err.error); + } + } catch (err) { + console.error('Erro ao salvar modelo:', err); + } + }); + } + + // ============================================================ + // GERAÇÃO / EMISSÃO DE RELATÓRIO PEDAGÓGICO + // ============================================================ + const emitirRelatorioModal = document.getElementById('emitirRelatorioModal'); + const btnEmitirRelatorio = document.getElementById('btnEmitirRelatorio'); + const btnCloseEmitirModal = document.getElementById('btnCloseEmitirModal'); + + const emitirFilterCrianca = document.getElementById('emitirFilterCrianca'); + const emitirFilterDataInicio = document.getElementById('emitirFilterDataInicio'); + const emitirFilterDataFim = document.getElementById('emitirFilterDataFim'); + const emitirFilterTag = document.getElementById('emitirFilterTag'); + const emitirFilterTemplate = document.getElementById('emitirFilterTemplate'); + + const emitirObsCountInfo = document.getElementById('emitirObsCountInfo'); + const btnGenerateCompiledReport = document.getElementById('btnGenerateCompiledReport'); + const emitirReportPreviewArea = document.getElementById('emitirReportPreviewArea'); + + const btnExportDocx = document.getElementById('btnExportDocx'); + const btnExportPdf = document.getElementById('btnExportPdf'); + + let activeCompiledReport = null; // Guarda o texto do relatório compilado ativo + + // Abrir modal de emissão + if (btnEmitirRelatorio) { + btnEmitirRelatorio.addEventListener('click', async () => { + emitirRelatorioModal.style.display = 'flex'; + activeCompiledReport = null; + emitirReportPreviewArea.innerHTML = ` +
+ Escolha uma criança e um modelo de relatório para começar. +
+ `; + btnExportDocx.disabled = true; + btnExportDocx.style.cursor = 'not-allowed'; + btnExportDocx.style.borderColor = 'var(--border-light)'; + btnExportDocx.style.color = 'var(--text-secondary)'; + + btnExportPdf.disabled = true; + btnExportPdf.style.cursor = 'not-allowed'; + btnExportPdf.style.borderColor = 'var(--border-light)'; + btnExportPdf.style.color = 'var(--text-secondary)'; + + emitirFilterDataInicio.value = ''; + emitirFilterDataFim.value = ''; + emitirFilterTag.value = ''; + + await loadFiltersData(); + }); + } + + // Fechar modal de emissão + if (btnCloseEmitirModal) { + btnCloseEmitirModal.addEventListener('click', () => { + emitirRelatorioModal.style.display = 'none'; + }); + } + if (emitirRelatorioModal) { + emitirRelatorioModal.addEventListener('click', (e) => { + if (e.target === emitirRelatorioModal) emitirRelatorioModal.style.display = 'none'; + }); + } + + // Carregar dados de filtros (crianças e templates) + async function loadFiltersData() { + try { + const res = await fetch('/api/observacoes/criancas'); + const criancas = await res.json(); + emitirFilterCrianca.innerHTML = '' + + criancas.map(c => ``).join(''); + } catch (err) { + console.error('Erro ao buscar crianças para filtros:', err); + } + + try { + const res = await fetch('/api/modelos'); + const modelos = await res.json(); + emitirFilterTemplate.innerHTML = '' + + modelos.map(m => ``).join(''); + } catch (err) { + console.error('Erro ao buscar modelos para filtros:', err); + } + + updateObservationsCount(); + } + + // Ouvintes para atualizar contagem + [emitirFilterCrianca, emitirFilterDataInicio, emitirFilterDataFim, emitirFilterTag, emitirFilterTemplate].forEach(el => { + if (el) el.addEventListener('change', updateObservationsCount); + }); + if (emitirFilterCrianca) { + emitirFilterCrianca.addEventListener('input', updateObservationsCount); + } + + // Recalcular quantidade de observações elegíveis + async function updateObservationsCount() { + const crianca = emitirFilterCrianca ? emitirFilterCrianca.value : ''; + const dataInicio = emitirFilterDataInicio ? emitirFilterDataInicio.value : ''; + const dataFim = emitirFilterDataFim ? emitirFilterDataFim.value : ''; + const tag = emitirFilterTag ? emitirFilterTag.value : ''; + const modeloId = emitirFilterTemplate ? emitirFilterTemplate.value : ''; + + if (!crianca || !modeloId) { + emitirObsCountInfo.textContent = 'Escolha uma criança e um modelo para prosseguir.'; + btnGenerateCompiledReport.disabled = true; + btnGenerateCompiledReport.style.opacity = '0.6'; + btnGenerateCompiledReport.style.cursor = 'not-allowed'; + return; + } + + try { + let url = `/api/observacoes/contar?crianca=${encodeURIComponent(crianca)}`; + if (dataInicio) url += `&dataInicio=${dataInicio}`; + if (dataFim) url += `&dataFim=${dataFim}`; + if (tag) url += `&tags=${encodeURIComponent(tag)}`; + + const res = await fetch(url); + const data = await res.json(); + const count = data.count || 0; + + if (count === 0) { + emitirObsCountInfo.innerHTML = `⚠️ Nenhuma observação encontrada para esses filtros.`; + btnGenerateCompiledReport.disabled = true; + btnGenerateCompiledReport.style.opacity = '0.6'; + btnGenerateCompiledReport.style.cursor = 'not-allowed'; + } else { + emitirObsCountInfo.innerHTML = `✨ ${count} observações selecionadas para compilação.`; + btnGenerateCompiledReport.disabled = false; + btnGenerateCompiledReport.style.opacity = '1'; + btnGenerateCompiledReport.style.cursor = 'pointer'; + } + } catch (err) { + console.error('Erro ao contar observações:', err); + } + } + + // Gerar relatório consolidado com IA + if (btnGenerateCompiledReport) { + btnGenerateCompiledReport.addEventListener('click', async () => { + const crianca = emitirFilterCrianca.value; + const dataInicio = emitirFilterDataInicio.value; + const dataFim = emitirFilterDataFim.value; + const tag = emitirFilterTag.value; + const modeloId = emitirFilterTemplate.value; + + if (!crianca || !modeloId) return; + + emitirReportPreviewArea.innerHTML = ` +
+
+
Analisando observações e gerando relatório pedagógico...
+
+ `; + btnGenerateCompiledReport.disabled = true; + btnGenerateCompiledReport.style.opacity = '0.6'; + + try { + const res = await fetch('/api/modelos/gerar-relatorio', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + crianca, + dataInicio, + dataFim, + tags: tag || undefined, + modeloId + }) + }); + + if (!res.ok) { + const errData = await res.json(); + throw new Error(errData.error || 'Erro desconhecido'); + } + + const data = await res.json(); + activeCompiledReport = data; + + // Renderizar prévia do relatório em markdown + emitirReportPreviewArea.innerHTML = ` +
+
Estudante: ${data.crianca}
+
Período: ${data.periodo}
+
Modelo Aplicado: ${data.templateNome}
+
Quantidade de Observações Consolidadas: ${data.totalObservacoes}
+
+
+ ${marked.parse(data.report)} +
+ `; + + // Ativar botões de exportação + btnExportDocx.disabled = false; + btnExportDocx.style.cursor = 'pointer'; + btnExportDocx.style.borderColor = 'var(--brand-green)'; + btnExportDocx.style.color = 'var(--brand-green)'; + + btnExportPdf.disabled = false; + btnExportPdf.style.cursor = 'pointer'; + btnExportPdf.style.borderColor = 'var(--brand-green)'; + btnExportPdf.style.color = 'var(--brand-green)'; + + } catch (err) { + console.error('Erro ao gerar relatório compilado:', err); + emitirReportPreviewArea.innerHTML = ` +
+ Erro ao gerar relatório: ${err.message} +
+ `; + } finally { + btnGenerateCompiledReport.disabled = false; + btnGenerateCompiledReport.style.opacity = '1'; + } + }); + } + + // Exportar para DOCX (Formato HTML-DOCX) + if (btnExportDocx) { + btnExportDocx.addEventListener('click', () => { + if (!activeCompiledReport) return; + + const htmlContent = ` + + + + Relatório Pedagógico - ${activeCompiledReport.crianca} + + + +
+
Estudante: ${activeCompiledReport.crianca}
+
Período: ${activeCompiledReport.periodo}
+
Modelo: ${activeCompiledReport.templateNome}
+
Quantidade de observações consolidadas: ${activeCompiledReport.totalObservacoes}
+
+
+ ${marked.parse(activeCompiledReport.report)} + + + `; + + const blob = new Blob(['\ufeff' + htmlContent], { type: 'application/msword' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `relatorio_${activeCompiledReport.crianca.replace(/\s+/g, '_')}_${Date.now()}.doc`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }); + } + + // Exportar para PDF (Abre janela de impressão do navegador) + if (btnExportPdf) { + btnExportPdf.addEventListener('click', () => { + if (!activeCompiledReport) return; + + const printWindow = window.open('', '_blank'); + printWindow.document.write(` + + + Relatório Pedagógico - ${activeCompiledReport.crianca} + + + +

Relatório Pedagógico de Desenvolvimento

+
+
Estudante: ${activeCompiledReport.crianca}
+
Período: ${activeCompiledReport.periodo}
+
Modelo Aplicado: ${activeCompiledReport.templateNome}
+
Quantidade de Observações: ${activeCompiledReport.totalObservacoes}
+
+
${marked.parse(activeCompiledReport.report)}
+ + + + `); + printWindow.document.close(); + }); + } }; // Player de áudio personalizado global para as mídias geradas diff --git a/public/index.html b/public/index.html index dd571fd..a82ba16 100644 --- a/public/index.html +++ b/public/index.html @@ -73,6 +73,22 @@ Minhas Observações + + + + + +
@@ -449,6 +465,155 @@
+ + + + + + diff --git a/server.js b/server.js index c54d115..ad98a69 100644 --- a/server.js +++ b/server.js @@ -238,6 +238,37 @@ function writeObsIndex(idx) { // Inicializar banco de dados e migrar dados se estiver vazio async function initDatabase() { try { + // 0. Garantir que a tabela de modelos exista e tenha dados padrão + await dbPool.query(` + CREATE TABLE IF NOT EXISTS escola.modelos ( + id SERIAL PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + finalidade TEXT, + periodicidade VARCHAR(100), + estrutura TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + `); + + const checkModelos = await dbPool.query("SELECT COUNT(*) FROM escola.modelos;"); + if (parseInt(checkModelos.rows[0].count) === 0) { + console.log('[Database Init] Inserindo modelos/templates padrão de relatório...'); + await dbPool.query(` + INSERT INTO escola.modelos (nome, finalidade, periodicidade, estrutura) VALUES + ($1, $2, $3, $4), + ($5, $6, $7, $8); + `, [ + "Relatório Semanal de Desenvolvimento", + "Acompanhar a evolução cognitiva e social das crianças semanalmente", + "Semanal", + `# Relatório Semanal de Desenvolvimento Pedagógico\n\n## 1. Aspectos Cognitivos e Linguagem\n- Como a criança interagiu com os materiais e desafios propostos esta semana.\n- Expressão verbal, argumentação e participação em rodas de conversa.\n\n## 2. Aspectos Socioemocionais e Autonomia\n- Colaboração com os colegas e resolução de pequenos conflitos.\n- Nível de independência na realização das atividades diárias.\n\n## 3. Aspectos Motores e Brincar\n- Coordenação motora ampla e fina durante as brincadeiras e atividades direcionadas.`, + "Avaliação Semestral Individual", + "Avaliar de forma global o progresso do aluno ao fim do semestre", + "Semestral", + `# Avaliação Semestral Individual - Educação Infantil\n\n## Introdução\nContextualização geral da adaptação e participação do aluno no semestre.\n\n## I. O Eu, o Outro e o Nós\n- Relações sociais, respeito às regras e empatia.\n\n## II. Corpo, Gestos e Movimentos\n- Expressividade, habilidades motoras e exploração do espaço.\n\n## III. Traços, Sons, Cores e Formas\n- Sensibilidade estética, linguagem artística e musical.\n\n## IV. Escuta, Fala, Pensamento e Imaginação\n- Oralidade, repertório de histórias e início da escrita/grafismo.\n\n## V. Espaços, Tempos, Quantidades, Relações e Transformações\n- Raciocínio lógico-matemático e curiosidade científica.` + ]); + } + // 1. Sincronizar cache de configurações do agente ou migrar const configRes = await dbPool.query("SELECT agent_name, kemily_avatar_url, camila_avatar_url, temperature_preset FROM escola.config WHERE key = 'current';"); if (configRes.rows.length > 0) { @@ -660,6 +691,317 @@ app.post('/api/observacao/salvar', requireAuth, async (req, res) => { res.json({ success: true, filename, id }); }); +// ============================================================ +// CRUD DE MODELOS DE RELATÓRIO (TEMPLATES) & COMPILAÇÃO +// ============================================================ + +// Listar todos os modelos +app.get('/api/modelos', requireAuth, async (req, res) => { + try { + const dbRes = await dbPool.query("SELECT * FROM escola.modelos ORDER BY nome ASC;"); + res.json(dbRes.rows); + } catch (err) { + console.error('Erro ao buscar modelos:', err); + res.status(500).json({ error: err.message }); + } +}); + +// Criar novo modelo +app.post('/api/modelos', requireAuth, async (req, res) => { + const { nome, finalidade, periodicidade, estrutura } = req.body; + if (!nome || !estrutura) return res.status(400).json({ error: 'Nome e estrutura são obrigatórios' }); + try { + const dbRes = await dbPool.query( + "INSERT INTO escola.modelos (nome, finalidade, periodicidade, estrutura) VALUES ($1, $2, $3, $4) RETURNING *;", + [nome, finalidade, periodicidade, estrutura] + ); + res.status(201).json(dbRes.rows[0]); + } catch (err) { + console.error('Erro ao criar modelo:', err); + res.status(500).json({ error: err.message }); + } +}); + +// Atualizar modelo existente +app.put('/api/modelos/:id', requireAuth, async (req, res) => { + const { id } = req.params; + const { nome, finalidade, periodicidade, estrutura } = req.body; + if (!nome || !estrutura) return res.status(400).json({ error: 'Nome e estrutura são obrigatórios' }); + try { + const dbRes = await dbPool.query( + "UPDATE escola.modelos SET nome = $1, finalidade = $2, periodicidade = $3, estrutura = $4 WHERE id = $5 RETURNING *;", + [nome, finalidade, periodicidade, estrutura, id] + ); + if (dbRes.rows.length === 0) return res.status(404).json({ error: 'Modelo não encontrado' }); + res.json(dbRes.rows[0]); + } catch (err) { + console.error('Erro ao atualizar modelo:', err); + res.status(500).json({ error: err.message }); + } +}); + +// Excluir modelo +app.delete('/api/modelos/:id', requireAuth, async (req, res) => { + const { id } = req.params; + try { + const dbRes = await dbPool.query("DELETE FROM escola.modelos WHERE id = $1 RETURNING *;", [id]); + if (dbRes.rows.length === 0) return res.status(404).json({ error: 'Modelo não encontrado' }); + res.json({ success: true, message: 'Modelo excluído com sucesso' }); + } catch (err) { + console.error('Erro ao excluir modelo:', err); + res.status(500).json({ error: err.message }); + } +}); + +// Obter lista única de crianças já observadas +app.get('/api/observacoes/criancas', requireAuth, async (req, res) => { + try { + const dbRes = await dbPool.query(` + SELECT DISTINCT jsonb_array_elements_text(criancas) as nome + FROM escola.observacoes + WHERE criancas IS NOT NULL AND jsonb_array_length(criancas) > 0 + ORDER BY nome; + `); + res.json(dbRes.rows.map(r => r.nome)); + } catch (err) { + console.error('Erro ao buscar crianças únicas:', err); + res.status(500).json({ error: err.message }); + } +}); + +// Contar observações baseadas em filtros +app.get('/api/observacoes/contar', requireAuth, async (req, res) => { + const { crianca, dataInicio, dataFim, tags } = req.query; + try { + let sql = `SELECT COUNT(*) FROM escola.observacoes WHERE 1=1`; + const params = []; + let pCount = 1; + + if (crianca) { + sql += ` AND EXISTS ( + SELECT 1 FROM jsonb_array_elements_text(criancas) AS c + WHERE LOWER(c) LIKE $${pCount} + )`; + params.push(`%${crianca.toLowerCase()}%`); + pCount++; + } + + if (dataInicio) { + sql += ` AND date >= $${pCount}`; + params.push(dataInicio); + pCount++; + } + + if (dataFim) { + sql += ` AND date <= $${pCount}`; + params.push(dataFim); + pCount++; + } + + if (tags) { + const tagList = tags.split(',').map(t => t.trim().toLowerCase()); + sql += ` AND EXISTS ( + SELECT 1 FROM jsonb_array_elements_text(tags) AS t + WHERE LOWER(t) = ANY($${pCount}) + )`; + params.push(tagList); + pCount++; + } + + const dbRes = await dbPool.query(sql, params); + res.json({ count: parseInt(dbRes.rows[0].count) }); + } catch (err) { + console.error('Erro ao contar observações:', err); + res.status(500).json({ error: err.message }); + } +}); + +// Gerar relatório consolidado com IA baseado em observações e um modelo +app.post('/api/modelos/gerar-relatorio', requireAuth, async (req, res) => { + const { crianca, dataInicio, dataFim, tags, modeloId } = req.body; + if (!modeloId) return res.status(400).json({ error: 'O ID do modelo é obrigatório' }); + + try { + // 1. Buscar o modelo/template + const templateRes = await dbPool.query("SELECT * FROM escola.modelos WHERE id = $1;", [modeloId]); + if (templateRes.rows.length === 0) return res.status(404).json({ error: 'Modelo de relatório não encontrado' }); + const template = templateRes.rows[0]; + + // 2. Buscar as observações filtradas + let sql = ` + SELECT date, time, report, criancas, tags + FROM escola.observacoes + WHERE 1=1 + `; + const params = []; + let pCount = 1; + + if (crianca) { + sql += ` AND EXISTS ( + SELECT 1 FROM jsonb_array_elements_text(criancas) AS c + WHERE LOWER(c) LIKE $${pCount} + )`; + params.push(`%${crianca.toLowerCase()}%`); + pCount++; + } + + if (dataInicio) { + sql += ` AND date >= $${pCount}`; + params.push(dataInicio); + pCount++; + } + + if (dataFim) { + sql += ` AND date <= $${pCount}`; + params.push(dataFim); + pCount++; + } + + if (tags) { + const tagList = Array.isArray(tags) ? tags : [tags]; + sql += ` AND EXISTS ( + SELECT 1 FROM jsonb_array_elements_text(tags) AS t + WHERE LOWER(t) = ANY($${pCount}) + )`; + params.push(tagList.map(t => t.toLowerCase())); + pCount++; + } + + sql += ` ORDER BY date ASC`; + const dbRes = await dbPool.query(sql, params); + const observations = dbRes.rows; + + if (observations.length === 0) { + return res.status(400).json({ error: 'Nenhuma observação encontrada para os filtros selecionados.' }); + } + + // 3. Montar prompt para IA usando a estrutura do template + const combinedObsText = observations.map((o, idx) => { + const dateText = o.date ? `Data: ${o.date}` : ''; + const tagsText = o.tags ? `Tags: ${Array.isArray(o.tags) ? o.tags.join(', ') : o.tags}` : ''; + return `[Observação #${idx + 1} ${dateText} ${tagsText}]\n${o.report}`; + }).join('\n\n---\n\n'); + + const agentConfig = readAgentConfig(); + const agentName = agentConfig.agentName || "Kemily"; + + const systemPrompt = `Você é a ${agentName}, uma assistente pedagógica e especialista em educação infantil. +Seu objetivo é analisar o histórico de observações de uma criança e redigir um relatório pedagógico consolidado e profissional. + +Você DEVE estruturar o relatório seguindo exatamente as seções e diretrizes fornecidas no modelo de relatório selecionado. + +MODELO DE RELATÓRIO: "${template.nome}" +FINALIDADE: "${template.finalidade || 'Não informada'}" +PERIODICIDADE: "${template.periodicidade || 'Não informada'}" + +ESTRUTURA/DIRETRIZES DO MODELO: +""" +${template.estrutura} +""" + +HISTÓRICO DE OBSERVAÇÕES REGISTRADAS: +""" +${combinedObsText.slice(0, 8000)} +""" + +Orientações adicionais: +- Redija o relatório em português brasileiro formal e adequado para pedagogia. +- Preencha cada seção da estrutura de forma rica e detalhada, baseando-se estritamente nos fatos reais descritos nas observações. +- Não invente acontecimentos que não estejam descritos nas observações, mas faça as conexões pedagógicas pertinentes de acordo com as evidências apresentadas. +- Retorne apenas o relatório em formato Markdown estruturado.`; + + let generatedReport = ''; + + // Tenta OpenRouter + try { + console.log('[Compilar Relatório] Chamando OpenRouter...'); + const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano', + messages: [{ role: 'user', content: systemPrompt }], + temperature: 0.5, + max_tokens: 2500 + }) + }); + if (response.ok) { + const data = await response.json(); + generatedReport = data.choices?.[0]?.message?.content || ''; + } else { + console.warn('[Compilar Relatório] Falha no OpenRouter status:', response.status); + } + } catch (err) { + console.error('[Compilar Relatório] Erro no OpenRouter:', err.message); + } + + // Fallback para Google Gemini + if (!generatedReport && process.env.GOOGLE_AI_API_KEY) { + try { + console.log('[Compilar Relatório] Fallback para Google Gemini...'); + const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash'}:generateContent?key=${process.env.GOOGLE_AI_API_KEY}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [{ parts: [{ text: systemPrompt }] }], + generationConfig: { temperature: 0.5, maxOutputTokens: 2500 } + }) + }); + if (response.ok) { + const data = await response.json(); + generatedReport = data.candidates?.[0]?.content?.parts?.[0]?.text || ''; + } + } catch (err) { + console.error('[Compilar Relatório] Erro no Google Gemini:', err.message); + } + } + + // Fallback para Groq + if (!generatedReport && process.env.GROQ_API_KEY) { + try { + console.log('[Compilar Relatório] Fallback para Groq...'); + const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.GROQ_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: process.env.GROQ_MODEL || 'llama-3.3-70b-versatile', + messages: [{ role: 'user', content: systemPrompt }], + temperature: 0.5, + max_tokens: 2500 + }) + }); + if (response.ok) { + const data = await response.json(); + generatedReport = data.choices?.[0]?.message?.content || ''; + } + } catch (err) { + console.error('[Compilar Relatório] Erro no Groq:', err.message); + } + } + + if (!generatedReport) { + throw new Error('Falha ao compilar relatório pedagógico com todos os provedores de IA.'); + } + + res.json({ + report: generatedReport, + templateNome: template.nome, + crianca: crianca || 'Todos os alunos', + periodo: (dataInicio || '') + (dataInicio && dataFim ? ' a ' : '') + (dataFim || 'Todo o período'), + totalObservacoes: observations.length + }); + } catch (err) { + console.error('Erro geral ao compilar relatório:', err); + res.status(500).json({ error: err.message }); + } +}); + // Rota: GET /api/observacoes — lista todas as observações (índice) app.get('/api/observacoes', requireAuth, async (req, res) => { try {