feat: Implementação completa dos módulos profissionais (Dossiê, Crises, Atas, Stickers, CSV e MindLab)
This commit is contained in:
+724
-2
@@ -3231,12 +3231,13 @@ const initApp = () => {
|
|||||||
const turmaObj = currentTurmas.find(t => t.id === c.turma_id);
|
const turmaObj = currentTurmas.find(t => t.id === c.turma_id);
|
||||||
const turmaNome = turmaObj ? turmaObj.nome : 'Sem turma';
|
const turmaNome = turmaObj ? turmaObj.nome : 'Sem turma';
|
||||||
return `
|
return `
|
||||||
<div style="background: var(--bg-secondary); border: 1px solid var(--border-light); padding: 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
|
<div style="background: var(--bg-secondary); border: 1px solid var(--border-light); padding: 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: 600; color: var(--text-primary);">${escapeHtml(c.nome)} ${c.especial ? '⭐' : ''}</div>
|
<div style="font-weight: 600; color: var(--text-primary);">${escapeHtml(c.nome)} ${c.especial ? '⭐' : ''}</div>
|
||||||
<div style="font-size: 0.8rem; color: var(--text-secondary);">${escapeHtml(turmaNome)} ${c.apelido ? '- ' + escapeHtml(c.apelido) : ''}</div>
|
<div style="font-size: 0.8rem; color: var(--text-secondary);">${escapeHtml(turmaNome)} ${c.apelido ? '- ' + escapeHtml(c.apelido) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 8px;">
|
<div style="display: flex; gap: 6px; align-items: center;">
|
||||||
|
<button onclick="viewChildTimeline('${c.id}')" style="background: rgba(59,130,246,0.12); border: 1px solid rgba(59,130,246,0.3); color: #60a5fa; cursor: pointer; padding: 4px 8px; border-radius: 6px; font-size: 0.75rem; font-weight: 600;" title="Ver Linha do Tempo e Dossiê">🔍 Dossiê</button>
|
||||||
<button onclick="editChild('${c.id}')" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px;" title="Editar">✏️</button>
|
<button onclick="editChild('${c.id}')" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px;" title="Editar">✏️</button>
|
||||||
<button onclick="deleteChild('${c.id}')" style="background: none; border: none; color: #ef4444; cursor: pointer; padding: 4px;" title="Excluir">🗑️</button>
|
<button onclick="deleteChild('${c.id}')" style="background: none; border: none; color: #ef4444; cursor: pointer; padding: 4px;" title="Excluir">🗑️</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -10822,3 +10823,724 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
item.click();
|
item.click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// MÓDULOS EXPANDIDOS: DOSSIÊ DO ALUNO, PROTOCOLO DE CRISES, ATAS & STICKERS
|
||||||
|
// ==========================================================================
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// 1. DOSSIÊ & LINHA DO TEMPO DO ALUNO
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const alunoTimelineModal = document.getElementById('alunoTimelineModal');
|
||||||
|
const btnCloseAlunoTimeline = document.getElementById('btnCloseAlunoTimeline');
|
||||||
|
const timelineAlunoNome = document.getElementById('timelineAlunoNome');
|
||||||
|
const timelineAlunoInfo = document.getElementById('timelineAlunoInfo');
|
||||||
|
const timelineContentArea = document.getElementById('timelineContentArea');
|
||||||
|
const btnExportFichaConselho = document.getElementById('btnExportFichaConselho');
|
||||||
|
let currentTimelineAlunoId = null;
|
||||||
|
|
||||||
|
if (btnCloseAlunoTimeline) btnCloseAlunoTimeline.addEventListener('click', () => alunoTimelineModal.style.display = 'none');
|
||||||
|
if (alunoTimelineModal) alunoTimelineModal.addEventListener('click', (e) => { if (e.target === alunoTimelineModal) alunoTimelineModal.style.display = 'none'; });
|
||||||
|
|
||||||
|
window.viewChildTimeline = async (alunoId) => {
|
||||||
|
currentTimelineAlunoId = alunoId;
|
||||||
|
alunoTimelineModal.style.display = 'flex';
|
||||||
|
timelineContentArea.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando dossiê...</p>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/alunos/${alunoId}/timeline`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Erro ao carregar dados');
|
||||||
|
|
||||||
|
const aluno = data.aluno;
|
||||||
|
timelineAlunoNome.textContent = `Dossiê: ${aluno.nome} ${aluno.especial ? '⭐ (Ed. Especial)' : ''}`;
|
||||||
|
timelineAlunoInfo.textContent = `Nasc: ${aluno.data_nasc ? new Date(aluno.data_nasc).toLocaleDateString('pt-BR') : 'Não informada'} • Pais: ${aluno.pais || 'Não informados'} • Saúde: ${aluno.observacoes_saude || 'Nenhuma alergia/restrição'}`;
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Seção Observações
|
||||||
|
const obs = data.timeline.observacoes || [];
|
||||||
|
html += `<h4 style="color:#f43f5e; margin:10px 0 6px 0; font-size:0.95rem;">📝 Observações Registradas (${obs.length})</h4>`;
|
||||||
|
if (obs.length === 0) {
|
||||||
|
html += '<p style="font-size:0.8rem; color:var(--text-secondary);">Nenhuma observação registrada ainda.</p>';
|
||||||
|
} else {
|
||||||
|
obs.forEach(o => {
|
||||||
|
html += `
|
||||||
|
<div style="background:rgba(244,63,94,0.05); border-left:3px solid #f43f5e; padding:8px 12px; border-radius:4px; font-size:0.82rem; margin-bottom:6px;">
|
||||||
|
<div style="display:flex; justify-content:space-between; color:var(--text-secondary); font-size:0.75rem;">
|
||||||
|
<strong>📅 ${o.date || ''} ${o.time || ''}</strong>
|
||||||
|
${o.tags ? `<span>🏷️ ${o.tags}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<p style="margin:4px 0 0 0; color:var(--text-primary);">${escapeHtml(o.report || '')}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seção Stickers & Conquistas
|
||||||
|
const stks = data.timeline.stickers || [];
|
||||||
|
html += `<h4 style="color:#fbbf24; margin:14px 0 6px 0; font-size:0.95rem;">🌟 Conquistas & Stickers Recebidos (${stks.length})</h4>`;
|
||||||
|
if (stks.length === 0) {
|
||||||
|
html += '<p style="font-size:0.8rem; color:var(--text-secondary);">Nenhum sticker concedido ainda.</p>';
|
||||||
|
} else {
|
||||||
|
html += '<div style="display:flex; flex-wrap:wrap; gap:8px;">';
|
||||||
|
stks.forEach(s => {
|
||||||
|
html += `
|
||||||
|
<div style="background:rgba(245,158,11,0.1); border:1px solid rgba(245,158,11,0.3); border-radius:6px; padding:6px 10px; font-size:0.8rem;">
|
||||||
|
<span style="font-size:1.1rem;">${s.icone || '🌟'}</span> <strong>${escapeHtml(s.titulo)}</strong>
|
||||||
|
<div style="font-size:0.7rem; color:var(--text-secondary);">${escapeHtml(s.categoria)} • ${new Date(s.created_at).toLocaleDateString('pt-BR')}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seção Histórias
|
||||||
|
const hists = data.timeline.historias || [];
|
||||||
|
if (hists.length > 0) {
|
||||||
|
html += `<h4 style="color:#10b981; margin:14px 0 6px 0; font-size:0.95rem;">📖 Histórias em que Participou (${hists.length})</h4>`;
|
||||||
|
hists.forEach(h => {
|
||||||
|
html += `
|
||||||
|
<div style="background:rgba(16,185,129,0.05); border-left:3px solid #10b981; padding:8px 12px; border-radius:4px; font-size:0.82rem; margin-bottom:6px;">
|
||||||
|
<strong>${escapeHtml(h.titulo || h.tema)}</strong>
|
||||||
|
<div style="font-size:0.72rem; color:var(--text-secondary);">${h.faixa_etaria} • ${new Date(h.created_at).toLocaleDateString('pt-BR')}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seção Ocorrências / Crises
|
||||||
|
const crises = data.timeline.crises || [];
|
||||||
|
if (crises.length > 0) {
|
||||||
|
html += `<h4 style="color:#ef4444; margin:14px 0 6px 0; font-size:0.95rem;">🚨 Registros de Mediação / Acolhimento (${crises.length})</h4>`;
|
||||||
|
crises.forEach(cr => {
|
||||||
|
html += `
|
||||||
|
<div style="background:rgba(239,68,68,0.05); border-left:3px solid #ef4444; padding:8px 12px; border-radius:4px; font-size:0.82rem; margin-bottom:6px;">
|
||||||
|
<div style="display:flex; justify-content:space-between; font-size:0.75rem; color:#f87171;">
|
||||||
|
<strong>${escapeHtml(cr.tipo_evento)} (${cr.intensidade})</strong>
|
||||||
|
<span>${new Date(cr.data_hora).toLocaleString('pt-BR')}</span>
|
||||||
|
</div>
|
||||||
|
<p style="margin:4px 0 0 0; color:var(--text-primary);">${escapeHtml(cr.descricao)}</p>
|
||||||
|
<div style="font-size:0.75rem; color:var(--text-secondary); margin-top:2px;"><strong>Acolhimento:</strong> ${escapeHtml(cr.medidas_tomadas)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seção Relatórios Semestrais
|
||||||
|
const rels = data.timeline.relatorios || [];
|
||||||
|
if (rels.length > 0) {
|
||||||
|
html += `<h4 style="color:#3b82f6; margin:14px 0 6px 0; font-size:0.95rem;">📄 Relatórios Semestrais Emitidos (${rels.length})</h4>`;
|
||||||
|
rels.forEach(r => {
|
||||||
|
html += `
|
||||||
|
<div style="background:rgba(59,130,246,0.05); border-left:3px solid #3b82f6; padding:8px 12px; border-radius:4px; font-size:0.82rem; margin-bottom:6px;">
|
||||||
|
<strong>${r.semestre}º Semestre de ${r.ano_letivo} (${r.faixa_etaria})</strong>
|
||||||
|
<p style="margin:4px 0 0 0; color:var(--text-primary); max-height:100px; overflow-y:auto; font-size:0.8rem;">${escapeHtml(r.conteudo_relatorio)}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
timelineContentArea.innerHTML = html;
|
||||||
|
} catch (err) {
|
||||||
|
timelineContentArea.innerHTML = `<p style="color:#ef4444; text-align:center;">Erro: ${err.message}</p>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exportar Ficha para Conselho de Classe
|
||||||
|
if (btnExportFichaConselho) {
|
||||||
|
btnExportFichaConselho.addEventListener('click', async () => {
|
||||||
|
if (!currentTimelineAlunoId) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/alunos/${currentTimelineAlunoId}/conselho-ficha`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Falha ao buscar ficha');
|
||||||
|
|
||||||
|
const aluno = data.aluno;
|
||||||
|
const win = window.open('', '_blank');
|
||||||
|
win.document.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Ficha de Conselho - ${aluno.nome}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 40px; color: #1e293b; line-height: 1.6; max-width: 800px; margin: 0 auto; }
|
||||||
|
h1 { color: #1e40af; border-bottom: 2px solid #cbd5e1; padding-bottom: 8px; margin-bottom: 16px; font-size: 1.6rem; }
|
||||||
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 20px; background: #f8fafc; padding: 15px; border-radius: 8px; border: 1px solid #e2e8f0; }
|
||||||
|
.btn-p { background: #3b82f6; color: white; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; cursor: pointer; float: right; }
|
||||||
|
@media print { .btn-p { display: none; } body { padding: 0; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="btn-p" onclick="window.print()">🖨️ Imprimir Ficha</button>
|
||||||
|
<h1>📋 Ficha Individual de Acompanhamento / Conselho de Classe</h1>
|
||||||
|
<div class="grid">
|
||||||
|
<div><strong>Aluno(a):</strong> ${aluno.nome}</div>
|
||||||
|
<div><strong>Turma:</strong> ${aluno.turma_nome || 'N/A'}</div>
|
||||||
|
<div><strong>Data Nasc:</strong> ${aluno.data_nasc ? new Date(aluno.data_nasc).toLocaleDateString('pt-BR') : 'N/A'}</div>
|
||||||
|
<div><strong>Educação Especial:</strong> ${aluno.especial ? 'Sim (' + (aluno.especial_detalhes || '') + ')' : 'Não'}</div>
|
||||||
|
<div><strong>Total de Observações:</strong> ${data.totalObservacoes}</div>
|
||||||
|
<div><strong>Emissão:</strong> ${data.dataFicha}</div>
|
||||||
|
</div>
|
||||||
|
<h3>🌟 Conquistas Pedagógicas:</h3>
|
||||||
|
<ul>
|
||||||
|
${(data.conquistas || []).map(c => `<li><strong>${c.titulo}</strong> (${c.categoria})</li>`).join('') || '<li>Nenhuma conquista pontual registrada.</li>'}
|
||||||
|
</ul>
|
||||||
|
<div style="margin-top: 50px; display: flex; justify-content: space-between; border-top: 1px solid #cbd5e1; padding-top: 20px;">
|
||||||
|
<div style="text-align: center; width: 45%;">___________________________<br>Professora Titular</div>
|
||||||
|
<div style="text-align: center; width: 45%;">___________________________<br>Coordenação Pedagógica</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
win.document.close();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Erro ao gerar ficha: ' + e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// 2. IMPORTAÇÃO CSV DE ALUNOS
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const csvImportModal = document.getElementById('csvImportModal');
|
||||||
|
const btnCloseCsvImport = document.getElementById('btnCloseCsvImport');
|
||||||
|
const csvImportTurmaSelect = document.getElementById('csvImportTurmaSelect');
|
||||||
|
const csvFileInput = document.getElementById('csvFileInput');
|
||||||
|
const csvTextContent = document.getElementById('csvTextContent');
|
||||||
|
const btnProcessCsvImport = document.getElementById('btnProcessCsvImport');
|
||||||
|
|
||||||
|
if (btnCloseCsvImport) btnCloseCsvImport.addEventListener('click', () => csvImportModal.style.display = 'none');
|
||||||
|
if (csvImportModal) csvImportModal.addEventListener('click', (e) => { if (e.target === csvImportModal) csvImportModal.style.display = 'none'; });
|
||||||
|
|
||||||
|
// Botão abrir CSV import (se presente na tela de turmas ou sidebar)
|
||||||
|
window.openCsvImportModal = () => {
|
||||||
|
csvImportModal.style.display = 'flex';
|
||||||
|
if (csvImportTurmaSelect) {
|
||||||
|
csvImportTurmaSelect.innerHTML = '';
|
||||||
|
(currentTurmas || []).forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t.id;
|
||||||
|
opt.textContent = t.nome;
|
||||||
|
csvImportTurmaSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (csvFileInput) {
|
||||||
|
csvFileInput.addEventListener('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
if (csvTextContent) csvTextContent.value = evt.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsText(file, 'UTF-8');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnProcessCsvImport) {
|
||||||
|
btnProcessCsvImport.addEventListener('click', async () => {
|
||||||
|
const turmaId = csvImportTurmaSelect ? csvImportTurmaSelect.value : null;
|
||||||
|
const csv = csvTextContent ? csvTextContent.value.trim() : '';
|
||||||
|
|
||||||
|
if (!turmaId || !csv) {
|
||||||
|
alert('Por favor, selecione a turma e forneça o conteúdo CSV.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btnProcessCsvImport.disabled = true;
|
||||||
|
btnProcessCsvImport.textContent = '⏳ Importando...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/alunos/import-csv', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ turma_id: turmaId, csv_content: csv })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Erro na importação');
|
||||||
|
|
||||||
|
alert(`✅ Sucesso! ${data.count} alunos foram importados para a turma.`);
|
||||||
|
csvImportModal.style.display = 'none';
|
||||||
|
if (typeof fetchAlunos === 'function') await fetchAlunos();
|
||||||
|
if (typeof renderAlunosList === 'function') renderAlunosList();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Erro ao importar CSV: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
btnProcessCsvImport.disabled = false;
|
||||||
|
btnProcessCsvImport.textContent = '📥 Importar Alunos';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// 3. PROTOCOLO DE CRISES & MEDIAÇÃO COMPORTAMENTAL
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const btnProtocoloCrise = document.getElementById('btnProtocoloCrise');
|
||||||
|
const criseModal = document.getElementById('criseModal');
|
||||||
|
const btnCloseCriseModal = document.getElementById('btnCloseCriseModal');
|
||||||
|
const btnOpenCrisesHistory = document.getElementById('btnOpenCrisesHistory');
|
||||||
|
const criseHistoryModal = document.getElementById('criseHistoryModal');
|
||||||
|
const btnCloseCriseHistory = document.getElementById('btnCloseCriseHistory');
|
||||||
|
const criseTurmaSelect = document.getElementById('criseTurmaSelect');
|
||||||
|
const criseAlunoInput = document.getElementById('criseAlunoInput');
|
||||||
|
const criseTipoSelect = document.getElementById('criseTipoSelect');
|
||||||
|
const criseIntensidadeSelect = document.getElementById('criseIntensidadeSelect');
|
||||||
|
const criseDescricaoInput = document.getElementById('criseDescricaoInput');
|
||||||
|
const criseMedidasInput = document.getElementById('criseMedidasInput');
|
||||||
|
const btnSalvarCrise = document.getElementById('btnSalvarCrise');
|
||||||
|
const criseWhatsappOutput = document.getElementById('criseWhatsappOutput');
|
||||||
|
const btnCopyCriseWhatsapp = document.getElementById('btnCopyCriseWhatsapp');
|
||||||
|
const btnSendWhatsappDirect = document.getElementById('btnSendWhatsappDirect');
|
||||||
|
const criseHistoryList = document.getElementById('criseHistoryList');
|
||||||
|
|
||||||
|
function populateCriseTurmas() {
|
||||||
|
if (!criseTurmaSelect) return;
|
||||||
|
criseTurmaSelect.innerHTML = '<option value="">-- Selecionar Turma --</option>';
|
||||||
|
(currentTurmas || []).forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t.id;
|
||||||
|
opt.textContent = t.nome;
|
||||||
|
criseTurmaSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnProtocoloCrise) {
|
||||||
|
btnProtocoloCrise.addEventListener('click', () => {
|
||||||
|
criseModal.style.display = 'flex';
|
||||||
|
populateCriseTurmas();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (btnCloseCriseModal) btnCloseCriseModal.addEventListener('click', () => criseModal.style.display = 'none');
|
||||||
|
if (criseModal) criseModal.addEventListener('click', (e) => { if (e.target === criseModal) criseModal.style.display = 'none'; });
|
||||||
|
|
||||||
|
if (btnOpenCrisesHistory) {
|
||||||
|
btnOpenCrisesHistory.addEventListener('click', async () => {
|
||||||
|
criseHistoryModal.style.display = 'flex';
|
||||||
|
if (!criseHistoryList) return;
|
||||||
|
criseHistoryList.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando...</p>';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/crises/list');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.length === 0) {
|
||||||
|
criseHistoryList.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhuma ocorrência registrada.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
criseHistoryList.innerHTML = data.map(c => `
|
||||||
|
<div style="background:rgba(255,255,255,0.02); border:1px solid var(--border-light); padding:12px; border-radius:8px; display:flex; flex-direction:column; gap:4px; position:relative;">
|
||||||
|
<div style="display:flex; justify-content:space-between; padding-right:24px;">
|
||||||
|
<strong style="color:#ef4444; font-size:0.9rem;">${escapeHtml(c.aluno_nome)} • ${escapeHtml(c.tipo_evento)}</strong>
|
||||||
|
<span style="font-size:0.72rem; color:var(--text-secondary);">${new Date(c.data_hora).toLocaleString('pt-BR')}</span>
|
||||||
|
</div>
|
||||||
|
<p style="margin:4px 0; font-size:0.82rem; color:var(--text-primary);">${escapeHtml(c.descricao)}</p>
|
||||||
|
<div style="font-size:0.75rem; color:var(--text-secondary);"><strong>Medidas:</strong> ${escapeHtml(c.medidas_tomadas)}</div>
|
||||||
|
<button onclick="deleteCriseRecord('${c.id}')" style="position:absolute; top:8px; right:8px; background:none; border:none; color:#ef4444; cursor:pointer;" title="Excluir">🗑️</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
criseHistoryList.innerHTML = `<p style="color:#ef4444;">Erro ao carregar: ${e.message}</p>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.deleteCriseRecord = async (id) => {
|
||||||
|
if (confirm('Deseja excluir este registro de mediação?')) {
|
||||||
|
await fetch(`/api/crises/${id}`, { method: 'DELETE' });
|
||||||
|
if (btnOpenCrisesHistory) btnOpenCrisesHistory.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (btnCloseCriseHistory) btnCloseCriseHistory.addEventListener('click', () => criseHistoryModal.style.display = 'none');
|
||||||
|
|
||||||
|
if (btnSalvarCrise) {
|
||||||
|
btnSalvarCrise.addEventListener('click', async () => {
|
||||||
|
const alunoNome = criseAlunoInput ? criseAlunoInput.value.trim() : '';
|
||||||
|
const tipo = criseTipoSelect ? criseTipoSelect.value : '';
|
||||||
|
const intensidade = criseIntensidadeSelect ? criseIntensidadeSelect.value : 'Moderada';
|
||||||
|
const desc = criseDescricaoInput ? criseDescricaoInput.value.trim() : '';
|
||||||
|
const medidas = criseMedidasInput ? criseMedidasInput.value.trim() : '';
|
||||||
|
const turmaId = criseTurmaSelect ? criseTurmaSelect.value : null;
|
||||||
|
|
||||||
|
if (!alunoNome || !desc) {
|
||||||
|
alert('Por favor, informe o nome da criança e a descrição do ocorrido.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btnSalvarCrise.disabled = true;
|
||||||
|
btnSalvarCrise.innerHTML = '⏳ Gerando Registro e Mensagem...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/crises/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
aluno_nome: alunoNome,
|
||||||
|
turma_id: turmaId,
|
||||||
|
tipo_evento: tipo,
|
||||||
|
intensidade: intensidade,
|
||||||
|
descricao: desc,
|
||||||
|
medidas_tomadas: medidas
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Falha ao registrar ocorrência');
|
||||||
|
|
||||||
|
if (criseWhatsappOutput) criseWhatsappOutput.textContent = data.mensagemWhatsapp || '';
|
||||||
|
if (btnSendWhatsappDirect) {
|
||||||
|
btnSendWhatsappDirect.href = `https://api.whatsapp.com/send?text=${encodeURIComponent(data.mensagemWhatsapp || '')}`;
|
||||||
|
btnSendWhatsappDirect.style.display = 'block';
|
||||||
|
}
|
||||||
|
alert('✅ Protocolo registrado com sucesso! Mensagem para os pais gerada.');
|
||||||
|
} catch (err) {
|
||||||
|
alert('Erro ao registrar crise: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
btnSalvarCrise.disabled = false;
|
||||||
|
btnSalvarCrise.innerHTML = '🛡️ Registrar & Gerar Mensagem WhatsApp';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnCopyCriseWhatsapp) {
|
||||||
|
btnCopyCriseWhatsapp.addEventListener('click', () => {
|
||||||
|
if (criseWhatsappOutput && criseWhatsappOutput.textContent) {
|
||||||
|
navigator.clipboard.writeText(criseWhatsappOutput.textContent);
|
||||||
|
alert('Mensagem copiada para a área de transferência!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// 4. ATAS DE REUNIÃO & CONSELHO DE CLASSE
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const btnAtasReuniao = document.getElementById('btnAtasReuniao');
|
||||||
|
const ataModal = document.getElementById('ataModal');
|
||||||
|
const btnCloseAtaModal = document.getElementById('btnCloseAtaModal');
|
||||||
|
const btnOpenAtasHistory = document.getElementById('btnOpenAtasHistory');
|
||||||
|
const atasHistoryModal = document.getElementById('atasHistoryModal');
|
||||||
|
const btnCloseAtasHistory = document.getElementById('btnCloseAtasHistory');
|
||||||
|
const ataTurmaSelect = document.getElementById('ataTurmaSelect');
|
||||||
|
const ataTipoSelect = document.getElementById('ataTipoSelect');
|
||||||
|
const ataTituloInput = document.getElementById('ataTituloInput');
|
||||||
|
const ataParticipantesInput = document.getElementById('ataParticipantesInput');
|
||||||
|
const ataPautaInput = document.getElementById('ataPautaInput');
|
||||||
|
const ataDiscussoesInput = document.getElementById('ataDiscussoesInput');
|
||||||
|
const ataDeliberacoesInput = document.getElementById('ataDeliberacoesInput');
|
||||||
|
const btnGerarAta = document.getElementById('btnGerarAta');
|
||||||
|
const ataOutputText = document.getElementById('ataOutputText');
|
||||||
|
const btnCopyAta = document.getElementById('btnCopyAta');
|
||||||
|
const btnPrintAtaPdf = document.getElementById('btnPrintAtaPdf');
|
||||||
|
const atasHistoryList = document.getElementById('atasHistoryList');
|
||||||
|
|
||||||
|
function populateAtaTurmas() {
|
||||||
|
if (!ataTurmaSelect) return;
|
||||||
|
ataTurmaSelect.innerHTML = '<option value="">-- Geral / Toda a Escola --</option>';
|
||||||
|
(currentTurmas || []).forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t.id;
|
||||||
|
opt.textContent = t.nome;
|
||||||
|
ataTurmaSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnAtasReuniao) {
|
||||||
|
btnAtasReuniao.addEventListener('click', () => {
|
||||||
|
ataModal.style.display = 'flex';
|
||||||
|
populateAtaTurmas();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (btnCloseAtaModal) btnCloseAtaModal.addEventListener('click', () => ataModal.style.display = 'none');
|
||||||
|
if (ataModal) ataModal.addEventListener('click', (e) => { if (e.target === ataModal) ataModal.style.display = 'none'; });
|
||||||
|
|
||||||
|
if (btnGerarAta) {
|
||||||
|
btnGerarAta.addEventListener('click', async () => {
|
||||||
|
const titulo = ataTituloInput ? ataTituloInput.value.trim() : '';
|
||||||
|
const tipo = ataTipoSelect ? ataTipoSelect.value : '';
|
||||||
|
const discussoes = ataDiscussoesInput ? ataDiscussoesInput.value.trim() : '';
|
||||||
|
const pauta = ataPautaInput ? ataPautaInput.value.trim() : '';
|
||||||
|
const part = ataParticipantesInput ? ataParticipantesInput.value.split(',').map(p => p.trim()) : [];
|
||||||
|
const delib = ataDeliberacoesInput ? ataDeliberacoesInput.value.trim() : '';
|
||||||
|
const turmaId = ataTurmaSelect ? ataTurmaSelect.value : null;
|
||||||
|
|
||||||
|
if (!titulo || !discussoes) {
|
||||||
|
alert('Por favor, informe o título da reunião e os tópicos discutidos.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btnGerarAta.disabled = true;
|
||||||
|
btnGerarAta.innerHTML = '⏳ Redigindo Ata Oficial com IA...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/atas/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
tipo_reuniao: tipo,
|
||||||
|
titulo: titulo,
|
||||||
|
turma_id: turmaId,
|
||||||
|
pauta: pauta,
|
||||||
|
participantes: part,
|
||||||
|
discussoes: discussoes,
|
||||||
|
deliberacoes: delib
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Falha ao redigir ata');
|
||||||
|
|
||||||
|
if (ataOutputText) ataOutputText.textContent = data.textoAta || '';
|
||||||
|
alert('✅ Ata formal redigida com sucesso!');
|
||||||
|
} catch (err) {
|
||||||
|
alert('Erro ao gerar ata: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
btnGerarAta.disabled = false;
|
||||||
|
btnGerarAta.innerHTML = '📋 Gerar Ata Oficial Formatada';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnCopyAta) {
|
||||||
|
btnCopyAta.addEventListener('click', () => {
|
||||||
|
if (ataOutputText && ataOutputText.textContent) {
|
||||||
|
navigator.clipboard.writeText(ataOutputText.textContent);
|
||||||
|
alert('Ata copiada para a área de transferência!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnPrintAtaPdf) {
|
||||||
|
btnPrintAtaPdf.addEventListener('click', () => {
|
||||||
|
if (!ataOutputText || !ataOutputText.textContent) return;
|
||||||
|
const win = window.open('', '_blank');
|
||||||
|
win.document.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Ata de Reunião - Pedagog</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Times New Roman', Times, serif; padding: 45px; color: #000; line-height: 1.8; max-width: 800px; margin: 0 auto; text-align: justify; }
|
||||||
|
h1 { text-align: center; font-size: 1.4rem; text-transform: uppercase; margin-bottom: 25px; }
|
||||||
|
.btn-p { background: #8b5cf6; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; cursor: pointer; float: right; font-family: sans-serif; }
|
||||||
|
@media print { .btn-p { display: none; } body { padding: 0; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="btn-p" onclick="window.print()">🖨️ Imprimir / Salvar PDF</button>
|
||||||
|
<h1>ATA DE REUNIÃO ESCOLAR</h1>
|
||||||
|
<div>${escapeHtml(ataOutputText.textContent).replace(/\n/g, '<br>')}</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
win.document.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnOpenAtasHistory) {
|
||||||
|
btnOpenAtasHistory.addEventListener('click', async () => {
|
||||||
|
atasHistoryModal.style.display = 'flex';
|
||||||
|
if (!atasHistoryList) return;
|
||||||
|
atasHistoryList.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando atas...</p>';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/atas/list');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.length === 0) {
|
||||||
|
atasHistoryList.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhuma ata salva.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
atasHistoryList.innerHTML = data.map(a => `
|
||||||
|
<div style="background:rgba(255,255,255,0.02); border:1px solid var(--border-light); padding:12px; border-radius:8px; display:flex; justify-content:space-between; align-items:center;">
|
||||||
|
<div>
|
||||||
|
<strong style="color:#a78bfa; font-size:0.92rem;">${escapeHtml(a.titulo)}</strong>
|
||||||
|
<div style="font-size:0.75rem; color:var(--text-secondary);">${escapeHtml(a.tipo_reuniao)} • ${new Date(a.data_reuniao).toLocaleDateString('pt-BR')}</div>
|
||||||
|
</div>
|
||||||
|
<button onclick="deleteAtaRecord('${a.id}')" style="background:none; border:none; color:#ef4444; cursor:pointer;" title="Excluir">🗑️</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
atasHistoryList.innerHTML = `<p style="color:#ef4444;">Erro: ${e.message}</p>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.deleteAtaRecord = async (id) => {
|
||||||
|
if (confirm('Deseja excluir esta ata?')) {
|
||||||
|
await fetch(`/api/atas/${id}`, { method: 'DELETE' });
|
||||||
|
if (btnOpenAtasHistory) btnOpenAtasHistory.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (btnCloseAtasHistory) btnCloseAtasHistory.addEventListener('click', () => atasHistoryModal.style.display = 'none');
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// 5. STICKERS GAMIFICADOS & CARTELA A4 DE IMPRESSÃO
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const btnStickersConquistas = document.getElementById('btnStickersConquistas');
|
||||||
|
const stickerModal = document.getElementById('stickerModal');
|
||||||
|
const btnCloseStickerModal = document.getElementById('btnCloseStickerModal');
|
||||||
|
const stickerTurmaSelect = document.getElementById('stickerTurmaSelect');
|
||||||
|
const stickerAlunoInput = document.getElementById('stickerAlunoInput');
|
||||||
|
const stickerCategoriaSelect = document.getElementById('stickerCategoriaSelect');
|
||||||
|
const stickerTituloInput = document.getElementById('stickerTituloInput');
|
||||||
|
const stickerDescricaoInput = document.getElementById('stickerDescricaoInput');
|
||||||
|
const btnEmitirSticker = document.getElementById('btnEmitirSticker');
|
||||||
|
const stickersListGrid = document.getElementById('stickersListGrid');
|
||||||
|
const btnPrintStickerSheet = document.getElementById('btnPrintStickerSheet');
|
||||||
|
|
||||||
|
function populateStickerTurmas() {
|
||||||
|
if (!stickerTurmaSelect) return;
|
||||||
|
stickerTurmaSelect.innerHTML = '<option value="">-- Selecionar Turma --</option>';
|
||||||
|
(currentTurmas || []).forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t.id;
|
||||||
|
opt.textContent = t.nome;
|
||||||
|
stickerTurmaSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStickersGrid() {
|
||||||
|
if (!stickersListGrid) return;
|
||||||
|
stickersListGrid.innerHTML = '<p style="text-align:center; color:var(--text-secondary); grid-column:1/-1;">Carregando...</p>';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/stickers/list');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.length === 0) {
|
||||||
|
stickersListGrid.innerHTML = '<p style="text-align:center; color:var(--text-secondary); grid-column:1/-1;">Nenhum sticker emitido ainda.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stickersListGrid.innerHTML = data.map(s => `
|
||||||
|
<div style="background:rgba(255,255,255,0.03); border:2px dashed rgba(245,158,11,0.4); border-radius:10px; padding:10px; text-align:center; display:flex; flex-direction:column; align-items:center; gap:4px; position:relative;">
|
||||||
|
<span style="font-size:1.8rem;">${s.icone || '🌟'}</span>
|
||||||
|
<strong style="font-size:0.8rem; color:#fbbf24;">${escapeHtml(s.titulo)}</strong>
|
||||||
|
<span style="font-size:0.75rem; color:var(--text-primary);">${escapeHtml(s.aluno_nome)}</span>
|
||||||
|
${s.qr_code_url ? `<img src="${s.qr_code_url}" style="width:50px; height:50px; margin-top:4px; border-radius:4px;" title="QR Code para a família">` : ''}
|
||||||
|
<button onclick="deleteStickerRecord('${s.id}')" style="position:absolute; top:4px; right:4px; background:none; border:none; color:#ef4444; font-size:0.8rem; cursor:pointer;" title="Excluir">✕</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
stickersListGrid.innerHTML = `<p style="color:#ef4444; grid-column:1/-1;">Erro: ${e.message}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnStickersConquistas) {
|
||||||
|
btnStickersConquistas.addEventListener('click', () => {
|
||||||
|
stickerModal.style.display = 'flex';
|
||||||
|
populateStickerTurmas();
|
||||||
|
loadStickersGrid();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (btnCloseStickerModal) btnCloseStickerModal.addEventListener('click', () => stickerModal.style.display = 'none');
|
||||||
|
if (stickerModal) stickerModal.addEventListener('click', (e) => { if (e.target === stickerModal) stickerModal.style.display = 'none'; });
|
||||||
|
|
||||||
|
window.deleteStickerRecord = async (id) => {
|
||||||
|
if (confirm('Deseja excluir este sticker?')) {
|
||||||
|
await fetch(`/api/stickers/${id}`, { method: 'DELETE' });
|
||||||
|
loadStickersGrid();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (btnEmitirSticker) {
|
||||||
|
btnEmitirSticker.addEventListener('click', async () => {
|
||||||
|
const alunoNome = stickerAlunoInput ? stickerAlunoInput.value.trim() : '';
|
||||||
|
const titulo = stickerTituloInput ? stickerTituloInput.value.trim() : '';
|
||||||
|
const cat = stickerCategoriaSelect ? stickerCategoriaSelect.value : 'Cooperação & Gentileza';
|
||||||
|
const desc = stickerDescricaoInput ? stickerDescricaoInput.value.trim() : '';
|
||||||
|
const turmaId = stickerTurmaSelect ? stickerTurmaSelect.value : null;
|
||||||
|
|
||||||
|
if (!alunoNome || !titulo || !desc) {
|
||||||
|
alert('Por favor, informe a criança, o título da conquista e a descrição.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let icone = '🌟';
|
||||||
|
if (cat.includes('Cooperação')) icone = '🤝';
|
||||||
|
else if (cat.includes('Curiosidade')) icone = '🔬';
|
||||||
|
else if (cat.includes('Autonomia')) icone = '🌱';
|
||||||
|
else if (cat.includes('Artística')) icone = '🎨';
|
||||||
|
else if (cat.includes('Coragem')) icone = '⭐';
|
||||||
|
else if (cat.includes('Roda')) icone = '🗣️';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/stickers/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
aluno_nome: alunoNome,
|
||||||
|
turma_id: turmaId,
|
||||||
|
titulo: titulo,
|
||||||
|
categoria: cat,
|
||||||
|
icone: icone,
|
||||||
|
descricao: desc
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Falha ao emitir sticker');
|
||||||
|
alert('✅ Conquista registrada com sucesso!');
|
||||||
|
loadStickersGrid();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Erro: ' + err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imprimir Cartela A4 com Stickers Recortáveis
|
||||||
|
if (btnPrintStickerSheet) {
|
||||||
|
btnPrintStickerSheet.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/stickers/list');
|
||||||
|
const stickers = await res.json();
|
||||||
|
if (stickers.length === 0) {
|
||||||
|
alert('Emita ao menos um sticker antes de imprimir a cartela.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const win = window.open('', '_blank');
|
||||||
|
let stickersHtml = '';
|
||||||
|
stickers.forEach(s => {
|
||||||
|
stickersHtml += `
|
||||||
|
<div style="border: 2px dashed #f59e0b; border-radius: 12px; padding: 12px; text-align: center; background: #fffbeb; page-break-inside: avoid; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||||
|
<span style="font-size: 2.2rem;">${s.icone || '🌟'}</span>
|
||||||
|
<strong style="font-size: 0.95rem; color: #b45309; margin-top: 4px;">${escapeHtml(s.titulo)}</strong>
|
||||||
|
<div style="font-size: 0.85rem; font-weight: bold; color: #1e293b;">${escapeHtml(s.aluno_nome)}</div>
|
||||||
|
<p style="font-size: 0.72rem; color: #64748b; margin: 4px 0 6px 0;">${escapeHtml(s.descricao)}</p>
|
||||||
|
${s.qr_code_url ? `<img src="${s.qr_code_url}" style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #e2e8f0;">` : ''}
|
||||||
|
<div style="font-size: 0.65rem; color: #94a3b8; margin-top: 4px;">Pedagog • Conquista Escolar</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
win.document.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Cartela de Adesivos - Pedagog</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 25px; margin: 0; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
|
||||||
|
.btn-p { background: #f59e0b; color: white; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; cursor: pointer; float: right; margin-bottom: 15px; }
|
||||||
|
@media print { .btn-p { display: none; } body { padding: 0; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="btn-p" onclick="window.print()">🖨️ Imprimir em Papel Adesivo / A4</button>
|
||||||
|
<h2 style="margin: 0 0 15px 0; color: #b45309;">🌟 Cartela de Conquistas da Turma</h2>
|
||||||
|
<div class="grid">
|
||||||
|
${stickersHtml}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
win.document.close();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Erro ao imprimir cartela: ' + e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
+362
-27
@@ -73,56 +73,64 @@
|
|||||||
<!-- Divisor -->
|
<!-- Divisor -->
|
||||||
<div class="sidebar-divider"></div>
|
<div class="sidebar-divider"></div>
|
||||||
|
|
||||||
<!-- Seção: Observações -->
|
<!-- Seção: Observações & Rotina -->
|
||||||
<div class="sidebar-actions-obs" style="display: flex; flex-wrap: wrap; gap: 6px; padding: 0 16px 4px 16px;">
|
<div class="sidebar-actions-obs" style="display: flex; flex-wrap: wrap; gap: 6px; padding: 0 16px 4px 16px;">
|
||||||
<!-- Botão: Minhas Observações -->
|
<!-- Botão: Minhas Observações -->
|
||||||
<button class="btn-observacoes" id="btnObservacoes" style="border-color: #f43f5e; color: #f43f5e; background: rgba(244, 63, 94, 0.05);">
|
<button class="btn-observacoes" id="btnObservacoes" style="border-color: #f43f5e; color: #f43f5e; background: rgba(244, 63, 94, 0.05);" title="Ver e gerenciar observações registradas">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Minhas Observações</span>
|
<span>Minhas Observações</span>
|
||||||
</button>
|
</button>
|
||||||
<!-- Botão: Obs. Especiais -->
|
<!-- Botão: Obs. Especiais -->
|
||||||
<button class="btn-observacoes" id="btnObsEspeciais" style="border-color: #f59e0b; color: #f59e0b; background: rgba(245, 158, 11, 0.05);">
|
<button class="btn-observacoes" id="btnObsEspeciais" style="border-color: #f59e0b; color: #f59e0b; background: rgba(245, 158, 11, 0.05);" title="Observações de Educação Especial e Inclusiva">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Obs. Educação Especial</span>
|
<span>Obs. Especial</span>
|
||||||
|
</button>
|
||||||
|
<!-- Botão: Protocolo de Crises -->
|
||||||
|
<button class="btn-observacoes" id="btnProtocoloCrise" style="border-color: #ef4444; color: #ef4444; background: rgba(239, 68, 68, 0.08);" title="Protocolo de Acolhimento & Mediação de Crises">
|
||||||
|
<span style="font-size: 1rem;">🚨</span>
|
||||||
|
<span>Protocolo de Crise</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Divisor -->
|
<!-- Divisor -->
|
||||||
<div class="sidebar-divider"></div>
|
<div class="sidebar-divider"></div>
|
||||||
|
|
||||||
<!-- Seção: Minha Turma -->
|
<!-- Seção: Minha Turma & Gestão -->
|
||||||
<div class="sidebar-actions-turma" style="display: flex; flex-wrap: wrap; gap: 6px; padding: 0 16px 4px 16px;">
|
<div class="sidebar-actions-turma" style="display: flex; flex-wrap: wrap; gap: 6px; padding: 0 16px 4px 16px;">
|
||||||
<button class="btn-observacoes" id="btnMinhaTurma" style="border-color: #3b82f6; color: #3b82f6; background: rgba(59, 130, 246, 0.05);">
|
<button class="btn-observacoes" id="btnMinhaTurma" style="border-color: #3b82f6; color: #3b82f6; background: rgba(59, 130, 246, 0.05);" title="Gerenciar turmas e lista de alunos">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Minha Turma</span>
|
<span>Minha Turma</span>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Botão: Stickers Gamificados -->
|
||||||
|
<button class="btn-observacoes" id="btnStickersConquistas" style="border-color: #f59e0b; color: #fbbf24; background: rgba(245, 158, 11, 0.08);" title="Stickers de Conquistas & Impressão com QR Code">
|
||||||
|
<span style="font-size: 1rem;">🌟</span>
|
||||||
|
<span>Stickers & Conquistas</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Divisor -->
|
<!-- Divisor -->
|
||||||
<div class="sidebar-divider"></div>
|
<div class="sidebar-divider"></div>
|
||||||
|
|
||||||
<!-- Seção: Relatórios -->
|
<!-- Seção: Relatórios & Atas -->
|
||||||
<div class="sidebar-actions-reports" style="display: flex; flex-wrap: wrap; gap: 6px; padding: 0 16px 4px 16px;">
|
<div class="sidebar-actions-reports" style="display: flex; flex-wrap: wrap; gap: 6px; padding: 0 16px 4px 16px;">
|
||||||
<!-- Botão: Emitir Relatório -->
|
<!-- Botão: Emitir Relatório -->
|
||||||
<button class="btn-observacoes" id="btnEmitirRelatorio" style="border-color: var(--brand-green); color: var(--brand-green); background: rgba(16, 163, 127, 0.05);">
|
<button class="btn-observacoes" id="btnEmitirRelatorio" style="border-color: var(--brand-green); color: var(--brand-green); background: rgba(16, 163, 127, 0.05);" title="Gerar relatório individual ou em lote">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Emitir Relatório</span>
|
<span>Emitir Relatórios</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Botão: Modelos de Relatório -->
|
<!-- Botão: Atas de Reunião -->
|
||||||
<button class="btn-observacoes" id="btnModelosRelatorio" style="border-color: #14b8a6; color: #14b8a6; background: rgba(20, 184, 166, 0.05);">
|
<button class="btn-observacoes" id="btnAtasReuniao" style="border-color: #8b5cf6; color: #a78bfa; background: rgba(139, 92, 246, 0.08);" title="Gerar e salvar Atas de Reuniões e Conselho">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
<span style="font-size: 1rem;">📋</span>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
<span>Atas de Reunião</span>
|
||||||
</svg>
|
|
||||||
<span>Modelos de Relatório</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2154,7 +2162,7 @@
|
|||||||
<div style="background: var(--bg-secondary); padding: 14px; border-radius: 12px; display: flex; gap: 14px; border: 1px solid var(--border-light); align-items: center;">
|
<div style="background: var(--bg-secondary); padding: 14px; border-radius: 12px; display: flex; gap: 14px; border: 1px solid var(--border-light); align-items: center;">
|
||||||
<img id="videoMindThumbnail" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail" style="width: 120px; aspect-ratio: 16/9; object-fit: cover; border-radius: 8px; border: 1px solid var(--border-light);">
|
<img id="videoMindThumbnail" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail" style="width: 120px; aspect-ratio: 16/9; object-fit: cover; border-radius: 8px; border: 1px solid var(--border-light);">
|
||||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||||
<h4 id="videoMindTitle" style="margin: 0; color: var(--text-primary); font-size: 1rem; font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">Título do Vídeo</h4>
|
<h4 id="videoMindTitle" style="margin: 0; color: var(--text-primary); font-size: 1rem; font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">Título do Vídeo</h4>
|
||||||
<span id="videoMindAuthor" style="font-size: 0.85rem; color: var(--text-secondary);">Canal</span>
|
<span id="videoMindAuthor" style="font-size: 0.85rem; color: var(--text-secondary);">Canal</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2664,27 +2672,354 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; gap: 12px;">
|
<div style="display: flex; align-items: center; gap: 12px;">
|
||||||
<span id="comicsPresCounter" style="background: rgba(255,255,255,0.15); padding: 4px 12px; border-radius: 20px; font-size: 0.85rem; font-weight: 700; color: #fef08a;">1 / 5</span>
|
<span id="comicsPresCounter" style="background: rgba(255,255,255,0.15); padding: 4px 12px; border-radius: 20px; font-size: 0.85rem; font-weight: 700; color: #fef08a;">1 / 5</span>
|
||||||
<button type="button" id="btnCloseComicsPresModal" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 1.2rem; display: flex; align-items: center; justify-content: center; transition: all 0.2s;">✕</button>
|
<!-- ========================================== -->
|
||||||
|
<!-- MODAL: PROTOCOLO DE CRISES & MEDIAÇÃO -->
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<div id="criseModal" class="settings-modal" style="display: none;">
|
||||||
|
<div class="settings-modal-content" style="max-width: 900px; width: 95%; max-height: 92vh; display: flex; flex-direction: column;">
|
||||||
|
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<span style="font-size: 1.4rem;">🚨</span>
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0; font-family: 'Outfit', sans-serif; font-size: 1.15rem; color: #ef4444;">Protocolo de Acolhimento & Mediação de Crise</h3>
|
||||||
|
<span style="font-size: 0.75rem; color: var(--text-secondary);">Registro seguro de ocorrência, checklist de intervenção e mensagem acolhedora para WhatsApp dos pais</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<button type="button" id="btnOpenCrisesHistory" style="background: rgba(239, 68, 68, 0.12); border: 1px solid rgba(239, 68, 68, 0.3); color: #f87171; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; font-weight: 600;">🕒 Histórico</button>
|
||||||
|
<button type="button" id="btnCloseCriseModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="comics-pres-stage">
|
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: row; gap: 20px; flex: 1; flex-wrap: wrap;">
|
||||||
<div class="comics-pres-card">
|
|
||||||
<img id="comicsPresImage" class="comics-pres-img" src="" alt="Quadrinho em Apresentação">
|
<!-- Formulário de Registro -->
|
||||||
<div id="comicsPresCaption" class="comics-pres-caption">
|
<div style="flex: 1.1; min-width: 300px; display: flex; flex-direction: column; gap: 12px;">
|
||||||
Falas do quadrinho aparecerão aqui...
|
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Turma</label>
|
||||||
|
<select id="criseTurmaSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||||
|
<option value="">-- Selecionar Turma --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Nome da Criança</label>
|
||||||
|
<input type="text" id="criseAlunoInput" placeholder="Ex: Lucas, Sofia..." class="obs-input" style="width: 100%; margin-top: 4px; padding: 8px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Tipo de Ocorrência</label>
|
||||||
|
<select id="criseTipoSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||||
|
<option value="Desregulação Sensorial / Emocional">Desregulação Sensorial / Emocional</option>
|
||||||
|
<option value="Conflito Físico / Mordida">Conflito Físico / Disputa / Mordida</option>
|
||||||
|
<option value="Queda / Pequeno Acidente">Queda / Pequeno Acidente / Arranhão</option>
|
||||||
|
<option value="Choro Intenso / Adaptação">Choro Intenso / Adaptação Escolar</option>
|
||||||
|
<option value="Recusa Alimentar / Sono">Recusa Alimentar / Sono</option>
|
||||||
|
<option value="Outro">Outro Comportamento Atípico</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Intensidade</label>
|
||||||
|
<select id="criseIntensidadeSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||||
|
<option value="Leve">🟢 Leve (Resolvido rápido)</option>
|
||||||
|
<option value="Moderada" selected>🟡 Moderada (Exigiu acolhimento)</option>
|
||||||
|
<option value="Alta">🔴 Alta (Necessitou apoio coordenador)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checklist de Ação Imediata -->
|
||||||
|
<div style="background: rgba(239,68,68,0.06); border: 1px solid rgba(239,68,68,0.2); border-radius: 8px; padding: 10px;">
|
||||||
|
<label style="color: #f87171; font-size: 0.72rem; font-weight: 700; text-transform: uppercase; display: block; margin-bottom: 6px;">⚡ Checklist de Ação Imediata:</label>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; font-size: 0.75rem; color: var(--text-primary);">
|
||||||
|
<label><input type="checkbox" id="chkSeguranca" checked> 1. Garantir segurança física</label>
|
||||||
|
<label><input type="checkbox" id="chkReduzirEstimulos" checked> 2. Reduzir barulho/luz</label>
|
||||||
|
<label><input type="checkbox" id="chkAgua"> 3. Oferecer água / respirar</label>
|
||||||
|
<label><input type="checkbox" id="chkAcolhimento" checked> 4. Abraço / Escuta afetiva</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Descrição do Ocorrido & Gatilho</label>
|
||||||
|
<textarea id="criseDescricaoInput" placeholder="Descreva brevemente o que aconteceu e o que disparou o momento..." class="obs-input" style="width: 100%; min-height: 55px; padding: 8px; font-size: 0.85rem; resize: vertical;"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Medidas Tomadas & Acolhimento</label>
|
||||||
|
<input type="text" id="criseMedidasInput" placeholder="Ex: Criança conduzida ao cantinho da leitura, acalmou-se em 5 min..." class="obs-input" style="width: 100%; padding: 8px; font-size: 0.85rem;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" id="btnSalvarCrise" style="width: 100%; padding: 10px; background: #ef4444; color: white; border: none; border-radius: 8px; font-weight: 700; font-size: 0.9rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px;">
|
||||||
|
<span>🛡️ Registrar & Gerar Mensagem WhatsApp</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lado Direito: Mensagem para a Família & Resolução -->
|
||||||
|
<div style="flex: 1; min-width: 280px; display: flex; flex-direction: column; gap: 12px; background: rgba(0,0,0,0.08); padding: 14px; border-radius: 10px; border: 1px solid var(--border-light);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<strong style="color: #34d399; font-size: 0.85rem;">📱 Mensagem Humanizada para a Família:</strong>
|
||||||
|
<button type="button" id="btnCopyCriseWhatsapp" style="background: rgba(16,185,129,0.15); border: 1px solid rgba(16,185,129,0.3); color: #34d399; padding: 3px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer;">📋 Copiar Texto</button>
|
||||||
|
</div>
|
||||||
|
<div id="criseWhatsappOutput" style="font-size: 0.85rem; line-height: 1.5; color: var(--text-primary); white-space: pre-wrap; background: rgba(255,255,255,0.03); padding: 12px; border-radius: 8px; border: 1px dashed var(--border-light); flex: 1; min-height: 140px;">
|
||||||
|
A mensagem para os pais aparecerá aqui com tom acolhedor e transparente após clicar em "Registrar".
|
||||||
|
</div>
|
||||||
|
<a id="btnSendWhatsappDirect" href="#" target="_blank" style="display: none; background: #10b981; color: white; text-decoration: none; padding: 8px; border-radius: 6px; font-size: 0.8rem; font-weight: 700; text-align: center;">
|
||||||
|
💬 Abrir no WhatsApp Web / Celular
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="comics-pres-nav">
|
<!-- Modal: Histórico de Crises -->
|
||||||
<button type="button" id="btnPresPrev" class="btn-pres-nav">
|
<div id="criseHistoryModal" class="settings-modal" style="display: none; z-index: 10001;">
|
||||||
<span>⬅️ Anterior</span>
|
<div class="settings-modal-content" style="max-width: 600px; width: 95%; max-height: 80vh; display: flex; flex-direction: column;">
|
||||||
|
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h3 style="margin: 0; font-family: 'Outfit', sans-serif; font-size: 1.1rem; color: #f87171;">🕒 Histórico de Ocorrências e Mediações</h3>
|
||||||
|
<button type="button" id="btnCloseCriseHistory" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="criseHistoryList" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; flex: 1;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<!-- MODAL: ATAS DE REUNIÃO & CONSELHO -->
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<div id="ataModal" class="settings-modal" style="display: none;">
|
||||||
|
<div class="settings-modal-content" style="max-width: 950px; width: 95%; max-height: 92vh; display: flex; flex-direction: column;">
|
||||||
|
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<span style="font-size: 1.4rem;">📋</span>
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0; font-family: 'Outfit', sans-serif; font-size: 1.15rem; color: #a78bfa;">Atas de Reunião & Conselho de Classe</h3>
|
||||||
|
<span style="font-size: 0.75rem; color: var(--text-secondary);">Geração de atas escolares formais, deliberações e pautas com formatação oficial</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<button type="button" id="btnOpenAtasHistory" style="background: rgba(139, 92, 246, 0.12); border: 1px solid rgba(139, 92, 246, 0.3); color: #c084fc; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; font-weight: 600;">🕒 Histórico de Atas</button>
|
||||||
|
<button type="button" id="btnCloseAtaModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: row; gap: 20px; flex: 1; flex-wrap: wrap;">
|
||||||
|
|
||||||
|
<!-- Formulário da Ata -->
|
||||||
|
<div style="flex: 1; min-width: 320px; display: flex; flex-direction: column; gap: 10px;">
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Tipo de Reunião</label>
|
||||||
|
<select id="ataTipoSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||||
|
<option value="Reunião de Pais e Mestres">Reunião de Pais e Mestres</option>
|
||||||
|
<option value="Conselho de Classe / Avaliação">Conselho de Classe / Avaliação</option>
|
||||||
|
<option value="HTPC / Formação Continuada">HTPC / Formação Continuada</option>
|
||||||
|
<option value="Alinhamento Pedagógico Interno">Alinhamento Pedagógico Interno</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group" style="flex: 1; min-width: 140px;">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Turma</label>
|
||||||
|
<select id="ataTurmaSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||||
|
<option value="">-- Geral / Toda a Escola --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Título da Reunião</label>
|
||||||
|
<input type="text" id="ataTituloInput" placeholder="Ex: Reunião de Acolhimento do 1º Bimestre..." class="obs-input" style="width: 100%; padding: 8px; font-size: 0.85rem;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Participantes Presentes</label>
|
||||||
|
<input type="text" id="ataParticipantesInput" placeholder="Ex: Professora Camila, Coordenação, Famílias..." class="obs-input" style="width: 100%; padding: 8px; font-size: 0.85rem;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Pauta da Reunião</label>
|
||||||
|
<input type="text" id="ataPautaInput" placeholder="Ex: Adaptação das crianças, rotina de alimentação, passeios..." class="obs-input" style="width: 100%; padding: 8px; font-size: 0.85rem;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Discussões & Tópicos Abordados</label>
|
||||||
|
<textarea id="ataDiscussoesInput" placeholder="Resuma os pontos tratados na reunião..." class="obs-input" style="width: 100%; min-height: 60px; padding: 8px; font-size: 0.85rem; resize: vertical;"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Deliberações & Decisões Acordadas</label>
|
||||||
|
<input type="text" id="ataDeliberacoesInput" placeholder="Ex: Combinados de pontualidade, envio de agenda..." class="obs-input" style="width: 100%; padding: 8px; font-size: 0.85rem;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" id="btnGerarAta" style="width: 100%; padding: 10px; background: #8b5cf6; color: white; border: none; border-radius: 8px; font-weight: 700; font-size: 0.9rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px;">
|
||||||
|
<span>📋 Gerar Ata Oficial Formatada</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" id="btnPresNext" class="btn-pres-nav" style="background: var(--brand-pink, #db2777); font-weight: 700;">
|
|
||||||
<span>Próximo ➡️</span>
|
</div>
|
||||||
|
|
||||||
|
<!-- Visualizador da Ata Formatada -->
|
||||||
|
<div style="flex: 1.2; min-width: 320px; display: flex; flex-direction: column; gap: 10px; background: rgba(0,0,0,0.08); padding: 14px; border-radius: 10px; border: 1px solid var(--border-light);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<strong style="color: #a78bfa; font-size: 0.85rem;">📄 Documento Oficial da Ata:</strong>
|
||||||
|
<div style="display: flex; gap: 6px;">
|
||||||
|
<button type="button" id="btnCopyAta" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 3px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer;">📋 Copiar</button>
|
||||||
|
<button type="button" id="btnPrintAtaPdf" style="background: #8b5cf6; color: white; border: none; padding: 3px 10px; border-radius: 4px; font-size: 0.72rem; font-weight: 700; cursor: pointer;">📄 Imprimir PDF</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="ataOutputText" style="font-family: 'Outfit', sans-serif; font-size: 0.88rem; line-height: 1.6; color: var(--text-primary); white-space: pre-wrap; background: rgba(255,255,255,0.03); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); flex: 1; min-height: 240px; overflow-y: auto;">
|
||||||
|
Preencha os dados da reunião e clique em "Gerar Ata Oficial" para redigir o documento formal.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Histórico de Atas -->
|
||||||
|
<div id="atasHistoryModal" class="settings-modal" style="display: none; z-index: 10001;">
|
||||||
|
<div class="settings-modal-content" style="max-width: 600px; width: 95%; max-height: 80vh; display: flex; flex-direction: column;">
|
||||||
|
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h3 style="margin: 0; font-family: 'Outfit', sans-serif; font-size: 1.1rem; color: #a78bfa;">🕒 Histórico de Atas de Reunião</h3>
|
||||||
|
<button type="button" id="btnCloseAtasHistory" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="atasHistoryList" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; flex: 1;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<!-- MODAL: STICKERS GAMIFICADOS & SHEET A4 -->
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<div id="stickerModal" class="settings-modal" style="display: none;">
|
||||||
|
<div class="settings-modal-content" style="max-width: 980px; width: 95%; max-height: 92vh; display: flex; flex-direction: column;">
|
||||||
|
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<span style="font-size: 1.4rem;">🌟</span>
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0; font-family: 'Outfit', sans-serif; font-size: 1.15rem; color: #fbbf24;">Stickers Gamificados & Conquistas Pedagógicas</h3>
|
||||||
|
<span style="font-size: 0.75rem; color: var(--text-secondary);">Emissão de badges de conquistas, cartela de adesivos A4 para impressão e QR Code para os pais</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="btnCloseStickerModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: row; gap: 20px; flex: 1; flex-wrap: wrap;">
|
||||||
|
|
||||||
|
<!-- Conceder Sticker -->
|
||||||
|
<div style="flex: 1; min-width: 300px; display: flex; flex-direction: column; gap: 12px;">
|
||||||
|
<h4 style="margin: 0; color: var(--text-primary); font-size: 0.92rem; border-bottom: 1px dashed var(--border-light); padding-bottom: 4px;">🏆 Conceder Conquista</h4>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<div class="settings-group" style="flex: 1;">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Turma</label>
|
||||||
|
<select id="stickerTurmaSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||||
|
<option value="">-- Selecionar Turma --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group" style="flex: 1;">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Criança</label>
|
||||||
|
<input type="text" id="stickerAlunoInput" placeholder="Ex: Arthur, Valentina..." class="obs-input" style="width: 100%; margin-top: 4px; padding: 8px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Categoria do Sticker</label>
|
||||||
|
<select id="stickerCategoriaSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
|
||||||
|
<option value="Cooperação & Gentileza">🤝 Cooperação & Gentileza (Ajudou o colega)</option>
|
||||||
|
<option value="Curiosidade & Ciência">🔬 Curiosidade & Ciência (Explorador nato)</option>
|
||||||
|
<option value="Autonomia & Cuidado">🌱 Autonomia & Cuidado (Guardou os brinquedos)</option>
|
||||||
|
<option value="Expressão Artística">🎨 Expressão Artística (Criatividade e cores)</option>
|
||||||
|
<option value="Superação & Coragem">⭐ Superação & Coragem (Tentou algo novo)</option>
|
||||||
|
<option value="Roda de Conversa">🗣️ Roda de Conversa (Ótimo ouvinte e narrador)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Título do Sticker</label>
|
||||||
|
<input type="text" id="stickerTituloInput" value="Guardião da Gentileza" class="obs-input" style="width: 100%; margin-top: 4px; padding: 8px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Descrição da Conquista</label>
|
||||||
|
<textarea id="stickerDescricaoInput" placeholder="Ex: Hoje o Arthur compartilhou os blocos e acolheu um amigo que estava triste..." class="obs-input" style="width: 100%; min-height: 50px; padding: 8px; font-size: 0.85rem; resize: vertical;"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" id="btnEmitirSticker" style="width: 100%; padding: 10px; background: #f59e0b; color: white; border: none; border-radius: 8px; font-weight: 700; font-size: 0.9rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px;">
|
||||||
|
<span>🌟 Salvar Conquista & Gerar Badge</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Visualizador da Cartela de Impressão A4 -->
|
||||||
|
<div style="flex: 1.2; min-width: 320px; display: flex; flex-direction: column; gap: 10px; background: rgba(0,0,0,0.08); padding: 14px; border-radius: 10px; border: 1px solid var(--border-light);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<strong style="color: #fbbf24; font-size: 0.85rem;">🖨️ Cartela de Adesivos A4 (Papel Autocolante):</strong>
|
||||||
|
<button type="button" id="btnPrintStickerSheet" style="background: #f59e0b; color: white; border: none; padding: 4px 10px; border-radius: 4px; font-size: 0.75rem; font-weight: 700; cursor: pointer;">📄 Imprimir Cartela A4</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="stickersListGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; max-height: 380px; overflow-y: auto; padding: 6px;">
|
||||||
|
<!-- Stickers renderizados dinamicamente -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<!-- MODAL: IMPORTAÇÃO CSV DE ALUNOS -->
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<div id="csvImportModal" class="settings-modal" style="display: none; z-index: 20002;">
|
||||||
|
<div class="settings-modal-content" style="max-width: 550px; width: 95%; max-height: 85vh; display: flex; flex-direction: column;">
|
||||||
|
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h3 style="margin: 0; font-family: 'Outfit', sans-serif; font-size: 1.1rem; color: #3b82f6;">📊 Importação de Alunos via CSV</h3>
|
||||||
|
<button type="button" id="btnCloseCsvImport" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-modal-body" style="padding: 20px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto;">
|
||||||
|
<p style="margin: 0; font-size: 0.82rem; color: var(--text-secondary);">
|
||||||
|
Carregue o arquivo CSV da secretaria ou cole a lista de chamada abaixo. O sistema reconhece colunas como: <strong>Nome, Data Nasc, Responsáveis, Contato, Alergias</strong>.
|
||||||
|
</p>
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Turma Destino</label>
|
||||||
|
<select id="csvImportTurmaSelect" class="obs-select" style="width: 100%; margin-top: 4px;"></select>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Arquivo CSV</label>
|
||||||
|
<input type="file" id="csvFileInput" accept=".csv,.txt" style="width: 100%; margin-top: 4px; font-size: 0.85rem;">
|
||||||
|
</div>
|
||||||
|
<div class="settings-group">
|
||||||
|
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Ou cole o conteúdo CSV:</label>
|
||||||
|
<textarea id="csvTextContent" placeholder="Nome,Data_Nasc,Pais,Alergias Arthur Silva,15/03/2020,Maria Silva,Nenhuma Valentina Santos,22/07/2020,Carlos Santos,Alergia a lactose" class="obs-input" style="width: 100%; min-height: 100px; padding: 8px; font-size: 0.8rem; font-family: monospace;"></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="btnProcessCsvImport" style="width: 100%; padding: 10px; background: #3b82f6; color: white; border: none; border-radius: 8px; font-weight: 700; font-size: 0.9rem; cursor: pointer;">
|
||||||
|
📥 Importar Alunos
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<!-- MODAL: TIMELINE DO ALUNO & DOSSIÊ -->
|
||||||
|
<!-- ========================================== -->
|
||||||
|
<div id="alunoTimelineModal" class="settings-modal" style="display: none; z-index: 20002;">
|
||||||
|
<div class="settings-modal-content" style="max-width: 750px; width: 95%; max-height: 88vh; display: flex; flex-direction: column;">
|
||||||
|
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<h3 id="timelineAlunoNome" style="margin: 0; font-family: 'Outfit', sans-serif; font-size: 1.15rem; color: #3b82f6;">Dossiê & Linha do Tempo da Criança</h3>
|
||||||
|
<span id="timelineAlunoInfo" style="font-size: 0.75rem; color: var(--text-secondary);">Observações, Conquistas, Histórias e Relatórios</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<button type="button" id="btnExportFichaConselho" style="background: rgba(59,130,246,0.15); border: 1px solid rgba(59,130,246,0.3); color: #60a5fa; padding: 4px 8px; border-radius: 6px; font-size: 0.72rem; font-weight: 600; cursor: pointer;">📄 Ficha Conselho</button>
|
||||||
|
<button type="button" id="btnCloseAlunoTimeline" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; flex: 1;">
|
||||||
|
<div id="timelineContentArea" style="display: flex; flex-direction: column; gap: 10px;">
|
||||||
|
<!-- Itens da timeline renderizados dinamicamente -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bibliotecas externas para renderizar markdown, PDFs, imagens e compactação ZIP -->
|
<!-- Bibliotecas externas para renderizar markdown, PDFs, imagens e compactação ZIP -->
|
||||||
|
|||||||
@@ -3157,6 +3157,100 @@ async function initDatabase() {
|
|||||||
tags TEXT[], -- array de tags para busca
|
tags TEXT[], -- array de tags para busca
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
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)...');
|
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
|
// Rota coringa para redirecionar para index.html
|
||||||
app.get('*', requireAuth, (req, res) => {
|
app.get('*', requireAuth, (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||||
|
|||||||
Reference in New Issue
Block a user