feat(master): unificar planos em um unico bloco com modal de novo plano e icones de editar/excluir
This commit is contained in:
@@ -131,9 +131,10 @@ public final class MasterHandlers implements HttpHandler {
|
||||
else HttpResponses.methodNotAllowed(ex);
|
||||
return;
|
||||
}
|
||||
if (parts.length == 2 && "planos".equals(parts[0]) && "PUT".equals(method)) {
|
||||
atualizarPlano(ex, parseId(parts[1]));
|
||||
return;
|
||||
if (parts.length == 2 && "planos".equals(parts[0])) {
|
||||
if ("PUT".equals(method)) { atualizarPlano(ex, parseId(parts[1])); return; }
|
||||
else if ("DELETE".equals(method)) { excluirPlano(ex, parseId(parts[1])); return; }
|
||||
else { HttpResponses.methodNotAllowed(ex); return; }
|
||||
}
|
||||
if (parts.length == 1 && "logs-envio".equals(parts[0]) && "GET".equals(method)) {
|
||||
listarLogsEnvio(ex);
|
||||
@@ -526,6 +527,28 @@ public final class MasterHandlers implements HttpHandler {
|
||||
|
||||
private void criarPlano(HttpExchange ex) throws Exception { salvarPlano(ex, null); }
|
||||
private void atualizarPlano(HttpExchange ex, long id) throws Exception { salvarPlano(ex, id); }
|
||||
private void excluirPlano(HttpExchange ex, long id) throws Exception {
|
||||
try (Connection conn = dataSourceProvider.getMasterConnection()) {
|
||||
conn.setAutoCommit(false);
|
||||
if (exists(conn, "SELECT 1 FROM assinaturas WHERE plano_id = ?", id)) {
|
||||
try (PreparedStatement ps = conn.prepareStatement("UPDATE planos SET ativo=false WHERE id=?")) {
|
||||
ps.setLong(1, id);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
} else {
|
||||
try (PreparedStatement ps = conn.prepareStatement("DELETE FROM plano_funcionalidades WHERE plano_id=?")) {
|
||||
ps.setLong(1, id);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
try (PreparedStatement ps = conn.prepareStatement("DELETE FROM planos WHERE id=?")) {
|
||||
ps.setLong(1, id);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
conn.commit();
|
||||
HttpResponses.json(ex, 200, Map.of("sucesso", true));
|
||||
}
|
||||
}
|
||||
|
||||
private void salvarPlano(HttpExchange ex, Long id) throws Exception {
|
||||
String raw = rawBody(ex);
|
||||
@@ -669,7 +692,7 @@ public final class MasterHandlers implements HttpHandler {
|
||||
private int diaVencimento(Connection conn,long empresaId)throws SQLException{Map<String,Object>a=one(conn,"SELECT dia_vencimento FROM assinaturas WHERE empresa_id=? AND status='ativa' ORDER BY id DESC LIMIT 1",List.of(empresaId));if(a==null||a.get("dia_vencimento")==null)return 7;int dia=((Number)a.get("dia_vencimento")).intValue();return Math.max(1,Math.min(28,dia));}
|
||||
|
||||
private long inserirPlano(Connection c, Map<String,String>b)throws SQLException{try(PreparedStatement ps=c.prepareStatement("INSERT INTO planos (nome,valor_mensal_centavos,descricao,ativo) VALUES (?,?,?,true) RETURNING id")){ps.setString(1,required(b,"nome"));ps.setLong(2,Long.parseLong(required(b,"valor_mensal_centavos")));ps.setString(3,b.get("descricao"));try(ResultSet rs=ps.executeQuery()){rs.next();return rs.getLong(1);}}}
|
||||
private long atualizarPlanoRow(Connection c,long id,Map<String,String>b)throws SQLException{try(PreparedStatement ps=c.prepareStatement("UPDATE planos SET nome=COALESCE(?,nome), valor_mensal_centavos=COALESCE(?,valor_mensal_centavos), descricao=COALESCE(?,descricao) WHERE id=?")){ps.setString(1,b.get("nome")); if(b.get("valor_mensal_centavos")==null)ps.setNull(2,Types.BIGINT);else ps.setLong(2,Long.parseLong(b.get("valor_mensal_centavos"))); ps.setString(3,b.get("descricao")); ps.setLong(4,id); ps.executeUpdate(); return id;}}
|
||||
private long atualizarPlanoRow(Connection c,long id,Map<String,String>b)throws SQLException{String sql="UPDATE planos SET nome=COALESCE(?,nome), valor_mensal_centavos=COALESCE(?,valor_mensal_centavos), descricao=COALESCE(?,descricao)"+(b.containsKey("ativo")?", ativo=?":"")+" WHERE id=?";try(PreparedStatement ps=c.prepareStatement(sql)){ps.setString(1,b.get("nome")); if(b.get("valor_mensal_centavos")==null)ps.setNull(2,Types.BIGINT);else ps.setLong(2,Long.parseLong(b.get("valor_mensal_centavos"))); ps.setString(3,b.get("descricao")); int idx=4; if(b.containsKey("ativo")){ps.setBoolean(idx++,Boolean.parseBoolean(b.get("ativo")));} ps.setLong(idx,id); ps.executeUpdate(); return id;}}
|
||||
private void sincronizarPlanoFuncionalidades(Connection c,long planoId,List<String> chaves)throws SQLException{try(PreparedStatement del=c.prepareStatement("DELETE FROM plano_funcionalidades WHERE plano_id=?")){del.setLong(1,planoId);del.executeUpdate();}try(PreparedStatement ins=c.prepareStatement("INSERT INTO plano_funcionalidades (plano_id,funcionalidade_id) SELECT ?, id FROM funcionalidades WHERE chave=?")){for(String chave:chaves){ins.setLong(1,planoId);ins.setString(2,chave);ins.addBatch();}ins.executeBatch();}}
|
||||
private void copiarFuncionalidadesPlano(Connection c,long empresaId,long planoId)throws SQLException{try(PreparedStatement ps=c.prepareStatement("INSERT INTO empresa_funcionalidades (empresa_id,funcionalidade_id,ativa) SELECT ?, funcionalidade_id, true FROM plano_funcionalidades WHERE plano_id=? ON CONFLICT (empresa_id,funcionalidade_id) DO UPDATE SET ativa=true")){ps.setLong(1,empresaId);ps.setLong(2,planoId);ps.executeUpdate();}}
|
||||
private void salvarAssinatura(Connection c,long empresaId,long planoId)throws SQLException{Map<String,Object> p=one(c,"SELECT valor_mensal_centavos FROM planos WHERE id=?",List.of(planoId));if(p==null)throw new IllegalArgumentException("Plano nao encontrado");try(PreparedStatement ps=c.prepareStatement("UPDATE assinaturas SET status='inativa' WHERE empresa_id=? AND status='ativa'")){ps.setLong(1,empresaId);ps.executeUpdate();}try(PreparedStatement ps=c.prepareStatement("INSERT INTO assinaturas (empresa_id,plano_id,valor_mensal_centavos,dia_vencimento,status) VALUES (?,?,?,?, 'ativa')")){ps.setLong(1,empresaId);ps.setLong(2,planoId);ps.setLong(3,((Number)p.get("valor_mensal_centavos")).longValue());ps.setInt(4,7);ps.executeUpdate();}}
|
||||
|
||||
@@ -1 +1,83 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Planos</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Planos</h1></div><section class="panel"><h2>Novo plano</h2><form id="plano-form" class="row"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>Valor em centavos</label><input name="valor_mensal_centavos" type="number" required></div><div class="field"><label>Descricao</label><input name="descricao"></div><button class="btn">Salvar</button></form></section><section class="panel"><h2>Planos cadastrados</h2><div class="table-wrap"><table><thead><tr><th>ID</th><th>Nome</th><th>Valor</th><th>Descricao</th><th>Status</th></tr></thead><tbody id="rows"></tbody></table></div></section></main><script>Layout.mount('master','planos');</script><script src="/master/planos.js"></script></body></html>
|
||||
<!doctype html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Planos</title>
|
||||
<link rel="stylesheet" href="/css/app.css">
|
||||
<script src="/js/auth.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/layout.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="topbar">
|
||||
<h1>Planos</h1>
|
||||
</div>
|
||||
<section class="panel">
|
||||
<div class="section-head">
|
||||
<h2>Planos cadastrados</h2>
|
||||
<div class="section-actions">
|
||||
<button class="header-add-btn" type="button" onclick="abrirModalPlano()" title="Novo plano" aria-label="Novo plano">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nome</th>
|
||||
<th>Valor</th>
|
||||
<th>Descrição</th>
|
||||
<th>Status</th>
|
||||
<th style="text-align: right;">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Modal de Plano (Popup) -->
|
||||
<div id="plano-modal" class="modal hidden">
|
||||
<div class="modal-backdrop" onclick="fecharModalPlano()"></div>
|
||||
<section class="modal-panel">
|
||||
<div class="modal-head">
|
||||
<h2 id="plano-modal-title">Novo plano</h2>
|
||||
<button class="btn secondary" type="button" onclick="fecharModalPlano()">Fechar</button>
|
||||
</div>
|
||||
<form id="plano-form" class="grid cols">
|
||||
<div class="field">
|
||||
<label>Nome do plano</label>
|
||||
<input name="nome" required placeholder="Ex: Básico, Profissional, Premium">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Valor em centavos</label>
|
||||
<input name="valor_mensal_centavos" type="number" required placeholder="Ex: 19990 (= R$ 199,90)">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Descrição</label>
|
||||
<input name="descricao" placeholder="Descrição do plano">
|
||||
</div>
|
||||
<div class="field" id="campo-status-plano" style="display: none;">
|
||||
<label>Status</label>
|
||||
<select name="ativo" id="plano-ativo">
|
||||
<option value="true">Ativo</option>
|
||||
<option value="false">Inativo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-actions" style="grid-column: 1 / -1;">
|
||||
<button class="btn secondary" type="button" onclick="fecharModalPlano()">Cancelar</button>
|
||||
<button class="btn" type="submit">Salvar plano</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<script>Layout.mount('master','planos');</script>
|
||||
<script src="/master/planos.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +1,105 @@
|
||||
const form=document.querySelector('#plano-form'),rows=document.querySelector('#rows');async function load(){try{const d=await Api.get('/api/master/planos');rows.innerHTML=(d.planos||[]).map(p=>`<tr><td>${p.id}</td><td>${p.nome}</td><td>${Util.money(p.valor_mensal_centavos)}</td><td>${p.descricao||''}</td><td>${Util.badge(p.ativo?'ativo':'inativo')}</td></tr>`).join('')}catch(e){Util.toast(e.message,true)}}form.onsubmit=async e=>{e.preventDefault();const b=Util.formData(form);b.funcionalidades=[];try{await Api.post('/api/master/planos',b);form.reset();Util.toast('Plano salvo');load()}catch(err){Util.toast(err.message,true)}};load();
|
||||
const form = document.querySelector('#plano-form');
|
||||
const rows = document.querySelector('#rows');
|
||||
const modal = document.querySelector('#plano-modal');
|
||||
const modalTitle = document.querySelector('#plano-modal-title');
|
||||
const campoStatus = document.querySelector('#campo-status-plano');
|
||||
let planos = [];
|
||||
let planoEditandoId = null;
|
||||
|
||||
const Icons = {
|
||||
edit: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>',
|
||||
trash: '<svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>'
|
||||
};
|
||||
|
||||
function esc(v){
|
||||
return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]));
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const d = await Api.get('/api/master/planos');
|
||||
planos = d.planos || [];
|
||||
render();
|
||||
} catch (e) {
|
||||
Util.toast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
rows.innerHTML = planos.length ? planos.map(p => `<tr>
|
||||
<td>${p.id}</td>
|
||||
<td>${esc(p.nome)}</td>
|
||||
<td>${Util.money(p.valor_mensal_centavos)}</td>
|
||||
<td>${esc(p.descricao || '-')}</td>
|
||||
<td>${Util.badge(p.ativo ? 'ativo' : 'inativo')}</td>
|
||||
<td style="text-align: right;">
|
||||
<div class="table-actions">
|
||||
<button class="table-icon-btn" type="button" onclick="editarPlano(${p.id})" title="Editar plano" aria-label="Editar plano">${Icons.edit}</button>
|
||||
<button class="table-icon-btn danger" type="button" onclick="excluirPlano(${p.id})" title="Excluir plano" aria-label="Excluir plano">${Icons.trash}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`).join('') : '<tr><td colspan="6">Nenhum plano cadastrado.</td></tr>';
|
||||
}
|
||||
|
||||
window.abrirModalPlano = () => {
|
||||
planoEditandoId = null;
|
||||
form.reset();
|
||||
modalTitle.textContent = 'Novo plano';
|
||||
campoStatus.style.display = 'none';
|
||||
modal?.classList.remove('hidden');
|
||||
form.querySelector('input[name="nome"]')?.focus();
|
||||
};
|
||||
|
||||
window.fecharModalPlano = () => {
|
||||
modal?.classList.add('hidden');
|
||||
};
|
||||
|
||||
window.editarPlano = (id) => {
|
||||
const p = planos.find(x => Number(x.id) === Number(id));
|
||||
if (!p) return;
|
||||
planoEditandoId = p.id;
|
||||
form.reset();
|
||||
modalTitle.textContent = 'Editar plano';
|
||||
form.querySelector('input[name="nome"]').value = p.nome || '';
|
||||
form.querySelector('input[name="valor_mensal_centavos"]').value = p.valor_mensal_centavos ?? '';
|
||||
form.querySelector('input[name="descricao"]').value = p.descricao || '';
|
||||
form.querySelector('#plano-ativo').value = p.ativo ? 'true' : 'false';
|
||||
campoStatus.style.display = 'block';
|
||||
modal?.classList.remove('hidden');
|
||||
form.querySelector('input[name="nome"]')?.focus();
|
||||
};
|
||||
|
||||
window.excluirPlano = async (id) => {
|
||||
const p = planos.find(x => Number(x.id) === Number(id));
|
||||
const nome = p ? ` "${p.nome}"` : '';
|
||||
if (!await Util.confirm(`Tem certeza que deseja excluir o plano${nome}?`, 'Excluir plano')) return;
|
||||
try {
|
||||
await Api.request(`/api/master/planos/${id}`, { method: 'DELETE' });
|
||||
Util.toast('Plano excluído com sucesso');
|
||||
load();
|
||||
} catch (err) {
|
||||
Util.toast(err.message, true);
|
||||
}
|
||||
};
|
||||
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const b = Util.formData(form);
|
||||
b.funcionalidades = [];
|
||||
try {
|
||||
if (planoEditandoId) {
|
||||
await Api.put(`/api/master/planos/${planoEditandoId}`, b);
|
||||
Util.toast('Plano atualizado com sucesso');
|
||||
} else {
|
||||
await Api.post('/api/master/planos', b);
|
||||
Util.toast('Plano cadastrado com sucesso');
|
||||
}
|
||||
form.reset();
|
||||
fecharModalPlano();
|
||||
load();
|
||||
} catch (err) {
|
||||
Util.toast(err.message, true);
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
Reference in New Issue
Block a user