fix(master): visibilidade de mensagens de erro e prevencao de slug duplicado

This commit is contained in:
Tiago
2026-10-10 17:16:37 -03:00
parent a143df50ea
commit bff0edf8cb
4 changed files with 79 additions and 14 deletions
@@ -397,7 +397,7 @@ public final class MasterHandlers implements HttpHandler {
String dbName = tenantProvisioner.databaseNameFromSlug(slug);
try (Connection conn = dataSourceProvider.getMasterConnection()) {
if (exists(conn, "SELECT 1 FROM empresas WHERE slug = ?", slug.replace('-', '_'))) {
HttpResponses.json(ex, 409, Map.of("erro", "slug_duplicado"));
HttpResponses.json(ex, 409, Map.of("erro", "slug_duplicado", "mensagem", "Este Slug ja esta cadastrado em outro cliente."));
return;
}
if (exists(conn, "SELECT 1 FROM funcionarios WHERE email = ?", emailPrincipal)) {
+6 -4
View File
@@ -633,21 +633,23 @@ th {
position: fixed;
left: 50%;
transform: translateX(-50%);
z-index: 20;
z-index: 1000;
max-width: min(720px, calc(100vw - 24px));
border-radius: var(--radius);
padding: 12px 14px;
box-shadow: var(--shadow-md);
padding: 12px 18px;
box-shadow: var(--shadow-lg);
font-weight: 600;
}
.toast {
bottom: 18px;
bottom: 24px;
background: #17202a;
color: #fff;
}
.toast.error {
background: var(--danger);
border: 1px solid rgba(255, 255, 255, .2);
}
.finance-banner {
+1
View File
@@ -57,6 +57,7 @@
<button class="btn secondary" type="button" onclick="fecharModalCadastro()">Fechar</button>
</div>
<form id="empresa-form" class="grid cols">
<div id="modal-error-msg" class="hidden" style="grid-column: 1 / -1; padding: 12px 16px; background: var(--danger-soft); color: var(--danger); border: 1px solid var(--danger); border-radius: var(--radius); font-size: 14px; font-weight: 600;"></div>
<div class="field">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label style="margin: 0;">Slug</label>
+71 -9
View File
@@ -16,12 +16,22 @@ function statusAction(e){const acao=e.status==='ativa'?'Bloquear':'Ativar';retur
function detalheRow(e){if(selecionado!==e.id||!modo)return'';if(modo==='ver')return `<tr><td colspan="5"><div class="inline-panel"><h2>${esc(e.nome)}</h2><p><strong>Slug:</strong> ${esc(e.slug)}</p><p><strong>CNPJ:</strong> ${esc(e.cnpj||'-')}</p><p><strong>Status:</strong> ${esc(e.status)}</p><p><strong>Plano:</strong> ${e.plano?`${esc(e.plano)} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</p><div class="field"><label>API Key de integração</label><input readonly value="${esc(e.api_key||'')}" onclick="this.select()"></div><div class="row compact"><button class="btn secondary" type="button" onclick="copiarApiKey(${e.id})">Copiar API Key</button><button class="btn secondary" type="button" onclick="solicitarAlteracaoApiKey(${e.id})">Solicitar alteração</button></div><p><strong>Conta principal:</strong> abra Editar para alterar dados da empresa; funcionarios ficam no acesso da empresa.</p><button class="btn secondary" onclick="fecharDetalhe()">Fechar</button></div></td></tr>`;return `<tr><td colspan="5"><form class="inline-panel grid cols" onsubmit="salvarEdicao(event,${e.id})"><div class="field"><label>Nome</label><input name="nome" value="${esc(e.nome||'')}" required></div><div class="field"><label>CNPJ</label><input name="cnpj" value="${esc(e.cnpj||'')}"></div><div class="field"><label>Plano</label><select name="plano_id">${planoOptions(e.plano_id)}</select></div><div class="row"><button class="btn">Salvar</button><button type="button" class="btn secondary" onclick="fecharDetalhe()">Cancelar</button></div></form></td></tr>`}
function proximoSlugSequencial(){
let max=0;
for(const e of empresas){
const n=parseInt(e.slug,10);
if(!isNaN(n)&&n>max)max=n;
const slugsExistentes = new Set(empresas.map(e => String(e.slug || '').trim().toLowerCase()));
let max = 0;
for (const e of empresas) {
const s = String(e.slug || '').trim();
const n = parseInt(s, 10);
if (!isNaN(n) && /^\d+$/.test(s)) {
if (n > max) max = n;
}
}
return String(max+1).padStart(4,'0');
let proximo = max + 1;
let candidato = String(proximo).padStart(4, '0');
while (slugsExistentes.has(candidato.toLowerCase())) {
proximo++;
candidato = String(proximo).padStart(4, '0');
}
return candidato;
}
function slugify(text){
@@ -34,6 +44,25 @@ function slugify(text){
.replace(/^_+|_+$/g,'');
}
function mostrarErroModal(msg){
const el = document.querySelector('#modal-error-msg');
if(el){
el.textContent = msg;
el.classList.remove('hidden');
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
function limparErroModal(){
const el = document.querySelector('#modal-error-msg');
if(el){
el.textContent = '';
el.classList.add('hidden');
}
const slugInput = empresaForm.querySelector('input[name="slug"]');
if(slugInput) slugInput.style.borderColor = '';
}
function render(){
const q=busca.value.trim();
const lista=empresas.filter(e=>match(e,q));
@@ -67,6 +96,7 @@ window.alternarTipoSlug=()=>{
const slugInput=empresaForm.querySelector('input[name="slug"]');
const nomeInput=empresaForm.querySelector('input[name="nome"]');
const btn=document.querySelector('#btn-slug-tipo');
limparErroModal();
if(modoSlugAuto==='sequencial'){
modoSlugAuto='nome';
if(btn)btn.textContent='Usar sequencial';
@@ -79,6 +109,7 @@ window.alternarTipoSlug=()=>{
};
window.abrirModalCadastro=()=>{
limparErroModal();
cadastroModal?.classList.remove('hidden');
const slugInput=empresaForm.querySelector('input[name="slug"]');
const btn=document.querySelector('#btn-slug-tipo');
@@ -86,11 +117,13 @@ window.abrirModalCadastro=()=>{
if(btn)btn.textContent='Gerar do nome';
if(slugInput){
slugInput.value=proximoSlugSequencial();
slugInput.style.borderColor='';
}
empresaForm.querySelector('input[name="nome"]')?.focus();
};
window.fecharModalCadastro=()=>{
limparErroModal();
cadastroModal?.classList.add('hidden');
};
@@ -112,20 +145,49 @@ nomeInput?.addEventListener('input',()=>{
}
});
empresaForm.querySelector('input[name="slug"]')?.addEventListener('input',()=>{
limparErroModal();
});
empresaForm.onsubmit=async e=>{
e.preventDefault();
limparErroModal();
const slugInput=empresaForm.querySelector('input[name="slug"]');
if(slugInput&&!slugInput.value.trim()){
if(!slugInput) return;
if(!slugInput.value.trim()){
slugInput.value=proximoSlugSequencial();
}
const slugValor = slugInput.value.trim().toLowerCase();
const duplicado = empresas.find(emp => String(emp.slug || '').trim().toLowerCase() === slugValor);
if(duplicado){
const msg = `O Slug "${slugInput.value}" já está cadastrado no cliente "${duplicado.nome}". Por favor, utilize o próximo código sugerido (${proximoSlugSequencial()}).`;
mostrarErroModal(msg);
slugInput.style.borderColor = 'var(--danger)';
slugInput.focus();
Util.toast(msg, true);
return;
}
try{
await Api.post('/api/master/empresas',Util.formData(empresaForm));
empresaForm.reset();
fecharModalCadastro();
Util.toast('Cliente cadastrado');
load();
Util.toast('Cliente cadastrado com sucesso!');
await load();
}catch(err){
Util.toast(err.message,true);
let msg = err.message || 'Erro ao cadastrar cliente';
if(msg.includes('slug_duplicado')){
msg = `O Slug "${slugInput.value}" já está em uso por outro cliente. Altere para outro código ou clique em "Usar sequencial".`;
slugInput.style.borderColor = 'var(--danger)';
slugInput.focus();
}else if(msg.includes('email_duplicado')){
msg = 'Este e-mail de login já está cadastrado no sistema.';
empresaForm.querySelector('input[name="email"]')?.focus();
}
mostrarErroModal(msg);
Util.toast(msg, true);
}
};