From 4437e19be53274a337bfa7e2a393e1484584e075 Mon Sep 17 00:00:00 2001 From: Tiago Date: Sat, 10 Oct 2026 20:24:32 -0300 Subject: [PATCH] fix(i18n): corrigir acentuacoes da lingua portuguesa em textos e mensagens --- .../condocontab/auth/http/AuthHandlers.java | 4 +- .../master/http/MasterHandlers.java | 6 +- .../tenant/http/ClienteHandlers.java | 56 +++++++++---------- .../provisioning/TenantProvisioner.java | 6 +- .../webapp/cliente/condominio-detalhe.html | 2 +- src/main/webapp/cliente/condominio-detalhe.js | 10 ++-- src/main/webapp/cliente/condominios.html | 2 +- src/main/webapp/cliente/condominios.js | 22 ++++---- src/main/webapp/cliente/configuracoes.html | 2 +- src/main/webapp/cliente/configuracoes.js | 2 +- src/main/webapp/cliente/financeiro.html | 2 +- src/main/webapp/cliente/financeiro.js | 4 +- src/main/webapp/cliente/lembretes.html | 2 +- src/main/webapp/cliente/lembretes.js | 10 ++-- src/main/webapp/master/configuracoes.html | 2 +- src/main/webapp/master/configuracoes.js | 2 +- src/main/webapp/master/financeiro.html | 2 +- src/main/webapp/master/financeiro.js | 2 +- 18 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/main/java/com/seconectai/condocontab/auth/http/AuthHandlers.java b/src/main/java/com/seconectai/condocontab/auth/http/AuthHandlers.java index e4a56aa..11d89d7 100644 --- a/src/main/java/com/seconectai/condocontab/auth/http/AuthHandlers.java +++ b/src/main/java/com/seconectai/condocontab/auth/http/AuthHandlers.java @@ -77,12 +77,12 @@ public final class AuthHandlers { return; } if (!"ativo".equals(funcionario.status())) { - HttpResponses.json(exchange, 403, Map.of("erro", "cadastro_desativado", "mensagem", "Cadastro de funcionario desativado.")); + HttpResponses.json(exchange, 403, Map.of("erro", "cadastro_desativado", "mensagem", "Cadastro de funcionário desativado.")); return; } Empresa empresa = new EmpresaDao(conn).buscarPorId(funcionario.empresaId()).orElse(null); if (empresa == null || !("ativa".equals(empresa.status()) || "bloqueada".equals(empresa.status()))) { - HttpResponses.json(exchange, 403, Map.of("erro", "empresa_inativa", "mensagem", "Escritorio nao esta ativo.")); + HttpResponses.json(exchange, 403, Map.of("erro", "empresa_inativa", "mensagem", "Escritório não está ativo.")); return; } Map claims = new LinkedHashMap<>(); diff --git a/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java b/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java index 8cce172..5b7b6d0 100644 --- a/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java +++ b/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java @@ -398,11 +398,11 @@ 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", "mensagem", "Este Slug ja esta cadastrado em outro cliente.")); + HttpResponses.json(ex, 409, Map.of("erro", "slug_duplicado", "mensagem", "Este Slug já está cadastrado em outro cliente.")); return; } if (exists(conn, "SELECT 1 FROM funcionarios WHERE email = ?", emailPrincipal)) { - HttpResponses.json(ex, 409, Map.of("erro", "email_duplicado", "mensagem", "Este e-mail ja esta cadastrado como login.")); + HttpResponses.json(ex, 409, Map.of("erro", "email_duplicado", "mensagem", "Este e-mail já está cadastrado como login.")); return; } EmpresaDao dao = new EmpresaDao(conn); @@ -713,7 +713,7 @@ public final class MasterHandlers implements HttpHandler { private Map body(HttpExchange ex)throws IOException{return SimpleJson.parseObject(rawBody(ex));} private String rawBody(HttpExchange ex)throws IOException{return new String(ex.getRequestBody().readAllBytes(),StandardCharsets.UTF_8);} private Map query(HttpExchange ex){Map q=new HashMap<>();String raw=ex.getRequestURI().getRawQuery();if(raw==null)return q;for(String p:raw.split("&")){String[]kv=p.split("=",2);q.put(URLDecoder.decode(kv[0],StandardCharsets.UTF_8),kv.length>1?URLDecoder.decode(kv[1],StandardCharsets.UTF_8):"");}return q;} - private String required(Map m,String k){String v=m.get(k);if(v==null||v.isBlank())throw new IllegalArgumentException("Campo obrigatorio: "+k);return v;} + private String required(Map m,String k){String v=m.get(k);if(v==null||v.isBlank())throw new IllegalArgumentException("Campo obrigatório: "+k);return v;} private Long optionalLong(String v){return v==null||v.isBlank()?null:Long.parseLong(v);} private long parseId(String s){return Long.parseLong(s);} private boolean exists(Connection c,String sql,String value)throws SQLException{try(PreparedStatement ps=c.prepareStatement(sql)){ps.setString(1,value);try(ResultSet rs=ps.executeQuery()){return rs.next();}}} diff --git a/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java b/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java index e95fa9c..6298906 100644 --- a/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java +++ b/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java @@ -50,11 +50,11 @@ public final class ClienteHandlers implements HttpHandler { String[] p=Arrays.stream(path.substring("/api".length()).split("/")).filter(s->!s.isBlank()).map(s->URLDecoder.decode(s,StandardCharsets.UTF_8)).toArray(String[]::new); long empresaId=(long)ex.getAttribute(AuthMiddleware.ATTR_EMPRESA_ID); long funcionarioId=(long)ex.getAttribute(AuthMiddleware.ATTR_FUNCIONARIO_ID); String cargo=String.valueOf(ex.getAttribute(AuthMiddleware.ATTR_CARGO)); try(Connection c=tenantDs.getConnection(empresaId)){ - if(p.length==1&&"funcionarios".equals(p[0])){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar funcionarios."));return;} if("POST".equals(method)) criarFuncionarioEmpresa(ex,empresaId); else if("GET".equals(method)) listarFuncionariosEmpresa(ex,empresaId); else HttpResponses.methodNotAllowed(ex); return; } - if(p.length==2&&"funcionarios".equals(p[0])){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar funcionarios."));return;} long func=id(p[1]); if("PUT".equals(method)) atualizarFuncionarioEmpresa(ex,empresaId,func,funcionarioId); else if("DELETE".equals(method)) excluirFuncionarioEmpresa(ex,empresaId,func,funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } + if(p.length==1&&"funcionarios".equals(p[0])){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar funcionários."));return;} if("POST".equals(method)) criarFuncionarioEmpresa(ex,empresaId); else if("GET".equals(method)) listarFuncionariosEmpresa(ex,empresaId); else HttpResponses.methodNotAllowed(ex); return; } + if(p.length==2&&"funcionarios".equals(p[0])){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar funcionários."));return;} long func=id(p[1]); if("PUT".equals(method)) atualizarFuncionarioEmpresa(ex,empresaId,func,funcionarioId); else if("DELETE".equals(method)) excluirFuncionarioEmpresa(ex,empresaId,func,funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==1&&"lembrete-pessoas".equals(p[0])&&"GET".equals(method)){ listarPessoasLembrete(ex,empresaId); return; } - if(p.length==1&&"extras".equals(p[0])){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar destinatarios extras."));return;} if("POST".equals(method)) criarExtra(ex,c,funcionarioId); else if("GET".equals(method)) listarExtras(ex,c); else HttpResponses.methodNotAllowed(ex); return; } - if(p.length==2&&"extras".equals(p[0])){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar destinatarios extras."));return;} long extra=id(p[1]); if("PUT".equals(method)) atualizarExtra(ex,c,extra,funcionarioId); else if("DELETE".equals(method)) excluirExtra(ex,c,extra,funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } + if(p.length==1&&"extras".equals(p[0])){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar destinatários extras."));return;} if("POST".equals(method)) criarExtra(ex,c,funcionarioId); else if("GET".equals(method)) listarExtras(ex,c); else HttpResponses.methodNotAllowed(ex); return; } + if(p.length==2&&"extras".equals(p[0])){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar destinatários extras."));return;} long extra=id(p[1]); if("PUT".equals(method)) atualizarExtra(ex,c,extra,funcionarioId); else if("DELETE".equals(method)) excluirExtra(ex,c,extra,funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==1&&"lembretes".equals(p[0])){ if("POST".equals(method)) criarLembrete(ex,c,empresaId,funcionarioId); else if("GET".equals(method)) listarLembretes(ex,c,empresaId,cargo,funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==3&&"lembretes".equals(p[0])&&"enviar".equals(p[2])&&"POST".equals(method)){ enviarLembreteManual(ex,c,empresaId,id(p[1]),cargo,funcionarioId); return; } if(p.length==2&&"lembretes".equals(p[0])){ long lembrete=id(p[1]); if("PUT".equals(method)) atualizarLembrete(ex,c,empresaId,lembrete,cargo,funcionarioId); else if("DELETE".equals(method)) excluirLembrete(ex,c,lembrete,cargo,funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } @@ -65,8 +65,8 @@ public final class ClienteHandlers implements HttpHandler { if(p.length==3&&"config".equals(p[0])&&"evolution-instancia".equals(p[1])&&"desconectar".equals(p[2])&&"POST".equals(method)){ desconectarEvolution(ex,empresaId); return; } if(p.length==3&&"config".equals(p[0])&&"evolution-instancia".equals(p[1])&&"reconectar".equals(p[2])&&"POST".equals(method)){ reconectarEvolution(ex,empresaId); return; } if(p.length==1&&"assembleias-calendario".equals(p[0])&&"GET".equals(method)){ listarAssembleiasCalendario(ex,c,empresaId,cargo,funcionarioId); return; } - if(p.length==1&&"assembleia-responsaveis".equals(p[0])){ if("GET".equals(method)) listarResponsaveisAssembleia(ex,c); else { if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar responsaveis de assembleia."));return;} if("POST".equals(method)) criarResponsavelAssembleia(ex,c,funcionarioId); else HttpResponses.methodNotAllowed(ex); } return; } - if(p.length==2&&"assembleia-responsaveis".equals(p[0])){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar responsaveis de assembleia."));return;} long resp=id(p[1]); if("PUT".equals(method)) atualizarResponsavelAssembleia(ex,c,resp,funcionarioId); else if("DELETE".equals(method)) excluirResponsavelAssembleia(ex,c,resp,funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } + if(p.length==1&&"assembleia-responsaveis".equals(p[0])){ if("GET".equals(method)) listarResponsaveisAssembleia(ex,c); else { if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar responsáveis de assembleia."));return;} if("POST".equals(method)) criarResponsavelAssembleia(ex,c,funcionarioId); else HttpResponses.methodNotAllowed(ex); } return; } + if(p.length==2&&"assembleia-responsaveis".equals(p[0])){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal da empresa pode gerenciar responsáveis de assembleia."));return;} long resp=id(p[1]); if("PUT".equals(method)) atualizarResponsavelAssembleia(ex,c,resp,funcionarioId); else if("DELETE".equals(method)) excluirResponsavelAssembleia(ex,c,resp,funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==1&&"respostas".equals(p[0])&&"POST".equals(method)){ registrarResposta(ex,c,funcionarioId,null); return; } if(p.length==3&&"condominios".equals(p[0])&&"avisos".equals(p[2])){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} if("POST".equals(method)) criarAviso(ex,c,empresaId,cond,funcionarioId); else if("GET".equals(method)) listarAvisos(ex,c,cond); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==4&&"condominios".equals(p[0])&&"avisos".equals(p[2])){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} if("GET".equals(method)) detalheAviso(ex,c,cond,id(p[3])); else if("DELETE".equals(method)) excluirAviso(ex,c,empresaId,cond,id(p[3]),funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } @@ -78,8 +78,8 @@ public final class ClienteHandlers implements HttpHandler { if(p.length==3&&"condominios".equals(p[0])&&"contatos".equals(p[2])&&"GET".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} listarContatos(ex,c,cond); return; } if(p.length==4&&"condominios".equals(p[0])&&"contatos".equals(p[2])&&"vincular".equals(p[3])&&"POST".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} vincularContato(ex,c,cond,funcionarioId); return; } if(p.length==4&&"condominios".equals(p[0])&&"contatos".equals(p[2])&&"DELETE".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} desvincularContato(ex,c,cond,id(p[3]),funcionarioId); return; } - if(p.length==3&&"condominios".equals(p[0])&&"funcionarios-vinculados".equals(p[2])){ long cond=id(p[1]); if("POST".equals(method)){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal pode vincular funcionarios a condominios."));return;} vincularFuncionario(ex,c,empresaId,cond,funcionarioId); } else if("GET".equals(method)){ if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} listarFuncionariosVinculados(ex,c,empresaId,cond); } else HttpResponses.methodNotAllowed(ex); return; } - if(p.length==4&&"condominios".equals(p[0])&&"funcionarios-vinculados".equals(p[2])&&"DELETE".equals(method)){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal pode desvincular funcionarios de condominios."));return;} execWrite(ex,c,"DELETE FROM funcionario_condominio WHERE condominio_id=? AND funcionario_id=?",Arrays.asList(id(p[1]),id(p[3])),funcionarioId,"funcionario_condominio",id(p[3])); return; } + if(p.length==3&&"condominios".equals(p[0])&&"funcionarios-vinculados".equals(p[2])){ long cond=id(p[1]); if("POST".equals(method)){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal pode vincular funcionários a condomínios."));return;} vincularFuncionario(ex,c,empresaId,cond,funcionarioId); } else if("GET".equals(method)){ if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} listarFuncionariosVinculados(ex,c,empresaId,cond); } else HttpResponses.methodNotAllowed(ex); return; } + if(p.length==4&&"condominios".equals(p[0])&&"funcionarios-vinculados".equals(p[2])&&"DELETE".equals(method)){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal pode desvincular funcionários de condomínios."));return;} execWrite(ex,c,"DELETE FROM funcionario_condominio WHERE condominio_id=? AND funcionario_id=?",Arrays.asList(id(p[1]),id(p[3])),funcionarioId,"funcionario_condominio",id(p[3])); return; } if(p.length==3&&"condominios".equals(p[0])&&"espacos-comuns".equals(p[2])){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} if("POST".equals(method)) criarEspaco(ex,c,cond,funcionarioId); else if("GET".equals(method)) listarParam(ex,c,"SELECT * FROM espacos_comuns WHERE condominio_id=? ORDER BY nome",cond,"espacos"); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==4&&"condominios".equals(p[0])&&"espacos-comuns".equals(p[2])){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} if("PUT".equals(method)) atualizarEspaco(ex,c,cond,id(p[3]),funcionarioId); else if("DELETE".equals(method)) excluirEspaco(ex,c,cond,id(p[3]),funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==3&&"condominios".equals(p[0])&&"assembleias".equals(p[2])){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} if("POST".equals(method)) criarAssembleia(ex,c,empresaId,cond,funcionarioId); else if("GET".equals(method)) listarParam(ex,c,"SELECT a.*, r.nome responsavel_nome FROM assembleias a LEFT JOIN assembleia_responsaveis r ON r.id=a.responsavel_id WHERE a.condominio_id=? ORDER BY a.data_hora DESC",cond,"assembleias"); else HttpResponses.methodNotAllowed(ex); return; } @@ -92,13 +92,13 @@ public final class ClienteHandlers implements HttpHandler { } } - private void criarCondominio(HttpExchange ex,Connection c,long fid,String cargo)throws Exception{Mapb=body(ex);String cnpj=cnpj(req(b,"cnpj"));if(one(c,"SELECT id FROM condominios WHERE regexp_replace(cnpj,'\\D','','g')=?",Arrays.asList(cnpj))!=null){HttpResponses.json(ex,400,Map.of("erro","cnpj_duplicado","mensagem","Ja existe um condominio cadastrado com este CNPJ."));return;}c.setAutoCommit(false);try{long id=insert(c,"INSERT INTO condominios (nome,cnpj,endereco) VALUES (?,?,?)",Arrays.asList(req(b,"nome"),cnpj,nullable(b.get("endereco"))));if(!adminEmpresa(cargo)){try(PreparedStatement ps=c.prepareStatement("INSERT INTO funcionario_condominio (funcionario_id,condominio_id) VALUES (?,?) ON CONFLICT DO NOTHING")){ps.setLong(1,fid);ps.setLong(2,id);ps.executeUpdate();}}audit(c,fid,"create","condominios",id,null,b);c.commit();detalheCondominio(ex,c,id);}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}} + private void criarCondominio(HttpExchange ex,Connection c,long fid,String cargo)throws Exception{Mapb=body(ex);String cnpj=cnpj(req(b,"cnpj"));if(one(c,"SELECT id FROM condominios WHERE regexp_replace(cnpj,'\\D','','g')=?",Arrays.asList(cnpj))!=null){HttpResponses.json(ex,400,Map.of("erro","cnpj_duplicado","mensagem","Já existe um condomínio cadastrado com este CNPJ."));return;}c.setAutoCommit(false);try{long id=insert(c,"INSERT INTO condominios (nome,cnpj,endereco) VALUES (?,?,?)",Arrays.asList(req(b,"nome"),cnpj,nullable(b.get("endereco"))));if(!adminEmpresa(cargo)){try(PreparedStatement ps=c.prepareStatement("INSERT INTO funcionario_condominio (funcionario_id,condominio_id) VALUES (?,?) ON CONFLICT DO NOTHING")){ps.setLong(1,fid);ps.setLong(2,id);ps.executeUpdate();}}audit(c,fid,"create","condominios",id,null,b);c.commit();detalheCondominio(ex,c,id);}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}} private void listarCondominios(HttpExchange ex,Connection c,long fid,String cargo)throws Exception{if(adminEmpresa(cargo)){listar(ex,c,"SELECT * FROM condominios ORDER BY nome","condominios");return;}HttpResponses.json(ex,200,Map.of("condominios",rows(c,"SELECT c.* FROM condominios c JOIN funcionario_condominio fc ON fc.condominio_id=c.id WHERE fc.funcionario_id=? ORDER BY c.nome",Arrays.asList(fid))));} private boolean podeAcessarCondominio(Connection c,String cargo,long fid,long cond)throws SQLException{return adminEmpresa(cargo)||count(c,"SELECT count(*) FROM funcionario_condominio WHERE funcionario_id=? AND condominio_id=?",fid,cond)>0;} private void detalheCondominio(HttpExchange ex,Connection c,long id)throws Exception{Map cond=one(c,"SELECT * FROM condominios WHERE id=?",Arrays.asList(id)); if(cond==null){HttpResponses.json(ex,404,Map.of("erro","condominio_nao_encontrado"));return;} Map sind=one(c,"SELECT cc.*, c.nome contato_nome,c.telefone contato_telefone,c.email contato_email FROM contato_condominio cc JOIN contatos c ON c.id=cc.contato_id WHERE cc.condominio_id=? AND cc.tipo='sindico' AND cc.status='ativo' ORDER BY cc.data_inicio_mandato DESC NULLS LAST LIMIT 1",Arrays.asList(id)); long ass=count(c,"SELECT count(*) FROM assembleias WHERE condominio_id=? AND data_hora>now() AND status='agendada'",id); long av=count(c,"SELECT count(*) FROM avisos WHERE condominio_id=? AND (data_expiracao IS NULL OR data_expiracao>now())",id); Map resp=new LinkedHashMap<>();resp.put("condominio",cond);resp.put("sindico_atual",sind);resp.put("assembleias_futuras",ass);resp.put("avisos_ativos",av);HttpResponses.json(ex,200,resp);} private void atualizarCondominio(HttpExchange ex,Connection c,long id,long fid)throws Exception{Mapb=body(ex); Map old=one(c,"SELECT * FROM condominios WHERE id=?",Arrays.asList(id)); if(old==null){HttpResponses.json(ex,404,Map.of("erro","condominio_nao_encontrado"));return;} try(PreparedStatement ps=c.prepareStatement("UPDATE condominios SET nome=COALESCE(?,nome), cnpj=COALESCE(?,cnpj), endereco=COALESCE(?,endereco) WHERE id=?")){ps.setString(1,b.get("nome"));ps.setString(2,b.get("cnpj"));ps.setString(3,b.get("endereco"));ps.setLong(4,id);ps.executeUpdate();} audit(c,fid,"update","condominios",id,old,b); detalheCondominio(ex,c,id);} private void listarRespostas(HttpExchange ex,Connection c,long cond)throws Exception{HttpResponses.json(ex,200,Map.of("respostas",rows(c,"SELECT r.*, co.nome contato_nome, co.telefone contato_telefone, co.email contato_email FROM respostas r LEFT JOIN contatos co ON co.id=r.contato_id WHERE r.condominio_id=? ORDER BY r.recebido_em DESC,id DESC",Arrays.asList(cond))));} - private void registrarResposta(HttpExchange ex,Connection c,long fid,Long condPath)throws Exception{String raw=rawBody(ex);Mapb=SimpleJson.parseObject(raw);Long contato=optLong(firstNonBlank(b.get("contato_id"),b.get("destinatario_id")));Long cond=condPath!=null?condPath:optLong(b.get("condominio_id"));if(cond==null&&contato!=null){Map link=one(c,"SELECT condominio_id FROM contato_condominio WHERE contato_id=? AND status='ativo' ORDER BY CASE WHEN tipo='sindico' THEN 0 ELSE 1 END, condominio_id LIMIT 1",Arrays.asList(contato));if(link!=null)cond=((Number)link.get("condominio_id")).longValue();}if(cond==null){HttpResponses.json(ex,400,Map.of("erro","condominio_obrigatorio","mensagem","Informe condominio_id ou contato_id vinculado a um condominio."));return;}Map contatoRow=contato==null?null:one(c,"SELECT id,nome,telefone,email FROM contatos WHERE id=?",Arrays.asList(contato));String texto=firstNonBlank(b.get("resposta"),b.get("resposta_texto"),b.get("mensagem"),b.get("texto"),b.get("body"));if(texto.isBlank()){HttpResponses.json(ex,400,Map.of("erro","resposta_obrigatoria","mensagem","Informe a resposta recebida."));return;}long rid=insert(c,"INSERT INTO respostas (condominio_id,contato_id,mensagem_id,tipo_origem,nome_origem,telefone_origem,resposta_texto,mensagem_original,payload,origem,status) VALUES (?,?,?,?,?,?,?,?,?,?,?)",Arrays.asList(cond,contato,optLong(b.get("mensagem_id")),nullable(firstNonBlank(b.get("tipo"),b.get("tipo_origem"))),nullable(firstNonBlank(b.get("nome"),contatoRow==null?null:str(contatoRow.get("nome")))),nullable(firstNonBlank(b.get("telefone"),contatoRow==null?null:str(contatoRow.get("telefone")))),texto,nullable(firstNonBlank(b.get("mensagem_original"),b.get("pergunta"),b.get("mensagem_enviada"))),raw,firstNonBlank(b.get("origem"),"n8n"),firstNonBlank(b.get("status"),"recebida")));audit(c,fid,"create","respostas",rid,null,b);HttpResponses.json(ex,201,Map.of("ok",true,"resposta",one(c,"SELECT * FROM respostas WHERE id=?",Arrays.asList(rid)))) ;} + private void registrarResposta(HttpExchange ex,Connection c,long fid,Long condPath)throws Exception{String raw=rawBody(ex);Mapb=SimpleJson.parseObject(raw);Long contato=optLong(firstNonBlank(b.get("contato_id"),b.get("destinatario_id")));Long cond=condPath!=null?condPath:optLong(b.get("condominio_id"));if(cond==null&&contato!=null){Map link=one(c,"SELECT condominio_id FROM contato_condominio WHERE contato_id=? AND status='ativo' ORDER BY CASE WHEN tipo='sindico' THEN 0 ELSE 1 END, condominio_id LIMIT 1",Arrays.asList(contato));if(link!=null)cond=((Number)link.get("condominio_id")).longValue();}if(cond==null){HttpResponses.json(ex,400,Map.of("erro","condominio_obrigatorio","mensagem","Informe condominio_id ou contato_id vinculado a um condomínio."));return;}Map contatoRow=contato==null?null:one(c,"SELECT id,nome,telefone,email FROM contatos WHERE id=?",Arrays.asList(contato));String texto=firstNonBlank(b.get("resposta"),b.get("resposta_texto"),b.get("mensagem"),b.get("texto"),b.get("body"));if(texto.isBlank()){HttpResponses.json(ex,400,Map.of("erro","resposta_obrigatoria","mensagem","Informe a resposta recebida."));return;}long rid=insert(c,"INSERT INTO respostas (condominio_id,contato_id,mensagem_id,tipo_origem,nome_origem,telefone_origem,resposta_texto,mensagem_original,payload,origem,status) VALUES (?,?,?,?,?,?,?,?,?,?,?)",Arrays.asList(cond,contato,optLong(b.get("mensagem_id")),nullable(firstNonBlank(b.get("tipo"),b.get("tipo_origem"))),nullable(firstNonBlank(b.get("nome"),contatoRow==null?null:str(contatoRow.get("nome")))),nullable(firstNonBlank(b.get("telefone"),contatoRow==null?null:str(contatoRow.get("telefone")))),texto,nullable(firstNonBlank(b.get("mensagem_original"),b.get("pergunta"),b.get("mensagem_enviada"))),raw,firstNonBlank(b.get("origem"),"n8n"),firstNonBlank(b.get("status"),"recebida")));audit(c,fid,"create","respostas",rid,null,b);HttpResponses.json(ex,201,Map.of("ok",true,"resposta",one(c,"SELECT * FROM respostas WHERE id=?",Arrays.asList(rid)))) ;} private void criarContato(HttpExchange ex,Connection c,long fid)throws Exception{Mapb=body(ex); long id=insert(c,"INSERT INTO contatos (nome,telefone,email) VALUES (?,?,?)",Arrays.asList(req(b,"nome"),nullable(b.get("telefone")),nullable(b.get("email")))); audit(c,fid,"create","contatos",id,null,b); HttpResponses.json(ex,201,Map.of("contato",one(c,"SELECT * FROM contatos WHERE id=?",Arrays.asList(id))));} private void vincularContato(HttpExchange ex,Connection c,long cond,long fid)throws Exception{Mapb=body(ex); Long contato=optLong(b.get("contato_id")); if(contato==null){contato=insert(c,"INSERT INTO contatos (nome,telefone,email) VALUES (?,?,?)",Arrays.asList(req(b,"nome"),nullable(b.get("telefone")),nullable(b.get("email"))));} String tipo=req(b,"tipo"); if("sindico".equals(tipo)){try(PreparedStatement ps=c.prepareStatement("UPDATE contato_condominio SET status='inativo', data_fim_mandato=current_date WHERE condominio_id=? AND tipo='sindico' AND status='ativo' AND contato_id<>?")){ps.setLong(1,cond);ps.setLong(2,contato);ps.executeUpdate();}} try(PreparedStatement ps=c.prepareStatement("INSERT INTO contato_condominio (contato_id,condominio_id,tipo,data_inicio_mandato,data_fim_mandato,status) VALUES (?,?,?,?,?,'ativo') ON CONFLICT (contato_id,condominio_id,tipo) DO UPDATE SET data_inicio_mandato=EXCLUDED.data_inicio_mandato,data_fim_mandato=EXCLUDED.data_fim_mandato,status='ativo'")){ps.setLong(1,contato);ps.setLong(2,cond);ps.setString(3,tipo);setDate(ps,4,b.get("data_inicio_mandato"));setDate(ps,5,b.get("data_fim_mandato"));ps.executeUpdate();} if(Set.of("sindico","subsindico","conselheiro").contains(tipo))vincularContatoAosAvisosRecorrentes(c,cond,contato); audit(c,fid,"create","contato_condominio",contato,null,b); listarContatos(ex,c,cond);} private void vincularContatoAosAvisosRecorrentes(Connection c,long cond,long contato)throws SQLException{ @@ -143,13 +143,13 @@ public final class ClienteHandlers implements HttpHandler { private Map criarAvisosAssembleia(Connection c,long emp,long cond,long assembleia,long fid,Mapb,Listdests,Long responsavelId)throws Exception{ List destinatarios=dests.isEmpty()?destinatariosPadraoAssembleia(c,emp,cond,responsavelId):dests; int total=destinatarios.size(); - if(total==0)return Map.of("enviados",0,"falhas",0,"mensagem","Nenhum destinatario vinculado ao condominio."); - if(!funcionalidadeAtiva(emp,"whatsapp_envio"))return Map.of("enviados",0,"falhas",1,"mensagem","WhatsApp nao liberado para esta empresa."); - if(evolutionAprovada(emp)==null)return Map.of("enviados",0,"falhas",1,"mensagem","WhatsApp nao aprovado para esta empresa."); + if(total==0)return Map.of("enviados",0,"falhas",0,"mensagem","Nenhum destinatário vinculado ao condomínio."); + if(!funcionalidadeAtiva(emp,"whatsapp_envio"))return Map.of("enviados",0,"falhas",1,"mensagem","WhatsApp não liberado para esta empresa."); + if(evolutionAprovada(emp)==null)return Map.of("enviados",0,"falhas",1,"mensagem","WhatsApp não aprovado para esta empresa."); Timestamp data=ts(req(b,"data_hora")); List avisos=new ArrayList<>(); avisos.add(criarAvisoAutomaticoAssembleia(c,cond,assembleia,fid,b,destinatarios,"Assembleia agendada","Nova assembleia agendada.",null)); - avisos.add(criarAvisoAutomaticoAssembleia(c,cond,assembleia,fid,b,destinatarios,"Lembrete de assembleia","Lembrete: a assembleia acontece amanha.",Timestamp.valueOf(data.toLocalDateTime().minusDays(1)))); + avisos.add(criarAvisoAutomaticoAssembleia(c,cond,assembleia,fid,b,destinatarios,"Lembrete de assembleia","Lembrete: a assembleia acontece amanhã.",Timestamp.valueOf(data.toLocalDateTime().minusDays(1)))); avisos.add(criarAvisoAutomaticoAssembleia(c,cond,assembleia,fid,b,destinatarios,"Lembrete de assembleia","Lembrete: a assembleia acontece hoje.",data)); return Map.of("aviso_ids",avisos,"destinatarios",total,"status","enfileirado"); } @@ -373,17 +373,17 @@ public final class ClienteHandlers implements HttpHandler { if(lembrete==null){HttpResponses.json(ex,404,Map.of("erro","lembrete_nao_encontrado"));return;} if(!podeAlterarLembrete(lembrete,cargo,fid)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas o criador do lembrete ou a conta principal pode enviar."));return;} List> dests=(List>)lembrete.getOrDefault("destinatarios",List.of()); - if(dests.isEmpty()){HttpResponses.json(ex,400,Map.of("erro","destinatario_obrigatorio","mensagem","Selecione ao menos um destinatario para enviar o lembrete."));return;} + if(dests.isEmpty()){HttpResponses.json(ex,400,Map.of("erro","destinatario_obrigatorio","mensagem","Selecione ao menos um destinatário para enviar o lembrete."));return;} Map envio=enviarLembreteWhatsApp(emp,id,lembrete); if(Number.class.isInstance(envio.get("falhas"))&&((Number)envio.get("falhas")).intValue()==0&&Number.class.isInstance(envio.get("enviados"))&&((Number)envio.get("enviados")).intValue()>0){finalizarOuReagendarLembrete(c,id);} HttpResponses.json(ex,200,Map.of("envio",envio)); } - private void atualizarLembrete(HttpExchange ex,Connection c,long emp,long id,String cargo,long fid)throws Exception{Map old=one(c,"SELECT * FROM lembretes WHERE id=?",Arrays.asList(id));if(old==null){HttpResponses.json(ex,404,Map.of("erro","lembrete_nao_encontrado"));return;}if(!podeAlterarLembrete(old,cargo,fid)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas o criador do lembrete ou a conta principal pode alterar."));return;}String statusAtual=str(old.get("status"));if("concluido".equalsIgnoreCase(statusAtual)||"enviado".equalsIgnoreCase(statusAtual)){HttpResponses.json(ex,409,Map.of("erro","lembrete_ja_enviado","mensagem","Este lembrete ja foi enviado e nao pode mais ser editado."));return;}String raw=rawBody(ex);Mapb=SimpleJson.parseObject(removeArrayField(raw,"destinatarios"));Listdests=longArray(raw,"destinatarios");c.setAutoCommit(false);try{try(PreparedStatement ps=c.prepareStatement("UPDATE lembretes SET titulo=COALESCE(?,titulo), mensagem=COALESCE(?,mensagem), data_hora=COALESCE(?,data_hora), status=COALESCE(?,status), recorrencia_meses=?, recorrencia_ativa=?, atualizado_em=now() WHERE id=?")){ps.setString(1,b.get("titulo"));ps.setString(2,b.get("mensagem"));if(b.containsKey("data_hora")){Timestamp t=ts(b.get("data_hora"));if(t==null)ps.setNull(3,Types.TIMESTAMP);else ps.setTimestamp(3,t);}else ps.setNull(3,Types.TIMESTAMP);ps.setString(4,b.get("status"));int rec=recorrenciaMeses(b.get("recorrencia_meses"));ps.setInt(5,rec);ps.setBoolean(6,rec>0);ps.setLong(7,id);ps.executeUpdate();}if(raw.contains("\"destinatarios\""))salvarDestinatariosLembrete(c,id,dests);Map atual=one(c,"SELECT status,data_hora FROM lembretes WHERE id=?",Arrays.asList(id));if(atual!=null&&"ativo".equalsIgnoreCase(str(atual.get("status"))))enfileirarLembrete(c,id,(Timestamp)atual.get("data_hora"));audit(c,fid,"update","lembretes",id,old,b);c.commit();HttpResponses.json(ex,200,Map.of("lembrete",lembreteComDetalhes(c,emp,id)));}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}} + private void atualizarLembrete(HttpExchange ex,Connection c,long emp,long id,String cargo,long fid)throws Exception{Map old=one(c,"SELECT * FROM lembretes WHERE id=?",Arrays.asList(id));if(old==null){HttpResponses.json(ex,404,Map.of("erro","lembrete_nao_encontrado"));return;}if(!podeAlterarLembrete(old,cargo,fid)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas o criador do lembrete ou a conta principal pode alterar."));return;}String statusAtual=str(old.get("status"));if("concluido".equalsIgnoreCase(statusAtual)||"enviado".equalsIgnoreCase(statusAtual)){HttpResponses.json(ex,409,Map.of("erro","lembrete_ja_enviado","mensagem","Este lembrete já foi enviado e não pode mais ser editado."));return;}String raw=rawBody(ex);Mapb=SimpleJson.parseObject(removeArrayField(raw,"destinatarios"));Listdests=longArray(raw,"destinatarios");c.setAutoCommit(false);try{try(PreparedStatement ps=c.prepareStatement("UPDATE lembretes SET titulo=COALESCE(?,titulo), mensagem=COALESCE(?,mensagem), data_hora=COALESCE(?,data_hora), status=COALESCE(?,status), recorrencia_meses=?, recorrencia_ativa=?, atualizado_em=now() WHERE id=?")){ps.setString(1,b.get("titulo"));ps.setString(2,b.get("mensagem"));if(b.containsKey("data_hora")){Timestamp t=ts(b.get("data_hora"));if(t==null)ps.setNull(3,Types.TIMESTAMP);else ps.setTimestamp(3,t);}else ps.setNull(3,Types.TIMESTAMP);ps.setString(4,b.get("status"));int rec=recorrenciaMeses(b.get("recorrencia_meses"));ps.setInt(5,rec);ps.setBoolean(6,rec>0);ps.setLong(7,id);ps.executeUpdate();}if(raw.contains("\"destinatarios\""))salvarDestinatariosLembrete(c,id,dests);Map atual=one(c,"SELECT status,data_hora FROM lembretes WHERE id=?",Arrays.asList(id));if(atual!=null&&"ativo".equalsIgnoreCase(str(atual.get("status"))))enfileirarLembrete(c,id,(Timestamp)atual.get("data_hora"));audit(c,fid,"update","lembretes",id,old,b);c.commit();HttpResponses.json(ex,200,Map.of("lembrete",lembreteComDetalhes(c,emp,id)));}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}} private void excluirLembrete(HttpExchange ex,Connection c,long id,String cargo,long fid)throws Exception{Map old=one(c,"SELECT * FROM lembretes WHERE id=?",Arrays.asList(id));if(old==null){HttpResponses.json(ex,404,Map.of("erro","lembrete_nao_encontrado"));return;}if(!podeAlterarLembrete(old,cargo,fid)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas o criador do lembrete ou a conta principal pode excluir."));return;}c.setAutoCommit(false);try{try(PreparedStatement ps=c.prepareStatement("DELETE FROM lembretes WHERE id=?")){ps.setLong(1,id);ps.executeUpdate();}audit(c,fid,"delete","lembretes",id,old,Map.of("ok",true));c.commit();HttpResponses.json(ex,200,Map.of("ok",true));}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}} private boolean podeAlterarLembrete(Map lembrete,String cargo,long fid){return adminEmpresa(cargo)||((Number)lembrete.get("criado_por_funcionario")).longValue()==fid;} private Map enviarLembreteWhatsApp(long emp,long lembreteId,Map lembrete)throws Exception{ Map inst=evolutionAprovada(emp); - if(inst==null)return Map.of("enviados",0,"falhas",1,"mensagem","WhatsApp nao aprovado para esta empresa."); + if(inst==null)return Map.of("enviados",0,"falhas",1,"mensagem","WhatsApp não aprovado para esta empresa."); String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2); String texto="Lembrete: "+str(lembrete.get("titulo"))+"\n\n"+str(lembrete.get("mensagem")); List> dests=(List>)lembrete.getOrDefault("destinatarios",List.of()); @@ -415,17 +415,17 @@ public final class ClienteHandlers implements HttpHandler { private Map lembreteComDetalhes(Connection c,long emp,long id)throws Exception{Map l=one(c,"SELECT * FROM lembretes WHERE id=?",Arrays.asList(id));if(l!=null&&emp>0)l.put("destinatarios",destinatariosLembrete(c,emp,id));return l;} private List> destinatariosLembrete(Connection c,long emp,long id)throws Exception{List> ids=rows(c,"SELECT funcionario_id FROM lembrete_destinatarios WHERE lembrete_id=? ORDER BY funcionario_id",Arrays.asList(id));List> out=new ArrayList<>();try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id,nome,email,telefone,cargo,status FROM funcionarios WHERE id=? AND empresa_id=?")){for(Map row:ids){ps.setLong(1,((Number)row.get("funcionario_id")).longValue());ps.setLong(2,emp);try(ResultSet rs=ps.executeQuery()){if(rs.next())out.add(resultMap(rs));}}}return out;} private void listarFuncionariosEmpresa(HttpExchange ex,long empresaId)throws Exception{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id,empresa_id,nome,email,telefone,cargo,status,criado_em FROM funcionarios WHERE empresa_id=? ORDER BY id")){ps.setLong(1,empresaId);List> out=new ArrayList<>();try(ResultSet rs=ps.executeQuery()){while(rs.next())out.add(resultMap(rs));}HttpResponses.json(ex,200,Map.of("funcionarios",out));}} - private void criarFuncionarioEmpresa(HttpExchange ex,long empresaId)throws Exception{Mapb=body(ex);try(Connection m=masterDs.getMasterConnection()){if(one(m,"SELECT id FROM funcionarios WHERE email=?",Arrays.asList(req(b,"email")))!=null){HttpResponses.json(ex,409,Map.of("erro","email_duplicado","mensagem","Este e-mail ja esta cadastrado."));return;}try(PreparedStatement ps=m.prepareStatement("INSERT INTO funcionarios (empresa_id,nome,email,telefone,senha_hash,cargo,status) VALUES (?,?,?,?,?,?,'ativo') RETURNING id")){ps.setLong(1,empresaId);ps.setString(2,req(b,"nome"));ps.setString(3,req(b,"email"));ps.setString(4,b.get("telefone"));ps.setString(5,com.seconectai.condocontab.auth.PasswordHasher.hash(req(b,"senha")));ps.setString(6,b.getOrDefault("cargo","funcionario"));try(ResultSet rs=ps.executeQuery()){rs.next();HttpResponses.json(ex,201,Map.of("id",rs.getLong(1),"ok",true));}}}} + private void criarFuncionarioEmpresa(HttpExchange ex,long empresaId)throws Exception{Mapb=body(ex);try(Connection m=masterDs.getMasterConnection()){if(one(m,"SELECT id FROM funcionarios WHERE email=?",Arrays.asList(req(b,"email")))!=null){HttpResponses.json(ex,409,Map.of("erro","email_duplicado","mensagem","Este e-mail já está cadastrado."));return;}try(PreparedStatement ps=m.prepareStatement("INSERT INTO funcionarios (empresa_id,nome,email,telefone,senha_hash,cargo,status) VALUES (?,?,?,?,?,?,'ativo') RETURNING id")){ps.setLong(1,empresaId);ps.setString(2,req(b,"nome"));ps.setString(3,req(b,"email"));ps.setString(4,b.get("telefone"));ps.setString(5,com.seconectai.condocontab.auth.PasswordHasher.hash(req(b,"senha")));ps.setString(6,b.getOrDefault("cargo","funcionario"));try(ResultSet rs=ps.executeQuery()){rs.next();HttpResponses.json(ex,201,Map.of("id",rs.getLong(1),"ok",true));}}}} private void atualizarFuncionarioEmpresa(HttpExchange ex,long empresaId,long funcId,long solicitante)throws Exception{ Mapb=body(ex); try(Connection m=masterDs.getMasterConnection()){ Map atual=one(m,"SELECT id,cargo FROM funcionarios WHERE id=? AND empresa_id=?",Arrays.asList(funcId,empresaId)); if(atual==null){HttpResponses.json(ex,404,Map.of("erro","funcionario_nao_encontrado"));return;} - if("admin_empresa".equals(str(atual.get("cargo")))&&funcId!=solicitante){HttpResponses.json(ex,400,Map.of("erro","admin_protegido","mensagem","A conta principal nao pode ser alterada por aqui."));return;} + if("admin_empresa".equals(str(atual.get("cargo")))&&funcId!=solicitante){HttpResponses.json(ex,400,Map.of("erro","admin_protegido","mensagem","A conta principal não pode ser alterada por aqui."));return;} String email=b.get("email"); if(email!=null&&!email.isBlank()){ Map dono=one(m,"SELECT id FROM funcionarios WHERE email=? AND id<>?",Arrays.asList(email,funcId)); - if(dono!=null){HttpResponses.json(ex,409,Map.of("erro","email_duplicado","mensagem","Este e-mail ja esta cadastrado."));return;} + if(dono!=null){HttpResponses.json(ex,409,Map.of("erro","email_duplicado","mensagem","Este e-mail já está cadastrado."));return;} } try(PreparedStatement ps=m.prepareStatement("UPDATE funcionarios SET nome=COALESCE(?,nome), email=COALESCE(?,email), telefone=COALESCE(?,telefone), status=COALESCE(?,status), senha_hash=COALESCE(?,senha_hash) WHERE id=? AND empresa_id=?")){ ps.setString(1,b.get("nome")); @@ -442,11 +442,11 @@ public final class ClienteHandlers implements HttpHandler { } } private void excluirFuncionarioEmpresa(HttpExchange ex,long empresaId,long funcId,long solicitante)throws Exception{ - if(funcId==solicitante){HttpResponses.json(ex,400,Map.of("erro","auto_exclusao_bloqueada","mensagem","Voce nao pode excluir seu proprio acesso."));return;} + if(funcId==solicitante){HttpResponses.json(ex,400,Map.of("erro","auto_exclusao_bloqueada","mensagem","Você não pode excluir seu próprio acesso."));return;} try(Connection m=masterDs.getMasterConnection()){ Map atual=one(m,"SELECT id,cargo FROM funcionarios WHERE id=? AND empresa_id=?",Arrays.asList(funcId,empresaId)); if(atual==null){HttpResponses.json(ex,404,Map.of("erro","funcionario_nao_encontrado"));return;} - if("admin_empresa".equals(str(atual.get("cargo")))){HttpResponses.json(ex,400,Map.of("erro","admin_protegido","mensagem","A conta principal nao pode ser excluida."));return;} + if("admin_empresa".equals(str(atual.get("cargo")))){HttpResponses.json(ex,400,Map.of("erro","admin_protegido","mensagem","A conta principal não pode ser excluída."));return;} try(PreparedStatement ps=m.prepareStatement("UPDATE funcionarios SET status='inativo' WHERE id=? AND empresa_id=?")){ps.setLong(1,funcId);ps.setLong(2,empresaId);ps.executeUpdate();} } try(Connection c=tenantDs.getConnection(empresaId);PreparedStatement ps=c.prepareStatement("DELETE FROM funcionario_condominio WHERE funcionario_id=?")){ @@ -457,7 +457,7 @@ public final class ClienteHandlers implements HttpHandler { } private boolean adminEmpresa(String cargo){return "admin_empresa".equals(cargo);} - private void conectarEvolution(HttpExchange ex,long empresaId)throws Exception{Mapb=body(ex);String url=b.getOrDefault("instance_url","");String numero=EvolutionApiClient.onlyDigits(req(b,"numero_whatsapp"));if(numero.length()<10){HttpResponses.json(ex,400,Map.of("erro","numero_whatsapp_invalido","mensagem","Informe o numero com DDD e codigo do pais."));return;}try(Connection m=masterDs.getMasterConnection()){liberarCadastrosEvolutionOrfaos(m,numero);Map existente=one(m,"SELECT id,empresa_id,status FROM evolution_instancias WHERE numero_whatsapp=? ORDER BY id DESC LIMIT 1",Arrays.asList(numero));if(existente!=null){HttpResponses.json(ex,409,Map.of("erro","numero_whatsapp_ja_cadastrado","mensagem","Este numero de WhatsApp ja esta cadastrado em uma conexao."));return;}}String instance="empresa"+empresaId+"_"+System.currentTimeMillis();String token=UUID.randomUUID().toString().replace("-","");var criada=evolutionApiClient.criarInstancia(url,instance,token,numero);try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("INSERT INTO evolution_instancias (empresa_id,instance_url,instance_token_enc,numero_whatsapp,status) VALUES (?,?,?,?, 'pendente') RETURNING id")){ps.setLong(1,empresaId);ps.setString(2,url);ps.setString(3,criada.instanceName()+":"+criada.token());ps.setString(4,numero);try(ResultSet rs=ps.executeQuery()){rs.next();Map resp=new LinkedHashMap<>();resp.put("id",rs.getLong(1));resp.put("instance_name",criada.instanceName());resp.put("status","pendente");resp.put("qrcode_base64",criada.qrcodeBase64());resp.put("mock",criada.mock());HttpResponses.json(ex,201,resp);}}} + private void conectarEvolution(HttpExchange ex,long empresaId)throws Exception{Mapb=body(ex);String url=b.getOrDefault("instance_url","");String numero=EvolutionApiClient.onlyDigits(req(b,"numero_whatsapp"));if(numero.length()<10){HttpResponses.json(ex,400,Map.of("erro","numero_whatsapp_invalido","mensagem","Informe o número com DDD e código do país."));return;}try(Connection m=masterDs.getMasterConnection()){liberarCadastrosEvolutionOrfaos(m,numero);Map existente=one(m,"SELECT id,empresa_id,status FROM evolution_instancias WHERE numero_whatsapp=? ORDER BY id DESC LIMIT 1",Arrays.asList(numero));if(existente!=null){HttpResponses.json(ex,409,Map.of("erro","numero_whatsapp_ja_cadastrado","mensagem","Este número de WhatsApp já está cadastrado em uma conexão."));return;}}String instance="empresa"+empresaId+"_"+System.currentTimeMillis();String token=UUID.randomUUID().toString().replace("-","");var criada=evolutionApiClient.criarInstancia(url,instance,token,numero);try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("INSERT INTO evolution_instancias (empresa_id,instance_url,instance_token_enc,numero_whatsapp,status) VALUES (?,?,?,?, 'pendente') RETURNING id")){ps.setLong(1,empresaId);ps.setString(2,url);ps.setString(3,criada.instanceName()+":"+criada.token());ps.setString(4,numero);try(ResultSet rs=ps.executeQuery()){rs.next();Map resp=new LinkedHashMap<>();resp.put("id",rs.getLong(1));resp.put("instance_name",criada.instanceName());resp.put("status","pendente");resp.put("qrcode_base64",criada.qrcodeBase64());resp.put("mock",criada.mock());HttpResponses.json(ex,201,resp);}}} private void liberarCadastrosEvolutionOrfaos(Connection m,String numero)throws Exception{List> existentes=rows(m,"SELECT id,instance_url,instance_token_enc FROM evolution_instancias WHERE numero_whatsapp=? ORDER BY id",Arrays.asList(numero));for(Map inst:existentes){String token=str(inst.get("instance_token_enc"));String instanceName=token.split(":",2)[0];if(instanceName.isBlank())continue;try{evolutionApiClient.estado(str(inst.get("instance_url")),instanceName);}catch(Exception err){if(!evolutionNaoEncontrada(err))throw err;try(PreparedStatement ps=m.prepareStatement("DELETE FROM evolution_instancias WHERE id=?")){ps.setLong(1,((Number)inst.get("id")).longValue());ps.executeUpdate();}}}} private boolean evolutionNaoEncontrada(Exception err){String msg=err.getMessage()==null?"":err.getMessage().toLowerCase(Locale.ROOT);return msg.contains("404")||msg.contains("not found")||msg.contains("instance not found")||msg.contains("nao encontrada")||msg.contains("não encontrada");} private void statusEvolution(HttpExchange ex,long empresaId)throws Exception{Map inst=evolutionInstancia(empresaId);if(inst==null){HttpResponses.json(ex,200,Map.of("status","nao_configurada"));return;}Map resp=new LinkedHashMap<>();resp.put("instancia",inst);try{String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2);resp.put("evolution",evolutionApiClient.estado(str(inst.get("instance_url")),parts[0]));}catch(Exception ignored){}HttpResponses.json(ex,200,resp);} @@ -486,7 +486,7 @@ public final class ClienteHandlers implements HttpHandler { } return out; } - private void criarAviso(HttpExchange ex,Connection c,long emp,long cond,long fid)throws Exception{String raw=rawBody(ex);Mapb=SimpleJson.parseObject(removeArrayField(removeArrayField(raw,"documentos"),"destinatarios"));boolean w=bool(b.get("enviar_whatsapp"));boolean e=bool(b.get("enviar_email"));List dests=destinatarios(raw);if((w||e)&&dests.isEmpty()){HttpResponses.json(ex,400,Map.of("erro","destinatario_obrigatorio","mensagem","Selecione ao menos um destinatario para enviar o aviso."));return;}Timestamp dataEnvio=ts(b.get("data_evento"));boolean agendado=dataEnvio!=null&&dataEnvio.after(Timestamp.valueOf(LocalDateTime.now(java.time.ZoneId.of("America/Sao_Paulo"))));if(w&&!funcionalidadeAtiva(emp,"whatsapp_envio")){HttpResponses.json(ex,400,Map.of("erro","funcionalidade_whatsapp_inativa","mensagem","Envio por WhatsApp nao esta liberado para esta empresa. Libere a funcionalidade no painel Master ou desmarque WhatsApp."));return;}if(e&&!funcionalidadeAtiva(emp,"email_envio")){HttpResponses.json(ex,400,Map.of("erro","funcionalidade_email_inativa","mensagem","Envio por e-mail nao esta liberado para esta empresa. Libere a funcionalidade no painel Master ou desmarque E-mail."));return;}if(w&&evolutionAprovada(emp)==null){HttpResponses.json(ex,400,Map.of("erro","instancia_pendente_aprovacao","mensagem","A conexao WhatsApp ainda nao foi aprovada no painel Master."));return;}c.setAutoCommit(false);try{int rec=recorrenciaMeses(b.get("recorrencia_meses"));long aviso=insert(c,"INSERT INTO avisos (condominio_id,assembleia_id,tipo,titulo,mensagem,data_evento,data_expiracao,prioridade,enviar_whatsapp,enviar_email,status_envio,criado_por_funcionario,recorrencia_meses,recorrencia_ativa) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",Arrays.asList(cond,optLong(b.get("assembleia_id")),b.getOrDefault("tipo","aviso"),req(b,"titulo"),req(b,"mensagem"),dataEnvio,ts(b.get("data_expiracao")),b.getOrDefault("prioridade","normal"),w,e,agendado?"agendado":"nao_enviado",fid,rec,rec>0));for(Long d:longArray(raw,"documentos")){insert(c,"INSERT INTO aviso_documentos (aviso_id,documento_id) VALUES (?,?) ON CONFLICT DO NOTHING",Arrays.asList(aviso,d));}for(Destinatario d:dests){insert(c,"INSERT INTO aviso_destinatarios (aviso_id,tipo_destinatario,destinatario_id) VALUES (?,?,?) ON CONFLICT DO NOTHING",Arrays.asList(aviso,d.tipo(),d.id()));}audit(c,fid,"create","avisos",aviso,null,b);if(w||e)enfileirarAviso(c,aviso,dataEnvio);c.commit();detalheAviso(ex,c,cond,aviso);}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}} + private void criarAviso(HttpExchange ex,Connection c,long emp,long cond,long fid)throws Exception{String raw=rawBody(ex);Mapb=SimpleJson.parseObject(removeArrayField(removeArrayField(raw,"documentos"),"destinatarios"));boolean w=bool(b.get("enviar_whatsapp"));boolean e=bool(b.get("enviar_email"));List dests=destinatarios(raw);if((w||e)&&dests.isEmpty()){HttpResponses.json(ex,400,Map.of("erro","destinatario_obrigatorio","mensagem","Selecione ao menos um destinatário para enviar o aviso."));return;}Timestamp dataEnvio=ts(b.get("data_evento"));boolean agendado=dataEnvio!=null&&dataEnvio.after(Timestamp.valueOf(LocalDateTime.now(java.time.ZoneId.of("America/Sao_Paulo"))));if(w&&!funcionalidadeAtiva(emp,"whatsapp_envio")){HttpResponses.json(ex,400,Map.of("erro","funcionalidade_whatsapp_inativa","mensagem","Envio por WhatsApp não está liberado para esta empresa. Libere a funcionalidade no painel Master ou desmarque WhatsApp."));return;}if(e&&!funcionalidadeAtiva(emp,"email_envio")){HttpResponses.json(ex,400,Map.of("erro","funcionalidade_email_inativa","mensagem","Envio por e-mail não está liberado para esta empresa. Libere a funcionalidade no painel Master ou desmarque E-mail."));return;}if(w&&evolutionAprovada(emp)==null){HttpResponses.json(ex,400,Map.of("erro","instancia_pendente_aprovacao","mensagem","A conexão WhatsApp ainda não foi aprovada no painel Master."));return;}c.setAutoCommit(false);try{int rec=recorrenciaMeses(b.get("recorrencia_meses"));long aviso=insert(c,"INSERT INTO avisos (condominio_id,assembleia_id,tipo,titulo,mensagem,data_evento,data_expiracao,prioridade,enviar_whatsapp,enviar_email,status_envio,criado_por_funcionario,recorrencia_meses,recorrencia_ativa) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",Arrays.asList(cond,optLong(b.get("assembleia_id")),b.getOrDefault("tipo","aviso"),req(b,"titulo"),req(b,"mensagem"),dataEnvio,ts(b.get("data_expiracao")),b.getOrDefault("prioridade","normal"),w,e,agendado?"agendado":"nao_enviado",fid,rec,rec>0));for(Long d:longArray(raw,"documentos")){insert(c,"INSERT INTO aviso_documentos (aviso_id,documento_id) VALUES (?,?) ON CONFLICT DO NOTHING",Arrays.asList(aviso,d));}for(Destinatario d:dests){insert(c,"INSERT INTO aviso_destinatarios (aviso_id,tipo_destinatario,destinatario_id) VALUES (?,?,?) ON CONFLICT DO NOTHING",Arrays.asList(aviso,d.tipo(),d.id()));}audit(c,fid,"create","avisos",aviso,null,b);if(w||e)enfileirarAviso(c,aviso,dataEnvio);c.commit();detalheAviso(ex,c,cond,aviso);}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}} private void listarAvisos(HttpExchange ex,Connection c,long cond)throws Exception{HttpResponses.json(ex,200,Map.of("avisos",rows(c,"SELECT * FROM avisos WHERE condominio_id=? ORDER BY data_publicacao DESC,id DESC",Arrays.asList(cond))));} private void detalheAviso(HttpExchange ex,Connection c,long cond,long aviso)throws Exception{Mapa=one(c,"SELECT * FROM avisos WHERE id=? AND condominio_id=?",Arrays.asList(aviso,cond));if(a==null){HttpResponses.json(ex,404,Map.of("erro","aviso_nao_encontrado"));return;}Map resp=new LinkedHashMap<>();resp.put("aviso",a);resp.put("documentos",rows(c,"SELECT ad.*, d.nome_arquivo, d.caminho_armazenamento FROM aviso_documentos ad JOIN documentos d ON d.id=ad.documento_id WHERE ad.aviso_id=?",Arrays.asList(aviso)));resp.put("destinatarios",rows(c,"SELECT * FROM aviso_destinatarios WHERE aviso_id=? ORDER BY tipo_destinatario,destinatario_id",Arrays.asList(aviso)));HttpResponses.json(ex,200,resp);} private void reenviarAviso(HttpExchange ex,Connection c,long emp,long cond,long aviso)throws Exception{try(PreparedStatement ps=c.prepareStatement("UPDATE avisos SET status_envio='nao_enviado' WHERE id=? AND condominio_id=?")){ps.setLong(1,aviso);ps.setLong(2,cond);if(ps.executeUpdate()==0){HttpResponses.json(ex,404,Map.of("erro","aviso_nao_encontrado"));return;}}enviarAviso(c,emp,cond,aviso,false);detalheAviso(ex,c,cond,aviso);} @@ -694,7 +694,7 @@ public final class ClienteHandlers implements HttpHandler { private Map resultMap(ResultSet rs)throws SQLException{Map m=new LinkedHashMap<>();ResultSetMetaData md=rs.getMetaData();for(int i=1;i<=md.getColumnCount();i++)m.put(md.getColumnLabel(i),rs.getObject(i));return m;} private void audit(Connection c,long fid,String acao,String tabela,long rid,Object old,Object neu)throws SQLException{try(PreparedStatement ps=c.prepareStatement("INSERT INTO log_auditoria (funcionario_id,acao,tabela_afetada,registro_id,dados_anteriores,dados_novos) VALUES (?,?,?,?,?::jsonb,?::jsonb)")){ps.setLong(1,fid);ps.setString(2,acao);ps.setString(3,tabela);ps.setLong(4,rid);ps.setString(5,old==null?null:Json.write(old));ps.setString(6,neu==null?null:Json.write(neu));ps.executeUpdate();}} private void bind(PreparedStatement ps,Listparams)throws SQLException{for(int i=0;i body(HttpExchange ex)throws IOException{return SimpleJson.parseObject(rawBody(ex));} private String rawBody(HttpExchange ex)throws IOException{return new String(ex.getRequestBody().readAllBytes(),StandardCharsets.UTF_8);} private String req(Mapm,String k){String v=m.get(k);if(v==null||v.isBlank())throw new IllegalArgumentException("Campo obrigatorio: "+k);return v;} private Object nullable(String v){return v==null||v.isBlank()?null:v;} private Long optLong(String v){return v==null||v.isBlank()?null:Long.parseLong(v);} private long id(String s){return Long.parseLong(s);} private void setDate(PreparedStatement ps,int i,String v)throws SQLException{if(v==null||v.isBlank())ps.setNull(i,Types.DATE);else ps.setDate(i,java.sql.Date.valueOf(LocalDate.parse(v)));} + private Map body(HttpExchange ex)throws IOException{return SimpleJson.parseObject(rawBody(ex));} private String rawBody(HttpExchange ex)throws IOException{return new String(ex.getRequestBody().readAllBytes(),StandardCharsets.UTF_8);} private String req(Mapm,String k){String v=m.get(k);if(v==null||v.isBlank())throw new IllegalArgumentException("Campo obrigatório: "+k);return v;} private Object nullable(String v){return v==null||v.isBlank()?null:v;} private Long optLong(String v){return v==null||v.isBlank()?null:Long.parseLong(v);} private long id(String s){return Long.parseLong(s);} private void setDate(PreparedStatement ps,int i,String v)throws SQLException{if(v==null||v.isBlank())ps.setNull(i,Types.DATE);else ps.setDate(i,java.sql.Date.valueOf(LocalDate.parse(v)));} private Timestamp ts(String v){ if(v==null||v.isBlank())return null; String x=v.trim().replace('T',' ').replace(",", " ").replaceAll("\\s+", " "); @@ -704,8 +704,8 @@ public final class ClienteHandlers implements HttpHandler { if(x.matches("\\d{2}/\\d{2}/\\d{4} \\d{2}:\\d{2}")){LocalDateTime dt=LocalDateTime.parse(x,java.time.format.DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"));return Timestamp.valueOf(dt);} if(x.matches("\\d{2}/\\d{2}/\\d{4} \\d{2}:\\d{2}:\\d{2}")){LocalDateTime dt=LocalDateTime.parse(x,java.time.format.DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"));return Timestamp.valueOf(dt);} }catch(Exception ignored){} - throw new IllegalArgumentException("Data/hora invalida. Use dd/mm/aaaa hh:mm."); - } private int recorrenciaMeses(String v){if(v==null||v.isBlank())return 0;int n=Integer.parseInt(v);if(n==0||n==1||n==3||n==12)return n;throw new IllegalArgumentException("Recorrencia invalida. Use 0, 1, 3 ou 12 meses.");} private boolean bool(String v){return "true".equalsIgnoreCase(v)||"1".equals(v)||"sim".equalsIgnoreCase(v);} private String str(Object v){return v==null?"":String.valueOf(v);} private String firstNonBlank(String...vals){for(String v:vals)if(v!=null&&!v.isBlank())return v;return "";} private String cnpj(String v){String d=v==null?"":v.replaceAll("\\D","");if(d.length()!=14)throw new IllegalArgumentException("CNPJ invalido. Informe 14 digitos.");return d;} + throw new IllegalArgumentException("Data/hora inválida. Use dd/mm/aaaa hh:mm."); + } private int recorrenciaMeses(String v){if(v==null||v.isBlank())return 0;int n=Integer.parseInt(v);if(n==0||n==1||n==3||n==12)return n;throw new IllegalArgumentException("Recorrência inválida. Use 0, 1, 3 ou 12 meses.");} private boolean bool(String v){return "true".equalsIgnoreCase(v)||"1".equals(v)||"sim".equalsIgnoreCase(v);} private String str(Object v){return v==null?"":String.valueOf(v);} private String firstNonBlank(String...vals){for(String v:vals)if(v!=null&&!v.isBlank())return v;return "";} private String cnpj(String v){String d=v==null?"":v.replaceAll("\\D","");if(d.length()!=14)throw new IllegalArgumentException("CNPJ inválido. Informe 14 dígitos.");return d;} private List longArray(String raw,String key){String a=arrayBody(raw,key);if(a.isBlank())return List.of();List out=new ArrayList<>();for(String x:a.split(",")){String v=x.trim().replace("\"","");if(!v.isBlank())out.add(Long.parseLong(v));}return out;} private List stringArray(String raw,String key){String a=arrayBody(raw,key);if(a.isBlank())return List.of();List out=new ArrayList<>();for(String x:a.split(",")){String v=x.trim().replace("\"","");if(!v.isBlank())out.add(v);}return out;} private List destinatarios(String raw){String a=arrayBody(raw,"destinatarios");if(a.isBlank())return List.of();List out=new ArrayList<>();for(String obj:a.split("\\},\\s*\\{")){String x=obj.replace("{","").replace("}","");String tipo=null;Long id=null;for(String kv:x.split(",")){String[]p=kv.split(":",2);if(p.length<2)continue;String k=p[0].trim().replace("\"","");String v=p[1].trim().replace("\"","");if("tipo".equals(k)||"tipo_destinatario".equals(k))tipo=v;if("id".equals(k)||"destinatario_id".equals(k))id=Long.parseLong(v);}if(tipo!=null&&id!=null)out.add(new Destinatario(tipo,id));}return out;} @@ -714,5 +714,5 @@ public final class ClienteHandlers implements HttpHandler { private Map query(HttpExchange ex){Mapq=new HashMap<>();String raw=ex.getRequestURI().getRawQuery();if(raw==null)return q;for(String part:raw.split("&")){String[]kv=part.split("=",2);q.put(URLDecoder.decode(kv[0],StandardCharsets.UTF_8),kv.length>1?URLDecoder.decode(kv[1],StandardCharsets.UTF_8):"");}return q;} private String databaseName(long emp)throws SQLException{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT database_name FROM empresas WHERE id=?")){ps.setLong(1,emp);try(ResultSet rs=ps.executeQuery()){rs.next();return rs.getString(1);}}} private String ext(String name){int p=name.lastIndexOf('.');return p<0?"":name.substring(p+1).toLowerCase();} - private Multipart parseMultipart(HttpExchange ex)throws IOException{String ct=ex.getRequestHeaders().getFirst("Content-Type");if(ct==null||!ct.contains("boundary="))throw new IllegalArgumentException("multipart/form-data esperado");String boundary="--"+ct.substring(ct.indexOf("boundary=")+9);byte[] body=ex.getRequestBody().readAllBytes();String text=new String(body,StandardCharsets.ISO_8859_1);String filename=null,tipo=null;byte[] fileBytes=null;for(String part:text.split(java.util.regex.Pattern.quote(boundary))){if(!part.contains("Content-Disposition"))continue;int hEnd=part.indexOf("\r\n\r\n");if(hEnd<0)continue;String headers=part.substring(0,hEnd);String content=part.substring(hEnd+4);if(content.endsWith("\r\n"))content=content.substring(0,content.length()-2);if(headers.contains("name=\"tipo\""))tipo=content.trim();if(headers.contains("name=\"arquivo\"")){int fp=headers.indexOf("filename=\"");if(fp>=0){int fe=headers.indexOf('"',fp+10);filename=headers.substring(fp+10,fe);}fileBytes=content.getBytes(StandardCharsets.ISO_8859_1);}}if(filename==null||fileBytes==null)throw new IllegalArgumentException("arquivo obrigatorio");return new Multipart(filename,tipo,fileBytes);} record Multipart(String filename,String tipo,byte[]bytes){} + private Multipart parseMultipart(HttpExchange ex)throws IOException{String ct=ex.getRequestHeaders().getFirst("Content-Type");if(ct==null||!ct.contains("boundary="))throw new IllegalArgumentException("multipart/form-data esperado");String boundary="--"+ct.substring(ct.indexOf("boundary=")+9);byte[] body=ex.getRequestBody().readAllBytes();String text=new String(body,StandardCharsets.ISO_8859_1);String filename=null,tipo=null;byte[] fileBytes=null;for(String part:text.split(java.util.regex.Pattern.quote(boundary))){if(!part.contains("Content-Disposition"))continue;int hEnd=part.indexOf("\r\n\r\n");if(hEnd<0)continue;String headers=part.substring(0,hEnd);String content=part.substring(hEnd+4);if(content.endsWith("\r\n"))content=content.substring(0,content.length()-2);if(headers.contains("name=\"tipo\""))tipo=content.trim();if(headers.contains("name=\"arquivo\"")){int fp=headers.indexOf("filename=\"");if(fp>=0){int fe=headers.indexOf('"',fp+10);filename=headers.substring(fp+10,fe);}fileBytes=content.getBytes(StandardCharsets.ISO_8859_1);}}if(filename==null||fileBytes==null)throw new IllegalArgumentException("arquivo obrigatório");return new Multipart(filename,tipo,fileBytes);} record Multipart(String filename,String tipo,byte[]bytes){} } diff --git a/src/main/java/com/seconectai/condocontab/tenant/provisioning/TenantProvisioner.java b/src/main/java/com/seconectai/condocontab/tenant/provisioning/TenantProvisioner.java index 2b09da2..d96cb30 100644 --- a/src/main/java/com/seconectai/condocontab/tenant/provisioning/TenantProvisioner.java +++ b/src/main/java/com/seconectai/condocontab/tenant/provisioning/TenantProvisioner.java @@ -37,14 +37,14 @@ public final class TenantProvisioner { public String databaseNameFromSlug(String slug) { if (slug == null || slug.isBlank()) { - throw new IllegalArgumentException("Slug obrigatorio para provisionamento do tenant"); + throw new IllegalArgumentException("Slug obrigatório para provisionamento do tenant"); } if (!slug.matches("[a-z0-9_-]+")) { - throw new IllegalArgumentException("Slug invalido: use apenas letras minusculas, numeros, hifen ou underscore"); + throw new IllegalArgumentException("Slug inválido: use apenas letras minúsculas, números, hífen ou underscore"); } String normalized = slug.replace('-', '_'); if (!normalized.matches("[a-z0-9_]+")) { - throw new IllegalArgumentException("Slug invalido apos normalizacao"); + throw new IllegalArgumentException("Slug inválido após normalização"); } return "db_empresa_" + normalized; } diff --git a/src/main/webapp/cliente/condominio-detalhe.html b/src/main/webapp/cliente/condominio-detalhe.html index 0f16fef..a47f712 100644 --- a/src/main/webapp/cliente/condominio-detalhe.html +++ b/src/main/webapp/cliente/condominio-detalhe.html @@ -1 +1 @@ -Detalhe do Condominio

Detalhe do Condominio

+Detalhe do Condomínio

Detalhe do Condomínio

diff --git a/src/main/webapp/cliente/condominio-detalhe.js b/src/main/webapp/cliente/condominio-detalhe.js index 7e14232..f0ffc49 100644 --- a/src/main/webapp/cliente/condominio-detalhe.js +++ b/src/main/webapp/cliente/condominio-detalhe.js @@ -1,4 +1,4 @@ -const id=new URLSearchParams(location.search).get('id');const sections=['contatos','espacos','assembleias','documentos','avisos','respostas'];document.querySelectorAll('.nav-tabs button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.nav-tabs button').forEach(x=>x.classList.remove('active'));b.classList.add('active');sections.forEach(s=>document.querySelector('#'+s).classList.toggle('hidden',s!==b.dataset.tab))});let contatos=[],docs=[],avisos=[],respostas=[],funcionariosVinculados=[],extras=[],assembleiasCalendario=[],calMonth=new Date(),espacos=[],assembleias=[],responsaveis=[];const diasNomes=['Domingo','Segunda','Terça','Quarta','Quinta','Sexta','Sábado'];function recLabel(v){return {1:'Mensal',3:'Trimestral',12:'Anual'}[Number(v)]||'Sem recorrencia'} +const id=new URLSearchParams(location.search).get('id');const sections=['contatos','espacos','assembleias','documentos','avisos','respostas'];document.querySelectorAll('.nav-tabs button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.nav-tabs button').forEach(x=>x.classList.remove('active'));b.classList.add('active');sections.forEach(s=>document.querySelector('#'+s).classList.toggle('hidden',s!==b.dataset.tab))});let contatos=[],docs=[],avisos=[],respostas=[],funcionariosVinculados=[],extras=[],assembleiasCalendario=[],calMonth=new Date(),espacos=[],assembleias=[],responsaveis=[];const diasNomes=['Domingo','Segunda','Terça','Quarta','Quinta','Sexta','Sábado'];function recLabel(v){return {1:'Mensal',3:'Trimestral',12:'Anual'}[Number(v)]||'Sem recorrência'} const Icons = { add: '', search: '', @@ -35,16 +35,16 @@ window.abrirAvisoNotificacao=()=>{const m=ensureAvisoNotifModal(),ids=new Set(av window.fecharAvisoNotificacao=()=>document.querySelector('#aviso-notif-modal')?.classList.add('hidden'); window.salvarAvisoNotificacao=()=>{const ids=new Set([...document.querySelectorAll('#aviso-notif-body input:checked')].map(x=>x.value));avisoDestInputs().forEach(x=>x.checked=ids.has(x.value));updateAvisoNotifSummary();fecharAvisoNotificacao()}; async function safeLoad(section,fn){try{await fn()}catch(e){document.querySelector('#'+section).innerHTML=`

${e.message}

`;Util.toast(e.message,true)}} -async function load(){try{const d=await Api.get(`/api/condominios/${id}`);document.querySelector('#header').innerHTML=`

${d.condominio.nome}

${d.condominio.endereco||''}

Sindico${d.sindico_atual?.contato_nome||'-'}
Assembleias futuras${d.assembleias_futuras||0}
Avisos ativos${d.avisos_ativos||0}
`;await safeLoad('contatos',loadContatos);await safeLoad('avisos',loadFuncionariosVinculados);await safeLoad('avisos',loadExtras);await safeLoad('assembleias',loadResponsaveis);await safeLoad('espacos',loadEspacos);await safeLoad('assembleias',loadAssembleias);await safeLoad('documentos',loadDocs);await safeLoad('avisos',loadAvisos);await safeLoad('respostas',loadRespostas)}catch(e){Util.toast(e.message,true)}} +async function load(){try{const d=await Api.get(`/api/condominios/${id}`);document.querySelector('#header').innerHTML=`

${d.condominio.nome}

${d.condominio.endereco||''}

Síndico${d.sindico_atual?.contato_nome||'-'}
Assembleias futuras${d.assembleias_futuras||0}
Avisos ativos${d.avisos_ativos||0}
`;await safeLoad('contatos',loadContatos);await safeLoad('avisos',loadFuncionariosVinculados);await safeLoad('avisos',loadExtras);await safeLoad('assembleias',loadResponsaveis);await safeLoad('espacos',loadEspacos);await safeLoad('assembleias',loadAssembleias);await safeLoad('documentos',loadDocs);await safeLoad('avisos',loadAvisos);await safeLoad('respostas',loadRespostas)}catch(e){Util.toast(e.message,true)}} async function loadContatos(){const d=await Api.get(`/api/condominios/${id}/contatos`);contatos=d.contatos||[];renderContatos()} function renderContatos(){ const q=termoDetalhe('contatos'); const lista=contatos.filter(c=>contemDetalhe(q,c.contato_nome,c.tipo,c.contato_telefone,c.contato_email)); - document.querySelector('#contatos').innerHTML=headerCadastro('Contatos','contatos','abrirNovoContato()')+`
${lista.map(c=>``).join('')||''}
NomeTipoTelefoneEmailAções
${esc(c.contato_nome)}${esc(c.tipo)}${esc(c.contato_telefone||'')}${esc(c.contato_email||'')}
Nenhum contato encontrado.
`; + document.querySelector('#contatos').innerHTML=headerCadastro('Contatos','contatos','abrirNovoContato()')+`
${lista.map(c=>``).join('')||''}
NomeTipoTelefoneE-mailAções
${esc(c.contato_nome)}${esc(c.tipo)}${esc(c.contato_telefone||'')}${esc(c.contato_email||'')}
Nenhum contato encontrado.
`; document.querySelector('#contatos-search-input')?.addEventListener('input',renderContatos); } window.desvincularContatoDetalhe=async (cid,tipo)=>{if(!await Util.confirm('Deseja desvincular este contato do condomínio?','Desvincular contato'))return;try{await Api.request(`/api/condominios/${id}/contatos/${cid}?tipo=${encodeURIComponent(tipo)}`,{method:'DELETE'});Util.toast('Contato desvinculado');loadContatos();load()}catch(e){Util.toast(e.message,true)}}; -window.abrirNovoContato=()=>{abrirDetailModal('Novo contato','
');document.querySelector('#contato-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/contatos/vincular`,Util.formData(e.target));Util.toast('Contato salvo');fecharDetailModal();loadContatos();load()}catch(err){Util.toast(err.message,true)}}}; +window.abrirNovoContato=()=>{abrirDetailModal('Novo contato','
');document.querySelector('#contato-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/contatos/vincular`,Util.formData(e.target));Util.toast('Contato salvo');fecharDetailModal();loadContatos();load()}catch(err){Util.toast(err.message,true)}}}; async function loadEspacos(){const d=await Api.get(`/api/condominios/${id}/espacos-comuns`);espacos=d.espacos||[];renderEspacos()} function renderEspacos(){ @@ -134,7 +134,7 @@ function renderDocs(){ window.excluirDocumentoDetalhe=async did=>{if(!await Util.confirm('Tem certeza que deseja excluir este documento?','Excluir documento'))return;try{await Api.request(`/api/condominios/${id}/documentos/${did}`,{method:'DELETE'});Util.toast('Documento excluído');loadDocs()}catch(err){Util.toast(err.message,true)}}; window.abrirNovoDocumento=()=>{abrirDetailModal('Novo documento','
');document.querySelector('#doc-form').onsubmit=async e=>{e.preventDefault();const fd=new FormData(e.target);try{await Api.post(`/api/condominios/${id}/documentos`,fd);Util.toast('Documento enviado');fecharDetailModal();loadDocs();loadAvisos()}catch(err){Util.toast(err.message,true)}}}; -function avisoFormHtml(){const destContatos=contatos.map(c=>``).join('');const destFuncionarios=funcionariosVinculados.map(f=>``).join('');const destExtras=extras.map(x=>``).join('');const dest=[destContatos,destFuncionarios,destExtras].filter(Boolean).join('');const docChecks=docs.map(d=>``).join('');return `
Nenhuma pessoa selecionada
${docChecks||'

Nenhum documento.

'}
`} +function avisoFormHtml(){const destContatos=contatos.map(c=>``).join('');const destFuncionarios=funcionariosVinculados.map(f=>``).join('');const destExtras=extras.map(x=>``).join('');const dest=[destContatos,destFuncionarios,destExtras].filter(Boolean).join('');const docChecks=docs.map(d=>``).join('');return `
Nenhuma pessoa selecionada
${docChecks||'

Nenhum documento.

'}
`} async function loadAvisos(){const d=await Api.get(`/api/condominios/${id}/avisos`);avisos=d.avisos||[];renderAvisos()} function renderAvisos(){ const q=termoDetalhe('avisos'); diff --git a/src/main/webapp/cliente/condominios.html b/src/main/webapp/cliente/condominios.html index 2a353c6..51475b2 100644 --- a/src/main/webapp/cliente/condominios.html +++ b/src/main/webapp/cliente/condominios.html @@ -1 +1 @@ -Cadastro

Cadastro

Condomínios

IDNomeCNPJEndereçoAções
+Cadastro

Cadastro

Condomínios

IDNomeCNPJEndereçoAções
diff --git a/src/main/webapp/cliente/condominios.js b/src/main/webapp/cliente/condominios.js index 49dad62..042aad5 100644 --- a/src/main/webapp/cliente/condominios.js +++ b/src/main/webapp/cliente/condominios.js @@ -17,7 +17,7 @@ const Icons = { function renderCondominios(){ const q=termo('#cond-search-input'); const lista=condominios.filter(c=>contem(c,q,['id','nome','cnpj','endereco'])); - rows.innerHTML=lista.map(c=>`${c.id}${esc(c.nome)}${esc(c.cnpj||'')}${esc(c.endereco||'')}`).join('')||'Nenhum condominio encontrado.'; + rows.innerHTML=lista.map(c=>`${c.id}${esc(c.nome)}${esc(c.cnpj||'')}${esc(c.endereco||'')}`).join('')||'Nenhum condomínio encontrado.'; } async function load(){ @@ -29,12 +29,12 @@ async function load(){ } window.abrirNovoCondominio=()=>{ - abrirModal('Novo condominio','
'+formActions('Criar')+'
'); + abrirModal('Novo condomínio','
'+formActions('Criar')+'
'); document.querySelector('#cond-form').onsubmit=async e=>{ e.preventDefault(); try{ await Api.post('/api/condominios',Util.formData(e.target)); - Util.toast('Condominio criado'); + Util.toast('Condomínio criado'); fecharModalCondominios(); await load(); await loadFuncionarios(); @@ -66,7 +66,7 @@ function renderFuncionarios(){ const acao=f.cargo==='admin_empresa'?'Acesso total':`
`; const opcoes=f.cargo==='admin_empresa'?'-':`
`; return ''+esc(f.nome)+''+esc(f.email)+''+esc(f.telefone||'')+''+esc(f.cargo||'')+''+Util.badge(f.status)+''+acao+''+opcoes+''; - }).join('')||'Nenhum funcionario encontrado.'; + }).join('')||'Nenhum funcionário encontrado.'; } async function loadFuncionarios(){ @@ -82,12 +82,12 @@ async function loadFuncionarios(){ } window.abrirNovoFuncionario=()=>{ - abrirModal('Novo funcionario','
'+formActions('Cadastrar funcionario')+'
'); + abrirModal('Novo funcionário','
'+formActions('Cadastrar funcionário')+'
'); document.querySelector('#func-form').onsubmit=async e=>{ e.preventDefault(); try{ await Api.post('/api/funcionarios',Util.formData(e.target)); - Util.toast('Funcionario cadastrado'); + Util.toast('Funcionário cadastrado'); fecharModalCondominios(); loadFuncionarios(); }catch(err){Util.toast(err.message,true)} @@ -95,19 +95,19 @@ window.abrirNovoFuncionario=()=>{ }; function funcionarioPorId(funcId){return funcionarios.find(x=>Number(x.id)===Number(funcId))} -window.abrirEditarFuncionario=funcId=>{const f=funcionarioPorId(funcId);if(!f)return;abrirModal('Editar funcionario','
'+formActions('Salvar')+'
');document.querySelector('#func-edit-form').onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/funcionarios/'+funcId,Util.formData(e.target));Util.toast('Funcionario atualizado');fecharModalCondominios();loadFuncionarios()}catch(err){Util.toast(err.message,true)}}}; +window.abrirEditarFuncionario=funcId=>{const f=funcionarioPorId(funcId);if(!f)return;abrirModal('Editar funcionário','
'+formActions('Salvar')+'
');document.querySelector('#func-edit-form').onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/funcionarios/'+funcId,Util.formData(e.target));Util.toast('Funcionário atualizado');fecharModalCondominios();loadFuncionarios()}catch(err){Util.toast(err.message,true)}}}; window.abrirExcluirFuncionario=async funcId=>{const f=funcionarioPorId(funcId);if(!f)return;if(!await Util.confirm('Deseja realmente excluir o acesso de "'+esc(f.nome||'')+'"? O funcionário será inativado e perderá o acesso ao sistema.','Excluir funcionário'))return;try{await Api.request('/api/funcionarios/'+funcId,{method:'DELETE'});Util.toast('Funcionário inativado');loadFuncionarios()}catch(err){Util.toast(err.message,true)}}; function renderModalCondominios(){const f=funcionarios.find(x=>Number(x.id)===Number(funcAtual));if(!f)return;const vinculados=funcLinks.get(Number(funcAtual))||[];const vinculadosIds=new Set(vinculados.map(c=>Number(c.id)));const disponiveis=condominios.filter(c=>!vinculadosIds.has(Number(c.id)));modalTitle.textContent=`Condomínios de ${f.nome}`;const listaVinculados=vinculados.map(c=>`
${esc(c.nome)}
`).join('')||'

Nenhum condomínio vinculado.

';const listaDisponiveis=disponiveis.map(c=>`
${esc(c.nome)}
`).join('')||'

Todos os condomínios já estão vinculados.

';modalBody.innerHTML=``} window.abrirModalCondominios=async funcId=>{funcAtual=Number(funcId);try{await refreshLinks();renderModalCondominios();modal.classList.remove('hidden')}catch(err){Util.toast(err.message,true)}}; window.fecharModalCondominios=()=>{modal.classList.add('hidden');funcAtual=null;modalBody.innerHTML=''}; -window.vincularFuncionarioCondominio=async (funcId,condId)=>{try{await Api.post(`/api/condominios/${condId}/funcionarios-vinculados`,{funcionario_id:funcId});Util.toast('Funcionario vinculado');await refreshLinks();renderModalCondominios()}catch(err){Util.toast(err.message,true)}}; -window.desvincularFuncionarioCondominio=async (funcId,condId)=>{try{await Api.request(`/api/condominios/${condId}/funcionarios-vinculados/${funcId}`,{method:'DELETE'});Util.toast('Funcionario desvinculado');await refreshLinks();renderModalCondominios()}catch(err){Util.toast(err.message,true)}}; +window.vincularFuncionarioCondominio=async (funcId,condId)=>{try{await Api.post(`/api/condominios/${condId}/funcionarios-vinculados`,{funcionario_id:funcId});Util.toast('Funcionário vinculado');await refreshLinks();renderModalCondominios()}catch(err){Util.toast(err.message,true)}}; +window.desvincularFuncionarioCondominio=async (funcId,condId)=>{try{await Api.request(`/api/condominios/${condId}/funcionarios-vinculados/${funcId}`,{method:'DELETE'});Util.toast('Funcionário desvinculado');await refreshLinks();renderModalCondominios()}catch(err){Util.toast(err.message,true)}}; function renderExtras(){ const q=termo('#extra-search-input'); const lista=extras.filter(x=>contem(x,q,['nome','email','telefone','status'])); - extraRows.innerHTML=lista.map(x=>''+esc(x.nome)+''+esc(x.email||'')+''+esc(x.telefone||'')+''+Util.badge(x.status)+'
').join('')||'Nenhum destinatario extra encontrado.'; + extraRows.innerHTML=lista.map(x=>''+esc(x.nome)+''+esc(x.email||'')+''+esc(x.telefone||'')+''+Util.badge(x.status)+'
').join('')||'Nenhum destinatário extra encontrado.'; } async function loadExtras(){ @@ -135,7 +135,7 @@ window.abrirNovoExtra=()=>{ function extraPorId(id){return extras.find(x=>Number(x.id)===Number(id))} window.abrirEditarExtra=id=>{const x=extraPorId(id);if(!x)return;abrirModal('Editar extra','
'+formActions('Salvar')+'
');document.querySelector('#extra-edit-form').onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/extras/'+id,Util.formData(e.target));Util.toast('Extra atualizado');fecharModalCondominios();loadExtras()}catch(err){Util.toast(err.message,true)}}}; -window.excluirExtra=async id=>{if(!await Util.confirm('Tem certeza que deseja excluir este destinatário extra?','Excluir extra'))return;try{await Api.request('/api/extras/'+id,{method:'DELETE'});Util.toast('Extra excluido');loadExtras()}catch(err){Util.toast(err.message,true)}}; +window.excluirExtra=async id=>{if(!await Util.confirm('Tem certeza que deseja excluir este destinatário extra?','Excluir extra'))return;try{await Api.request('/api/extras/'+id,{method:'DELETE'});Util.toast('Extra excluído');loadExtras()}catch(err){Util.toast(err.message,true)}}; window.alternarBuscaCadastro=tipo=>{ const strip=document.querySelector(`#${tipo}-search`),input=document.querySelector(`#${tipo}-search-input`); diff --git a/src/main/webapp/cliente/configuracoes.html b/src/main/webapp/cliente/configuracoes.html index d5afdbb..bb65ed5 100644 --- a/src/main/webapp/cliente/configuracoes.html +++ b/src/main/webapp/cliente/configuracoes.html @@ -1 +1 @@ -WhatsApp

WhatsApp

WhatsApp

Status

\ No newline at end of file +WhatsApp

WhatsApp

WhatsApp

Status

\ No newline at end of file diff --git a/src/main/webapp/cliente/configuracoes.js b/src/main/webapp/cliente/configuracoes.js index d02fef2..ed82353 100644 --- a/src/main/webapp/cliente/configuracoes.js +++ b/src/main/webapp/cliente/configuracoes.js @@ -1 +1 @@ -const form=document.querySelector('#wa-form'),statusEl=document.querySelector('#status'),qr=document.querySelector('#qr'),qrBox=document.querySelector('#qr-box'),pairing=document.querySelector('#pairing'),btnQr=document.querySelector('#btn-qr'),btnReconnect=document.querySelector('#btn-reconnect'),btnDisconnect=document.querySelector('#btn-disconnect');async function load(){try{const d=await Api.get('/api/config/evolution-instancia/status');const i=d.instancia;const evo=d.evolution||{};statusEl.innerHTML=i?`

Status aprovacao: ${Util.badge(i.status)}

Numero: ${i.numero_whatsapp||'-'}

Conexao: ${Util.badge(evo.state||'-')}

`:`

Status: ${Util.badge(d.status)}

`;const has=i!=null;btnQr.disabled=!has;btnReconnect.disabled=!has;btnDisconnect.disabled=!has}catch(e){Util.toast(e.message,true)}}function showQr(d){qrBox.classList.remove('hidden');const base64=d.qrcode_base64||d.qr_code_base64;if(base64){qr.src=base64.startsWith('data:image/')?base64:'data:image/png;base64,'+base64;qr.classList.remove('hidden')}else{qr.classList.add('hidden');Util.toast('Evolution nao retornou QR Code nesta tentativa',true)}pairing.value=d.pairing_code||''}form.onsubmit=async e=>{e.preventDefault();try{const d=await Api.post('/api/config/evolution-instancia/conectar',Util.formData(form));statusEl.innerHTML=`

WhatsApp cadastrado com sucesso. Aguarde a liberacao no painel Master.

Status aprovacao: ${Util.badge(d.status)}

Instancia: ${d.instance_name}

`;showQr(d);Util.toast('WhatsApp cadastrado com sucesso');setTimeout(load,1600)}catch(err){Util.toast(err.message,true)}};btnQr.onclick=async()=>{try{const d=await Api.post('/api/config/evolution-instancia/qrcode',{});showQr(d);Util.toast('QR Code atualizado')}catch(e){Util.toast(e.message,true)}};btnReconnect.onclick=async()=>{try{await Api.post('/api/config/evolution-instancia/reconectar',{});Util.toast('Reconexao solicitada');setTimeout(load,1200)}catch(e){Util.toast(e.message,true)}};btnDisconnect.onclick=async()=>{if(!await Util.confirm('Desconectar esta sessao do WhatsApp?','Desconectar WhatsApp'))return;try{await Api.post('/api/config/evolution-instancia/desconectar',{});Util.toast('Sessao desconectada');setTimeout(load,1200)}catch(e){Util.toast(e.message,true)}};load(); +const form=document.querySelector('#wa-form'),statusEl=document.querySelector('#status'),qr=document.querySelector('#qr'),qrBox=document.querySelector('#qr-box'),pairing=document.querySelector('#pairing'),btnQr=document.querySelector('#btn-qr'),btnReconnect=document.querySelector('#btn-reconnect'),btnDisconnect=document.querySelector('#btn-disconnect');async function load(){try{const d=await Api.get('/api/config/evolution-instancia/status');const i=d.instancia;const evo=d.evolution||{};statusEl.innerHTML=i?`

Status de aprovação: ${Util.badge(i.status)}

Número: ${i.numero_whatsapp||'-'}

Conexão: ${Util.badge(evo.state||'-')}

`:`

Status: ${Util.badge(d.status)}

`;const has=i!=null;btnQr.disabled=!has;btnReconnect.disabled=!has;btnDisconnect.disabled=!has}catch(e){Util.toast(e.message,true)}}function showQr(d){qrBox.classList.remove('hidden');const base64=d.qrcode_base64||d.qr_code_base64;if(base64){qr.src=base64.startsWith('data:image/')?base64:'data:image/png;base64,'+base64;qr.classList.remove('hidden')}else{qr.classList.add('hidden');Util.toast('Evolution não retornou QR Code nesta tentativa',true)}pairing.value=d.pairing_code||''}form.onsubmit=async e=>{e.preventDefault();try{const d=await Api.post('/api/config/evolution-instancia/conectar',Util.formData(form));statusEl.innerHTML=`

WhatsApp cadastrado com sucesso. Aguarde a liberação no painel Master.

Status de aprovação: ${Util.badge(d.status)}

Instância: ${d.instance_name}

`;showQr(d);Util.toast('WhatsApp cadastrado com sucesso');setTimeout(load,1600)}catch(err){Util.toast(err.message,true)}};btnQr.onclick=async()=>{try{const d=await Api.post('/api/config/evolution-instancia/qrcode',{});showQr(d);Util.toast('QR Code atualizado')}catch(e){Util.toast(e.message,true)}};btnReconnect.onclick=async()=>{try{await Api.post('/api/config/evolution-instancia/reconectar',{});Util.toast('Reconexão solicitada');setTimeout(load,1200)}catch(e){Util.toast(e.message,true)}};btnDisconnect.onclick=async()=>{if(!await Util.confirm('Desconectar esta sessão do WhatsApp?','Desconectar WhatsApp'))return;try{await Api.post('/api/config/evolution-instancia/desconectar',{});Util.toast('Sessão desconectada');setTimeout(load,1200)}catch(e){Util.toast(e.message,true)}};load(); diff --git a/src/main/webapp/cliente/financeiro.html b/src/main/webapp/cliente/financeiro.html index 18e03af..38560b2 100644 --- a/src/main/webapp/cliente/financeiro.html +++ b/src/main/webapp/cliente/financeiro.html @@ -1 +1 @@ -Financeiro

Financeiro

Periodo atual

\ No newline at end of file +Financeiro

Financeiro

Período atual

\ No newline at end of file diff --git a/src/main/webapp/cliente/financeiro.js b/src/main/webapp/cliente/financeiro.js index 49b2932..7914600 100644 --- a/src/main/webapp/cliente/financeiro.js +++ b/src/main/webapp/cliente/financeiro.js @@ -2,8 +2,8 @@ let pagamento=null,timer=null;const statusEl=document.querySelector('#status'),a function data(v){if(!v)return'-';const d=new Date(v+'T00:00:00');return Number.isNaN(d.getTime())?String(v):d.toLocaleDateString('pt-BR')} function dataHora(v){if(!v)return'-';const d=new Date(String(v).replace(' ','T'));return Number.isNaN(d.getTime())?String(v):d.toLocaleString('pt-BR')} function diasRestantes(v){const d=new Date(String(v).replace(' ','T'));if(Number.isNaN(d.getTime()))return null;return Math.max(0,Math.ceil((d.getTime()-Date.now())/86400000))} -async function load(){try{const d=await Api.get('/api/financeiro/periodo-atual');pagamento=d.pagamento;const p=d.periodo||{},a=d.assinatura||{};const tempAtiva=p.liberacao_temporaria_ate&&new Date(String(p.liberacao_temporaria_ate).replace(' ','T'))>new Date();alertEl.innerHTML=tempAtiva?'
Liberação temporária ativa

Seu acesso foi liberado manualmente por '+(diasRestantes(p.liberacao_temporaria_ate)||1)+' dia(s), até '+dataHora(p.liberacao_temporaria_ate)+'. O pagamento definitivo continua pendente e deve ser feito pelo PIX.

':'';statusEl.innerHTML='
Periodo de utilizacao'+data(p.data_inicio)+' a '+data(p.data_fim)+''+(tempAtiva?'Liberacao temporaria':'Mes fechado')+'
Vencimento'+data(p.data_vencimento)+'Dia '+(a.dia_vencimento||7)+' de cada mes
Mensalidade'+Util.money(a.valor_mensal_centavos)+''+Util.badge(tempAtiva?'liberado temporariamente':p.status)+'
Pagamento'+Util.badge(pagamento?.status)+'
';if(pagamento&&pagamento.qr_code_base64)showPix(pagamento)}catch(e){Util.toast(e.message,true)}} +async function load(){try{const d=await Api.get('/api/financeiro/periodo-atual');pagamento=d.pagamento;const p=d.periodo||{},a=d.assinatura||{};const tempAtiva=p.liberacao_temporaria_ate&&new Date(String(p.liberacao_temporaria_ate).replace(' ','T'))>new Date();alertEl.innerHTML=tempAtiva?'
Liberação temporária ativa

Seu acesso foi liberado manualmente por '+(diasRestantes(p.liberacao_temporaria_ate)||1)+' dia(s), até '+dataHora(p.liberacao_temporaria_ate)+'. O pagamento definitivo continua pendente e deve ser feito pelo PIX.

':'';statusEl.innerHTML='
Período de utilização'+data(p.data_inicio)+' a '+data(p.data_fim)+''+(tempAtiva?'Liberação temporária':'Mês fechado')+'
Vencimento'+data(p.data_vencimento)+'Dia '+(a.dia_vencimento||7)+' de cada mês
Mensalidade'+Util.money(a.valor_mensal_centavos)+''+Util.badge(tempAtiva?'liberado temporariamente':p.status)+'
Pagamento'+Util.badge(pagamento?.status)+'
';if(pagamento&&pagamento.qr_code_base64)showPix(pagamento)}catch(e){Util.toast(e.message,true)}} function showPix(p){pix.classList.remove('hidden');qr.src='data:image/png;base64,'+p.qr_code_base64;payload.value=p.qr_code_payload||'';if(timer)clearInterval(timer);timer=setInterval(async()=>{const d=await Api.get('/api/financeiro/pagamentos/'+p.id);if(d.pagamento?.status==='pago'){clearInterval(timer);Util.toast('Pagamento confirmado');load()}},5000)} document.querySelector('#gerar').onclick=async()=>{try{const d=await Api.post('/api/financeiro/pagamentos',{});pagamento=d.pagamento;showPix(pagamento);load()}catch(e){Util.toast(e.message,true)}}; -document.querySelector('#copy').onclick=()=>navigator.clipboard.writeText(payload.value).then(()=>Util.toast('Codigo copiado')); +document.querySelector('#copy').onclick=()=>navigator.clipboard.writeText(payload.value).then(()=>Util.toast('Código copiado')); load(); diff --git a/src/main/webapp/cliente/lembretes.html b/src/main/webapp/cliente/lembretes.html index 3e6dd3d..7b13aa0 100644 --- a/src/main/webapp/cliente/lembretes.html +++ b/src/main/webapp/cliente/lembretes.html @@ -1 +1 @@ -Lembrete

Lembrete

Lembretes

TítuloData/HoraStatusDestinatáriosAções
+Lembrete

Lembrete

Lembretes

TítuloData/HoraStatusDestinatáriosAções
diff --git a/src/main/webapp/cliente/lembretes.js b/src/main/webapp/cliente/lembretes.js index 8c6a104..30988c3 100644 --- a/src/main/webapp/cliente/lembretes.js +++ b/src/main/webapp/cliente/lembretes.js @@ -3,7 +3,7 @@ let pessoas=[],lembretes=[],form=null,refreshTimer=null; function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))} function toInputDate(v){if(!v)return'';const d=new Date(v);if(Number.isNaN(d.getTime()))return'';const pad=n=>String(n).padStart(2,'0');return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`} -function recLabel(v){return {1:"Mensal",3:"Trimestral",12:"Anual"}[Number(v)]||"Sem recorrencia"} +function recLabel(v){return {1:"Mensal",3:"Trimestral",12:"Anual"}[Number(v)]||"Sem recorrência"} function termo(){return document.querySelector('#lembrete-search-input')?.value.trim().toLowerCase()||''} function selectedDestIds(){return form?new Set([...form.querySelectorAll('input[name=dest]:checked')].map(x=>String(x.value))):new Set()} function updateNotifSummary(){const n=selectedDestIds().size;const el=document.querySelector('#notif-summary');if(el)el.textContent=n?(n+' selecionado'+(n>1?'s':'')):'Nenhuma pessoa selecionada'} @@ -91,14 +91,14 @@ window.alternarBuscaLembretes=()=>{ }; function ensureNotifModal(){let m=document.querySelector('#notif-modal');if(m)return m;document.body.insertAdjacentHTML('beforeend','');return document.querySelector('#notif-modal')} -window.abrirNotificacao=()=>{if(!form)return;const m=ensureNotifModal(),ids=selectedDestIds();document.querySelector('#notif-modal-body').innerHTML=pessoas.map(p=>'').join('')||'

Nenhum funcionario ativo.

';m.classList.remove('hidden')}; +window.abrirNotificacao=()=>{if(!form)return;const m=ensureNotifModal(),ids=selectedDestIds();document.querySelector('#notif-modal-body').innerHTML=pessoas.map(p=>'').join('')||'

Nenhum funcionário ativo.

';m.classList.remove('hidden')}; window.fecharNotificacao=()=>document.querySelector('#notif-modal')?.classList.add('hidden'); window.salvarNotificacao=()=>{if(!form)return;const ids=new Set([...document.querySelectorAll('#notif-modal-body input:checked')].map(x=>String(x.value)));form.querySelectorAll('input[name=dest]').forEach(x=>x.checked=ids.has(String(x.value)));updateNotifSummary();fecharNotificacao()}; window.editar=id=>{ const l=lembretes.find(x=>Number(x.id)===Number(id)); if(!l)return; - if(!l.pode_editar){Util.toast('Este lembrete ja foi enviado e nao pode mais ser editado.',true);return;} + if(!l.pode_editar){Util.toast('Este lembrete já foi enviado e não pode mais ser editado.',true);return;} modalTitle.textContent='Editar lembrete'; modalBody.innerHTML=formHtml(); modal.classList.remove('hidden'); @@ -111,12 +111,12 @@ window.editar=id=>{ form.recorrencia_meses.value=String(l.recorrencia_meses||0); const ids=new Set((l.destinatarios||[]).map(x=>Number(x.id))); form.querySelectorAll('input[name=dest]').forEach(x=>x.checked=ids.has(Number(x.value))); - document.querySelector('#submit-btn').textContent='Salvar alteracao'; + document.querySelector('#submit-btn').textContent='Salvar alteração'; updateNotifSummary(); }; window.enviar=async id=>{if(!await Util.confirm('Deseja disparar o envio deste lembrete agora?','Enviar lembrete'))return;try{const r=await Api.post(`/api/lembretes/${id}/enviar`,{});const en=r.envio||{};Util.toast(en.falhas?("Enviados: "+(en.enviados||0)+". Falhas: "+en.falhas):("Lembrete enviado para "+(en.enviados||0)));await load()}catch(e){Util.toast(e.message,true)}}; -window.excluir=async id=>{if(!await Util.confirm('Tem certeza que deseja excluir este lembrete? Esta ação não pode ser desfeita.','Excluir lembrete'))return;try{await Api.request(`/api/lembretes/${id}`,{method:'DELETE'});Util.toast('Lembrete excluido');await load();fecharLembreteModal()}catch(e){Util.toast(e.message,true)}}; +window.excluir=async id=>{if(!await Util.confirm('Tem certeza que deseja excluir este lembrete? Esta ação não pode ser desfeita.','Excluir lembrete'))return;try{await Api.request(`/api/lembretes/${id}`,{method:'DELETE'});Util.toast('Lembrete excluído');await load();fecharLembreteModal()}catch(e){Util.toast(e.message,true)}}; document.querySelector('#lembrete-search-input')?.addEventListener('input',renderLembretes); (async()=>{try{await loadPessoas();await load();refreshTimer=setInterval(load,5000)}catch(e){Util.toast(e.message,true)}})(); diff --git a/src/main/webapp/master/configuracoes.html b/src/main/webapp/master/configuracoes.html index d19e71f..8f3b396 100644 --- a/src/main/webapp/master/configuracoes.html +++ b/src/main/webapp/master/configuracoes.html @@ -1 +1 @@ -Configuracoes

Configuracoes

E-mail

AbacatePay

Acessos WhatsApp

EmpresaNumeroStatusSolicitado emAcoes
\ No newline at end of file +Configurações

Configurações

E-mail

AbacatePay

Acessos WhatsApp

EmpresaNúmeroStatusSolicitado emAções
\ No newline at end of file diff --git a/src/main/webapp/master/configuracoes.js b/src/main/webapp/master/configuracoes.js index 0617a59..efe4ac4 100644 --- a/src/main/webapp/master/configuracoes.js +++ b/src/main/webapp/master/configuracoes.js @@ -1 +1 @@ -const emailForm=document.querySelector('#email-form'),abacateForm=document.querySelector('#abacate-form'),emailRows=document.querySelector('#email-rows'),abacateRows=document.querySelector('#abacate-rows'),evolutionRows=document.querySelector('#evolution-rows');let empresas=[];const emailKeys=['smtp_from','smtp_host','smtp_password','smtp_port','smtp_starttls','smtp_user'];const abacateKeys=['abacatepay_api_key','abacatepay_webhook_secret'];function renderConfig(target,cfg,keys,labels){target.innerHTML=keys.map(k=>`

${labels[k]||k}: ${cfg[k]||''}

`).join('')}async function load(){try{const d=await Api.get('/api/master/configuracoes');const cfg=d.configuracoes||{};for(const [k,v] of Object.entries(cfg)){if(emailForm.elements[k])emailForm.elements[k].value=v;if(abacateForm.elements[k])abacateForm.elements[k].value=v}renderConfig(emailRows,cfg,emailKeys,{smtp_from:'Remetente',smtp_host:'Host',smtp_password:'Senha',smtp_port:'Porta',smtp_starttls:'StartTLS',smtp_user:'Usuario'});renderConfig(abacateRows,cfg,abacateKeys,{abacatepay_api_key:'API Key',abacatepay_webhook_secret:'Webhook Secret'});await loadEvolution()}catch(e){Util.toast(e.message,true)}}async function loadEvolution(){const d=await Api.get('/api/master/empresas');empresas=d.empresas||[];const grupos=await Promise.all(empresas.map(async e=>{try{const r=await Api.get(`/api/master/empresas/${e.id}/evolution-instancias`);return (r.instancias||[]).map(i=>({...i,empresa_nome:e.nome}))}catch(_){return[]}}));const instancias=grupos.flat();evolutionRows.innerHTML=instancias.length?instancias.map(i=>`${i.empresa_nome}${i.numero_whatsapp||'-'}${Util.badge(i.status)}${Util.date(i.solicitado_em)} `).join(''):'Nenhuma solicitacao encontrada.'}async function aprovar(emp,id){try{await Api.post(`/api/master/empresas/${emp}/evolution-instancias/${id}/aprovar`,{});Util.toast('Acesso liberado');loadEvolution()}catch(e){Util.toast(e.message,true)}}async function rejeitar(emp,id){try{await Api.post(`/api/master/empresas/${emp}/evolution-instancias/${id}/rejeitar`,{});Util.toast('Acesso rejeitado');loadEvolution()}catch(e){Util.toast(e.message,true)}}async function excluir(emp,id){if(!await Util.confirm('Excluir esta conexao de WhatsApp?','Excluir conexão'))return;try{await Api.request(`/api/master/empresas/${emp}/evolution-instancias/${id}`,{method:'DELETE'});Util.toast('Conexao excluida');loadEvolution()}catch(e){Util.toast(e.message,true)}}emailForm.onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/master/configuracoes',Util.formData(emailForm));Util.toast('Configuracoes de e-mail salvas');load()}catch(err){Util.toast(err.message,true)}};abacateForm.onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/master/configuracoes',Util.formData(abacateForm));Util.toast('Configuracoes AbacatePay salvas');load()}catch(err){Util.toast(err.message,true)}};load(); +const emailForm=document.querySelector('#email-form'),abacateForm=document.querySelector('#abacate-form'),emailRows=document.querySelector('#email-rows'),abacateRows=document.querySelector('#abacate-rows'),evolutionRows=document.querySelector('#evolution-rows');let empresas=[];const emailKeys=['smtp_from','smtp_host','smtp_password','smtp_port','smtp_starttls','smtp_user'];const abacateKeys=['abacatepay_api_key','abacatepay_webhook_secret'];function renderConfig(target,cfg,keys,labels){target.innerHTML=keys.map(k=>`

${labels[k]||k}: ${cfg[k]||''}

`).join('')}async function load(){try{const d=await Api.get('/api/master/configuracoes');const cfg=d.configuracoes||{};for(const [k,v] of Object.entries(cfg)){if(emailForm.elements[k])emailForm.elements[k].value=v;if(abacateForm.elements[k])abacateForm.elements[k].value=v}renderConfig(emailRows,cfg,emailKeys,{smtp_from:'Remetente',smtp_host:'Host',smtp_password:'Senha',smtp_port:'Porta',smtp_starttls:'StartTLS',smtp_user:'Usuário'});renderConfig(abacateRows,cfg,abacateKeys,{abacatepay_api_key:'API Key',abacatepay_webhook_secret:'Webhook Secret'});await loadEvolution()}catch(e){Util.toast(e.message,true)}}async function loadEvolution(){const d=await Api.get('/api/master/empresas');empresas=d.empresas||[];const grupos=await Promise.all(empresas.map(async e=>{try{const r=await Api.get(`/api/master/empresas/${e.id}/evolution-instancias`);return (r.instancias||[]).map(i=>({...i,empresa_nome:e.nome}))}catch(_){return[]}}));const instancias=grupos.flat();evolutionRows.innerHTML=instancias.length?instancias.map(i=>`${i.empresa_nome}${i.numero_whatsapp||'-'}${Util.badge(i.status)}${Util.date(i.solicitado_em)} `).join(''):'Nenhuma solicitação encontrada.'}async function aprovar(emp,id){try{await Api.post(`/api/master/empresas/${emp}/evolution-instancias/${id}/aprovar`,{});Util.toast('Acesso liberado');loadEvolution()}catch(e){Util.toast(e.message,true)}}async function rejeitar(emp,id){try{await Api.post(`/api/master/empresas/${emp}/evolution-instancias/${id}/rejeitar`,{});Util.toast('Acesso rejeitado');loadEvolution()}catch(e){Util.toast(e.message,true)}}async function excluir(emp,id){if(!await Util.confirm('Excluir esta conexão de WhatsApp?','Excluir conexão'))return;try{await Api.request(`/api/master/empresas/${emp}/evolution-instancias/${id}`,{method:'DELETE'});Util.toast('Conexão excluída');loadEvolution()}catch(e){Util.toast(e.message,true)}}emailForm.onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/master/configuracoes',Util.formData(emailForm));Util.toast('Configurações de e-mail salvas');load()}catch(err){Util.toast(err.message,true)}};abacateForm.onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/master/configuracoes',Util.formData(abacateForm));Util.toast('Configurações da AbacatePay salvas');load()}catch(err){Util.toast(err.message,true)}};load(); diff --git a/src/main/webapp/master/financeiro.html b/src/main/webapp/master/financeiro.html index a1ba221..952c9e9 100644 --- a/src/main/webapp/master/financeiro.html +++ b/src/main/webapp/master/financeiro.html @@ -1 +1 @@ -Financeiro Master

Financeiro Master

Situacao das empresas

EmpresaStatusPeriodo de utilizacaoVencimentoAcao
\ No newline at end of file +Financeiro Master

Financeiro Master

Situação das empresas

EmpresaStatusPeríodo de utilizaçãoVencimentoAção
\ No newline at end of file diff --git a/src/main/webapp/master/financeiro.js b/src/main/webapp/master/financeiro.js index ccde91c..f832221 100644 --- a/src/main/webapp/master/financeiro.js +++ b/src/main/webapp/master/financeiro.js @@ -4,5 +4,5 @@ function periodo(e){return ''+data(e.data_inicio)+' a '+data(e.data_fim) async function load(){try{const d=await Api.get('/api/master/empresas');const es=d.empresas||[];cards.innerHTML='
Ativas'+es.filter(e=>e.status==='ativa').length+'
Bloqueadas'+es.filter(e=>e.status==='bloqueada').length+'
Pagas'+es.filter(e=>e.periodo_status==='pago').length+'
';rows.innerHTML=es.map(e=>{const bloqueada=e.status==='bloqueada'||e.periodo_status==='bloqueado';const liberar=bloqueada?' ':'';return ''+e.nome+''+Util.badge(e.status)+''+periodo(e)+'
'+liberar+''}).join('')}catch(e){Util.toast(e.message,true)}} async function salvarVencimento(id,dia){try{await Api.put('/api/master/empresas/'+id+'/vencimento',{dia_vencimento:Number(dia)});Util.toast('Vencimento atualizado');load()}catch(e){Util.toast(e.message,true)}} async function lembrar(id){try{await Api.post('/api/master/empresas/'+id+'/lembrete-cobranca',{canal:'email'});Util.toast('Lembrete registrado')}catch(e){Util.toast(e.message,true)}} -async function desbloquear(id){if(!await Util.confirm('Desbloquear manualmente esta empresa e liberar o periodo atual?','Desbloquear empresa'))return;try{await Api.post('/api/master/empresas/'+id+'/desbloquear',{});Util.toast('Empresa desbloqueada');load()}catch(e){Util.toast(e.message,true)}} +async function desbloquear(id){if(!await Util.confirm('Desbloquear manualmente esta empresa e liberar o período atual?','Desbloquear empresa'))return;try{await Api.post('/api/master/empresas/'+id+'/desbloquear',{});Util.toast('Empresa desbloqueada');load()}catch(e){Util.toast(e.message,true)}} load();