Implementação do CRUD de modelos de relatório e menu de emissão de relatórios compilados com filtros e exportação DOCX/PDF
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user