Chácara Equilíbrio · painel administrativo

Entrar no painel

Acesso restrito ao administrador da Chácara Equilíbrio.

Esqueci a senha

" + "

Relatório de reservas

Chácara Equilíbrio · Gerado em " + esc(fmtDataHora(new Date().toISOString())) + "
" + "
Hospedagem: " + esc($("dashHospedagem").selectedOptions[0] ? $("dashHospedagem").selectedOptions[0].textContent : "Todas") + " · Período: " + esc(fmtData(dashCache.range.inicio) + " a " + fmtData(dashCache.range.fim) + " (" + dashCache.range.dias + " dias)") + "
" + "

Indicadores

" + kpisRows.map(function (r) { return ""; }).join("") + "
" + esc(r[0]) + "" + esc(r[1]) + "
" + "

Reservas por mês (" + esc(dashCache.anoSel) + ")

" + mesRows + "
MêsTotalConfirmadasPendentesEncerradasFaturamento
" + "

Comparativo anual

" + anoRows + "
AnoTotalConfirmadasPendentesEncerradasFaturamento
" + "

Histórico de reservas

" + linhas + "
DataClienteE-mailTelefoneHospedagemCheck-inCheck-outHóspedesValorReservaPagamento
" + "
Relatório gerado automaticamente pelo painel da Chácara Equilíbrio.
" + ""; var w = window.open("", "_blank"); if (!w) { setMsg($("dashMsg"), "err", "Permita pop-ups para exportar em PDF."); return; } w.document.write(html); w.document.close(); w.focus(); setTimeout(function () { w.print(); }, 400); } $("dashAplicar").addEventListener("click", carregarDashboard); $("dashAtualizar").addEventListener("click", carregarDashboard); $("dashCsv").addEventListener("click", exportarCsv); $("dashPdf").addEventListener("click", exportarPdf); $("dashAno").addEventListener("change", carregarDashboard); $("dashPeriodo").addEventListener("change", function () { $("dashPers").style.display = $("dashPeriodo").value === "personalizado" ? "inline-flex" : "none"; }); var __dashResizeT = null; window.addEventListener("resize", function () { clearTimeout(__dashResizeT); __dashResizeT = setTimeout(function () { if (dashCache && document.getElementById("tab-relatorios").classList.contains("active")) { renderDashMensal(); renderDashAnual(); } }, 250); }); /* ---------- Reservas ---------- */ var reservasCache = []; var reservaSelecionada = null; function carregarReservas() { var f = $("resFiltroStatus").value; var h = $("resFiltroHosp").value; var p = $("resFiltroPagamento").value; var q = $("resBusca").value.trim(); var params = [ "status=" + encodeURIComponent(f), "hospedagem=" + encodeURIComponent(h), "pagamento=" + encodeURIComponent(p) ]; if (q) params.push("q=" + encodeURIComponent(q)); if ($("resInicio").value) params.push("inicio=" + encodeURIComponent($("resInicio").value)); if ($("resFim").value) params.push("fim=" + encodeURIComponent($("resFim").value)); var url = "api/admin/reservas?" + params.join("&"); apiGet(url) .then(function (r) { if (!r.ok) { setMsg($("resMsg"), "err", "Falha ao carregar reservas."); return; } reservasCache = r.d.reservas || []; resTotais = r.d; renderReservas(); }) .catch(function () { setMsg($("resMsg"), "err", "Erro ao carregar reservas."); }); } var resTotais = null; function renderReservas() { var tb = $("resTbody"); tb.innerHTML = ""; if (resTotais) { $("resTotais").textContent = "Filtro: " + (resTotais.total || 0) + " reserva(s) · Confirmadas: " + (resTotais.confirmadas || 0) + " · Pendentes: " + (resTotais.pendentes || 0) + " · Soma dos totais: " + fmtBRL(resTotais.soma || 0); } if (!reservasCache.length) { var tr = document.createElement("tr"); var td = document.createElement("td"); td.colSpan = 7; td.textContent = "Nenhuma reserva encontrada."; td.style.color = "var(--muted)"; tr.appendChild(td); tb.appendChild(tr); $("resDetail").innerHTML = ""; return; } reservasCache.forEach(function (r) { var tr = document.createElement("tr"); tr.className = "clickable"; tr.addEventListener("click", function () { renderDetalhe(r); }); var td = function (txt) { var x = document.createElement("td"); x.innerHTML = txt; return x; }; var status = STATUS[r.status] || r.status; tr.appendChild(td(esc(r.id))); tr.appendChild(td(esc(fmtDataHora(r.criadoEm)))); tr.appendChild(td(esc(fmtData(r.checkin) + " → " + fmtData(r.checkout)) + "
" + esc((HOSP_LABEL[r.hospedagem] || r.hospedagem) + (r.noites ? " · " + r.noites + " noite(s)" : "")) + "")); tr.appendChild(td(esc((r.adultos || 0) + " adulto(s)" + (r.criancas ? ", " + r.criancas + " cr." : "")))); tr.appendChild(td(esc(fmtBRL(r.valores && r.valores.total)))); tr.appendChild(td("" + esc(status) + "")); tr.appendChild(td(acoesReserva(r))); tb.appendChild(tr); tr.querySelectorAll("button[data-acao]").forEach(function (b) { b.addEventListener("click", function (ev) { ev.stopPropagation(); executarAcao(b.getAttribute("data-acao"), r); }); }); }); renderDetalhe(reservaSelecionada && reservasCache.find(function (x) { return x.id === reservaSelecionada.id; }) || reservasCache[0]); } function acoesReserva(r) { var botoes = ""; var podeConf = pode("confirmar_reservas"); var podeCanc = pode("cancelar_reservas"); var podeEst = pode("gerenciar_pagamentos"); if (r.status === "pendente") { if (podeConf) botoes += ""; if (podeCanc) botoes += ""; if (podeEst && r.pagamento && r.pagamento.id) { botoes += ""; } } else if (r.status === "confirmada") { if (podeConf) botoes += ""; if (podeCanc) botoes += ""; if (podeEst && r.pagamento && r.pagamento.id) { botoes += ""; } } return botoes ? "" + botoes + "" : ""; } var AC_LABEL = { criada: "Criada", teste: "Modo de teste", confirmada: "Confirmada", pendente: "Pendente", recusada: "Recusada", cancelada: "Cancelada", expirada: "Expirada", estornada: "Estornada", pagamento: "Pagamento" }; function renderDetalhe(r) { if (!r) { $("resDetail").innerHTML = ""; return; } reservaSelecionada = r; var v = r.valores || {}; var pg = r.pagamento || {}; var hist = (r.historico || []).map(function (h) { var titulo = AC_LABEL[h.acao] || h.acao; if (h.de != null && h.para != null && h.de !== h.para) { titulo += " (" + (STATUS[h.de] || h.de) + " → " + (STATUS[h.para] || h.para) + ")"; } var extra = []; if (h.valor != null) extra.push(fmtBRL(h.valor)); if (h.pagamentoMeio) extra.push(PAG_LABEL[h.pagamentoMeio] || h.pagamentoMeio); if (h.pagamentoId) extra.push("MP " + h.pagamentoId); var extraTxt = extra.length ? " · " + esc(extra.join(" · ")) + "" : ""; return "
  • " + esc(titulo) + "" + (h.detalhe ? " — " + esc(h.detalhe) : "") + extraTxt + "
  • "; }).join(""); $("resDetail").innerHTML = "
    " + "
    " + "
    Hóspede" + esc(r.pagador ? r.pagador.nome : "") + "
    " + esc(r.pagador ? r.pagador.email : "") + "
    " + esc(r.pagador ? r.pagador.telefone : "") + "
    " + esc((r.hospedagem === "cabana" ? "Nossa Cabana" : "Chácara Equilíbrio")) + (r.teste ? " · reserva de teste" : "") + "
    " + "
    Período" + esc(fmtData(r.checkin) + " → " + fmtData(r.checkout)) + "
    " + esc((r.noites || 0) + " noite(s)") + "
    " + "
    PagamentoStatus: " + esc(pg.status || "—") + "
    Meio: " + esc(pg.meio || "—") + "
    ID MP: " + esc(pg.id || "—") + "
    " + "
    ValoresDiárias: " + esc(fmtBRL(v.diarias)) + "
    Extras: " + esc(fmtBRL(v.extraHospedes)) + "
    Taxas: " + esc(fmtBRL(v.taxas)) + "
    Desconto: " + esc(fmtBRL(v.desconto)) + "
    " + "
    Total" + esc(fmtBRL(v.total)) + (v.sinalValor !== v.total ? "
    Sinal pago: " + esc(fmtBRL(v.sinalValor)) : "") + (r.aceitouPolitica ? "
    Aceitou a Política de Reserva" : "") + "
    " + "
    Observações" + esc(r.observacoes || "—") + "
    " + "
    " + "
    " + acoesReserva(r) + "
    " + "
    Histórico
    " + "
    "; $("resDetail").querySelectorAll("button[data-acao]").forEach(function (b) { b.addEventListener("click", function (ev) { ev.stopPropagation(); executarAcao(b.getAttribute("data-acao"), r); }); }); } function executarAcao(acao, r) { var conf = acao === "cancelar" ? "Tem certeza que deseja cancelar esta reserva?" : acao === "estornar" ? "Estornar o pagamento da reserva " + r.id + "? O reembolso será processado pelo Mercado Pago." : acao === "enviar-checkin" ? "Enviar AGORA o e-mail de check-in para " + (r.pagador ? r.pagador.email : "") + "? (Somente o modelo de check-in será enviado.)" : "Confirmar manualmente a reserva " + r.id + "?"; if (!window.confirm(conf)) return; apiMut("POST", "api/admin/reservas/" + encodeURIComponent(r.id) + "/" + acao, null) .then(function (res) { if (!res.ok) { setMsg($("resMsg"), "err", (res.d && res.d.erro) || "Falha na operação."); return; } setMsg($("resMsg"), "ok", acao === "cancelar" ? "Reserva cancelada com sucesso." : acao === "estornar" ? "Reserva estornada com sucesso." : acao === "enviar-checkin" ? "E-mail de check-in enviado ao hóspede." : "Reserva confirmada com sucesso."); carregarReservas(); carregarDashboard(); }) .catch(function () { setMsg($("resMsg"), "err", "Erro de conexão."); }); } function exportarReservas() { var f = $("resFiltroStatus").value; var h = $("resFiltroHosp").value; var p = $("resFiltroPagamento").value; var q = $("resBusca").value.trim(); var params = ["status=" + encodeURIComponent(f), "hospedagem=" + encodeURIComponent(h), "pagamento=" + encodeURIComponent(p)]; if (q) params.push("q=" + encodeURIComponent(q)); if ($("resInicio").value) params.push("inicio=" + encodeURIComponent($("resInicio").value)); if ($("resFim").value) params.push("fim=" + encodeURIComponent($("resFim").value)); window.location.href = "api/admin/reservas/exportar?" + params.join("&"); } $("resFiltroHosp").addEventListener("change", carregarReservas); $("resFiltroStatus").addEventListener("change", carregarReservas); $("resFiltroPagamento").addEventListener("change", carregarReservas); $("resInicio").addEventListener("change", carregarReservas); $("resFim").addEventListener("change", carregarReservas); $("resBusca").addEventListener("input", function () { clearTimeout(window.__resBuscaT); window.__resBuscaT = setTimeout(carregarReservas, 300); }); $("resAtualizar").addEventListener("click", carregarReservas); $("resExportar").addEventListener("click", exportarReservas); /* ---------- Cupons ---------- */ var cuponsCache = []; var cupomEditandoId = ""; function carregarCupons() { apiGet("api/admin/cupons") .then(function (r) { if (!r.ok) { setMsg($("cupMsg"), "err", "Falha ao carregar cupons."); return; } cuponsCache = r.d.cupons || []; renderCupons(); }) .catch(function () { setMsg($("cupMsg"), "err", "Erro ao carregar cupons."); }); } function renderCupons() { var tb = $("cupTbody"); tb.innerHTML = ""; if (!cuponsCache.length) { var tr = document.createElement("tr"); var td = document.createElement("td"); td.colSpan = 9; td.textContent = "Nenhum cupom cadastrado."; td.style.color = "var(--muted)"; tr.appendChild(td); tb.appendChild(tr); return; } cuponsCache.forEach(function (c) { var tr = document.createElement("tr"); var td = function (txt) { var x = document.createElement("td"); x.innerHTML = txt; return x; }; tr.appendChild(td("" + esc(c.codigo) + "")); tr.appendChild(td(c.tipo === "fixo" ? "Fixo" : "Percentual")); tr.appendChild(td(esc(c.tipo === "fixo" ? fmtBRL(c.valor) : c.valor + "%"))); tr.appendChild(td(esc(c.validade ? fmtData(c.validade) : "—"))); tr.appendChild(td(esc(c.dataInicio ? fmtData(c.dataInicio) : "—"))); tr.appendChild(td(String(c.usos || 0))); tr.appendChild(td(c.maxUses != null ? String(c.maxUses) : "∞")); tr.appendChild(td(c.ativo ? "Sim" : "Não")); var acoes = document.createElement("td"); var wrap = document.createElement("span"); wrap.className = "actions"; var btnEdit = document.createElement("button"); btnEdit.className = "btn btn-sm btn-ghost"; btnEdit.type = "button"; btnEdit.textContent = "Editar"; btnEdit.addEventListener("click", function () { preencherCupom(c); }); wrap.appendChild(btnEdit); var btnToggle = document.createElement("button"); btnToggle.className = "btn btn-sm " + (c.ativo ? "btn-danger" : "btn-primary"); btnToggle.type = "button"; btnToggle.textContent = c.ativo ? "Desativar" : "Ativar"; btnToggle.title = c.ativo ? "Desativar cupom sem apagá-lo" : "Reativar cupom"; btnToggle.addEventListener("click", function () { alternarCupom(c); }); wrap.appendChild(btnToggle); var btnDel = document.createElement("button"); btnDel.className = "btn btn-sm btn-danger"; btnDel.type = "button"; btnDel.textContent = "Excluir"; btnDel.title = "Excluir definitivamente o cupom"; btnDel.addEventListener("click", function () { excluirCupom(c); }); if (!pode("excluir_dados")) btnDel.style.display = "none"; wrap.appendChild(btnDel); acoes.appendChild(wrap); tr.appendChild(acoes); tb.appendChild(tr); }); } function alternarCupom(c) { var novoAtivo = !(c.ativo !== false); var body = { codigo: c.codigo, tipo: c.tipo === "fixo" ? "fixo" : "percentual", valor: c.valor, validade: c.validade || "", dataInicio: c.dataInicio || "", maxUses: c.maxUses != null ? c.maxUses : null, minNoites: c.minNoites || 0, tiposPermitidos: c.tiposPermitidos || [], ativo: novoAtivo, descricao: c.descricao || "" }; apiMut("POST", "api/admin/cupons", body) .then(function (r) { if (!r.ok) { setMsg($("cupMsg"), "err", (r.d && r.d.erro) || "Falha ao alterar o cupom."); return; } setMsg($("cupMsg"), "ok", "Cupom " + c.codigo + (novoAtivo ? " ativado." : " desativado.")); carregarCupons(); }) .catch(function () { setMsg($("cupMsg"), "err", "Erro de conexão."); }); } function excluirCupom(c) { if (!window.confirm("Excluir definitivamente o cupom " + c.codigo + "? Esta ação não pode ser desfeita.")) return; apiMut("DELETE", "api/admin/cupons", { codigo: c.codigo }) .then(function (r) { if (!r.ok) { setMsg($("cupMsg"), "err", (r.d && r.d.erro) || "Falha ao excluir o cupom."); return; } setMsg($("cupMsg"), "ok", "Cupom " + c.codigo + " excluído definitivamente."); if (cupomEditandoId === c.codigo) limparCupomForm(); carregarCupons(); }) .catch(function () { setMsg($("cupMsg"), "err", "Erro de conexão."); }); } function preencherCupom(c) { cupomEditandoId = c.codigo; $("cupFormTitle").textContent = "Editar cupom " + c.codigo; $("cupCodigo").value = c.codigo; $("cupTipo").value = c.tipo === "fixo" ? "fixo" : "percentual"; $("cupValor").value = c.valor; $("cupValidade").value = c.validade || ""; $("cupDataInicio").value = c.dataInicio || ""; $("cupMaxUses").value = c.maxUses != null ? c.maxUses : ""; $("cupMinNoites").value = c.minNoites || 0; $("cupTipos").value = (c.tiposPermitidos || []).join(", "); $("cupDescricao").value = c.descricao || ""; $("cupAtivo").checked = c.ativo !== false; $("cupFormCard").scrollIntoView({ behavior: "smooth", block: "start" }); } function limparCupomForm() { cupomEditandoId = ""; $("cupFormTitle").textContent = "Novo cupom"; $("cupForm").reset(); $("cupAtivo").checked = true; } $("cupLimpar").addEventListener("click", limparCupomForm); $("cupForm").addEventListener("submit", function (e) { e.preventDefault(); var body = { codigo: $("cupCodigo").value.trim(), tipo: $("cupTipo").value, valor: Number($("cupValor").value), validade: $("cupValidade").value || "", dataInicio: $("cupDataInicio").value || "", maxUses: $("cupMaxUses").value === "" ? null : Number($("cupMaxUses").value), minNoites: Number($("cupMinNoites").value) || 0, tiposPermitidos: $("cupTipos").value.split(",").map(function (s) { return s.trim(); }).filter(Boolean), ativo: $("cupAtivo").checked, descricao: $("cupDescricao").value.trim() }; if (!body.codigo) { setMsg($("cupMsg"), "err", "Informe o código do cupom."); return; } apiMut("POST", "api/admin/cupons", body) .then(function (r) { if (!r.ok) { setMsg($("cupMsg"), "err", (r.d && r.d.erro) || "Falha ao salvar cupom."); return; } setMsg($("cupMsg"), "ok", "Cupom salvo."); limparCupomForm(); carregarCupons(); }) .catch(function () { setMsg($("cupMsg"), "err", "Erro de conexão."); }); }); /* ---------- Configurações ---------- */ var cfg = null; function carregarConfig() { apiGet("api/admin/config") .then(function (r) { if (!r.ok) { setMsg($("cfgMsg"), "err", "Falha ao carregar configurações."); return; } cfg = r.d; renderConfig(); }) .catch(function () { setMsg($("cfgMsg"), "err", "Erro ao carregar configurações."); }); } function renderConfig() { if (!cfg) return; $("cfgMaxHospedes").value = cfg.maxHospedes || 16; $("cfgMinNoites").value = cfg.minNoites || 1; $("cfgMaxNoites").value = cfg.maxNoites || 30; $("cfgHoldMin").value = cfg.holdMinutos || 30; $("cfgIncluidos").value = (cfg.hospedes && cfg.hospedes.incluidos) || 0; $("cfgExtraPor").value = (cfg.hospedes && cfg.hospedes.extraPorHospede) || 0; $("cfgFeriados").value = (cfg.feriados || []).join("\n"); $("cfgPolitica").value = cfg.politicaCancelamento || ""; $("cfgPoliticaReserva").value = cfg.politicaReserva || ""; $("cfgPoliticaObrigatoria").checked = cfg.politicaObrigatoria !== false; $("cfgWa").value = (cfg.contato && cfg.contato.whatsapp) || ""; $("cfgTel").value = (cfg.contato && cfg.contato.telefone) || ""; $("cfgEmail").value = (cfg.contato && cfg.contato.email) || ""; $("cfgEndereco").value = (cfg.local && cfg.local.endereco) || ""; $("cfgCheckinH").value = (cfg.local && cfg.local.checkinHorario) || ""; $("cfgCheckoutH").value = (cfg.local && cfg.local.checkoutHorario) || ""; renderTipos(); renderTaxas(); renderRegras(); } function renderTipos() { var wrap = $("cfgTipos"); wrap.innerHTML = ""; (cfg.tipoReserva || []).forEach(function (t, i) { var row = document.createElement("div"); row.className = "field-row"; row.style.marginBottom = "10px"; row.innerHTML = "
    " + "
    " + "
    " + "
    " + "
    " + "
    " + ""; row.querySelector("[data-del]").addEventListener("click", function () { cfg.tipoReserva.splice(i, 1); renderTipos(); }); wrap.appendChild(row); }); var add = document.createElement("button"); add.className = "btn btn-ghost btn-sm"; add.type = "button"; add.textContent = "+ Adicionar tipo"; add.addEventListener("click", function () { cfg.tipoReserva.push({ id: "novo" + (cfg.tipoReserva.length + 1), nome: "Novo tipo", precoPorNoite: 0, precoFixo: 0, minNoites: 1, descricao: "" }); renderTipos(); }); wrap.appendChild(add); } function renderTaxas() { var wrap = $("cfgTaxas"); wrap.innerHTML = ""; (cfg.taxas || []).forEach(function (t, i) { var row = document.createElement("div"); row.className = "field-row"; row.style.marginBottom = "10px"; row.innerHTML = "
    " + "
    " + ""; row.querySelector("[data-del]").addEventListener("click", function () { cfg.taxas.splice(i, 1); renderTaxas(); }); wrap.appendChild(row); }); var add = document.createElement("button"); add.className = "btn btn-ghost btn-sm"; add.type = "button"; add.textContent = "+ Adicionar taxa"; add.addEventListener("click", function () { cfg.taxas.push({ nome: "Nova taxa", valor: 0 }); renderTaxas(); }); wrap.appendChild(add); } function renderRegras() { var wrap = $("cfgRegras"); wrap.innerHTML = ""; if (!cfg.regrasCasa) cfg.regrasCasa = []; cfg.regrasCasa.forEach(function (r, i) { var row = document.createElement("div"); row.className = "field-row"; row.style.marginBottom = "10px"; row.innerHTML = "
    " + "
    " + "
    " + "" + "" + "" + "
    "; row.querySelector("[data-move='-1']").addEventListener("click", function () { if (i === 0) return; var a = cfg.regrasCasa.splice(i, 1)[0]; cfg.regrasCasa.splice(i - 1, 0, a); renderRegras(); }); row.querySelector("[data-move='1']").addEventListener("click", function () { if (i >= cfg.regrasCasa.length - 1) return; var a = cfg.regrasCasa.splice(i, 1)[0]; cfg.regrasCasa.splice(i + 1, 0, a); renderRegras(); }); row.querySelector("[data-del]").addEventListener("click", function () { cfg.regrasCasa.splice(i, 1); renderRegras(); }); wrap.appendChild(row); }); } function lerCfgDosFormularios() { var tipos = []; $("cfgTipos").querySelectorAll(".field-row").forEach(function (row) { var t = {}; row.querySelectorAll("[data-k]").forEach(function (inp) { var k = inp.getAttribute("data-k"); var v = inp.value; if (k === "precoPorNoite" || k === "precoFixo" || k === "minNoites") v = Number(v) || 0; t[k] = v; }); if (t.id && t.nome) tipos.push(t); }); var taxas = []; $("cfgTaxas").querySelectorAll(".field-row").forEach(function (row) { var t = {}; row.querySelectorAll("[data-k]").forEach(function (inp) { var k = inp.getAttribute("data-k"); var v = inp.value; if (k === "valor") v = Number(v) || 0; t[k] = v; }); if (t.nome) taxas.push(t); }); return { maxHospedes: Number($("cfgMaxHospedes").value), minNoites: Number($("cfgMinNoites").value), maxNoites: Number($("cfgMaxNoites").value), holdMinutos: Number($("cfgHoldMin").value), hospedes: { incluidos: Number($("cfgIncluidos").value) || 0, extraPorHospede: Number($("cfgExtraPor").value) || 0 }, tipoReserva: tipos, taxas: taxas, feriados: $("cfgFeriados").value.split("\n").map(function (s) { return s.trim(); }).filter(Boolean), politicaCancelamento: $("cfgPolitica").value.trim(), politicaReserva: $("cfgPoliticaReserva").value.trim(), politicaObrigatoria: $("cfgPoliticaObrigatoria").checked, regrasCasa: (function () { var regras = []; $("cfgRegras").querySelectorAll(".field-row").forEach(function (row) { var t = {}; row.querySelectorAll("[data-k]").forEach(function (inp) { t[inp.getAttribute("data-k")] = inp.value.trim(); }); if (t.item) regras.push({ item: t.item, detalhe: t.detalhe || "" }); }); return regras; })(), contato: { whatsapp: $("cfgWa").value.trim(), telefone: $("cfgTel").value.trim(), email: $("cfgEmail").value.trim() }, local: { endereco: $("cfgEndereco").value.trim(), checkinHorario: $("cfgCheckinH").value.trim(), checkoutHorario: $("cfgCheckoutH").value.trim() } }; } var cfgRegrasAdd = $("cfgRegrasAdd"); if (cfgRegrasAdd) { cfgRegrasAdd.addEventListener("click", function () { if (!cfg.regrasCasa) cfg.regrasCasa = []; cfg.regrasCasa.push({ item: "Nova regra", detalhe: "" }); renderRegras(); }); } $("cfgSalvar").addEventListener("click", function () { var body = lerCfgDosFormularios(); if (!body.tipoReserva.length) { setMsg($("cfgMsg"), "err", "Mantenha ao menos um tipo de reserva."); return; } apiMut("POST", "api/admin/config", body) .then(function (r) { if (!r.ok) { setMsg($("cfgMsg"), "err", (r.d && r.d.erro) || "Falha ao salvar."); return; } setMsg($("cfgMsg"), "ok", "Configurações salvas."); carregarConfig(); }) .catch(function () { setMsg($("cfgMsg"), "err", "Erro de conexão."); }); }); /* ---------- Empreendimentos ---------- */ function carregarEmpreendimentos() { apiGet("api/admin/empreendimentos") .then(function (r) { if (!r.ok) { setMsg($("empMsg"), "err", "Falha ao carregar empreendimentos."); return; } renderEmpreendimentos(r.d.empreendimentos || []); }) .catch(function () { setMsg($("empMsg"), "err", "Erro ao carregar empreendimentos."); }); } function renderEmpreendimentos(lista) { var wrap = $("empList"); wrap.innerHTML = ""; var icalFontes = { "cal-config.json": "cal-config.json", "cal-config-cabana.json": "cal-config-cabana.json" }; lista.forEach(function (e, i) { var card = document.createElement("div"); card.className = "card emp-card"; card.style.marginBottom = "16px"; var icalRow = ""; if (e.icalAtivo) { icalRow = "
    " + "" + "" + "Fonte do feed: " + esc(e.icalFonte || "cal-config.json") + ". " + (e.icalStatus ? "Última sincronização: " + esc(fmtDataHora(e.icalStatus)) : "Ainda não sincronizado.") + (e.icalErro ? " " + esc(e.icalErro) + "" : "") + "" + "
    " + "
    " + "" + "" + "Cole esta URL no calendário do anúncio no Airbnb (para bloquear/liberar as datas automaticamente conforme as reservas do site). Reserva confirmada bloqueia; cancelada libera." + "
    "; } card.innerHTML = "

    " + esc(e.nome) + "

    " + "
    " + "
    " + "
    " + "
    " + "
    " + "
    " + "
    " + "
    " + "
    " + "
    " + "" + "
    " + "
    " + icalRow; wrap.appendChild(card); }); var salvar = document.createElement("button"); salvar.className = "btn btn-primary"; salvar.type = "button"; salvar.textContent = "Salvar empreendimentos"; salvar.addEventListener("click", function () { var out = []; var ok = true; Array.prototype.forEach.call(wrap.querySelectorAll(".emp-card"), function (c) { var o = {}; c.querySelectorAll("[data-k]").forEach(function (el) { var k = el.getAttribute("data-k"); if (k === "icalAtivo") { o[k] = el.checked; return; } o[k] = el.value.trim(); }); o.precoDiaria = parseFloat(o.precoDiaria) || 0; o.precoFixo = parseFloat(o.precoFixo) || 0; o.minNoites = parseInt(o.minNoites, 10) || 1; if (!o.id || !o.nome) ok = false; out.push(o); }); if (!ok) { setMsg($("empMsg"), "err", "Preencha ID e nome de cada empreendimento."); return; } apiMut("POST", "api/admin/empreendimentos", { empreendimentos: out }) .then(function (r) { if (!r.ok) { setMsg($("empMsg"), "err", (r.d && r.d.erro) || "Falha ao salvar."); return; } setMsg($("empMsg"), "ok", "Empreendimentos salvos. Calendários sincronizados."); carregarEmpreendimentos(); }) .catch(function () { setMsg($("empMsg"), "err", "Erro de conexão."); }); }); wrap.appendChild(salvar); } /* ---------- Integração com o Airbnb (Configurações) ---------- */ function carregarAirbnb() { apiGet("api/admin/airbnb") .then(function (r) { if (!r.ok) { setMsg($("airbnbMsg"), "err", "Falha ao carregar a integração com o Airbnb."); return; } renderAirbnb(r.d.empreendimentos || []); }) .catch(function () { setMsg($("airbnbMsg"), "err", "Erro ao carregar a integração com o Airbnb."); }); } function renderAirbnb(lista) { var wrap = $("airbnbList"); wrap.innerHTML = ""; lista.forEach(function (e, i) { var num = i + 1; var card = document.createElement("div"); card.className = "card"; card.style.marginBottom = "16px"; var statusHtml; if (!e.ativo) { statusHtml = "
    Integração desativada para este empreendimento (ative na aba Empreendimentos).
    "; } else if (e.configurado && e.ultimaSincronizacao) { statusHtml = "
    Sincronizado. Última sincronização: " + esc(fmtDataHora(e.ultimaSincronizacao)) + " — " + (e.diasBloqueados || 0) + " dia(s) importado(s) do Airbnb bloqueando o calendário no site.
    "; } else if (e.configurado) { statusHtml = "
    Configurado, aguardando a primeira sincronização.
    "; } else if (e.erro) { statusHtml = "
    " + esc(e.erro) + "
    "; } else { statusHtml = "
    Ainda não conectado ao Airbnb.
    "; } card.innerHTML = "

    Calendário " + num + " — " + esc(e.nome) + "

    " + "
    " + "" + "" + "Reserva confirmada no site bloqueia esta data; cancelada libera de novo, automaticamente." + "
    " + "
    " + "" + "" + "Fonte atual: " + esc(e.icalFonte || "") + "." + "
    " + "
    " + "" + "" + "" + "
    " + statusHtml; card.querySelector("[data-acao='conectar']").addEventListener("click", function () { conectarAirbnb(e.id, card.querySelector("#abUrl_" + i).value); }); card.querySelector("[data-acao='sincronizar']").addEventListener("click", function () { sincronizarAirbnb(e.id, card); }); card.querySelector("[data-acao='desconectar']").addEventListener("click", function () { if (window.confirm("Desconectar o Calendário " + num + " (" + e.nome + ")? As datas importadas do Airbnb deixarão de bloquear o site.")) { desconectarAirbnb(e.id); } }); wrap.appendChild(card); }); } function conectarAirbnb(id, url) { setMsg($("airbnbMsg"), "", "Conectando…"); apiMut("POST", "api/admin/airbnb", { empreendimento: id, icalUrl: url }) .then(function (r) { if (!r.ok) { setMsg($("airbnbMsg"), "err", (r.d && r.d.erro) || "Falha ao conectar."); return; } var e = r.d.empreendimento; if (e && e.erro) setMsg($("airbnbMsg"), "err", "Calendário conectado, mas a sincronização falhou: " + e.erro); else setMsg($("airbnbMsg"), "ok", "Calendário conectado. Sincronização com o Airbnb concluída."); carregarAirbnb(); }) .catch(function () { setMsg($("airbnbMsg"), "err", "Erro de conexão."); }); } function sincronizarAirbnb(id, card) { var btn = card.querySelector("[data-acao='sincronizar']"); btn.disabled = true; setMsg($("airbnbMsg"), "", "Sincronizando…"); apiMut("POST", "api/admin/airbnb/sincronizar", { empreendimento: id }) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("airbnbMsg"), "err", (r.d && r.d.erro) || "Falha ao sincronizar."); return; } var e = r.d.empreendimento; if (e && e.erro) setMsg($("airbnbMsg"), "err", e.erro); else setMsg($("airbnbMsg"), "ok", "Sincronizado com sucesso em " + fmtDataHora(e && e.ultimaSincronizacao) + "."); carregarAirbnb(); }) .catch(function () { btn.disabled = false; setMsg($("airbnbMsg"), "err", "Erro de conexão."); }); } function desconectarAirbnb(id) { apiMut("POST", "api/admin/airbnb/desconectar", { empreendimento: id }) .then(function (r) { if (!r.ok) { setMsg($("airbnbMsg"), "err", (r.d && r.d.erro) || "Falha ao desconectar."); return; } setMsg($("airbnbMsg"), "ok", "Calendário desconectado. As datas importadas deixaram de bloquear o site."); carregarAirbnb(); }) .catch(function () { setMsg($("airbnbMsg"), "err", "Erro de conexão."); }); } /* ---------- Galeria de Fotos da Chácara ---------- */ var CHA_CAT = []; var CHA_LISTA = []; var chaPrevias = []; function carregarChaFotos() { apiGet("api/admin/fotos?hospedagem=chacara") .then(function (r) { if (!r.ok) { setMsg($("chaFotosMsg"), "err", "Falha ao carregar as fotos da Chácara."); return; } CHA_CAT = r.d.categorias || []; CHA_LISTA = r.d.fotos || []; renderChaSelect(); renderChaGrid(); }) .catch(function () { setMsg($("chaFotosMsg"), "err", "Erro ao carregar as fotos."); }); } function renderChaSelect() { var sel = $("chaFotosCategoria"); sel.innerHTML = ""; CHA_CAT.forEach(function (c) { var o = document.createElement("option"); o.value = c; o.textContent = c; sel.appendChild(o); }); } function lerArquivoBase64(arq, cb) { var leitor = new FileReader(); leitor.onload = function () { cb(leitor.result); }; leitor.onerror = function () { setMsg($("chaFotosMsg"), "err", "Não foi possível ler o arquivo."); }; leitor.readAsDataURL(arq); } /* Pré-visualização antes de salvar: lê os arquivos selecionados e monta a lista de novas fotos com legenda por item. */ $("chaFotosArquivos").addEventListener("change", function () { var arquivos = Array.prototype.slice.call(this.files || []); if (!arquivos.length) { renderChaPrevia(); return; } setMsg($("chaFotosMsg"), "ok", "Lendo " + arquivos.length + " foto(s)..."); var pendentes = arquivos.length; var lidas = []; arquivos.forEach(function (arq) { lerArquivoBase64(arq, function (dataUrl) { lidas.push({ arquivo: dataUrl, legenda: "", categoria: $("chaFotosCategoria").value, principal: false }); pendentes -= 1; if (pendentes === 0) { chaPrevias = lidas; setMsg($("chaFotosMsg"), "ok", arquivos.length + " foto(s) prontas. Revise a prévia e envie."); renderChaPrevia(); } }); }); }); function renderChaPrevia() { var wrap = $("chaFotosPrevisto"); wrap.innerHTML = ""; var botoes = $("chaFotosBotoes"); if (!chaPrevias.length) { botoes.style.display = "none"; return; } botoes.style.display = "flex"; var capaPadrao = $("chaFotosCapaPadrao").checked; var grid = document.createElement("div"); grid.style.cssText = "display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:12px;margin-top:14px;"; chaPrevias.forEach(function (p, i) { var card = document.createElement("div"); card.style.cssText = "border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--card);"; var img = document.createElement("img"); img.src = p.arquivo; img.style.cssText = "width:100%;aspect-ratio:4/3;object-fit:cover;display:block;background:#e7e3d9;"; card.appendChild(img); var corpo = document.createElement("div"); corpo.style.cssText = "padding:8px;display:flex;flex-direction:column;gap:6px;"; var capa = document.createElement("label"); capa.style.cssText = "font-size:12px;display:flex;align-items:center;gap:6px;color:var(--ink);"; var chk = document.createElement("input"); chk.type = "checkbox"; chk.checked = i === 0 && capaPadrao; chk.addEventListener("change", function () { chaPrevias.forEach(function (x, k) { x.principal = k === i ? chk.checked : false; }); }); capa.appendChild(chk); capa.appendChild(document.createTextNode("Definir como capa")); corpo.appendChild(capa); card.appendChild(corpo); grid.appendChild(card); }); wrap.appendChild(grid); } $("chaFotosLimpar").addEventListener("click", function () { chaPrevias = []; $("chaFotosArquivos").value = ""; renderChaPrevia(); clearMsg($("chaFotosMsg")); }); $("chaFotosEnviar").addEventListener("click", function () { if (!chaPrevias.length) { setMsg($("chaFotosMsg"), "err", "Selecione ao menos uma foto."); return; } setMsg($("chaFotosMsg"), "ok", "Enviando " + chaPrevias.length + " foto(s)..."); apiMut("POST", "api/admin/fotos/lote", { hospedagem: "chacara", fotos: chaPrevias }) .then(function (r) { if (!r.ok) { setMsg($("chaFotosMsg"), "err", (r.d && r.d.erro) || "Falha ao enviar as fotos."); return; } setMsg($("chaFotosMsg"), "ok", (r.d.fotos || []).length + " foto(s) enviadas. A galeria do site já foi atualizada."); chaPrevias = []; $("chaFotosArquivos").value = ""; renderChaPrevia(); carregarChaFotos(); }) .catch(function () { setMsg($("chaFotosMsg"), "err", "Erro de conexão."); }); }); function chaMsgErro(d) { return (d && d.erro) || "Não foi possível concluir."; } function renderChaGrid() { var grid = $("chaFotosGrid"); grid.innerHTML = ""; if (!CHA_LISTA.length) { var p = document.createElement("p"); p.textContent = "Nenhuma foto da Chácara cadastrada ainda."; p.style.color = "var(--muted)"; grid.appendChild(p); return; } var porCat = {}; CHA_LISTA.forEach(function (f) { (porCat[f.categoria] = porCat[f.categoria] || []).push(f); }); CHA_CAT.forEach(function (cat) { var fotos = porCat[cat]; if (!fotos || !fotos.length) return; var sec = document.createElement("div"); sec.style.marginBottom = "28px"; var head = document.createElement("h3"); head.style.cssText = "font-size:14px;color:var(--forest);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px;border-bottom:1px solid var(--line);padding-bottom:8px;"; head.textContent = cat + " (" + fotos.length + ")"; sec.appendChild(head); var wrap = document.createElement("div"); wrap.style.cssText = "display:flex;flex-wrap:wrap;gap:16px;"; fotos.forEach(function (f) { wrap.appendChild(chaCardFoto(f)); }); sec.appendChild(wrap); grid.appendChild(sec); }); } function chaCardFoto(f) { var card = document.createElement("div"); card.style.cssText = "width:220px;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--card);display:flex;flex-direction:column;"; card.setAttribute("data-id", f.id); var img = document.createElement("img"); img.src = f.url; img.alt = f.legenda || f.categoria || "Foto da Chácara"; img.title = "Clique para prévia em tela cheia"; img.style.cssText = "width:100%;aspect-ratio:4/3;object-fit:cover;display:block;cursor:zoom-in;background:#e7e3d9;"; img.addEventListener("click", function () { window.open(f.url, "_blank", "noopener"); }); card.appendChild(img); var badge = document.createElement("div"); badge.style.cssText = "display:flex;justify-content:space-between;align-items:center;padding:8px 10px 0;"; var nome = document.createElement("span"); nome.textContent = f.principal ? "★ Capa" : "Foto"; nome.style.cssText = f.principal ? "font-size:11px;color:#8a5a10;font-weight:600;" : "font-size:11px;color:var(--muted);"; badge.appendChild(nome); var cont = document.createElement("span"); cont.textContent = f.ordem !== undefined ? "#" + (f.ordem + 1) : ""; cont.style.cssText = "font-size:11px;color:var(--muted);"; badge.appendChild(cont); card.appendChild(badge); var corpo = document.createElement("div"); corpo.style.cssText = "padding:8px 10px 10px;display:flex;flex-direction:column;gap:8px;"; var sel = document.createElement("select"); sel.setAttribute("aria-label", "Categoria da foto"); sel.style.cssText = "width:100%;padding:6px 8px;font-size:13px;border:1px solid var(--line);border-radius:6px;background:var(--white);color:var(--ink);"; CHA_CAT.forEach(function (c) { var o = document.createElement("option"); o.value = c; o.textContent = c; if (c === f.categoria) o.selected = true; sel.appendChild(o); }); sel.addEventListener("change", function () { chaAtualizar(f.id, { categoria: sel.value }); }); corpo.appendChild(sel); var leg = document.createElement("input"); leg.type = "text"; leg.value = f.legenda || ""; leg.maxLength = 120; leg.placeholder = "Legenda (opcional)"; leg.setAttribute("aria-label", "Legenda da foto"); leg.style.cssText = "width:100%;padding:6px 8px;font-size:13px;border:1px solid var(--line);border-radius:6px;background:var(--white);color:var(--ink);"; var timer = null; leg.addEventListener("input", function () { clearTimeout(timer); timer = setTimeout(function () { chaAtualizar(f.id, { legenda: leg.value }); }, 700); }); corpo.appendChild(leg); var botoes = document.createElement("div"); botoes.style.cssText = "display:flex;gap:6px;flex-wrap:wrap;"; function btn(txt, titulo, fn, perigo) { var b = document.createElement("button"); b.type = "button"; b.textContent = txt; b.title = titulo || ""; b.style.cssText = "flex:1;min-width:0;padding:5px 6px;font-size:12px;border:1px solid " + (perigo ? "#c33" : "var(--line)") + ";border-radius:6px;background:var(--white);color:" + (perigo ? "#a52222" : "var(--ink)") + ";cursor:pointer;"; b.addEventListener("click", fn); return b; } if (!f.principal) { botoes.appendChild(btn("★ Capa", "Definir como capa da galeria", function () { chaAtualizar(f.id, { principal: true }); })); } botoes.appendChild(btn("↑", "Mover para cima", function () { chaReordenar(f.id, "cima"); })); botoes.appendChild(btn("↓", "Mover para baixo", function () { chaReordenar(f.id, "baixo"); })); var sub = btn("Substituir", "Substituir o arquivo por um novo upload", function () { fileSub.click(); }); botoes.appendChild(sub); var fileSub = document.createElement("input"); fileSub.type = "file"; fileSub.accept = "image/jpeg,image/png,image/webp,image/avif"; fileSub.style.display = "none"; fileSub.addEventListener("change", function () { var arq = fileSub.files && fileSub.files[0]; if (!arq) return; lerArquivoBase64(arq, function (dataUrl) { chaSubstituir(f.id, dataUrl); }); fileSub.value = ""; }); botoes.appendChild(fileSub); if (pode("excluir_dados")) { botoes.appendChild(btn("Excluir", "Excluir esta foto", function () { chaExcluir(f.id); }, true)); } corpo.appendChild(botoes); card.appendChild(corpo); return card; } function chaAtualizar(id, d) { apiMut("POST", "api/admin/fotos/" + encodeURIComponent(id), d) .then(function (r) { if (!r.ok) { setMsg($("chaFotosMsg"), "err", chaMsgErro(r.d)); return; } carregarChaFotos(); }) .catch(function () { setMsg($("chaFotosMsg"), "err", "Erro de conexão."); }); } function chaReordenar(id, dir) { apiMut("POST", "api/admin/fotos/" + encodeURIComponent(id) + "/reordenar", { dir: dir }) .then(function (r) { if (!r.ok) { setMsg($("chaFotosMsg"), "err", chaMsgErro(r.d)); return; } carregarChaFotos(); }) .catch(function () { setMsg($("chaFotosMsg"), "err", "Erro de conexão."); }); } function chaSubstituir(id, dataUrl) { setMsg($("chaFotosMsg"), "ok", "Enviando nova imagem..."); apiMut("POST", "api/admin/fotos/" + encodeURIComponent(id) + "/substituir", { arquivo: dataUrl }) .then(function (r) { if (!r.ok) { setMsg($("chaFotosMsg"), "err", chaMsgErro(r.d)); return; } setMsg($("chaFotosMsg"), "ok", "Foto substituída."); carregarChaFotos(); }) .catch(function () { setMsg($("chaFotosMsg"), "err", "Erro ao substituir."); }); } function chaExcluir(id) { if (!window.confirm("Excluir esta foto permanentemente? Esta ação não pode ser desfeita.")) return; apiMut("POST", "api/admin/fotos/" + encodeURIComponent(id) + "/excluir") .then(function (r) { if (!r.ok) { setMsg($("chaFotosMsg"), "err", chaMsgErro(r.d)); return; } setMsg($("chaFotosMsg"), "ok", "Foto excluída. A galeria do site foi atualizada."); carregarChaFotos(); }) .catch(function () { setMsg($("chaFotosMsg"), "err", "Erro ao excluir."); }); } /* ---------- Domínio ---------- */ function carregarDominio() { apiGet("api/admin/dominio") .then(function (r) { if (!r.ok) { setMsg($("domMsg"), "err", "Falha ao carregar o domínio."); return; } renderDominio(r.d); }) .catch(function () { setMsg($("domMsg"), "err", "Erro ao carregar o domínio."); }); } function renderDominio(d) { d = d || {}; var fonte = d.fonte || ""; var rotuloFonte = fonte === "SITE_URL" ? "Variável de ambiente (SITE_URL)" : fonte === "painel" ? "Painel administrativo" : "Não configurado (modo automático)"; $("domAtual").textContent = d.dominio || "(endereço atual do site)"; $("domFonte").textContent = rotuloFonte; $("domInput").value = d.painel || ""; var info = $("domInfo"); if (fonte === "SITE_URL") { setMsg(info, "info", "O domínio definitivo é controlado pela variável de ambiente SITE_URL e tem prioridade sobre este campo. Para usar um domínio diferente, ajuste SITE_URL na hospedagem."); } else if (fonte === "painel") { setMsg(info, "info", "Domínio oficial definido no painel. Canonical, sitemap, robots, e-mails e pagamentos passam a usar este endereço."); } else { setMsg(info, "info", "Nenhum domínio oficial configurado. O site usa o endereço atual e as páginas ficam com noindex para não indexar o endereço temporário. Informe o domínio aqui ou defina SITE_URL na hospedagem."); } var h = d.https || {}; var hrow = $("domHttpsRow"); var hmsg = $("domHttpsMsg"); if (h.status === "ok") { hrow.style.display = "grid"; $("domHttps").innerHTML = "✓ Ativo e configurado corretamente"; $("domUrl").textContent = h.url || "—"; var cd = h.certificado || {}; var validaMsg = (function () { try { return new Date(cd.validoAte).toLocaleDateString("pt-BR"); } catch (_) { return String(cd.validoAte || ""); } })(); setMsg(hmsg, "ok", "Certificado SSL válido (" + esc(cd.emitente || "—") + "), expira em " + esc(validaMsg) + ", cobrindo " + esc((cd.san || []).join(", ")) + ". HTTPS ativo e configurado corretamente."); } else if (h.status === "pendente") { hrow.style.display = "grid"; $("domHttps").innerHTML = "✗ " + esc(h.detalhes || "Pendente") + ""; $("domUrl").textContent = h.url || "—"; setMsg(hmsg, "err", h.detalhes || "HTTPS pendente."); } else { hrow.style.display = "none"; hmsg.className = "msg"; hmsg.textContent = ""; } } $("domForm").addEventListener("submit", function (e) { e.preventDefault(); var btn = e.target.querySelector("button[type=submit]"); btn.disabled = true; apiMut("POST", "api/admin/dominio", { dominio: $("domInput").value.trim() }) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("domMsg"), "err", (r.d && r.d.erro) || "Falha ao salvar o domínio."); return; } setMsg($("domMsg"), "ok", "Domínio salvo."); renderDominio(r.d); }) .catch(function () { btn.disabled = false; setMsg($("domMsg"), "err", "Erro de conexão."); }); }); /* ---------- Bloqueios ---------- */ function carregarBloqueios() { apiGet("api/admin/bloqueios") .then(function (r) { if (!r.ok) { setMsg($("bloqMsg"), "err", "Falha ao carregar bloqueios."); return; } var tb = $("bloqTbody"); tb.innerHTML = ""; var lista = r.d.bloqueios || []; if (!lista.length) { var tr = document.createElement("tr"); var td = document.createElement("td"); td.colSpan = 5; td.textContent = "Nenhum bloqueio manual."; td.style.color = "var(--muted)"; tr.appendChild(td); tb.appendChild(tr); return; } lista.forEach(function (b) { var tr = document.createElement("tr"); var tdc = function (t) { var x = document.createElement("td"); x.textContent = t; return x; }; tr.appendChild(tdc(HOSP_LABEL[b.hospedagem] || "Chácara Equilíbrio")); tr.appendChild(tdc(fmtData(b.checkin) + " → " + fmtData(b.checkout))); tr.appendChild(tdc(b.motivo || "—")); tr.appendChild(tdc(fmtDataHora(b.criadoEm))); var tdA = document.createElement("td"); var btn = document.createElement("button"); btn.className = "btn btn-sm btn-danger"; btn.type = "button"; btn.textContent = "Remover"; btn.addEventListener("click", function () { if (!window.confirm("Remover este bloqueio? As datas voltam a ficar disponíveis.")) return; apiMut("POST", "api/admin/bloqueios/" + encodeURIComponent(b.id) + "/remover", null) .then(function (res) { if (!res.ok) { setMsg($("bloqMsg"), "err", (res.d && res.d.erro) || "Falha."); return; } setMsg($("bloqMsg"), "ok", "Bloqueio removido."); carregarBloqueios(); }) .catch(function () { setMsg($("bloqMsg"), "err", "Erro de conexão."); }); }); tdA.appendChild(btn); tr.appendChild(tdA); tb.appendChild(tr); }); }) .catch(function () { setMsg($("bloqMsg"), "err", "Erro ao carregar bloqueios."); }); } $("bloqForm").addEventListener("submit", function (e) { e.preventDefault(); var body = { hospedagem: $("bloqHosp").value, checkin: $("bloqCheckin").value, checkout: $("bloqCheckout").value, motivo: $("bloqMotivo").value.trim() }; if (!body.checkin || !body.checkout) { setMsg($("bloqMsg"), "err", "Informe as datas."); return; } if (body.checkout <= body.checkin) { setMsg($("bloqMsg"), "err", "O check-out deve ser depois do check-in."); return; } apiMut("POST", "api/admin/bloqueios", body) .then(function (r) { if (!r.ok) { setMsg($("bloqMsg"), "err", (r.d && r.d.erro) || "Falha ao adicionar."); return; } setMsg($("bloqMsg"), "ok", "Bloqueio adicionado."); $("bloqForm").reset(); carregarBloqueios(); }) .catch(function () { setMsg($("bloqMsg"), "err", "Erro de conexão."); }); }); /* ---------- Mercado Pago ---------- */ var mpContaData = null; function setPill(el, conectado, texto) { if (!el) return; el.className = "mp-pill " + (conectado ? "on" : "off"); el.textContent = texto; } function carregarMpConta() { apiGet("api/admin/mp/conta").then(function (r) { if (!r.ok) return; var s = r.d; mpContaData = s; setPill($("mpPill"), s.configurado, s.configurado ? "Conectada" : "Não conectada"); $("mpNome").textContent = s.nome || "—"; $("mpEmail").textContent = s.email || "—"; $("mpContaId").textContent = s.contaId || "—"; $("mpAmbiente").textContent = s.configurado ? (s.ambiente === "sandbox" ? "Teste / Sandbox" : "Produção") : "—"; $("mpFonte").textContent = s.fonte || "—"; $("mpSecretStatus").textContent = s.temWebhookSecret ? "definido" : "não definido"; $("mpNotifStatus").textContent = s.temNotificationUrl ? "definida" : "não definida"; var t = s.ultimoTeste || {}; $("mpTesteStatus").textContent = t.em ? (t.ok ? "conexão ok em " : "falhou em ") + new Date(t.em).toLocaleString("pt-BR") + (t.erro ? " — " + t.erro : "") : "nunca realizado"; var info = $("mpInfo"); info.className = "msg info"; info.textContent = s.oauthHabilitado ? "OAuth do Mercado Pago habilitado: use “Conectar Mercado Pago” para autorizar a conta sem digitar credenciais. O access token nunca sai do servidor." : "Credenciais ficam armazenadas no servidor (nunca no navegador). Para usar o fluxo OAuth, informe o App ID (client_id) e o Client Secret em “Editar conta”. Se o MP_ACCESS_TOKEN estiver no ambiente, ele tem prioridade sobre o painel."; }).catch(function () { /* silencioso */ }); } function carregarMpResumo() { apiGet("api/admin/mp/resumo").then(function (r) { if (!r.ok) return; var d = r.d || {}; $("mpTotalRecebido").textContent = fmtBRL(d.totalRecebido || 0); $("mpAprovados").textContent = d.aprovados || 0; $("mpPendentes").textContent = d.pendentes || 0; $("mpRecusados").textContent = d.recusados || 0; $("mpReembolsos").textContent = d.reembolsos || 0; var tb = $("mpTransacoes"); var lista = d.ultimasTransacoes || []; if (!lista.length) { tb.innerHTML = 'Nenhuma transação ainda.'; return; } tb.innerHTML = lista.map(function (t) { var meio = t.meio ? (MEIO_LABEL[t.meio] || t.meio) : "—"; return "" + esc(t.id) + "" + esc(t.nome || "—") + "" + fmtBRL(t.valor) + "" + esc(STATUS[t.status] || t.status) + "" + esc(meio) + "" + esc(t.mpId || "—") + ""; }).join(""); }).catch(function () { /* silencioso */ }); } $("mpForm").addEventListener("submit", function (e) { e.preventDefault(); var map = { mpNomeInput: "nome", mpEmailInput: "email", mpContaIdInput: "contaId", mpToken: "accessToken", mpSecret: "webhookSecret", mpNotifUrl: "notificationUrl", mpPublicKey: "publicKey", mpClientId: "clientId", mpClientSecret: "clientSecret" }; var body = {}; Object.keys(map).forEach(function (id) { var v = $(id).value.trim(); if (v) body[map[id]] = v; }); if (!body.accessToken && !body.clientId && !body.clientSecret) { setMsg($("mpMsg"), "err", "Informe ao menos o access token (ou App ID + Client Secret para habilitar o OAuth)."); return; } var btn = e.target.querySelector("button[type=submit]"); btn.disabled = true; apiMut("POST", "api/admin/mp", body) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("mpMsg"), "err", (r.d && r.d.erro) || "Falha ao salvar."); return; } var txt = "Configuração salva. Mercado Pago: " + (r.d.fonte || "—") + "."; if (r.d.avisoEnv) txt += " " + r.d.avisoEnv; setMsg($("mpMsg"), "ok", txt); $("mpForm").reset(); $("mpFormCard").hidden = true; carregarMpConta(); carregarMpResumo(); }) .catch(function () { btn.disabled = false; setMsg($("mpMsg"), "err", "Erro de conexão."); }); }); $("mpEditar").addEventListener("click", function () { var c = mpContaData || {}; $("mpNomeInput").value = c.nome || ""; $("mpEmailInput").value = c.email || ""; $("mpContaIdInput").value = c.contaId || ""; $("mpFormCard").hidden = false; $("mpNomeInput").focus(); }); $("mpCancelar").addEventListener("click", function () { $("mpFormCard").hidden = true; $("mpForm").reset(); }); $("mpConectar").addEventListener("click", function () { if (!mpContaData || !mpContaData.oauthHabilitado) { setMsg($("mpMsg"), "info", "O fluxo OAuth precisa do App ID (client_id) e do Client Secret da sua aplicação Mercado Pago. Configure-os em “Editar conta”, ou cole o access token manualmente."); $("mpFormCard").hidden = false; $("mpClientId").focus(); return; } setMsg($("mpMsg"), "info", "Redirecionando para a autorização do Mercado Pago…"); apiGet("api/admin/mp/oauth/start") .then(function (r) { if (!r.ok) { setMsg($("mpMsg"), "err", (r.d && r.d.erro) || "Não foi possível iniciar o OAuth."); return; } window.location.href = r.d.url; }) .catch(function () { setMsg($("mpMsg"), "err", "Erro ao iniciar o OAuth."); }); }); $("mpDesconectar").addEventListener("click", function () { if (!window.confirm("Desconectar a conta do Mercado Pago? Os pagamentos ficarão desativados até uma nova conexão.")) return; var btn = $("mpDesconectar"); btn.disabled = true; apiMut("POST", "api/admin/mp/disconnect", {}) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("mpMsg"), "err", (r.d && r.d.erro) || "Não foi possível desconectar."); return; } setMsg($("mpMsg"), "info", (r.d && r.d.mensagem) || "Conta desconectada."); $("mpFormCard").hidden = true; carregarMpConta(); carregarMpResumo(); }) .catch(function () { btn.disabled = false; setMsg($("mpMsg"), "err", "Erro de conexão."); }); }); $("mpTestar").addEventListener("click", function () { var btn = $("mpTestar"); var tokenCampo = $("mpToken").value.trim(); btn.disabled = true; setMsg($("mpMsg"), "info", "Testando conexão com o Mercado Pago…"); apiMut("POST", "api/admin/mp/test", tokenCampo ? { accessToken: tokenCampo } : {}) .then(function (r) { btn.disabled = false; var d = r.d || {}; if (r.ok && d.ok) { setMsg($("mpMsg"), "ok", tokenCampo ? "Conexão ok: o token informado foi aceito pela API. Agora clique em Salvar configuração para guardá-lo." : "Conexão ok: a conta está ativa na API do Mercado Pago."); carregarMpConta(); } else { setMsg($("mpMsg"), "err", (d.erro) || "Falha na conexão."); } }) .catch(function () { btn.disabled = false; setMsg($("mpMsg"), "err", "Erro de conexão."); }); }); /* Feedback após o retorno do OAuth (admin recebe redirect com ?mpOauth=) */ (function () { var v = null; try { v = new URLSearchParams(location.search).get("mpOauth") || null; } catch (_) { v = null; } if (!v) return; setTimeout(function () { setMsg($("mpMsg"), v === "conectado" ? "ok" : "err", v === "conectado" ? "Conta do Mercado Pago conectada com sucesso." : "Não foi possível conectar a conta do Mercado Pago."); var tb = document.querySelector('[data-tab="mp"]'); if (tb) tb.click(); carregarMpConta(); carregarMpResumo(); }, 300); })(); /* ---------- E-mail ---------- */ function carregarEmail() { apiGet("api/admin/email") .then(function (r) { if (!r.ok) return; var s = r.d; var box = $("emailStatusBox"); box.hidden = false; $("emailEstado").textContent = s.configurado ? "Configurado" : "Não configurado (reservas ainda não enviam e-mail)"; $("emailFonte").textContent = s.fonte || "—"; $("emailSmtpHost").textContent = s.temSmtpHost ? $("emailHost").value || "definido" : "não definido"; $("emailSmtpPort").textContent = s.smtpPort + (s.smtpSecure ? " (TLS direta)" : " (STARTTLS)"); $("emailSmtpSenha").textContent = s.smtpSenhaDefinida ? "definida" : "não definida"; var t = s.ultimoTeste; $("emailTesteStatus").textContent = t.em ? (t.ok ? "envio ok em " : "falhou em ") + new Date(t.em).toLocaleString("pt-BR") + (t.erro ? " — " + t.erro : "") : "nunca realizado"; $("emailMsgBoasVindas").value = s.mensagemBoasVindas || ""; var info = $("emailInfo"); info.className = "msg info"; info.textContent = "Quando configurado, cada reserva envia 2 e-mails: notificação para o destinatário acima e confirmação com o resumo para o cliente. Falhas ficam registradas abaixo e podem ser reenviadas. A mensagem de boas-vindas do e-mail ao cliente pode ser personalizada no quadro abaixo; os dados da reserva são sempre gerados automaticamente. O e-mail de check-in é um modelo separado, enviado no dia do check-in."; }) .catch(function () { /* silencioso */ }); carregarModeloCheckin(); carregarEmailsLista(); carregarEmailFotos(); } function carregarModeloCheckin() { apiGet("api/admin/email/modelo-checkin") .then(function (r) { if (!r.ok || !r.d || !r.d.modelo) return; var m = r.d.modelo; $("emailAssuntoCheckin").value = m.assuntoCheckin || ""; $("emailMsgCheckin").value = m.mensagemCheckin || ""; $("emailRegrasCheckin").value = m.regrasCheckin || ""; }) .catch(function () { /* silencioso */ }); } function carregarEmailsLista() { apiGet("api/admin/emails") .then(function (r) { if (!r.ok) return; var tb = $("emailsTbody"); tb.innerHTML = ""; var lista = r.d.emails || []; if (!lista.length) { var td = document.createElement("td"); td.colSpan = 6; td.setAttribute("data-label", ""); td.textContent = "Nenhum e-mail enviado ainda."; td.style.color = "var(--muted)"; var tr = document.createElement("tr"); tr.className = "tbl-msg"; tr.appendChild(td); tb.appendChild(tr); return; } lista.forEach(function (e) { var tr = document.createElement("tr"); tr.innerHTML = "" + esc(fmtDataHora(e.em)) + "" + "" + esc(e.tipo || "—") + "" + "" + esc(e.para || "—") + "" + "" + esc((e.assunto || "—").slice(0, 60)) + "" + "" + (e.status === "enviado" ? "enviado" : 'falhou') + "" + "" + (e.reservaId ? '' : "") + ""; if (e.erro) { var tdErr = document.createElement("td"); tdErr.colSpan = 6; tdErr.setAttribute("data-label", ""); tdErr.textContent = "Erro: " + e.erro; tdErr.style.color = "var(--muted)"; tdErr.style.fontSize = "12px"; var tr2 = document.createElement("tr"); tr2.className = "tbl-msg"; tr2.appendChild(tdErr); tb.appendChild(tr2); } tb.appendChild(tr); }); Array.prototype.forEach.call(tb.querySelectorAll("[data-reenviar]"), function (b) { b.addEventListener("click", function () { var id = b.getAttribute("data-reenviar"); b.disabled = true; apiMut("POST", "api/admin/emails/" + encodeURIComponent(id) + "/reenviar", null) .then(function (r) { b.disabled = false; if (!r.ok) { setMsg($("emailMsg"), "err", (r.d && r.d.erro) || "Falha no reenvio."); return; } setMsg($("emailMsg"), "ok", "E-mail reenviado."); carregarEmailsLista(); }) .catch(function () { b.disabled = false; setMsg($("emailMsg"), "err", "Erro de conexão."); }); }); }); }) .catch(function () { /* silencioso */ }); } $("emailForm").addEventListener("submit", function (e) { e.preventDefault(); var body = { destinatarioReservas: $("emailDest").value.trim(), remetenteNome: $("emailRemNome").value.trim(), remetenteEmail: $("emailRem").value.trim(), smtpHost: $("emailHost").value.trim(), smtpPort: Number($("emailPort").value), smtpSecure: $("emailSecure").checked, smtpUsuario: $("emailUser").value.trim(), smtpSenha: $("emailPass").value.trim() }; if (!body.destinatarioReservas) { setMsg($("emailMsg"), "err", "Informe o e-mail destinatário das reservas."); return; } var btn = e.target.querySelector("button[type=submit]"); btn.disabled = true; apiMut("POST", "api/admin/email", body) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("emailMsg"), "err", (r.d && r.d.erro) || "Falha ao salvar."); return; } var txt = "Configuração salva. E-mail: " + (r.d.configurado ? "ativo." : "incompleto — preencha remetente e SMTP."); if (r.d.avisoEnv) txt += " " + r.d.avisoEnv; setMsg($("emailMsg"), "ok", txt); $("emailPass").value = ""; carregarEmail(); }) .catch(function () { btn.disabled = false; setMsg($("emailMsg"), "err", "Erro de conexão."); }); }); $("emailTestar").addEventListener("click", function () { var btn = $("emailTestar"); btn.disabled = true; setMsg($("emailMsg"), "info", "Enviando e-mail de teste…"); apiMut("POST", "api/admin/email/teste", {}) .then(function (r) { btn.disabled = false; var d = r.d || {}; if (r.ok && d.ok) { setMsg($("emailMsg"), "ok", "E-mail de teste enviado com sucesso para " + (d.para || "o destinatário configurado") + "."); carregarEmail(); } else { setMsg($("emailMsg"), "err", (d.erro) || "Falha no envio de teste."); carregarEmail(); } }) .catch(function () { btn.disabled = false; setMsg($("emailMsg"), "err", "Erro de conexão."); }); }); $("emailsAtualizar").addEventListener("click", carregarEmailsLista); /* ---------- Mensagem de boas-vindas ---------- */ $("emailSalvarMsg").addEventListener("click", function () { var btn = $("emailSalvarMsg"); btn.disabled = true; setMsg($("emailMsg"), "info", "Salvando mensagem de boas-vindas…"); apiMut("POST", "api/admin/email/mensagem", { acao: "salvar", mensagemBoasVindas: $("emailMsgBoasVindas").value }) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("emailMsg"), "err", (r.d && r.d.erro) || "Falha ao salvar."); return; } $("emailMsgBoasVindas").value = r.d.mensagemBoasVindas || ""; setMsg($("emailMsg"), "ok", "Mensagem de boas-vindas salva."); }) .catch(function () { btn.disabled = false; setMsg($("emailMsg"), "err", "Erro de conexão."); }); }); $("emailRestaurarMsg").addEventListener("click", function () { if (!window.confirm("Restaurar a mensagem padrão de boas-vindas?")) return; var btn = $("emailRestaurarMsg"); btn.disabled = true; setMsg($("emailMsg"), "info", "Restaurando mensagem padrão…"); apiMut("POST", "api/admin/email/mensagem", { acao: "restaurar" }) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("emailMsg"), "err", (r.d && r.d.erro) || "Falha ao restaurar."); return; } $("emailMsgBoasVindas").value = r.d.mensagemBoasVindas || ""; setMsg($("emailMsg"), "ok", "Mensagem padrão restaurada."); }) .catch(function () { btn.disabled = false; setMsg($("emailMsg"), "err", "Erro de conexão."); }); }); $("emailTestarMsg").addEventListener("click", function () { var btn = $("emailTestarMsg"); btn.disabled = true; setMsg($("emailMsg"), "info", "Enviando e-mail de teste com dados fictícios…"); apiMut("POST", "api/admin/email/teste-mensagem", {}) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("emailMsg"), "err", (r.d && r.d.erro) || "Falha no envio do e-mail de teste."); return; } setMsg($("emailMsg"), "ok", "E-mail de teste enviado para " + (r.d.para || "o destinatário configurado") + "."); }) .catch(function () { btn.disabled = false; setMsg($("emailMsg"), "err", "Erro de conexão."); }); }); /* ---------- Modelo do e-mail de check-in (independente do de boas-vindas) ---------- */ $("emailSalvarCheckin").addEventListener("click", function () { var btn = $("emailSalvarCheckin"); btn.disabled = true; setMsg($("emailMsg"), "info", "Salvando modelo de check-in…"); apiMut("POST", "api/admin/email/modelo-checkin", { assuntoCheckin: $("emailAssuntoCheckin").value.trim(), mensagemCheckin: $("emailMsgCheckin").value, regrasCheckin: $("emailRegrasCheckin").value, }) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("emailMsg"), "err", (r.d && r.d.erro) || "Falha ao salvar."); return; } carregarModeloCheckin(); setMsg($("emailMsg"), "ok", "Modelo de e-mail de check-in salvo."); }) .catch(function () { btn.disabled = false; setMsg($("emailMsg"), "err", "Erro de conexão."); }); }); $("emailRestaurarCheckin").addEventListener("click", function () { if (!window.confirm("Restaurar o modelo padrão de e-mail de check-in?")) return; var btn = $("emailRestaurarCheckin"); btn.disabled = true; setMsg($("emailMsg"), "info", "Restaurando modelo padrão…"); apiMut("POST", "api/admin/email/modelo-checkin", { acao: "restaurar" }) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("emailMsg"), "err", (r.d && r.d.erro) || "Falha ao restaurar."); return; } carregarModeloCheckin(); setMsg($("emailMsg"), "ok", "Modelo padrão de check-in restaurado."); }) .catch(function () { btn.disabled = false; setMsg($("emailMsg"), "err", "Erro de conexão."); }); }); $("emailTestarCheckin").addEventListener("click", function () { var btn = $("emailTestarCheckin"); btn.disabled = true; setMsg($("emailMsg"), "info", "Enviando e-mail de teste de check-in (somente o modelo de check-in)…"); apiMut("POST", "api/admin/email/teste-mensagem", { modelo: "checkin", hospedagem: "chacara" }) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("emailMsg"), "err", (r.d && r.d.erro) || "Falha no envio do e-mail de teste."); return; } setMsg($("emailMsg"), "ok", "E-mail de teste de check-in enviado para " + (r.d.para || "o destinatário configurado") + "."); }) .catch(function () { btn.disabled = false; setMsg($("emailMsg"), "err", "Erro de conexão."); }); }); /* ---------- Imagens do e-mail de confirmação (por empreendimento) ---------- */ var EF_HOSP = "chacara"; var EF_DATA = { chacara: null, cabana: null }; function carregarEmailFotos() { apiGet("api/admin/email/fotos") .then(function (r) { if (!r.ok) { setMsg($("emailFotosMsg"), "err", "Falha ao carregar as imagens do e-mail."); return; } EF_DATA.chacara = r.d.chacara || null; EF_DATA.cabana = r.d.cabana || null; renderEmailFotos(); }) .catch(function () { setMsg($("emailFotosMsg"), "err", "Erro ao carregar as imagens do e-mail."); }); } function efHosp() { return EF_DATA[EF_HOSP] || { tituloCheckin: "", textoCheckin: "", tituloBebidas: "", textoBebidas: "", legendasPadrao: [], imagens: [null, null, null, null] }; } function efNome() { return EF_HOSP === "cabana" ? "Nossa Cabana" : "Chácara"; } function efPadraoLegenda(i) { var d = efHosp(); return (d.legendasPadrao && d.legendasPadrao[i]) || ("Imagem " + (i + 1)); } function renderEmailFotos() { var body = $("emailFotosBody"); if (!body) return; Array.prototype.forEach.call(document.querySelectorAll("#emailFotosTabs .seg-btn"), function (b) { var ativo = b.getAttribute("data-ef-hosp") === EF_HOSP; b.classList.toggle("active", ativo); b.setAttribute("aria-selected", ativo ? "true" : "false"); }); var d = efHosp(); body.innerHTML = ""; var campos = document.createElement("div"); campos.className = "fields"; campos.innerHTML = '
    ' + '
    ' + '
    ' + '
    '; body.appendChild(campos); $("efTituloCheckin").value = d.tituloCheckin || ""; $("efTextoCheckin").value = d.textoCheckin || ""; $("efTituloBebidas").value = d.tituloBebidas || ""; $("efTextoBebidas").value = d.textoBebidas || ""; var grid = document.createElement("div"); grid.className = "ef-grid"; for (var i = 0; i < 4; i++) grid.appendChild(efSlot(i)); body.appendChild(grid); } function efSlot(i) { var d = efHosp(); var slot = d.imagens[i]; var card = document.createElement("div"); card.className = "ef-slot"; var head = document.createElement("div"); head.className = "ef-slot-head"; head.textContent = "Imagem " + (i + 1); card.appendChild(head); if (slot && slot.arquivo) { var img = document.createElement("img"); img.className = "ef-preview"; img.alt = "Pré-visualização da imagem " + (i + 1); img.src = slot.url || slot.arquivo; card.appendChild(img); var lab = document.createElement("div"); lab.className = "field"; lab.style.marginTop = "8px"; lab.innerHTML = ""; lab.querySelector("input").value = slot.legenda || ""; card.appendChild(lab); var acoes = document.createElement("div"); acoes.className = "row-actions"; acoes.innerHTML = '' + '' + '' + "" + '' + '' + '' + ""; card.appendChild(acoes); } else { var add = document.createElement("div"); add.className = "ef-add"; add.innerHTML = ''; card.appendChild(add); } return card; } function abrirArquivoImagem(pos, substituir) { var input = document.createElement("input"); input.type = "file"; input.accept = "image/jpeg,image/png,image/webp,image/avif,image/gif"; input.style.display = "none"; document.body.appendChild(input); input.addEventListener("change", function () { var f = input.files && input.files[0]; input.remove(); if (!f) return; var leitor = new FileReader(); leitor.onload = function () { var d = efHosp(); var atual = d.imagens[pos]; if (substituir && atual) { atual.arquivo = leitor.result; } else { d.imagens[pos] = { arquivo: leitor.result, legenda: efPadraoLegenda(pos) }; } renderEmailFotos(); }; leitor.readAsDataURL(f); }); input.click(); } var efBody = $("emailFotosBody"); if (efBody) { efBody.addEventListener("click", function (e) { var t = e.target; if (!t || !t.closest) return; t = t.closest("[data-ef-add],[data-ef-sub],[data-ef-del],[data-ef-move]"); if (!t) return; if (t.hasAttribute("data-ef-add")) { abrirArquivoImagem(Number(t.getAttribute("data-ef-add")), false); } else if (t.hasAttribute("data-ef-sub")) { abrirArquivoImagem(Number(t.getAttribute("data-ef-sub")), true); } else if (t.hasAttribute("data-ef-del")) { efHosp().imagens[Number(t.getAttribute("data-ef-del"))] = null; renderEmailFotos(); } else if (t.hasAttribute("data-ef-move")) { var p = Number(t.getAttribute("data-ef-pos")); var dir = Number(t.getAttribute("data-ef-move")); var imgs = efHosp().imagens; var q = p + dir; if (q < 0 || q > 3) return; var tmp = imgs[p]; imgs[p] = imgs[q]; imgs[q] = tmp; renderEmailFotos(); } }); efBody.addEventListener("change", function (e) { var t = e.target; if (!t || !t.classList || !t.classList.contains("ef-legenda")) return; var s = efHosp().imagens[Number(t.getAttribute("data-ef-pos"))]; if (s) s.legenda = t.value; }); } Array.prototype.forEach.call(document.querySelectorAll("#emailFotosTabs .seg-btn"), function (b) { b.addEventListener("click", function () { EF_HOSP = b.getAttribute("data-ef-hosp") || "chacara"; clearMsg($("emailFotosMsg")); renderEmailFotos(); }); }); $("emailFotosSalvar").addEventListener("click", function () { var d = efHosp(); var slots = d.imagens.map(function (s) { if (!s) return null; var o = { legenda: s.legenda || "" }; if (s.id && typeof s.arquivo === "string" && s.arquivo.indexOf("data:image/") !== 0) { o.id = s.id; } else { o.arquivo = s.arquivo; } return o; }); var body = { hospedagem: EF_HOSP, tituloCheckin: $("efTituloCheckin").value.trim(), textoCheckin: $("efTextoCheckin").value.trim(), tituloBebidas: $("efTituloBebidas").value.trim(), textoBebidas: $("efTextoBebidas").value.trim(), slots: slots }; var btn = $("emailFotosSalvar"); btn.disabled = true; setMsg($("emailFotosMsg"), "info", "Salvando imagens do e-mail…"); apiMut("POST", "api/admin/email/fotos/lote", body) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("emailFotosMsg"), "err", (r.d && r.d.erro) || "Falha ao salvar."); return; } EF_DATA[EF_HOSP] = r.d.empreendimento; renderEmailFotos(); setMsg($("emailFotosMsg"), "ok", "Seções e imagens do e-mail de check-in salvas para " + efNome() + "."); }) .catch(function () { btn.disabled = false; setMsg($("emailFotosMsg"), "err", "Erro de conexão."); }); }); $("emailFotosTestar").addEventListener("click", function () { var btn = $("emailFotosTestar"); btn.disabled = true; setMsg($("emailFotosMsg"), "info", "Enviando e-mail de teste de check-in…"); apiMut("POST", "api/admin/email/teste-mensagem", { modelo: "checkin", hospedagem: EF_HOSP }) .then(function (r) { btn.disabled = false; if (!r.ok) { setMsg($("emailFotosMsg"), "err", (r.d && r.d.erro) || "Falha no envio do e-mail de teste."); return; } setMsg($("emailFotosMsg"), "ok", "E-mail de teste de check-in enviado com as imagens de " + efNome() + "."); }) .catch(function () { btn.disabled = false; setMsg($("emailFotosMsg"), "err", "Erro de conexão."); }); }); /* ---------- Fotos da Nossa Cabana ---------- */ var FOTOS_CAT = []; var FOTOS_LISTA = []; function carregarFotos() { apiGet("api/admin/fotos") .then(function (r) { if (!r.ok) { setMsg($("fotosMsg"), "err", "Falha ao carregar fotos."); return; } FOTOS_CAT = r.d.categorias || []; FOTOS_LISTA = r.d.fotos || []; renderFotosSelect(); renderFotosGrid(); }) .catch(function () { setMsg($("fotosMsg"), "err", "Erro ao carregar fotos."); }); } function renderFotosSelect() { var sel = $("fotosCategoria"); sel.innerHTML = ""; FOTOS_CAT.forEach(function (c) { var o = document.createElement("option"); o.value = c; o.textContent = c; sel.appendChild(o); }); } function fotosMsgErro(d) { return (d && d.erro) || "Não foi possível concluir."; } function renderFotosGrid() { var grid = $("fotosGrid"); grid.innerHTML = ""; if (!FOTOS_LISTA.length) { var p = document.createElement("p"); p.textContent = "Nenhuma foto cadastrada ainda."; p.style.color = "var(--muted)"; grid.appendChild(p); return; } var porCat = {}; FOTOS_LISTA.forEach(function (f) { (porCat[f.categoria] = porCat[f.categoria] || []).push(f); }); FOTOS_CAT.forEach(function (cat) { var fotos = porCat[cat]; if (!fotos || !fotos.length) return; var sec = document.createElement("div"); sec.style.marginBottom = "28px"; var head = document.createElement("h3"); head.style.cssText = "font-size:14px;color:var(--forest);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px;border-bottom:1px solid var(--line);padding-bottom:8px;"; head.textContent = cat + " (" + fotos.length + ")"; sec.appendChild(head); var wrap = document.createElement("div"); wrap.style.cssText = "display:flex;flex-wrap:wrap;gap:16px;"; fotos.forEach(function (f) { wrap.appendChild(cardFoto(f)); }); sec.appendChild(wrap); grid.appendChild(sec); }); } function cardFoto(f) { var card = document.createElement("div"); card.style.cssText = "width:220px;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--card);display:flex;flex-direction:column;"; card.setAttribute("data-id", f.id); var img = document.createElement("img"); img.src = f.url; img.alt = f.legenda || f.categoria || "Foto da Nossa Cabana"; img.title = "Clique para prévia em tela cheia"; img.style.cssText = "width:100%;aspect-ratio:4/3;object-fit:cover;display:block;cursor:zoom-in;background:#e7e3d9;"; img.addEventListener("click", function () { window.open(f.url, "_blank", "noopener"); }); card.appendChild(img); var badge = document.createElement("div"); badge.style.cssText = "display:flex;justify-content:space-between;align-items:center;padding:8px 10px 0;"; var nome = document.createElement("span"); nome.textContent = f.principal ? "★ Principal" : "Foto"; nome.style.cssText = f.principal ? "font-size:11px;color:#8a5a10;font-weight:600;" : "font-size:11px;color:var(--muted);"; badge.appendChild(nome); var cont = document.createElement("span"); cont.textContent = f.ordem !== undefined ? "#" + (f.ordem + 1) : ""; cont.style.cssText = "font-size:11px;color:var(--muted);"; badge.appendChild(cont); card.appendChild(badge); var corpo = document.createElement("div"); corpo.style.cssText = "padding:8px 10px 10px;display:flex;flex-direction:column;gap:8px;"; var sel = document.createElement("select"); sel.setAttribute("aria-label", "Categoria da foto"); sel.style.cssText = "width:100%;padding:6px 8px;font-size:13px;border:1px solid var(--line);border-radius:6px;background:var(--white);color:var(--ink);"; FOTOS_CAT.forEach(function (c) { var o = document.createElement("option"); o.value = c; o.textContent = c; if (c === f.categoria) o.selected = true; sel.appendChild(o); }); sel.addEventListener("change", function () { atualizarFoto(f.id, { categoria: sel.value }); }); corpo.appendChild(sel); var leg = document.createElement("input"); leg.type = "text"; leg.value = f.legenda || ""; leg.maxLength = 120; leg.placeholder = "Legenda (opcional)"; leg.setAttribute("aria-label", "Legenda da foto"); leg.style.cssText = "width:100%;padding:6px 8px;font-size:13px;border:1px solid var(--line);border-radius:6px;background:var(--white);color:var(--ink);"; var timer = null; leg.addEventListener("input", function () { clearTimeout(timer); timer = setTimeout(function () { atualizarFoto(f.id, { legenda: leg.value }); }, 700); }); corpo.appendChild(leg); var botoes = document.createElement("div"); botoes.style.cssText = "display:flex;gap:6px;flex-wrap:wrap;"; function btn(txt, titulo, fn) { var b = document.createElement("button"); b.type = "button"; b.textContent = txt; b.title = titulo || ""; b.style.cssText = "flex:1;min-width:0;padding:5px 6px;font-size:12px;border:1px solid var(--line);border-radius:6px;background:var(--white);color:var(--ink);cursor:pointer;"; b.addEventListener("click", fn); return b; } if (!f.principal) { botoes.appendChild(btn("★ Principal", "Definir como foto principal", function () { atualizarFoto(f.id, { principal: true }); })); } botoes.appendChild(btn("↑", "Mover para cima", function () { reordenarFoto(f.id, "cima"); })); botoes.appendChild(btn("↓", "Mover para baixo", function () { reordenarFoto(f.id, "baixo"); })); var sub = btn("Substituir", "Substituir o arquivo por um novo upload", function () { fileSub.click(); }); botoes.appendChild(sub); var fileSub = document.createElement("input"); fileSub.type = "file"; fileSub.accept = "image/jpeg,image/png,image/webp,image/avif"; fileSub.style.display = "none"; fileSub.addEventListener("change", function () { var arq = fileSub.files && fileSub.files[0]; if (!arq) return; lerArquivoBase64(arq, function (dataUrl) { substituirFoto(f.id, dataUrl); }); fileSub.value = ""; }); botoes.appendChild(fileSub); if (pode("excluir_dados")) { botoes.appendChild(btn("Excluir", "Excluir esta foto", function () { excluirFoto(f.id); })); } corpo.appendChild(botoes); card.appendChild(corpo); return card; } function lerArquivoBase64(arq, cb) { var leitor = new FileReader(); leitor.onload = function () { cb(leitor.result); }; leitor.onerror = function () { setMsg($("fotosMsg"), "err", "Não foi possível ler o arquivo."); }; leitor.readAsDataURL(arq); } function atualizarFoto(id, d) { apiMut("POST", "api/admin/fotos/" + encodeURIComponent(id), d) .then(function (r) { if (!r.ok) { setMsg($("fotosMsg"), "err", fotosMsgErro(r.d)); return; } carregarFotos(); }) .catch(function () { setMsg($("fotosMsg"), "err", "Erro de conexão."); }); } function reordenarFoto(id, dir) { apiMut("POST", "api/admin/fotos/" + encodeURIComponent(id) + "/reordenar", { dir: dir }) .then(function (r) { if (!r.ok) { setMsg($("fotosMsg"), "err", fotosMsgErro(r.d)); return; } carregarFotos(); }) .catch(function () { setMsg($("fotosMsg"), "err", "Erro de conexão."); }); } function substituirFoto(id, dataUrl) { setMsg($("fotosMsg"), "ok", "Enviando nova imagem..."); apiMut("POST", "api/admin/fotos/" + encodeURIComponent(id) + "/substituir", { arquivo: dataUrl }) .then(function (r) { if (!r.ok) { setMsg($("fotosMsg"), "err", fotosMsgErro(r.d)); return; } setMsg($("fotosMsg"), "ok", "Foto substituída."); carregarFotos(); }) .catch(function () { setMsg($("fotosMsg"), "err", "Erro ao substituir."); }); } function excluirFoto(id) { if (!window.confirm("Excluir esta foto permanentemente? Esta ação não pode ser desfeita.")) return; apiMut("POST", "api/admin/fotos/" + encodeURIComponent(id) + "/excluir") .then(function (r) { if (!r.ok) { setMsg($("fotosMsg"), "err", fotosMsgErro(r.d)); return; } setMsg($("fotosMsg"), "ok", "Foto excluída."); carregarFotos(); }) .catch(function () { setMsg($("fotosMsg"), "err", "Erro ao excluir."); }); } $("fotosAtualizar").addEventListener("click", carregarFotos); $("fotosEnviar").addEventListener("click", function () { var arq = $("fotosArquivo").files && $("fotosArquivo").files[0]; if (!arq) { setMsg($("fotosMsg"), "err", "Selecione uma imagem para enviar."); return; } var categoria = $("fotosCategoria").value; var legenda = $("fotosLegenda").value.trim(); var principal = $("fotosPrincipal").checked; setMsg($("fotosMsg"), "ok", "Enviando foto..."); lerArquivoBase64(arq, function (dataUrl) { apiMut("POST", "api/admin/fotos", { arquivo: dataUrl, categoria: categoria, legenda: legenda, principal: principal }) .then(function (r) { if (!r.ok) { setMsg($("fotosMsg"), "err", fotosMsgErro(r.d)); return; } setMsg($("fotosMsg"), "ok", "Foto enviada."); $("fotosArquivo").value = ""; $("fotosLegenda").value = ""; $("fotosPrincipal").checked = false; carregarFotos(); }) .catch(function () { setMsg($("fotosMsg"), "err", "Erro ao enviar a foto."); }); }); }); /* ---------- Logs ---------- */ function carregarLogs() { apiGet("api/admin/logs") .then(function (r) { if (!r.ok) { return; } renderLogs($("logsPag"), r.d.pagamentos || []); renderLogs($("logsSync"), r.d.sincronizacao || []); renderLogs($("logsAcesso"), r.d.acessos || []); }) .catch(function () { /* silencioso */ }); } function renderLogs(el, lista) { el.innerHTML = ""; if (!lista.length) { var p = document.createElement("p"); p.textContent = "Nenhum registro ainda."; p.style.color = "var(--muted)"; el.appendChild(p); return; } lista.forEach(function (l) { var div = document.createElement("div"); div.className = "log-line"; var head = document.createElement("div"); head.className = "lg-head"; var tm = document.createElement("time"); tm.textContent = fmtDataHora(l.em); var tp = document.createElement("span"); tp.className = "lg-tipo" + (/erro|falha|falhou|bloqueado|recusada|cancelada|expirada|estornada/.test(String(l.tipo || "")) ? " erro" : ""); tp.textContent = l.tipo || "—"; head.appendChild(tm); head.appendChild(tp); div.appendChild(head); Object.keys(l).forEach(function (k) { if (k === "em" || k === "tipo") return; var v = (typeof l[k] === "object" ? JSON.stringify(l[k]) : String(l[k] == null ? "" : l[k])); if (v === "" || v === "{}" || v === "null" || v === "undefined") return; var row = document.createElement("div"); row.className = "lg-campo"; var kk = document.createElement("span"); kk.className = "lg-k"; kk.textContent = k; var vv = document.createElement("span"); vv.className = "lg-v"; vv.textContent = v; row.appendChild(kk); row.appendChild(vv); div.appendChild(row); }); el.appendChild(div); }); } $("logsAtualizar").addEventListener("click", carregarLogs); /* ---------- Senha ---------- */ $("senhaForm").addEventListener("submit", function (e) { e.preventDefault(); var nova = $("senhaNova").value; if (nova.length < 8) { setMsg($("senhaMsg"), "err", "A nova senha deve ter ao menos 8 caracteres."); return; } apiMut("POST", "api/admin/senha", { senhaAtual: $("senhaAtual").value, senhaNova: nova }) .then(function (r) { if (!r.ok) { setMsg($("senhaMsg"), "err", (r.d && r.d.erro) || "Falha ao alterar."); return; } setMsg($("senhaMsg"), "ok", "Senha alterada com sucesso."); $("senhaForm").reset(); }) .catch(function () { setMsg($("senhaMsg"), "err", "Erro de conexão."); }); }); /* ---------- Usuários e Permissões (exclusivo do Master) ---------- */ var usCatPerm = []; var usLista = []; var usEditandoId = null; function carregarUsuarios() { apiGet("api/admin/usuarios") .then(function (r) { if (!r.ok) { setMsg($("usMsg"), "err", (r.d && r.d.erro) || "Falha ao carregar usuários."); return; } usCatPerm = r.d.permissoes || []; usLista = r.d.usuarios || []; renderUsPermissoes(); renderUsuarios(); }) .catch(function () { setMsg($("usMsg"), "err", "Erro de conexão."); }); } function renderUsPermissoes() { var box = $("usPerms"); box.innerHTML = ""; usCatPerm.forEach(function (p) { var lbl = document.createElement("label"); lbl.style.cssText = "display:flex;align-items:center;gap:6px;font-size:13px;font-weight:400;cursor:pointer;"; var cb = document.createElement("input"); cb.type = "checkbox"; cb.value = p.id; cb.style.width = "auto"; lbl.appendChild(cb); lbl.appendChild(document.createTextNode(p.rotulo)); box.appendChild(lbl); }); } function permsMarcadas() { var out = []; document.querySelectorAll("#usPerms input[type=checkbox]").forEach(function (cb) { if (cb.checked) out.push(cb.value); }); return out; } function limparFormUs() { $("usId").value = ""; $("usNome").value = ""; $("usEmail").value = ""; $("usTelefone").value = ""; $("usSenha").value = ""; $("usNivel").value = "baixo"; $("usAtivo").checked = true; document.querySelectorAll("#usPerms input[type=checkbox]").forEach(function (cb) { cb.checked = false; }); atualizarPermsField(); } function atualizarPermsField() { $("usPermsField").style.display = $("usNivel").value === "personalizado" ? "" : "none"; } $("usNivel").addEventListener("change", atualizarPermsField); $("usNovo").addEventListener("click", function () { usEditandoId = null; limparFormUs(); $("usFormCard").style.display = "block"; $("usNome").focus(); }); $("usCancelar").addEventListener("click", function () { $("usFormCard").style.display = "none"; usEditandoId = null; }); $("usAtualizar").addEventListener("click", carregarUsuarios); function renderUsuarios() { var tb = $("usTbody"); tb.innerHTML = ""; if (!usLista.length) { var tr = document.createElement("tr"); var td0 = document.createElement("td"); td0.colSpan = 6; td0.textContent = "Nenhum coanfitrião cadastrado."; td0.style.color = "var(--muted)"; tr.appendChild(td0); tb.appendChild(tr); return; } var rotuloNivel = { baixo: "Baixo", medio: "Médio", personalizado: "Personalizado" }; usLista.forEach(function (u) { var tr = document.createElement("tr"); var tdc = function (t) { var x = document.createElement("td"); x.textContent = t; return x; }; tr.appendChild(tdc(u.nome || "")); tr.appendChild(tdc(u.email || "")); var nivel = rotuloNivel[u.nivel] || u.nivel; tr.appendChild(tdc(nivel + (u.nivel === "personalizado" ? " (" + (u.permissoes || []).length + " perms.)" : ""))); var tdSt = document.createElement("td"); var badge = document.createElement("span"); badge.className = "badge " + (u.ativo === false ? "cancelada" : "confirmada"); badge.textContent = u.ativo === false ? "Desativado" : "Ativo"; tdSt.appendChild(badge); tr.appendChild(tdSt); tr.appendChild(tdc(u.ultimoAcesso ? fmtDataHora(u.ultimoAcesso) : "—")); var tdA = document.createElement("td"); var btn = function (txt, classe, fn) { var b = document.createElement("button"); b.className = "btn btn-sm " + classe; b.type = "button"; b.textContent = txt; b.style.margin = "2px"; b.addEventListener("click", fn); return b; }; tdA.appendChild(btn("Editar", "btn-ghost", function () { abrirEdicao(u); })); tdA.appendChild(btn(u.ativo === false ? "Ativar" : "Desativar", u.ativo === false ? "btn-primary" : "btn-danger", function () { alternarAtivo(u); })); tdA.appendChild(btn("Excluir", "btn-danger", function () { excluirUsuario(u); })); tr.appendChild(tdA); tb.appendChild(tr); }); } function abrirEdicao(u) { usEditandoId = u.id; $("usId").value = u.id; $("usNome").value = u.nome || ""; $("usEmail").value = u.email || ""; $("usTelefone").value = u.telefone || ""; $("usSenha").value = ""; $("usNivel").value = u.nivel || "baixo"; $("usAtivo").checked = u.ativo !== false; document.querySelectorAll("#usPerms input[type=checkbox]").forEach(function (cb) { cb.checked = (u.permissoes || []).indexOf(cb.value) > -1; }); atualizarPermsField(); $("usFormCard").style.display = "block"; $("usNome").focus(); } $("usForm").addEventListener("submit", function (e) { e.preventDefault(); var body = { nome: $("usNome").value.trim(), email: $("usEmail").value.trim(), telefone: $("usTelefone").value.trim(), nivel: $("usNivel").value, ativo: $("usAtivo").checked }; if ($("usSenha").value) body.senha = $("usSenha").value; if (body.nivel === "personalizado") body.permissoes = permsMarcadas(); var rota = usEditandoId ? "api/admin/usuarios/" + encodeURIComponent(usEditandoId) : "api/admin/usuarios"; apiMut("POST", rota, body) .then(function (r) { if (!r.ok) { setMsg($("usMsg"), "err", (r.d && r.d.erro) || "Falha ao salvar."); return; } setMsg($("usMsg"), "ok", usEditandoId ? "Coanfitrião atualizado." : "Coanfitrião criado."); $("usFormCard").style.display = "none"; usEditandoId = null; carregarUsuarios(); }) .catch(function () { setMsg($("usMsg"), "err", "Erro de conexão."); }); }); function alternarAtivo(u) { apiMut("POST", "api/admin/usuarios/" + encodeURIComponent(u.id), { ativo: u.ativo === false }) .then(function (r) { if (!r.ok) { setMsg($("usMsg"), "err", (r.d && r.d.erro) || "Falha."); return; } setMsg($("usMsg"), "ok", u.ativo === false ? "Coanfitrião ativado." : "Coanfitrião desativado — o acesso foi revogado imediatamente."); carregarUsuarios(); }) .catch(function () { setMsg($("usMsg"), "err", "Erro de conexão."); }); } function excluirUsuario(u) { if (!window.confirm("Excluir o coanfitrião " + u.nome + " (" + u.email + ")? O acesso será revogado imediatamente.")) return; apiMut("DELETE", "api/admin/usuarios/" + encodeURIComponent(u.id), null) .then(function (r) { if (!r.ok) { setMsg($("usMsg"), "err", (r.d && r.d.erro) || "Falha ao excluir."); return; } setMsg($("usMsg"), "ok", "Coanfitrião excluído — o acesso foi revogado."); carregarUsuarios(); }) .catch(function () { setMsg($("usMsg"), "err", "Erro de conexão."); }); } /* ---------- Chat com hóspedes ---------- */ var chatConversas = []; var chatAbertaId = null; var chatSse = null; var STATUS_LABEL = { aberta: "Aberta", em_atendimento: "Em atendimento", encerrada: "Encerrada" }; function chatTempo(iso) { if (!iso) return ""; var d = new Date(iso); if (isNaN(d.getTime())) return ""; var agora = new Date(); var diff = (agora - d) / 1000; if (diff < 60) return "agora"; if (diff < 3600) return Math.floor(diff / 60) + " min"; if (diff < 86400) return Math.floor(diff / 3600) + " h"; return d.toLocaleDateString("pt-BR"); } function chatDia(iso) { var d = new Date(iso); if (isNaN(d.getTime())) return ""; return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" }); } function setChatBadge() { var b = $("chatTabBadge"); var total = 0; chatConversas.forEach(function (c) { if (c.naoLidas && c.naoLidas > 0) total += c.naoLidas; }); if (total > 0) { b.textContent = total > 9 ? "9+" : String(total); b.hidden = false; } else b.hidden = true; } function carregarChat() { apiGet("api/admin/chat/conversas") .then(function (r) { if (!r.ok) { setMsg($("chatMsg"), "err", (r.d && r.d.erro) || "Não foi possível carregar o chat."); return; } chatConversas = (r.d.conversas || []).slice(); setChatBadge(); renderChatLista(); }) .catch(function () { /* best-effort */ }); } function renderChatLista() { var el = $("chatLista"); var st = $("chatFiltroStatus").value; var hs = $("chatFiltroHosp").value; var lista = chatConversas.filter(function (c) { if (st && c.status !== st) return false; if (hs && c.hospedagem !== hs) return false; return true; }); if (!lista.length) { el.innerHTML = '
    Nenhuma conversa encontrada.
    '; return; } lista.sort(function (a, b) { return (b.atualizadoEm || "").localeCompare(a.atualizadoEm || ""); }); var html = ""; lista.forEach(function (c) { var ult = c.ultimaMensagem || ""; var autor = ""; if (c.ultimaDe === "admin") autor = "Você: "; else if (c.ultimaDe === "sistema") autor = "Sistema: "; html += '"; }); el.innerHTML = html; Array.prototype.forEach.call(el.querySelectorAll("[data-chat-id]"), function (b) { b.addEventListener("click", function () { abrirChat(b.getAttribute("data-chat-id")); }); }); } function abrirChat(id) { chatAbertaId = id; renderChatLista(); setMsg($("chatMsg"), "", ""); apiGet("api/admin/chat/conversas/" + encodeURIComponent(id)) .then(function (r) { if (!r.ok) { setMsg($("chatMsg"), "err", (r.d && r.d.erro) || "Não foi possível abrir a conversa."); return; } renderChatDetalhe(r.d.conversa); apiMut("POST", "api/admin/chat/conversas/" + encodeURIComponent(id) + "/lida", {}).then(function () { carregarChat(); }); }) .catch(function () { /* best-effort */ }); } function renderChatDetalhe(c) { $("chatDetNome").innerHTML = esc(c.cliente && c.cliente.nome ? c.cliente.nome : "Cliente") + '' + STATUS_LABEL[c.status] + ""; var meta = ""; if (c.cliente) { meta += "E-mail: " + esc(c.cliente.email || "–"); if (c.cliente.telefone) meta += " · Tel: " + esc(c.cliente.telefone); } meta += "
    Hospedagem: " + esc(c.hospedagem === "cabana" ? "Nossa Cabana" : "Chácara Equilíbrio"); if (c.reservaVinculada && c.reserva) { meta += "
    Reserva: " + esc(c.reserva.codigo || c.reserva.id || "") + " · " + esc(fmtData(c.reserva.checkin) + " → " + fmtData(c.reserva.checkout)) + " · " + esc(c.reserva.nome || c.reserva.pagador || "") + (c.reserva.status ? " · " + (STATUS[c.reserva.status] || c.reserva.status) : ""); } $("chatDetMeta").innerHTML = meta; renderChatThread(c); $("chatDetResponde").hidden = c.status === "encerrada"; $("chatDetInput").value = ""; $("chatDetInput").style.height = "auto"; var st = $("chatDetStatus"); var btns = ""; [["aberta", "Marcar aberta"], ["em_atendimento", "Em atendimento"], ["encerrada", "Encerrar"]].forEach(function (p) { btns += '"; }); st.innerHTML = btns; Array.prototype.forEach.call(st.querySelectorAll("[data-status]"), function (b) { b.addEventListener("click", function () { var novo = b.getAttribute("data-status"); if (novo === c.status) return; apiMut("POST", "api/admin/chat/conversas/" + encodeURIComponent(c.id) + "/status", { status: novo }) .then(function (r) { if (!r.ok) { setMsg($("chatMsg"), "err", (r.d && r.d.erro) || "Não foi possível alterar o status."); return; } carregarChat(); abrirChat(c.id); }) .catch(function () { setMsg($("chatMsg"), "err", "Erro de conexão."); }); }); }); } function renderChatThread(c) { var el = $("chatDetThread"); el.innerHTML = ""; var ultimoDia = ""; (c.mensagens || []).forEach(function (m) { var dia = chatDia(m.em); if (dia && dia !== ultimoDia) { ultimoDia = dia; var d = document.createElement("div"); d.className = "chat-dia"; d.textContent = dia; el.appendChild(d); } var b = document.createElement("div"); b.className = "chat-bubble " + esc(m.de); b.textContent = m.texto; if (m.de !== "sistema") { var meta = document.createElement("span"); meta.className = "chat-bubble-meta"; meta.textContent = fmtDataHora(m.em) + (m.de === "admin" && m.autor ? " · " + esc(m.autor) : ""); b.appendChild(meta); } el.appendChild(b); }); el.scrollTop = el.scrollHeight; } function enviarRespostaChat() { if (!chatAbertaId) return; var input = $("chatDetInput"); var texto = input.value.trim(); if (!texto) return; input.value = ""; input.style.height = "auto"; apiMut("POST", "api/admin/chat/conversas/" + encodeURIComponent(chatAbertaId) + "/mensagens", { texto: texto }) .then(function (r) { if (!r.ok) { setMsg($("chatMsg"), "err", (r.d && r.d.erro) || "Não foi possível enviar a resposta."); input.value = texto; return; } abrirChat(chatAbertaId); carregarChat(); }) .catch(function () { input.value = texto; setMsg($("chatMsg"), "err", "Erro de conexão."); }); } function conectarChatSSE() { if (chatSse || !pode("atender_chat")) return; try { chatSse = new EventSource("api/admin/eventos", { withCredentials: true }); chatSse.addEventListener("chat", function (ev) { var d; try { d = JSON.parse(ev.data); } catch (_) { return; } if (!d || !d.tipo) return; if (chatAbertaId && d.conversaId === chatAbertaId && (d.tipo === "nova_mensagem" || d.tipo === "status")) { abrirChat(chatAbertaId); } else if (d.tipo === "nova_conversa" || d.tipo === "nova_mensagem") { carregarChat(); } }); chatSse.onerror = function () { if (chatSse) { try { chatSse.close(); } catch (_) {} } chatSse = null; setTimeout(conectarChatSSE, 8000); }; } catch (_) { chatSse = null; } } $("chatAtualizar").addEventListener("click", function () { carregarChat(); }); $("chatFiltroStatus").addEventListener("change", renderChatLista); $("chatFiltroHosp").addEventListener("change", renderChatLista); $("chatDetSend").addEventListener("click", enviarRespostaChat); $("chatDetInput").addEventListener("keydown", function (e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); enviarRespostaChat(); } }); $("chatDetInput").addEventListener("input", function () { var el = $("chatDetInput"); el.style.height = "auto"; el.style.height = Math.min(el.scrollHeight, 120) + "px"; }); /* SSE do chat: conecta quando o painel está visível e o usuário tem permissão */ var chatSseIniciado = false; function iniciarChatSse() { if (chatSseIniciado || !pode("atender_chat")) return; chatSseIniciado = true; conectarChatSSE(); } setTimeout(iniciarChatSse, 1500); })();