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 91a424c..e95fa9c 100644 --- a/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java +++ b/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java @@ -82,7 +82,7 @@ public final class ClienteHandlers implements HttpHandler { 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])&&"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 * FROM assembleias WHERE condominio_id=? ORDER BY data_hora DESC",cond,"assembleias"); 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; } if(p.length==4&&"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("PUT".equals(method)) atualizarAssembleia(ex,c,empresaId,cond,id(p[3]),funcionarioId); else if("DELETE".equals(method)) excluirAssembleia(ex,c,empresaId,cond,id(p[3]),funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==5&&"condominios".equals(p[0])&&"assembleias".equals(p[2])&&"notificar".equals(p[4])&&"POST".equals(method)){ long cond=id(p[1]); notificarAssembleia(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; } @@ -121,15 +121,16 @@ public final class ClienteHandlers implements HttpHandler { Mapb=SimpleJson.parseObject(removeArrayField(raw,"destinatarios")); Listdests=destinatarios(raw); Timestamp dataHora=ts(req(b,"data_hora")); - Map conflito=validarAgendaResponsaveisAssembleia(c,dataHora,null); + Long responsavelId=optLong(b.get("responsavel_id")); + Map conflito=validarAgendaResponsaveisAssembleia(c,dataHora,null,responsavelId); if(conflito!=null){HttpResponses.json(ex,409,conflito);return;} long id; Map 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"),dataHora,nullable(b.get("local")),nullable(b.get("pauta")))); + id=insert(c,"INSERT INTO assembleias (condominio_id,tipo,data_hora,local,pauta,responsavel_id) VALUES (?,?,?,?,?,?)",Arrays.asList(cond,req(b,"tipo"),dataHora,nullable(b.get("local")),nullable(b.get("pauta")),responsavelId)); audit(c,fid,"create","assembleias",id,null,b); - envio=criarAvisosAssembleia(c,emp,cond,id,fid,b,dests); + envio=criarAvisosAssembleia(c,emp,cond,id,fid,b,dests,responsavelId); c.commit(); }catch(Exception err){ c.rollback(); @@ -137,10 +138,10 @@ public final class ClienteHandlers implements HttpHandler { }finally{ c.setAutoCommit(true); } - HttpResponses.json(ex,201,Map.of("assembleia",one(c,"SELECT * FROM assembleias WHERE id=?",Arrays.asList(id)),"envio",envio)); + HttpResponses.json(ex,201,Map.of("assembleia",one(c,"SELECT a.*, r.nome responsavel_nome FROM assembleias a LEFT JOIN assembleia_responsaveis r ON r.id=a.responsavel_id WHERE a.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,emp,cond):dests; + private Map criarAvisosAssembleia(Connection c,long emp,long cond,long assembleia,long fid,Mapb,Listdests,Long responsavelId)throws Exception{ + List destinatarios=dests.isEmpty()?destinatariosPadraoAssembleia(c,emp,cond,responsavelId):dests; int total=destinatarios.size(); if(total==0)return Map.of("enviados",0,"falhas",0,"mensagem","Nenhum destinatario vinculado ao condominio."); if(!funcionalidadeAtiva(emp,"whatsapp_envio"))return Map.of("enviados",0,"falhas",1,"mensagem","WhatsApp nao liberado para esta empresa."); @@ -152,7 +153,7 @@ 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 emp,long cond)throws SQLException{ + private List destinatariosPadraoAssembleia(Connection c,long emp,long cond,Long responsavelId)throws SQLException{ List out=new ArrayList<>(); 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()); @@ -162,7 +163,11 @@ public final class ClienteHandlers implements HttpHandler { } 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()); - for(Map row:rows(c,"SELECT id FROM assembleia_responsaveis WHERE status='ativo'",Arrays.asList()))addDestinatario(out,vistos,"assembleia",((Number)row.get("id")).longValue()); + if(responsavelId!=null){ + addDestinatario(out,vistos,"assembleia",responsavelId); + } else { + for(Map 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 out,Set vistos,String tipo,long id){if(vistos.add(tipo+":"+id))out.add(new Destinatario(tipo,id));} @@ -182,7 +187,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{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;} Timestamp novaData=b.get("data_hora")==null?null:ts(b.get("data_hora")); if(novaData!=null){Map 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 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;} Timestamp novaData=b.get("data_hora")==null?null:ts(b.get("data_hora")); Long responsavelId=b.containsKey("responsavel_id")?optLong(b.get("responsavel_id")):(old.get("responsavel_id")!=null?((Number)old.get("responsavel_id")).longValue():null); if(novaData!=null){Map conflito=validarAgendaResponsaveisAssembleia(c,novaData,aid,responsavelId); 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), responsavel_id=COALESCE(?,responsavel_id) 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")); if(responsavelId==null)ps.setNull(6,Types.BIGINT);else ps.setLong(6,responsavelId); ps.setLong(7,aid);ps.setLong(8,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 a.*, r.nome responsavel_nome FROM assembleias a LEFT JOIN assembleia_responsaveis r ON r.id=a.responsavel_id WHERE a.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<>(); @@ -194,9 +199,10 @@ public final class ClienteHandlers implements HttpHandler { } } if(dests.isEmpty()){ - Map a=one(c,"SELECT condominio_id FROM assembleias WHERE id=?",Arrays.asList(aid)); + Map a=one(c,"SELECT condominio_id, responsavel_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); + Long respId=a!=null&&a.get("responsavel_id")!=null?((Number)a.get("responsavel_id")).longValue():null; + dests=destinatariosPadraoAssembleia(c,emp,cid,respId); } 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();} @@ -218,7 +224,8 @@ public final class ClienteHandlers implements HttpHandler { HttpResponses.json(ex,400,Map.of("erro","instancia_pendente_aprovacao","mensagem","A conexão WhatsApp não está configurada ou ainda não foi aprovada pelo painel Master.")); return; } - List padrao=destinatariosPadraoAssembleia(c,emp,cond); + Long respId=ass.get("responsavel_id")!=null?((Number)ass.get("responsavel_id")).longValue():null; + List padrao=destinatariosPadraoAssembleia(c,emp,cond,respId); if(padrao.isEmpty()){ HttpResponses.json(ex,400,Map.of("erro","sem_destinatarios","mensagem","Nenhum contato ou funcionário vinculado para receber a notificação deste condomínio.")); return; @@ -304,23 +311,20 @@ public final class ClienteHandlers implements HttpHandler { private void criarResponsavelAssembleia(HttpExchange ex,Connection c,long fid)throws Exception{String raw=rawBody(ex);Mapb=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 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);Mapb=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 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 validarAgendaResponsaveisAssembleia(Connection c,Timestamp dataHora,Long ignorarAssembleia)throws Exception{ - if(dataHora==null)return null; - List> responsaveis=rows(c,"SELECT id,nome,dias_semana FROM assembleia_responsaveis WHERE status='ativo' ORDER BY nome",Arrays.asList()); - if(responsaveis.isEmpty())return null; + private Map validarAgendaResponsaveisAssembleia(Connection c,Timestamp dataHora,Long ignorarAssembleia,Long responsavelId)throws Exception{ + if(dataHora==null||responsavelId==null)return null; + Map r=one(c,"SELECT id,nome,dias_semana FROM assembleia_responsaveis WHERE id=? AND status='ativo'",Arrays.asList(responsavelId)); + if(r==null)return Map.of("erro","responsavel_invalido","mensagem","Responsável selecionado não encontrado ou inativo."); int dia=dataHora.toLocalDateTime().getDayOfWeek().getValue()%7; - List indisponiveis=new ArrayList<>(); - for(Map r:responsaveis){ - Set dias=new HashSet<>(Arrays.asList(str(r.get("dias_semana")).split(","))); - if(!dias.contains(String.valueOf(dia)))indisponiveis.add(str(r.get("nome"))); + Set dias=new HashSet<>(Arrays.asList(str(r.get("dias_semana")).split(","))); + if(!dias.contains(String.valueOf(dia))){ + return Map.of("erro","responsavel_indisponivel","mensagem","O responsável "+r.get("nome")+" não está disponível para este dia da semana."); } - 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 params=new ArrayList<>();params.add(dataHora);if(ignorarAssembleia!=null)params.add(ignorarAssembleia); + String sql="SELECT a.id,a.data_hora FROM assembleias a WHERE a.responsavel_id=? AND a.status<>'cancelada' AND a.data_hora::date=?::date"+(ignorarAssembleia==null?"":" AND a.id<>?"); + List params=new ArrayList<>();params.add(responsavelId);params.add(dataHora);if(ignorarAssembleia!=null)params.add(ignorarAssembleia); List> conflitos=rows(c,sql,params); if(!conflitos.isEmpty()){ - List 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 Map.of("erro","assembleia_duplicada_responsavel","mensagem","O responsável "+r.get("nome")+" já possui uma assembleia agendada para esta data."); } return null; } @@ -463,7 +467,7 @@ public final class ClienteHandlers implements HttpHandler { private Map evolutionInstancia(long empresaId)throws Exception{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id,empresa_id,instance_url,instance_token_enc,numero_whatsapp,status,solicitado_em,aprovado_em FROM evolution_instancias WHERE empresa_id=? ORDER BY solicitado_em DESC,id DESC LIMIT 1")){ps.setLong(1,empresaId);try(ResultSet rs=ps.executeQuery()){return rs.next()?resultMap(rs):null;}}} private void listarAssembleiasCalendario(HttpExchange ex,Connection c,long emp,String cargo,long fid)throws Exception{ - List> out=rows(c,"SELECT a.*, co.nome condominio_nome FROM assembleias a JOIN condominios co ON co.id=a.condominio_id ORDER BY a.data_hora DESC",Arrays.asList()); + List> out=rows(c,"SELECT a.*, co.nome condominio_nome, r.nome responsavel_nome FROM assembleias a JOIN condominios co ON co.id=a.condominio_id LEFT JOIN assembleia_responsaveis r ON r.id=a.responsavel_id ORDER BY a.data_hora DESC",Arrays.asList()); for(Mapa:out){ long aid=((Number)a.get("id")).longValue(); Map aviso=one(c,"SELECT id FROM avisos WHERE assembleia_id=? ORDER BY id DESC LIMIT 1",Arrays.asList(aid)); diff --git a/src/main/resources/migrations/tenant/020_assembleia_responsavel_id.sql b/src/main/resources/migrations/tenant/020_assembleia_responsavel_id.sql new file mode 100644 index 0000000..b2e5d54 --- /dev/null +++ b/src/main/resources/migrations/tenant/020_assembleia_responsavel_id.sql @@ -0,0 +1,2 @@ +ALTER TABLE assembleias ADD COLUMN IF NOT EXISTS responsavel_id BIGINT REFERENCES assembleia_responsaveis(id); +CREATE INDEX IF NOT EXISTS idx_assembleias_responsavel ON assembleias(responsavel_id); diff --git a/src/main/webapp/cliente/assembleias.html b/src/main/webapp/cliente/assembleias.html index 244b925..d765125 100644 --- a/src/main/webapp/cliente/assembleias.html +++ b/src/main/webapp/cliente/assembleias.html @@ -1 +1 @@ -Assembleia

Assembleia

Todas as assembleias

CondomínioTipoDataLocalStatusNotificadosAções

Responsáveis pela assembleia

NomeWhatsAppE-mailDias disponíveisStatusAções
+Assembleia

Assembleia

Todas as assembleias

CondomínioTipoDataLocalResponsávelStatusNotificadosAções

Responsáveis pela assembleia

NomeWhatsAppE-mailDias disponíveisStatusAções
diff --git a/src/main/webapp/cliente/assembleias.js b/src/main/webapp/cliente/assembleias.js index 37fcef9..a6e52c9 100644 --- a/src/main/webapp/cliente/assembleias.js +++ b/src/main/webapp/cliente/assembleias.js @@ -23,8 +23,8 @@ const Icons = { function renderAssembleias(){ const q=termo(); - const lista=assembleias.filter(a=>contem(q,a.condominio_nome,a.tipo,a.data_hora,a.local,a.status,destinatariosLabel(a))); - rows.innerHTML=lista.map(a=>`${esc(a.condominio_nome||'-')}${esc(a.tipo)}${Util.date(a.data_hora)}${esc(a.local||'')}${Util.badge(a.status)}${esc(destinatariosLabel(a))}
`).join('')||'Nenhuma assembleia encontrada.'; + const lista=assembleias.filter(a=>contem(q,a.condominio_nome,a.tipo,a.data_hora,a.local,a.responsavel_nome,a.status,destinatariosLabel(a))); + rows.innerHTML=lista.map(a=>`${esc(a.condominio_nome||'-')}${esc(a.tipo)}${Util.date(a.data_hora)}${esc(a.local||'')}${esc(a.responsavel_nome||'-')}${Util.badge(a.status)}${esc(destinatariosLabel(a))}
`).join('')||'Nenhuma assembleia encontrada.'; } function ensureCalendarModal(){let m=document.querySelector('#assembleia-calendar-modal');if(m)return m;document.body.insertAdjacentHTML('beforeend','');return document.querySelector('#assembleia-calendar-modal')} function renderCalendarioAssembleias(selected){ensureCalendarModal();const title=document.querySelector('#calendar-title'),grid=document.querySelector('#calendar-grid');title.textContent=monthLabel(calMonth);const y=calMonth.getFullYear(),m=calMonth.getMonth(),start=new Date(y,m,1-new Date(y,m,1).getDay()),todayKey=dateKey(new Date());let html=['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'].map(x=>'
'+x+'
').join('');for(let i=0;i<42;i++){const d=new Date(start);d.setDate(start.getDate()+i);const key=dateKey(d),items=assembleias.filter(a=>dateKey(a.data_hora)===key),classes=['calendar-day'];if(d.getMonth()!==m)classes.push('other');if(items.length)classes.push('busy');if(key===todayKey)classes.push('today');html+=''}grid.innerHTML=html;grid.querySelectorAll('.calendar-day').forEach(btn=>btn.onclick=()=>selecionarDiaCalendario(btn.dataset.date));if(selected)selecionarDiaCalendario(selected,false)} @@ -77,7 +77,7 @@ window.alternarBuscaAssembleias=()=>{ window.abrirCalendarioAssembleias=async()=>{try{if(!assembleias.length)await load();calMonth=new Date();ensureCalendarModal().classList.remove('hidden');renderCalendarioAssembleias()}catch(e){Util.toast(e.message,true)}}; window.fecharCalendarioAssembleias=()=>document.querySelector('#assembleia-calendar-modal')?.classList.add('hidden'); window.mudarMesCalendario=delta=>{calMonth=new Date(calMonth.getFullYear(),calMonth.getMonth()+delta,1);renderCalendarioAssembleias()}; -window.selecionarDiaCalendario=(key,mark=true)=>{if(mark)document.querySelectorAll('.calendar-day').forEach(x=>x.classList.toggle('selected',x.dataset.date===key));const list=document.querySelector('#calendar-day-list'),title=document.querySelector('#calendar-day-title'),items=assembleias.filter(a=>dateKey(a.data_hora)===key);const [yy,mm,dd]=key.split('-');title.textContent=dd+'/'+mm+'/'+yy;if(!items.length){list.innerHTML='

Nenhuma assembleia agendada neste dia.

';return;}list.innerHTML=items.map(a=>'
'+esc(a.condominio_nome||'-')+''+Util.date(a.data_hora)+''+esc(a.tipo)+' - '+esc(a.local||'-')+'

'+esc(destinatariosLabel(a))+'

').join('')}; +window.selecionarDiaCalendario=(key,mark=true)=>{if(mark)document.querySelectorAll('.calendar-day').forEach(x=>x.classList.toggle('selected',x.dataset.date===key));const list=document.querySelector('#calendar-day-list'),title=document.querySelector('#calendar-day-title'),items=assembleias.filter(a=>dateKey(a.data_hora)===key);const [yy,mm,dd]=key.split('-');title.textContent=dd+'/'+mm+'/'+yy;if(!items.length){list.innerHTML='

Nenhuma assembleia agendada neste dia.

';return;}list.innerHTML=items.map(a=>'
'+esc(a.condominio_nome||'-')+''+Util.date(a.data_hora)+''+esc(a.tipo)+' - '+esc(a.local||'-')+'Responsável: '+esc(a.responsavel_nome||'-')+'

'+esc(destinatariosLabel(a))+'

').join('')}; window.enviarNotificacaoAssembleia=async(btn,condominioId,assembleiaId)=>{if(!await Util.confirm('Deseja enviar a notificação para os contatos e responsáveis desta assembleia?','Enviar notificação'))return;const original=btn?btn.innerHTML:'';if(btn){btn.disabled=true;btn.innerHTML='...';}try{const res=await Api.post(`/api/condominios/${condominioId}/assembleias/${assembleiaId}/notificar`,{});if(res&&res.ok===false){Util.toast(res.mensagem||'Falha ao enviar notificação.',true);}else{Util.toast(res?.mensagem||'Notificação enviada com sucesso!');}await load();}catch(err){Util.toast(err.message,true);}finally{if(btn){btn.disabled=false;btn.innerHTML=original;}}}; window.excluirAssembleia=async(condominioId,assembleiaId)=>{if(!await Util.confirm('Tem certeza que deseja excluir esta assembleia? Esta ação não pode ser desfeita.','Excluir assembleia'))return;try{const res=await Api.request(`/api/condominios/${condominioId}/assembleias/${assembleiaId}`,{method:'DELETE'});Util.toast(res?.mensagem||'Assembleia excluída com sucesso.');await load();if(document.querySelector('#assembleia-calendar-modal')&&!document.querySelector('#assembleia-calendar-modal').classList.contains('hidden')){renderCalendarioAssembleias();}}catch(err){Util.toast(err.message,true);}}; window.abrirNovoResponsavel=()=>abrirResponsavel({}); diff --git a/src/main/webapp/cliente/condominio-detalhe.js b/src/main/webapp/cliente/condominio-detalhe.js index dc5aea6..7e14232 100644 --- a/src/main/webapp/cliente/condominio-detalhe.js +++ b/src/main/webapp/cliente/condominio-detalhe.js @@ -1,4 +1,4 @@ -const id=new URLSearchParams(location.search).get('id');const sections=['contatos','espacos','assembleias','documentos','avisos','respostas'];document.querySelectorAll('.nav-tabs button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.nav-tabs button').forEach(x=>x.classList.remove('active'));b.classList.add('active');sections.forEach(s=>document.querySelector('#'+s).classList.toggle('hidden',s!==b.dataset.tab))});let contatos=[],docs=[],avisos=[],respostas=[],funcionariosVinculados=[],extras=[],assembleiasCalendario=[],calMonth=new Date(),espacos=[],assembleias=[];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 recorrencia'} const Icons = { add: '', search: '', @@ -25,7 +25,7 @@ function renderCalendarioAssembleias(selected){ensureCalendarModal();const title window.abrirCalendarioAssembleias=async()=>{try{const d=await Api.get('/api/assembleias-calendario');assembleiasCalendario=d.assembleias||[];calMonth=new Date();ensureCalendarModal().classList.remove('hidden');renderCalendarioAssembleias()}catch(e){Util.toast(e.message,true)}}; window.fecharCalendarioAssembleias=()=>document.querySelector('#assembleia-calendar-modal')?.classList.add('hidden'); window.mudarMesCalendario=delta=>{calMonth=new Date(calMonth.getFullYear(),calMonth.getMonth()+delta,1);renderCalendarioAssembleias()}; -window.selecionarDiaCalendario=(key,mark=true)=>{if(mark)document.querySelectorAll('.calendar-day').forEach(x=>x.classList.toggle('selected',x.dataset.date===key));const list=document.querySelector('#calendar-day-list'),title=document.querySelector('#calendar-day-title'),items=assembleiasCalendario.filter(a=>dateKey(a.data_hora)===key);const [yy,mm,dd]=key.split('-');title.textContent=dd+'/'+mm+'/'+yy;if(!items.length){list.innerHTML='

Nenhuma assembleia agendada neste dia.

';return;}list.innerHTML=items.map(a=>{const dest=(a.destinatarios||[]).map(x=>x.nome+' ('+x.tipo+')').join(', ')||'Nenhum destinatário registrado';return '
'+a.condominio_nome+''+Util.date(a.data_hora)+''+a.tipo+' - '+(a.local||'-')+'

'+dest+'

'}).join('')}; +window.selecionarDiaCalendario=(key,mark=true)=>{if(mark)document.querySelectorAll('.calendar-day').forEach(x=>x.classList.toggle('selected',x.dataset.date===key));const list=document.querySelector('#calendar-day-list'),title=document.querySelector('#calendar-day-title'),items=assembleiasCalendario.filter(a=>dateKey(a.data_hora)===key);const [yy,mm,dd]=key.split('-');title.textContent=dd+'/'+mm+'/'+yy;if(!items.length){list.innerHTML='

Nenhuma assembleia agendada neste dia.

';return;}list.innerHTML=items.map(a=>{const dest=(a.destinatarios||[]).map(x=>x.nome+' ('+x.tipo+')').join(', ')||'Nenhum destinatário registrado';return '
'+esc(a.condominio_nome||'-')+''+Util.date(a.data_hora)+''+esc(a.tipo)+' - '+(esc(a.local)||'-')+'Responsável: '+esc(a.responsavel_nome||'-')+'

'+dest+'

'}).join('')}; window.reenviarAvisoAssembleia=async(cond,aid)=>{try{const res=await Api.post('/api/condominios/'+cond+'/assembleias/'+aid+'/notificar',{});if(res&&res.ok===false){Util.toast(res.mensagem||'Falha ao enviar notificação.',true);}else{Util.toast(res?.mensagem||'Notificação enviada com sucesso!');}await abrirCalendarioAssembleias();await loadAssembleias();await load();}catch(e){Util.toast(e.message,true)}}; window.cancelarAssembleiaCalendario=async(cond,aid)=>{if(!await Util.confirm('Cancelar esta assembleia?','Cancelar assembleia'))return;try{await Api.put('/api/condominios/'+cond+'/assembleias/'+aid,{status:'cancelada'});Util.toast('Assembleia cancelada');await abrirCalendarioAssembleias();await loadAssembleias();await load()}catch(e){Util.toast(e.message,true)}}; function ensureAvisoNotifModal(){let m=document.querySelector('#aviso-notif-modal');if(m)return m;document.body.insertAdjacentHTML('beforeend','');return document.querySelector('#aviso-notif-modal')} @@ -35,7 +35,7 @@ window.abrirAvisoNotificacao=()=>{const m=ensureAvisoNotifModal(),ids=new Set(av window.fecharAvisoNotificacao=()=>document.querySelector('#aviso-notif-modal')?.classList.add('hidden'); window.salvarAvisoNotificacao=()=>{const ids=new Set([...document.querySelectorAll('#aviso-notif-body input:checked')].map(x=>x.value));avisoDestInputs().forEach(x=>x.checked=ids.has(x.value));updateAvisoNotifSummary();fecharAvisoNotificacao()}; async function safeLoad(section,fn){try{await fn()}catch(e){document.querySelector('#'+section).innerHTML=`

${e.message}

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

${d.condominio.nome}

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

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

${d.condominio.nome}

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

Sindico${d.sindico_atual?.contato_nome||'-'}
Assembleias futuras${d.assembleias_futuras||0}
Avisos ativos${d.avisos_ativos||0}
`;await safeLoad('contatos',loadContatos);await safeLoad('avisos',loadFuncionariosVinculados);await safeLoad('avisos',loadExtras);await safeLoad('assembleias',loadResponsaveis);await safeLoad('espacos',loadEspacos);await safeLoad('assembleias',loadAssembleias);await safeLoad('documentos',loadDocs);await safeLoad('avisos',loadAvisos);await safeLoad('respostas',loadRespostas)}catch(e){Util.toast(e.message,true)}} async function loadContatos(){const d=await Api.get(`/api/condominios/${id}/contatos`);contatos=d.contatos||[];renderContatos()} function renderContatos(){ const q=termoDetalhe('contatos'); @@ -57,16 +57,71 @@ window.abrirEditarEspaco=eid=>{const x=espacos.find(e=>Number(e.id)===Number(eid window.excluirEspacoDetalhe=async eid=>{if(!await Util.confirm('Tem certeza que deseja excluir este espaço comum?','Excluir espaço'))return;try{await Api.request(`/api/condominios/${id}/espacos-comuns/${eid}`,{method:'DELETE'});Util.toast('Espaço excluído');loadEspacos()}catch(err){Util.toast(err.message,true)}}; window.abrirNovoEspaco=()=>{abrirDetailModal('Novo espaço comum','
');document.querySelector('#espaco-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/espacos-comuns`,Util.formData(e.target));Util.toast('Espaço salvo');fecharDetailModal();loadEspacos()}catch(err){Util.toast(err.message,true)}}}; +async function loadResponsaveis(){try{const d=await Api.get('/api/assembleia-responsaveis');responsaveis=(d.responsaveis||[]).filter(r=>r.status==='ativo');}catch(e){}} async function loadAssembleias(){const d=await Api.get(`/api/condominios/${id}/assembleias`);assembleias=d.assembleias||[];renderAssembleias()} function renderAssembleias(){ const q=termoDetalhe('assembleias'); - const lista=assembleias.filter(a=>contemDetalhe(q,a.tipo,a.data_hora,a.local,a.status)); + const lista=assembleias.filter(a=>contemDetalhe(q,a.tipo,a.data_hora,a.local,a.responsavel_nome,a.status)); const calendar=''; - document.querySelector('#assembleias').innerHTML=headerCadastro('Assembleias','assembleias','abrirNovaAssembleia()',calendar)+`
${lista.map(a=>``).join('')||''}
TipoDataLocalStatusAções
${esc(a.tipo)}${Util.date(a.data_hora)}${esc(a.local||'')}${Util.badge(a.status)}
Nenhuma assembleia encontrada.
`; + document.querySelector('#assembleias').innerHTML=headerCadastro('Assembleias','assembleias','abrirNovaAssembleia()',calendar)+`
${lista.map(a=>``).join('')||''}
TipoDataLocalResponsávelStatusAções
${esc(a.tipo)}${Util.date(a.data_hora)}${esc(a.local||'')}${esc(a.responsavel_nome||'-')}${Util.badge(a.status)}
Nenhuma assembleia encontrada.
`; document.querySelector('#assembleias-search-input')?.addEventListener('input',renderAssembleias); } window.excluirAssembleiaDetalhe=async aid=>{if(!await Util.confirm('Tem certeza que deseja excluir esta assembleia? Esta ação não pode ser desfeita.','Excluir assembleia'))return;try{await Api.request(`/api/condominios/${id}/assembleias/${aid}`,{method:'DELETE'});Util.toast('Assembleia excluída');await loadAssembleias();await load()}catch(err){Util.toast(err.message,true)}}; -window.abrirNovaAssembleia=()=>{abrirDetailModal('Nova assembleia','
');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');fecharDetailModal();loadAssembleias();load()}catch(err){Util.toast(err.message,true)}}}; +window.abrirNovaAssembleia=async ()=>{ + if(!responsaveis.length)await loadResponsaveis(); + abrirDetailModal('Nova assembleia',`
`); + const dtInput=document.querySelector('#ass-data-hora'); + const selResp=document.querySelector('#ass-responsavel'); + function filtrarResponsaveisPorData(){ + const val=dtInput?.value; + if(!val){ + selResp.innerHTML=''; + return; + } + const datePart=val.split('T')[0]; + if(!datePart){ + selResp.innerHTML=''; + return; + } + const [y,m,d]=datePart.split('-').map(Number); + if(!y||!m||!d){ + selResp.innerHTML=''; + return; + } + const dateObj=new Date(y,m-1,d); + const dia=dateObj.getDay(); + const diaNome=diasNomes[dia]||''; + const disp=(responsaveis||[]).filter(r=>{ + if(r.status&&r.status!=='ativo')return false; + const dias=String(r.dias_semana||'').split(',').map(x=>x.trim()); + return dias.includes(String(dia)); + }); + if(!disp.length){ + selResp.innerHTML=``; + }else{ + selResp.innerHTML=``+disp.map(r=>``).join(''); + } + } + dtInput?.addEventListener('input',filtrarResponsaveisPorData); + dtInput?.addEventListener('change',filtrarResponsaveisPorData); + document.querySelector('#ass-form').onsubmit=async e=>{ + e.preventDefault(); + const f=e.target; + if(!f.responsavel_id.value){ + Util.toast('Selecione um responsável disponível para a data da assembleia.',true); + return; + } + try{ + await Api.post(`/api/condominios/${id}/assembleias`,Util.formData(f)); + Util.toast('Assembleia salva'); + fecharDetailModal(); + 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||[];renderDocs()}