Adiciona responsaveis de assembleia

This commit is contained in:
Tiago
2026-10-09 08:56:02 -03:00
parent 5ad451b7d8
commit 839986a8c8
4 changed files with 102 additions and 9 deletions
@@ -65,6 +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&&"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; }
@@ -115,12 +117,14 @@ public final class ClienteHandlers implements HttpHandler {
String raw=rawBody(ex);
Map<String,String>b=SimpleJson.parseObject(removeArrayField(raw,"destinatarios"));
List<Destinatario>dests=destinatarios(raw);
if(raw.contains("\"destinatarios\"")&&dests.isEmpty()){HttpResponses.json(ex,400,Map.of("erro","destinatario_obrigatorio","mensagem","Selecione ao menos um membro para receber as mensagens da assembleia."));return;}
Timestamp dataHora=ts(req(b,"data_hora"));
Map<String,Object> conflito=validarAgendaResponsaveisAssembleia(c,dataHora,null);
if(conflito!=null){HttpResponses.json(ex,409,conflito);return;}
long id;
Map<String,Object> envio;
c.setAutoCommit(false);
try{
id=insert(c,"INSERT INTO assembleias (condominio_id,tipo,data_hora,local,pauta) VALUES (?,?,?,?,?)",Arrays.asList(cond,req(b,"tipo"),ts(req(b,"data_hora")),nullable(b.get("local")),nullable(b.get("pauta"))));
id=insert(c,"INSERT INTO assembleias (condominio_id,tipo,data_hora,local,pauta) VALUES (?,?,?,?,?)",Arrays.asList(cond,req(b,"tipo"),dataHora,nullable(b.get("local")),nullable(b.get("pauta"))));
audit(c,fid,"create","assembleias",id,null,b);
envio=criarAvisosAssembleia(c,emp,cond,id,fid,b,dests);
c.commit();
@@ -155,6 +159,7 @@ public final class ClienteHandlers implements HttpHandler {
}
for(Map<String,Object> row:rows(c,"SELECT funcionario_id FROM funcionario_condominio WHERE condominio_id=?",Arrays.asList(cond)))addDestinatario(out,vistos,"funcionario",((Number)row.get("funcionario_id")).longValue());
for(Map<String,Object> row:rows(c,"SELECT id FROM destinatarios_extra WHERE status='ativo'",Arrays.asList()))addDestinatario(out,vistos,"extra",((Number)row.get("id")).longValue());
for(Map<String,Object> row:rows(c,"SELECT id FROM assembleia_responsaveis WHERE status='ativo'",Arrays.asList()))addDestinatario(out,vistos,"assembleia",((Number)row.get("id")).longValue());
return out;
}
private void addDestinatario(List<Destinatario> out,Set<String> vistos,String tipo,long id){if(vistos.add(tipo+":"+id))out.add(new Destinatario(tipo,id));}
@@ -174,7 +179,7 @@ public final class ClienteHandlers implements HttpHandler {
if(b.get("pauta")!=null&&!b.get("pauta").isBlank())mensagem.append("\nPauta: ").append(b.get("pauta"));
return mensagem.toString();
}
private void atualizarAssembleia(HttpExchange ex,Connection c,long emp,long cond,long aid,long fid)throws Exception{Map<String,String>b=body(ex); Map<String,Object> old=one(c,"SELECT * FROM assembleias WHERE id=? AND condominio_id=?",Arrays.asList(aid,cond)); if(old==null){HttpResponses.json(ex,404,Map.of("erro","assembleia_nao_encontrada"));return;} c.setAutoCommit(false);try{try(PreparedStatement ps=c.prepareStatement("UPDATE assembleias SET tipo=COALESCE(?,tipo), data_hora=COALESCE(?,data_hora), local=COALESCE(?,local), pauta=COALESCE(?,pauta), status=COALESCE(?,status) WHERE id=? AND condominio_id=?")){ps.setString(1,b.get("tipo")); if(b.get("data_hora")==null)ps.setNull(2,Types.TIMESTAMP);else ps.setTimestamp(2,ts(b.get("data_hora"))); ps.setString(3,b.get("local"));ps.setString(4,b.get("pauta"));ps.setString(5,b.get("status"));ps.setLong(6,aid);ps.setLong(7,cond);ps.executeUpdate();} if("cancelada".equalsIgnoreCase(b.get("status"))&&!"cancelada".equalsIgnoreCase(str(old.get("status"))))criarAvisoCancelamentoAssembleia(c,emp,cond,aid,fid,old); audit(c,fid,"update","assembleias",aid,old,b); c.commit();HttpResponses.json(ex,200,Map.of("assembleia",one(c,"SELECT * FROM assembleias WHERE id=?",Arrays.asList(aid))));}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}}
private void atualizarAssembleia(HttpExchange ex,Connection c,long emp,long cond,long aid,long fid)throws Exception{Map<String,String>b=body(ex); Map<String,Object> old=one(c,"SELECT * FROM assembleias WHERE id=? AND condominio_id=?",Arrays.asList(aid,cond)); if(old==null){HttpResponses.json(ex,404,Map.of("erro","assembleia_nao_encontrada"));return;} Timestamp novaData=b.get("data_hora")==null?null:ts(b.get("data_hora")); if(novaData!=null){Map<String,Object> conflito=validarAgendaResponsaveisAssembleia(c,novaData,aid); if(conflito!=null){HttpResponses.json(ex,409,conflito);return;}} c.setAutoCommit(false);try{try(PreparedStatement ps=c.prepareStatement("UPDATE assembleias SET tipo=COALESCE(?,tipo), data_hora=COALESCE(?,data_hora), local=COALESCE(?,local), pauta=COALESCE(?,pauta), status=COALESCE(?,status) WHERE id=? AND condominio_id=?")){ps.setString(1,b.get("tipo")); if(novaData==null)ps.setNull(2,Types.TIMESTAMP);else ps.setTimestamp(2,novaData); ps.setString(3,b.get("local"));ps.setString(4,b.get("pauta"));ps.setString(5,b.get("status"));ps.setLong(6,aid);ps.setLong(7,cond);ps.executeUpdate();} if("cancelada".equalsIgnoreCase(b.get("status"))&&!"cancelada".equalsIgnoreCase(str(old.get("status"))))criarAvisoCancelamentoAssembleia(c,emp,cond,aid,fid,old); audit(c,fid,"update","assembleias",aid,old,b); c.commit();HttpResponses.json(ex,200,Map.of("assembleia",one(c,"SELECT * FROM assembleias WHERE id=?",Arrays.asList(aid))));}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}}
private void criarAvisoCancelamentoAssembleia(Connection c,long emp,long cond,long aid,long fid,Map<String,Object>old)throws SQLException{
List<Map<String,Object>> avisos=rows(c,"SELECT id FROM avisos WHERE assembleia_id=?",Arrays.asList(aid));
Set<String> vistos=new HashSet<>();
@@ -204,6 +209,41 @@ public final class ClienteHandlers implements HttpHandler {
private void criarExtra(HttpExchange ex,Connection c,long fid)throws Exception{Map<String,String>b=body(ex);long id=insert(c,"INSERT INTO destinatarios_extra (nome,email,telefone,status) VALUES (?,?,?,'ativo')",Arrays.asList(req(b,"nome"),nullable(b.get("email")),nullable(b.get("telefone"))));audit(c,fid,"create","destinatarios_extra",id,null,b);HttpResponses.json(ex,201,Map.of("extra",one(c,"SELECT * FROM destinatarios_extra WHERE id=?",Arrays.asList(id))));}
private void atualizarExtra(HttpExchange ex,Connection c,long id,long fid)throws Exception{Map<String,Object> old=one(c,"SELECT * FROM destinatarios_extra WHERE id=?",Arrays.asList(id));if(old==null){HttpResponses.json(ex,404,Map.of("erro","extra_nao_encontrado"));return;}Map<String,String>b=body(ex);try(PreparedStatement ps=c.prepareStatement("UPDATE destinatarios_extra SET nome=COALESCE(?,nome), email=COALESCE(?,email), telefone=COALESCE(?,telefone), status=COALESCE(?,status), atualizado_em=now() WHERE id=?")){ps.setString(1,b.get("nome"));ps.setString(2,b.get("email"));ps.setString(3,b.get("telefone"));ps.setString(4,b.get("status"));ps.setLong(5,id);ps.executeUpdate();}audit(c,fid,"update","destinatarios_extra",id,old,b);HttpResponses.json(ex,200,Map.of("extra",one(c,"SELECT * FROM destinatarios_extra WHERE id=?",Arrays.asList(id))));}
private void excluirExtra(HttpExchange ex,Connection c,long id,long fid)throws Exception{Map<String,Object> old=one(c,"SELECT * FROM destinatarios_extra WHERE id=?",Arrays.asList(id));if(old==null){HttpResponses.json(ex,404,Map.of("erro","extra_nao_encontrado"));return;}try(PreparedStatement ps=c.prepareStatement("UPDATE destinatarios_extra SET status='excluido', atualizado_em=now() WHERE id=?")){ps.setLong(1,id);ps.executeUpdate();}audit(c,fid,"delete","destinatarios_extra",id,old,Map.of("ok",true));HttpResponses.json(ex,200,Map.of("ok",true));}
private void listarResponsaveisAssembleia(HttpExchange ex,Connection c)throws Exception{HttpResponses.json(ex,200,Map.of("responsaveis",rows(c,"SELECT * FROM assembleia_responsaveis WHERE status<>'excluido' ORDER BY nome",Arrays.asList())));}
private void criarResponsavelAssembleia(HttpExchange ex,Connection c,long fid)throws Exception{String raw=rawBody(ex);Map<String,String>b=SimpleJson.parseObject(removeArrayField(raw,"dias_semana"));String dias=diasSemana(raw,b.get("dias_semana"));long id=insert(c,"INSERT INTO assembleia_responsaveis (nome,telefone,email,dias_semana,status) VALUES (?,?,?,?,'ativo')",Arrays.asList(req(b,"nome"),nullable(b.get("telefone")),nullable(b.get("email")),dias));audit(c,fid,"create","assembleia_responsaveis",id,null,b);HttpResponses.json(ex,201,Map.of("responsavel",one(c,"SELECT * FROM assembleia_responsaveis WHERE id=?",Arrays.asList(id))));}
private void atualizarResponsavelAssembleia(HttpExchange ex,Connection c,long id,long fid)throws Exception{Map<String,Object> old=one(c,"SELECT * FROM assembleia_responsaveis WHERE id=?",Arrays.asList(id));if(old==null){HttpResponses.json(ex,404,Map.of("erro","responsavel_nao_encontrado"));return;}String raw=rawBody(ex);Map<String,String>b=SimpleJson.parseObject(removeArrayField(raw,"dias_semana"));String dias=raw.contains("\"dias_semana\"")?diasSemana(raw,b.get("dias_semana")):null;try(PreparedStatement ps=c.prepareStatement("UPDATE assembleia_responsaveis SET nome=COALESCE(?,nome), telefone=COALESCE(?,telefone), email=COALESCE(?,email), dias_semana=COALESCE(?,dias_semana), status=COALESCE(?,status), atualizado_em=now() WHERE id=?")){ps.setString(1,b.get("nome"));ps.setString(2,b.get("telefone"));ps.setString(3,b.get("email"));ps.setString(4,dias);ps.setString(5,b.get("status"));ps.setLong(6,id);ps.executeUpdate();}audit(c,fid,"update","assembleia_responsaveis",id,old,b);HttpResponses.json(ex,200,Map.of("responsavel",one(c,"SELECT * FROM assembleia_responsaveis WHERE id=?",Arrays.asList(id))));}
private void excluirResponsavelAssembleia(HttpExchange ex,Connection c,long id,long fid)throws Exception{Map<String,Object> old=one(c,"SELECT * FROM assembleia_responsaveis WHERE id=?",Arrays.asList(id));if(old==null){HttpResponses.json(ex,404,Map.of("erro","responsavel_nao_encontrado"));return;}try(PreparedStatement ps=c.prepareStatement("UPDATE assembleia_responsaveis SET status='excluido', atualizado_em=now() WHERE id=?")){ps.setLong(1,id);ps.executeUpdate();}audit(c,fid,"delete","assembleia_responsaveis",id,old,Map.of("ok",true));HttpResponses.json(ex,200,Map.of("ok",true));}
private Map<String,Object> validarAgendaResponsaveisAssembleia(Connection c,Timestamp dataHora,Long ignorarAssembleia)throws Exception{
if(dataHora==null)return null;
List<Map<String,Object>> responsaveis=rows(c,"SELECT id,nome,dias_semana FROM assembleia_responsaveis WHERE status='ativo' ORDER BY nome",Arrays.asList());
if(responsaveis.isEmpty())return null;
int dia=dataHora.toLocalDateTime().getDayOfWeek().getValue()%7;
List<String> indisponiveis=new ArrayList<>();
for(Map<String,Object> r:responsaveis){
Set<String> dias=new HashSet<>(Arrays.asList(str(r.get("dias_semana")).split(",")));
if(!dias.contains(String.valueOf(dia)))indisponiveis.add(str(r.get("nome")));
}
if(!indisponiveis.isEmpty())return Map.of("erro","responsavel_indisponivel","mensagem","Nao e possivel agendar assembleia neste dia da semana para: "+String.join(", ",indisponiveis)+".","responsaveis",indisponiveis);
String sql="SELECT a.id,a.data_hora,r.nome FROM assembleias a CROSS JOIN assembleia_responsaveis r WHERE r.status='ativo' AND a.status<>'cancelada' AND a.data_hora::date=?::date"+(ignorarAssembleia==null?"":" AND a.id<>?");
List<Object> params=new ArrayList<>();params.add(dataHora);if(ignorarAssembleia!=null)params.add(ignorarAssembleia);
List<Map<String,Object>> conflitos=rows(c,sql,params);
if(!conflitos.isEmpty()){
List<String> nomes=conflitos.stream().map(x->str(x.get("nome"))).distinct().toList();
return Map.of("erro","assembleia_duplicada_responsavel","mensagem","Ja existe assembleia agendada neste dia para: "+String.join(", ",nomes)+".","responsaveis",nomes);
}
return null;
}
private String diasSemana(String raw,String direto){
List<String> vals=stringArray(raw,"dias_semana");
if(vals.isEmpty()&&direto!=null&&!direto.isBlank())vals=Arrays.asList(direto.split(","));
Set<String> ok=new LinkedHashSet<>();
for(String v:vals){
String x=v.trim().replace("\"","");
if(x.matches("[0-6]"))ok.add(x);
}
if(ok.isEmpty())throw new IllegalArgumentException("Selecione ao menos um dia da semana.");
return String.join(",",ok);
}
private void listarPessoasLembrete(HttpExchange ex,long empresaId)throws Exception{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id,nome,email,telefone,cargo,status FROM funcionarios WHERE empresa_id=? AND status='ativo' ORDER BY nome")){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("pessoas",out));}}
private void criarLembrete(HttpExchange ex,Connection c,long emp,long fid)throws Exception{
String raw=rawBody(ex);
@@ -347,7 +387,7 @@ public final class ClienteHandlers implements HttpHandler {
List<Map<String,Object>> out=new ArrayList<>();
for(Map<String,Object>d:dests){
String tipo=str(d.get("tipo_destinatario"));long did=((Number)d.get("destinatario_id")).longValue();Map<String,Object> pessoa;
if("funcionario".equals(tipo))pessoa=funcionario(emp,did);else if("extra".equals(tipo))pessoa=one(c,"SELECT id,nome,email,telefone FROM destinatarios_extra WHERE id=?",Arrays.asList(did));else pessoa=one(c,"SELECT id,nome,email,telefone FROM contatos WHERE id=?",Arrays.asList(did));
if("funcionario".equals(tipo))pessoa=funcionario(emp,did);else if("extra".equals(tipo))pessoa=one(c,"SELECT id,nome,email,telefone FROM destinatarios_extra WHERE id=?",Arrays.asList(did));else if("assembleia".equals(tipo))pessoa=one(c,"SELECT id,nome,email,telefone FROM assembleia_responsaveis WHERE id=?",Arrays.asList(did));else pessoa=one(c,"SELECT id,nome,email,telefone FROM contatos WHERE id=?",Arrays.asList(did));
if(pessoa!=null){Map<String,Object> item=new LinkedHashMap<>(pessoa);item.put("tipo",tipo);out.add(item);}
}
return out;
@@ -357,7 +397,7 @@ public final class ClienteHandlers implements HttpHandler {
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);}
private void excluirAviso(HttpExchange ex,Connection c,long emp,long cond,long aviso,long fid)throws Exception{Map<String,Object> old=one(c,"SELECT * FROM avisos WHERE id=? AND condominio_id=?",Arrays.asList(aviso,cond));if(old==null){HttpResponses.json(ex,404,Map.of("erro","aviso_nao_encontrado"));return;}c.setAutoCommit(false);try{try(PreparedStatement ps=c.prepareStatement("DELETE FROM aviso_documentos WHERE aviso_id=?")){ps.setLong(1,aviso);ps.executeUpdate();}try(PreparedStatement ps=c.prepareStatement("DELETE FROM aviso_destinatarios WHERE aviso_id=?")){ps.setLong(1,aviso);ps.executeUpdate();}try(PreparedStatement ps=c.prepareStatement("DELETE FROM avisos WHERE id=? AND condominio_id=?")){ps.setLong(1,aviso);ps.setLong(2,cond);ps.executeUpdate();}audit(c,fid,"delete","avisos",aviso,old,Map.of("ok",true));c.commit();try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("DELETE FROM logs_envio_avisos WHERE empresa_id=? AND condominio_id=? AND aviso_id=?")){ps.setLong(1,emp);ps.setLong(2,cond);ps.setLong(3,aviso);ps.executeUpdate();}HttpResponses.json(ex,200,Map.of("ok",true));}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}}
private void enviarAviso(Connection c,long emp,long cond,long aviso,boolean apenasFalhos)throws Exception{Map<String,Object>a=one(c,"SELECT * FROM avisos WHERE id=? AND condominio_id=?",Arrays.asList(aviso,cond));if(a==null)return;boolean w=Boolean.TRUE.equals(a.get("enviar_whatsapp"));boolean e=Boolean.TRUE.equals(a.get("enviar_email"));Map<String,Object> inst=w?evolutionAprovada(emp):null;Map<String,String> smtp=e?smtpConfig():Map.of();List<Map<String,Object>> dests=rows(c,"SELECT * FROM aviso_destinatarios WHERE aviso_id=?",Arrays.asList(aviso));boolean allOk=true;int tentativas=0;if(dests.isEmpty())allOk=false;for(Map<String,Object>d:dests){String tipo=String.valueOf(d.get("tipo_destinatario"));long did=((Number)d.get("destinatario_id")).longValue();Map<String,Object> contato="funcionario".equals(tipo)?funcionario(emp,did):("extra".equals(tipo)?one(c,"SELECT id,nome,telefone,email FROM destinatarios_extra WHERE id=? AND status='ativo'",Arrays.asList(did)):one(c,"SELECT id,nome,telefone,email FROM contatos WHERE id=?",Arrays.asList(did)));if(contato==null){allOk=false;continue;}String nome=String.valueOf(contato.get("nome"));if(w&&deveTentar(emp,aviso,did,"whatsapp",apenasFalhos)){tentativas++;String tel=telefoneWhatsapp(contato.get("telefone"));try{if(tel.length()<12)throw new IllegalArgumentException("telefone_invalido");String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2);String resposta=evolutionApiClient.enviarTexto(str(inst.get("instance_url")),parts[0],parts.length>1?parts[1]:"",tel,msg(a));logEnvio(emp,cond,aviso,did,nome,"whatsapp","enviado",limite(resposta));}catch(Exception err){allOk=false;logEnvio(emp,cond,aviso,did,nome,"whatsapp","falhou",err.getMessage());}}if(e&&deveTentar(emp,aviso,did,"email",apenasFalhos)){tentativas++;String email=str(contato.get("email"));try{if(!email.contains("@"))throw new IllegalArgumentException("email_invalido");emailClient.enviar(smtp,email,str(a.get("titulo")),msg(a));logEnvio(emp,cond,aviso,did,nome,"email","enviado",null);}catch(Exception err){allOk=false;logEnvio(emp,cond,aviso,did,nome,"email","falhou",err.getMessage());}}}if(allOk&&tentativas>0&&reagendarAvisoRecorrente(c,aviso,a))return;try(PreparedStatement ps=c.prepareStatement("UPDATE avisos SET status_envio=?, ultimo_envio=CASE WHEN ? THEN now() ELSE ultimo_envio END WHERE id=?")){boolean ok=allOk&&tentativas>0;ps.setString(1,ok?"enviado":"falhou");ps.setBoolean(2,ok);ps.setLong(3,aviso);ps.executeUpdate();}}
private void enviarAviso(Connection c,long emp,long cond,long aviso,boolean apenasFalhos)throws Exception{Map<String,Object>a=one(c,"SELECT * FROM avisos WHERE id=? AND condominio_id=?",Arrays.asList(aviso,cond));if(a==null)return;boolean w=Boolean.TRUE.equals(a.get("enviar_whatsapp"));boolean e=Boolean.TRUE.equals(a.get("enviar_email"));Map<String,Object> inst=w?evolutionAprovada(emp):null;Map<String,String> smtp=e?smtpConfig():Map.of();List<Map<String,Object>> dests=rows(c,"SELECT * FROM aviso_destinatarios WHERE aviso_id=?",Arrays.asList(aviso));boolean allOk=true;int tentativas=0;if(dests.isEmpty())allOk=false;for(Map<String,Object>d:dests){String tipo=String.valueOf(d.get("tipo_destinatario"));long did=((Number)d.get("destinatario_id")).longValue();Map<String,Object> contato="funcionario".equals(tipo)?funcionario(emp,did):("extra".equals(tipo)?one(c,"SELECT id,nome,telefone,email FROM destinatarios_extra WHERE id=? AND status='ativo'",Arrays.asList(did)):("assembleia".equals(tipo)?one(c,"SELECT id,nome,telefone,email FROM assembleia_responsaveis WHERE id=? AND status='ativo'",Arrays.asList(did)):one(c,"SELECT id,nome,telefone,email FROM contatos WHERE id=?",Arrays.asList(did))));if(contato==null){allOk=false;continue;}String nome=String.valueOf(contato.get("nome"));if(w&&deveTentar(emp,aviso,did,"whatsapp",apenasFalhos)){tentativas++;String tel=telefoneWhatsapp(contato.get("telefone"));try{if(tel.length()<12)throw new IllegalArgumentException("telefone_invalido");String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2);String resposta=evolutionApiClient.enviarTexto(str(inst.get("instance_url")),parts[0],parts.length>1?parts[1]:"",tel,msg(a));logEnvio(emp,cond,aviso,did,nome,"whatsapp","enviado",limite(resposta));}catch(Exception err){allOk=false;logEnvio(emp,cond,aviso,did,nome,"whatsapp","falhou",err.getMessage());}}if(e&&deveTentar(emp,aviso,did,"email",apenasFalhos)){tentativas++;String email=str(contato.get("email"));try{if(!email.contains("@"))throw new IllegalArgumentException("email_invalido");emailClient.enviar(smtp,email,str(a.get("titulo")),msg(a));logEnvio(emp,cond,aviso,did,nome,"email","enviado",null);}catch(Exception err){allOk=false;logEnvio(emp,cond,aviso,did,nome,"email","falhou",err.getMessage());}}}if(allOk&&tentativas>0&&reagendarAvisoRecorrente(c,aviso,a))return;try(PreparedStatement ps=c.prepareStatement("UPDATE avisos SET status_envio=?, ultimo_envio=CASE WHEN ? THEN now() ELSE ultimo_envio END WHERE id=?")){boolean ok=allOk&&tentativas>0;ps.setString(1,ok?"enviado":"falhou");ps.setBoolean(2,ok);ps.setLong(3,aviso);ps.executeUpdate();}}
private boolean reagendarAvisoRecorrente(Connection c,long aviso,Map<String,Object>a)throws SQLException{
int meses=a.get("recorrencia_meses")==null?0:((Number)a.get("recorrencia_meses")).intValue();
if(!Boolean.TRUE.equals(a.get("recorrencia_ativa"))||meses<=0)return false;
@@ -573,6 +613,7 @@ public final class ClienteHandlers implements HttpHandler {
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;}
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;}
private String arrayBody(String raw,String key){String marker="\""+key+"\"";int p=raw.indexOf(marker);if(p<0)return"";int s=raw.indexOf('[',p);if(s<0)return"";int depth=0;for(int i=s;i<raw.length();i++){char ch=raw.charAt(i);if(ch=='[')depth++;if(ch==']'&&--depth==0)return raw.substring(s+1,i).trim();}return"";}
private String removeArrayField(String raw,String key){String marker="\""+key+"\"";int p=raw.indexOf(marker);if(p<0)return raw;int s=raw.indexOf('[',p);if(s<0)return raw;int depth=0;for(int i=s;i<raw.length();i++){char ch=raw.charAt(i);if(ch=='[')depth++;if(ch==']'&&--depth==0)return raw.substring(0,s)+"\"\""+raw.substring(i+1);}return raw;} record Destinatario(String tipo,long id){}
@@ -0,0 +1,12 @@
CREATE TABLE assembleia_responsaveis (
id BIGSERIAL PRIMARY KEY,
nome VARCHAR(160) NOT NULL,
telefone VARCHAR(30),
email VARCHAR(160),
dias_semana VARCHAR(30) NOT NULL DEFAULT '',
status VARCHAR(20) NOT NULL DEFAULT 'ativo',
criado_em TIMESTAMP NOT NULL DEFAULT now(),
atualizado_em TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX idx_assembleia_responsaveis_status ON assembleia_responsaveis(status, nome);
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Assembleia</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>Assembleia</h1></div><section class="panel"><div class="section-head"><h2>Todas as assembleias</h2><div class="section-actions"><button class="search-icon-btn" type="button" onclick="alternarBuscaAssembleias()" aria-label="Buscar assembleias" 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></div></div><div id="assembleia-search" class="search-strip hidden"><input id="assembleia-search-input" placeholder="Buscar por condominio, tipo, data, local ou status"></div><div class="table-wrap"><table><thead><tr><th>Condominio</th><th>Tipo</th><th>Data</th><th>Local</th><th>Status</th><th>Notificados</th><th></th></tr></thead><tbody id="rows"></tbody></table></div></section></main><script>Layout.mount('empresa','assembleias');</script><script src="/cliente/assembleias.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>Assembleia</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>Assembleia</h1></div><section class="panel"><div class="section-head"><h2>Todas as assembleias</h2><div class="section-actions"><button class="search-icon-btn" type="button" onclick="alternarBuscaAssembleias()" aria-label="Buscar assembleias" 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></div></div><div id="assembleia-search" class="search-strip hidden"><input id="assembleia-search-input" placeholder="Buscar por condominio, tipo, data, local ou status"></div><div class="table-wrap"><table><thead><tr><th>Condominio</th><th>Tipo</th><th>Data</th><th>Local</th><th>Status</th><th>Notificados</th><th></th></tr></thead><tbody id="rows"></tbody></table></div></section><section class="panel"><div class="section-head"><h2>Responsáveis pela assembleia</h2><div class="section-actions"><button class="btn" type="button" onclick="abrirNovoResponsavel()">Novo</button></div></div><div class="table-wrap"><table><thead><tr><th>Nome</th><th>WhatsApp</th><th>E-mail</th><th>Dias disponíveis</th><th>Status</th><th>Ações</th></tr></thead><tbody id="responsaveis-rows"></tbody></table></div></section><div id="responsavel-modal" class="modal hidden"><div class="modal-backdrop" onclick="fecharResponsavelModal()"></div><section class="modal-panel"><div class="modal-head"><h2 id="responsavel-modal-title">Responsável</h2><button class="btn secondary" type="button" onclick="fecharResponsavelModal()">Fechar</button></div><div id="responsavel-modal-body"></div></section></div></main><script>Layout.mount('empresa','assembleias');</script><script src="/cliente/assembleias.js"></script></body></html>
+43 -3
View File
@@ -1,10 +1,16 @@
const rows=document.querySelector('#rows');
let assembleias=[];
const rows=document.querySelector('#rows'),responsaveisRows=document.querySelector('#responsaveis-rows'),modal=document.querySelector('#responsavel-modal'),modalTitle=document.querySelector('#responsavel-modal-title'),modalBody=document.querySelector('#responsavel-modal-body');
let assembleias=[],responsaveis=[];
const diasNomes=['Domingo','Segunda','Terça','Quarta','Quinta','Sexta','Sábado'];
function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]))}
function termo(){return document.querySelector('#assembleia-search-input')?.value.trim().toLowerCase()||''}
function contem(q,...vals){return !q||vals.some(v=>String(v??'').toLowerCase().includes(q))}
function destinatariosLabel(a){return (a.destinatarios||[]).map(x=>x.nome+' ('+x.tipo+')').join(', ')||'-'}
function diasLabel(v){return String(v||'').split(',').filter(x=>x!=='').map(x=>diasNomes[Number(x)]||x).join(', ')||'-'}
function formHtml(r={}){
const dias=new Set(String(r.dias_semana||'').split(','));
return `<form id="responsavel-form" class="grid cols"><input type="hidden" name="id" value="${r.id||''}"><div class="field"><label>Nome</label><input name="nome" required value="${esc(r.nome||'')}"></div><div class="field"><label>WhatsApp</label><input name="telefone" value="${esc(r.telefone||'')}"></div><div class="field"><label>E-mail</label><input name="email" type="email" value="${esc(r.email||'')}"></div><div class="field"><label>Status</label><select name="status"><option value="ativo" ${r.status==='ativo'||!r.status?'selected':''}>Ativo</option><option value="inativo" ${r.status==='inativo'?'selected':''}>Inativo</option></select></div><div class="field"><label>Dias da semana</label><div class="row compact">${diasNomes.map((d,i)=>`<label><input type="checkbox" name="dias" value="${i}" ${dias.has(String(i))?'checked':''}> ${d}</label>`).join('')}</div></div><div class="row"><button class="btn">${r.id?'Salvar':'Cadastrar'}</button><button class="btn secondary" type="button" onclick="fecharResponsavelModal()">Cancelar</button></div></form>`;
}
function renderAssembleias(){
const q=termo();
@@ -19,12 +25,46 @@ async function load(){
renderAssembleias();
}catch(e){Util.toast(e.message,true)}
}
async function loadResponsaveis(){
try{
const d=await Api.get('/api/assembleia-responsaveis');
responsaveis=d.responsaveis||[];
renderResponsaveis();
}catch(e){Util.toast(e.message,true)}
}
function renderResponsaveis(){
const admin=Auth.isAdminEmpresa();
responsaveisRows.innerHTML=responsaveis.map(r=>`<tr><td><strong>${esc(r.nome)}</strong></td><td>${esc(r.telefone||'')}</td><td>${esc(r.email||'')}</td><td>${esc(diasLabel(r.dias_semana))}</td><td>${Util.badge(r.status)}</td><td>${admin?`<button class="btn secondary" onclick="editarResponsavel(${r.id})">Editar</button> <button class="btn danger" onclick="excluirResponsavel(${r.id})">Excluir</button>`:'-'}</td></tr>`).join('')||'<tr><td colspan="6">Nenhum responsável cadastrado.</td></tr>';
}
function abrirResponsavel(r){
modalTitle.textContent=r?.id?'Editar responsável':'Novo responsável';
modalBody.innerHTML=formHtml(r||{});
modal.classList.remove('hidden');
document.querySelector('#responsavel-form').onsubmit=async e=>{
e.preventDefault();
const f=e.target,b=Util.formData(f);
b.dias_semana=[...f.querySelectorAll('input[name=dias]:checked')].map(x=>x.value);
if(!b.dias_semana.length){Util.toast('Selecione ao menos um dia da semana.',true);return;}
try{
if(b.id)await Api.put('/api/assembleia-responsaveis/'+b.id,b);else await Api.post('/api/assembleia-responsaveis',b);
Util.toast(b.id?'Responsável atualizado':'Responsável cadastrado');
fecharResponsavelModal();
await loadResponsaveis();
await load();
}catch(err){Util.toast(err.message,true)}
};
}
window.alternarBuscaAssembleias=()=>{
const strip=document.querySelector('#assembleia-search'),input=document.querySelector('#assembleia-search-input');
strip.classList.toggle('hidden');
if(!strip.classList.contains('hidden'))input.focus();
};
window.abrirNovoResponsavel=()=>abrirResponsavel({});
window.editarResponsavel=id=>{const r=responsaveis.find(x=>Number(x.id)===Number(id));if(r)abrirResponsavel(r)};
window.fecharResponsavelModal=()=>{modal.classList.add('hidden');modalBody.innerHTML=''};
window.excluirResponsavel=async id=>{if(!confirm('Excluir este responsável de assembleia?'))return;try{await Api.request('/api/assembleia-responsaveis/'+id,{method:'DELETE'});Util.toast('Responsável excluído');await loadResponsaveis();await load()}catch(e){Util.toast(e.message,true)}};
document.querySelector('#assembleia-search-input')?.addEventListener('input',renderAssembleias);
load();
if(!Auth.isAdminEmpresa())document.querySelector('[onclick="abrirNovoResponsavel()"]')?.classList.add('hidden');
(async()=>{await loadResponsaveis();await load()})();