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'}
+
+ Editar
+ Excluir
+
+
+ `).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 = 'Selecione a criança ' +
+ criancas.map(c => `${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 = 'Selecione o modelo ' +
+ modelos.map(m => `${m.nome} `).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}
+
+
+
+
+
+ ${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
+
+ ${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
+
+
+
+
+
+ Emitir Relatório
+
+
+
+
+
+
+
+ Modelos de Relatório
+
+
@@ -449,6 +465,155 @@
+
+
+
+
+
+
+
+
+
Modelos Salvos
+ + Novo Modelo
+
+
+
+
Carregando modelos...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Criança
+
+ Selecione a criança
+
+
+
+
+ Data Início
+
+
+
+
+ Data Fim
+
+
+
+
+ Tags
+
+ Todas as tags
+ Social
+ Cognitivo
+ Motor
+ Linguagem
+ Emocional
+ Colaborativo
+ Autônomo
+
+
+
+
+ Modelo / Template
+
+ Selecione o modelo
+
+
+
+
+
+
+ Selecione uma criança para buscar observações...
+
+ Gerar Relatório com IA
+
+
+
+
+
+
+
Visualização do Relatório
+
+
+ 💾 DOCX
+
+
+ 🖨️ PDF
+
+
+
+
+
+
+ Escolha uma criança e um modelo de relatório para começar.
+
+
+
+
+
+
+
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 {