feat: Implementação completa dos módulos profissionais (Dossiê, Crises, Atas, Stickers, CSV e MindLab)
This commit is contained in:
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user