fix(i18n): corrigir acentuacoes da lingua portuguesa em textos e mensagens
This commit is contained in:
@@ -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<String, Object> claims = new LinkedHashMap<>();
|
||||
|
||||
@@ -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<String,String> 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<String,String> query(HttpExchange ex){Map<String,String> 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<String,String> 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<String,String> 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();}}}
|
||||
|
||||
@@ -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{Map<String,String>b=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{Map<String,String>b=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<String,Object> 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<String,Object> 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<String,Object> 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{Map<String,String>b=body(ex); Map<String,Object> 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);Map<String,String>b=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<String,Object> 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<String,Object> 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);Map<String,String>b=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<String,Object> 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<String,Object> 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{Map<String,String>b=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{Map<String,String>b=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<String,Object> criarAvisosAssembleia(Connection c,long emp,long cond,long assembleia,long fid,Map<String,String>b,List<Destinatario>dests,Long responsavelId)throws Exception{
|
||||
List<Destinatario> 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<Long> 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<Map<String,Object>> dests=(List<Map<String,Object>>)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<String,Object> 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<String,Object> 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);Map<String,String>b=SimpleJson.parseObject(removeArrayField(raw,"destinatarios"));List<Long>dests=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<String,Object> 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<String,Object> 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);Map<String,String>b=SimpleJson.parseObject(removeArrayField(raw,"destinatarios"));List<Long>dests=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<String,Object> 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<String,Object> 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<String,Object> lembrete,String cargo,long fid){return adminEmpresa(cargo)||((Number)lembrete.get("criado_por_funcionario")).longValue()==fid;}
|
||||
private Map<String,Object> enviarLembreteWhatsApp(long emp,long lembreteId,Map<String,Object> lembrete)throws Exception{
|
||||
Map<String,Object> 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<Map<String,Object>> dests=(List<Map<String,Object>>)lembrete.getOrDefault("destinatarios",List.of());
|
||||
@@ -415,17 +415,17 @@ public final class ClienteHandlers implements HttpHandler {
|
||||
private Map<String,Object> lembreteComDetalhes(Connection c,long emp,long id)throws Exception{Map<String,Object> 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<Map<String,Object>> destinatariosLembrete(Connection c,long emp,long id)throws Exception{List<Map<String,Object>> ids=rows(c,"SELECT funcionario_id FROM lembrete_destinatarios WHERE lembrete_id=? ORDER BY funcionario_id",Arrays.asList(id));List<Map<String,Object>> 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<String,Object> 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<Map<String,Object>> 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{Map<String,String>b=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{Map<String,String>b=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{
|
||||
Map<String,String>b=body(ex);
|
||||
try(Connection m=masterDs.getMasterConnection()){
|
||||
Map<String,Object> 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<String,Object> 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<String,Object> 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{Map<String,String>b=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<String,Object> 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<String,Object> 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{Map<String,String>b=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<String,Object> 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<String,Object> 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<Map<String,Object>> existentes=rows(m,"SELECT id,instance_url,instance_token_enc FROM evolution_instancias WHERE numero_whatsapp=? ORDER BY id",Arrays.asList(numero));for(Map<String,Object> 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<String,Object> inst=evolutionInstancia(empresaId);if(inst==null){HttpResponses.json(ex,200,Map.of("status","nao_configurada"));return;}Map<String,Object> 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);Map<String,String>b=SimpleJson.parseObject(removeArrayField(removeArrayField(raw,"documentos"),"destinatarios"));boolean w=bool(b.get("enviar_whatsapp"));boolean e=bool(b.get("enviar_email"));List<Destinatario> 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);Map<String,String>b=SimpleJson.parseObject(removeArrayField(removeArrayField(raw,"documentos"),"destinatarios"));boolean w=bool(b.get("enviar_whatsapp"));boolean e=bool(b.get("enviar_email"));List<Destinatario> 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{Map<String,Object>a=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<String,Object> 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<String,Object> resultMap(ResultSet rs)throws SQLException{Map<String,Object> 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,List<?>params)throws SQLException{for(int i=0;i<params.size();i++){Object v=params.get(i); if(v==null)ps.setObject(i+1,null); else ps.setObject(i+1,v);}}
|
||||
private Map<String,String> 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(Map<String,String>m,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<String,String> 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(Map<String,String>m,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<Long> longArray(String raw,String key){String a=arrayBody(raw,key);if(a.isBlank())return List.of();List<Long> 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<String> stringArray(String raw,String key){String a=arrayBody(raw,key);if(a.isBlank())return List.of();List<String> out=new ArrayList<>();for(String x:a.split(",")){String v=x.trim().replace("\"","");if(!v.isBlank())out.add(v);}return out;}
|
||||
private List<Destinatario> destinatarios(String raw){String a=arrayBody(raw,"destinatarios");if(a.isBlank())return List.of();List<Destinatario> 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<String,String> query(HttpExchange ex){Map<String,String>q=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){}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Detalhe do Condominio</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Detalhe do Condominio</h1></div><section class="panel" id="header"></section><div class="nav-tabs"><button data-tab="contatos" class="active">Contatos</button><button data-tab="espacos">Espaços</button><button data-tab="assembleias">Assembleias</button><button data-tab="documentos">Documentos</button><button data-tab="avisos">Avisos</button><button data-tab="respostas">Respostas</button></div><section class="panel tab" id="contatos"></section><section class="panel tab hidden" id="espacos"></section><section class="panel tab hidden" id="assembleias"></section><section class="panel tab hidden" id="documentos"></section><section class="panel tab hidden" id="avisos"></section><section class="panel tab hidden" id="respostas"></section></main><script>Layout.mount('empresa','condominios');</script><script src="/cliente/condominio-detalhe.js"></script></body></html>
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Detalhe do Condomínio</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Detalhe do Condomínio</h1></div><section class="panel" id="header"></section><div class="nav-tabs"><button data-tab="contatos" class="active">Contatos</button><button data-tab="espacos">Espaços</button><button data-tab="assembleias">Assembleias</button><button data-tab="documentos">Documentos</button><button data-tab="avisos">Avisos</button><button data-tab="respostas">Respostas</button></div><section class="panel tab" id="contatos"></section><section class="panel tab hidden" id="espacos"></section><section class="panel tab hidden" id="assembleias"></section><section class="panel tab hidden" id="documentos"></section><section class="panel tab hidden" id="avisos"></section><section class="panel tab hidden" id="respostas"></section></main><script>Layout.mount('empresa','condominios');</script><script src="/cliente/condominio-detalhe.js"></script></body></html>
|
||||
|
||||
@@ -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: '<svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>',
|
||||
search: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg>',
|
||||
@@ -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=`<p>${e.message}</p>`;Util.toast(e.message,true)}}
|
||||
async function load(){try{const d=await Api.get(`/api/condominios/${id}`);document.querySelector('#header').innerHTML=`<h2>${d.condominio.nome}</h2><p>${d.condominio.endereco||''}</p><div class="cards"><div class="stat">Sindico<strong>${d.sindico_atual?.contato_nome||'-'}</strong></div><div class="stat">Assembleias futuras<strong>${d.assembleias_futuras||0}</strong></div><div class="stat">Avisos ativos<strong>${d.avisos_ativos||0}</strong></div></div>`;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=`<h2>${d.condominio.nome}</h2><p>${d.condominio.endereco||''}</p><div class="cards"><div class="stat">Síndico<strong>${d.sindico_atual?.contato_nome||'-'}</strong></div><div class="stat">Assembleias futuras<strong>${d.assembleias_futuras||0}</strong></div><div class="stat">Avisos ativos<strong>${d.avisos_ativos||0}</strong></div></div>`;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()')+`<div class="table-wrap"><table><thead><tr><th>Nome</th><th>Tipo</th><th>Telefone</th><th>Email</th><th style="text-align:right">Ações</th></tr></thead><tbody>${lista.map(c=>`<tr><td>${esc(c.contato_nome)}</td><td>${esc(c.tipo)}</td><td>${esc(c.contato_telefone||'')}</td><td>${esc(c.contato_email||'')}</td><td style="text-align:right"><div class="table-actions"><button class="table-icon-btn danger" type="button" onclick="desvincularContatoDetalhe(${c.contato_id},'${esc(c.tipo)}')" title="Desvincular contato" aria-label="Desvincular contato">${Icons.trash}</button></div></td></tr>`).join('')||'<tr><td colspan="5">Nenhum contato encontrado.</td></tr>'}</tbody></table></div>`;
|
||||
document.querySelector('#contatos').innerHTML=headerCadastro('Contatos','contatos','abrirNovoContato()')+`<div class="table-wrap"><table><thead><tr><th>Nome</th><th>Tipo</th><th>Telefone</th><th>E-mail</th><th style="text-align:right">Ações</th></tr></thead><tbody>${lista.map(c=>`<tr><td>${esc(c.contato_nome)}</td><td>${esc(c.tipo)}</td><td>${esc(c.contato_telefone||'')}</td><td>${esc(c.contato_email||'')}</td><td style="text-align:right"><div class="table-actions"><button class="table-icon-btn danger" type="button" onclick="desvincularContatoDetalhe(${c.contato_id},'${esc(c.tipo)}')" title="Desvincular contato" aria-label="Desvincular contato">${Icons.trash}</button></div></td></tr>`).join('')||'<tr><td colspan="5">Nenhum contato encontrado.</td></tr>'}</tbody></table></div>`;
|
||||
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','<form id="contato-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>Telefone</label><input name="telefone"></div><div class="field"><label>E-mail</label><input name="email"></div><div class="field"><label>Tipo</label><select name="tipo"><option value="sindico">Sindico</option><option value="subsindico">Subsindico</option><option value="conselheiro">Conselheiro</option></select></div><div class="modal-actions" style="grid-column: 1 / -1;"><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button><button class="btn" type="submit">Cadastrar contato</button></div></form>');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','<form id="contato-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>Telefone</label><input name="telefone"></div><div class="field"><label>E-mail</label><input name="email"></div><div class="field"><label>Tipo</label><select name="tipo"><option value="sindico">Síndico</option><option value="subsindico">Subsíndico</option><option value="conselheiro">Conselheiro</option></select></div><div class="modal-actions" style="grid-column: 1 / -1;"><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button><button class="btn" type="submit">Cadastrar contato</button></div></form>');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','<form id="doc-form" class="grid cols"><div class="field"><label>Tipo</label><input name="tipo"></div><div class="field"><label>Arquivo</label><input name="arquivo" type="file" required></div><div class="modal-actions" style="grid-column: 1 / -1;"><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button><button class="btn" type="submit">Enviar documento</button></div></form>');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=>`<label><input type="checkbox" name="dest" value="contato:${c.contato_id}"> ${esc(c.contato_nome)} (${esc(c.tipo)})</label>`).join('');const destFuncionarios=funcionariosVinculados.map(f=>`<label><input type="checkbox" name="dest" value="funcionario:${f.funcionario_id||f.id}"> ${esc(f.nome)} (funcionario)</label>`).join('');const destExtras=extras.map(x=>`<label><input type="checkbox" name="dest" value="extra:${x.id}"> ${esc(x.nome)} (extra)</label>`).join('');const dest=[destContatos,destFuncionarios,destExtras].filter(Boolean).join('');const docChecks=docs.map(d=>`<label><input type="checkbox" name="doc" value="${d.id}"> ${esc(d.nome_arquivo)}</label>`).join('');return `<form id="aviso-form" class="grid cols"><div class="field"><label>Titulo</label><input name="titulo" required></div><div class="field"><label>Tipo</label><input name="tipo" value="aviso"></div><div class="field"><label>Prioridade</label><select name="prioridade"><option>normal</option><option>alta</option><option>urgente</option></select></div><div class="field" style="grid-column: 1 / -1;"><label>Mensagem</label><textarea name="mensagem" required></textarea></div><div class="field"><label>Data/hora de envio</label><input name="data_evento" type="datetime-local"></div><div class="field"><label>Recorrencia</label><select name="recorrencia_meses"><option value="0">Sem recorrencia</option><option value="1">1 vez por mes</option><option value="3">A cada 3 meses</option><option value="12">1 vez por ano</option></select></div><div class="field" style="grid-column: 1 / -1;"><label>Destinatarios</label><div style="display:flex;gap:10px;align-items:center;"><button class="btn secondary" type="button" onclick="abrirAvisoNotificacao()">Notificação</button> <span id="aviso-notif-summary" class="muted">Nenhuma pessoa selecionada</span></div><div id="aviso-dests-hidden" class="hidden">${dest}</div></div><div class="field" style="grid-column: 1 / -1;"><label>Documentos</label>${docChecks||'<p>Nenhum documento.</p>'}</div><div style="grid-column: 1 / -1; display:flex; gap:16px;"><label><input type="checkbox" name="enviar_whatsapp" value="true"> Enviar por WhatsApp</label><label><input type="checkbox" name="enviar_email" value="true"> Enviar por e-mail</label></div><div class="modal-actions" style="grid-column: 1 / -1;"><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button><button class="btn" type="submit">Criar aviso</button></div></form>`}
|
||||
function avisoFormHtml(){const destContatos=contatos.map(c=>`<label><input type="checkbox" name="dest" value="contato:${c.contato_id}"> ${esc(c.contato_nome)} (${esc(c.tipo)})</label>`).join('');const destFuncionarios=funcionariosVinculados.map(f=>`<label><input type="checkbox" name="dest" value="funcionario:${f.funcionario_id||f.id}"> ${esc(f.nome)} (funcionário)</label>`).join('');const destExtras=extras.map(x=>`<label><input type="checkbox" name="dest" value="extra:${x.id}"> ${esc(x.nome)} (extra)</label>`).join('');const dest=[destContatos,destFuncionarios,destExtras].filter(Boolean).join('');const docChecks=docs.map(d=>`<label><input type="checkbox" name="doc" value="${d.id}"> ${esc(d.nome_arquivo)}</label>`).join('');return `<form id="aviso-form" class="grid cols"><div class="field"><label>Título</label><input name="titulo" required></div><div class="field"><label>Tipo</label><input name="tipo" value="aviso"></div><div class="field"><label>Prioridade</label><select name="prioridade"><option>normal</option><option>alta</option><option>urgente</option></select></div><div class="field" style="grid-column: 1 / -1;"><label>Mensagem</label><textarea name="mensagem" required></textarea></div><div class="field"><label>Data/hora de envio</label><input name="data_evento" type="datetime-local"></div><div class="field"><label>Recorrência</label><select name="recorrencia_meses"><option value="0">Sem recorrência</option><option value="1">1 vez por mês</option><option value="3">A cada 3 meses</option><option value="12">1 vez por ano</option></select></div><div class="field" style="grid-column: 1 / -1;"><label>Destinatários</label><div style="display:flex;gap:10px;align-items:center;"><button class="btn secondary" type="button" onclick="abrirAvisoNotificacao()">Notificação</button> <span id="aviso-notif-summary" class="muted">Nenhuma pessoa selecionada</span></div><div id="aviso-dests-hidden" class="hidden">${dest}</div></div><div class="field" style="grid-column: 1 / -1;"><label>Documentos</label>${docChecks||'<p>Nenhum documento.</p>'}</div><div style="grid-column: 1 / -1; display:flex; gap:16px;"><label><input type="checkbox" name="enviar_whatsapp" value="true"> Enviar por WhatsApp</label><label><input type="checkbox" name="enviar_email" value="true"> Enviar por e-mail</label></div><div class="modal-actions" style="grid-column: 1 / -1;"><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button><button class="btn" type="submit">Criar aviso</button></div></form>`}
|
||||
async function loadAvisos(){const d=await Api.get(`/api/condominios/${id}/avisos`);avisos=d.avisos||[];renderAvisos()}
|
||||
function renderAvisos(){
|
||||
const q=termoDetalhe('avisos');
|
||||
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Cadastro</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Cadastro</h1></div><section class="panel"><div class="section-head"><h2>Condomínios</h2><div class="section-actions"><button class="search-icon-btn" type="button" onclick="alternarBuscaCadastro('cond')" aria-label="Buscar condomínios" title="Buscar"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg></button><button class="header-add-btn" type="button" onclick="abrirNovoCondominio()" title="Novo condomínio" aria-label="Novo condomínio"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></button></div></div><div id="cond-search" class="search-strip hidden"><input id="cond-search-input" placeholder="Buscar por nome, CNPJ ou endereço"></div><div class="table-wrap"><table><thead><tr><th>ID</th><th>Nome</th><th>CNPJ</th><th>Endereço</th><th style="text-align:right">Ações</th></tr></thead><tbody id="rows"></tbody></table></div></section><section id="funcionarios-panel" class="panel hidden"><div class="section-head"><h2>Funcionários</h2><div class="section-actions"><button class="search-icon-btn" type="button" onclick="alternarBuscaCadastro('func')" aria-label="Buscar funcionários" title="Buscar"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg></button><button class="header-add-btn" type="button" onclick="abrirNovoFuncionario()" title="Novo funcionário" aria-label="Novo funcionário"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></button></div></div><div id="func-search" class="search-strip hidden"><input id="func-search-input" placeholder="Buscar por nome, e-mail, telefone ou cargo"></div><div class="table-wrap"><table><thead><tr><th>Nome</th><th>E-mail</th><th>Telefone</th><th>Cargo</th><th>Status</th><th>Condomínios</th><th>Ações</th></tr></thead><tbody id="func-rows"></tbody></table></div></section><section id="extras-panel" class="panel hidden"><div class="section-head"><h2>Extra</h2><div class="section-actions"><button class="search-icon-btn" type="button" onclick="alternarBuscaCadastro('extra')" aria-label="Buscar extras" title="Buscar"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg></button><button class="header-add-btn" type="button" onclick="abrirNovoExtra()" title="Novo destinatário extra" aria-label="Novo destinatário extra"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></button></div></div><div id="extra-search" class="search-strip hidden"><input id="extra-search-input" placeholder="Buscar por nome, e-mail ou WhatsApp"></div><div class="table-wrap"><table><thead><tr><th>Nome</th><th>E-mail</th><th>WhatsApp</th><th>Status</th><th>Ações</th></tr></thead><tbody id="extra-rows"></tbody></table></div></section><div id="cond-modal" class="modal hidden"><div class="modal-backdrop" onclick="fecharModalCondominios()"></div><section class="modal-panel"><div class="modal-head"><h2 id="cond-modal-title">Condominios</h2><button class="btn secondary" type="button" onclick="fecharModalCondominios()">Fechar</button></div><div id="cond-modal-body"></div></section></div></main><script>Layout.mount('empresa','condominios');</script><script src="/cliente/condominios.js"></script></body></html>
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Cadastro</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Cadastro</h1></div><section class="panel"><div class="section-head"><h2>Condomínios</h2><div class="section-actions"><button class="search-icon-btn" type="button" onclick="alternarBuscaCadastro('cond')" aria-label="Buscar condomínios" title="Buscar"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg></button><button class="header-add-btn" type="button" onclick="abrirNovoCondominio()" title="Novo condomínio" aria-label="Novo condomínio"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></button></div></div><div id="cond-search" class="search-strip hidden"><input id="cond-search-input" placeholder="Buscar por nome, CNPJ ou endereço"></div><div class="table-wrap"><table><thead><tr><th>ID</th><th>Nome</th><th>CNPJ</th><th>Endereço</th><th style="text-align:right">Ações</th></tr></thead><tbody id="rows"></tbody></table></div></section><section id="funcionarios-panel" class="panel hidden"><div class="section-head"><h2>Funcionários</h2><div class="section-actions"><button class="search-icon-btn" type="button" onclick="alternarBuscaCadastro('func')" aria-label="Buscar funcionários" title="Buscar"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg></button><button class="header-add-btn" type="button" onclick="abrirNovoFuncionario()" title="Novo funcionário" aria-label="Novo funcionário"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></button></div></div><div id="func-search" class="search-strip hidden"><input id="func-search-input" placeholder="Buscar por nome, e-mail, telefone ou cargo"></div><div class="table-wrap"><table><thead><tr><th>Nome</th><th>E-mail</th><th>Telefone</th><th>Cargo</th><th>Status</th><th>Condomínios</th><th>Ações</th></tr></thead><tbody id="func-rows"></tbody></table></div></section><section id="extras-panel" class="panel hidden"><div class="section-head"><h2>Extra</h2><div class="section-actions"><button class="search-icon-btn" type="button" onclick="alternarBuscaCadastro('extra')" aria-label="Buscar extras" title="Buscar"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg></button><button class="header-add-btn" type="button" onclick="abrirNovoExtra()" title="Novo destinatário extra" aria-label="Novo destinatário extra"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></button></div></div><div id="extra-search" class="search-strip hidden"><input id="extra-search-input" placeholder="Buscar por nome, e-mail ou WhatsApp"></div><div class="table-wrap"><table><thead><tr><th>Nome</th><th>E-mail</th><th>WhatsApp</th><th>Status</th><th>Ações</th></tr></thead><tbody id="extra-rows"></tbody></table></div></section><div id="cond-modal" class="modal hidden"><div class="modal-backdrop" onclick="fecharModalCondominios()"></div><section class="modal-panel"><div class="modal-head"><h2 id="cond-modal-title">Condomínios</h2><button class="btn secondary" type="button" onclick="fecharModalCondominios()">Fechar</button></div><div id="cond-modal-body"></div></section></div></main><script>Layout.mount('empresa','condominios');</script><script src="/cliente/condominios.js"></script></body></html>
|
||||
|
||||
@@ -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=>`<tr><td>${c.id}</td><td>${esc(c.nome)}</td><td>${esc(c.cnpj||'')}</td><td>${esc(c.endereco||'')}</td><td style="text-align:right"><div class="table-actions"><a class="table-icon-btn" href="/cliente/condominio-detalhe.html?id=${c.id}" title="Abrir condomínio" aria-label="Abrir condomínio">${Icons.eye}</a></div></td></tr>`).join('')||'<tr><td colspan="5">Nenhum condominio encontrado.</td></tr>';
|
||||
rows.innerHTML=lista.map(c=>`<tr><td>${c.id}</td><td>${esc(c.nome)}</td><td>${esc(c.cnpj||'')}</td><td>${esc(c.endereco||'')}</td><td style="text-align:right"><div class="table-actions"><a class="table-icon-btn" href="/cliente/condominio-detalhe.html?id=${c.id}" title="Abrir condomínio" aria-label="Abrir condomínio">${Icons.eye}</a></div></td></tr>`).join('')||'<tr><td colspan="5">Nenhum condomínio encontrado.</td></tr>';
|
||||
}
|
||||
|
||||
async function load(){
|
||||
@@ -29,12 +29,12 @@ async function load(){
|
||||
}
|
||||
|
||||
window.abrirNovoCondominio=()=>{
|
||||
abrirModal('Novo condominio','<form id="cond-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>CNPJ</label><input name="cnpj" required></div><div class="field"><label>Endereco</label><input name="endereco"></div>'+formActions('Criar')+'</form>');
|
||||
abrirModal('Novo condomínio','<form id="cond-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>CNPJ</label><input name="cnpj" required></div><div class="field"><label>Endereço</label><input name="endereco"></div>'+formActions('Criar')+'</form>');
|
||||
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'?'<span class="muted">Acesso total</span>':`<div class="table-actions start"><button class="table-icon-btn" type="button" onclick="abrirModalCondominios(${f.id})" title="Vincular condomínios" aria-label="Vincular condomínios">${Icons.building}</button></div>`;
|
||||
const opcoes=f.cargo==='admin_empresa'?'<span class="muted">-</span>':`<div class="table-actions start"><button class="table-icon-btn" type="button" onclick="abrirEditarFuncionario(${f.id})" title="Editar funcionário" aria-label="Editar funcionário">${Icons.edit}</button><button class="table-icon-btn danger" type="button" onclick="abrirExcluirFuncionario(${f.id})" title="Excluir funcionário" aria-label="Excluir funcionário">${Icons.trash}</button></div>`;
|
||||
return '<tr><td>'+esc(f.nome)+'</td><td>'+esc(f.email)+'</td><td>'+esc(f.telefone||'')+'</td><td>'+esc(f.cargo||'')+'</td><td>'+Util.badge(f.status)+'</td><td>'+acao+'</td><td>'+opcoes+'</td></tr>';
|
||||
}).join('')||'<tr><td colspan="7">Nenhum funcionario encontrado.</td></tr>';
|
||||
}).join('')||'<tr><td colspan="7">Nenhum funcionário encontrado.</td></tr>';
|
||||
}
|
||||
|
||||
async function loadFuncionarios(){
|
||||
@@ -82,12 +82,12 @@ async function loadFuncionarios(){
|
||||
}
|
||||
|
||||
window.abrirNovoFuncionario=()=>{
|
||||
abrirModal('Novo funcionario','<form id="func-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>E-mail de login</label><input name="email" type="email" required></div><div class="field"><label>WhatsApp</label><input name="telefone"></div><div class="field"><label>Senha inicial</label><input name="senha" type="password" required></div>'+formActions('Cadastrar funcionario')+'</form>');
|
||||
abrirModal('Novo funcionário','<form id="func-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>E-mail de login</label><input name="email" type="email" required></div><div class="field"><label>WhatsApp</label><input name="telefone"></div><div class="field"><label>Senha inicial</label><input name="senha" type="password" required></div>'+formActions('Cadastrar funcionário')+'</form>');
|
||||
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','<form id="func-edit-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required value="'+esc(f.nome||'')+'"></div><div class="field"><label>E-mail de login</label><input name="email" type="email" required value="'+esc(f.email||'')+'"></div><div class="field"><label>WhatsApp</label><input name="telefone" value="'+esc(f.telefone||'')+'"></div><div class="field"><label>Status</label><select name="status"><option value="ativo" '+(f.status==='ativo'?'selected':'')+'>Ativo</option><option value="inativo" '+(f.status==='inativo'?'selected':'')+'>Inativo</option></select></div><div class="field"><label>Nova senha</label><input name="senha" type="password"></div>'+formActions('Salvar')+'</form>');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','<form id="func-edit-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required value="'+esc(f.nome||'')+'"></div><div class="field"><label>E-mail de login</label><input name="email" type="email" required value="'+esc(f.email||'')+'"></div><div class="field"><label>WhatsApp</label><input name="telefone" value="'+esc(f.telefone||'')+'"></div><div class="field"><label>Status</label><select name="status"><option value="ativo" '+(f.status==='ativo'?'selected':'')+'>Ativo</option><option value="inativo" '+(f.status==='inativo'?'selected':'')+'>Inativo</option></select></div><div class="field"><label>Nova senha</label><input name="senha" type="password"></div>'+formActions('Salvar')+'</form>');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=>`<div class="cond-item"><span>${esc(c.nome)}</span><button class="btn danger" type="button" onclick="desvincularFuncionarioCondominio(${funcAtual},${c.id})">Desvincular</button></div>`).join('')||'<p class="muted">Nenhum condomínio vinculado.</p>';const listaDisponiveis=disponiveis.map(c=>`<div class="cond-item"><span>${esc(c.nome)}</span><button class="btn secondary" type="button" onclick="vincularFuncionarioCondominio(${funcAtual},${c.id})">Vincular</button></div>`).join('')||'<p class="muted">Todos os condomínios já estão vinculados.</p>';modalBody.innerHTML=`<div class="modal-grid"><section><h3>Vinculados</h3>${listaVinculados}</section><section><h3>Não vinculados</h3>${listaDisponiveis}</section></div><div class="modal-actions" style="margin-top: 18px;"><button class="btn secondary" type="button" onclick="fecharModalCondominios()">Fechar</button></div>`}
|
||||
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=>'<tr><td>'+esc(x.nome)+'</td><td>'+esc(x.email||'')+'</td><td>'+esc(x.telefone||'')+'</td><td>'+Util.badge(x.status)+'</td><td><div class="table-actions start"><button class="table-icon-btn" type="button" onclick="abrirEditarExtra('+x.id+')" title="Editar extra" aria-label="Editar extra">'+Icons.edit+'</button><button class="table-icon-btn danger" type="button" onclick="excluirExtra('+x.id+')" title="Excluir extra" aria-label="Excluir extra">'+Icons.trash+'</button></div></td></tr>').join('')||'<tr><td colspan="5">Nenhum destinatario extra encontrado.</td></tr>';
|
||||
extraRows.innerHTML=lista.map(x=>'<tr><td>'+esc(x.nome)+'</td><td>'+esc(x.email||'')+'</td><td>'+esc(x.telefone||'')+'</td><td>'+Util.badge(x.status)+'</td><td><div class="table-actions start"><button class="table-icon-btn" type="button" onclick="abrirEditarExtra('+x.id+')" title="Editar extra" aria-label="Editar extra">'+Icons.edit+'</button><button class="table-icon-btn danger" type="button" onclick="excluirExtra('+x.id+')" title="Excluir extra" aria-label="Excluir extra">'+Icons.trash+'</button></div></td></tr>').join('')||'<tr><td colspan="5">Nenhum destinatário extra encontrado.</td></tr>';
|
||||
}
|
||||
|
||||
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','<form id="extra-edit-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required value="'+esc(x.nome||'')+'"></div><div class="field"><label>E-mail</label><input name="email" type="email" value="'+esc(x.email||'')+'"></div><div class="field"><label>WhatsApp</label><input name="telefone" value="'+esc(x.telefone||'')+'"></div><div class="field"><label>Status</label><select name="status"><option value="ativo" '+(x.status==='ativo'?'selected':'')+'>Ativo</option><option value="inativo" '+(x.status==='inativo'?'selected':'')+'>Inativo</option></select></div>'+formActions('Salvar')+'</form>');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`);
|
||||
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>WhatsApp</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>WhatsApp</h1></div><section class="panel"><h2>WhatsApp</h2><form id="wa-form" class="row"><div class="field"><label>Numero WhatsApp</label><input name="numero_whatsapp" required placeholder="5511999999999"></div><button class="btn">Cadastrar</button></form></section><section class="panel"><h2>Status</h2><div id="status"></div><div class="row"><button id="btn-qr" class="btn secondary" type="button">Conectar via QR Code</button><button id="btn-reconnect" class="btn secondary" type="button">Reconectar</button><button id="btn-disconnect" class="btn danger" type="button">Desconectar</button></div><div id="qr-box" class="hidden"><h2>QR Code</h2><img id="qr" class="qr hidden" alt="QR Code Evolution"><div class="field"><label>Codigo de pareamento</label><input id="pairing" readonly></div></div></section></main><script>Layout.mount('empresa','configuracoes');</script><script src="/cliente/configuracoes.js"></script></body></html>
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>WhatsApp</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>WhatsApp</h1></div><section class="panel"><h2>WhatsApp</h2><form id="wa-form" class="row"><div class="field"><label>Número WhatsApp</label><input name="numero_whatsapp" required placeholder="5511999999999"></div><button class="btn">Cadastrar</button></form></section><section class="panel"><h2>Status</h2><div id="status"></div><div class="row"><button id="btn-qr" class="btn secondary" type="button">Conectar via QR Code</button><button id="btn-reconnect" class="btn secondary" type="button">Reconectar</button><button id="btn-disconnect" class="btn danger" type="button">Desconectar</button></div><div id="qr-box" class="hidden"><h2>QR Code</h2><img id="qr" class="qr hidden" alt="QR Code Evolution"><div class="field"><label>Código de pareamento</label><input id="pairing" readonly></div></div></section></main><script>Layout.mount('empresa','configuracoes');</script><script src="/cliente/configuracoes.js"></script></body></html>
|
||||
@@ -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?`<p>Status aprovacao: ${Util.badge(i.status)}</p><p>Numero: ${i.numero_whatsapp||'-'}</p><p>Conexao: ${Util.badge(evo.state||'-')}</p>`:`<p>Status: ${Util.badge(d.status)}</p>`;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=`<p class="success-message">WhatsApp cadastrado com sucesso. Aguarde a liberacao no painel Master.</p><p>Status aprovacao: ${Util.badge(d.status)}</p><p>Instancia: ${d.instance_name}</p>`;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?`<p>Status de aprovação: ${Util.badge(i.status)}</p><p>Número: ${i.numero_whatsapp||'-'}</p><p>Conexão: ${Util.badge(evo.state||'-')}</p>`:`<p>Status: ${Util.badge(d.status)}</p>`;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=`<p class="success-message">WhatsApp cadastrado com sucesso. Aguarde a liberação no painel Master.</p><p>Status de aprovação: ${Util.badge(d.status)}</p><p>Instância: ${d.instance_name}</p>`;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();
|
||||
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Financeiro</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Financeiro</h1></div><section class="panel"><h2>Periodo atual</h2><div id="finance-alert"></div><div id="status" class="cards"></div><button id="gerar" class="btn">Gerar cobranca PIX</button></section><section id="pix" class="panel hidden"><h2>PIX</h2><img id="qr" class="qr" alt="QR Code PIX"><div class="field"><label>Copia e cola</label><textarea id="payload" readonly></textarea></div><button id="copy" class="btn secondary">Copiar</button></section></main><script>Layout.mount('empresa','financeiro');</script><script src="/cliente/financeiro.js"></script></body></html>
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Financeiro</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Financeiro</h1></div><section class="panel"><h2>Período atual</h2><div id="finance-alert"></div><div id="status" class="cards"></div><button id="gerar" class="btn">Gerar cobrança PIX</button></section><section id="pix" class="panel hidden"><h2>PIX</h2><img id="qr" class="qr" alt="QR Code PIX"><div class="field"><label>Copia e cola</label><textarea id="payload" readonly></textarea></div><button id="copy" class="btn secondary">Copiar</button></section></main><script>Layout.mount('empresa','financeiro');</script><script src="/cliente/financeiro.js"></script></body></html>
|
||||
@@ -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?'<div class="inline-panel finance-temp"><strong>Liberação temporária ativa</strong><p>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.</p></div>':'';statusEl.innerHTML='<div class="stat">Periodo de utilizacao<strong>'+data(p.data_inicio)+' a '+data(p.data_fim)+'</strong><span class="muted">'+(tempAtiva?'Liberacao temporaria':'Mes fechado')+'</span></div><div class="stat">Vencimento<strong>'+data(p.data_vencimento)+'</strong><span class="muted">Dia '+(a.dia_vencimento||7)+' de cada mes</span></div><div class="stat">Mensalidade<strong>'+Util.money(a.valor_mensal_centavos)+'</strong><span>'+Util.badge(tempAtiva?'liberado temporariamente':p.status)+'</span></div><div class="stat">Pagamento<strong>'+Util.badge(pagamento?.status)+'</strong></div>';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?'<div class="inline-panel finance-temp"><strong>Liberação temporária ativa</strong><p>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.</p></div>':'';statusEl.innerHTML='<div class="stat">Período de utilização<strong>'+data(p.data_inicio)+' a '+data(p.data_fim)+'</strong><span class="muted">'+(tempAtiva?'Liberação temporária':'Mês fechado')+'</span></div><div class="stat">Vencimento<strong>'+data(p.data_vencimento)+'</strong><span class="muted">Dia '+(a.dia_vencimento||7)+' de cada mês</span></div><div class="stat">Mensalidade<strong>'+Util.money(a.valor_mensal_centavos)+'</strong><span>'+Util.badge(tempAtiva?'liberado temporariamente':p.status)+'</span></div><div class="stat">Pagamento<strong>'+Util.badge(pagamento?.status)+'</strong></div>';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();
|
||||
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Lembrete</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Lembrete</h1></div><section class="panel"><div class="section-head"><h2>Lembretes</h2><div class="section-actions"><button class="search-icon-btn" type="button" onclick="alternarBuscaLembretes()" aria-label="Buscar lembretes" title="Buscar"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg></button><button class="header-add-btn" type="button" onclick="abrirNovoLembrete()" title="Novo lembrete" aria-label="Novo lembrete"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></button></div></div><div id="lembrete-search" class="search-strip hidden"><input id="lembrete-search-input" placeholder="Buscar por titulo, mensagem, status ou destinatario"></div><div class="table-wrap"><table><thead><tr><th>Título</th><th>Data/Hora</th><th>Status</th><th>Destinatários</th><th style="text-align:right">Ações</th></tr></thead><tbody id="rows"></tbody></table></div></section><div id="lembrete-modal" class="modal hidden"><div class="modal-backdrop" onclick="fecharLembreteModal()"></div><section class="modal-panel"><div class="modal-head"><h2 id="lembrete-modal-title">Novo lembrete</h2><button class="btn secondary" type="button" onclick="fecharLembreteModal()">Fechar</button></div><div id="lembrete-modal-body"></div></section></div></main><script>Layout.mount('empresa','lembretes');</script><script src="/cliente/lembretes.js"></script></body></html>
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Lembrete</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Lembrete</h1></div><section class="panel"><div class="section-head"><h2>Lembretes</h2><div class="section-actions"><button class="search-icon-btn" type="button" onclick="alternarBuscaLembretes()" aria-label="Buscar lembretes" title="Buscar"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg></button><button class="header-add-btn" type="button" onclick="abrirNovoLembrete()" title="Novo lembrete" aria-label="Novo lembrete"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></button></div></div><div id="lembrete-search" class="search-strip hidden"><input id="lembrete-search-input" placeholder="Buscar por título, mensagem, status ou destinatário"></div><div class="table-wrap"><table><thead><tr><th>Título</th><th>Data/Hora</th><th>Status</th><th>Destinatários</th><th style="text-align:right">Ações</th></tr></thead><tbody id="rows"></tbody></table></div></section><div id="lembrete-modal" class="modal hidden"><div class="modal-backdrop" onclick="fecharLembreteModal()"></div><section class="modal-panel"><div class="modal-head"><h2 id="lembrete-modal-title">Novo lembrete</h2><button class="btn secondary" type="button" onclick="fecharLembreteModal()">Fechar</button></div><div id="lembrete-modal-body"></div></section></div></main><script>Layout.mount('empresa','lembretes');</script><script src="/cliente/lembretes.js"></script></body></html>
|
||||
|
||||
@@ -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','<div id="notif-modal" class="modal hidden"><div class="modal-backdrop" onclick="fecharNotificacao()"></div><section class="modal-panel"><div class="modal-head"><h2>Notificação</h2><button class="btn secondary" type="button" onclick="fecharNotificacao()">Fechar</button></div><div id="notif-modal-body"></div><div class="modal-actions"><button class="btn secondary" type="button" onclick="fecharNotificacao()">Cancelar</button><button class="btn" type="button" onclick="salvarNotificacao()">Salvar</button></div></section></div>');return document.querySelector('#notif-modal')}
|
||||
window.abrirNotificacao=()=>{if(!form)return;const m=ensureNotifModal(),ids=selectedDestIds();document.querySelector('#notif-modal-body').innerHTML=pessoas.map(p=>'<label><input type="checkbox" value="'+p.id+'" '+(ids.has(String(p.id))?'checked':'')+'> '+esc(p.nome)+' ('+esc(p.cargo||'')+')</label>').join('')||'<p class="muted">Nenhum funcionario ativo.</p>';m.classList.remove('hidden')};
|
||||
window.abrirNotificacao=()=>{if(!form)return;const m=ensureNotifModal(),ids=selectedDestIds();document.querySelector('#notif-modal-body').innerHTML=pessoas.map(p=>'<label><input type="checkbox" value="'+p.id+'" '+(ids.has(String(p.id))?'checked':'')+'> '+esc(p.nome)+' ('+esc(p.cargo||'')+')</label>').join('')||'<p class="muted">Nenhum funcionário ativo.</p>';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)}})();
|
||||
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Configuracoes</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Configuracoes</h1></div><section class="panel"><h2>E-mail</h2><form id="email-form" class="grid cols"><div class="field"><label>Host</label><input name="smtp_host"></div><div class="field"><label>Porta</label><input name="smtp_port" type="number"></div><div class="field"><label>Usuario</label><input name="smtp_user"></div><div class="field"><label>Senha</label><input name="smtp_password" type="password"></div><div class="field"><label>Remetente</label><input name="smtp_from"></div><div class="field"><label>StartTLS</label><select name="smtp_starttls"><option value="true">Sim</option><option value="false">Nao</option></select></div><button class="btn">Salvar e-mail</button></form><div id="email-rows"></div></section><section class="panel"><h2>AbacatePay</h2><form id="abacate-form" class="grid cols"><div class="field"><label>API Key</label><input name="abacatepay_api_key" type="password" placeholder="Cole a chave da AbacatePay"></div><div class="field"><label>Webhook Secret</label><input name="abacatepay_webhook_secret" type="password" placeholder="Segredo do webhook"></div><button class="btn">Salvar AbacatePay</button></form><div id="abacate-rows"></div></section><section class="panel"><h2>Acessos WhatsApp</h2><div class="table-wrap"><table><thead><tr><th>Empresa</th><th>Numero</th><th>Status</th><th>Solicitado em</th><th>Acoes</th></tr></thead><tbody id="evolution-rows"></tbody></table></div></section></main><script>Layout.mount('master','configuracoes');</script><script src="/master/configuracoes.js"></script></body></html>
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Configurações</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Configurações</h1></div><section class="panel"><h2>E-mail</h2><form id="email-form" class="grid cols"><div class="field"><label>Host</label><input name="smtp_host"></div><div class="field"><label>Porta</label><input name="smtp_port" type="number"></div><div class="field"><label>Usuário</label><input name="smtp_user"></div><div class="field"><label>Senha</label><input name="smtp_password" type="password"></div><div class="field"><label>Remetente</label><input name="smtp_from"></div><div class="field"><label>StartTLS</label><select name="smtp_starttls"><option value="true">Sim</option><option value="false">Não</option></select></div><button class="btn">Salvar e-mail</button></form><div id="email-rows"></div></section><section class="panel"><h2>AbacatePay</h2><form id="abacate-form" class="grid cols"><div class="field"><label>API Key</label><input name="abacatepay_api_key" type="password" placeholder="Cole a chave da AbacatePay"></div><div class="field"><label>Webhook Secret</label><input name="abacatepay_webhook_secret" type="password" placeholder="Segredo do webhook"></div><button class="btn">Salvar AbacatePay</button></form><div id="abacate-rows"></div></section><section class="panel"><h2>Acessos WhatsApp</h2><div class="table-wrap"><table><thead><tr><th>Empresa</th><th>Número</th><th>Status</th><th>Solicitado em</th><th>Ações</th></tr></thead><tbody id="evolution-rows"></tbody></table></div></section></main><script>Layout.mount('master','configuracoes');</script><script src="/master/configuracoes.js"></script></body></html>
|
||||
@@ -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=>`<p><strong>${labels[k]||k}</strong>: ${cfg[k]||''}</p>`).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=>`<tr><td>${i.empresa_nome}</td><td>${i.numero_whatsapp||'-'}</td><td>${Util.badge(i.status)}</td><td>${Util.date(i.solicitado_em)}</td><td><button class="btn secondary" onclick="aprovar(${i.empresa_id},${i.id})">Aprovar</button> <button class="btn danger" onclick="rejeitar(${i.empresa_id},${i.id})">Rejeitar</button> <button class="btn danger" onclick="excluir(${i.empresa_id},${i.id})">Excluir</button></td></tr>`).join(''):'<tr><td colspan="5">Nenhuma solicitacao encontrada.</td></tr>'}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=>`<p><strong>${labels[k]||k}</strong>: ${cfg[k]||''}</p>`).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=>`<tr><td>${i.empresa_nome}</td><td>${i.numero_whatsapp||'-'}</td><td>${Util.badge(i.status)}</td><td>${Util.date(i.solicitado_em)}</td><td><button class="btn secondary" onclick="aprovar(${i.empresa_id},${i.id})">Aprovar</button> <button class="btn danger" onclick="rejeitar(${i.empresa_id},${i.id})">Rejeitar</button> <button class="btn danger" onclick="excluir(${i.empresa_id},${i.id})">Excluir</button></td></tr>`).join(''):'<tr><td colspan="5">Nenhuma solicitação encontrada.</td></tr>'}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();
|
||||
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Financeiro Master</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Financeiro Master</h1></div><section class="panel"><h2>Situacao das empresas</h2><div class="cards" id="cards"></div><div class="table-wrap"><table><thead><tr><th>Empresa</th><th>Status</th><th>Periodo de utilizacao</th><th>Vencimento</th><th>Acao</th></tr></thead><tbody id="rows"></tbody></table></div></section></main><script>Layout.mount('master','financeiro');</script><script src="/master/financeiro.js"></script></body></html>
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Financeiro Master</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Financeiro Master</h1></div><section class="panel"><h2>Situação das empresas</h2><div class="cards" id="cards"></div><div class="table-wrap"><table><thead><tr><th>Empresa</th><th>Status</th><th>Período de utilização</th><th>Vencimento</th><th>Ação</th></tr></thead><tbody id="rows"></tbody></table></div></section></main><script>Layout.mount('master','financeiro');</script><script src="/master/financeiro.js"></script></body></html>
|
||||
@@ -4,5 +4,5 @@ function periodo(e){return '<strong>'+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='<div class="stat">Ativas<strong>'+es.filter(e=>e.status==='ativa').length+'</strong></div><div class="stat">Bloqueadas<strong>'+es.filter(e=>e.status==='bloqueada').length+'</strong></div><div class="stat">Pagas<strong>'+es.filter(e=>e.periodo_status==='pago').length+'</strong></div>';rows.innerHTML=es.map(e=>{const bloqueada=e.status==='bloqueada'||e.periodo_status==='bloqueado';const liberar=bloqueada?' <button class="btn" onclick="desbloquear('+e.id+')">Desbloquear</button>':'';return '<tr><td>'+e.nome+'</td><td>'+Util.badge(e.status)+'</td><td>'+periodo(e)+'</td><td><div class="field inline"><label>Dia vencimento</label><input type="number" min="1" max="28" value="'+(e.dia_vencimento||7)+'" onchange="salvarVencimento('+e.id+',this.value)"></div></td><td><button class="btn secondary" onclick="lembrar('+e.id+')">Lembrete</button>'+liberar+'</td></tr>'}).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();
|
||||
|
||||
Reference in New Issue
Block a user