From 44c04d0b2340c07529d790eb9502391d8db99c8a Mon Sep 17 00:00:00 2001 From: Tiago Date: Wed, 30 Sep 2026 15:12:39 -0300 Subject: [PATCH] Automatiza destinatarios de assembleias --- .../tenant/http/ClienteHandlers.java | 27 +++++++++++++------ src/main/webapp/cliente/condominio-detalhe.js | 2 +- src/main/webapp/css/app.css | 2 -- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java b/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java index 1ec0d02..1d269fc 100644 --- a/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java +++ b/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java @@ -79,7 +79,7 @@ public final class ClienteHandlers implements HttpHandler { 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])&&"PUT".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} atualizarEspaco(ex,c,cond,id(p[3]),funcionarioId); 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 * FROM assembleias WHERE condominio_id=? ORDER BY data_hora DESC",cond,"assembleias"); else HttpResponses.methodNotAllowed(ex); return; } - if(p.length==4&&"condominios".equals(p[0])&&"assembleias".equals(p[2])&&"PUT".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} atualizarAssembleia(ex,c,cond,id(p[3]),funcionarioId); return; } + if(p.length==4&&"condominios".equals(p[0])&&"assembleias".equals(p[2])&&"PUT".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} atualizarAssembleia(ex,c,empresaId,cond,id(p[3]),funcionarioId); return; } if(p.length==3&&"condominios".equals(p[0])&&"documentos".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)) uploadDocumento(ex,c,empresaId,cond,funcionarioId); else if("GET".equals(method)) listarParam(ex,c,"SELECT * FROM documentos WHERE condominio_id=? ORDER BY criado_em DESC",cond,"documentos"); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==3&&"documentos".equals(p[0])&&"download".equals(p[2])&&"GET".equals(method)){ downloadDocumento(ex,c,id(p[1])); return; } HttpResponses.json(ex,404,Map.of("erro","endpoint_nao_encontrado")); @@ -129,7 +129,7 @@ public final class ClienteHandlers implements HttpHandler { HttpResponses.json(ex,201,Map.of("assembleia",one(c,"SELECT * FROM assembleias WHERE id=?",Arrays.asList(id)),"envio",envio)); } private Map criarAvisosAssembleia(Connection c,long emp,long cond,long assembleia,long fid,Mapb,Listdests)throws Exception{ - List destinatarios=dests.isEmpty()?destinatariosPadraoAssembleia(c,cond):dests; + List destinatarios=dests.isEmpty()?destinatariosPadraoAssembleia(c,emp,cond):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."); @@ -141,12 +141,19 @@ public final class ClienteHandlers implements HttpHandler { 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"); } - private List destinatariosPadraoAssembleia(Connection c,long cond)throws SQLException{ + private List destinatariosPadraoAssembleia(Connection c,long emp,long cond)throws SQLException{ List out=new ArrayList<>(); - for(Map row:rows(c,"SELECT contato_id FROM contato_condominio WHERE condominio_id=? AND status=?",Arrays.asList(cond,"ativo")))out.add(new Destinatario("contato",((Number)row.get("contato_id")).longValue())); - for(Map row:rows(c,"SELECT funcionario_id FROM funcionario_condominio WHERE condominio_id=?",Arrays.asList(cond)))out.add(new Destinatario("funcionario",((Number)row.get("funcionario_id")).longValue())); + Set vistos=new HashSet<>(); + for(Map row:rows(c,"SELECT contato_id FROM contato_condominio WHERE condominio_id=? AND status=? AND tipo IN ('sindico','subsindico','conselheiro')",Arrays.asList(cond,"ativo")))addDestinatario(out,vistos,"contato",((Number)row.get("contato_id")).longValue()); + try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id FROM funcionarios WHERE empresa_id=? AND cargo='admin_empresa' AND status='ativo'")){ + ps.setLong(1,emp); + try(ResultSet rs=ps.executeQuery()){while(rs.next())addDestinatario(out,vistos,"funcionario",rs.getLong(1));} + } + for(Map 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 row:rows(c,"SELECT id FROM destinatarios_extra WHERE status='ativo'",Arrays.asList()))addDestinatario(out,vistos,"extra",((Number)row.get("id")).longValue()); return out; } + private void addDestinatario(List out,Set vistos,String tipo,long id){if(vistos.add(tipo+":"+id))out.add(new Destinatario(tipo,id));} private long criarAvisoAutomaticoAssembleia(Connection c,long cond,long assembleia,long fid,Mapb,Listdests,String titulo,String intro,Timestamp envio)throws SQLException{ Timestamp agora=Timestamp.valueOf(LocalDateTime.now(java.time.ZoneId.of("America/Sao_Paulo"))); Timestamp agendado=envio!=null&&envio.before(agora)?agora:envio; @@ -163,8 +170,8 @@ 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 cond,long aid,long fid)throws Exception{Mapb=body(ex); Map 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,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 cond,long aid,long fid,Mapold)throws SQLException{ + private void atualizarAssembleia(HttpExchange ex,Connection c,long emp,long cond,long aid,long fid)throws Exception{Mapb=body(ex); Map 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 criarAvisoCancelamentoAssembleia(Connection c,long emp,long cond,long aid,long fid,Mapold)throws SQLException{ List> avisos=rows(c,"SELECT id FROM avisos WHERE assembleia_id=?",Arrays.asList(aid)); Set vistos=new HashSet<>(); Listdests=new ArrayList<>(); @@ -174,7 +181,11 @@ public final class ClienteHandlers implements HttpHandler { if(vistos.add(key))dests.add(new Destinatario(tipo,id)); } } - if(dests.isEmpty())dests=destinatariosPadraoAssembleia(c,cond); + if(dests.isEmpty()){ + Map a=one(c,"SELECT condominio_id FROM assembleias WHERE id=?",Arrays.asList(aid)); + long cid=a!=null&&a.get("condominio_id")!=null?((Number)a.get("condominio_id")).longValue():cond; + dests=destinatariosPadraoAssembleia(c,emp,cid); + } try(PreparedStatement ps=c.prepareStatement("UPDATE avisos SET status_envio='cancelado' WHERE assembleia_id=? AND status_envio IN ('nao_enviado','agendado','falhou')")){ps.setLong(1,aid);ps.executeUpdate();} try(PreparedStatement ps=c.prepareStatement("UPDATE fila_envio SET status='ignorado', ultimo_erro='Assembleia cancelada', locked_at=NULL, atualizado_em=now() WHERE tipo='aviso' AND referencia_id IN (SELECT id FROM avisos WHERE assembleia_id=?) AND status IN ('pendente','processando','falhou')")){ps.setLong(1,aid);ps.executeUpdate();} Mapb=new HashMap<>();b.put("data_hora",String.valueOf(old.get("data_hora")));b.put("local",str(old.get("local")));b.put("pauta",str(old.get("pauta"))); diff --git a/src/main/webapp/cliente/condominio-detalhe.js b/src/main/webapp/cliente/condominio-detalhe.js index c0b3749..aa43bf4 100644 --- a/src/main/webapp/cliente/condominio-detalhe.js +++ b/src/main/webapp/cliente/condominio-detalhe.js @@ -20,7 +20,7 @@ async function safeLoad(section,fn){try{await fn()}catch(e){document.querySelect async function load(){try{const d=await Api.get(`/api/condominios/${id}`);document.querySelector('#header').innerHTML=`

${d.condominio.nome}

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

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

Contatos

${contatos.map(c=>``).join('')}
NomeTipoTelefoneEmail
${c.contato_nome}${c.tipo}${c.contato_telefone||''}${c.contato_email||''}
`;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');loadContatos()}catch(err){Util.toast(err.message,true)}}} async function loadEspacos(){const d=await Api.get(`/api/condominios/${id}/espacos-comuns`);document.querySelector('#espacos').innerHTML=`

Espacos comuns

${(d.espacos||[]).map(x=>``).join('')}
NomeValorIndiceDescricao
${x.nome}${Util.money(x.valor_utilizacao_centavos)}${x.indice_correcao||''}${x.descricao||''}
`;document.querySelector('#espaco-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/espacos-comuns`,Util.formData(e.target));Util.toast('Espaco salvo');loadEspacos()}catch(err){Util.toast(err.message,true)}}} -async function loadAssembleias(){const d=await Api.get(`/api/condominios/${id}/assembleias`);const destContatos=contatos.map(c=>``).join('');const destFuncionarios=funcionariosVinculados.map(f=>``).join('');const dest=[destContatos,destFuncionarios].filter(Boolean).join('')||'

Nenhum membro cadastrado.

';document.querySelector('#assembleias').innerHTML=`

Assembleias

${dest}
${(d.assembleias||[]).map(a=>``).join('')}
TipoDataLocalStatus
${a.tipo}${Util.date(a.data_hora)}${a.local||''}${Util.badge(a.status)}
`;document.querySelector('#ass-form').onsubmit=async e=>{e.preventDefault();const f=e.target,b=Util.formData(f);b.destinatarios=[...f.querySelectorAll('input[name=dest]:checked')].map(x=>{const [tipo,did]=x.value.split(':');return{tipo_destinatario:tipo,id:Number(did)}});try{await Api.post(`/api/condominios/${id}/assembleias`,b);Util.toast('Assembleia salva');loadAssembleias();load()}catch(err){Util.toast(err.message,true)}}} +async function loadAssembleias(){const d=await Api.get(`/api/condominios/${id}/assembleias`);document.querySelector('#assembleias').innerHTML=`

Assembleias

${(d.assembleias||[]).map(a=>``).join('')}
TipoDataLocalStatus
${a.tipo}${Util.date(a.data_hora)}${a.local||''}${Util.badge(a.status)}
`;document.querySelector('#ass-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/assembleias`,Util.formData(e.target));Util.toast('Assembleia salva');loadAssembleias();load()}catch(err){Util.toast(err.message,true)}}} async function loadFuncionariosVinculados(){const d=await Api.get(`/api/condominios/${id}/funcionarios-vinculados`);funcionariosVinculados=d.funcionarios||[]} async function loadExtras(){const d=await Api.get(`/api/extras`);extras=(d.extras||[]).filter(x=>x.status==='ativo')} async function loadDocs(){const d=await Api.get(`/api/condominios/${id}/documentos`);docs=d.documentos||[];document.querySelector('#documentos').innerHTML=`

Documentos

${docs.map(d=>``).join('')}
IDArquivoTipoDownload
${d.id}${d.nome_arquivo}${d.tipo||''}Baixar
`;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');loadDocs()}catch(err){Util.toast(err.message,true)}}} diff --git a/src/main/webapp/css/app.css b/src/main/webapp/css/app.css index 5306daf..434e2e9 100644 --- a/src/main/webapp/css/app.css +++ b/src/main/webapp/css/app.css @@ -3,6 +3,4 @@ .success-message{display:inline-block;margin:0 0 10px;padding:10px 12px;border:1px solid #bbf7d0;border-radius:8px;background:#dcfce7;color:var(--ok);font-weight:700} .section-head{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:14px}.section-head h2{margin:0}.icon-btn{position:relative;width:42px;height:42px;border:1px solid var(--line);border-radius:6px;background:#fff;cursor:pointer;display:inline-grid;place-items:center}.icon-btn:hover{border-color:var(--brand);background:#eef8fb}.calendar-open::before{content:"";width:20px;height:18px;border:2px solid var(--brand);border-radius:3px;box-shadow:inset 0 5px 0 #e8f3f7;position:relative}.calendar-open::after{content:"";position:absolute;width:12px;height:2px;background:var(--brand);transform:translateY(-7px)}.calendar-modal{width:min(1120px,100%)}.calendar-layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px}.calendar-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}.calendar-toolbar strong{text-transform:capitalize}.calendar-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));border:1px solid var(--line);border-radius:8px;overflow:hidden;background:#fff}.calendar-weekday{padding:10px;background:#f8fafc;color:var(--muted);font-size:12px;font-weight:700;text-align:center;border-bottom:1px solid var(--line)}.calendar-day{min-height:86px;border:0;border-right:1px solid var(--line);border-bottom:1px solid var(--line);background:#fff;padding:8px;text-align:left;color:var(--text);cursor:pointer;position:relative}.calendar-day:nth-child(7n+7){border-right:0}.calendar-day.other{background:#f8fafc;color:#98a2b3}.calendar-day.busy{background:#fff7ed}.calendar-day.busy span{font-weight:700;color:var(--warn)}.calendar-day.today{box-shadow:inset 0 0 0 2px var(--brand)}.calendar-day.selected{background:#e8f3f7}.calendar-day b{position:absolute;right:8px;bottom:8px;background:var(--brand);color:#fff;border-radius:999px;padding:2px 7px;font-size:12px}.calendar-side{border:1px solid var(--line);border-radius:8px;padding:14px;background:#fff;min-height:420px}.calendar-side h3{margin:0 0 12px}.calendar-event{border:1px solid var(--line);border-radius:8px;padding:10px;margin-bottom:10px;display:grid;gap:6px}.calendar-event span{font-size:13px}.calendar-event p{margin:0;color:var(--muted);font-size:13px}@media(max-width:900px){.calendar-layout{grid-template-columns:1fr}.calendar-day{min-height:64px}.calendar-side{min-height:auto}} -.check-list{display:grid;gap:8px;max-height:150px;overflow:auto;border:1px solid var(--line);border-radius:6px;padding:10px;background:#fff}.check-list label{display:flex;align-items:center;gap:8px;color:var(--text)}.check-list input{width:auto} - @media(max-width:520px){.content{padding:12px}h1{font-size:22px}h2{font-size:17px}.panel{padding:12px;border-radius:6px}.login-page{padding:16px}.login-box{padding:20px}.modal{padding:8px;align-items:start}.modal-panel{max-height:calc(100vh - 16px);padding:14px}.modal-head{align-items:flex-start}.modal-head .btn{width:auto}.calendar-grid{font-size:12px}.calendar-weekday{padding:8px 4px}.calendar-day{min-height:54px;padding:5px}.calendar-day b{right:4px;bottom:4px;padding:1px 5px}.calendar-toolbar .btn{width:auto}.section-head{align-items:flex-start}.icon-btn{width:40px;min-width:40px}.cond-item{display:grid;grid-template-columns:1fr;align-items:stretch}.cond-item .btn{width:100%}}