Entrar

E-mail ou senha incorretos.

Dashboard Analytics

Atualizando…
Total
solicitações
Integral
todas freq.
Semi-Int.
todas freq.
Tarde
13:30h
Diária
R$ 150,00
📅 Crianças por Dia da Semana
Segunda0
Nenhuma criança
Terça0
Nenhuma criança
Quarta0
Nenhuma criança
Quinta0
Nenhuma criança
Sexta0
Nenhuma criança
Solicitações 0
CriançaResponsávelSérieTurnoDiasDataAçõ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
Segunda0
Nenhuma criança
Terça0
Nenhuma criança
Quarta0
Nenhuma criança
Quinta0
Nenhuma criança
Sexta0
Nenhuma criança
🎯 Ocupação por Atividade
Inscrições Recebidas 0
CriançaResponsávelSérieAtividades e TurmasData
Carregando…
Séries 0

➕ Nova Série

Menor = aparece primeiro.
📅 Inscrições por Dia da Semana
Segunda
Terça
Quarta
Quinta
Sexta
Inscrições Recebidas 0
CriançaResponsávelSérieAtividadesData
⏳ Carregando…
🎯 Atividades Cadastradas 0

🍽️ Configuração de Almoço

Defina o preço padrão do almoço. Cada turma pode ter um preço personalizado ao ser cadastrada.

➕ Nova Atividade

Cada turma tem um nome (ex: "Turma A — 14h") e seus próprios horários.
Pendentes
aguardando validação
Aprovados
diplomas validados
Rejeitados
diplomas negados
Total de Pontos
horas distribuídas
🏆 Ranking de Professoras
⏳ Carregando…
Diplomas Enviados 0
ProfessoraCursoHorasStatusEnviado emValidado porAções
⏳ Carregando…
🏥 Atestados das Professoras

Os atestados são gerenciados pelo Portal da Secretaria. Acesse secretaria.html para aprovar ou rejeitar atestados.

Orçamento por Turma
⏳ Carregando…
⏳ Carregando…
🏷️ Categorias
Catalogo de Insumos 0

➕ Novo Insumo

Como a professora pede (ex: unidade, folha)
Embalagem de compra
Ex: 100 (caixa c/ 100 un)
Turmas 0
Associe as professoras às suas turmas no painel de Professoras.

➕ Nova Turma

Associar Professoras a Turmas
💰 Orçamento Padrão
Aplica o mesmo valor para todas as turmas — no mês selecionado ou em todos os meses de um ano.
R$ ou
⏳ Carregando…
Calendario Escolar 2026
Feriados Datas Comemorativas Reunioes Atividades Planejamentos Report Cards Recesso

Bom dia!

Alunos
Presença
Receita Mês
Pendências
Dashboard Analytics
Carregando...
Dashboard Financeiro
Carregando...
Carregando...
Carregando...
Plano de Contas
Carregando...
Demonstração do Resultado do Exercício
Carregando...
Balanço Patrimonial
Carregando...
Carregando...
Boletos Emitidos
Carregando...
Notas Fiscais
Carregando...
Pipeline de Leads
Carregando...
Todos os Leads
Carregando...
Templates de Mensagem
Carregando...
Vagas Disponíveis
Carregando...

Adicionar/Editar Serie

Matriculas e Reservas
Carregando...
Configuracao de Series por Idade

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).

Carregando...

Replicar series para outro ano

Copia todas as series do ano selecionado acima para:

Adicionar Serie

Carregando...
Acione um alerta de emergência para notificar toda a equipe imediatamente.
Histórico
🔍 Achados & Perdidos 0
Carregando...
Pendentes
aguardando análise
Em Execução
em andamento
Concluídas
este mês
Críticas
urgência máxima
Chamados de Manutenção 0
UrgênciaDescriçãoLocalSolicitanteEquipeStatusDataAções
⏳ Carregando…
Total
professoras
Sem Plano
não iniciaram
Aguardando
para aprovar
Em Andamento
planos aprovados
Encerrados
ciclo concluído
Semáforo de PDIs
ProfessoraStatus PDISubmetido emAções
⏳ Carregando…

📅 Criar Ciclo de PDI

Ciclos Cadastrados
Reuniões Agendadas 0
DataHorárioResponsávelComAssunto
⏳ Carregando…
👩‍💼 Gestoras
🕐 Horários Disponíveis

+ Novo Horário

Equipe 0

➕ Novo Membro da Equipe

🔑 Alterar Minha Senha

Solicitações Pendentes 0
Carregando…
E-mails Autorizados

Apenas e-mails cadastrados aqui (ou na tabela familias) conseguem fazer login no formulário público.

Carregando…
Familias 0
Carregando...
Solicitacoes 0
Carregando...

Validar Diploma

PDI
Competências
Metas SMART
Check-ins

🔑 Definir Senha

Configuração de Notas
Disciplinas 0
DisciplinaSérieProfessor(a)Carga (h)Ações
Carregando...
Períodos Letivos 0
Período#AnoInícioFimAções
Carregando...
Visão Geral de Notas
Selecione série, período e disciplina para visualizar as notas.
Controle de Frequência
Selecione uma série para ver as chamadas.
Diário de Classe Digital
Selecione série e disciplina para visualizar o diário.
Conversas
Carregando conversas...
Pesquisas & Enquetes 0
Carregando...
Formulários de Matrícula
Carregando...
Status de Matrículas 0
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)}
`).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 => ``).join(''); document.getElementById('manutModalContent').innerHTML = `
Descrição
${esc(m.descricao)}
Local
${esc(m.localizacao)}
Urgência
${URGENCIA_LABEL[m.urgencia]}
${m.foto_url ? `
` : ''}
`; 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)}
`).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 ``; }).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':''})
${items.map(m => ``).join('')}
Urg. Descrição Local Status Data
${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(''); } 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 => `

${cargos[g.cargo]||g.cargo}

Geralmente o próprio e-mail da gestora.
`).join(''); // Popula select de gestoras para horários const sel = document.getElementById('gestoraSlotSelect'); sel.innerHTML = '' + list.map(g => ``).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)} `).join(''); document.getElementById('solicitacoesWrap').innerHTML = `
${rows}
Nome CPF E-mail Telefone Criança Data
`; } 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)} `).join(''); document.getElementById('autorizadosWrap').innerHTML = ` ${rows}
E-mail Nome Adicionado por Data
`; } 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 `
${lbl}
${esc(p.nome)}
${p.pontuacao}
pontos
`; }).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' ? ` ` : `${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 = '' + ciclos.map(c => ``).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 ? `` : ''; 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=>``).join('')}
`; }).join('')}
`; // Metas document.getElementById('pdiReviewMetas').innerHTML = metas.length ? metas.map(m => `
${esc(m.descricao)}
📏 ${esc(m.indicador)} · 📅 ${m.prazo} · ${m.progressao_pct||0}% concluído
${META_STATUS_G[m.status]||m.status}
`).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)}
` : `
` }
`).join('') : '
Nenhum check-in registrado.
'; // Workflow actions let actionsHtml = ''; if (status === 'aguardando_aprovacao') { actionsHtml = `
Aprovação
`; } else if (status === 'em_andamento') { actionsHtml = `
Encerrar PDI — Nota Final
${[1,2,3,4].map(n=>``).join('')} Selecione
`; } 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 ? `` : ''}
`; } 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 ``; } 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` : `` }
`; }).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 = '' + profs.map(p => ``).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}
`; }).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 => `
${it.nome}
${almNRFmtBRL(it.preco_unit)} / ${it.unidade}
` ).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}`}
${it.ativo ? `` : ''}
`; }).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)
`).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}
`).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 = ` ${turmas.map(t => ` `).join('')}
Turma Orçamento (R$)
${t.nome}
`; } 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 = '' + almCategorias.map(c => ``).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)}
` ).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 => ``).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 = ` ${lista.map(r => ` `).join('')}
Aluno Responsável CPF Série
${esc(r.nome_aluno)}
${esc(r.email||'')}
${esc(r.nome_responsavel||'—')} ${esc(r.cpf||'—')}
`; // 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 = ` ${lista.map(r => ` `).join('')}
Criança Responsável E-mail Turno
${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×'))}
`; } 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}
${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' : ''}
`; }).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) => `
${m}
`).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 = '' + (Array.isArray(contas)?contas:[]).map(c => ``).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'?``:''}
`).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 = `${items.map(m => ``).join('')}
Familia Crianca Turno Valor Status
${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'?``:''}
`; } 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 = `
${MS.map(m => ``).join('')} ${renderSection('RECEITAS', d.receitas, '#2d7a3a')} ${d.total_receitas_mes.map(v => ``).join('')} ${renderSection('DESPESAS', d.despesas, '#e53e3e')} ${d.total_despesas_mes.map(v => ``).join('')} ${d.resultado_mes.map(v => ``).join('')}
Conta${m}TOTAL
TOTAL RECEITAS${fmtR(v)}${fmtR(d.total_receitas_mes.reduce((s,v)=>s+v,0))}
TOTAL DESPESAS${fmtR(v)}${fmtR(d.total_despesas_mes.reduce((s,v)=>s+v,0))}
RESULTADO${fmtR(v)}${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
${items.map(it => ``).join('')}
Data Descricao Valor Status Lancamento
${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||'—'}
`; } 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'?``:''}
`).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 `
${esc(est.nome)} ${leads.length}
${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) : ''}
${l.telefone ? `` : ''}
`).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)}

${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?``:''}
Nova interacao:
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 ? ` ${items.map(c => { const minAnos = Math.floor(c.idade_min_meses / 12); const maxAnos = Math.floor(c.idade_max_meses / 12); return ``; }).join('')}
Serie Idade Min (meses) Idade Max (meses) Data Corte Faixa Etaria
${esc(c.serie)} ${c.idade_min_meses} ${c.idade_max_meses} ${c.data_corte_ref}/${c.ano_ref} ${minAnos} a ${maxAnos} anos
` : '
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||'')}
`).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}
${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 = `${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 ``; }).join('')}
Serie Turmas Vagas/T Total Reservas Matriculados Disponiveis
${esc(v.serie)} ${v.qtd_turmas} ${v.vagas_por_turma} ${v.vagas_total} ${v.reservas||0} ${v.matriculados||0} ${disp}
${Math.round(pct)}% ocupado
`; } 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'):''}
${m.status==='reserva'?``:''}
`; }).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 ? ` ${turmas.map(t => { const pct = t.limite > 0 ? Math.min(100, (t.usado / t.limite) * 100) : 0; return ``; }).join('')}
Turma Usado Limite
${esc(t.nome)} ${t.usado}
` : '
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(``); acoes.push(``); } if (it.status === 'aprovado') acoes.push(``); if (it.status === 'impresso') acoes.push(``); return `
📎 ${esc(it.arquivo_nome||'Arquivo')} ${it.copias} copias ${IMP_PAPEL_G[it.tipo_papel]||it.tipo_papel} ${IMP_STATUS_G[it.status]}
${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')}
`).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 ? `` : ''}
`; }).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 = '
' + convs.map(c => ``).join('') + '
ConversaÚltima MensagemNão Lidas
${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+'' : '—'}
'; } 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 = '
' + pesquisas.map(p => ``).join('') + '
TítuloTipoStatusLimiteAções
${esc(p.titulo)} ${esc(p.tipo)} ${p.ativo ? 'Ativa' : 'Inativa'} ${p.data_limite ? new Date(p.data_limite+'T12:00:00').toLocaleDateString('pt-BR') : '—'}
'; } 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 = `
${esc(d.titulo || 'Formulário')}

${d.campos.length} campos configurados

${d.campos.map(c => ``).join('')}
CampoTipoObrigatório
${esc(c.label || c.nome)}${esc(c.tipo)}${c.obrigatorio ? '✅' : '—'}
`; } 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 = '
' + mats.map(m => ``).join('') + '
CriançaSérieResponsávelStatusDocs
${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 : '—'}
'; } // ═══════════════════════════════════════════════════════════ // 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 => ``).join(''); document.getElementById('discProf').innerHTML = '' + profs.map(p => ``).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 = '' + series.map(s => ``).join(''); document.getElementById('nvPeriodo').innerHTML = '' + periodos.map(p => ``).join(''); document.getElementById('nvDisc').innerHTML = '' + discs.map(d => ``).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 = '
'; for (const av of avaliacoes) html += ``; html += ''; for (const al of alunos) { html += ``; let soma = 0, count = 0; for (const av of avaliacoes) { const nota = notasMap[al.email]?.[av.id]; const val = nota?.valor ?? nota?.conceito ?? '—'; html += ``; 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 += ``; } html += '
Aluno${esc(av.nome)} (${av.peso}x)Média
${esc(al.nome_aluno)}${val}${media}
'; 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) => '
' + s.icon + '
' + s.text + '
' ).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(); } });