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