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 1d269fc..a0a4f45 100644 --- a/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java +++ b/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java @@ -65,11 +65,13 @@ 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&&"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; } if(p.length==5&&"condominios".equals(p[0])&&"avisos".equals(p[2])&&"reenviar".equals(p[4])&&"POST".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} reenviarAviso(ex,c,empresaId,cond,id(p[3])); return; } if(p.length==1&&"condominios".equals(p[0])){ if("POST".equals(method)) criarCondominio(ex,c,funcionarioId,cargo); else if("GET".equals(method)) listarCondominios(ex,c,funcionarioId,cargo); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==2&&"condominios".equals(p[0])){ long id=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,id)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} if("GET".equals(method)) detalheCondominio(ex,c,id); else if("PUT".equals(method)) atualizarCondominio(ex,c,id,funcionarioId); else HttpResponses.methodNotAllowed(ex); return; } + if(p.length==3&&"condominios".equals(p[0])&&"respostas".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)) listarRespostas(ex,c,cond); else if("POST".equals(method)) registrarResposta(ex,c,funcionarioId,cond); else HttpResponses.methodNotAllowed(ex); return; } if(p.length==1&&"contatos".equals(p[0])&&"POST".equals(method)){ criarContato(ex,c,funcionarioId); return; } if(p.length==3&&"condominios".equals(p[0])&&"contatos".equals(p[2])&&"GET".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} listarContatos(ex,c,cond); return; } if(p.length==4&&"condominios".equals(p[0])&&"contatos".equals(p[2])&&"vincular".equals(p[3])&&"POST".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} vincularContato(ex,c,cond,funcionarioId); return; } @@ -91,6 +93,8 @@ public final class ClienteHandlers implements HttpHandler { private boolean podeAcessarCondominio(Connection c,String cargo,long fid,long cond)throws SQLException{return adminEmpresa(cargo)||count(c,"SELECT count(*) FROM funcionario_condominio WHERE funcionario_id=? AND condominio_id=?",fid,cond)>0;} private void detalheCondominio(HttpExchange ex,Connection c,long id)throws Exception{Map cond=one(c,"SELECT * FROM condominios WHERE id=?",Arrays.asList(id)); if(cond==null){HttpResponses.json(ex,404,Map.of("erro","condominio_nao_encontrado"));return;} Map sind=one(c,"SELECT cc.*, c.nome contato_nome,c.telefone contato_telefone,c.email contato_email FROM contato_condominio cc JOIN contatos c ON c.id=cc.contato_id WHERE cc.condominio_id=? AND cc.tipo='sindico' AND cc.status='ativo' ORDER BY cc.data_inicio_mandato DESC NULLS LAST LIMIT 1",Arrays.asList(id)); long ass=count(c,"SELECT count(*) FROM assembleias WHERE condominio_id=? AND data_hora>now() AND status='agendada'",id); long av=count(c,"SELECT count(*) FROM avisos WHERE condominio_id=? AND (data_expiracao IS NULL OR data_expiracao>now())",id); Map resp=new LinkedHashMap<>();resp.put("condominio",cond);resp.put("sindico_atual",sind);resp.put("assembleias_futuras",ass);resp.put("avisos_ativos",av);HttpResponses.json(ex,200,resp);} private void atualizarCondominio(HttpExchange ex,Connection c,long id,long fid)throws Exception{Mapb=body(ex); Map old=one(c,"SELECT * FROM condominios WHERE id=?",Arrays.asList(id)); if(old==null){HttpResponses.json(ex,404,Map.of("erro","condominio_nao_encontrado"));return;} try(PreparedStatement ps=c.prepareStatement("UPDATE condominios SET nome=COALESCE(?,nome), cnpj=COALESCE(?,cnpj), endereco=COALESCE(?,endereco) WHERE id=?")){ps.setString(1,b.get("nome"));ps.setString(2,b.get("cnpj"));ps.setString(3,b.get("endereco"));ps.setLong(4,id);ps.executeUpdate();} audit(c,fid,"update","condominios",id,old,b); detalheCondominio(ex,c,id);} + private void listarRespostas(HttpExchange ex,Connection c,long cond)throws Exception{HttpResponses.json(ex,200,Map.of("respostas",rows(c,"SELECT r.*, co.nome contato_nome, co.telefone contato_telefone, co.email contato_email FROM respostas r LEFT JOIN contatos co ON co.id=r.contato_id WHERE r.condominio_id=? ORDER BY r.recebido_em DESC,id DESC",Arrays.asList(cond))));} + private void registrarResposta(HttpExchange ex,Connection c,long fid,Long condPath)throws Exception{String raw=rawBody(ex);Mapb=SimpleJson.parseObject(raw);Long contato=optLong(firstNonBlank(b.get("contato_id"),b.get("destinatario_id")));Long cond=condPath!=null?condPath:optLong(b.get("condominio_id"));if(cond==null&&contato!=null){Map link=one(c,"SELECT condominio_id FROM contato_condominio WHERE contato_id=? AND status='ativo' ORDER BY CASE WHEN tipo='sindico' THEN 0 ELSE 1 END, condominio_id LIMIT 1",Arrays.asList(contato));if(link!=null)cond=((Number)link.get("condominio_id")).longValue();}if(cond==null){HttpResponses.json(ex,400,Map.of("erro","condominio_obrigatorio","mensagem","Informe condominio_id ou contato_id vinculado a um condominio."));return;}Map contatoRow=contato==null?null:one(c,"SELECT id,nome,telefone,email FROM contatos WHERE id=?",Arrays.asList(contato));String texto=firstNonBlank(b.get("resposta"),b.get("resposta_texto"),b.get("mensagem"),b.get("texto"),b.get("body"));if(texto.isBlank()){HttpResponses.json(ex,400,Map.of("erro","resposta_obrigatoria","mensagem","Informe a resposta recebida."));return;}long rid=insert(c,"INSERT INTO respostas (condominio_id,contato_id,mensagem_id,tipo_origem,nome_origem,telefone_origem,resposta_texto,mensagem_original,payload,origem,status) VALUES (?,?,?,?,?,?,?,?,?,?,?)",Arrays.asList(cond,contato,optLong(b.get("mensagem_id")),nullable(firstNonBlank(b.get("tipo"),b.get("tipo_origem"))),nullable(firstNonBlank(b.get("nome"),contatoRow==null?null:str(contatoRow.get("nome")))),nullable(firstNonBlank(b.get("telefone"),contatoRow==null?null:str(contatoRow.get("telefone")))),texto,nullable(firstNonBlank(b.get("mensagem_original"),b.get("pergunta"),b.get("mensagem_enviada"))),raw,firstNonBlank(b.get("origem"),"n8n"),firstNonBlank(b.get("status"),"recebida")));audit(c,fid,"create","respostas",rid,null,b);HttpResponses.json(ex,201,Map.of("ok",true,"resposta",one(c,"SELECT * FROM respostas WHERE id=?",Arrays.asList(rid)))) ;} private void criarContato(HttpExchange ex,Connection c,long fid)throws Exception{Mapb=body(ex); long id=insert(c,"INSERT INTO contatos (nome,telefone,email) VALUES (?,?,?)",Arrays.asList(req(b,"nome"),nullable(b.get("telefone")),nullable(b.get("email")))); audit(c,fid,"create","contatos",id,null,b); HttpResponses.json(ex,201,Map.of("contato",one(c,"SELECT * FROM contatos WHERE id=?",Arrays.asList(id))));} private void vincularContato(HttpExchange ex,Connection c,long cond,long fid)throws Exception{Mapb=body(ex); Long contato=optLong(b.get("contato_id")); if(contato==null){contato=insert(c,"INSERT INTO contatos (nome,telefone,email) VALUES (?,?,?)",Arrays.asList(req(b,"nome"),nullable(b.get("telefone")),nullable(b.get("email"))));} String tipo=req(b,"tipo"); if("sindico".equals(tipo)){try(PreparedStatement ps=c.prepareStatement("UPDATE contato_condominio SET status='inativo', data_fim_mandato=current_date WHERE condominio_id=? AND tipo='sindico' AND status='ativo' AND contato_id<>?")){ps.setLong(1,cond);ps.setLong(2,contato);ps.executeUpdate();}} try(PreparedStatement ps=c.prepareStatement("INSERT INTO contato_condominio (contato_id,condominio_id,tipo,data_inicio_mandato,data_fim_mandato,status) VALUES (?,?,?,?,?,'ativo') ON CONFLICT (contato_id,condominio_id,tipo) DO UPDATE SET data_inicio_mandato=EXCLUDED.data_inicio_mandato,data_fim_mandato=EXCLUDED.data_fim_mandato,status='ativo'")){ps.setLong(1,contato);ps.setLong(2,cond);ps.setString(3,tipo);setDate(ps,4,b.get("data_inicio_mandato"));setDate(ps,5,b.get("data_fim_mandato"));ps.executeUpdate();} if(Set.of("sindico","subsindico","conselheiro").contains(tipo))vincularContatoAosAvisosRecorrentes(c,cond,contato); audit(c,fid,"create","contato_condominio",contato,null,b); listarContatos(ex,c,cond);} private void vincularContatoAosAvisosRecorrentes(Connection c,long cond,long contato)throws SQLException{ @@ -567,7 +571,7 @@ public final class ClienteHandlers implements HttpHandler { if(x.matches("\\d{2}/\\d{2}/\\d{4} \\d{2}:\\d{2}:\\d{2}")){LocalDateTime dt=LocalDateTime.parse(x,java.time.format.DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"));return Timestamp.valueOf(dt);} }catch(Exception ignored){} throw new IllegalArgumentException("Data/hora invalida. Use dd/mm/aaaa hh:mm."); - } private int recorrenciaMeses(String v){if(v==null||v.isBlank())return 0;int n=Integer.parseInt(v);if(n==0||n==1||n==3||n==12)return n;throw new IllegalArgumentException("Recorrencia invalida. Use 0, 1, 3 ou 12 meses.");} private boolean bool(String v){return "true".equalsIgnoreCase(v)||"1".equals(v)||"sim".equalsIgnoreCase(v);} private String str(Object v){return v==null?"":String.valueOf(v);} private String 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 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 longArray(String raw,String key){String a=arrayBody(raw,key);if(a.isBlank())return List.of();List out=new ArrayList<>();for(String x:a.split(",")){String v=x.trim().replace("\"","");if(!v.isBlank())out.add(Long.parseLong(v));}return out;} private List destinatarios(String raw){String a=arrayBody(raw,"destinatarios");if(a.isBlank())return List.of();List out=new ArrayList<>();for(String obj:a.split("\\},\\s*\\{")){String x=obj.replace("{","").replace("}","");String tipo=null;Long id=null;for(String kv:x.split(",")){String[]p=kv.split(":",2);if(p.length<2)continue;String k=p[0].trim().replace("\"","");String v=p[1].trim().replace("\"","");if("tipo".equals(k)||"tipo_destinatario".equals(k))tipo=v;if("id".equals(k)||"destinatario_id".equals(k))id=Long.parseLong(v);}if(tipo!=null&&id!=null)out.add(new Destinatario(tipo,id));}return out;} 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;iDetalhe do Condominio

Detalhe do Condominio

\ No newline at end of file +Detalhe do Condominio

Detalhe do Condominio

diff --git a/src/main/webapp/cliente/condominio-detalhe.js b/src/main/webapp/cliente/condominio-detalhe.js index f35510f..ed21ef0 100644 --- a/src/main/webapp/cliente/condominio-detalhe.js +++ b/src/main/webapp/cliente/condominio-detalhe.js @@ -1,7 +1,8 @@ -const id=new URLSearchParams(location.search).get('id');const sections=['contatos','espacos','assembleias','documentos','avisos'];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=[],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=[];function recLabel(v){return {1:'Mensal',3:'Trimestral',12:'Anual'}[Number(v)]||'Sem recorrencia'} function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))} function lupaBtn(fn,label){return ``} function headerCadastro(titulo,tipo,novoFn,extra=''){const q=esc(termoDetalhe(tipo)),open=document.querySelector(`#${tipo}-search`)&&!document.querySelector(`#${tipo}-search`)?.classList.contains('hidden');return `

${titulo}

${extra}${lupaBtn(`alternarBuscaDetalhe('${tipo}')`,'Buscar '+titulo)}
`} +function headerConsulta(titulo,tipo,extra=''){const q=esc(termoDetalhe(tipo)),open=document.querySelector(`#${tipo}-search`)&&!document.querySelector(`#${tipo}-search`)?.classList.contains('hidden');return `

${titulo}

${extra}${lupaBtn(`alternarBuscaDetalhe('${tipo}')`,'Buscar '+titulo)}
`} function termoDetalhe(tipo){return document.querySelector(`#${tipo}-search-input`)?.value.trim().toLowerCase()||''} function contemDetalhe(q,...vals){return !q||vals.some(v=>String(v??'').toLowerCase().includes(q))} window.alternarBuscaDetalhe=tipo=>{const strip=document.querySelector(`#${tipo}-search`),input=document.querySelector(`#${tipo}-search-input`);if(!strip||!input)return;strip.classList.toggle('hidden');if(!strip.classList.contains('hidden'))input.focus()}; @@ -26,7 +27,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)}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('espacos',loadEspacos);await safeLoad('assembleias',loadAssembleias);await safeLoad('documentos',loadDocs);await safeLoad('avisos',loadAvisos);await safeLoad('respostas',loadRespostas)}catch(e){Util.toast(e.message,true)}} async function loadContatos(){const d=await Api.get(`/api/condominios/${id}/contatos`);contatos=d.contatos||[];renderContatos()} function renderContatos(){const q=termoDetalhe('contatos');const lista=contatos.filter(c=>contemDetalhe(q,c.contato_nome,c.tipo,c.contato_telefone,c.contato_email));document.querySelector('#contatos').innerHTML=headerCadastro('Contatos','contatos','abrirNovoContato()')+`
${lista.map(c=>``).join('')||''}
NomeTipoTelefoneEmail
${esc(c.contato_nome)}${esc(c.tipo)}${esc(c.contato_telefone||'')}${esc(c.contato_email||'')}
Nenhum contato encontrado.
`;document.querySelector('#contatos-search-input')?.addEventListener('input',renderContatos)} window.abrirNovoContato=()=>{abrirDetailModal('Novo contato','
');document.querySelector('#contato-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/contatos/vincular`,Util.formData(e.target));Util.toast('Contato salvo');fecharDetailModal();loadContatos();load()}catch(err){Util.toast(err.message,true)}}}; @@ -48,6 +49,8 @@ function avisoFormHtml(){const destContatos=contatos.map(c=>`