☰
Dashboard Analytics
🔔
📄 Exportar PDF
Compartilhar PDF
🔐 Ativar login por biometria (Face ID / Digital)
×
Notificações Marcar todas como lidas
📅 Crianças por Dia da Semana
Solicitações 0
Todos
🔴 Integral
🟠 Semi-Integral
🔵 Tarde
🟣 Diária
Criança Responsável Série Turno Dias Data Ações
⏳ Carregando…
Total de Inscrições
—
em atividades
Atividades Ativas
—
com inscrições abertas
Turmas Lotadas
—
sem vagas disponíveis
Vagas Disponíveis
—
total em aberto
📅 Crianças por Dia da Semana
🎯 Ocupação por Atividade
Inscrições Recebidas 0
Criança Responsável Série Atividades e Turmas Data
⏳
Carregando…
📥 Modelo Inscricoes Excel
📂 Importar Inscricoes
📅 Inscrições por Dia da Semana
Inscrições Recebidas 0
Criança Responsável Série Atividades Data
⏳ Carregando…
🎯 Atividades Cadastradas 0
Pendentes
—
aguardando validação
Aprovados
—
diplomas validados
Rejeitados
—
diplomas negados
Total de Pontos
—
horas distribuídas
🏆 Ranking de Professoras
Diplomas Enviados
0
Todos
⏳ Pendentes
✅ Aprovados
❌ Rejeitados
Professora Curso Horas Status Enviado em Validado por Ações
⏳ Carregando…
🏥 Atestados das Professoras
Os atestados são gerenciados pelo Portal da Secretaria. Acesse secretaria.html para aprovar ou rejeitar atestados.
⏳ Carregando…
✅ Nenhuma requisição pendente.
Todos os status
⏳ Pendente
✅ Aprovado
❌ Rejeitado
Filtrar
⏳ Carregando…
Cadastrar todos os insumos
Cancelar
🏷️ Categorias
⚙️ Gerenciar
Catalogo de Insumos 0
Turmas 0
Associe as professoras às suas turmas no painel de Professoras.
Associar Professoras a Turmas
⏳ Pendentes de compra
✅ Já comprados
Todos
Atualizar
✅ Marcar selecionados como comprado
📝 Criar Requisição
⏳ Carregando…
📝 Criar Requisição de Insumos
×
Professora *
— Selecione a professora —
🔍 Buscar no catálogo
📊 Importar planilha
Modelo de planilha
Baixe, preencha e faça o upload
📥 Baixar modelo Excel
📂
Clique ou arraste sua planilha
.xlsx · .xls · .csv
🛒 Carrinho
Nenhum item adicionado ainda.
Observação (opcional)
Total: R$ 0,00
✅ Criar Requisição
Analisar Requisição
×
Nota para a professora (opcional)
✅ Ao aprovar, os itens com melhor preço encontrado serão automaticamente encaminhados para a fila de compras.
✅ Aprovar e Encaminhar Compras
❌ Rejeitar
Calendario Escolar 2026
‹
›
+ Novo Evento
Feriados
Datas Comemorativas
Reunioes
Atividades
Planejamentos
Report Cards
Recesso
🍁
Bem-vindo ao Lumied!
Configure sua escola em 5 passos simples.
1
Dados da Escola
Nome, logo, contato
→
2
Séries e Turmas
Cadastre Bear Care, Year 1, Year 2...
→
3
Professoras
Cadastre sua equipe docente
→
4
Famílias
Importe ou cadastre as famílias
→
5
Primeiro Aviso
Envie uma mensagem para os pais
→
Pular por agora
Começar Setup →
👥 Novos Leads
⏳ Aprovar Material
🏦 Boletos
📝 Ver Notas
🖨️ Impressões
🚨 Emergência
Dashboard Analytics
2025 2026 2027
Dashboard Financeiro
2025 2026 2027
Todos
Receitas
Despesas
+ Novo Lançamento
Plano de Contas
+ Nova Conta
Demonstração do Resultado do Exercício
2025 2026 2027
Boletos Emitidos
+ Emitir Boleto
Notas Fiscais
+ Emitir NF
Pipeline de Leads
+ Novo Lead
Templates de Mensagem
+ Novo Template
Vagas Disponíveis
2026 2027
Matriculas e Reservas
2026 2027
Configuracao de Series por Idade
2026 2027 2028
Define automaticamente a serie da crianca com base na data de nascimento e na data de corte. A idade e calculada na data de corte (ex: 31/03 do ano de referencia).
⏳ Pendentes
📋 Todas
📊 Cotas por Turma
Acione um alerta de emergência para notificar toda a equipe imediatamente.
🔥
Incêndio
🚷
Intruso
🚑
Emergência Médica
🏃
Evacuação
Histórico
Pendentes
—
aguardando análise
📄 Relatório por Equipe
⚙️ Configurar Equipes
Equipes de Manutenção
+ Adicionar
Chamados de Manutenção 0
Todos
Pendentes
Em Execução
Concluídas
Críticas
Urgência Descrição Local Solicitante Equipe Status Data Ações
⏳ Carregando…
Relatório de Pendências por Equipe
🖨 Imprimir
📲 WhatsApp
✕
Em Andamento
—
planos aprovados
Encerrados
—
ciclo concluído
Semáforo de PDIs
Professora Status PDI Submetido em Ações
⏳ Carregando…
Reuniões Agendadas 0
Data Horário Responsável Com Assunto
⏳ Carregando…
👩💼 Gestoras
🕐 Horários Disponíveis
Logotipo
🍁 Nenhum logotipo carregado
Equipe 0
Todos
Gerentes
Professoras
Assistentes
Secretaria
Manutenção
Solicitações Pendentes 0
E-mails Autorizados
Confirmar Importacao
Cancelar
Solicitacoes 0
Alterar Turno
Cancelar
Salvar
Validar Diploma
Observação (opcional)
Cancelar
Confirmar
✕
PDI
Competências
Metas SMART
Check-ins
🔑 Definir Senha
Nova Senha
Cancelar
Salvar Senha
Disciplinas 0
+ Nova Disciplina
Disciplina Série Professor(a) Carga (h) Ações
Carregando...
Períodos Letivos 0
+ Novo Período
Período # Ano Início Fim Ações
Carregando...
Visão Geral de Notas
Série...
Período...
Disciplina...
Selecione série, período e disciplina para visualizar as notas.
Diário de Classe Digital
Série...
Disciplina...
Selecione série e disciplina para visualizar o diário.
Pesquisas & Enquetes 0
+ Nova Pesquisa
Carregando...
Status de Matrículas 0
Todos Reserva Matriculado Cancelado
Carregando...
iframe.contentDocument.close();
await new Promise(r => setTimeout(r, 800)); // aguarda renderização
const canvas = await html2canvas(iframe.contentDocument.body, {
scale: 2,
useCORS: true,
backgroundColor: '#f8f5f0',
width: 1000,
windowWidth: 1000
});
document.body.removeChild(iframe);
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
const pdfW = pdf.internal.pageSize.getWidth();
const pdfH = pdf.internal.pageSize.getHeight();
const imgW = pdfW;
const imgH = (canvas.height * pdfW) / canvas.width;
const imgData = canvas.toDataURL('image/jpeg', 0.92);
let posY = 0;
let pageH = pdfH;
let totalH = imgH;
// Divide em páginas se necessário
while (posY < totalH) {
if (posY > 0) pdf.addPage();
pdf.addImage(imgData, 'JPEG', 0, -posY, imgW, imgH);
posY += pageH;
}
pdf.save(`maple-bear-${titulo}-${new Date().toLocaleDateString('pt-BR').replace(/\//g,'-')}.pdf`);
btn.innerHTML = '📄 Exportar PDF'; btn.disabled = false;
}
async function compartilharWhatsApp() {
const isDashAtiv = document.getElementById('panelDashAtiv').classList.contains('active');
const titulo = isDashAtiv ? 'dashboard-atividades' : 'dashboard-turnos';
const htmlContent = isDashAtiv ? buildRelatorioAtivHTML() : buildRelatorioTurnosHTML();
const btn = document.getElementById('btnWhatsapp');
btn.textContent = '⏳ Preparando…'; btn.disabled = true;
try {
// 1. Gera o PDF em memória (mesmo processo do exportarPDF)
const iframe = document.createElement('iframe');
iframe.style.cssText = 'position:fixed;top:-9999px;left:-9999px;width:1000px;height:1px;border:none;';
document.body.appendChild(iframe);
iframe.contentDocument.open();
iframe.contentDocument.write('
' + htmlContent + '');
iframe.contentDocument.close();
await new Promise(r => setTimeout(r, 800));
const canvas = await html2canvas(iframe.contentDocument.body, {
scale: 2, useCORS: true, backgroundColor: '#f8f5f0', width: 1000, windowWidth: 1000
});
document.body.removeChild(iframe);
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
const pdfW = pdf.internal.pageSize.getWidth();
const pdfH = pdf.internal.pageSize.getHeight();
const imgW = pdfW;
const imgH = (canvas.height * pdfW) / canvas.width;
const imgData = canvas.toDataURL('image/jpeg', 0.92);
let posY = 0;
while (posY < imgH) {
if (posY > 0) pdf.addPage();
pdf.addImage(imgData, 'JPEG', 0, -posY, imgW, imgH);
posY += pdfH;
}
// 2. Converte para base64 e faz upload via API
const pdfBase64 = pdf.output('datauristring').split(',')[1];
const d = await api({ action: 'relatorio_upload', base64: pdfBase64, nome: titulo });
if (d.error) throw new Error(d.error);
// 3. Abre WhatsApp com o link
const data = new Date().toLocaleDateString('pt-BR', {day:'2-digit',month:'2-digit',year:'numeric'});
const tipo = isDashAtiv ? 'Atividades Extraclasse' : 'Turnos 2026';
const msg = `🍁 *Maple Bear*\n\n📊 *Relatório — Dashboard de ${tipo}*\n📅 ${data}\n\n📄 Clique para abrir o PDF:\n${d.url}`;
window.open('https://wa.me/?text=' + encodeURIComponent(msg), '_blank');
} catch(e) {
showToast('Erro ao gerar link: ' + e.message, 'error');
}
btn.innerHTML = `
Compartilhar PDF`;
btn.disabled = false;
}
// ── PROFESSORAS ───────────────────────────────────────
async function loadProfessoras() {
const [profs, almocoConf] = await Promise.all([
api({ action:'professoras_list' }),
api({ action:'config_get', chave:'almoco_preco' })
]);
const list = Array.isArray(profs) ? profs : [];
document.getElementById('profsCount').textContent = list.length;
const el = document.getElementById('profsList');
if (!list.length) {
el.innerHTML = '
Nenhuma professora cadastrada.
';
} else {
el.innerHTML = list.map(p => `
${esc(p.nome)}
${esc(p.email)}
🔑 Senha
🗑
`).join('');
}
// Preenche preço do almoço
document.getElementById('almocoPreco').value = almocoConf?.valor || '50.00';
}
async function createProfessora() {
const nome = document.getElementById('newProfNome').value.trim();
const email = document.getElementById('newProfEmail').value.trim();
if (!nome || !email) return showAlert('prof','error','Nome e e-mail são obrigatórios.');
const d = await api({ action:'professoras_create', nome, email });
if (d.error) return showAlert('prof','error', d.error);
showAlert('prof','success','✅ Professora adicionada!');
document.getElementById('newProfNome').value = '';
document.getElementById('newProfEmail').value = '';
loadProfessoras();
}
async function deleteProfessora(id, nome) {
if (!confirm('Remover professora "'+nome+'"?')) return;
await api({ action:'professoras_delete', id });
loadProfessoras();
}
async function saveAlmocoPreco() {
const preco = parseFloat(document.getElementById('almocoPreco').value);
if (isNaN(preco) || preco < 0) return showAlert('almoco','error','Informe um valor válido.');
const d = await api({ action:'config_set', chave:'almoco_preco', valor: preco.toFixed(2) });
if (d.error) return showAlert('almoco','error', d.error);
showAlert('almoco','success','✅ Preço salvo!');
}
// ── MANUTENÇÃO ──────────────────────────────────────────
let manutData = [], manutFilter = 'todos';
const URGENCIA_LABEL = { baixa:'🟢 Baixa', media:'🟡 Média', alta:'🟠 Alta', critica:'🔴 Crítica' };
const MANUT_STATUS = { pendente:'⏳ Pendente', aprovada:'✅ Aprovada', em_execucao:'🔧 Em Execução', concluida:'✅ Concluída', rejeitada:'❌ Rejeitada' };
let EQUIPES = [];
let relatorioEquipesSel = new Set();
async function loadManutEquipes() {
const d = await api({ action:'manut_equipes_list' });
EQUIPES = Array.isArray(d) ? d.map(e => e.nome) : [];
}
async function loadManutPanel() {
if (!EQUIPES.length) await loadManutEquipes();
const d = await api({ action:'manutencao_list' });
manutData = Array.isArray(d) ? d : (d.data || []);
updateManutStats();
renderManutTable();
}
function updateManutStats() {
document.getElementById('manutPend').textContent = manutData.filter(m => m.status === 'pendente').length;
document.getElementById('manutExec').textContent = manutData.filter(m => m.status === 'em_execucao').length;
const now = new Date(), y = now.getFullYear(), mo = now.getMonth();
document.getElementById('manutDone').textContent = manutData.filter(m => m.status === 'concluida' && m.data_conclusao && new Date(m.data_conclusao).getMonth() === mo && new Date(m.data_conclusao).getFullYear() === y).length;
document.getElementById('manutCrit').textContent = manutData.filter(m => m.urgencia === 'critica' && m.status !== 'concluida' && m.status !== 'rejeitada').length;
}
function setManutFilter(f, btn) {
manutFilter = f;
document.querySelectorAll('#panelManutencao .filter-bar .fb').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderManutTable();
}
function renderManutTable() {
let list = manutData;
if (manutFilter === 'critica') list = list.filter(m => m.urgencia === 'critica' && m.status !== 'concluida');
else if (manutFilter !== 'todos') list = list.filter(m => m.status === manutFilter);
document.getElementById('manutCount').textContent = list.length;
const tb = document.getElementById('manutBody');
if (!list.length) { tb.innerHTML = '
Nenhum chamado encontrado. '; return; }
tb.innerHTML = list.map(m => {
const user = m.usuarios || {};
const data = new Date(m.criado_em).toLocaleDateString('pt-BR');
return `
${URGENCIA_LABEL[m.urgencia]||m.urgencia}
${esc(m.descricao?.substring(0,80))} ${m.foto_url ? ' 📎 foto ' : ''}
${esc(m.localizacao)}
${esc(user.nome||'—')}${esc(user.email||'')}
${esc(m.equipe_responsavel||'—')}
${MANUT_STATUS[m.status]||m.status}
${data}
✎
🗑
`;
}).join('');
}
function openManutModal(id) {
const m = manutData.find(x => x.id === id);
if (!m) return;
document.getElementById('manutModalTitle').textContent = 'Chamado #' + id.substring(0,8);
const equipesOpts = EQUIPES.map(e => `
${e} `).join('');
document.getElementById('manutModalContent').innerHTML = `
Descrição
${esc(m.descricao)}
Local
${esc(m.localizacao)}
Urgência
${URGENCIA_LABEL[m.urgencia]}
${m.foto_url ? `
` : ''}
Encaminhar para equipe
— selecione — ${equipesOpts}
Observação
🔧 Iniciar Execução
✅ Concluir
✕ Rejeitar
`;
document.getElementById('manutModalOverlay').classList.add('show');
}
function closeManutModal() { document.getElementById('manutModalOverlay').classList.remove('show'); }
async function updateManutStatus(id, status) {
const equipe = document.getElementById('manutEquipe')?.value || undefined;
const obs = document.getElementById('manutObs')?.value?.trim() || undefined;
const d = await api({ action:'manutencao_update_status', id, status, equipe_responsavel: equipe, observacao_gerente: obs });
if (d.error) { showToast('Erro: ' + d.error, 'error'); return; }
showToast('Status atualizado!', 'success');
closeManutModal();
loadManutPanel();
}
async function deleteManut(id) {
if (!confirm('Remover este chamado?')) return;
const d = await api({ action:'manutencao_delete', id });
if (d.error) { showToast(d.error, 'error'); return; }
loadManutPanel();
}
// ── CONFIG EQUIPES MANUTENÇÃO ────────────────────────
function toggleManutEquipesConfig() {
const el = document.getElementById('manutEquipesConfig');
el.style.display = el.style.display === 'none' ? 'block' : 'none';
if (el.style.display === 'block') renderManutEquipesConfig();
}
async function renderManutEquipesConfig() {
const d = await api({ action: 'manut_equipes_list_all' });
const equipes = Array.isArray(d) ? d : [];
const el = document.getElementById('manutEquipesList');
el.innerHTML = equipes.map(e => `
${esc(e.nome)}
${e.ativo?'Desativar':'Ativar'}
`).join('');
}
async function toggleManutEquipe(id, ativo) {
await api({ action: 'manut_equipe_toggle', id, ativo });
await loadManutEquipes();
renderManutEquipesConfig();
}
async function addManutEquipe() {
const nome = document.getElementById('manutNovaEquipe').value.trim();
if (!nome) return;
const d = await api({ action: 'manut_equipe_save', nome });
if (d.error) { showToast(d.error, 'error'); return; }
document.getElementById('manutNovaEquipe').value = '';
await loadManutEquipes();
renderManutEquipesConfig();
}
// ── RELATÓRIO MANUTENÇÃO ───────────────────────────
function openManutRelatorio() {
relatorioEquipesSel = new Set([...EQUIPES, 'Sem equipe']);
renderRelatorioFiltros();
renderRelatorioConteudo();
document.getElementById('manutRelatorioOverlay').classList.add('show');
}
function closeManutRelatorio() { document.getElementById('manutRelatorioOverlay').classList.remove('show'); }
function renderRelatorioFiltros() {
const todas = [...EQUIPES, 'Sem equipe'];
document.getElementById('relatorioEquipesFiltro').innerHTML = todas.map(e => {
const sel = relatorioEquipesSel.has(e);
return `
${esc(e)} `;
}).join('');
}
function toggleRelatorioEquipe(e) {
if (relatorioEquipesSel.has(e)) relatorioEquipesSel.delete(e);
else relatorioEquipesSel.add(e);
renderRelatorioFiltros();
renderRelatorioConteudo();
}
function renderRelatorioConteudo() {
const pendentes = manutData.filter(m => m.status !== 'concluida' && m.status !== 'rejeitada');
const agrupado = {};
for (const m of pendentes) {
const eq = m.equipe_responsavel || 'Sem equipe';
if (!relatorioEquipesSel.has(eq)) continue;
if (!agrupado[eq]) agrupado[eq] = [];
agrupado[eq].push(m);
}
const el = document.getElementById('relatorioConteudo');
if (!Object.keys(agrupado).length) {
el.innerHTML = '
Nenhum chamado pendente para as equipes selecionadas.
';
return;
}
el.innerHTML = Object.entries(agrupado).map(([eq, items]) => `
${esc(eq)} (${items.length} pendência${items.length>1?'s':''})
Urg.
Descrição
Local
Status
Data
${items.map(m => `
${URGENCIA_LABEL[m.urgencia]||m.urgencia}
${esc(m.descricao?.substring(0,80))}
${esc(m.localizacao)}
${MANUT_STATUS[m.status]||m.status}
${new Date(m.criado_em).toLocaleDateString('pt-BR')}
`).join('')}
`).join('');
}
function printManutRelatorio() {
const conteudo = document.getElementById('relatorioConteudo').innerHTML;
const win = window.open('', '_blank');
win.document.write(`
Relatório Manutenção — Maple Bear
Relatório de Pendências — Manutenção
Maple Bear Caxias do Sul · ${new Date().toLocaleDateString('pt-BR',{day:'2-digit',month:'long',year:'numeric'})}
${conteudo}`);
win.document.close();
setTimeout(() => win.print(), 300);
}
function shareManutWhatsApp() {
const equipes = [...relatorioEquipesSel];
const pendentes = manutData.filter(m => m.status !== 'concluida' && m.status !== 'rejeitada');
let texto = '*RELATÓRIO DE MANUTENÇÃO — Maple Bear*\n' + new Date().toLocaleDateString('pt-BR') + '\n\n';
for (const eq of equipes) {
const items = pendentes.filter(m => (m.equipe_responsavel || 'Sem equipe') === eq);
if (!items.length) continue;
texto += '*' + eq + '* (' + items.length + ')\n';
items.forEach(m => {
const urg = {baixa:'🟢',media:'🟡',alta:'🟠',critica:'🔴'}[m.urgencia]||'';
texto += urg + ' ' + (m.descricao||'').substring(0,60) + ' — _' + (m.localizacao||'') + '_\n';
});
texto += '\n';
}
window.open('https://wa.me/?text=' + encodeURIComponent(texto), '_blank');
}
// ── REUNIÕES ──────────────────────────────────────────
const CALENDAR_URL = SUPABASE_URL + '/functions/v1/calendar';
async function callCalendar(body) {
const r = await fetch(CALENDAR_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'apikey': SUPABASE_ANON, 'Authorization': 'Bearer ' + SUPABASE_ANON },
body: JSON.stringify(body)
});
return r.json();
}
const DIAS_SEMANA = ['', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta'];
async function loadReunioesPanel() {
await Promise.all([loadReunioesList(), loadGestorasGerente()]);
}
async function loadReunioesList() {
const data = await callCalendar({ action: 'reunioes_list' });
const list = Array.isArray(data) ? data : [];
document.getElementById('reunioesCount').textContent = list.length;
const tb = document.getElementById('reunioesListBody');
if (!list.length) { tb.innerHTML = '
📭 Nenhuma reunião agendada. '; return; }
tb.innerHTML = list.map(r => {
const dataFmt = new Date(r.data_reuniao + 'T12:00:00').toLocaleDateString('pt-BR', { weekday:'short', day:'2-digit', month:'2-digit' });
return `
${dataFmt}
${r.hora_inicio.substring(0,5)} – ${r.hora_fim.substring(0,5)}
${esc(r.nome_resp)}${esc(r.email_resp)}
${esc(r.gestoras?.nome||'')}
${esc(r.assunto||'—')}
🗑
`;
}).join('');
}
async function loadGestorasGerente() {
const data = await callCalendar({ action: 'gestoras_list' });
const list = Array.isArray(data) ? data : [];
const cargos = { diretora: 'Diretora Pedagógica', coordenadora: 'Coordenadora Pedagógica' };
document.getElementById('gestorasGerente').innerHTML = list.map(g => `
`).join('');
// Popula select de gestoras para horários
const sel = document.getElementById('gestoraSlotSelect');
sel.innerHTML = '
Selecione a gestora… ' +
list.map(g => `
${g.nome} (${cargos[g.cargo]||g.cargo}) `).join('');
}
async function saveGestora(id) {
const nome = document.getElementById('gnome_' + id).value.trim();
const email = document.getElementById('gemail_' + id).value.trim();
const calendar_id = document.getElementById('gcal_' + id).value.trim();
const d = await callCalendar({ action: 'gestoras_update', id, nome, email, calendar_id });
if (d.error) { showToast('Erro: ' + d.error, 'error'); return; }
showToast('Salvo com sucesso!', 'success');
}
async function loadHorariosList() {
const gestora_id = document.getElementById('gestoraSlotSelect').value;
const el = document.getElementById('horariosList');
if (!gestora_id) { el.innerHTML = ''; return; }
const data = await callCalendar({ action: 'horarios_list', gestora_id });
const list = Array.isArray(data) ? data : [];
if (!list.length) { el.innerHTML = '
Nenhum horário cadastrado.
'; return; }
el.innerHTML = list.map(h => `
${DIAS_SEMANA[h.dia_semana]}
${h.hora_inicio.substring(0,5)} – ${h.hora_fim.substring(0,5)}
🗑
`).join('');
}
async function createHorario() {
const gestora_id = document.getElementById('gestoraSlotSelect').value;
const dia_semana = parseInt(document.getElementById('newSlotDia').value);
const hora_inicio = document.getElementById('newSlotInicio').value;
const hora_fim = document.getElementById('newSlotFim').value;
if (!gestora_id) return showAlert('horario', 'error', 'Selecione a gestora.');
if (!hora_inicio || !hora_fim) return showAlert('horario', 'error', 'Informe início e fim.');
if (hora_inicio >= hora_fim) return showAlert('horario', 'error', 'O horário de início deve ser antes do fim.');
const d = await callCalendar({ action: 'horarios_create', gestora_id, dia_semana, hora_inicio, hora_fim });
if (d.error) return showAlert('horario', 'error', d.error);
showAlert('horario', 'success', '✅ Horário adicionado!');
loadHorariosList();
}
async function deleteHorario(id) {
if (!confirm('Remover este horário?')) return;
await callCalendar({ action: 'horarios_delete', id });
loadHorariosList();
}
async function cancelarReuniaoGerente(id) {
if (!confirm('Cancelar esta reunião?')) return;
await callCalendar({ action: 'cancelar_reuniao', id });
loadReunioesList();
}
// ── CONTROLE DE ACESSO ────────────────────────────────
const ACESSO = SUPABASE_URL + '/functions/v1/acesso';
async function callAcesso(body) {
const token = getToken();
const headers = { 'Content-Type': 'application/json', 'apikey': ANON };
if (token) headers['Authorization'] = 'Bearer ' + token;
try {
const r = await fetch(ACESSO, { method: 'POST', headers, body: JSON.stringify(body) });
const d = await r.json();
if (!r.ok && !d.error) d.error = d.message || `Erro ${r.status}`;
if (d.error === 'Sessão inválida ou expirada. Faça login novamente.') { doLogout(); }
return d;
} catch (e) {
return { error: 'Falha de conexão com o servidor.' };
}
}
async function loadAcesso() {
carregarSolicitacoes();
carregarAutorizados();
}
async function carregarSolicitacoes() {
document.getElementById('solicitacoesWrap').innerHTML =
'
Carregando…
';
const d = await callAcesso({ action: 'solicitacoes_list' });
if (d.error) {
document.getElementById('solicitacoesWrap').innerHTML =
`
Erro ao carregar solicitações: ${escHtml(d.error)}
`;
return;
}
const lista = d.data || [];
document.getElementById('acessoBadge').textContent = lista.length;
if (lista.length === 0) {
document.getElementById('solicitacoesWrap').innerHTML =
'
Nenhuma solicitação pendente.
';
return;
}
const rows = lista.map(s => `
${escHtml(s.nome)}
${escHtml(s.cpf)}
${escHtml(s.email)}
${escHtml(s.telefone)}
${escHtml(s.nome_crianca)}
${fmtDate(s.criado_em)}
✓ Aprovar
✕ Rejeitar
`).join('');
document.getElementById('solicitacoesWrap').innerHTML = `
Nome
CPF
E-mail
Telefone
Criança
Data
${rows}
`;
}
async function acAprovar(id) {
const d = await callAcesso({ action: 'aprovar', id });
if (d.error) { showToast('Erro: ' + d.error, 'error'); return; }
carregarSolicitacoes();
carregarAutorizados();
}
async function acRejeitar(id, nome) {
if (!confirm('Rejeitar solicitação de ' + nome + '?')) return;
const d = await callAcesso({ action: 'rejeitar', id });
if (d.error) { showToast('Erro: ' + d.error, 'error'); return; }
carregarSolicitacoes();
}
async function carregarAutorizados() {
document.getElementById('autorizadosWrap').innerHTML =
'
Carregando…
';
const d = await callAcesso({ action: 'list' });
const lista = d.data || [];
if (lista.length === 0) {
document.getElementById('autorizadosWrap').innerHTML =
'
Nenhum e-mail autorizado cadastrado.
';
return;
}
const rows = lista.map(u => `
${escHtml(u.email)}
${escHtml(u.nome || '—')}
${escHtml(u.criado_por || '—')}
${fmtDate(u.criado_em)}
Remover
`).join('');
document.getElementById('autorizadosWrap').innerHTML = `
E-mail
Nome
Adicionado por
Data
${rows}
`;
}
async function acAddUser() {
const email = document.getElementById('acEmail').value.trim();
const nome = document.getElementById('acNome').value.trim();
const errEl = document.getElementById('acErr');
const okEl = document.getElementById('acOk');
errEl.style.display = 'none'; okEl.style.display = 'none';
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errEl.textContent = 'Informe um e-mail válido.'; errEl.style.display = 'block'; return;
}
const d = await callAcesso({ action: 'add', email, nome });
if (d.error) { errEl.textContent = d.error; errEl.style.display = 'block'; return; }
document.getElementById('acEmail').value = '';
document.getElementById('acNome').value = '';
okEl.textContent = email + ' adicionado.'; okEl.style.display = 'block';
setTimeout(() => { okEl.style.display = 'none'; }, 3000);
carregarAutorizados();
}
async function acRemover(id, email) {
if (!confirm('Remover acesso de ' + email + '?')) return;
const d = await callAcesso({ action: 'remove', id });
if (d.error) { showToast('Erro: ' + d.error, 'error'); return; }
carregarAutorizados();
}
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
function fmtDate(iso) {
if (!iso) return '—';
return new Date(iso).toLocaleDateString('pt-BR');
}
// ── SECRETARIA ────────────────────────────────────────
async function loadSecretarias() {
const d = await callDiplomas({ action: 'secretarias_list' });
const list = d.data || [];
document.getElementById('secsCount').textContent = list.length;
const el = document.getElementById('secsList');
if (!list.length) {
el.innerHTML = '
Nenhuma secretária cadastrada.
';
return;
}
el.innerHTML = list.map(s => `
${(s.nome||'?')[0].toUpperCase()}
${esc(s.nome)}
${esc(s.email)} · Desde ${new Date(s.criado_em).toLocaleDateString('pt-BR')}
Secretaria
🗑
`).join('');
}
async function createSecretaria() {
const nome = document.getElementById('newSecNome').value.trim();
const email = document.getElementById('newSecEmail').value.trim();
const senha = document.getElementById('newSecSenha').value;
if (!nome || !email || !senha) return showAlert('sec','error','Preencha todos os campos.');
if (senha.length < 6) return showAlert('sec','error','Senha mínima de 6 caracteres.');
const d = await callDiplomas({ action: 'secretaria_create', nome, email, senha });
if (d.error) return showAlert('sec','error',d.error);
showAlert('sec','success','✅ Secretária "'+nome+'" criada!');
document.getElementById('newSecNome').value = '';
document.getElementById('newSecEmail').value = '';
document.getElementById('newSecSenha').value = '';
loadSecretarias();
}
async function deleteSecretaria(id, nome) {
if (!confirm('Remover "'+nome+'" da secretaria?')) return;
const d = await callDiplomas({ action: 'secretaria_delete', id });
if (d.error) { showToast(d.error, 'error'); return; }
loadSecretarias();
}
// ── DIPLOMAS ──────────────────────────────────────────
async function callDiplomas(body) {
const token = getToken();
const headers = { 'Content-Type': 'application/json', 'apikey': ANON, 'Authorization': 'Bearer ' + ANON };
const r = await fetch(DIPLOMAS_API, { method: 'POST', headers, body: JSON.stringify({ ...body, _token: token }) });
const d = await r.json();
if (d.error === 'Sessão inválida ou expirada. Faça login novamente.') { doLogout(); }
return d;
}
async function loadDiplomasPanel() {
await Promise.all([ loadDipRanking(), loadDipTable() ]);
}
async function loadDipRanking() {
const d = await callDiplomas({ action: 'ranking' });
const lista = d.data || [];
const el = document.getElementById('dipRankingList');
if (!lista.length) {
el.innerHTML = '
Nenhuma professora com pontos ainda.
';
return;
}
el.innerHTML = lista.map((p, i) => {
const pos = i + 1;
const cls = pos === 1 ? 'g1' : pos === 2 ? 'g2' : pos === 3 ? 'g3' : 'gN';
const lbl = pos <= 3 ? ['🥇','🥈','🥉'][pos-1] : pos + 'º';
return `
`;
}).join('');
}
async function loadDipTable() {
const d = await callDiplomas({ action: 'diplomas_all', status: dipFilter });
const lista = d.data || [];
document.getElementById('dipCount').textContent = lista.length;
// Stats from all diplomas (fetch fresh)
const all = await callDiplomas({ action: 'diplomas_all', status: 'todos' });
const allList = all.data || [];
document.getElementById('dipPendentes').textContent = allList.filter(x => x.status === 'pendente').length;
document.getElementById('dipAprovados').textContent = allList.filter(x => x.status === 'aprovado').length;
document.getElementById('dipRejeitados').textContent = allList.filter(x => x.status === 'rejeitado').length;
document.getElementById('dipTotalPts').textContent = allList.filter(x => x.status === 'aprovado').reduce((s, x) => s + (x.pontuacao || 0), 0);
const tb = document.getElementById('dipTableBody');
if (!lista.length) { tb.innerHTML = '
📭 Nenhum diploma encontrado. '; return; }
tb.innerHTML = lista.map(d => {
const STATUS_HTML = {
pendente: '
⏳ Pendente ',
aprovado: '
✅ Aprovado ',
rejeitado: '
❌ Rejeitado ',
};
const date = new Date(d.criado_em).toLocaleDateString('pt-BR');
const prof = d.professoras || {};
const fileLink = d.arquivo_url
? `
📎 Ver `
: '—';
const acoes = d.status === 'pendente'
? `
✓ Aprovar
✕ Rejeitar `
: `
${d.status === 'aprovado' ? '+'+d.pontuacao+' pts' : '—'} `;
return `
${esc(prof.nome||'—')} ${esc(prof.email||'')}
${esc(d.nome_curso)}${d.arquivo_url?' '+fileLink:''}
${d.carga_horaria}h
${STATUS_HTML[d.status] || d.status}
${date}
${esc(d.validado_por||'—')}
${acoes}
`;
}).join('');
}
function dipSetFilter(f, btn) {
dipFilter = f;
document.querySelectorAll('.dip-filter-bar .fb').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
loadDipTable();
}
// Observação modal
function openDipModal(tipo, id, nomeProfessora, nomeCurso) {
dipPendingAction = { tipo, id };
const title = tipo === 'aprovar' ? '✅ Aprovar Diploma' : '❌ Rejeitar Diploma';
const desc = tipo === 'aprovar'
? `Aprovar o diploma de "
${esc(nomeCurso)} " de ${esc(nomeProfessora)}? Os pontos serão creditados automaticamente.`
: `Rejeitar o diploma de "
${esc(nomeCurso)} " de ${esc(nomeProfessora)}?`;
document.getElementById('obsModalTitle').textContent = title;
document.getElementById('obsModalDesc').innerHTML = desc;
const btn = document.getElementById('obsModalBtn');
btn.textContent = tipo === 'aprovar' ? 'Aprovar' : 'Rejeitar';
btn.style.background = tipo === 'aprovar' ? 'var(--green)' : 'var(--red)';
document.getElementById('obsModalText').value = '';
document.getElementById('obsModalOverlay').classList.add('show');
}
function closeObsModal() { document.getElementById('obsModalOverlay').classList.remove('show'); dipPendingAction = null; }
async function confirmDipAction() {
if (!dipPendingAction) return;
const { tipo, id } = dipPendingAction;
const observacao = document.getElementById('obsModalText').value.trim();
const btn = document.getElementById('obsModalBtn');
btn.disabled = true; btn.textContent = 'Salvando…';
const action = tipo === 'aprovar' ? 'diploma_aprovar' : 'diploma_rejeitar';
const d = await callDiplomas({ action, id, observacao: observacao || undefined });
btn.disabled = false;
if (d.error) { showToast('Erro: ' + d.error, 'error'); btn.textContent = tipo === 'aprovar' ? 'Aprovar' : 'Rejeitar'; return; }
closeObsModal();
loadDiplomasPanel();
}
// Senha modal
function openSenhaModal(id, nome) {
editingSenhaId = id;
document.getElementById('senhaModalDesc').textContent = 'Definindo senha de acesso ao portal para: ' + nome;
document.getElementById('senhaModalInput').value = '';
document.getElementById('senhaModalErr').classList.remove('show');
document.getElementById('senhaModalOk').classList.remove('show');
document.getElementById('senhaModalOverlay').classList.add('show');
}
function closeSenhaModal() { document.getElementById('senhaModalOverlay').classList.remove('show'); editingSenhaId = null; }
async function saveProfSenha() {
const senha = document.getElementById('senhaModalInput').value;
const errEl = document.getElementById('senhaModalErr');
const okEl = document.getElementById('senhaModalOk');
errEl.classList.remove('show'); okEl.classList.remove('show');
if (!senha || senha.length < 6) { errEl.textContent = 'Senha mínima de 6 caracteres.'; errEl.classList.add('show'); return; }
const btn = document.getElementById('senhaModalBtn');
btn.disabled = true; btn.textContent = 'Salvando…';
const d = await callDiplomas({ action: 'professora_set_senha', professora_id: editingSenhaId, senha });
btn.disabled = false; btn.textContent = 'Salvar Senha';
if (d.error) { errEl.textContent = d.error; errEl.classList.add('show'); return; }
okEl.textContent = '✅ Senha definida com sucesso!'; okEl.classList.add('show');
setTimeout(() => closeSenhaModal(), 1500);
}
// ── PDI (GESTORA) ─────────────────────────────────────────
const GCOMP_LABELS = {
linguagem:'🗣️ Proficiência no Idioma', metodologia:'📚 Metodologia e Didática',
avaliacao:'📊 Avaliação da Aprendizagem', intercultural:'🌍 Competência Intercultural',
colaboracao:'🤝 Colaboração e Comunidade', inovacao:'💡 Inovação e Tecnologia',
desenvolvimento:'📈 Desenvolvimento Profissional',
};
const G_STATUS = {rascunho:'✏️ Rascunho',aguardando_aprovacao:'⏳ Aguardando',em_andamento:'✅ Em Andamento',encerrado:'🏁 Encerrado'};
const G_NOTA = ['','Em Desenvolvimento','Atende','Supera','Referência'];
const META_STATUS_G = {pendente:'⏳ Pendente',em_andamento:'🔄 Em andamento',concluido:'✅ Concluído',revisado:'🔁 Revisado'};
let pdiReviewData = null;
async function loadPdiPanel() {
await Promise.all([ loadPdiCiclos(), loadPdiPainel() ]);
}
async function loadPdiCiclos() {
const d = await callDiplomas({ action: 'pdi_ciclos_list' });
const ciclos = d.data || [];
const sel = document.getElementById('pdiCicloSelect');
if (sel) {
sel.innerHTML = '
— Ciclo ativo — ' +
ciclos.map(c => `
${esc(c.nome)}${c.ativo?' ✓':''} `).join('');
}
const listEl = document.getElementById('pdiCiclosList');
if (listEl) {
listEl.innerHTML = ciclos.length
? ciclos.map(c => `
${esc(c.nome)}
${c.ano} · ${c.ativo?'🟢 Ativo':'⚫ Inativo'}
`).join('')
: '
Nenhum ciclo criado.
';
}
}
async function loadPdiPainel() {
const cicloId = document.getElementById('pdiCicloSelect')?.value || '';
const d = await callDiplomas({ action: 'pdi_painel', ciclo_id: cicloId || undefined });
if (!d.ciclo) {
document.getElementById('pdiSemTableBody').innerHTML = '
Nenhum ciclo ativo. Crie um ciclo de PDI ao lado. ';
['pdiStatTotal','pdiStatSem','pdiStatAguard','pdiStatAtivo','pdiStatEnc'].forEach(id => {
const el = document.getElementById(id); if (el) el.textContent = '—';
});
return;
}
const rows = d.professoras || [];
document.getElementById('pdiStatTotal').textContent = rows.length;
document.getElementById('pdiStatSem').textContent = rows.filter(r => !r.pdi).length;
document.getElementById('pdiStatAguard').textContent = rows.filter(r => r.pdi?.status === 'aguardando_aprovacao').length;
document.getElementById('pdiStatAtivo').textContent = rows.filter(r => r.pdi?.status === 'em_andamento').length;
document.getElementById('pdiStatEnc').textContent = rows.filter(r => r.pdi?.status === 'encerrado').length;
const tb = document.getElementById('pdiSemTableBody');
tb.innerHTML = rows.map(r => {
const { professora: p, pdi } = r;
const status = pdi?.status;
const dotClass = !pdi ? 'vermelho' : (status === 'em_andamento' || status === 'encerrado') ? 'verde' : 'amarelo';
const chipClass = status || 'sem-pdi';
const chipLabel = status ? G_STATUS[status] : '🔴 Sem PDI';
const submittedAt = pdi?.submetido_em ? new Date(pdi.submetido_em).toLocaleDateString('pt-BR') : '—';
const acoes = pdi ? `
Revisar PDI ` : '
— ';
return `
${esc(p.nome)} ${esc(p.email)}
${chipLabel}
${submittedAt}
${acoes}
`;
}).join('') || '
Nenhuma professora cadastrada. ';
}
async function criarCiclo() {
const nome = (document.getElementById('pdiCicloNome')?.value || '').trim();
const ano = parseInt(document.getElementById('pdiCicloAno')?.value) || new Date().getFullYear();
const inicio = document.getElementById('pdiCicloInicio')?.value || '';
const fim = document.getElementById('pdiCicloFim')?.value || '';
const errEl = document.getElementById('pdiCicloErr');
const okEl = document.getElementById('pdiCicloOk');
errEl.classList.remove('show'); okEl.classList.remove('show');
if (!nome || !inicio || !fim) { errEl.textContent = 'Preencha todos os campos.'; errEl.classList.add('show'); return; }
const btn = document.querySelector('#panelPdi .btn-create');
btn.disabled = true; btn.textContent = 'Criando…';
const d = await callDiplomas({ action: 'pdi_ciclo_criar', nome, ano, data_inicio: inicio, data_fim: fim });
btn.disabled = false; btn.textContent = 'Criar Ciclo';
if (d.error) { errEl.textContent = d.error; errEl.classList.add('show'); }
else {
okEl.textContent = '✅ Ciclo criado!'; okEl.classList.add('show');
document.getElementById('pdiCicloNome').value = '';
document.getElementById('pdiCicloAno').value = '';
document.getElementById('pdiCicloInicio').value = '';
document.getElementById('pdiCicloFim').value = '';
await loadPdiPanel();
}
}
async function openPdiReview(pdiId) {
const d = await callDiplomas({ action: 'pdi_prof_view', pdi_id: pdiId });
if (d.error || !d.data) { showToast('Erro ao carregar PDI: ' + (d.error || 'não encontrado'), 'error'); return; }
pdiReviewData = d.data;
renderPdiReviewModal(d.data);
document.getElementById('pdiReviewModal').classList.add('open');
}
function closePdiReview() {
document.getElementById('pdiReviewModal').classList.remove('open');
pdiReviewData = null;
}
function renderPdiReviewModal(pdi) {
const prof = pdi.professoras || {};
const ciclo = pdi.pdi_ciclos || {};
const comps = pdi.pdi_competencias || [];
const metas = pdi.pdi_metas || [];
const checkins = pdi.pdi_acompanhamentos || [];
const status = pdi.status;
document.getElementById('pdiReviewTitle').textContent = `PDI — ${esc(prof.nome || '')}`;
document.getElementById('pdiReviewSubtitle').textContent = `${ciclo.nome || ''} · ${G_STATUS[status] || status}`;
// Competências (grid with manager rating)
document.getElementById('pdiReviewComps').innerHTML = `
${Object.entries(GCOMP_LABELS).map(([area, label]) => {
const c = comps.find(x => x.area === area) || {};
return `
${label}
Professora: ${c.nota_auto||'—'} · Gestora:
${[1,2,3,4].map(n=>`${n} `).join('')}
`;
}).join('')}
Salvar Avaliação de Competências
`;
// Metas
document.getElementById('pdiReviewMetas').innerHTML = metas.length
? metas.map(m => `
`).join('')
: '
Nenhuma meta definida.
';
// Check-ins
document.getElementById('pdiReviewCheckins').innerHTML = checkins.length
? checkins.map(a => `
${a.tipo==='semestral'?'📅 Semestral':'🏁 Final'} · ${new Date(a.criado_em).toLocaleDateString('pt-BR')}
${esc(a.relato_professora||'')}
${a.feedback_gestora
? `
Feedback: ${esc(a.feedback_gestora)}
`
: `
Salvar
`
}
`).join('')
: '
Nenhum check-in registrado.
';
// Workflow actions
let actionsHtml = '';
if (status === 'aguardando_aprovacao') {
actionsHtml = `
Aprovação
Feedback (opcional para aprovar, obrigatório para devolver)
✅ Aprovar PDI
↩ Devolver para revisão
`;
} else if (status === 'em_andamento') {
actionsHtml = `
Encerrar PDI — Nota Final
${[1,2,3,4].map(n=>`${n} `).join('')}
Selecione
Feedback final (obrigatório)
🏁 Encerrar PDI
`;
} else if (status === 'encerrado') {
actionsHtml = `
PDI Encerrado
${pdi.nota_final||'—'}
${G_NOTA[pdi.nota_final]||''}
${esc(pdi.feedback_gestora||'')}
`;
}
document.getElementById('pdiReviewActions').innerHTML = actionsHtml;
}
function selectGNota(area, nota) {
const rg = document.getElementById(`gnsel_${area}`);
if (!rg) return;
rg.querySelectorAll('.pdi-note-btn').forEach((btn, i) => btn.classList.toggle('sel', i + 1 === nota));
rg.dataset.selected = nota;
}
function selectNotaFinal(n) {
document.querySelectorAll('.pdi-nota-btn').forEach((btn, i) => btn.classList.toggle('sel', i + 1 === n));
const lbl = document.getElementById('pdiNFLabel'); if (lbl) lbl.textContent = G_NOTA[n] || '';
const sel = document.getElementById('pdiNotaFinalSel'); if (sel) sel.dataset.selected = n;
}
async function saveCompetenciasGerente(pdiId) {
const errEl = document.getElementById('gcompErr');
const okEl = document.getElementById('gcompOk');
if (errEl) errEl.classList.remove('show');
if (okEl) okEl.classList.remove('show');
const competencias = Object.keys(GCOMP_LABELS).map(area => ({
area,
nota_gestora: parseInt(document.getElementById(`gnsel_${area}`)?.dataset.selected || '0'),
comentario: (document.getElementById(`gcmt_${area}`)?.value || '').trim() || undefined,
})).filter(c => c.nota_gestora > 0);
if (!competencias.length) { if (errEl) { errEl.textContent = 'Avalie ao menos uma competência.'; errEl.classList.add('show'); } return; }
const d = await callDiplomas({ action: 'pdi_competencias_gerente', pdi_id: pdiId, competencias });
if (d.error) { if (errEl) { errEl.textContent = d.error; errEl.classList.add('show'); } }
else { if (okEl) { okEl.textContent = '✅ Avaliação salva!'; okEl.classList.add('show'); } }
}
async function aprovaPdi(pdiId) {
const feedback = (document.getElementById('pdiAprovFeedback')?.value || '').trim();
const errEl = document.getElementById('pdiAprovErr');
const d = await callDiplomas({ action: 'pdi_aprovar', pdi_id: pdiId, feedback: feedback || undefined });
if (d.error) { if (errEl) { errEl.textContent = d.error; errEl.classList.add('show'); } return; }
closePdiReview(); await loadPdiPainel();
}
async function rejeitaPdi(pdiId) {
const feedback = (document.getElementById('pdiAprovFeedback')?.value || '').trim();
const errEl = document.getElementById('pdiAprovErr');
if (!feedback) { if (errEl) { errEl.textContent = 'Informe o motivo da devolução.'; errEl.classList.add('show'); } return; }
const d = await callDiplomas({ action: 'pdi_rejeitar', pdi_id: pdiId, feedback });
if (d.error) { if (errEl) { errEl.textContent = d.error; errEl.classList.add('show'); } return; }
closePdiReview(); await loadPdiPainel();
}
async function encerrarPdi(pdiId) {
const errEl = document.getElementById('pdiEncErr');
if (errEl) errEl.classList.remove('show');
const nota_final = parseInt(document.getElementById('pdiNotaFinalSel')?.dataset.selected || '0');
const feedback = (document.getElementById('pdiEncFeedback')?.value || '').trim();
if (!nota_final) { if (errEl) { errEl.textContent = 'Selecione a nota final (1-4).'; errEl.classList.add('show'); } return; }
if (!feedback) { if (errEl) { errEl.textContent = 'Informe o feedback final.'; errEl.classList.add('show'); } return; }
const d = await callDiplomas({ action: 'pdi_nota_final', pdi_id: pdiId, nota_final, feedback_gestora: feedback });
if (d.error) { if (errEl) { errEl.textContent = d.error; errEl.classList.add('show'); } return; }
closePdiReview(); await loadPdiPainel();
}
async function saveCheckinFb(acompId) {
const val = (document.getElementById(`chkFb_${acompId}`)?.value || '').trim();
if (!val) return;
const d = await callDiplomas({ action: 'pdi_checkin_feedback', acompanhamento_id: acompId, feedback_gestora: val });
if (d.error) { showToast('Erro: ' + d.error, 'error'); return; }
if (pdiReviewData) await openPdiReview(pdiReviewData.id);
}
// ── ALMOXARIFADO ─────────────────────────────────────────
let almReviewId = null;
let almReviewItens = []; // itens da req em análise
let almSelectedPrices = {}; // idx → melhor resultado de preço por item
const almFmtBRL = v => 'R$ ' + parseFloat(v||0).toLocaleString('pt-BR',{minimumFractionDigits:2,maximumFractionDigits:2});
const almMesBR = mes => { const [y,m]=mes.split('-'); return ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'][parseInt(m)-1]+'/'+y; };
function almShowTab(tab, btn) {
document.querySelectorAll('#almTabs .alm-tab').forEach(b => b.classList.remove('active'));
document.querySelectorAll('#panelAlmoxarifado .alm-tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('almTab-' + tab).classList.add('active');
if (tab === 'dashboard') almLoadDashboard();
if (tab === 'pendentes') almLoadPendentes();
if (tab === 'todas') almLoadTodas();
if (tab === 'insumos') almLoadInsumos();
if (tab === 'turmas') almLoadTurmas();
if (tab === 'orcamentos') almLoadOrcamentos();
if (tab === 'relatorio') almLoadRelatorio();
if (tab === 'compras') almLoadCompras();
}
// ── MONTH NAV HELPERS ──────────────────────────────────
const MESES_PT = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
function monthNavSet(inputId, val) {
document.getElementById(inputId).value = val;
const [y, m] = val.split('-');
const yEl = document.getElementById(inputId + 'Year');
const mEl = document.getElementById(inputId + 'Month');
if (yEl) yEl.textContent = y;
if (mEl) mEl.textContent = MESES_PT[parseInt(m)-1];
}
function monthNavYear(inputId, delta, callback) {
const cur = document.getElementById(inputId).value || new Date().toISOString().slice(0,7);
const [y, m] = cur.split('-').map(Number);
monthNavSet(inputId, (y + delta) + '-' + String(m).padStart(2,'0'));
if (callback) callback();
}
function monthNavMonth(inputId, delta, callback) {
const cur = document.getElementById(inputId).value || new Date().toISOString().slice(0,7);
const [y, m] = cur.split('-').map(Number);
const d = new Date(y, m - 1 + delta, 1);
monthNavSet(inputId, d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0'));
if (callback) callback();
}
function monthNavToday(inputId, callback) {
monthNavSet(inputId, new Date().toISOString().slice(0,7));
if (callback) callback();
}
// Backward compat
function monthNav(inputId, delta, callback) { monthNavMonth(inputId, delta, callback); }
function almInitMonths() {
const mes = new Date().toISOString().slice(0,7);
['almDashMes','almOrcMes','almRelMes','almTodasMes'].forEach(id => {
if (!document.getElementById(id).value) monthNavSet(id, mes);
});
}
async function almInitPanel() {
almInitMonths();
almLoadDashboard();
almLoadPendentes();
almLoadCategorias();
}
async function almLoadDashboard() {
const mes = document.getElementById('almDashMes').value || new Date().toISOString().slice(0,7);
const d = await callDiplomas({ action: 'alm_painel', mes });
const stats = document.getElementById('almDashStats');
stats.innerHTML = `
Pendentes
${d.pendentes??0}
aguardando análise
Total Aprovado
${almFmtBRL(d.totalAprovado)}
em ${almMesBR(mes)}
Turmas Ativas
${(d.turmas||[]).length}
com orçamento
`;
const badge = document.getElementById('almPendBadge');
if (d.pendentes > 0) { badge.textContent = d.pendentes; badge.style.display = 'inline'; }
else badge.style.display = 'none';
const turmasEl = document.getElementById('almDashTurmas');
if (!(d.turmas||[]).length) { turmasEl.innerHTML = '
Nenhuma turma cadastrada.
'; return; }
turmasEl.innerHTML = d.turmas.map(t => {
const pct = t.orcamento > 0 ? Math.min(100, (t.gasto/t.orcamento)*100) : 0;
const barColor = pct>=90 ? '#e53e3e' : pct>=70 ? '#f6a623' : '#48bb78';
return `
${t.nome}
${almFmtBRL(t.gasto)} / ${almFmtBRL(t.orcamento)}
`;
}).join('');
}
async function almLoadPendentes() {
document.getElementById('almPendLoading').style.display = 'block';
document.getElementById('almPendEmpty').style.display = 'none';
document.getElementById('almPendList').innerHTML = '';
const d = await callDiplomas({ action: 'alm_pendentes' });
document.getElementById('almPendLoading').style.display = 'none';
const reqs = d.data || [];
const badge = document.getElementById('almPendBadge');
badge.textContent = reqs.length; badge.style.display = reqs.length ? 'inline' : 'none';
if (!reqs.length) { document.getElementById('almPendEmpty').style.display = 'block'; return; }
document.getElementById('almPendList').innerHTML = reqs.map(r => almRenderReqRow(r, true)).join('');
}
async function almLoadTodas() {
const mes = document.getElementById('almTodasMes').value;
const status = document.getElementById('almTodasStatus').value;
document.getElementById('almTodasLoading').style.display = 'block';
document.getElementById('almTodasList').innerHTML = '';
const d = await callDiplomas({ action: 'alm_todas_reqs', mes, status });
document.getElementById('almTodasLoading').style.display = 'none';
const reqs = d.data || [];
if (!reqs.length) { document.getElementById('almTodasList').innerHTML = '
Nenhuma requisição encontrada.
'; return; }
document.getElementById('almTodasList').innerHTML = reqs.map(r => almRenderReqRow(r, r.status==='pendente')).join('');
}
function almRenderReqRow(r, showBtn) {
const statusLbl = {pendente:'⏳ Pendente',aprovado:'✅ Aprovado',rejeitado:'❌ Rejeitado'}[r.status]||r.status;
const itens = r.itens || [];
const turmaColor = r.series?.cor || '#3B82F6';
const turmaNome = r.series?.nome || '—';
const profNome = r.professoras?.nome || r.professoras?.email || '—';
const data = new Date(r.criado_em).toLocaleDateString('pt-BR');
const itensHtml = itens.map(it =>
`
${it.nome} ×${it.qty_solicitado} `
).join('');
return `
${statusLbl}
${profNome}
${turmaNome} · ${almMesBR(r.mes)} · ${data}
${almFmtBRL(r.total)}
${itensHtml}
${r.observacao ? `
📝 ${r.observacao}
` : ''}
${r.nota_gerente ? `
💬 ${r.nota_gerente}
` : ''}
${showBtn ? `
Analisar ` : ''}
`;
}
async function almAbrirReview(id) {
almReviewId = id;
almSelectedPrices = {};
document.getElementById('almReviewNota').value = '';
document.getElementById('almReviewErr').classList.remove('show');
document.getElementById('almAprovInfo').style.display = 'none';
const d = await callDiplomas({ action: 'alm_pendentes' });
const req = (d.data||[]).find(r => r.id === id);
if (!req) { showToast('Requisição não encontrada.', 'warning'); return; }
almReviewItens = req.itens || [];
document.getElementById('almReviewContent').innerHTML = `
${req.professoras?.nome||'—'} · ${req.series?.nome||'—'} · ${almMesBR(req.mes)} · ${new Date(req.criado_em).toLocaleDateString('pt-BR')}
${almReviewItens.map((it, i) => `
${it.nome}
${almFmtBRL(it.preco_unit)} / ${it.unidade}
Pedido: ${it.qty_solicitado}
Aprovar:
⏳ Buscando melhor preço em Mercado Livre, Shopee e Amazon…
`).join('')}
Total pedido: ${almFmtBRL(req.total)}
${req.observacao ? `
📝 ${req.observacao}
` : ''}`;
document.getElementById('almReviewModal').style.display = 'block';
// Buscar preços em paralelo sem bloquear o modal
almReviewItens.forEach((it, i) => almFetchPrecos(it.nome, it.unidade, i, it));
}
const platIcon = { 'Zoom': '🔎', 'Mercado Livre': '🛒', 'Shopee': '🧡', 'Amazon': '📦', 'Reval': '🏪' };
const matchColor = m => m >= 80 ? '#2d7a2d' : m >= 50 ? '#b07d00' : '#c0392b';
const matchLabel = m => m >= 80 ? 'Alta precisão' : m >= 50 ? 'Precisão média' : 'Baixa precisão';
// Cart links: ML has a real pre-filled checkout URL; Shopee/Amazon go to product/search page
function almCartUrl(r, qty) {
if (r.plataforma === 'Mercado Livre' && r.url_carrinho) {
// Replace quantity=1 with actual approved qty
return r.url_carrinho.replace('item.quantity=1', `item.quantity=${Math.max(1, Math.round(qty))}`);
}
return r.url_produto; // Shopee/Amazon: product or search page
}
function almCartLabel(r) {
if (r.plataforma === 'Mercado Livre' && r.url_carrinho) return '🛒 Abrir carrinho';
if (r.tipo === 'busca') return '🔍 Buscar';
return '🔗 Ver produto';
}
function almRenderPriceOption(r, idx, rIdx, qty, selected) {
const isBusca = r.tipo === 'busca';
const cartUrl = almCartUrl(r, qty || 1);
const radioName = 'almPriceSel_' + idx;
return `
${platIcon[r.plataforma]||'🔗'}
${r.nome||r.plataforma}
${r.plataforma}${!isBusca && r.match > 0 ? ` · ${r.match}% ` : ''}
${r.preco_fmt}
${almCartLabel(r)}
`;
}
let almAllPrices = {}; // idx -> array of results
async function almFetchPrecos(nome, unidade, idx, itemData) {
const el = document.getElementById('alm-price-' + idx);
if (!el) return;
const d = await callDiplomas({ action: 'alm_buscar_precos', nome, unidade });
if (!document.getElementById('alm-price-' + idx)) return;
if (d.error || !d.data || !d.data.length) {
el.innerHTML = `
Nao foi possivel buscar precos.
`;
return;
}
almAllPrices[idx] = { results: d.data, itemData, nome };
almSelectPrice(idx, 0); // seleciona o mais barato por padrao
}
function almSelectPrice(idx, rIdx) {
const info = almAllPrices[idx];
if (!info) return;
const all = info.results;
const selected = all[rIdx];
const qtyInput = document.querySelector(`#almReviewContent .alm-item-qty[data-idx="${idx}"]`);
const qty = qtyInput ? parseFloat(qtyInput.value) || 1 : 1;
// Salva selecao
almSelectedPrices[idx] = {
insumo_nome: info.itemData?.nome || info.nome,
insumo_id: info.itemData?.insumo_id || null,
qty,
plataforma: selected.plataforma,
produto_nome: selected.nome,
preco_unit: selected.preco,
match_pct: selected.match,
url_produto: selected.url_produto,
url_carrinho: almCartUrl(selected, qty),
};
// Mostra info quando todas buscas terminaram
if (Object.keys(almSelectedPrices).length === almReviewItens.length)
document.getElementById('almAprovInfo').style.display = 'block';
// Renderiza todas as opcoes com radio
const el = document.getElementById('alm-price-' + idx);
if (!el) return;
el.innerHTML = `
Escolha o fornecedor:
${all.map((r, i) => almRenderPriceOption(r, idx, i, qty, i === rIdx)).join('')}
`;
}
function almFecharReview() {
document.getElementById('almReviewModal').style.display = 'none';
almReviewId = null;
almSelectedPrices = {};
almReviewItens = [];
}
async function almAprovar() {
if (!almReviewId) return;
const nota = document.getElementById('almReviewNota').value.trim();
const errEl = document.getElementById('almReviewErr');
errEl.classList.remove('show');
// Collect qty overrides per item
const inputs = document.querySelectorAll('#almReviewContent .alm-item-qty');
const itens_aprovados = Array.from(inputs).map(inp => ({
insumo_id: inp.dataset.id,
qty_aprovado: parseFloat(inp.value) || 0,
}));
// Sync qtys into selectedPrices (manager may have changed qty after prices loaded)
inputs.forEach(inp => {
const i = parseInt(inp.dataset.idx);
if (almSelectedPrices[i]) {
const qty = parseFloat(inp.value) || 1;
almSelectedPrices[i].qty = qty;
almSelectedPrices[i].url_carrinho = almCartUrl(
{ plataforma: almSelectedPrices[i].plataforma, url_carrinho: almSelectedPrices[i].url_carrinho, url_produto: almSelectedPrices[i].url_produto },
qty
);
}
});
// 1. Approve the requisition
const dAprov = await callDiplomas({ action: 'alm_aprovar', id: almReviewId, nota_gerente: nota, itens_aprovados });
if (dAprov.error) { errEl.textContent = dAprov.error; errEl.classList.add('show'); return; }
// 2. Encaminhar compras (for items where price was found)
const itensCompra = Object.values(almSelectedPrices).filter(it => it.plataforma);
if (itensCompra.length) {
await callDiplomas({ action: 'alm_encaminhar_compra', requisicao_id: almReviewId, itens: itensCompra });
}
almFecharReview();
almLoadPendentes();
almLoadDashboard();
}
async function almRejeitar() {
if (!almReviewId) return;
const nota = document.getElementById('almReviewNota').value.trim();
if (!nota && !confirm('Rejeitar sem nota explicativa?')) return;
const errEl = document.getElementById('almReviewErr');
errEl.classList.remove('show');
const d = await callDiplomas({ action: 'alm_rejeitar', id: almReviewId, nota_gerente: nota });
if (d.error) { errEl.textContent = d.error; errEl.classList.add('show'); return; }
almFecharReview();
almLoadPendentes();
almLoadDashboard();
}
// ── Aba Compras ───────────────────────────────────────────
async function almLoadCompras() {
const status = document.getElementById('almComprasStatus').value;
document.getElementById('almComprasLoading').style.display = 'block';
document.getElementById('almComprasList').innerHTML = '';
const d = await callDiplomas({ action: 'alm_compras_todas', status });
document.getElementById('almComprasLoading').style.display = 'none';
const items = d.data || [];
// Update badge (pending count)
const pendentes = items.filter(c => c.status === 'pendente').length;
const badge = document.getElementById('almComprasBadge');
badge.textContent = pendentes;
badge.style.display = pendentes > 0 ? 'inline' : 'none';
if (!items.length) {
document.getElementById('almComprasList').innerHTML = '
Nenhum item encontrado.
';
return;
}
// Group by plataforma
const groups = {};
for (const c of items) {
if (!groups[c.plataforma]) groups[c.plataforma] = [];
groups[c.plataforma].push(c);
}
document.getElementById('almComprasList').innerHTML = Object.entries(groups).map(([plat, cols]) => `
${platIcon[plat]||'🔗'} ${plat}
${cols.map(c => {
const req = c.alm_requisicoes || {};
const turma = req.series?.nome || '—';
const prof = req.professoras?.nome || '—';
const comprado = c.status === 'comprado';
const cancelado = c.status === 'cancelado';
return `
${!comprado && !cancelado ? `
` : '
'}
${c.insumo_nome}
${prof} · ${turma} · ${req.mes ? almMesBR(req.mes) : '—'}
${c.produto_nome ? `
${c.produto_nome}
` : ''}
×${c.qty} ${c.preco_unit != null ? '· ' + almFmtBRL(c.preco_unit) + ' un.' : ''}
${c.preco_total != null ? `
Total: ${almFmtBRL(c.preco_total)}
` : ''}
${c.match_pct ? `
${c.match_pct}% ${matchLabel(c.match_pct)}
` : ''}
${c.url_carrinho ? `
🛒 Abrir carrinho
` : c.url_produto ? `
🔗 Ver produto
` : ''}
${comprado
? `
✅ Comprado `
: cancelado
? `
Cancelado `
: `
✅ Marcar comprado `
}
`;
}).join('')}
`).join('');
}
async function almMarcarComprado(ids) {
if (!ids?.length) return;
const d = await callDiplomas({ action: 'alm_marcar_comprado', ids });
if (!d.error) {
const okEl = document.getElementById('almComprasOk');
okEl.textContent = `✅ ${ids.length} item(ns) marcado(s) como comprado.`;
okEl.classList.add('show');
setTimeout(() => okEl.classList.remove('show'), 3000);
almLoadCompras();
}
}
async function almMarcarSelecionados() {
const checks = document.querySelectorAll('.alm-compra-chk:checked');
const ids = Array.from(checks).map(c => c.dataset.id);
if (!ids.length) { showToast('Selecione ao menos um item.', 'warning'); return; }
await almMarcarComprado(ids);
}
// ── Criar Requisição (gerente em nome de professora) ──────
let almNRCart = [];
let almNRCatalogo = [];
let almNRXlsxParsed = [];
const almNRNorm = s => (s||'').toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g,'')
.replace(/[^a-z0-9\s]/g,'').replace(/\s+/g,' ').trim();
const almNRFmtBRL = v => 'R$ ' + parseFloat(v||0).toLocaleString('pt-BR',{minimumFractionDigits:2,maximumFractionDigits:2});
function almGerarModelo() {
const wb = XLSX.utils.book_new();
const wsReq = XLSX.utils.aoa_to_sheet([
['Nome do Insumo *', 'Quantidade *', 'Unidade', 'Observação'],
['Papel Sulfite A4 75g Resma 500fls', 2, 'resma', 'Para impressão de avisos'],
['Caneta Esferográfica Azul', 10, 'unidade', ''],
['Fita Adesiva Transparente 45mm', 5, 'rolo', ''],
['', '', '', ''], ['', '', '', ''], ['', '', '', ''],
['', '', '', ''], ['', '', '', ''], ['', '', '', ''],
]);
wsReq['!cols'] = [{ wch: 42 }, { wch: 14 }, { wch: 14 }, { wch: 36 }];
XLSX.utils.book_append_sheet(wb, wsReq, 'Requisição');
const wsInst = XLSX.utils.aoa_to_sheet([
['INSTRUÇÕES — Modelo de Requisição de Insumos · Maple Bear'],
[''],
['1. Preencha a aba "Requisição" a partir da linha 2 (linha 1 é o cabeçalho).'],
['2. Não altere nem remova as colunas existentes.'],
['3. Salve e faça o upload no portal.'],
[''],
['COLUNAS:'],
[' Nome do Insumo * → Obrigatório. Nome do material. Seja específico.'],
[' Quantidade * → Obrigatório. Número inteiro ou decimal. Ex: 2 ou 1.5'],
[' Unidade → Opcional. Ex: unidade, resma, kg, litro, rolo, caixa.'],
[' Observação → Opcional. Informações adicionais.'],
[''],
['DICAS:'],
[' • Os nomes são comparados automaticamente com o catálogo do almoxarifado.'],
[' • Itens não encontrados no catálogo são marcados com ⚠ mas ainda aceitos.'],
]);
wsInst['!cols'] = [{ wch: 72 }];
XLSX.utils.book_append_sheet(wb, wsInst, 'Instruções');
XLSX.writeFile(wb, 'modelo_requisicao_maple_bear.xlsx');
}
async function almAbrirNovaReq() {
almNRCart = [];
almNRXlsxParsed = [];
document.getElementById('almNRObs').value = '';
document.getElementById('almNRErr').classList.remove('show');
document.getElementById('almNRSearchInput').value = '';
document.getElementById('almNRXlsxResult').style.display = 'none';
almNRSwitchTab('buscar');
almNRRenderCart();
// Load catalog
if (!almNRCatalogo.length) {
const d = await callDiplomas({ action: 'alm_catalogo' });
almNRCatalogo = d.data || [];
}
almNRFiltrarCatalogo();
// Load professoras into selector
const profsRaw = await api({ action: 'professoras_list' }).catch(() => []);
const profs = Array.isArray(profsRaw) ? profsRaw : [];
const sel = document.getElementById('almNRProfessora');
sel.innerHTML = '
— Selecione a professora — ' +
profs.map(p => `
${p.nome}${p.email?' · '+p.email:''} `).join('');
document.getElementById('almNovaReqModal').style.display = 'block';
}
function almFecharNovaReq() {
document.getElementById('almNovaReqModal').style.display = 'none';
almNRCart = [];
almNRXlsxParsed = [];
}
function almNRSwitchTab(tab) {
document.getElementById('almNRTabBuscar').style.display = tab==='buscar' ?'block':'none';
document.getElementById('almNRTabPlanilha').style.display = tab==='planilha' ?'block':'none';
document.getElementById('almNRTabBtnBuscar').classList.toggle('active', tab==='buscar');
document.getElementById('almNRTabBtnPlanilha').classList.toggle('active', tab==='planilha');
}
function almNRFiltrarCatalogo() {
const q = document.getElementById('almNRSearchInput').value.toLowerCase();
const filtered = almNRCatalogo.filter(it =>
it.nome.toLowerCase().includes(q) || (it.categoria||'').toLowerCase().includes(q)
);
const el = document.getElementById('almNRCatalogList');
if (!filtered.length) { el.innerHTML = '
Nenhum insumo encontrado.
'; return; }
const cats = {};
for (const it of filtered) { const c = it.categoria||'Geral'; (cats[c]||(cats[c]=[])).push(it); }
el.innerHTML = Object.entries(cats).map(([cat, items]) =>
`
${cat}
` +
items.map(it => {
const inCart = almNRCart.find(c => c.insumo_id === it.id);
return `
${it.nome}
${almNRFmtBRL(it.preco)} / ${it.unidade} · Estoque: ${it.estoque_qty}
${inCart?'✓ Adicionado':'+ Adicionar'}
`;
}).join('')
).join('');
}
function almNRAddCart(id) {
if (almNRCart.find(c => c.insumo_id === id)) return;
const it = almNRCatalogo.find(c => c.id === id);
if (!it) return;
almNRCart.push({ insumo_id: it.id, nome: it.nome, unidade: it.unidade, preco_unit: it.preco, qty_solicitado: 1 });
almNRRenderCart();
almNRFiltrarCatalogo();
}
function almNRRemoveCart(id) {
almNRCart = almNRCart.filter(c => c.insumo_id !== id);
almNRRenderCart();
almNRFiltrarCatalogo();
}
function almNRRenderCart() {
const emptyEl = document.getElementById('almNRCartEmpty');
const listEl = document.getElementById('almNRCartList');
if (!almNRCart.length) { emptyEl.style.display='block'; listEl.innerHTML=''; almNRUpdateTotal(); return; }
emptyEl.style.display = 'none';
listEl.innerHTML = almNRCart.map(it =>
`
`
).join('');
almNRUpdateTotal();
}
function almNRUpdateQty(id, val) {
const it = almNRCart.find(c => c.insumo_id === id);
if (it) it.qty_solicitado = Math.max(0.5, parseFloat(val)||1);
almNRUpdateTotal();
}
function almNRUpdateTotal() {
const t = almNRCart.reduce((s,it) => s + it.qty_solicitado * it.preco_unit, 0);
document.getElementById('almNRTotal').textContent = almNRFmtBRL(t);
}
function almNRMatchCatalogo(nome) {
const qNorm = almNRNorm(nome);
const qWords = qNorm.split(' ').filter(Boolean);
if (!qWords.length) return null;
let hit = almNRCatalogo.find(c => almNRNorm(c.nome) === qNorm);
if (hit) return { item: hit, pct: 100 };
let best = null, bestScore = 0;
for (const c of almNRCatalogo) {
const cSet = new Set(almNRNorm(c.nome).split(' ').filter(Boolean));
const score = qWords.filter(w => cSet.has(w)).length / qWords.length;
if (score > bestScore) { bestScore = score; best = c; }
}
if (bestScore >= 0.5) return { item: best, pct: Math.round(bestScore*100) };
return null;
}
function almNRHandleDrop(e) { e.preventDefault(); const f=e.dataTransfer.files[0]; if(f) almNRProcessarXlsx(f); }
async function almNRProcessarXlsx(file) {
almNRXlsxParsed = [];
document.getElementById('almNRXlsxResult').style.display = 'none';
if (!almNRCatalogo.length) {
const d = await callDiplomas({ action: 'alm_catalogo' });
almNRCatalogo = d.data || [];
}
try {
const buf = await file.arrayBuffer();
const wb = XLSX.read(new Uint8Array(buf), { type:'array' });
const sheetName = wb.SheetNames.find(n => !n.toLowerCase().includes('instru')) || wb.SheetNames[0];
const rows = XLSX.utils.sheet_to_json(wb.Sheets[sheetName], { header:1, defval:'' });
const data = rows.slice(1)
.filter(r => String(r[0]||'').trim())
.map(r => ({ nome:String(r[0]||'').trim(), qty:parseFloat(String(r[1]).replace(',','.'))||0, unidade:String(r[2]||'').trim()||null, obs:String(r[3]||'').trim() }))
.filter(it => it.qty > 0);
if (!data.length) { showToast('Nenhum item encontrado na planilha.', 'warning'); return; }
let matched = 0;
almNRXlsxParsed = data.map(it => {
const hit = almNRMatchCatalogo(it.nome);
if (hit) matched++;
return { nome: hit?hit.item.nome:it.nome, insumo_id:hit?hit.item.id:null, qty_solicitado:it.qty, unidade:it.unidade||(hit?hit.item.unidade:'unidade'), preco_unit:hit?hit.item.preco:0, catalogado:!!hit, pct:hit?hit.pct:0 };
});
document.getElementById('almNRXlsxSummary').textContent = `${data.length} item(ns) · ${matched} no catálogo · ${data.length-matched} não catalogado(s)`;
document.getElementById('almNRXlsxRows').innerHTML = almNRXlsxParsed.map((it,i) =>
`
${it.catalogado?'✅':'⚠️'}
${it.nome}
${it.catalogado?it.pct+'% precisão':'Não no catálogo'}
×${it.qty_solicitado} ${it.unidade}
`
).join('');
document.getElementById('almNRXlsxResult').style.display = 'block';
} catch(e) { showToast('Erro ao ler o arquivo. Verifique o formato.', 'error'); }
}
function almNRImportarXlsx() {
for (const it of almNRXlsxParsed) {
const dup = almNRCart.find(c => it.insumo_id ? c.insumo_id===it.insumo_id : c.nome===it.nome);
if (dup) dup.qty_solicitado += it.qty_solicitado;
else almNRCart.push({ insumo_id:it.insumo_id, nome:it.nome, unidade:it.unidade, preco_unit:it.preco_unit, qty_solicitado:it.qty_solicitado });
}
almNRRenderCart();
almNRSwitchTab('buscar');
document.getElementById('almNRXlsxResult').style.display = 'none';
almNRXlsxParsed = [];
}
async function almSubmitNovaReq() {
const errEl = document.getElementById('almNRErr');
errEl.classList.remove('show');
const profId = document.getElementById('almNRProfessora').value;
if (!profId) { errEl.textContent='Selecione a professora.'; errEl.classList.add('show'); return; }
if (!almNRCart.length) { errEl.textContent='Adicione pelo menos um item.'; errEl.classList.add('show'); return; }
const btn = document.getElementById('almNRBtnEnviar');
btn.disabled = true; btn.textContent = 'Criando…';
const d = await callDiplomas({
action: 'alm_criar_req_gerente',
professora_id: profId,
itens: almNRCart,
observacao: document.getElementById('almNRObs').value.trim(),
});
btn.disabled = false; btn.textContent = '✅ Criar Requisição';
if (d.error) { errEl.textContent = d.error; errEl.classList.add('show'); return; }
almFecharNovaReq();
almLoadCompras();
}
async function almLoadInsumos() {
const d = await callDiplomas({ action: 'alm_insumos_list' });
const insumos = d.data || [];
document.getElementById('almInsumosCount').textContent = insumos.filter((i)=>i.ativo).length;
document.getElementById('almInsumosList').innerHTML = insumos.map((it) => {
return `
${it.nome}
${it.categoria||'Geral'} · Estoque: ${it.estoque_qty} ${it.unidade}
${it.qtd_por_embalagem > 1 ? ` · Embalagem: ${it.unidade_compra||'cx'} c/ ${it.qtd_por_embalagem} ${it.unidade}` : ''}
${almFmtBRL(it.preco)} ${it.qtd_por_embalagem > 1 ? `/${it.unidade_compra||'emb'} (${almFmtBRL(it.preco/it.qtd_por_embalagem)}/${it.unidade}) ` : `/${it.unidade}`}
Editar
📊${it.preco_atualizado_em?' '+new Date(it.preco_atualizado_em).toLocaleDateString('pt-BR',{day:'2-digit',month:'short'}):''}
${it.ativo ? `
Desativar ` : ''}
`;
}).join('') || '
Nenhum insumo cadastrado.
';
}
async function almEditarInsumoById(id) {
const d = await callDiplomas({ action: 'alm_insumos_list' });
const it = (d.data || []).find(i => i.id === id);
if (it) almEditarInsumo(it);
}
function almEditarInsumo(it) {
document.getElementById('almInsumoId').value = it.id;
document.getElementById('almInsumoNome').value = it.nome;
document.getElementById('almInsumoDesc').value = it.descricao||'';
document.getElementById('almInsumoUnidade').value = it.unidade;
document.getElementById('almInsumoEstoque').value = it.estoque_qty;
document.getElementById('almInsumoPreco').value = it.preco;
document.getElementById('almInsumoCategoria').value= it.categoria||'';
document.getElementById('almInsumoUnidadeCompra').value = it.unidade_compra||'';
document.getElementById('almInsumoQtdEmb').value = it.qtd_por_embalagem||1;
almAtualizarPrecoUnit();
document.getElementById('almInsumoFormTitle').textContent = '✏️ Editar Insumo';
document.getElementById('almInsumoFormCard').scrollIntoView({ behavior:'smooth' });
}
function almLimparInsumoForm() {
['almInsumoId','almInsumoNome','almInsumoDesc','almInsumoEstoque','almInsumoPreco','almInsumoCategoria','almInsumoUnidadeCompra'].forEach(id => document.getElementById(id).value='');
document.getElementById('almInsumoUnidade').value = 'unidade';
document.getElementById('almInsumoQtdEmb').value = '1';
document.getElementById('almPrecoUnitInfo').textContent = '';
document.getElementById('almInsumoFormTitle').textContent = '➕ Novo Insumo';
}
function almAtualizarPrecoUnit() {
const preco = parseFloat(document.getElementById('almInsumoPreco').value) || 0;
const qtd = parseFloat(document.getElementById('almInsumoQtdEmb').value) || 1;
const el = document.getElementById('almPrecoUnitInfo');
if (qtd > 1 && preco > 0) {
el.textContent = 'Preco por ' + document.getElementById('almInsumoUnidade').value + ': ' + almFmtBRL(preco / qtd);
el.style.color = '#2d7a3a';
el.style.fontWeight = '600';
} else {
el.textContent = '';
}
}
async function almSalvarInsumo() {
const errEl = document.getElementById('almInsumoErr');
const okEl = document.getElementById('almInsumoOk');
errEl.classList.remove('show'); okEl.classList.remove('show');
const d = await callDiplomas({
action: 'alm_insumo_save',
id: document.getElementById('almInsumoId').value || undefined,
nome: document.getElementById('almInsumoNome').value.trim(),
descricao: document.getElementById('almInsumoDesc').value.trim(),
unidade: document.getElementById('almInsumoUnidade').value.trim() || 'unidade',
estoque_qty: parseFloat(document.getElementById('almInsumoEstoque').value) || 0,
preco: parseFloat(document.getElementById('almInsumoPreco').value) || 0,
categoria: document.getElementById('almInsumoCategoria').value.trim(),
unidade_compra: document.getElementById('almInsumoUnidadeCompra').value.trim() || null,
qtd_por_embalagem: parseFloat(document.getElementById('almInsumoQtdEmb').value) || 1,
});
if (d.error) { errEl.textContent = d.error; errEl.classList.add('show'); return; }
okEl.textContent = '✅ Insumo salvo!'; okEl.classList.add('show');
almLimparInsumoForm();
almLoadInsumos();
}
async function almDesativarInsumo(id) {
if (!confirm('Desativar este insumo?')) return;
const d = await callDiplomas({ action: 'alm_insumo_del', id });
if (!d.error) almLoadInsumos();
}
async function almLoadTurmas() {
const d = await callDiplomas({ action: 'alm_series_list' });
const turmas = d.data || [];
document.getElementById('almTurmasCount').textContent = turmas.length;
document.getElementById('almTurmasList').innerHTML = turmas.map(t => `
${t.nome}
${(t.professoras||[]).length} professora(s)
Editar
Remover
`).join('') || '
Nenhuma turma cadastrada.
';
// Professor-turma assignment list
const profsRaw = await api({ action: 'professoras_list' }).catch(() => []);
const allProfs = Array.isArray(profsRaw) ? profsRaw : [];
document.getElementById('almProfTurmaList').innerHTML = allProfs.length
? allProfs.map((p) => `
${p.nome} ${p.email}
— Sem turma —
${turmas.map((t) => `${t.nome} `).join('')}
`).join('')
: '
Carregue o painel de professoras primeiro.
';
}
function almEditarTurma(t) {
document.getElementById('almTurmaId').value = t.id;
document.getElementById('almTurmaNome').value = t.nome;
document.getElementById('almTurmaCor').value = t.cor || '#3B82F6';
document.getElementById('almTurmaFormTitle').textContent = '✏️ Editar Turma';
}
async function almSalvarTurma() {
const errEl = document.getElementById('almTurmaErr');
const okEl = document.getElementById('almTurmaOk');
errEl.classList.remove('show'); okEl.classList.remove('show');
const d = await callDiplomas({
action: 'alm_turma_save',
id: document.getElementById('almTurmaId').value || undefined,
nome: document.getElementById('almTurmaNome').value.trim(),
cor: document.getElementById('almTurmaCor').value,
});
if (d.error) { errEl.textContent = d.error; errEl.classList.add('show'); return; }
okEl.textContent = '✅ Turma salva!'; okEl.classList.add('show');
document.getElementById('almTurmaId').value = '';
document.getElementById('almTurmaNome').value = '';
document.getElementById('almTurmaFormTitle').textContent = '➕ Nova Turma';
almLoadTurmas();
}
async function almDelTurma(id) {
if (!confirm('Remover esta turma?')) return;
await callDiplomas({ action: 'alm_turma_del', id });
almLoadTurmas();
}
async function almSetProfTurma(professora_id, turma_id) {
await callDiplomas({ action: 'alm_prof_set_turma', professora_id, turma_id: turma_id || null });
}
async function almLoadOrcamentos() {
const mes = document.getElementById('almOrcMes').value || new Date().toISOString().slice(0,7);
const d = await callDiplomas({ action: 'alm_orcamentos_list', mes });
const turmas = d.data || [];
if (!turmas.length) { document.getElementById('almOrcList').innerHTML = '
Cadastre turmas primeiro.
'; return; }
document.getElementById('almOrcList').innerHTML = `
`;
}
async function almSalvarOrcamento(turma_id, mes) {
const valor = parseFloat(document.getElementById('orc-' + turma_id).value) || 0;
const d = await callDiplomas({ action: 'alm_orcamento_set', turma_id, mes, valor });
const okEl = document.getElementById('almOrcOk');
if (!d.error) { okEl.textContent = '✅ Orçamento salvo!'; okEl.classList.add('show'); setTimeout(() => okEl.classList.remove('show'), 2500); }
}
async function almAplicarOrcPadrao() {
const valor = parseFloat(document.getElementById('almOrcPadrao').value) || 0;
const mes = document.getElementById('almOrcMes').value || new Date().toISOString().slice(0,7);
if (!confirm('Aplicar R$ ' + valor.toFixed(2).replace('.',',') + ' para TODAS as turmas em ' + mes + '?')) return;
const d = await callDiplomas({ action: 'alm_orcamentos_list', mes });
const turmas = d.data || [];
for (const t of turmas) {
await callDiplomas({ action: 'alm_orcamento_set', turma_id: t.id, mes, valor });
}
showToast('Orçamento padrão aplicado a ' + turmas.length + ' turmas!', 'success');
almLoadOrcamentos();
}
async function almAplicarOrcAnual() {
const valor = parseFloat(document.getElementById('almOrcPadrao').value) || 0;
const ano = document.getElementById('almOrcAno').value || new Date().getFullYear();
if (!confirm('Aplicar R$ ' + valor.toFixed(2).replace('.',',') + ' para TODAS as turmas em TODOS os 12 meses de ' + ano + '?')) return;
// Busca turmas usando qualquer mês (só precisa da lista)
const d = await callDiplomas({ action: 'alm_orcamentos_list', mes: ano + '-01' });
const turmas = d.data || [];
if (!turmas.length) { showToast('Nenhuma turma cadastrada.', 'warning'); return; }
let total = 0;
for (let m = 1; m <= 12; m++) {
const mes = ano + '-' + String(m).padStart(2, '0');
for (const t of turmas) {
await callDiplomas({ action: 'alm_orcamento_set', turma_id: t.id, mes, valor });
total++;
}
}
showToast('Orçamento aplicado: ' + turmas.length + ' turmas × 12 meses (' + total + ' registros)!', 'success');
almLoadOrcamentos();
}
// ── IMPORTAR INSUMOS VIA EXCEL ──────────────────────
let almInsumoXlsxParsed = [];
function almGerarModeloInsumos() {
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet([
['Nome *', 'Descrição', 'Unidade', 'Preço Unitário (R$)', 'Categoria', 'Estoque Inicial'],
['Papel Sulfite A4 75g', 'Resma 500 folhas', 'resma', 12.90, 'Papelaria', 100],
['Caneta Esferográfica Azul', 'Ponta fina', 'unidade', 2.50, 'Canetas', 200],
['TNT Vermelho', 'Rolo 50m', 'rolo', 35.00, 'Decoração', 10],
['Cola Bastão 40g', '', 'unidade', 4.50, 'Papelaria', 50],
['','','','','',''],['','','','','',''],['','','','','',''],
['','','','','',''],['','','','','',''],
]);
ws['!cols'] = [{ wch: 36 }, { wch: 24 }, { wch: 12 }, { wch: 18 }, { wch: 16 }, { wch: 16 }];
XLSX.utils.book_append_sheet(wb, ws, 'Insumos');
const wsInst = XLSX.utils.aoa_to_sheet([
['INSTRUÇÕES — Modelo de Cadastro de Insumos · Maple Bear'],
[''],
['1. Preencha a aba "Insumos" a partir da linha 2 (linha 1 é o cabeçalho).'],
['2. Apenas o Nome é obrigatório. Os demais campos são opcionais.'],
['3. Categorias sugeridas: Papelaria, Canetas, Decoração, Limpeza, Higiene, Descartáveis, Didático, Escritório'],
['4. Salve o arquivo e faça o upload no portal.'],
]);
wsInst['!cols'] = [{ wch: 72 }];
XLSX.utils.book_append_sheet(wb, wsInst, 'Instruções');
XLSX.writeFile(wb, 'modelo_insumos_maple_bear.xlsx');
}
async function almImportarInsumosXlsx(input) {
const file = input.files[0];
if (!file) return;
input.value = '';
try {
const buf = await file.arrayBuffer();
const wb = XLSX.read(new Uint8Array(buf), { type:'array' });
const sheetName = wb.SheetNames.find(n => !n.toLowerCase().includes('instru')) || wb.SheetNames[0];
const rows = XLSX.utils.sheet_to_json(wb.Sheets[sheetName], { header:1, defval:'' });
almInsumoXlsxParsed = rows.slice(1)
.filter(r => String(r[0]||'').trim())
.map(r => ({
nome: String(r[0]||'').trim(),
descricao: String(r[1]||'').trim(),
unidade: String(r[2]||'').trim() || 'unidade',
preco: parseFloat(String(r[3]).replace(',','.'))||0,
categoria: String(r[4]||'').trim(),
estoque_qty: parseFloat(String(r[5]).replace(',','.'))||0,
}));
if (!almInsumoXlsxParsed.length) { showToast('Nenhum insumo encontrado na planilha.','warning'); return; }
document.getElementById('almInsumoXlsxSummary').textContent = almInsumoXlsxParsed.length + ' insumo(s) encontrado(s)';
document.getElementById('almInsumoXlsxRows').innerHTML = almInsumoXlsxParsed.map((it,i) =>
`
${esc(it.nome)}
${esc(it.categoria||'—')} · ${esc(it.unidade)} · R$ ${it.preco.toFixed(2).replace('.',',')} · Est: ${it.estoque_qty}
`
).join('');
document.getElementById('almInsumoXlsxResult').style.display = 'block';
} catch(e) { showToast('Erro ao ler arquivo: ' + e.message, 'error'); }
}
async function almConfirmarImportInsumos() {
let ok = 0, erros = 0;
for (const it of almInsumoXlsxParsed) {
const d = await callDiplomas({ action:'alm_insumo_save', ...it });
if (d.error) erros++; else ok++;
}
showToast(ok + ' insumo(s) cadastrado(s)' + (erros ? ', ' + erros + ' erro(s)' : ''), erros ? 'warning' : 'success');
document.getElementById('almInsumoXlsxResult').style.display = 'none';
almInsumoXlsxParsed = [];
almLoadInsumos();
}
// ── MERCADO LIVRE ────────────────────────────────────
async function checkMLStatus() {
const d = await callDiplomas({ action: 'ml_status' });
const el = document.getElementById('mlStatus');
const btn = document.getElementById('almBtnML');
if (d.connected) {
el.innerHTML = '
✅ Mercado Livre conectado ';
btn.textContent = '🛒 ML Conectado';
btn.style.background = '#2d7a3a';
btn.style.color = '#fff';
} else {
el.innerHTML = '
⚠️ Mercado Livre não conectado — preços limitados ';
}
}
async function conectarML() {
const d = await callDiplomas({ action: 'ml_auth_url' });
if (d.url) window.open(d.url, '_blank');
}
// ── ATUALIZAR PREÇOS ─────────────────────────────────
async function almAtualizarPrecos() {
const btn = document.getElementById('almBtnAtualizarPrecos');
btn.disabled = true;
const insData = await callDiplomas({ action: 'alm_insumos_list' });
const insumos = (insData.data || []).filter(i => i.ativo);
if (!insumos.length) { showToast('Nenhum insumo ativo.', 'warning'); btn.disabled = false; btn.textContent = '🔄 Atualizar Precos'; return; }
let atualizados = 0;
const total = insumos.length;
const fontesResumo = {};
for (let i = 0; i < total; i++) {
const ins = insumos[i];
btn.textContent = `⏳ ${i+1}/${total} — ${ins.nome.substring(0,25)}...`;
try {
const d = await callDiplomas({ action: 'alm_buscar_precos', nome: ins.nome, unidade: ins.unidade });
const resultados = d.data || [];
// Acumula status das fontes
if (d.fontes) {
for (const [f, info] of Object.entries(d.fontes)) {
if (!fontesResumo[f]) fontesResumo[f] = { ok: 0, erro: 0, status: info.status };
if (info.produtos > 0) fontesResumo[f].ok++;
else fontesResumo[f].erro++;
fontesResumo[f].status = info.status;
if (info.erro) fontesResumo[f].ultimoErro = info.erro;
}
}
const produtos = resultados.filter(r => r.tipo === 'produto' && r.preco != null && r.match >= 70);
if (produtos.length) {
produtos.sort((a, b) => a.preco - b.preco);
const melhor = produtos[0];
// Inclui todas as fontes consultadas no historico
const fontesConsultadas = Object.entries(d.fontes || {}).map(([f,info]) =>
`${f}: ${info.status}${info.produtos > 0 ? ' ('+info.produtos+' resultados)' : ''}${info.erro ? ' — '+info.erro : ''}`
).join(' | ');
await callDiplomas({
action: 'alm_insumo_atualizar_auto',
id: ins.id,
preco: melhor.preco,
produto_nome: melhor.nome + ' [Fontes: ' + fontesConsultadas + ']',
fonte: melhor.plataforma,
url: melhor.url_produto,
match_pct: melhor.match,
});
atualizados++;
}
} catch(_) {}
}
btn.disabled = false;
btn.textContent = '🔄 Atualizar Precos';
// Mostra resumo das fontes
const fontesInfo = Object.entries(fontesResumo).map(([f, info]) => {
const icon = info.ok > 0 ? '✅' : info.status === 'apenas link' ? '🔗' : '❌';
return `${icon} ${f}: ${info.ok}/${total}`;
}).join(' · ');
showToast(`${atualizados}/${total} atualizados · ${fontesInfo}`, atualizados > 0 ? 'success' : 'warning', 8000);
almLoadInsumos();
}
async function almVerHistorico(id, nome) {
const d = await callDiplomas({ action: 'alm_insumo_historico', id });
const hist = d.data || [];
if (!hist.length) { showToast('Nenhum historico de preco para este item.', 'info'); return; }
const html = hist.map(h => {
const dt = new Date(h.criado_em).toLocaleDateString('pt-BR', { day:'2-digit', month:'short', year:'numeric', hour:'2-digit', minute:'2-digit' });
const mudouEmb = h.qtd_emb_nova !== h.qtd_emb_anterior;
return `
${dt} · ${esc(h.fonte||'?')}
${almFmtBRL(h.preco_anterior)} → ${almFmtBRL(h.preco_novo)}
${esc(h.produto_encontrado||'—')}
${mudouEmb ? `
Embalagem: ${h.unidade_compra_nova||'?'} c/ ${h.qtd_emb_nova} un
` : ''}
${h.url ? `
Ver produto ` : ''}
`;
}).join('');
// Usar modal simples
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:600;display:flex;align-items:center;justify-content:center;padding:20px;';
overlay.onclick = e => { if (e.target === overlay) overlay.remove(); };
overlay.innerHTML = `
Historico de Precos — ${esc(nome)}
×
${html}
`;
document.body.appendChild(overlay);
}
// ── CATEGORIAS DE INSUMOS ────────────────────────────
let almCategorias = [];
async function almLoadCategorias() {
const d = await api({ action: 'alm_categorias_list' });
almCategorias = Array.isArray(d) ? d : [];
// Atualiza chips
document.getElementById('almCatChips').innerHTML = almCategorias.map(c =>
`
${esc(c.nome)} `
).join('');
// Atualiza select no form de insumo
const sel = document.getElementById('almInsumoCategoria');
if (sel && sel.tagName === 'SELECT') {
const val = sel.value;
sel.innerHTML = '
— sem categoria — ' + almCategorias.map(c => `
${esc(c.nome)} `).join('');
sel.value = val;
}
}
function toggleAlmCatConfig() {
const el = document.getElementById('almCatConfig');
el.style.display = el.style.display === 'none' ? 'block' : 'none';
if (el.style.display === 'block') renderAlmCatConfig();
}
async function renderAlmCatConfig() {
const d = await api({ action: 'alm_categorias_list_all' });
const cats = Array.isArray(d) ? d : [];
document.getElementById('almCatList').innerHTML = cats.map(c =>
`
${esc(c.nome)}
${c.ativo?'Desativar':'Ativar'}
`
).join('');
}
async function toggleAlmCat(id, ativo) {
await api({ action: 'alm_categoria_toggle', id, ativo });
await almLoadCategorias();
renderAlmCatConfig();
}
async function addAlmCategoria() {
const nome = document.getElementById('almNovaCat').value.trim();
if (!nome) return;
const d = await api({ action: 'alm_categoria_save', nome });
if (d.error) { showToast(d.error, 'error'); return; }
document.getElementById('almNovaCat').value = '';
await almLoadCategorias();
renderAlmCatConfig();
}
async function almLoadRelatorio() {
const mes = document.getElementById('almRelMes').value || new Date().toISOString().slice(0,7);
const d = await callDiplomas({ action: 'alm_relatorio', mes });
const grupos = d.data || [];
if (!grupos.length) { document.getElementById('almRelatorio').innerHTML = '
Nenhuma requisição no período.
'; return; }
document.getElementById('almRelatorio').innerHTML = grupos.map(g => {
const pct = g.orcamento > 0 ? Math.min(100,(g.gasto/g.orcamento)*100).toFixed(1) : 0;
const barColor = pct>=90?'#e53e3e':pct>=70?'#f6a623':'#48bb78';
return `
${g.turma.nome}
Orçamento: ${almFmtBRL(g.orcamento)}
✅ Aprovado: ${almFmtBRL(g.gasto)}
⏳ Pendente: ${almFmtBRL(g.pendente)}
❌ Rejeitado: ${almFmtBRL(g.rejeitado)}
${pct}% do orçamento utilizado
`;
}).join('');
}
// ── FAMÍLIAS (tabela familias) ────────────────────────
let famDados = [];
let famSeriesOpts = '';
// ── IMPORTACAO DE FAMILIAS E ATIVIDADES ──────────────
function gerarModeloFamilias() {
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet([
['Nome do Responsavel *', 'E-mail *', 'Nome da Crianca *', 'CPF', 'Serie', 'Turno'],
['Maria Silva', 'maria@email.com', 'Pedro Silva', '000.000.000-00', 'Toddler', 'integral_5x'],
['Maria Silva', 'maria@email.com', 'Ana Silva', '', 'Nursery', 'semi_4x'],
['Ana Costa', 'ana@email.com', 'Julia Costa', '', 'Year 1', 'tarde'],
['Carlos Souza', 'carlos@email.com', 'Lucas Souza', '', 'Year 2', 'integral_3x'],
['','','','','',''],['','','','','',''],['','','','','',''],
]);
ws['!cols'] = [{ wch: 28 }, { wch: 28 }, { wch: 24 }, { wch: 16 }, { wch: 14 }, { wch: 20 }];
XLSX.utils.book_append_sheet(wb, ws, 'Familias');
// Aba de turnos para referencia
const wsTurnos = XLSX.utils.aoa_to_sheet([
['Codigo do Turno', 'Descricao', 'Valor Mensal'],
['integral_5x', 'Integral · 5x na semana (todos os dias)', 'R$ 4.395,00'],
['integral_4x', 'Integral · 4x na semana', 'R$ 4.303,57'],
['integral_3x', 'Integral · 3x na semana', 'R$ 4.072,13'],
['integral_2x', 'Integral · 2x na semana', 'R$ 3.760,70'],
['integral_1x', 'Integral · 1x na semana', 'R$ 3.300,00'],
['semi_5x', 'Semi-Integral · 5x na semana', 'R$ 4.030,00'],
['semi_4x', 'Semi-Integral · 4x na semana', 'R$ 3.991,57'],
['semi_3x', 'Semi-Integral · 3x na semana', 'R$ 3.773,13'],
['semi_2x', 'Semi-Integral · 2x na semana', 'R$ 3.534,70'],
['semi_1x', 'Semi-Integral · 1x na semana', 'R$ 3.196,27'],
['tarde', 'Apenas a Tarde (inicio 13:30h)', '—'],
['diaria', 'Diaria avulsa', 'R$ 150,00'],
]);
wsTurnos['!cols'] = [{ wch: 16 }, { wch: 40 }, { wch: 16 }];
XLSX.utils.book_append_sheet(wb, wsTurnos, 'Turnos Referencia');
const wsInst = XLSX.utils.aoa_to_sheet([
['INSTRUCOES — Importacao de Familias e Turnos'],
[''],
['Colunas obrigatorias: Nome do Responsavel, E-mail, Nome da Crianca'],
['CPF e Serie sao opcionais. Turno e IMPORTANTE para controle.'],
[''],
['TURNO: use o codigo exato da aba "Turnos Referencia"'],
[' Ex: integral_5x, semi_3x, tarde, diaria'],
[''],
['Cada linha = uma crianca. Se um responsavel tem 2 filhos, use 2 linhas com o mesmo email.'],
['O email sera usado para liberar o acesso ao portal dos pais.'],
]);
wsInst['!cols'] = [{ wch: 72 }];
XLSX.utils.book_append_sheet(wb, wsInst, 'Instrucoes');
XLSX.writeFile(wb, 'modelo_familias_maple_bear.xlsx');
}
function gerarModeloAtividades() {
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet([
['Nome da Crianca *', 'E-mail Responsavel *', 'Nome Responsavel', 'Serie', 'Atividade *', 'Turma da Atividade'],
['Pedro Silva', 'maria@email.com', 'Maria Silva', 'Toddler', 'Natacao', 'Turma A — 14h'],
['Pedro Silva', 'maria@email.com', 'Maria Silva', 'Toddler', 'Futebol', 'Turma B — 15h'],
['Julia Costa', 'ana@email.com', 'Ana Costa', 'Year 1', 'Bale', 'Turma A — 14h'],
['','','','','',''],['','','','','',''],
]);
ws['!cols'] = [{ wch: 24 }, { wch: 28 }, { wch: 24 }, { wch: 14 }, { wch: 20 }, { wch: 22 }];
XLSX.utils.book_append_sheet(wb, ws, 'Atividades');
const wsInst = XLSX.utils.aoa_to_sheet([
['INSTRUCOES — Importacao de Inscricoes em Atividades'],
[''],
['Colunas obrigatorias: Nome da Crianca, E-mail Responsavel, Atividade'],
['Cada linha = uma inscricao. Se uma crianca faz 2 atividades, use 2 linhas.'],
['A atividade deve existir previamente no sistema (Gerenciar Atividades).'],
]);
wsInst['!cols'] = [{ wch: 72 }];
XLSX.utils.book_append_sheet(wb, wsInst, 'Instrucoes');
XLSX.writeFile(wb, 'modelo_atividades_maple_bear.xlsx');
}
let famImportData = [];
let famImportMode = ''; // 'familias' ou 'atividades'
async function importarFamiliasXlsx(input) {
const file = input.files[0];
if (!file) return;
input.value = '';
famImportMode = 'familias';
try {
const buf = await file.arrayBuffer();
const wb = XLSX.read(new Uint8Array(buf), { type:'array' });
const sheetName = wb.SheetNames.find(n => !n.toLowerCase().includes('instru')) || wb.SheetNames[0];
const rows = XLSX.utils.sheet_to_json(wb.Sheets[sheetName], { header:1, defval:'' });
famImportData = rows.slice(1).filter(r => String(r[0]||'').trim() && String(r[1]||'').trim()).map(r => ({
nome_responsavel: String(r[0]||'').trim(),
email: String(r[1]||'').trim(),
nome_aluno: String(r[2]||'').trim(),
cpf: String(r[3]||'').trim(),
serie: String(r[4]||'').trim(),
turno: String(r[5]||'').trim(),
}));
if (!famImportData.length) { showToast('Nenhum registro encontrado.','warning'); return; }
document.getElementById('famImportSummary').textContent = famImportData.length + ' familia(s) encontrada(s)';
document.getElementById('famImportRows').innerHTML = famImportData.map(f =>
`
${esc(f.nome_responsavel)} · ${esc(f.email)} · ${esc(f.nome_aluno)} · ${esc(f.serie||'—')} · ${esc(f.turno||'—')}
`
).join('');
document.getElementById('famImportBtn').textContent = 'Importar ' + famImportData.length + ' familias';
document.getElementById('famImportBtn').onclick = confirmarImportFamilias;
document.getElementById('famImportResult').style.display = 'block';
} catch(e) { showToast('Erro ao ler arquivo.','error'); }
}
async function importarAtividadesXlsx(input) {
const file = input.files[0];
if (!file) return;
input.value = '';
famImportMode = 'atividades';
try {
const buf = await file.arrayBuffer();
const wb = XLSX.read(new Uint8Array(buf), { type:'array' });
const sheetName = wb.SheetNames.find(n => !n.toLowerCase().includes('instru')) || wb.SheetNames[0];
const rows = XLSX.utils.sheet_to_json(wb.Sheets[sheetName], { header:1, defval:'' });
famImportData = rows.slice(1).filter(r => String(r[0]||'').trim() && String(r[4]||'').trim()).map(r => ({
nome_crianca: String(r[0]||'').trim(),
email: String(r[1]||'').trim(),
nome_resp: String(r[2]||'').trim(),
serie: String(r[3]||'').trim(),
atividade: String(r[4]||'').trim(),
turma: String(r[5]||'').trim(),
}));
if (!famImportData.length) { showToast('Nenhum registro encontrado.','warning'); return; }
document.getElementById('ativImportSummary').textContent = famImportData.length + ' inscricao(oes) encontrada(s)';
document.getElementById('ativImportRows').innerHTML = famImportData.map(f =>
`
${esc(f.nome_crianca)} · ${esc(f.email)} · ${esc(f.atividade)} · ${esc(f.turma||'—')}
`
).join('');
document.getElementById('ativImportBtn').textContent = 'Importar ' + famImportData.length + ' inscricoes';
document.getElementById('ativImportResult').style.display = 'block';
} catch(e) { showToast('Erro ao ler arquivo.','error'); }
}
async function confirmarImportFamilias() {
const btn = document.getElementById('famImportBtn');
btn.disabled = true; btn.textContent = 'Importando...';
let ok = 0, erros = 0;
for (const f of famImportData) {
try {
// Insere em familias (acesso ao portal)
await fetch(SUPABASE_URL + '/rest/v1/familias', {
method: 'POST', headers: { 'apikey': SUPABASE_ANON, 'Authorization': 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json', 'Prefer': 'return=minimal' },
body: JSON.stringify({ nome_responsavel: f.nome_responsavel, nome_aluno: f.nome_aluno, email: f.email, cpf: f.cpf || null }),
});
// Se tem turno, insere tambem em solicitacoes
if (f.turno) {
await api({ action: 'public_submit', email: f.email, nome_resp: f.nome_responsavel, nome_crianca: f.nome_aluno, serie: f.serie || null, turno: f.turno });
}
ok++;
} catch(_) { erros++; }
}
showToast(ok + ' familia(s) importada(s)' + (erros ? ', ' + erros + ' erro(s)' : ''), erros ? 'warning' : 'success');
document.getElementById('famImportResult').style.display = 'none';
famImportData = [];
btn.disabled = false;
loadFamiliasPanel();
}
async function confirmarImportAtividades() {
const btn = document.getElementById('ativImportBtn');
btn.disabled = true; btn.textContent = 'Importando...';
let ok = 0, erros = 0;
for (const f of famImportData) {
try {
await api({ action: 'inscricao_atividade_submit', email: f.email, nome_resp: f.nome_resp, nome_crianca: f.nome_crianca, serie: f.serie || null, atividades_detalhe: [{ nome: f.atividade, turma_selecionada: f.turma || null }] });
ok++;
} catch(_) { erros++; }
}
showToast(ok + ' inscricao(oes) importada(s)' + (erros ? ', ' + erros + ' erro(s)' : ''), erros ? 'warning' : 'success');
document.getElementById('ativImportResult').style.display = 'none';
famImportData = [];
btn.disabled = false;
}
function toggleCadastroForm() {
const el = document.getElementById('cadFormWrap');
el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
async function loadFamiliasPanel() {
loadCadastroPanel();
if (!famSeriesOpts) {
const s = await api({ action: 'series_list' });
const arr = Array.isArray(s) ? s : [];
famSeriesOpts = arr.map(x => `
${esc(x.nome)} `).join('');
}
const d = await api({ action: 'familias_list' });
famDados = Array.isArray(d) ? d : [];
renderFamiliasLista();
}
function renderFamiliasLista() {
const q = (document.getElementById('famSearch')?.value || '').toLowerCase();
const lista = famDados.filter(r =>
!q || [r.nome_aluno, r.nome_responsavel, r.email, r.cpf, r.serie].some(v => v?.toLowerCase().includes(q))
);
document.getElementById('famCount').textContent = lista.length;
if (!lista.length) {
document.getElementById('famLista').innerHTML = '
Nenhuma família encontrada.
';
return;
}
document.getElementById('famLista').innerHTML = `
Aluno
Responsável
CPF
Série
${lista.map(r => `
${esc(r.nome_aluno)} ${esc(r.email||'')}
${esc(r.nome_responsavel||'—')}
${esc(r.cpf||'—')}
— sem série —
${famSeriesOpts}
🗑
`).join('')}
`;
// Seta valores dos selects de série
lista.forEach(r => {
const sel = document.getElementById('famSel_' + r.cpf);
if (sel && r.serie) sel.value = r.serie;
});
}
async function updateFamSerie(cpf, serie) {
const d = await api({ action: 'familias_update', cpf, serie: serie || null });
if (d.error) alert('Erro ao atualizar série: ' + d.error);
}
async function deleteFamilia(cpf, nome) {
if (!confirm('Remover ' + nome + ' da lista de famílias?')) return;
await api({ action: 'familias_delete', cpf });
await loadFamiliasPanel();
}
function exportarFamiliasCSV() {
if (!famDados.length) { showToast('Nenhuma familia para exportar','error'); return; }
const headers = ['nome_aluno','nome_responsavel','cpf','email','serie','turno'];
const csvRows = [headers.join(';')];
for (const r of famDados) {
csvRows.push(headers.map(h => {
let val = (r[h] || '').toString().replace(/"/g, '""');
if (val.includes(';') || val.includes('"') || val.includes('\n')) val = '"' + val + '"';
return val;
}).join(';'));
}
const bom = '\uFEFF';
const blob = new Blob([bom + csvRows.join('\r\n')], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'familias_' + new Date().toISOString().slice(0,10) + '.csv';
a.click();
URL.revokeObjectURL(url);
showToast('CSV exportado!','success');
}
// ── CADASTRAR FAMÍLIA ─────────────────────────────────
let cadDados = [];
async function loadCadastroPanel() {
// Carrega séries no select
const sel = document.getElementById('cadSerie');
if (sel.options.length <= 1) {
const d = await api({ action: 'series_list' });
(Array.isArray(d) ? d : []).forEach(s => {
const o = document.createElement('option');
o.value = s.nome; o.textContent = s.nome;
sel.appendChild(o);
});
}
// Carrega lista de solicitações
const d = await api({ action: 'solicitacoes_list' });
cadDados = Array.isArray(d) ? d : (allData || []);
renderCadastroLista();
}
function renderCadastroLista() {
const q = (document.getElementById('cadSearch')?.value || '').toLowerCase();
const lista = cadDados.filter(r =>
!q || [r.nome_crianca, r.nome_resp, r.email, r.serie].some(v => v?.toLowerCase().includes(q))
);
document.getElementById('cadCount').textContent = lista.length;
if (!lista.length) {
document.getElementById('cadLista').innerHTML = '
Nenhuma solicitação encontrada.
';
return;
}
document.getElementById('cadLista').innerHTML = `
Criança
Responsável
E-mail
Turno
${lista.map(r => `
${esc(r.nome_crianca)} ${r.serie ? `${esc(r.serie)} ` : ''}
${esc(r.nome_resp||'—')}
${esc(r.email||'—')}
${esc((r.turno||'').replace(/_/g,' ').replace(/(\d)x/,'$1×'))}
✅ Aprovar
🗑
`).join('')}
`;
}
async function submitCadastro() {
const nomeResp = document.getElementById('cadNomeResp').value.trim();
const email = document.getElementById('cadEmail').value.trim();
const nomeCrianca = document.getElementById('cadNomeCrianca').value.trim();
const serie = document.getElementById('cadSerie').value;
const turno = document.getElementById('cadTurno').value;
const errEl = document.getElementById('cadErr');
const okEl = document.getElementById('cadOk');
errEl.style.display = 'none'; okEl.style.display = 'none';
if (!nomeResp || !email || !nomeCrianca || !turno) {
errEl.textContent = 'Preencha nome do responsável, e-mail, nome da criança e turno.';
errEl.style.display = 'block'; return;
}
const btn = document.getElementById('cadBtn');
btn.disabled = true; btn.textContent = 'Salvando…';
const d = await api({ action: 'public_submit', email, nome_resp: nomeResp, nome_crianca: nomeCrianca, serie: serie||null, turno }).catch(() => ({ error: 'Erro de conexão.' }));
btn.disabled = false; btn.textContent = 'Cadastrar';
if (d.error) { errEl.textContent = d.error; errEl.style.display = 'block'; return; }
okEl.textContent = `${nomeCrianca} cadastrada com sucesso!`;
okEl.style.display = 'block';
document.getElementById('cadNomeResp').value = '';
document.getElementById('cadEmail').value = '';
document.getElementById('cadNomeCrianca').value = '';
document.getElementById('cadSerie').value = '';
document.getElementById('cadTurno').value = '';
await loadCadastroPanel();
}
async function aprovarSolicitacao(id) {
const sol = cadDados.find(r => r.id === id);
if (!sol) return;
if (!confirm('Aprovar acesso de ' + (sol.nome_resp||sol.email) + ' (' + (sol.nome_crianca||'') + ')?')) return;
// Insere na tabela familias para liberar acesso
try {
await fetch(SUPABASE_URL + '/rest/v1/familias', {
method: 'POST',
headers: { 'apikey': SUPABASE_ANON, 'Authorization': 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json', 'Prefer': 'return=minimal' },
body: JSON.stringify({ nome_responsavel: sol.nome_resp, nome_aluno: sol.nome_crianca, email: sol.email, cpf: sol.cpf || null }),
});
showToast('Acesso aprovado para ' + (sol.nome_resp||sol.email) + '!', 'success');
await loadFamiliasPanel();
} catch(e) { showToast('Erro ao aprovar.', 'error'); }
}
async function deleteCadastro(id, nome) {
if (!confirm(`Remover cadastro de ${nome}?`)) return;
await api({ action: 'solicitacoes_delete', id });
await loadCadastroPanel();
}
// ── NOTIFICAÇÕES ─────────────────────────────────────
let notifData = [];
async function loadNotificacoes() {
if (!currentGerente) return;
const d = await api({ action: 'notif_list', portal: 'gerente', email: currentGerente.email });
notifData = Array.isArray(d?.data) ? d.data : [];
renderNotif();
}
function renderNotif() {
const el = document.getElementById('notifList');
const dot = document.getElementById('notifDot');
const unread = notifData.filter(n => !n.lida);
dot.style.display = unread.length > 0 ? 'block' : 'none';
if (!notifData.length) { el.innerHTML = '
Nenhuma notificação.
'; return; }
el.innerHTML = notifData.slice(0, 20).map(n => {
const dt = new Date(n.criado_em);
const tempo = dt.toLocaleDateString('pt-BR', { day:'2-digit', month:'short' }) + ' ' + dt.toLocaleTimeString('pt-BR', { hour:'2-digit', minute:'2-digit' });
return `
${esc(n.titulo)} ${esc(n.mensagem)}
${tempo}
`;
}).join('');
}
function toggleNotifPanel() {
const p = document.getElementById('notifPanel');
p.style.display = p.style.display === 'none' ? 'block' : 'none';
}
async function marcarTodasLidas() {
await api({ action: 'notif_marcar_todas', portal: 'gerente', email: currentGerente.email });
notifData.forEach(n => n.lida = true);
renderNotif();
}
// ── CALENDARIO ESCOLAR ──────────────────────────────
let calEventos = [];
let calAno = new Date().getFullYear();
const CAL_TIPOS = { feriado:'Feriado', reuniao:'Reuniao', evento:'Evento', data_comemorativa:'Data Comemorativa', recesso:'Recesso', avaliacao:'Avaliacao' };
const CAL_MESES = ['JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER'];
const CAL_DIAS = ['D','S','T','Q','Q','S','S'];
function calMudarAno(delta) { calAno += delta; loadCalendario(); }
function calToggleForm() {
const el = document.getElementById('calFormWrap');
el.style.display = el.style.display === 'none' ? 'block' : 'none';
if (el.style.display === 'none') limparCalForm();
}
async function loadCalendario() {
document.getElementById('calAnoLabel').textContent = calAno;
const d = await api({ action: 'calendario_list', ano: String(calAno) });
calEventos = Array.isArray(d) ? d : [];
renderCalAnual();
}
function renderCalAnual() {
const today = new Date();
let html = '';
for (let m = 0; m < 12; m++) {
const firstDay = new Date(calAno, m, 1).getDay();
const daysInMonth = new Date(calAno, m+1, 0).getDate();
const isCurrentMonth = today.getFullYear() === calAno && today.getMonth() === m;
// Count school days (weekdays without feriado/recesso)
let diasLetivos = 0;
for (let d = 1; d <= daysInMonth; d++) {
const dow = new Date(calAno, m, d).getDay();
if (dow === 0 || dow === 6) continue;
const ds = `${calAno}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
const isFeriado = calEventos.some(e => (e.tipo==='feriado'||e.tipo==='recesso') && e.data_inicio<=ds && (e.data_fim||e.data_inicio)>=ds);
if (!isFeriado) diasLetivos++;
}
html += `
`;
html += `
${CAL_MESES[m]}
`;
html += `
`;
html += CAL_DIAS.map(d => `
${d}
`).join('');
for (let i = 0; i < firstDay; i++) html += '
';
for (let d = 1; d <= daysInMonth; d++) {
const ds = `${calAno}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
const evts = calEventos.filter(e => e.data_inicio <= ds && (e.data_fim || e.data_inicio) >= ds);
const isToday = isCurrentMonth && today.getDate() === d;
const dow = new Date(calAno, m, d).getDay();
const isSunday = dow === 0;
let bg = 'transparent';
if (evts.length) bg = evts[0].cor || '#FFD54F';
const clickEvt = evts.length ? `onclick="calDiaClick('${ds}')"` : '';
html += `
${d}
`;
}
html += '
';
// Events list for this month
const monthEvents = calEventos.filter(e => {
const em = parseInt(e.data_inicio.split('-')[1]);
return em === m+1;
});
if (monthEvents.length) {
html += `
`;
html += `
DIAS LETIVOS: ${diasLetivos}
`;
monthEvents.forEach(e => {
const day = parseInt(e.data_inicio.split('-')[2]);
html += `
${String(day).padStart(2,'0')}
- ${esc(e.titulo)}
`;
});
html += '
';
} else {
html += `
DIAS LETIVOS: ${diasLetivos}
`;
}
html += '
';
}
document.getElementById('calAnualGrid').innerHTML = html;
}
function calDiaClick(ds) {
const evts = calEventos.filter(e => e.data_inicio <= ds && (e.data_fim || e.data_inicio) >= ds);
if (!evts.length) return;
const dt = new Date(ds + 'T12:00:00');
const diaFmt = dt.toLocaleDateString('pt-BR', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' });
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:600;display:flex;align-items:center;justify-content:center;padding:20px;';
overlay.onclick = e => { if (e.target === overlay) overlay.remove(); };
overlay.innerHTML = `
${ds.split('-')[2]}
${diaFmt}
x
${evts.map(e => {
const tipoLabel = CAL_TIPOS[e.tipo] || e.tipo;
return `
${esc(e.titulo)}
${tipoLabel}
${e.descricao ? `
${esc(e.descricao)}
` : ''}
${e.data_inicio === e.data_fim || !e.data_fim ? new Date(e.data_inicio+'T12:00:00').toLocaleDateString('pt-BR',{day:'2-digit',month:'long'}) : new Date(e.data_inicio+'T12:00:00').toLocaleDateString('pt-BR',{day:'2-digit',month:'short'}) + ' a ' + new Date(e.data_fim+'T12:00:00').toLocaleDateString('pt-BR',{day:'2-digit',month:'short'})}
${e.visivel_pais ? ' · Pais' : ''}${e.visivel_professoras ? ' · Professoras' : ''}
Editar
Excluir
`;
}).join('')}
`;
document.body.appendChild(overlay);
}
function editarEvento(e) {
document.getElementById('calEventoId').value = e.id;
document.getElementById('calTitulo').value = e.titulo;
document.getElementById('calDescricao').value = e.descricao || '';
document.getElementById('calDataInicio').value = e.data_inicio;
document.getElementById('calDataFim').value = e.data_fim || '';
document.getElementById('calTipo').value = e.tipo;
document.getElementById('calCor').value = e.cor || '#C8102E';
document.getElementById('calVisPais').checked = e.visivel_pais;
document.getElementById('calVisProf').checked = e.visivel_professoras;
document.getElementById('calFormTitle').textContent = 'Editar Evento';
}
function limparCalForm() {
['calEventoId','calTitulo','calDescricao','calDataInicio','calDataFim'].forEach(id => document.getElementById(id).value = '');
document.getElementById('calTipo').value = 'evento';
document.getElementById('calCor').value = '#C8102E';
document.getElementById('calVisPais').checked = true;
document.getElementById('calVisProf').checked = true;
document.getElementById('calFormTitle').textContent = 'Novo Evento';
}
async function salvarEvento() {
const titulo = document.getElementById('calTitulo').value.trim();
const dataInicio = document.getElementById('calDataInicio').value;
if (!titulo || !dataInicio) { document.getElementById('calErr').textContent = 'Titulo e data obrigatorios.'; document.getElementById('calErr').classList.add('show'); return; }
document.getElementById('calErr').classList.remove('show');
await api({
action: 'calendario_save',
id: document.getElementById('calEventoId').value || undefined,
titulo, descricao: document.getElementById('calDescricao').value.trim(),
data_inicio: dataInicio, data_fim: document.getElementById('calDataFim').value || dataInicio,
tipo: document.getElementById('calTipo').value, cor: document.getElementById('calCor').value,
visivel_pais: document.getElementById('calVisPais').checked,
visivel_professoras: document.getElementById('calVisProf').checked,
});
limparCalForm();
showToast('Evento salvo!', 'success');
loadCalendario();
}
async function excluirEvento(id) {
if (!confirm('Excluir este evento?')) return;
await api({ action: 'calendario_delete', id });
loadCalendario();
}
// ── ANALYTICS DASHBOARD ────────────────────────────
const MESES_CURTOS = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'];
async function loadAnalytics() {
const ano = document.getElementById('analyticsAno').value;
const el = document.getElementById('analyticsContent');
el.innerHTML = '
Carregando...
';
const d = await api({ action: 'analytics_dashboard', ano });
if (d.error) { el.innerHTML = '
Erro ao carregar.
'; return; }
const maxSol = Math.max(...d.solicitacoes_por_mes, 1);
const maxGasto = Math.max(...d.gastos_almox_por_mes, 1);
const maxManut = Math.max(...d.manutencao_por_mes, 1);
el.innerHTML = `
Solicitacoes de Turno por Mes
${d.solicitacoes_por_mes.map((v,i) => `
${v||''}
${MESES_CURTOS[i]}
`).join('')}
Gastos Almoxarifado por Mes (R$)
${d.gastos_almox_por_mes.map((v,i) => `
${v>0?almFmtBRL(v):''}
${MESES_CURTOS[i]}
`).join('')}
Chamados de Manutencao
${d.manutencao_por_mes.map((v,i) => `
${v||''}
${MESES_CURTOS[i]}
`).join('')}
${Object.entries(d.manutencao_status||{}).map(([s,c]) => `${MANUT_STATUS[s]||s}: ${c} `).join('')}
Atividades Extracurriculares
${(d.atividades||[]).length ? d.atividades.map(a => `
${esc(a.nome)}
${a.inscritos}
inscritos
`).join('') : '
Nenhuma atividade cadastrada.
'}
`;
}
// ── FINANCEIRO ─────────────────────────────────────
let finTipoFilter = 'todos';
function setFinTipo(t, btn) { finTipoFilter = t; document.querySelectorAll('#panelFinLanc .fb').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); loadFinLancamentos(); }
function toggleFinLancForm() { const el = document.getElementById('finLancForm'); el.style.display = el.style.display==='none'?'block':'none'; }
function toggleFinContaForm() { const el = document.getElementById('finContaForm'); el.style.display = el.style.display==='none'?'block':'none'; }
async function loadFinDashboard() {
const ano = document.getElementById('finDashAno').value;
const d = await api({ action: 'fin_dashboard', ano });
if (d.error) { document.getElementById('finDashContent').innerHTML = '
Erro.
'; return; }
const fmtR = v => 'R$ ' + parseFloat(v||0).toLocaleString('pt-BR',{minimumFractionDigits:2});
const maxVal = Math.max(...d.receitas_mes, ...d.despesas_mes, 1);
document.getElementById('finDashContent').innerHTML = `
Total Receitas
${fmtR(d.total_receitas)}
Total Despesas
${fmtR(d.total_despesas)}
Saldo
${fmtR(d.total_receitas-d.total_despesas)}
Pendente
${fmtR(d.pendente)}
Receitas x Despesas por Mes
${MESES_CURTOS.map((m,i) => `
`).join('')}
Receitas
Despesas
${d.mensalidades ? `
Mensalidades ${ano}
Total: ${fmtR(d.mensalidades.total)}
Pago: ${fmtR(d.mensalidades.pago)}
Pendente: ${fmtR(d.mensalidades.pendente)}
` : ''}`;
}
async function loadFinLancamentos() {
const mes = document.getElementById('finLancMes').value || new Date().toISOString().slice(0,7);
if (!document.getElementById('finLancMes').value) monthNavSet('finLancMes', mes);
const tipo = finTipoFilter === 'todos' ? undefined : finTipoFilter;
const d = await api({ action: 'fin_lancamentos_list', mes, tipo });
const items = Array.isArray(d) ? d : [];
const el = document.getElementById('finLancList');
if (!items.length) { el.innerHTML = '
Nenhum lancamento neste mes.
'; return; }
// Carregar contas para o select do form
const contas = await api({ action: 'fin_plano_contas_list' });
const sel = document.getElementById('flConta');
if (sel && sel.options.length <= 1) {
sel.innerHTML = '
-- conta -- ' + (Array.isArray(contas)?contas:[]).map(c => `
${esc(c.codigo||'')} ${esc(c.nome)} `).join('');
}
const fmtR = v => 'R$ ' + parseFloat(v||0).toLocaleString('pt-BR',{minimumFractionDigits:2});
el.innerHTML = items.map(l => `
${esc(l.descricao)}
${l.fin_plano_contas?.nome||'—'} · ${l.fornecedor||'—'} · ${new Date(l.data_lancamento+'T12:00:00').toLocaleDateString('pt-BR')}
${l.tipo==='receita'?'+':'−'} ${fmtR(l.valor)}
${l.status}
${l.status==='pendente'?`
Pagar `:''}
`).join('');
}
async function salvarFinLanc() {
const d = await api({ action:'fin_lancamento_save', tipo:document.getElementById('flTipo').value, conta_id:document.getElementById('flConta').value||null, descricao:document.getElementById('flDesc').value.trim(), valor:document.getElementById('flValor').value, data_lancamento:document.getElementById('flData').value, data_vencimento:document.getElementById('flVenc').value||null, fornecedor:document.getElementById('flForn').value.trim()||null });
if (d.error) { showToast(d.error,'error'); return; }
showToast('Lancamento salvo!','success'); toggleFinLancForm(); loadFinLancamentos();
}
async function pagarFinLanc(id) { await api({ action:'fin_lancamento_pagar', id }); showToast('Pago!','success'); loadFinLancamentos(); }
async function loadFinMensalidades() {
const mes = document.getElementById('finMensMes').value || new Date().toISOString().slice(0,7);
if (!document.getElementById('finMensMes').value) monthNavSet('finMensMes', mes);
const d = await api({ action:'fin_mensalidades_list', mes });
const items = Array.isArray(d) ? d : [];
const el = document.getElementById('finMensList');
const fmtR = v => 'R$ ' + parseFloat(v||0).toLocaleString('pt-BR',{minimumFractionDigits:2});
if (!items.length) { el.innerHTML = '
Nenhuma mensalidade gerada para este mes. Clique "Gerar Mensalidades".
'; return; }
el.innerHTML = `
Familia
Crianca
Turno
Valor
Status
${items.map(m => `
${esc(m.familia_nome||m.familia_email)}
${esc(m.crianca_nome||'—')}
${esc((m.turno||'').replace(/_/g,' '))}
${fmtR(m.valor_total)}
${m.status}
${m.status!=='pago'?`Pagar `:''}
`).join('')}
`;
}
async function gerarMensalidades() {
const mes = document.getElementById('finMensMes').value || new Date().toISOString().slice(0,7);
if (!confirm('Gerar mensalidades para ' + mes + '?')) return;
const d = await api({ action:'fin_gerar_mensalidades', mes });
showToast(d.geradas + ' mensalidade(s) gerada(s)!','success'); loadFinMensalidades();
}
async function pagarMensalidade(id) { await api({ action:'fin_mensalidade_pagar', id }); showToast('Pago!','success'); loadFinMensalidades(); }
async function loadFinContas() {
const d = await api({ action:'fin_plano_contas_list' });
const items = Array.isArray(d) ? d : [];
document.getElementById('finContasList').innerHTML = items.length ? items.map(c => `
${esc(c.codigo||'')}
${esc(c.nome)}
${c.tipo}
`).join('') : '
Nenhuma conta cadastrada.
';
}
async function salvarFinConta() {
const d = await api({ action:'fin_plano_contas_save', codigo:document.getElementById('fcCodigo').value.trim(), nome:document.getElementById('fcNome').value.trim(), tipo:document.getElementById('fcTipo').value });
if (d.error) { showToast(d.error,'error'); return; }
showToast('Conta salva!','success'); toggleFinContaForm(); loadFinContas();
document.getElementById('fcCodigo').value=''; document.getElementById('fcNome').value='';
}
// ── DRE ────────────────────────────────────────────
async function loadFinDre() {
const ano = document.getElementById('dreAno').value;
const d = await api({ action:'fin_dre', ano });
if (d.error) { document.getElementById('dreContent').innerHTML = '
Erro.
'; return; }
const fmtR = v => 'R$ ' + parseFloat(v||0).toLocaleString('pt-BR',{minimumFractionDigits:2});
const MS = MESES_CURTOS;
const renderSection = (title, items, color) => {
if (!items.length) return '';
return `
${title} ` +
items.filter(c=>c.total>0).map(c => `
${esc(c.codigo||'')} ${esc(c.nome)}
${c.meses.map(v => `${v>0?fmtR(v):'—'} `).join('')}
${fmtR(c.total)}
`).join('');
};
document.getElementById('dreContent').innerHTML = `
Conta
${MS.map(m => `${m} `).join('')}
TOTAL
${renderSection('RECEITAS', d.receitas, '#2d7a3a')}
TOTAL RECEITAS
${d.total_receitas_mes.map(v => `${fmtR(v)} `).join('')}
${fmtR(d.total_receitas_mes.reduce((s,v)=>s+v,0))}
${renderSection('DESPESAS', d.despesas, '#e53e3e')}
TOTAL DESPESAS
${d.total_despesas_mes.map(v => `${fmtR(v)} `).join('')}
${fmtR(d.total_despesas_mes.reduce((s,v)=>s+v,0))}
RESULTADO
${d.resultado_mes.map(v => `${fmtR(v)} `).join('')}
${fmtR(d.resultado_mes.reduce((s,v)=>s+v,0))}
`;
}
// ── BALANCO PATRIMONIAL ────────────────────────────
async function loadFinBalanco() {
const mes = document.getElementById('balancoMes').value || new Date().toISOString().slice(0,7);
if (!document.getElementById('balancoMes').value) monthNavSet('balancoMes', mes);
const d = await api({ action:'fin_balanco', mes });
if (d.error) { document.getElementById('balancoContent').innerHTML = '
Erro.
'; return; }
const fmtR = v => 'R$ ' + parseFloat(v||0).toLocaleString('pt-BR',{minimumFractionDigits:2});
const renderGroup = (title, items, color) => `
${title}
${items.map(c => `
${esc(c.codigo||'')} ${esc(c.nome)}
${fmtR(c.saldo)}
`).join('')}
`;
document.getElementById('balancoContent').innerHTML = `
${renderGroup('ATIVO', d.ativos, '#1a6bb5')}
TOTAL ATIVO ${fmtR(d.total_ativo)}
${renderGroup('PASSIVO', d.passivos, '#e53e3e')}
${renderGroup('PATRIMONIO LIQUIDO', d.patrimonio, '#6b3fa0')}
Resultado do Periodo
${fmtR(d.lucro_periodo)}
PASSIVO + PL ${fmtR(d.total_passivo + d.total_pl)}
Para editar saldos patrimoniais, use o Plano de Contas.
`;
}
// ── CONCILIACAO BANCARIA ──────────────────────────
async function loadFinConciliacao() {
const mes = document.getElementById('concMes').value || new Date().toISOString().slice(0,7);
if (!document.getElementById('concMes').value) monthNavSet('concMes', mes);
const d = await api({ action:'fin_extrato_list', mes });
const items = Array.isArray(d) ? d : [];
const el = document.getElementById('concContent');
if (!items.length) { el.innerHTML = '
Nenhum extrato importado para este mes. Importe um arquivo Excel ou OFX.
'; return; }
const fmtR = v => 'R$ ' + parseFloat(v||0).toLocaleString('pt-BR',{minimumFractionDigits:2});
const conciliados = items.filter(i => i.conciliado).length;
el.innerHTML = `
${conciliados}/${items.length} conciliados
Data
Descricao
Valor
Status
Lancamento
${items.map(it => `
${new Date(it.data_transacao+'T12:00:00').toLocaleDateString('pt-BR')}
${esc(it.descricao)}
${it.tipo==='credito'?'+':'−'} ${fmtR(it.valor)}
${it.conciliado?'Conciliado':'Pendente'}
${it.fin_lancamentos?.descricao||'—'}
`).join('')}
`;
}
async function importarExtrato(input) {
const file = input.files[0]; if (!file) return; input.value = '';
try {
const buf = await file.arrayBuffer();
const wb = XLSX.read(new Uint8Array(buf), { type:'array' });
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header:1, defval:'' });
const itens = rows.slice(1).filter(r => r[0] && r[2]).map(r => ({
data: typeof r[0]==='number' ? new Date((r[0]-25569)*86400*1000).toISOString().slice(0,10) : String(r[0]).trim(),
descricao: String(r[1]||'').trim(), valor: parseFloat(String(r[2]).replace(',','.')) || 0, saldo: r[3] ? parseFloat(String(r[3]).replace(',','.')) : null,
}));
if (!itens.length) { showToast('Nenhum item no extrato.','warning'); return; }
const d = await api({ action:'fin_extrato_importar', itens });
showToast(d.importados + ' transacoes importadas!','success');
loadFinConciliacao();
} catch(e) { showToast('Erro ao ler extrato.','error'); }
}
async function autoConciliar() {
const mes = document.getElementById('concMes').value || new Date().toISOString().slice(0,7);
const d = await api({ action:'fin_extrato_auto_conciliar', mes });
showToast(d.conciliados + ' transacoes conciliadas automaticamente!','success');
loadFinConciliacao();
}
// ── BOLETOS INTER ──────────────────────────────────
function toggleEmitirBoleto() { const el=document.getElementById('emitirBoletoForm'); el.style.display=el.style.display==='none'?'block':'none'; }
async function emitirBoleto() {
const cpf = document.getElementById('bolCpf').value.trim();
const nome = document.getElementById('bolNome').value.trim();
const valor = document.getElementById('bolValor').value;
const venc = document.getElementById('bolVenc').value;
const desc = document.getElementById('bolDesc').value.trim();
const errEl = document.getElementById('bolErr');
errEl.classList.remove('show');
if (!cpf || !valor || !venc) { errEl.textContent='CPF, valor e vencimento obrigatorios.'; errEl.classList.add('show'); return; }
const btn = document.getElementById('bolBtn');
btn.disabled=true; btn.textContent='Emitindo...';
const d = await api({ action:'fin_emitir_boleto', cpf_pagador:cpf, nome_pagador:nome, valor, vencimento:venc, descricao:desc||'Mensalidade Maple Bear' });
btn.disabled=false; btn.textContent='Emitir Boleto no Inter';
if (d.error) { errEl.textContent=d.error; errEl.classList.add('show'); return; }
showToast('Boleto emitido! N° ' + (d.nosso_numero||'—'),'success',5000);
toggleEmitirBoleto();
loadFinBoletos();
}
async function loadFinBoletos() {
const d = await api({ action:'fin_boletos_emitidos_list' });
const items = Array.isArray(d) ? d : [];
const el = document.getElementById('finBoletosContent');
const fmtR = v => 'R$ '+parseFloat(v||0).toLocaleString('pt-BR',{minimumFractionDigits:2});
if (!items.length) { el.innerHTML='
Nenhum boleto emitido.
'; return; }
el.innerHTML = items.map(b => `
${esc(b.familia_nome||b.cpf_pagador)}
${esc(b.descricao||'—')} · Venc: ${new Date(b.vencimento+'T12:00:00').toLocaleDateString('pt-BR')}
${b.linha_digitavel?`
${esc(b.linha_digitavel)}
`:''}
${b.pix_copia_cola?`
📋 Copiar PIX
`:''}
${fmtR(b.valor)}
${b.status}
`).join('');
}
// ── NOTAS FISCAIS ─────────────────────────────────
function toggleEmitirNf() { const el=document.getElementById('emitirNfForm'); el.style.display=el.style.display==='none'?'block':'none'; }
async function emitirNf() {
const cpf = document.getElementById('nfCpf').value.trim();
const nome = document.getElementById('nfNome').value.trim();
const valor = document.getElementById('nfValor').value;
const desc = document.getElementById('nfDesc').value.trim();
if (!valor || !desc) { showToast('Valor e descricao obrigatorios.','warning'); return; }
const btn = document.getElementById('nfBtn');
btn.disabled=true; btn.textContent='Registrando...';
const d = await api({ action:'fin_nf_emitir', cpf_cnpj_tomador:cpf, familia_nome:nome, valor, descricao_servico:desc });
btn.disabled=false; btn.textContent='Registrar NF';
if (d.error) { showToast(d.error,'error'); return; }
showToast('NF registrada!','success');
toggleEmitirNf();
loadFinNfs();
}
async function loadFinNfs() {
const d = await api({ action:'fin_nf_list' });
const items = Array.isArray(d) ? d : [];
const el = document.getElementById('finNfsContent');
const fmtR = v => 'R$ '+parseFloat(v||0).toLocaleString('pt-BR',{minimumFractionDigits:2});
if (!items.length) { el.innerHTML='
Nenhuma NF registrada.
'; return; }
el.innerHTML = items.map(n => `
${esc(n.familia_nome||n.cpf_cnpj_tomador||'—')}
${esc(n.descricao_servico||'—')} · ${new Date(n.criado_em).toLocaleDateString('pt-BR')}
${n.numero_nf?`
NF n° ${esc(n.numero_nf)}
`:''}
${fmtR(n.valor)}
${n.status}
${n.status==='pendente'?`
Marcar Emitida `:''}
`).join('');
}
async function marcarNfEmitida(id) {
const num = prompt('Numero da NF:');
if (!num) return;
const cod = prompt('Codigo de verificacao (opcional):');
await api({ action:'fin_nf_marcar_emitida', id, numero_nf:num, codigo_verificacao:cod||'' });
showToast('NF marcada como emitida!','success');
loadFinNfs();
}
// ── CRM ────────────────────────────────────────────
let crmEstagios = [], crmLeads = [];
function toggleCrmLeadForm() { const el=document.getElementById('crmLeadForm'); el.style.display=el.style.display==='none'?'block':'none'; }
function toggleCrmTemplateForm() { const el=document.getElementById('crmTemplateForm'); el.style.display=el.style.display==='none'?'block':'none'; }
async function loadCrmKanban() {
const [estData, leadData] = await Promise.all([api({action:'crm_estagios_list'}), api({action:'crm_leads_list'})]);
crmEstagios = Array.isArray(estData) ? estData : [];
crmLeads = Array.isArray(leadData) ? leadData : [];
renderKanban();
}
function renderKanban() {
const board = document.getElementById('crmKanbanBoard');
if (!crmEstagios.length) { board.innerHTML = '
Nenhum estagio.
'; return; }
board.innerHTML = crmEstagios.map(est => {
const leads = crmLeads.filter(l => l.estagio_id === est.id);
return `
${leads.length ? leads.map(l => `
${esc(l.nome_responsavel)}
${l.nome_crianca ? '👶 ' + esc(l.nome_crianca) + (l.data_nascimento ? ' (' + crmIdadeTexto(l.data_nascimento) + ')' : '') : ''}
${l.serie_interesse ? ' 🎒 ' + esc(l.serie_interesse) : ''}
${l.telefone ? ' 📱 ' + esc(l.telefone) : ''}
${l.origem ? ' 📍 ' + esc(l.origem) : ''}
Detalhes
${l.telefone ? `WhatsApp ` : ''}
`).join('') : '
Arraste leads aqui
'}
`;
}).join('');
}
async function dropLead(e, estagioId) {
e.preventDefault();
const leadId = e.dataTransfer.getData('text');
if (!leadId) return;
await api({ action:'crm_lead_mover', id: leadId, estagio_id: estagioId });
const lead = crmLeads.find(l => l.id === leadId);
if (lead) lead.estagio_id = estagioId;
renderKanban();
}
async function salvarCrmLead() {
const estagios = crmEstagios.length ? crmEstagios : await api({action:'crm_estagios_list'});
const primeiroEstagio = (Array.isArray(estagios)?estagios:crmEstagios)[0]?.id;
const d = await api({ action:'crm_lead_save',
nome_responsavel: document.getElementById('crmNome').value.trim(),
telefone: document.getElementById('crmTel').value.trim(),
email: document.getElementById('crmEmail').value.trim(),
nome_crianca: document.getElementById('crmCrianca').value.trim(),
data_nascimento: document.getElementById('crmNasc').value || null,
serie_interesse: document.getElementById('crmSerie').value.trim(),
origem: document.getElementById('crmOrigem').value,
observacoes: document.getElementById('crmObs').value.trim(),
estagio_id: primeiroEstagio,
});
if (d.error) { showToast(d.error,'error'); return; }
showToast('Lead salvo!','success'); toggleCrmLeadForm();
['crmNome','crmTel','crmEmail','crmCrianca','crmNasc','crmSerie','crmObs'].forEach(id=>document.getElementById(id).value='');
document.getElementById('crmSerieAuto').textContent = '';
loadCrmKanban();
}
function abrirWhatsApp(tel, nome) {
const num = tel.replace(/\D/g,'');
const fullNum = num.length <= 11 ? '55' + num : num;
window.open('https://wa.me/' + fullNum, '_blank');
}
async function abrirLeadDetalhe(id) {
let lead = crmLeads.find(l => l.id === id);
if (!lead) {
const all = await api({ action:'crm_leads_list' });
crmLeads = Array.isArray(all) ? all : [];
lead = crmLeads.find(l => l.id === id);
}
if (!lead) return;
const intData = await api({ action:'crm_interacoes_list', lead_id: id });
const interacoes = Array.isArray(intData) ? intData : [];
const tipoIcons = { ligacao:'📞', email:'📧', whatsapp:'💬', visita:'🏫', reuniao:'📅', nota:'📝', outro:'📌' };
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:600;display:flex;align-items:center;justify-content:center;padding:20px;overflow-y:auto;';
overlay.onclick = e => { if(e.target===overlay) overlay.remove(); };
overlay.innerHTML = `
${esc(lead.nome_responsavel)}
x
${lead.telefone?`
📱 ${esc(lead.telefone)}
`:''}
${lead.email?`
📧 ${esc(lead.email)}
`:''}
${lead.nome_crianca?`
👶 ${esc(lead.nome_crianca)} ${lead.idade_crianca?'('+esc(lead.idade_crianca)+')':''}
`:''}
${lead.serie_interesse?`
🎒 ${esc(lead.serie_interesse)}
`:''}
${lead.origem?`
📍 ${esc(lead.origem)}
`:''}
${lead.valor_mensalidade?`
💰 R$ ${lead.valor_mensalidade}
`:''}
${lead.observacoes?`
${esc(lead.observacoes)}
`:''}
${lead.telefone?`💬 WhatsApp `:''}
📅 Agendar Reuniao
📝 Registrar Reserva/Matricula
Historico (${interacoes.length})
${interacoes.map(i => `
${tipoIcons[i.tipo]||'📌'} ${esc(i.descricao)}
${new Date(i.criado_em).toLocaleString('pt-BR')} · ${esc(i.criado_por||'')}
`).join('') || '
Nenhuma interacao.
'}
`;
document.body.appendChild(overlay);
}
async function salvarCrmInteracao(leadId) {
const tipo = document.getElementById('crmIntTipo').value;
const desc = document.getElementById('crmIntDesc').value.trim();
if (!desc) return;
await api({ action:'crm_interacao_save', lead_id: leadId, tipo, descricao: desc });
document.querySelector('div[style*=fixed]')?.remove();
abrirLeadDetalhe(leadId);
}
function agendarReuniaoCrm(leadId, nome) {
const titulo = prompt('Titulo da reuniao:', 'Visita ' + nome);
if (!titulo) return;
const data = prompt('Data e hora (AAAA-MM-DD HH:MM):', new Date().toISOString().slice(0,16).replace('T',' '));
if (!data) return;
api({ action:'crm_reuniao_save', lead_id: leadId, titulo, data_hora: data.replace(' ','T') + ':00', local: 'Maple Bear Caxias do Sul' }).then(d => {
if (d.error) showToast(d.error,'error');
else {
showToast('Reuniao agendada!','success');
// Abrir Google Calendar
const dtStart = data.replace(/[-: ]/g,'').slice(0,15) + '00';
const dtEnd = new Date(new Date(data.replace(' ','T')).getTime()+30*60000).toISOString().replace(/[-:]/g,'').slice(0,15) + '00';
window.open(`https://calendar.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(titulo)}&dates=${dtStart}/${dtEnd}&location=${encodeURIComponent('Maple Bear Caxias do Sul')}&details=${encodeURIComponent('Lead: '+nome)}`, '_blank');
}
});
}
function crmIdadeTexto(dataNasc) {
if (!dataNasc) return '';
const nasc = new Date(dataNasc + 'T12:00:00');
const hoje = new Date();
let anos = hoje.getFullYear() - nasc.getFullYear();
let meses = hoje.getMonth() - nasc.getMonth();
if (meses < 0) { anos--; meses += 12; }
if (hoje.getDate() < nasc.getDate()) meses--;
if (meses < 0) { anos--; meses += 12; }
if (anos < 1) return meses + ' meses';
if (anos === 1 && meses > 0) return '1 ano e ' + meses + 'm';
if (anos === 1) return '1 ano';
return anos + ' anos' + (meses > 0 ? ' e ' + meses + 'm' : '');
}
async function crmAutoSerie() {
const nasc = document.getElementById('crmNasc').value;
if (!nasc) return;
const d = await api({ action: 'crm_calcular_serie', data_nascimento: nasc });
const el = document.getElementById('crmSerieAuto');
if (d.serie) {
document.getElementById('crmSerie').value = d.serie;
el.textContent = '(auto: ' + d.serie + ')';
} else {
el.textContent = '(idade: ' + Math.floor(d.idade_meses / 12) + ' anos)';
}
}
async function loadCrmConfigSeries() {
const anoFiltro = document.getElementById('csAnoFiltro')?.value || new Date().getFullYear();
const d = await api({ action: 'config_series_idade_list', ano: parseInt(anoFiltro) });
const items = Array.isArray(d) ? d : [];
document.getElementById('crmConfigSeriesContent').innerHTML = items.length ? `
Serie
Idade Min (meses)
Idade Max (meses)
Data Corte
Faixa Etaria
${items.map(c => {
const minAnos = Math.floor(c.idade_min_meses / 12);
const maxAnos = Math.floor(c.idade_max_meses / 12);
return `
${esc(c.serie)}
${c.idade_min_meses}
${c.idade_max_meses}
${c.data_corte_ref}/${c.ano_ref}
${minAnos} a ${maxAnos} anos
X
`;
}).join('')}
` : '
Nenhuma configuracao.
';
}
async function salvarCrmConfigSerie() {
const d = await api({ action: 'config_series_idade_save',
serie: document.getElementById('csNome').value.trim(),
idade_min_meses: document.getElementById('csMin').value,
idade_max_meses: document.getElementById('csMax').value,
data_corte_ref: document.getElementById('csCorte').value || '03-31',
ano_ref: document.getElementById('csAno').value || document.getElementById('csAnoFiltro').value || new Date().getFullYear(),
});
if (d.error) { showToast(d.error, 'error'); return; }
showToast('Salvo!', 'success');
document.getElementById('csNome').value = '';
loadCrmConfigSeries();
}
async function deleteCrmConfigSerie(id) {
if (!confirm('Excluir?')) return;
await api({ action: 'config_series_idade_delete', id });
loadCrmConfigSeries();
}
async function replicarSeriesAno() {
const anoOrigem = document.getElementById('csAnoFiltro').value;
const anoDestino = document.getElementById('csAnoDestino').value;
if (anoOrigem === anoDestino) { showToast('Ano de origem e destino iguais','error'); return; }
if (!confirm('Replicar todas as series de ' + anoOrigem + ' para ' + anoDestino + '?\n\nSeries ja existentes no ano destino serao mantidas.')) return;
const d = await api({ action: 'config_series_idade_atualizar_ano', ano_origem: parseInt(anoOrigem), ano_destino: parseInt(anoDestino) });
if (d.error) { showToast(d.error,'error'); return; }
showToast(d.total + ' series replicadas para ' + anoDestino + '!', 'success');
document.getElementById('csAnoFiltro').value = anoDestino;
loadCrmConfigSeries();
}
async function loadCrmLeads() {
const d = await api({ action:'crm_leads_list' });
const items = Array.isArray(d) ? d : [];
document.getElementById('crmLeadsList').innerHTML = items.length ? items.map(l => `
${esc(l.nome_responsavel)}
${esc(l.nome_crianca||'')} · ${esc(l.telefone||'')} · ${esc(l.crm_estagios?.nome||'?')} · ${esc(l.origem||'')}
Detalhes
`).join('') : '
Nenhum lead.
';
}
async function loadCrmTemplates() {
const d = await api({ action:'crm_templates_list' });
const items = Array.isArray(d) ? d : [];
const catLabels = { boas_vindas:'Boas-vindas', follow_up:'Follow-up', visita:'Visita', pos_visita:'Pos-Visita', proposta:'Proposta', matricula:'Matricula', geral:'Geral' };
document.getElementById('crmTemplatesList').innerHTML = items.length ? items.map(t => `
${esc(t.nome)}
${catLabels[t.categoria]||t.categoria}
📋 Copiar
${esc(t.conteudo)}
`).join('') : '
Nenhum template.
';
}
function copiarTemplate(id) {
const tpl = document.querySelector(`#crmTemplatesList div`);
// Buscar do API de novo para ter o conteudo
api({ action:'crm_templates_list' }).then(d => {
const items = Array.isArray(d) ? d : [];
const t = items.find(x => x.id === id);
if (t) { navigator.clipboard.writeText(t.conteudo); showToast('Template copiado!','success'); }
});
}
async function salvarCrmTemplate() {
const d = await api({ action:'crm_template_save', nome: document.getElementById('tplNome').value.trim(), categoria: document.getElementById('tplCat').value, conteudo: document.getElementById('tplConteudo').value.trim() });
if (d.error) { showToast(d.error,'error'); return; }
showToast('Template salvo!','success'); toggleCrmTemplateForm();
document.getElementById('tplNome').value=''; document.getElementById('tplConteudo').value='';
loadCrmTemplates();
}
// ── CRM VAGAS ────────────────────────────
async function loadCrmVagas() {
const ano = parseInt(document.getElementById('vagasAno').value);
document.getElementById('vagAno').value = ano;
const d = await api({ action:'crm_vagas_list', ano });
const items = Array.isArray(d) ? d : [];
const el = document.getElementById('crmVagasContent');
if (!items.length) { el.innerHTML = '
Nenhuma serie configurada para ' + ano + '.
'; return; }
el.innerHTML = `
Serie
Turmas
Vagas/T
Total
Reservas
Matriculados
Disponiveis
${items.map(v => {
const ocupadas = (v.reservas||0) + (v.matriculados||0);
const disp = v.vagas_total - ocupadas;
const pct = v.vagas_total > 0 ? Math.min(100, (ocupadas / v.vagas_total) * 100) : 0;
const cor = pct >= 90 ? '#e53e3e' : pct >= 70 ? '#f6a623' : '#48bb78';
return `
${esc(v.serie)}
${v.qtd_turmas}
${v.vagas_por_turma}
${v.vagas_total}
${v.reservas||0}
${v.matriculados||0}
${disp}
${Math.round(pct)}% ocupado
`;
}).join('')}
`;
}
async function salvarCrmVaga() {
const serie = document.getElementById('vagSerie').value.trim();
const qtd_turmas = parseInt(document.getElementById('vagQtd').value) || 1;
const vagas_por_turma = parseInt(document.getElementById('vagCap').value) || 18;
const ano = parseInt(document.getElementById('vagAno').value) || 2026;
if (!serie) { showToast('Informe a serie','error'); return; }
const d = await api({ action:'crm_vagas_save', serie, ano, qtd_turmas, vagas_por_turma });
if (d.error) { showToast(d.error,'error'); return; }
showToast('Vaga salva!','success');
document.getElementById('vagSerie').value = '';
document.getElementById('vagasAno').value = String(ano);
loadCrmVagas();
}
// ── CRM MATRICULAS ────────────────────────────
async function loadCrmMatriculas() {
const ano = parseInt(document.getElementById('matrAno').value);
const [matrData, vagasData] = await Promise.all([
api({ action:'crm_matriculas_list', ano }),
api({ action:'crm_vagas_list', ano })
]);
const items = Array.isArray(matrData) ? matrData : [];
const vagas = Array.isArray(vagasData) ? vagasData : [];
const el = document.getElementById('crmMatriculasContent');
const statusLabels = { reserva:'🟡 Reserva', matriculado:'🟢 Matriculado', cancelado:'🔴 Cancelado' };
const statusDot = { reserva:'#f6a623', matriculado:'#2d7a3a', cancelado:'#e53e3e' };
const letras = 'ABCDEFGHIJ';
// Agrupar por serie
const porSerie = {};
for (const v of vagas) { if (!porSerie[v.serie]) porSerie[v.serie] = { vagas: v, items: [] }; }
for (const m of items) {
if (!porSerie[m.serie]) porSerie[m.serie] = { vagas: null, items: [] };
porSerie[m.serie].items.push(m);
}
const series = Object.keys(porSerie).sort((a,b) => {
const oa = porSerie[a].vagas?.ordem ?? 99;
const ob = porSerie[b].vagas?.ordem ?? 99;
return oa - ob;
});
if (!series.length) { el.innerHTML = '
Nenhuma serie configurada para ' + ano + '.
'; return; }
// Gerar cards — se qtd_turmas > 1, separar em turmas A, B, C...
let html = '';
for (const serie of series) {
const g = porSerie[serie];
const v = g.vagas;
const qtdTurmas = v ? v.qtd_turmas : 1;
const vagasPorTurma = v ? v.vagas_por_turma : '?';
if (qtdTurmas <= 1) {
// Turma unica — renderiza como antes
html += renderTurmaCard(serie, serie, g.items, v ? v.vagas_total : null, qtdTurmas, statusLabels, statusDot, letras, ano);
} else {
// Multiplas turmas — separar A, B, C...
for (let t = 0; t < qtdTurmas; t++) {
const letra = letras[t] || String(t+1);
const nomeTurma = serie + ' ' + letra;
const turmaItems = g.items.filter(m => (m.turma || 'A') === letra);
html += renderTurmaCard(serie, nomeTurma, turmaItems, vagasPorTurma, 1, statusLabels, statusDot, letras, ano);
}
// Alunos sem turma definida (turma diferente das esperadas)
const letrasValidas = Array.from({length: qtdTurmas}, (_, i) => letras[i] || String(i+1));
const semTurma = g.items.filter(m => m.turma && !letrasValidas.includes(m.turma));
if (semTurma.length) {
html += renderTurmaCard(serie, serie + ' (sem turma)', semTurma, null, 1, statusLabels, statusDot, letras, ano);
}
}
}
el.innerHTML = html;
}
function renderTurmaCard(serieBase, nomeTurma, items, vagasTotal, qtdTurmas, statusLabels, statusDot, letras, ano) {
const ativos = items.filter(m => m.status !== 'cancelado');
const inativos = items.filter(m => m.status === 'cancelado');
const reservas = items.filter(m => m.status === 'reserva').length;
const matriculados = items.filter(m => m.status === 'matriculado').length;
const cancelados = inativos.length;
const ocupados = reservas + matriculados;
const disp = vagasTotal != null ? vagasTotal - ocupados : '?';
const pct = vagasTotal && vagasTotal > 0 ? Math.min(100, (ocupados / vagasTotal) * 100) : 0;
const cor = pct >= 90 ? '#e53e3e' : pct >= 70 ? '#f6a623' : '#48bb78';
return `
${esc(nomeTurma)}
${matriculados} matriculado${matriculados!==1?'s':''}
·
${reservas} reserva${reservas!==1?'s':''}
${cancelados?`· ${cancelados} cancelado${cancelados!==1?'s':''} `:''}
${vagasTotal!=null?`
${disp}/${vagasTotal} vagas
`:''}
${ativos.length ? ativos.map(m => {
const nascStr = m.data_nascimento ? new Date(m.data_nascimento+'T12:00:00').toLocaleDateString('pt-BR') : '';
const dataStr = m.data_matricula || m.data_reserva || '';
return `
${esc(m.nome_crianca)}
Resp: ${esc(m.nome_responsavel)}${nascStr?' · Nasc: '+nascStr:''}${m.telefone?' · '+esc(m.telefone):''}${m.email?' · '+esc(m.email):''}
${statusLabels[m.status]||m.status}
${dataStr?new Date(dataStr+'T12:00:00').toLocaleDateString('pt-BR'):''}
${Array.from({length:10},(_, i)=>letras[i]).filter(l=>l).map(l=>`${l} `).join('')}
${m.status==='reserva'?`Matricular `:''}
Cancelar
`;
}).join('') : '
Nenhuma crianca nesta turma.
'}
${inativos.length ? `
Cancelados (${inativos.length})
${inativos.map(m => `
${esc(m.nome_crianca)}
${esc(m.nome_responsavel)}
`).join('')}
` : ''}
`;
}
async function mudarTurmaMatricula(id, novaTurma) {
const d = await api({ action:'crm_matricula_atualizar_turma', id, turma: novaTurma });
if (d.error) { showToast(d.error,'error'); return; }
showToast('Turma alterada para ' + novaTurma,'success');
loadCrmMatriculas();
}
async function atualizarMatricula(id, novoStatus) {
const labels = { matriculado:'matricular', cancelado:'cancelar' };
if (!confirm('Deseja ' + (labels[novoStatus]||novoStatus) + ' este registro?')) return;
const d = await api({ action:'crm_matricula_atualizar_status', id, status: novoStatus });
if (d.error) { showToast(d.error,'error'); return; }
showToast('Status atualizado!','success');
loadCrmMatriculas();
// Refresh vagas if that panel was loaded
if (document.getElementById('panelCrmVagas').style.display !== 'none') loadCrmVagas();
}
async function criarMatriculaDoLead(leadId) {
let lead = crmLeads.find(l => l.id === leadId);
if (!lead) {
const all = await api({ action:'crm_leads_list' });
lead = (Array.isArray(all) ? all : []).find(l => l.id === leadId);
}
if (!lead) { showToast('Lead nao encontrado','error'); return; }
const ano = prompt('Ano da matricula:', '2026');
if (!ano) return;
// Buscar vagas para saber quantas turmas existem
const vagasD = await api({ action:'crm_vagas_list', ano: parseInt(ano) });
const vagasList = Array.isArray(vagasD) ? vagasD : [];
const serieVaga = vagasList.find(v => v.serie === lead.serie_interesse);
let turma = 'A';
if (serieVaga && serieVaga.qtd_turmas > 1) {
const letras = 'ABCDEFGHIJ'.slice(0, serieVaga.qtd_turmas).split('');
turma = prompt('Turma (' + letras.join(', ') + '):', 'A');
if (!turma) return;
turma = turma.toUpperCase();
}
const status = confirm('Ja tem contrato assinado?\n\nOK = Matriculado\nCancelar = Reserva') ? 'matriculado' : 'reserva';
const d = await api({
action: 'crm_matricula_criar',
lead_id: leadId,
nome_responsavel: lead.nome_responsavel,
nome_crianca: lead.nome_crianca || '',
serie: lead.serie_interesse || '',
ano: parseInt(ano),
status, turma,
email: lead.email || '',
telefone: lead.telefone || '',
data_nascimento: lead.data_nascimento || ''
});
if (d.error) { showToast(d.error,'error'); return; }
showToast(status === 'matriculado' ? 'Matricula registrada!' : 'Reserva registrada!', 'success');
// Move lead to 'fechado' stage if exists
const estagios = await api({ action:'crm_estagios_list' });
const stgFechado = Array.isArray(estagios) ? estagios.find(e => e.nome.toLowerCase().includes('fechado') || e.nome.toLowerCase().includes('ganho') || e.nome.toLowerCase().includes('won')) : null;
if (stgFechado) {
await api({ action:'crm_lead_save', id: leadId, estagio_id: stgFechado.id });
}
// Close modal and reload
document.querySelector('div[style*=fixed]')?.remove();
loadCrmKanban();
}
// ── IMPRESSOES (gerente) ────────────────────────────
let impGerenteFilter = 'pendentes';
const IMP_STATUS_G = { pendente:'⏳ Pendente', aprovado:'✅ Aprovado', rejeitado:'❌ Rejeitado', impresso:'🖨️ Impresso', entregue:'📦 Entregue' };
const IMP_PAPEL_G = { sulfite:'Sulfite A4', desenho:'Papel Desenho', cartolina:'Cartolina', foto:'Fotográfico', adesivo:'Adesivo' };
function setImpFilter(f, btn) {
impGerenteFilter = f;
document.querySelectorAll('#panelImpressoes .fb').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
loadImpressoesGerente();
}
async function loadImpressoesGerente() {
const el = document.getElementById('impGerenteContent');
if (impGerenteFilter === 'orcamento') {
const d = await api({ action: 'impressoes_orcamento_list', mes: new Date().toISOString().slice(0,7) });
const turmas = Array.isArray(d) ? d : [];
el.innerHTML = turmas.length ? `
` : '
Nenhuma turma.
';
return;
}
const d = impGerenteFilter === 'pendentes'
? await api({ action: 'impressoes_pendentes' })
: await api({ action: 'impressoes_todas', mes: new Date().toISOString().slice(0,7) });
const items = Array.isArray(d) ? d : [];
if (!items.length) { el.innerHTML = '
Nenhuma solicitacao.
'; return; }
el.innerHTML = items.map(it => {
const dt = new Date(it.criado_em).toLocaleDateString('pt-BR', { day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit' });
const acoes = [];
if (it.status === 'pendente') {
acoes.push(`
Aprovar `);
acoes.push(`
Rejeitar `);
}
if (it.status === 'aprovado') acoes.push(`
Impresso `);
if (it.status === 'impresso') acoes.push(`
Entregue `);
return `
${esc(it.professora_nome||'?')} · ${esc(it.turma_nome||'?')} · ${dt}
${it.para_dia ? ' · Para ' + new Date(it.para_dia+'T12:00:00').toLocaleDateString('pt-BR',{day:'2-digit',month:'short'}) : ''}
${it.observacao ? `
${esc(it.observacao)}
` : ''}
${acoes.join('')}
`;
}).join('');
}
async function impAprovar(id) {
await api({ action: 'impressao_aprovar', id });
showToast('Impressao aprovada!', 'success');
loadImpressoesGerente();
}
async function impRejeitar(id) {
const nota = prompt('Motivo da rejeicao (opcional):');
await api({ action: 'impressao_rejeitar', id, nota: nota || '' });
loadImpressoesGerente();
}
async function impMarcarImpresso(id) {
await api({ action: 'impressao_marcar_impresso', id });
showToast('Marcado como impresso!', 'success');
loadImpressoesGerente();
}
async function impMarcarEntregue(id) {
await api({ action: 'impressao_marcar_entregue', id });
showToast('Marcado como entregue!', 'success');
loadImpressoesGerente();
}
async function setImpLimite(turmaId, limite) {
const mes = new Date().toISOString().slice(0,7);
await api({ action: 'impressoes_orcamento_set', turma_id: turmaId, mes, limite });
showToast('Limite atualizado!', 'success');
}
// ── EMERGENCIA ──────────────────────────────────────
const EMERG_TIPOS = { incendio:'🔥 Incêndio', intruso:'🚷 Intruso', emergencia_medica:'🚑 Emergência Médica', evacuacao:'🏃 Evacuação', outro:'⚠️ Outro' };
async function acionarEmergencia(tipo) {
const msg = prompt('Mensagem adicional (opcional):');
if (!confirm('ATENÇÃO: Isso vai notificar TODA a equipe da escola imediatamente. Confirma o alerta de ' + (EMERG_TIPOS[tipo]||tipo) + '?')) return;
const d = await api({ action: 'emergencia_acionar', tipo, mensagem: msg || null });
if (d.error) { showToast(d.error, 'error'); return; }
showToast('ALERTA DE EMERGÊNCIA ACIONADO! Toda a equipe foi notificada.', 'error', 8000);
loadEmergencia();
}
async function loadEmergencia() {
const [ativos, hist] = await Promise.all([
api({ action: 'emergencia_ativos' }),
api({ action: 'emergencia_historico' }),
]);
// Ativos
const ativosArr = Array.isArray(ativos) ? ativos : [];
const el = document.getElementById('emergenciaAtivos');
if (ativosArr.length) {
el.innerHTML = '
⚠️ ALERTAS ATIVOS
' +
ativosArr.map(a => `
${EMERG_TIPOS[a.tipo]||a.tipo}
${a.mensagem ? `
${esc(a.mensagem)}
` : ''}
Por ${esc(a.acionado_por)} · ${new Date(a.criado_em).toLocaleString('pt-BR')}
✅ Resolver
`).join('');
} else {
el.innerHTML = '';
}
// Historico
const histArr = Array.isArray(hist) ? hist : [];
document.getElementById('emergenciaHistorico').innerHTML = histArr.length ? histArr.map(a => {
const dt = new Date(a.criado_em).toLocaleString('pt-BR');
return `
${EMERG_TIPOS[a.tipo]?.charAt(0)||'⚠️'}
${EMERG_TIPOS[a.tipo]||a.tipo}
${dt} · ${esc(a.acionado_por)}${a.resolvido_por ? ' · Resolvido por '+esc(a.resolvido_por) : ''}
${a.ativo?'Ativo':'Resolvido'}
`;
}).join('') : '
Nenhum alerta registrado.
';
}
async function resolverEmergencia(id) {
if (!confirm('Confirma que a emergência foi resolvida?')) return;
await api({ action: 'emergencia_resolver', id });
showToast('Emergência resolvida.', 'success');
loadEmergencia();
}
// ── ACHADOS E PERDIDOS ───────────────────────────────
async function loadAchadosGerente() {
const d = await callDiplomas({ action: 'achados_lista_equipe' });
const items = d.data || [];
document.getElementById('achadosCount').textContent = items.length;
const el = document.getElementById('achadosGerenteLista');
if (!items.length) { el.innerHTML = '
Nenhum item registrado.
'; return; }
el.innerHTML = items.map(it => {
const dt = new Date(it.criado_em).toLocaleDateString('pt-BR', { day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit' });
const isPublico = it.status === 'publico' || new Date(it.publicar_em) <= new Date();
return `
${it.foto_url ? `
` : '
📦
'}
${esc(it.descricao)}
${it.local_encontrado ? `
📍 ${esc(it.local_encontrado)}
` : ''}
Por ${esc(it.postado_por_nome||'—')} · ${dt}
${isPublico?'Visível para pais':'Apenas equipe — publica em '+new Date(it.publicar_em).toLocaleString('pt-BR',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'})}
${!isPublico ? `Publicar Agora ` : ''}
Devolvido
Excluir
`;
}).join('');
}
async function achadoPublicar(id) {
await callDiplomas({ action: 'achados_publicar', id });
showToast('Item publicado para os pais!', 'success');
loadAchadosGerente();
}
async function achadoDevolver(id) {
const para = prompt('Nome de quem retirou o item (opcional):');
await callDiplomas({ action: 'achados_devolver', id, devolvido_para: para || '' });
showToast('Item marcado como devolvido!', 'success');
loadAchadosGerente();
}
async function achadoExcluir(id) {
if (!confirm('Excluir este item?')) return;
await callDiplomas({ action: 'achados_excluir', id });
loadAchadosGerente();
}
// ── WEBAUTHN / BIOMETRIA ─────────────────────────────
(async function() {
if (!await WebAuthnClient.isPlatformAvailable()) return;
const bioEmail = localStorage.getItem('mb_bio_email');
if (bioEmail) {
document.getElementById('bioLoginBtn').style.display = 'block';
document.getElementById('loginEmail').value = bioEmail;
}
})();
function showBioRegisterButton() {
if (typeof WebAuthnClient === 'undefined') return;
WebAuthnClient.isPlatformAvailable().then(ok => {
console.log('[WebAuthn] isPlatformAvailable:', ok);
if (!ok) return;
if (!localStorage.getItem('mb_bio_email') && !sessionStorage.getItem('bio_banner_shown')) { document.getElementById('bioRegBanner').style.display = 'block'; sessionStorage.setItem('bio_banner_shown', '1'); }
});
}
async function registerBiometric() {
const btn = document.getElementById('bioRegBtn');
btn.disabled = true; btn.textContent = 'Aguarde…';
try {
const ch = await api({ action:'webauthn_register_challenge', rp_id: location.hostname });
if (ch.error) { showToast(ch.error,'error'); return; }
const credential = await WebAuthnClient.register(ch);
const r = await api({ action:'webauthn_register_verify', credential, rp_id: location.hostname });
if (r.error) { showToast(r.error,'error'); return; }
localStorage.setItem('mb_bio_email', currentGerente.email);
btn.style.display = 'none';
showToast('Biometria ativada!','success');
} catch(e) { if (e.name !== 'NotAllowedError') showToast('Erro.','error'); }
finally { btn.disabled = false; btn.textContent = '🔐 Ativar biometria'; }
}
async function doBioLogin() {
const btn = document.getElementById('bioLoginBtn');
const errEl = document.getElementById('loginError');
btn.disabled = true; btn.textContent = 'Verificando…'; errEl.style.display = 'none';
try {
const email = localStorage.getItem('mb_bio_email');
const ch = await api({ action:'webauthn_login_challenge', email, rp_id: location.hostname });
if (ch.error) { errEl.textContent = ch.error; errEl.style.display = 'block'; return; }
const assertion = await WebAuthnClient.authenticate(ch);
const d = await api({ action:'webauthn_login_verify', credential: assertion, rp_id: location.hostname });
if (d.error) { errEl.textContent = d.error; errEl.style.display = 'block'; return; }
localStorage.setItem('mb_token', d.token);
localStorage.setItem('mb_nome', d.nome);
localStorage.setItem('mb_email', d.email);
showApp(d);
} catch(e) { if (e.name !== 'NotAllowedError') { errEl.textContent = 'Erro.'; errEl.style.display = 'block'; } }
finally { btn.disabled = false; btn.textContent = '🔐 Entrar com biometria'; }
}
checkSession();
// ═══════════════════════════════════════════════════════════
// COMUNICAÇÃO — CHAT
// ═══════════════════════════════════════════════════════════
const COMUNICACAO = SUPABASE_URL + '/functions/v1/comunicacao';
async function apiCom(body) {
const token = getToken();
const r = await fetch(COMUNICACAO, { method:'POST', headers:{'Content-Type':'application/json','apikey':ANON,'Authorization':'Bearer '+ANON}, body: JSON.stringify({...body, _token: token}) });
return r.json();
}
async function loadChatConversas() {
const el = document.getElementById('chatConversasContent');
el.innerHTML = '
Carregando...';
const d = await apiCom({ action:'chat_conversas_list', usuario_tipo:'gerente', usuario_id: currentGerente?.email });
const convs = Array.isArray(d) ? d : [];
if (!convs.length) { el.innerHTML = '
Nenhuma conversa. Envie um aviso para começar.
'; return; }
el.innerHTML = '
Conversa Última Mensagem Não Lidas ' +
convs.map(c => `
${esc(c.titulo || c.tipo)} ${c.chat_participantes?.map(p=>p.usuario_nome).join(', ') || ''}
${c.ultima_mensagem ? esc(c.ultima_mensagem.conteudo?.substring(0,60)) : '—'}
${c.nao_lidas > 0 ? ''+c.nao_lidas+' ' : '—'}
`).join('') + '
';
}
async function novoChatAviso() {
const conteudo = prompt('Mensagem do aviso:');
if (!conteudo) return;
const d = await apiCom({ action:'chat_avisos_turma', conteudo, titulo:'Aviso Geral' });
if (d.error) return showToast(d.error,'error');
showToast('Aviso enviado!');
loadChatConversas();
}
// ═══════════════════════════════════════════════════════════
// PESQUISAS / ENQUETES
// ═══════════════════════════════════════════════════════════
const DIPLOMAS_FN = SUPABASE_URL + '/functions/v1/diplomas';
async function apiDiplomas(body) {
const token = getToken();
const r = await fetch(DIPLOMAS_FN, { method:'POST', headers:{'Content-Type':'application/json','apikey':ANON,'Authorization':'Bearer '+ANON}, body: JSON.stringify({...body, _token: token}) });
return r.json();
}
async function loadPesquisasPanel() {
const d = await apiDiplomas({ action:'pesquisa_list' });
const pesquisas = Array.isArray(d) ? d : [];
document.getElementById('pesquisasCount').textContent = pesquisas.length;
const el = document.getElementById('pesquisasContent');
if (!pesquisas.length) { el.innerHTML = '
Nenhuma pesquisa criada.
'; return; }
el.innerHTML = '
Título Tipo Status Limite Ações ' +
pesquisas.map(p => `
${esc(p.titulo)}
${esc(p.tipo)}
${p.ativo ? 'Ativa ' : 'Inativa '}
${p.data_limite ? new Date(p.data_limite+'T12:00:00').toLocaleDateString('pt-BR') : '—'}
📊
🗑️
`).join('') + '
';
}
function showPesquisaForm() { document.getElementById('pesquisaFormWrap').style.display='block'; document.getElementById('pesquisaId').value=''; document.getElementById('pesquisaTitulo').value=''; document.getElementById('pesquisaDesc').value=''; }
function cancelarPesquisa() { document.getElementById('pesquisaFormWrap').style.display='none'; }
async function salvarPesquisa() {
const titulo = document.getElementById('pesquisaTitulo').value.trim();
if (!titulo) return showToast('Título obrigatório','error');
const d = await apiDiplomas({ action:'pesquisa_create', titulo, tipo: document.getElementById('pesquisaTipo').value, descricao: document.getElementById('pesquisaDesc').value, data_limite: document.getElementById('pesquisaLimite').value || null });
if (d.error) return showToast(d.error,'error');
showToast('Pesquisa criada!');
cancelarPesquisa();
loadPesquisasPanel();
}
async function deletarPesquisa(id) { if (!confirm('Excluir?')) return; await apiDiplomas({ action:'pesquisa_delete', id }); loadPesquisasPanel(); }
async function verResultadosPesquisa(id) {
const d = await apiDiplomas({ action:'pesquisa_resultados', pesquisa_id: id });
alert('Total de respondentes: ' + (d.total_respondentes || 0) + '\nPerguntas: ' + (d.perguntas?.length || 0) + '\nRespostas: ' + (d.respostas?.length || 0));
}
// ═══════════════════════════════════════════════════════════
// MATRÍCULA ONLINE
// ═══════════════════════════════════════════════════════════
async function loadMatriculaForms() {
const d = await api({ action:'matricula_formulario_get', ano: new Date().getFullYear(), tipo:'nova' });
const el = document.getElementById('matriculaFormContent');
if (!d || !d.campos?.length) { el.innerHTML = '
Nenhum formulário configurado para este ano.
'; return; }
el.innerHTML = `
`;
}
async function loadMatriculaStatus() {
const status = document.getElementById('matStatusFiltro')?.value || '';
const d = await api({ action:'matricula_status_list', ano: new Date().getFullYear(), status: status || undefined });
const mats = Array.isArray(d) ? d : [];
document.getElementById('matStatusCount').textContent = mats.length;
const el = document.getElementById('matStatusContent');
if (!mats.length) { el.innerHTML = '
Nenhuma matrícula encontrada.
'; return; }
const statusColors = { reserva:'background:rgba(212,131,10,.1);color:#c27800;', matriculado:'background:rgba(45,122,58,.1);color:var(--green);', cancelado:'background:rgba(200,16,46,.1);color:var(--red);' };
el.innerHTML = '
Criança Série Responsável Status Docs ' +
mats.map(m => `
${esc(m.nome_crianca)}
${esc(m.serie || '—')}
${esc(m.nome_responsavel || '—')}${esc(m.email || '')}
${esc(m.status)}
${(m.matricula_documentos || []).length > 0 ? '📎 '+m.matricula_documentos.length : '—'}
`).join('') + '
';
}
// ═══════════════════════════════════════════════════════════
// ACADÊMICO — NOTAS / BOLETIM
// ═══════════════════════════════════════════════════════════
const ACADEMICO = SUPABASE_URL + '/functions/v1/academico';
async function apiAcademico(body) {
const token = getToken();
const r = await fetch(ACADEMICO, {
method:'POST',
headers:{'Content-Type':'application/json','apikey':ANON,'Authorization':'Bearer '+ANON},
body: JSON.stringify({...body, _token: token})
});
return r.json();
}
// ── Config Notas ──
async function loadNotasConfig() {
const d = await apiAcademico({ action:'notas_config_get' });
if (d.error) return;
document.getElementById('ncTipo').value = d.tipo_avaliacao || 'numerico';
document.getElementById('ncFormula').value = d.formula_media || 'aritmetica';
document.getElementById('ncMedia').value = d.media_aprovacao || 7;
document.getElementById('ncPeriodos').value = d.periodos_tipo || 'bimestral';
document.getElementById('ncRecup').value = d.permite_recuperacao !== false ? 'true' : 'false';
document.getElementById('ncPesoRecup').value = (d.peso_recuperacao || 0.4) * 100;
}
async function salvarNotasConfig() {
const d = await apiAcademico({
action:'notas_config_update',
tipo_avaliacao: document.getElementById('ncTipo').value,
formula_media: document.getElementById('ncFormula').value,
media_aprovacao: parseFloat(document.getElementById('ncMedia').value) || 7,
periodos_tipo: document.getElementById('ncPeriodos').value,
permite_recuperacao: document.getElementById('ncRecup').value === 'true',
peso_recuperacao: (parseFloat(document.getElementById('ncPesoRecup').value) || 40) / 100
});
if (d.error) return showToast(d.error, 'error');
showToast('Configuração salva!', 'success');
}
// ── Períodos ──
async function loadNotasPeriodos() {
const ano = new Date().getFullYear();
const d = await apiAcademico({ action:'notas_periodos_list', ano });
const periodos = Array.isArray(d) ? d : [];
document.getElementById('periodoCount').textContent = periodos.length;
const tbody = document.getElementById('periodosBody');
if (!periodos.length) { tbody.innerHTML = '
Nenhum período cadastrado '; return; }
tbody.innerHTML = periodos.map(p => `
${esc(p.nome)}
${p.numero}
${p.ano}
${p.data_inicio ? new Date(p.data_inicio+'T12:00:00').toLocaleDateString('pt-BR') : '—'}
${p.data_fim ? new Date(p.data_fim+'T12:00:00').toLocaleDateString('pt-BR') : '—'}
✏️
🗑️
`).join('');
}
let _periodos = [];
function showPeriodoForm() {
document.getElementById('periodoEditId').value = '';
document.getElementById('periodoNome').value = '';
document.getElementById('periodoNum').value = '';
document.getElementById('periodoAno').value = new Date().getFullYear();
document.getElementById('periodoInicio').value = '';
document.getElementById('periodoFim').value = '';
document.getElementById('periodoFormWrap').style.display = 'block';
}
function cancelarPeriodo() { document.getElementById('periodoFormWrap').style.display = 'none'; }
async function editarPeriodo(id) {
const d = await apiAcademico({ action:'notas_periodos_list', ano: new Date().getFullYear() });
const p = (Array.isArray(d) ? d : []).find(x => x.id === id);
if (!p) return;
document.getElementById('periodoEditId').value = p.id;
document.getElementById('periodoNome').value = p.nome;
document.getElementById('periodoNum').value = p.numero;
document.getElementById('periodoAno').value = p.ano;
document.getElementById('periodoInicio').value = p.data_inicio || '';
document.getElementById('periodoFim').value = p.data_fim || '';
document.getElementById('periodoFormWrap').style.display = 'block';
}
async function salvarPeriodo() {
const id = document.getElementById('periodoEditId').value;
const data = {
nome: document.getElementById('periodoNome').value.trim(),
numero: parseInt(document.getElementById('periodoNum').value) || 1,
ano: parseInt(document.getElementById('periodoAno').value) || new Date().getFullYear(),
data_inicio: document.getElementById('periodoInicio').value || null,
data_fim: document.getElementById('periodoFim').value || null
};
if (!data.nome) return showToast('Nome obrigatório', 'error');
const d = id
? await apiAcademico({ action:'notas_periodos_update', id, ...data })
: await apiAcademico({ action:'notas_periodos_create', ...data });
if (d.error) return showToast(d.error, 'error');
showToast(id ? 'Período atualizado!' : 'Período criado!');
cancelarPeriodo();
loadNotasPeriodos();
}
async function deletarPeriodo(id) {
if (!confirm('Excluir este período?')) return;
const d = await apiAcademico({ action:'notas_periodos_delete', id });
if (d.error) return showToast(d.error, 'error');
showToast('Período excluído');
loadNotasPeriodos();
}
// ── Disciplinas ──
async function loadNotasDisciplinas() {
const [dRes, sRes, pRes] = await Promise.all([
apiAcademico({ action:'notas_disciplinas_list' }),
api({ action:'series_list' }),
api({ action:'professoras_list' })
]);
const discs = Array.isArray(dRes) ? dRes : [];
const series = Array.isArray(sRes) ? sRes : (sRes.data || []);
const profs = Array.isArray(pRes) ? pRes : (pRes.data || []);
document.getElementById('discCount').textContent = discs.length;
// Populate selects
document.getElementById('discSerie').innerHTML = '
— ' + series.map(s => `
${esc(s.nome)} `).join('');
document.getElementById('discProf').innerHTML = '
— ' + profs.map(p => `
${esc(p.nome)} `).join('');
const tbody = document.getElementById('discBody');
if (!discs.length) { tbody.innerHTML = '
Nenhuma disciplina '; return; }
tbody.innerHTML = discs.map(d => `
${esc(d.nome)}
${d.series?.nome || '—'}
${d.professoras?.nome || '—'}
${d.carga_horaria || 0}h
✏️
🗑️
`).join('');
}
let _discs = [];
function showDiscForm() {
document.getElementById('discId').value = '';
document.getElementById('discNome').value = '';
document.getElementById('discSerie').value = '';
document.getElementById('discProf').value = '';
document.getElementById('discCarga').value = 4;
document.getElementById('discFormWrap').style.display = 'block';
}
function cancelarDisc() { document.getElementById('discFormWrap').style.display = 'none'; }
async function editarDisc(id) {
const d = await apiAcademico({ action:'notas_disciplinas_list' });
const disc = (Array.isArray(d) ? d : []).find(x => x.id === id);
if (!disc) return;
document.getElementById('discId').value = disc.id;
document.getElementById('discNome').value = disc.nome;
document.getElementById('discSerie').value = disc.serie_id || '';
document.getElementById('discProf').value = disc.professor_id || '';
document.getElementById('discCarga').value = disc.carga_horaria || 0;
document.getElementById('discFormWrap').style.display = 'block';
}
async function salvarDisc() {
const id = document.getElementById('discId').value;
const data = {
nome: document.getElementById('discNome').value.trim(),
serie_id: document.getElementById('discSerie').value || null,
professor_id: document.getElementById('discProf').value || null,
carga_horaria: parseInt(document.getElementById('discCarga').value) || 0
};
if (!data.nome) return showToast('Nome obrigatório', 'error');
const d = id
? await apiAcademico({ action:'notas_disciplinas_update', id, ...data })
: await apiAcademico({ action:'notas_disciplinas_create', ...data });
if (d.error) return showToast(d.error, 'error');
showToast(id ? 'Disciplina atualizada!' : 'Disciplina criada!');
cancelarDisc();
loadNotasDisciplinas();
}
async function deletarDisc(id) {
if (!confirm('Desativar esta disciplina?')) return;
const d = await apiAcademico({ action:'notas_disciplinas_delete', id });
if (d.error) return showToast(d.error, 'error');
showToast('Disciplina desativada');
loadNotasDisciplinas();
}
// ── Visão Geral de Notas ──
async function initNotasVisao() {
const [sRes, pRes, dRes] = await Promise.all([
api({ action:'series_list' }),
apiAcademico({ action:'notas_periodos_list', ano: new Date().getFullYear() }),
apiAcademico({ action:'notas_disciplinas_list' })
]);
const series = Array.isArray(sRes) ? sRes : (sRes.data || []);
const periodos = Array.isArray(pRes) ? pRes : [];
const discs = Array.isArray(dRes) ? dRes : [];
document.getElementById('nvSerie').innerHTML = '
Série... ' + series.map(s => `
${esc(s.nome)} `).join('');
document.getElementById('nvPeriodo').innerHTML = '
Período... ' + periodos.map(p => `
${esc(p.nome)} `).join('');
document.getElementById('nvDisc').innerHTML = '
Disciplina... ' + discs.map(d => `
${esc(d.nome)} `).join('');
}
async function loadNotasVisao() {
const serieId = document.getElementById('nvSerie').value;
const periodoId = document.getElementById('nvPeriodo').value;
const discId = document.getElementById('nvDisc').value;
if (!serieId || !periodoId || !discId) return;
const content = document.getElementById('notasVisaoContent');
content.innerHTML = '
Carregando...';
// Buscar avaliações desta disciplina/período
const [avRes, alRes] = await Promise.all([
apiAcademico({ action:'notas_avaliacoes_list', disciplina_id: discId, periodo_id: periodoId }),
apiAcademico({ action:'notas_alunos_serie', serie_id: serieId })
]);
const avaliacoes = Array.isArray(avRes) ? avRes : [];
const alunos = Array.isArray(alRes) ? alRes : [];
if (!avaliacoes.length) { content.innerHTML = '
Nenhuma avaliação cadastrada para esta disciplina/período.
'; return; }
if (!alunos.length) { content.innerHTML = '
Nenhum aluno encontrado nesta série.
'; return; }
// Buscar notas de todas as avaliações
const notasMap = {};
for (const av of avaliacoes) {
const nRes = await apiAcademico({ action:'notas_lancamentos_list', avaliacao_id: av.id });
for (const n of (Array.isArray(nRes) ? nRes : [])) {
if (!notasMap[n.aluno_email]) notasMap[n.aluno_email] = {};
notasMap[n.aluno_email][av.id] = n;
}
}
// Renderizar tabela
let html = '
Aluno ';
for (const av of avaliacoes) html += `${esc(av.nome)} (${av.peso}x) `;
html += 'Média ';
for (const al of alunos) {
html += `${esc(al.nome_aluno)} `;
let soma = 0, count = 0;
for (const av of avaliacoes) {
const nota = notasMap[al.email]?.[av.id];
const val = nota?.valor ?? nota?.conceito ?? '—';
html += `${val} `;
if (nota?.valor !== null && nota?.valor !== undefined && av.tipo !== 'recuperacao') { soma += nota.valor; count++; }
}
const media = count > 0 ? (soma / count).toFixed(1) : '—';
const mediaClass = count > 0 && parseFloat(media) >= 7 ? 'color:var(--green);font-weight:700;' : count > 0 ? 'color:var(--red);font-weight:700;' : '';
html += `${media} `;
}
html += '
';
content.innerHTML = html;
}
// ── OFFLINE DETECTION ──────────────────────────────────
function showOfflineBanner(show) {
let b = document.getElementById('offlineBanner');
if (!b) {
b = document.createElement('div');
b.id = 'offlineBanner';
b.style.cssText = 'position:fixed;bottom:0;left:0;right:0;background:#d4830a;color:#fff;text-align:center;padding:10px;font-size:13px;font-weight:600;z-index:9999;font-family:"DM Sans",sans-serif;transition:transform .3s;';
b.textContent = '📡 Sem conexão com a internet';
document.body.appendChild(b);
}
b.style.transform = show ? 'translateY(0)' : 'translateY(100%)';
}
window.addEventListener('offline', () => showOfflineBanner(true));
window.addEventListener('online', () => showOfflineBanner(false));
if (!navigator.onLine) showOfflineBanner(true);
// ── CLEANUP ON UNLOAD ─────────────────────────────────
window.addEventListener('beforeunload', () => { if (pollInterval) clearInterval(pollInterval); });
// ═══ ONBOARDING WIZARD ═══
async function checkOnboarding() {
// Show wizard if escola has no series, no professoras, no familias
try {
const [sRes, pRes, fRes] = await Promise.all([
api({ action: 'series_list' }),
api({ action: 'professoras_list' }),
api({ action: 'familias_list' })
]);
const series = Array.isArray(sRes) ? sRes : (sRes.data || []);
const profs = Array.isArray(pRes) ? pRes : (pRes.data || []);
const fams = Array.isArray(fRes) ? fRes : (fRes.data || []);
// If all empty or very few, show wizard
if (series.length <= 1 && profs.length === 0 && fams.length === 0) {
document.getElementById('onboardingWizard').style.display = 'flex';
}
// Mark completed steps
if (series.length > 0) markOnboardingStep(2);
if (profs.length > 0) markOnboardingStep(3);
if (fams.length > 0) markOnboardingStep(4);
} catch(e) {}
}
function markOnboardingStep(n) {
const el = document.getElementById('obCheck' + n);
if (el) { el.textContent = '✓'; el.style.background = 'var(--green)'; el.style.color = '#fff'; el.style.borderColor = 'var(--green)'; }
}
function goOnboardingStep(n) {
closeOnboarding();
if (n === 1) showPanel('logo');
if (n === 2) showPanel('series');
if (n === 3) showPanel('equipe');
if (n === 4) showPanel('familias');
if (n === 5) showPanel('chatConversas');
}
function closeOnboarding() { document.getElementById('onboardingWizard').style.display = 'none'; localStorage.setItem('mb_onboarding_done', '1'); }
// ═══ MORNING BRIEFING ═══
function loadMorningBriefing() {
const now = new Date();
const hour = now.getHours();
const greeting = hour < 12 ? 'Bom dia' : hour < 18 ? 'Boa tarde' : 'Boa noite';
const firstName = (currentGerente?.nome || 'Gestor').split(/\s/)[0];
document.getElementById('briefingGreeting').textContent = greeting + ', ' + firstName + '!';
document.getElementById('briefingDate').textContent = now.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}
// Chamar após login
const _origShowApp = showApp;
showApp = function(user) {
_origShowApp(user);
loadMorningBriefing();
if (!localStorage.getItem('mb_onboarding_done')) checkOnboarding();
};
// ═══ BUSCA RÁPIDA (Ctrl+K) ═══
const searchData = [
{icon:'📊',text:'Dashboard Analytics',panel:'analytics'},
{icon:'📝',text:'Notas / Visão Geral',panel:'notasVisao'},
{icon:'✅',text:'Frequência',panel:'frequencia'},
{icon:'📖',text:'Diário de Classe',panel:'diarioClasse'},
{icon:'🏆',text:'Diplomas',panel:'diplomas'},
{icon:'📈',text:'Growth Plan',panel:'pdi'},
{icon:'📦',text:'Almoxarifado Dashboard',panel:'almDash'},
{icon:'⏳',text:'Almoxarifado Pendentes',panel:'almPend'},
{icon:'🗃️',text:'Insumos',panel:'almInsumos'},
{icon:'💰',text:'Dashboard Financeiro',panel:'finDash'},
{icon:'📝',text:'Lançamentos',panel:'finLanc'},
{icon:'🧾',text:'Mensalidades',panel:'finMens'},
{icon:'📈',text:'DRE',panel:'finDre'},
{icon:'⚖️',text:'Balanço Patrimonial',panel:'finBalanco'},
{icon:'🏦',text:'Boletos',panel:'finBoletos'},
{icon:'📋',text:'CRM Pipeline',panel:'crmKanban'},
{icon:'👥',text:'Leads',panel:'crmLeads'},
{icon:'💬',text:'Templates WhatsApp',panel:'crmTemplates'},
{icon:'🎯',text:'Vagas',panel:'crmVagas'},
{icon:'📝',text:'Matrículas',panel:'crmMatriculas'},
{icon:'💬',text:'Chat / Conversas',panel:'chatConversas'},
{icon:'📊',text:'Pesquisas',panel:'pesquisas'},
{icon:'📅',text:'Calendário',panel:'calendario'},
{icon:'👥',text:'Equipe',panel:'equipe'},
{icon:'👨👩👧',text:'Famílias',panel:'familias'},
{icon:'🔧',text:'Manutenção',panel:'manutencao'},
{icon:'🔍',text:'Achados & Perdidos',panel:'achados'},
{icon:'🖨️',text:'Impressões',panel:'impressoes'},
{icon:'🚨',text:'Emergência',panel:'emergencia'},
{icon:'⚙️',text:'Config Notas',panel:'notasConfig'},
{icon:'📚',text:'Disciplinas',panel:'notasDisciplinas'},
{icon:'📅',text:'Períodos Letivos',panel:'notasPeriodos'},
{icon:'📋',text:'Formulários Matrícula',panel:'matriculaForm'},
{icon:'📊',text:'Status Matrículas',panel:'matriculaStatus'},
];
// Create search overlay
const searchOverlay = document.createElement('div');
searchOverlay.className = 'search-overlay';
searchOverlay.innerHTML = '
';
searchOverlay.addEventListener('click', (e) => { if (e.target === searchOverlay) closeSearch(); });
document.body.appendChild(searchOverlay);
function openSearch() {
searchOverlay.classList.add('show');
const input = document.getElementById('searchInput');
input.value = '';
input.focus();
renderSearchResults('');
}
function closeSearch() { searchOverlay.classList.remove('show'); }
function renderSearchResults(query) {
const q = query.toLowerCase();
const filtered = q ? searchData.filter(s => s.text.toLowerCase().includes(q)) : searchData.slice(0, 8);
document.getElementById('searchResults').innerHTML = filtered.map((s, i) =>
'
'
).join('') || '
Nenhum resultado
';
}
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); openSearch(); }
if (e.key === 'Escape' && searchOverlay.classList.contains('show')) closeSearch();
if (searchOverlay.classList.contains('show') && e.key === 'Enter') {
const sel = document.querySelector('.search-result.selected');
if (sel) sel.click();
}
});
document.getElementById('searchInput')?.addEventListener('input', (e) => renderSearchResults(e.target.value));
// ═══ AUTO-SAVE ═══
let autoSaveIndicator = document.createElement('div');
autoSaveIndicator.className = 'autosave-indicator';
autoSaveIndicator.textContent = '✓ Salvo';
document.body.appendChild(autoSaveIndicator);
function showAutoSaved() {
autoSaveIndicator.classList.add('show');
setTimeout(() => autoSaveIndicator.classList.remove('show'), 1500);
}
// Auto-save on input blur for form fields
document.addEventListener('change', (e) => {
if (e.target.matches('input, select, textarea') && e.target.closest('.form-card, .ff')) {
showAutoSaved();
}
});