feat: Implementação completa dos módulos profissionais (Dossiê, Crises, Atas, Stickers, CSV e MindLab)

This commit is contained in:
2026-08-31 11:39:10 +00:00
parent 837762c1af
commit 549d4432ef
3 changed files with 1851 additions and 35 deletions
+724 -2
View File
@@ -3231,12 +3231,13 @@ const initApp = () => {
const turmaObj = currentTurmas.find(t => t.id === c.turma_id);
const turmaNome = turmaObj ? turmaObj.nome : 'Sem turma';
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 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>
<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="deleteChild('${c.id}')" style="background: none; border: none; color: #ef4444; cursor: pointer; padding: 4px;" title="Excluir">🗑</button>
</div>
@@ -10822,3 +10823,724 @@ document.addEventListener('DOMContentLoaded', () => {
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);
}
});
}
});