1import { api } from '../api.js'; 2import { esc, BRL } from '../utils.js'; 3import { toast } from '../ui/toast.js'; 4import { modalShow, closeModal } from '../ui/modal.js'; 5import { isMaster } from '../state.js'; 6 7export async function viewConfig() { 8 const [cfg, planosRes] = await Promise.all([ 9 api.get('/config'), 10 api.get('/planos'), 11 ]); 12 const planos = planosRes.items || planosRes; 13 setTimeout(() => bind(planos), 0); 14 15 return ` 16 <div class="card"><div class="hd"><h3>Configurações gerais</h3></div> 17 <div class="bd" style="padding:18px"> 18 <div class="field"><label>Dia de corte (inclusão/cancelamento)</label> 19 <input id="cf_dia_corte" type="number" min="1" max="28" value="${esc(cfg.dia_corte || '15')}"></div> 20 <div class="field"><label>WhatsApp Central (com DDI, só dÃgitos)</label> 21 <input id="cf_wa_central" value="${esc(cfg.whatsapp_central || '')}"></div> 22 <div class="field"><label>WhatsApp Financeiro</label> 23 <input id="cf_wa_fin" value="${esc(cfg.whatsapp_financeiro || '')}"></div> 24 <div class="field"><label>Nome da Central</label> 25 <input id="cf_nome" value="${esc(cfg.nome_central || 'Mais Saúde Card')}"></div> 26 <button class="btn pri" id="cf_save">Salvar</button> 27 </div> 28 </div> 29 30 <div class="card"><div class="hd"><h3>ð¬ Envios WhatsApp</h3></div> 31 <div class="bd" style="padding:18px"> 32 <label style="display:flex;align-items:center;gap:10px;font-weight:normal;cursor:pointer"> 33 <input type="checkbox" id="cf_wa_ativo" ${cfg.wa_envios_ativo === '1' ? 'checked' : ''}> 34 <span><b>Envios WhatsApp ativos</b> (kill switch global)</span> 35 </label> 36 <p class="mini" style="margin-top:8px;color:var(--mut)"> 37 Quando desligado, nenhuma mensagem é enviada â nem automática de NF nem manual. 38 A empresa também precisa ter <b>wa_ativo=1</b> individualmente. 39 </p> 40 </div> 41 </div> 42 43 <div class="card"><div class="hd"><h3>âï¸ Envios Email</h3></div> 44 <div class="bd" style="padding:18px"> 45 <label style="display:flex;align-items:center;gap:10px;font-weight:normal;cursor:pointer"> 46 <input type="checkbox" id="cf_email_ativo" ${cfg.email_envios_ativo === '1' ? 'checked' : ''}> 47 <span><b>Envios Email ativos</b> (kill switch global)</span> 48 </label> 49 <p class="mini" style="margin-top:8px;color:var(--mut)"> 50 Quando desligado, nenhum email sai â nem automático nem manual. 51 A empresa também precisa ter <b>email_ativo=1</b> individualmente. 52 </p> 53 ${isMaster() ? '<button class="btn sm" id="cf_smtp_edit" style="margin-top:12px">â Configurar servidor SMTPâ¦</button>' : ''} 54 <div id="cf_smtp_status" class="mini" style="margin-top:8px;color:var(--mut)">Carregando status do SMTPâ¦</div> 55 </div> 56 </div> 57 58 <div class="card"><div class="hd"><h3>Planos do catálogo</h3><div class="sp"></div> 59 <span class="mini">${planos.length} plano${planos.length !== 1 ? 's' : ''} ativo${planos.length !== 1 ? 's' : ''}</span> 60 ${isMaster() ? '<button class="btn pri sm" data-novo-plano>+ Novo plano</button>' : ''} 61 </div> 62 <div class="bd" style="overflow:auto"><table> 63 <thead><tr> 64 <th>Nome</th><th>Tipo</th><th>Titular</th><th>Dep</th><th>Dep extra</th><th>Max deps</th><th class="noprint">Ações</th> 65 </tr></thead> 66 <tbody>${planos.map(p => ` 67 <tr${p.novo_bloqueado ? ' style="opacity:.65"' : ''}> 68 <td><b>${esc(p.nome)}</b>${p.novo_bloqueado ? ' <span class="bdg b-bad" style="font-size:10px;margin-left:6px">ð« Bloqueado para novas adesões</span>' : ''}</td> 69 <td><span class="bdg b-info">${esc(p.tipo)}</span></td> 70 <td><b>${BRL(p.valor_titular_padrao / 100)}</b></td> 71 <td>${p.permite_dependentes 72 ? (p.cobra_dependente ? `${BRL((p.valor_dependente_padrao || 0) / 100)}/dep` : 'grátis') 73 : 'â'}</td> 74 <td>${(p.valor_dep_extra || 0) > 0 ? `${BRL(p.valor_dep_extra / 100)} (acima do ${p.max_dependentes || 0}º)` : 'â'}</td> 75 <td>${p.max_dependentes ?? 'â'}</td> 76 <td class="noprint"> 77 ${isMaster() ? `<button class="btn sm" data-edit-plano="${p.id}">Editar</button>` : ''} 78 </td> 79 </tr>`).join('')} 80 </tbody> 81 </table></div> 82 </div> 83 <p class="mini">Alterações no plano afetam apenas <b>novas inscrições</b>. Pra recalcular inscrições existentes usa <b>Preços negociados</b> na ficha da empresa (aplica em todas as ATIVAS).</p> 84 `; 85} 86 87function bind(planos) { 88 document.getElementById('cf_save').onclick = async () => { 89 const updates = { 90 dia_corte: document.getElementById('cf_dia_corte').value, 91 whatsapp_central: document.getElementById('cf_wa_central').value, 92 whatsapp_financeiro: document.getElementById('cf_wa_fin').value, 93 nome_central: document.getElementById('cf_nome').value, 94 }; 95 try { 96 for (const [k, v] of Object.entries(updates)) { 97 await api.put('/config/' + k, { valor: v }); 98 } 99 toast('Configurações salvas'); 100 } catch (e) { toast('Erro: ' + e.message); } 101 }; 102 document.getElementById('cf_email_ativo')?.addEventListener('change', async (e) => { 103 const v = e.target.checked ? '1' : '0'; 104 try { 105 await api.put('/config/email_envios_ativo', { valor: v });
106 toast(v === '1' ? 'â Envios Email ATIVADOS' : '⸠Envios Email desligados'); 107 } catch (err) { 108 e.target.checked = !e.target.checked; 109 toast('Erro: ' + (err.body?.error || err.message)); 110 } 111 }); 112 document.getElementById('cf_smtp_edit')?.addEventListener('click', abrirModalSmtp); 113 atualizarStatusSmtp(); 114 document.getElementById('cf_wa_ativo')?.addEventListener('change', async (e) => { 115 const v = e.target.checked ? '1' : '0'; 116 try { 117 await api.put('/config/wa_envios_ativo', { valor: v }); 118 toast(v === '1' ? 'â Envios WhatsApp ATIVADOS' : '⸠Envios WhatsApp desligados'); 119 } catch (err) { 120 e.target.checked = !e.target.checked; 121 toast('Erro: ' + (err?.body?.error || err?.message || 'falha')); 122 } 123 }); 124 document.querySelector('[data-novo-plano]')?.addEventListener('click', () => modalPlano(null)); 125 document.querySelectorAll('[data-edit-plano]').forEach(el => { 126 el.addEventListener('click', () => { 127 const p = planos.find(x => x.id === +el.dataset.editPlano); 128 modalPlano(p); 129 }); 130 }); 131} 132 133function modalPlano(p) { 134 const novo = !p; 135 p = p || { nome: '', tipo: 'INDIVIDUAL', permite_dependentes: 0, max_dependentes: null, 136 cobra_dependente: 0, valor_titular_padrao: 0, valor_dependente_padrao: 0, valor_dep_extra: 0 }; 137 const brl = (v) => ((v || 0) / 100).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 138 139 modalShow(novo ? 'Novo plano' : `Editar: ${p.nome}`, ` 140 <div class="field"><label>Nome do plano *</label> 141 <input id="pl_nome" value="${esc(p.nome)}" placeholder="Ex: Familiar + Odonto"></div> 142 <div style="display:flex;gap:10px"> 143 <div class="field" style="flex:1"><label>Tipo *</label> 144 <select id="pl_tipo"> 145 <option value="INDIVIDUAL" ${p.tipo === 'INDIVIDUAL' ? 'selected' : ''}>Individual</option> 146 <option value="FAMILIAR" ${p.tipo === 'FAMILIAR' ? 'selected' : ''}>Familiar</option> 147 <option value="PREMIUM" ${p.tipo === 'PREMIUM' ? 'selected' : ''}>Premium</option> 148 </select> 149 </div> 150 <div class="field" style="flex:1"><label>Valor titular (R$) *</label> 151 <input id="pl_val_tit" type="text" inputmode="decimal" value="${brl(p.valor_titular_padrao)}"></div> 152 </div> 153 <hr style="margin:14px 0;border:0;border-top:1px solid var(--line)"> 154 <h4 style="margin-bottom:8px;font-size:13px">Dependentes e adesões</h4> 155 <div class="chk-grupo"> 156 <label class="chk-row"><input type="checkbox" id="pl_permite" ${p.permite_dependentes ? 'checked' : ''}> 157 <span><b>Permite dependentes</b><small>O titular pode incluir familiares neste plano.</small></span></label> 158 <label class="chk-row" id="pl_cobra_row" style="${p.permite_dependentes ? '' : 'display:none'}"><input type="checkbox" id="pl_cobra_dep" ${p.cobra_dependente ? 'checked' : ''}> 159 <span><b>Cobra por dependente (ilimitado)</b><small>Cada dependente paga o valor definido abaixo. Desmarcado: dependentes inclusos até o limite.</small></span></label> 160 <label class="chk-row"><input type="checkbox" id="pl_novo_bloq" ${p.novo_bloqueado ? 'checked' : ''}> 161 <span><b>ð« Bloquear novas adesões</b><small>Inscrições atuais continuam valendo; só impede criar novas.</small></span></label> 162 </div> 163 <div id="pl_dep_box" style="${p.permite_dependentes ? '' : 'display:none'};padding:10px;background:var(--azul-cl2, #F6F9FE);border-radius:6px"> 164 <div class="field"><label>Máx. deps inclusos <span class="mini" style="font-weight:400;text-transform:none">(deixe vazio = â)</span></label> 165 <input id="pl_max_deps" type="number" min="0" value="${p.max_dependentes ?? ''}"></div> 166 <div id="pl_val_dep_box" style="${p.cobra_dependente ? '' : 'display:none'}"> 167 <div class="field"><label>Valor cobrado por dep (R$)</label> 168 <input id="pl_val_dep" type="text" inputmode="decimal" value="${brl(p.valor_dependente_padrao)}"></div> 169 </div> 170 <div id="pl_val_extra_box" style="${!p.cobra_dependente ? '' : 'display:none'}"> 171 <div class="field"><label>Valor extra por dep acima do limite (R$) <span class="mini" style="font-weight:400;text-transform:none">(0 = não cobra)</span></label> 172 <input id="pl_val_extra" type="text" inputmode="decimal" value="${brl(p.valor_dep_extra)}"></div> 173 </div> 174 </div> 175 `, [ 176 { t: 'Cancelar', fn: closeModal }, 177 { t: novo ? 'Criar plano' : 'Salvar', cls: 'pri', fn: async () => { 178 const parseVal = (s) => Math.round(parseFloat((s || '').replace(/\./g, '').replace(',', '.') || '0') * 100); 179 const permite = document.getElementById('pl_permite').checked; 180 const cobra = permite && document.getElementById('pl_cobra_dep').checked; 181 const body = { 182 nome: document.getElementById('pl_nome').value.trim(), 183 tipo: document.getElementById('pl_tipo').value, 184 valor_titular_padrao: parseVal(document.getElementById('pl_val_tit').value), 185 permite_dependentes: permite ? 1 : 0, 186 max_dependentes: permite ? (parseInt(document.getElementById('pl_max_deps').value, 10) || null) : 0, 187 cobra_dependente: cobra ? 1 : 0, 188 valor_dependente_padrao: cobra ? parseVal(document.getElementById('pl_val_dep').value) : 0, 189 valor_dep_extra: (permite && !cobra) ? parseVal(document.getElementById('pl_val_extra').value) : 0, 190 novo_bloqueado: document.getElementById('pl_novo_bloq')?.checked ? 1 : 0, 191 }; 192 if (!body.nome) return toast('Nome obrigatório'); 193 if (body.valor_titular_padrao < 0) return toast('Valor titular inválido');
194 try { 195 if (novo) await api.post('/planos', body); 196 else await api.patch('/planos/' + p.id, body); 197 closeModal(); 198 toast(novo ? 'Plano criado' : 'Plano atualizado'); 199 import('../router.js').then(m => m.render()); 200 } catch (e) { 201 toast('Erro: ' + (e.body?.error || e.message)); 202 } 203 }}, 204 ]); 205 206 // Toggles 207 document.getElementById('pl_permite').onchange = (e) => { 208 document.getElementById('pl_dep_box').style.display = e.target.checked ? '' : 'none'; 209 document.getElementById('pl_cobra_row').style.display = e.target.checked ? '' : 'none'; 210 }; 211 document.getElementById('pl_cobra_dep').onchange = (e) => { 212 document.getElementById('pl_val_dep_box').style.display = e.target.checked ? '' : 'none'; 213 document.getElementById('pl_val_extra_box').style.display = e.target.checked ? 'none' : ''; 214 }; 215} 216 217// ----------------------------------------------------------------------------- 218// SMTP â master pode ver/editar servidor+usuário+senha+FROM sem que os valores 219// passem por logs ou repositório. Senha nunca sai do relay (endpoint só devolve 220// "smtp_pass_definida:true|false"). Deixar em branco no form = mantém a atual. 221// ----------------------------------------------------------------------------- 222 223async function atualizarStatusSmtp() { 224 const el = document.getElementById('cf_smtp_status'); 225 if (!el) return; 226 try { 227 const c = await api.get('/email/config'); 228 const ok = c.configurado; 229 el.innerHTML = ok 230 ? `<span style="color:var(--verde,#0f9d6c)">â Configurado</span> · ${esc(c.smtp_user || 'â')} via ${esc(c.smtp_host || 'â')}:${esc(String(c.smtp_port || ''))}` 231 : `<span style="color:var(--verm,#b23a26)">â Não configurado</span> â clique em "Configurar servidor SMTP" pra completar`; 232 } catch (e) { 233 el.innerHTML = `<span style="color:var(--verm)">â Erro ao consultar relay</span>`; 234 } 235} 236 237async function abrirModalSmtp() { 238 let cfg; 239 try { 240 cfg = await api.get('/email/config'); 241 } catch (e) { 242 toast('Não foi possÃvel conectar ao relay: ' + (e.body?.detalhes || e.message)); 243 return; 244 } 245 modalShow('Configuração do servidor SMTP', ` 246 <p class="mini" style="margin-bottom:14px;color:var(--mut)"> 247 Os valores são salvos no relay do VPS. A senha nunca é exibida â 248 <b>deixe em branco pra manter a atual</b>. 249 </p> 250 <div class="field"><label>Servidor SMTP</label> 251 <input id="sm_host" value="${esc(cfg.smtp_host || '')}" placeholder="mail.provedor.com.br"></div> 252 <div style="display:flex;gap:10px"> 253 <div class="field" style="flex:0 0 120px"><label>Porta</label> 254 <input id="sm_port" type="number" value="${esc(String(cfg.smtp_port || 465))}"></div> 255 <div class="field" style="flex:1"><label>Conexão</label> 256 <input value="${(String(cfg.smtp_port) === '465' ? 'SSL/TLS (465)' : 'STARTTLS (587)')}" disabled></div> 257 </div> 258 <div class="field"><label>Usuário / e-mail remetente</label> 259 <input id="sm_user" value="${esc(cfg.smtp_user || '')}" placeholder="[email protected]"></div> 260 <div class="field"><label>Senha ${cfg.smtp_pass_definida ? '<span class="mini" style="color:var(--verde,#0f9d6c)">(definida â deixe em branco pra manter)</span>' : '<span class="mini" style="color:var(--verm)">(ainda não definida)</span>'}</label> 261 <input id="sm_pass" type="password" autocomplete="new-password" placeholder="${cfg.smtp_pass_definida ? 'â¢â¢â¢â¢â¢â¢â¢â¢' : 'nova senha'}"></div> 262 <div class="field"><label>Nome do remetente (que aparece no email)</label> 263 <input id="sm_from" value="${esc(cfg.from_name || 'Mais Saúde Card')}" placeholder="Mais Saúde Card"></div> 264 <div id="sm_result" class="mini" style="margin-top:12px"></div> 265 `, [ 266 { t: 'Salvar e testar', cls: 'pri', fn: async () => { 267 const btn = document.querySelector('.modal .btn.pri'); 268 const res = document.getElementById('sm_result'); 269 const body = { 270 smtp_host: document.getElementById('sm_host').value.trim(), 271 smtp_port: document.getElementById('sm_port').value.trim(), 272 smtp_user: document.getElementById('sm_user').value.trim(), 273 smtp_pass: document.getElementById('sm_pass').value, // sem trim â senhas podem ter espaço 274 from_name: document.getElementById('sm_from').value.trim(), 275 }; 276 if (!body.smtp_host || !body.smtp_user) { 277 toast('Servidor e usuário são obrigatórios'); return; 278 } 279 if (btn) { btn.disabled = true; btn.textContent = 'Salvandoâ¦'; } 280 res.innerHTML = '<span style="color:var(--mut)">Salvandoâ¦</span>';
281 try { 282 await api.put('/email/config', body); 283 res.innerHTML = '<span style="color:var(--mut)">Testando conexão SMTPâ¦</span>'; 284 const verify = await api.post('/email/verify', {}); 285 if (verify.ok) { 286 res.innerHTML = '<span style="color:var(--verde,#0f9d6c)">â Conexão SMTP OK â servidor respondeu corretamente</span>'; 287 toast('SMTP configurado e testado'); 288 atualizarStatusSmtp(); 289 } else { 290 res.innerHTML = `<span style="color:var(--verm)">â Servidor recusou: ${esc(verify.detalhes || 'erro')}</span><br><span class="mini">Config foi salva. Ajuste e clique de novo em "Salvar e testar".</span>`; 291 } 292 } catch (e) { 293 res.innerHTML = `<span style="color:var(--verm)">â ${esc(e.body?.detalhes || e.message)}</span>`; 294 } finally { 295 if (btn) { btn.disabled = false; btn.textContent = 'Salvar e testar'; } 296 } 297 }}, 298 { t: 'Só testar', fn: async () => { 299 const res = document.getElementById('sm_result'); 300 res.innerHTML = '<span style="color:var(--mut)">Testando conexão SMTPâ¦</span>'; 301 try { 302 const verify = await api.post('/email/verify', {}); 303 if (verify.ok) res.innerHTML = '<span style="color:var(--verde,#0f9d6c)">â Conexão SMTP OK</span>'; 304 else res.innerHTML = `<span style="color:var(--verm)">â Servidor recusou: ${esc(verify.detalhes || 'erro')}</span>`; 305 } catch (e) { 306 res.innerHTML = `<span style="color:var(--verm)">â ${esc(e.body?.detalhes || e.message)}</span>`; 307 } 308 }}, 309 { t: 'Fechar', fn: closeModal }, 310 ]); 311}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.