From 9ec2de5459c6d7b873483c50d8c8285cb7cd3206 Mon Sep 17 00:00:00 2001 From: Tiago Date: Fri, 9 Oct 2026 15:50:47 -0300 Subject: [PATCH] Adiciona icones de Envio e Lixeira para assembleias com suporte a exclusao --- .../tenant/http/ClienteHandlers.java | 34 +++++++++- src/main/webapp/cliente/assembleias.js | 7 +- src/main/webapp/cliente/condominio-detalhe.js | 3 +- src/main/webapp/css/app.css | 65 +++++++++++++++++++ 4 files changed, 104 insertions(+), 5 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 c2b82a9..b02bec0 100644 --- a/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java +++ b/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java @@ -83,7 +83,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,empresaId,cond,id(p[3]),funcionarioId); 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; } if(p.length==3&&"documentos".equals(p[0])&&"download".equals(p[2])&&"GET".equals(method)){ downloadDocumento(ex,c,id(p[1])); return; } @@ -257,6 +257,38 @@ public final class ClienteHandlers implements HttpHandler { HttpResponses.json(ex,200,Map.of("ok",true,"status_envio",st,"mensagem","Notificação enviada com sucesso!")); } } + private void excluirAssembleia(HttpExchange ex,Connection c,long emp,long cond,long aid,long fid)throws Exception{ + 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","mensagem","Assembleia não encontrada.")); + return; + } + List> avisos=rows(c,"SELECT id FROM avisos WHERE assembleia_id=?",Arrays.asList(aid)); + c.setAutoCommit(false); + try{ + for(Map av:avisos){ + long avisoId=((Number)av.get("id")).longValue(); + try(PreparedStatement ps=c.prepareStatement("DELETE FROM aviso_documentos WHERE aviso_id=?")){ps.setLong(1,avisoId);ps.executeUpdate();} + try(PreparedStatement ps=c.prepareStatement("DELETE FROM aviso_destinatarios WHERE aviso_id=?")){ps.setLong(1,avisoId);ps.executeUpdate();} + try(PreparedStatement ps=c.prepareStatement("DELETE FROM fila_envio WHERE tipo='aviso' AND referencia_id=?")){ps.setLong(1,avisoId);ps.executeUpdate();} + try(PreparedStatement ps=c.prepareStatement("DELETE FROM avisos WHERE id=?")){ps.setLong(1,avisoId);ps.executeUpdate();} + 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,avisoId);ps.executeUpdate();} + } + try(PreparedStatement ps=c.prepareStatement("DELETE FROM assembleias WHERE id=? AND condominio_id=?")){ + ps.setLong(1,aid); + ps.setLong(2,cond); + ps.executeUpdate(); + } + audit(c,fid,"delete","assembleias",aid,old,Map.of("ok",true)); + c.commit(); + HttpResponses.json(ex,200,Map.of("ok",true,"mensagem","Assembleia excluída com sucesso.")); + }catch(Exception err){ + c.rollback(); + throw err; + }finally{ + c.setAutoCommit(true); + } + } private void uploadDocumento(HttpExchange ex,Connection c,long emp,long cond,long fid)throws Exception{Multipart mp=parseMultipart(ex); if(mp.bytes.length>MAX_UPLOAD){HttpResponses.json(ex,400,Map.of("erro","arquivo_muito_grande"));return;} String ext=ext(mp.filename); if(!EXT_OK.contains(ext)){HttpResponses.json(ex,400,Map.of("erro","extensao_nao_permitida"));return;} String db=databaseName(emp); Path dir=uploadRoot.resolve(db).resolve(String.valueOf(cond)); Files.createDirectories(dir); String safe=UUID.randomUUID()+"-"+mp.filename.replaceAll("[^a-zA-Z0-9._-]","_"); Path file=dir.resolve(safe); Files.write(file,mp.bytes); String rel=db+"/"+cond+"/"+safe; long id=insert(c,"INSERT INTO documentos (condominio_id,tipo,nome_arquivo,caminho_armazenamento,enviado_por) VALUES (?,?,?,?,?)",Arrays.asList(cond,nullable(mp.tipo),mp.filename,rel,fid)); audit(c,fid,"create","documentos",id,null,Map.of("nome_arquivo",mp.filename)); HttpResponses.json(ex,201,Map.of("documento",one(c,"SELECT * FROM documentos WHERE id=?",Arrays.asList(id))));} private void downloadDocumento(HttpExchange ex,Connection c,long id)throws Exception{Mapd=one(c,"SELECT * FROM documentos WHERE id=?",Arrays.asList(id)); if(d==null){HttpResponses.json(ex,404,Map.of("erro","documento_nao_encontrado"));return;} Path f=uploadRoot.resolve(String.valueOf(d.get("caminho_armazenamento"))); if(!Files.exists(f)){HttpResponses.json(ex,404,Map.of("erro","arquivo_nao_encontrado"));return;} ex.getResponseHeaders().set("Content-Disposition","attachment; filename=\""+d.get("nome_arquivo")+"\""); ex.sendResponseHeaders(200,Files.size(f)); Files.copy(f,ex.getResponseBody()); ex.close();} diff --git a/src/main/webapp/cliente/assembleias.js b/src/main/webapp/cliente/assembleias.js index 39464ab..52df4ab 100644 --- a/src/main/webapp/cliente/assembleias.js +++ b/src/main/webapp/cliente/assembleias.js @@ -18,7 +18,7 @@ function formHtml(r={}){ 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.'; + 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.'; } 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)} @@ -68,8 +68,9 @@ 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.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.textContent:'';if(btn){btn.disabled=true;btn.textContent='Enviando...';}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.textContent=original;}}}; +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.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.textContent:'';if(btn){btn.disabled=true;btn.textContent='...';}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.textContent=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({}); 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=''}; diff --git a/src/main/webapp/cliente/condominio-detalhe.js b/src/main/webapp/cliente/condominio-detalhe.js index ca3ac86..81b0b00 100644 --- a/src/main/webapp/cliente/condominio-detalhe.js +++ b/src/main/webapp/cliente/condominio-detalhe.js @@ -37,7 +37,8 @@ function renderEspacos(){const q=termoDetalhe('espacos');const lista=espacos.fil window.abrirNovoEspaco=()=>{abrirDetailModal('Novo espaco 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('Espaco salvo');fecharDetailModal();loadEspacos()}catch(err){Util.toast(err.message,true)}}}; 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 calendar='';document.querySelector('#assembleias').innerHTML=headerCadastro('Assembleias','assembleias','abrirNovaAssembleia()',calendar)+`
${lista.map(a=>``).join('')||''}
TipoDataLocalStatus
${esc(a.tipo)}${Util.date(a.data_hora)}${esc(a.local||'')}${Util.badge(a.status)}
Nenhuma assembleia encontrada.
`;document.querySelector('#assembleias-search-input')?.addEventListener('input',renderAssembleias)} +function renderAssembleias(){const q=termoDetalhe('assembleias');const lista=assembleias.filter(a=>contemDetalhe(q,a.tipo,a.data_hora,a.local,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-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)}}}; 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')} diff --git a/src/main/webapp/css/app.css b/src/main/webapp/css/app.css index 2bab213..4de31e3 100644 --- a/src/main/webapp/css/app.css +++ b/src/main/webapp/css/app.css @@ -486,6 +486,71 @@ td .btn, white-space: nowrap; } +.table-actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.table-icon-btn { + width: 36px; + height: 36px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: #ffffff; + color: var(--muted-strong); + cursor: pointer; + display: inline-grid; + place-items: center; + padding: 0; + box-shadow: none; + transition: all .16s ease; +} + +.table-icon-btn:hover { + background: var(--bg-soft); + color: var(--brand); + border-color: var(--brand); + transform: translateY(-1px); +} + +.table-icon-btn.send { + color: var(--brand); +} + +.table-icon-btn.send:hover { + background: var(--brand-soft); + border-color: var(--brand); + color: var(--brand-strong); +} + +.table-icon-btn.danger { + color: var(--danger); +} + +.table-icon-btn.danger:hover { + background: var(--danger-soft); + border-color: var(--danger); + color: var(--danger); +} + +.table-icon-btn svg { + width: 18px; + height: 18px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} + +.table-icon-btn:disabled { + opacity: .5; + cursor: not-allowed; + transform: none; +} + tbody tr:hover { background: #f8fbfc; }