Substitui botoes de texto por icones nas secoes de Detalhe do Condominio

This commit is contained in:
Tiago
2026-10-09 16:50:17 -03:00
parent cc7adefe54
commit b2fba5a441
3 changed files with 53 additions and 12 deletions
@@ -81,11 +81,12 @@ public final class ClienteHandlers implements HttpHandler {
if(p.length==3&&"condominios".equals(p[0])&&"funcionarios-vinculados".equals(p[2])){ long cond=id(p[1]); if("POST".equals(method)){ if(!adminEmpresa(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_restrito","mensagem","Apenas a conta principal pode vincular funcionarios a condominios."));return;} vincularFuncionario(ex,c,empresaId,cond,funcionarioId); } else if("GET".equals(method)){ if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} listarFuncionariosVinculados(ex,c,empresaId,cond); } else HttpResponses.methodNotAllowed(ex); return; }
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])&&"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==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==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==4&&"condominios".equals(p[0])&&"documentos".equals(p[2])&&"DELETE".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} excluirDocumento(ex,c,cond,id(p[3]),funcionarioId); 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"));
}
@@ -114,6 +115,7 @@ public final class ClienteHandlers implements HttpHandler {
private void listarFuncionariosVinculados(HttpExchange ex,Connection c,long emp,long cond)throws Exception{List<Map<String,Object>> links=rows(c,"SELECT * FROM funcionario_condominio WHERE condominio_id=? ORDER BY funcionario_id",Arrays.asList(cond));List<Map<String,Object>> out=new ArrayList<>();try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id,nome,email,telefone,cargo,status FROM funcionarios WHERE id=? AND empresa_id=?")){for(Map<String,Object> link:links){long func=((Number)link.get("funcionario_id")).longValue();ps.setLong(1,func);ps.setLong(2,emp);try(ResultSet rs=ps.executeQuery()){if(rs.next()){Map<String,Object> item=resultMap(rs);item.put("funcionario_id",func);item.put("condominio_id",cond);out.add(item);}}}}HttpResponses.json(ex,200,Map.of("funcionarios",out));}
private void criarEspaco(HttpExchange ex,Connection c,long cond,long fid)throws Exception{Map<String,String>b=body(ex); long id=insert(c,"INSERT INTO espacos_comuns (condominio_id,nome,descricao,valor_utilizacao_centavos,indice_correcao) VALUES (?,?,?,?,?)",Arrays.asList(cond,req(b,"nome"),nullable(b.get("descricao")),optLong(b.get("valor_utilizacao_centavos")),nullable(b.get("indice_correcao")))); audit(c,fid,"create","espacos_comuns",id,null,b); HttpResponses.json(ex,201,Map.of("espaco",one(c,"SELECT * FROM espacos_comuns WHERE id=?",Arrays.asList(id))));}
private void atualizarEspaco(HttpExchange ex,Connection c,long cond,long eid,long fid)throws Exception{Map<String,String>b=body(ex); Map<String,Object> old=one(c,"SELECT * FROM espacos_comuns WHERE id=? AND condominio_id=?",Arrays.asList(eid,cond)); if(old==null){HttpResponses.json(ex,404,Map.of("erro","espaco_nao_encontrado"));return;} Long novo=optLong(b.get("valor_utilizacao_centavos")); try(PreparedStatement ps=c.prepareStatement("UPDATE espacos_comuns SET nome=COALESCE(?,nome), descricao=COALESCE(?,descricao), valor_utilizacao_centavos=COALESCE(?,valor_utilizacao_centavos), indice_correcao=COALESCE(?,indice_correcao), data_ultimo_reajuste=CASE WHEN ? IS NOT NULL AND ?<>valor_utilizacao_centavos THEN current_date ELSE data_ultimo_reajuste END WHERE id=? AND condominio_id=?")){ps.setString(1,b.get("nome"));ps.setString(2,b.get("descricao")); if(novo==null)ps.setNull(3,Types.BIGINT);else ps.setLong(3,novo); ps.setString(4,b.get("indice_correcao")); if(novo==null){ps.setNull(5,Types.BIGINT);ps.setNull(6,Types.BIGINT);}else{ps.setLong(5,novo);ps.setLong(6,novo);} ps.setLong(7,eid);ps.setLong(8,cond);ps.executeUpdate();} audit(c,fid,"update","espacos_comuns",eid,old,b); HttpResponses.json(ex,200,Map.of("espaco",one(c,"SELECT * FROM espacos_comuns WHERE id=?",Arrays.asList(eid))));}
private void excluirEspaco(HttpExchange ex,Connection c,long cond,long eid,long fid)throws Exception{Map<String,Object> old=one(c,"SELECT * FROM espacos_comuns WHERE id=? AND condominio_id=?",Arrays.asList(eid,cond));if(old==null){HttpResponses.json(ex,404,Map.of("erro","espaco_nao_encontrado"));return;}try(PreparedStatement ps=c.prepareStatement("DELETE FROM espacos_comuns WHERE id=? AND condominio_id=?")){ps.setLong(1,eid);ps.setLong(2,cond);ps.executeUpdate();}audit(c,fid,"delete","espacos_comuns",eid,old,Map.of("ok",true));HttpResponses.json(ex,200,Map.of("ok",true));}
private void criarAssembleia(HttpExchange ex,Connection c,long emp,long cond,long fid)throws Exception{
String raw=rawBody(ex);
Map<String,String>b=SimpleJson.parseObject(removeArrayField(raw,"destinatarios"));
@@ -291,6 +293,7 @@ public final class ClienteHandlers implements HttpHandler {
}
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{Map<String,Object>d=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();}
private void excluirDocumento(HttpExchange ex,Connection c,long cond,long did,long fid)throws Exception{Map<String,Object> old=one(c,"SELECT * FROM documentos WHERE id=? AND condominio_id=?",Arrays.asList(did,cond));if(old==null){HttpResponses.json(ex,404,Map.of("erro","documento_nao_encontrado"));return;}try(PreparedStatement ps=c.prepareStatement("DELETE FROM aviso_documentos WHERE documento_id=?")){ps.setLong(1,did);ps.executeUpdate();}try(PreparedStatement ps=c.prepareStatement("DELETE FROM documentos WHERE id=? AND condominio_id=?")){ps.setLong(1,did);ps.setLong(2,cond);ps.executeUpdate();}audit(c,fid,"delete","documentos",did,old,Map.of("ok",true));HttpResponses.json(ex,200,Map.of("ok",true));}
private void listarExtras(HttpExchange ex,Connection c)throws Exception{HttpResponses.json(ex,200,Map.of("extras",rows(c,"SELECT * FROM destinatarios_extra WHERE status<>'excluido' ORDER BY nome",Arrays.asList())));}
@@ -1 +1 @@
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Detalhe do Condominio</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Detalhe do Condominio</h1></div><section class="panel" id="header"></section><div class="nav-tabs"><button data-tab="contatos" class="active">Contatos</button><button data-tab="espacos">Espacos</button><button data-tab="assembleias">Assembleias</button><button data-tab="documentos">Documentos</button><button data-tab="avisos">Avisos</button><button data-tab="respostas">Respostas</button></div><section class="panel tab" id="contatos"></section><section class="panel tab hidden" id="espacos"></section><section class="panel tab hidden" id="assembleias"></section><section class="panel tab hidden" id="documentos"></section><section class="panel tab hidden" id="avisos"></section><section class="panel tab hidden" id="respostas"></section></main><script>Layout.mount('empresa','condominios');</script><script src="/cliente/condominio-detalhe.js"></script></body></html>
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Detalhe do Condominio</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Detalhe do Condominio</h1></div><section class="panel" id="header"></section><div class="nav-tabs"><button data-tab="contatos" class="active">Contatos</button><button data-tab="espacos">Espaços</button><button data-tab="assembleias">Assembleias</button><button data-tab="documentos">Documentos</button><button data-tab="avisos">Avisos</button><button data-tab="respostas">Respostas</button></div><section class="panel tab" id="contatos"></section><section class="panel tab hidden" id="espacos"></section><section class="panel tab hidden" id="assembleias"></section><section class="panel tab hidden" id="documentos"></section><section class="panel tab hidden" id="avisos"></section><section class="panel tab hidden" id="respostas"></section></main><script>Layout.mount('empresa','condominios');</script><script src="/cliente/condominio-detalhe.js"></script></body></html>
+48 -10
View File
@@ -1,7 +1,15 @@
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 Icons = {
add: '<svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>',
search: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg>',
send: '<svg viewBox="0 0 24 24" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>',
edit: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>',
trash: '<svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>',
download: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>'
};
function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]))}
function lupaBtn(fn,label){return `<button class="search-icon-btn" type="button" onclick="${fn}" aria-label="${label}" title="Buscar"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m16.5 16.5 4 4"></path></svg></button>`}
function headerCadastro(titulo,tipo,novoFn,extra=''){const q=esc(termoDetalhe(tipo)),open=document.querySelector(`#${tipo}-search`)&&!document.querySelector(`#${tipo}-search`)?.classList.contains('hidden');return `<div class="section-head"><h2>${titulo}</h2><div class="section-actions">${lupaBtn(`alternarBuscaDetalhe('${tipo}')`,'Buscar '+titulo)}${extra}<button class="btn" type="button" onclick="${novoFn}">Novo</button></div></div><div id="${tipo}-search" class="search-strip ${open?'':'hidden'}"><input id="${tipo}-search-input" placeholder="Buscar" value="${q}"></div>`}
function headerCadastro(titulo,tipo,novoFn,extra=''){const q=esc(termoDetalhe(tipo)),open=document.querySelector(`#${tipo}-search`)&&!document.querySelector(`#${tipo}-search`)?.classList.contains('hidden');return `<div class="section-head"><h2>${titulo}</h2><div class="section-actions">${lupaBtn(`alternarBuscaDetalhe('${tipo}')`,'Buscar '+titulo)}${extra}<button class="header-add-btn" type="button" onclick="${novoFn}" title="Novo ${titulo.toLowerCase()}" aria-label="Novo ${titulo.toLowerCase()}">${Icons.add}</button></div></div><div id="${tipo}-search" class="search-strip ${open?'':'hidden'}"><input id="${tipo}-search-input" placeholder="Buscar" value="${q}"></div>`}
function headerConsulta(titulo,tipo,extra=''){const q=esc(termoDetalhe(tipo)),open=document.querySelector(`#${tipo}-search`)&&!document.querySelector(`#${tipo}-search`)?.classList.contains('hidden');return `<div class="section-head"><h2>${titulo}</h2><div class="section-actions">${extra}${lupaBtn(`alternarBuscaDetalhe('${tipo}')`,'Buscar '+titulo)}</div></div><div id="${tipo}-search" class="search-strip ${open?'':'hidden'}"><input id="${tipo}-search-input" placeholder="Buscar" value="${q}"></div>`}
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))}
@@ -29,29 +37,59 @@ window.salvarAvisoNotificacao=()=>{const ids=new Set([...document.querySelectorA
async function safeLoad(section,fn){try{await fn()}catch(e){document.querySelector('#'+section).innerHTML=`<p>${e.message}</p>`;Util.toast(e.message,true)}}
async function load(){try{const d=await Api.get(`/api/condominios/${id}`);document.querySelector('#header').innerHTML=`<h2>${d.condominio.nome}</h2><p>${d.condominio.endereco||''}</p><div class="cards"><div class="stat">Sindico<strong>${d.sindico_atual?.contato_nome||'-'}</strong></div><div class="stat">Assembleias futuras<strong>${d.assembleias_futuras||0}</strong></div><div class="stat">Avisos ativos<strong>${d.avisos_ativos||0}</strong></div></div>`;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()')+`<div class="table-wrap"><table><thead><tr><th>Nome</th><th>Tipo</th><th>Telefone</th><th>Email</th></tr></thead><tbody>${lista.map(c=>`<tr><td>${esc(c.contato_nome)}</td><td>${esc(c.tipo)}</td><td>${esc(c.contato_telefone||'')}</td><td>${esc(c.contato_email||'')}</td></tr>`).join('')||'<tr><td colspan="4">Nenhum contato encontrado.</td></tr>'}</tbody></table></div>`;document.querySelector('#contatos-search-input')?.addEventListener('input',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()')+`<div class="table-wrap"><table><thead><tr><th>Nome</th><th>Tipo</th><th>Telefone</th><th>Email</th><th style="text-align:right">Ações</th></tr></thead><tbody>${lista.map(c=>`<tr><td>${esc(c.contato_nome)}</td><td>${esc(c.tipo)}</td><td>${esc(c.contato_telefone||'')}</td><td>${esc(c.contato_email||'')}</td><td style="text-align:right"><div class="table-actions"><button class="table-icon-btn danger" type="button" onclick="desvincularContatoDetalhe(${c.contato_id},'${esc(c.tipo)}')" title="Desvincular contato" aria-label="Desvincular contato">${Icons.trash}</button></div></td></tr>`).join('')||'<tr><td colspan="5">Nenhum contato encontrado.</td></tr>'}</tbody></table></div>`;
document.querySelector('#contatos-search-input')?.addEventListener('input',renderContatos);
}
window.desvincularContatoDetalhe=async (cid,tipo)=>{if(!await Util.confirm('Deseja desvincular este contato do condomínio?','Desvincular contato'))return;try{await Api.request(`/api/condominios/${id}/contatos/${cid}?tipo=${encodeURIComponent(tipo)}`,{method:'DELETE'});Util.toast('Contato desvinculado');loadContatos();load()}catch(e){Util.toast(e.message,true)}};
window.abrirNovoContato=()=>{abrirDetailModal('Novo contato','<form id="contato-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>Telefone</label><input name="telefone"></div><div class="field"><label>E-mail</label><input name="email"></div><div class="field"><label>Tipo</label><select name="tipo"><option value="sindico">Sindico</option><option value="subsindico">Subsindico</option><option value="conselheiro">Conselheiro</option></select></div><div class="row"><button class="btn">Cadastrar</button><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button></div></form>');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)}}};
async function loadEspacos(){const d=await Api.get(`/api/condominios/${id}/espacos-comuns`);espacos=d.espacos||[];renderEspacos()}
function renderEspacos(){const q=termoDetalhe('espacos');const lista=espacos.filter(x=>contemDetalhe(q,x.nome,x.valor_utilizacao_centavos,x.indice_correcao,x.descricao));document.querySelector('#espacos').innerHTML=headerCadastro('Espacos comuns','espacos','abrirNovoEspaco()')+`<div class="table-wrap"><table><thead><tr><th>Nome</th><th>Valor</th><th>Indice</th><th>Descricao</th></tr></thead><tbody>${lista.map(x=>`<tr><td>${esc(x.nome)}</td><td>${Util.money(x.valor_utilizacao_centavos)}</td><td>${esc(x.indice_correcao||'')}</td><td>${esc(x.descricao||'')}</td></tr>`).join('')||'<tr><td colspan="4">Nenhum espaco encontrado.</td></tr>'}</tbody></table></div>`;document.querySelector('#espacos-search-input')?.addEventListener('input',renderEspacos)}
window.abrirNovoEspaco=()=>{abrirDetailModal('Novo espaco comum','<form id="espaco-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>Valor centavos</label><input name="valor_utilizacao_centavos" type="number"></div><div class="field"><label>Indice</label><input name="indice_correcao"></div><div class="field"><label>Descricao</label><input name="descricao"></div><div class="row"><button class="btn">Cadastrar</button><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button></div></form>');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)}}};
function renderEspacos(){
const q=termoDetalhe('espacos');
const lista=espacos.filter(x=>contemDetalhe(q,x.nome,x.valor_utilizacao_centavos,x.indice_correcao,x.descricao));
document.querySelector('#espacos').innerHTML=headerCadastro('Espaços comuns','espacos','abrirNovoEspaco()')+`<div class="table-wrap"><table><thead><tr><th>Nome</th><th>Valor</th><th>Índice</th><th>Descrição</th><th style="text-align:right">Ações</th></tr></thead><tbody>${lista.map(x=>`<tr><td>${esc(x.nome)}</td><td>${Util.money(x.valor_utilizacao_centavos)}</td><td>${esc(x.indice_correcao||'')}</td><td>${esc(x.descricao||'')}</td><td style="text-align:right"><div class="table-actions"><button class="table-icon-btn" type="button" onclick="abrirEditarEspaco(${x.id})" title="Editar espaço" aria-label="Editar espaço">${Icons.edit}</button><button class="table-icon-btn danger" type="button" onclick="excluirEspacoDetalhe(${x.id})" title="Excluir espaço" aria-label="Excluir espaço">${Icons.trash}</button></div></td></tr>`).join('')||'<tr><td colspan="5">Nenhum espaço encontrado.</td></tr>'}</tbody></table></div>`;
document.querySelector('#espacos-search-input')?.addEventListener('input',renderEspacos);
}
window.abrirEditarEspaco=eid=>{const x=espacos.find(e=>Number(e.id)===Number(eid));if(!x)return;abrirDetailModal('Editar espaço comum','<form id="espaco-edit-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required value="'+esc(x.nome||'')+'"></div><div class="field"><label>Valor centavos</label><input name="valor_utilizacao_centavos" type="number" value="'+(x.valor_utilizacao_centavos??'')+'"></div><div class="field"><label>Índice</label><input name="indice_correcao" value="'+esc(x.indice_correcao||'')+'"></div><div class="field"><label>Descrição</label><input name="descricao" value="'+esc(x.descricao||'')+'"></div><div class="row"><button class="btn">Salvar</button><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button></div></form>');document.querySelector('#espaco-edit-form').onsubmit=async e=>{e.preventDefault();try{await Api.put(`/api/condominios/${id}/espacos-comuns/${eid}`,Util.formData(e.target));Util.toast('Espaço atualizado');fecharDetailModal();loadEspacos()}catch(err){Util.toast(err.message,true)}}};
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','<form id="espaco-form" class="grid cols"><div class="field"><label>Nome</label><input name="nome" required></div><div class="field"><label>Valor centavos</label><input name="valor_utilizacao_centavos" type="number"></div><div class="field"><label>Índice</label><input name="indice_correcao"></div><div class="field"><label>Descrição</label><input name="descricao"></div><div class="row"><button class="btn">Cadastrar</button><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button></div></form>');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 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='<button class="icon-btn calendar-open" type="button" onclick="abrirCalendarioAssembleias()" title="Calendário de assembleias" aria-label="Calendário de assembleias"></button>';document.querySelector('#assembleias').innerHTML=headerCadastro('Assembleias','assembleias','abrirNovaAssembleia()',calendar)+`<div class="table-wrap"><table><thead><tr><th>Tipo</th><th>Data</th><th>Local</th><th>Status</th><th>Ações</th></tr></thead><tbody>${lista.map(a=>`<tr><td>${esc(a.tipo)}</td><td>${Util.date(a.data_hora)}</td><td>${esc(a.local||'')}</td><td>${Util.badge(a.status)}</td><td><div class="table-actions"><button class="table-icon-btn send" type="button" onclick="reenviarAvisoAssembleia(${id},${a.id})" title="Enviar notificação" aria-label="Enviar notificação"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg></button><button class="table-icon-btn danger" type="button" onclick="excluirAssembleiaDetalhe(${a.id})" title="Excluir assembleia" aria-label="Excluir assembleia"><svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg></button></div></td></tr>`).join('')||'<tr><td colspan="5">Nenhuma assembleia encontrada.</td></tr>'}</tbody></table></div>`;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);}};
function renderAssembleias(){
const q=termoDetalhe('assembleias');
const lista=assembleias.filter(a=>contemDetalhe(q,a.tipo,a.data_hora,a.local,a.status));
const calendar='<button class="icon-btn calendar-open" type="button" onclick="abrirCalendarioAssembleias()" title="Calendário de assembleias" aria-label="Calendário de assembleias"></button>';
document.querySelector('#assembleias').innerHTML=headerCadastro('Assembleias','assembleias','abrirNovaAssembleia()',calendar)+`<div class="table-wrap"><table><thead><tr><th>Tipo</th><th>Data</th><th>Local</th><th>Status</th><th style="text-align:right">Ações</th></tr></thead><tbody>${lista.map(a=>`<tr><td>${esc(a.tipo)}</td><td>${Util.date(a.data_hora)}</td><td>${esc(a.local||'')}</td><td>${Util.badge(a.status)}</td><td style="text-align:right"><div class="table-actions"><button class="table-icon-btn send" type="button" onclick="reenviarAvisoAssembleia(${id},${a.id})" title="Enviar notificação" aria-label="Enviar notificação">${Icons.send}</button><button class="table-icon-btn danger" type="button" onclick="excluirAssembleiaDetalhe(${a.id})" title="Excluir assembleia" aria-label="Excluir assembleia">${Icons.trash}</button></div></td></tr>`).join('')||'<tr><td colspan="5">Nenhuma assembleia encontrada.</td></tr>'}</tbody></table></div>`;
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','<form id="ass-form" class="grid cols"><div class="field"><label>Tipo</label><input name="tipo" required></div><div class="field"><label>Data e hora</label><input name="data_hora" type="datetime-local" required></div><div class="field"><label>Local</label><input name="local"></div><div class="field"><label>Pauta</label><input name="pauta"></div><div class="row"><button class="btn">Cadastrar</button><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button></div></form>');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')}
async function loadDocs(){const d=await Api.get(`/api/condominios/${id}/documentos`);docs=d.documentos||[];renderDocs()}
function renderDocs(){const q=termoDetalhe('documentos');const lista=docs.filter(d=>contemDetalhe(q,d.id,d.nome_arquivo,d.tipo));document.querySelector('#documentos').innerHTML=headerCadastro('Documentos','documentos','abrirNovoDocumento()')+`<div class="table-wrap"><table><thead><tr><th>ID</th><th>Arquivo</th><th>Tipo</th><th>Download</th></tr></thead><tbody>${lista.map(d=>`<tr><td>${d.id}</td><td>${esc(d.nome_arquivo)}</td><td>${esc(d.tipo||'')}</td><td><a href="/api/documentos/${d.id}/download">Baixar</a></td></tr>`).join('')||'<tr><td colspan="4">Nenhum documento encontrado.</td></tr>'}</tbody></table></div>`;document.querySelector('#documentos-search-input')?.addEventListener('input',renderDocs)}
function renderDocs(){
const q=termoDetalhe('documentos');
const lista=docs.filter(d=>contemDetalhe(q,d.id,d.nome_arquivo,d.tipo));
document.querySelector('#documentos').innerHTML=headerCadastro('Documentos','documentos','abrirNovoDocumento()')+`<div class="table-wrap"><table><thead><tr><th>ID</th><th>Arquivo</th><th>Tipo</th><th style="text-align:right">Ações</th></tr></thead><tbody>${lista.map(d=>`<tr><td>${d.id}</td><td>${esc(d.nome_arquivo)}</td><td>${esc(d.tipo||'')}</td><td style="text-align:right"><div class="table-actions"><a class="table-icon-btn" href="/api/documentos/${d.id}/download" title="Baixar arquivo" aria-label="Baixar arquivo">${Icons.download}</a><button class="table-icon-btn danger" type="button" onclick="excluirDocumentoDetalhe(${d.id})" title="Excluir documento" aria-label="Excluir documento">${Icons.trash}</button></div></td></tr>`).join('')||'<tr><td colspan="4">Nenhum documento encontrado.</td></tr>'}</tbody></table></div>`;
document.querySelector('#documentos-search-input')?.addEventListener('input',renderDocs);
}
window.excluirDocumentoDetalhe=async did=>{if(!await Util.confirm('Tem certeza que deseja excluir este documento?','Excluir documento'))return;try{await Api.request(`/api/condominios/${id}/documentos/${did}`,{method:'DELETE'});Util.toast('Documento excluído');loadDocs()}catch(err){Util.toast(err.message,true)}};
window.abrirNovoDocumento=()=>{abrirDetailModal('Novo documento','<form id="doc-form" class="row"><div class="field"><label>Tipo</label><input name="tipo"></div><div class="field"><label>Arquivo</label><input name="arquivo" type="file" required></div><div class="row"><button class="btn">Enviar</button><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button></div></form>');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');fecharDetailModal();loadDocs();loadAvisos()}catch(err){Util.toast(err.message,true)}}};
function avisoFormHtml(){const destContatos=contatos.map(c=>`<label><input type="checkbox" name="dest" value="contato:${c.contato_id}"> ${esc(c.contato_nome)} (${esc(c.tipo)})</label>`).join('');const destFuncionarios=funcionariosVinculados.map(f=>`<label><input type="checkbox" name="dest" value="funcionario:${f.funcionario_id||f.id}"> ${esc(f.nome)} (funcionario)</label>`).join('');const destExtras=extras.map(x=>`<label><input type="checkbox" name="dest" value="extra:${x.id}"> ${esc(x.nome)} (extra)</label>`).join('');const dest=[destContatos,destFuncionarios,destExtras].filter(Boolean).join('');const docChecks=docs.map(d=>`<label><input type="checkbox" name="doc" value="${d.id}"> ${esc(d.nome_arquivo)}</label>`).join('');return `<form id="aviso-form" class="grid cols"><div class="field"><label>Titulo</label><input name="titulo" required></div><div class="field"><label>Tipo</label><input name="tipo" value="aviso"></div><div class="field"><label>Prioridade</label><select name="prioridade"><option>normal</option><option>alta</option><option>urgente</option></select></div><div class="field"><label>Mensagem</label><textarea name="mensagem" required></textarea></div><div class="field"><label>Data/hora de envio</label><input name="data_evento" type="datetime-local"></div><div class="field"><label>Recorrencia</label><select name="recorrencia_meses"><option value="0">Sem recorrencia</option><option value="1">1 vez por mes</option><option value="3">A cada 3 meses</option><option value="12">1 vez por ano</option></select></div><div class="field"><label>Destinatarios</label><button class="btn secondary" type="button" onclick="abrirAvisoNotificacao()">Notificação</button> <span id="aviso-notif-summary" class="muted">Nenhuma pessoa selecionada</span><div id="aviso-dests-hidden" class="hidden">${dest}</div></div><div class="field"><label>Documentos</label>${docChecks||'<p>Nenhum documento.</p>'}</div><label><input type="checkbox" name="enviar_whatsapp" value="true"> Enviar por WhatsApp</label><label><input type="checkbox" name="enviar_email" value="true"> Enviar por e-mail</label><div class="row"><button class="btn">Criar aviso</button><button class="btn secondary" type="button" onclick="fecharDetailModal()">Cancelar</button></div></form>`}
async function loadAvisos(){const d=await Api.get(`/api/condominios/${id}/avisos`);avisos=d.avisos||[];renderAvisos()}
function renderAvisos(){const q=termoDetalhe('avisos');const lista=avisos.filter(a=>contemDetalhe(q,a.titulo,a.status_envio,a.data_evento,a.data_publicacao,recLabel(a.recorrencia_meses)));document.querySelector('#avisos').innerHTML=headerCadastro('Avisos','avisos','abrirNovoAviso()')+`<div class="table-wrap"><table><thead><tr><th>Titulo</th><th>Status</th><th>Envio</th><th>Publicado em</th><th>Recorrencia</th><th>Acoes</th></tr></thead><tbody>${lista.map(a=>`<tr><td>${esc(a.titulo)}</td><td>${Util.badge(a.status_envio)}</td><td>${Util.date(a.data_evento)}</td><td>${Util.date(a.data_publicacao)}</td><td>${recLabel(a.recorrencia_meses)}</td><td><button class="btn secondary" onclick="reenviarAviso(${a.id})">Reenviar</button> <button class="btn danger" onclick="excluirAviso(${a.id})">Excluir</button></td></tr>`).join('')||'<tr><td colspan="6">Nenhum aviso encontrado.</td></tr>'}</tbody></table></div>`;document.querySelector('#avisos-search-input')?.addEventListener('input',renderAvisos)}
function renderAvisos(){
const q=termoDetalhe('avisos');
const lista=avisos.filter(a=>contemDetalhe(q,a.titulo,a.status_envio,a.data_evento,a.data_publicacao,recLabel(a.recorrencia_meses)));
document.querySelector('#avisos').innerHTML=headerCadastro('Avisos','avisos','abrirNovoAviso()')+`<div class="table-wrap"><table><thead><tr><th>Título</th><th>Status</th><th>Envio</th><th>Publicado em</th><th>Recorrência</th><th style="text-align:right">Ações</th></tr></thead><tbody>${lista.map(a=>`<tr><td>${esc(a.titulo)}</td><td>${Util.badge(a.status_envio)}</td><td>${Util.date(a.data_evento)}</td><td>${Util.date(a.data_publicacao)}</td><td>${recLabel(a.recorrencia_meses)}</td><td style="text-align:right"><div class="table-actions"><button class="table-icon-btn send" type="button" onclick="reenviarAviso(${a.id})" title="Reenviar aviso" aria-label="Reenviar aviso">${Icons.send}</button><button class="table-icon-btn danger" type="button" onclick="excluirAviso(${a.id})" title="Excluir aviso" aria-label="Excluir aviso">${Icons.trash}</button></div></td></tr>`).join('')||'<tr><td colspan="6">Nenhum aviso encontrado.</td></tr>'}</tbody></table></div>`;
document.querySelector('#avisos-search-input')?.addEventListener('input',renderAvisos);
}
window.abrirNovoAviso=()=>{abrirDetailModal('Novo aviso',avisoFormHtml());updateAvisoNotifSummary();document.querySelector('#aviso-form').onsubmit=async e=>{e.preventDefault();const f=e.target,b=Util.formData(f);b.enviar_whatsapp=!!f.enviar_whatsapp.checked;b.enviar_email=!!f.enviar_email.checked;b.destinatarios=[...f.querySelectorAll('input[name=dest]:checked')].map(x=>{const [tipo,did]=x.value.split(':');return{tipo_destinatario:tipo,id:Number(did)}});b.documentos=[...f.querySelectorAll('input[name=doc]:checked')].map(x=>Number(x.value));try{await Api.post(`/api/condominios/${id}/avisos`,b);Util.toast('Aviso criado');fecharDetailModal();loadAvisos();load()}catch(err){Util.toast(err.message,true)}}};
async function loadRespostas(){const d=await Api.get(`/api/condominios/${id}/respostas`);respostas=d.respostas||[];renderRespostas()}
function renderRespostas(){const q=termoDetalhe('respostas');const lista=respostas.filter(r=>contemDetalhe(q,r.resposta_texto,r.mensagem_original,r.nome_origem,r.telefone_origem,r.contato_nome,r.mensagem_id,r.origem,r.status,r.tipo_origem));document.querySelector('#respostas').innerHTML=headerConsulta('Respostas','respostas')+`<div class="table-wrap"><table><thead><tr><th>Recebido em</th><th>Contato</th><th>Telefone</th><th>Mensagem ID</th><th>Resposta</th><th>Origem</th><th>Status</th></tr></thead><tbody>${lista.map(r=>`<tr><td>${Util.date(r.recebido_em)}</td><td><strong>${esc(r.contato_nome||r.nome_origem||'-')}</strong><br><span class="muted">${esc(r.tipo_origem||'')}</span></td><td>${esc(r.contato_telefone||r.telefone_origem||'')}</td><td>${esc(r.mensagem_id||'')}</td><td>${esc(r.resposta_texto||'')}<br><span class="muted">${esc(r.mensagem_original||'')}</span></td><td>${esc(r.origem||'')}</td><td>${Util.badge(r.status)}</td></tr>`).join('')||'<tr><td colspan="7">Nenhuma resposta registrada.</td></tr>'}</tbody></table></div>`;document.querySelector('#respostas-search-input')?.addEventListener('input',renderRespostas)}
window.reenviarAviso=async avisoId=>{try{await Api.post(`/api/condominios/${id}/avisos/${avisoId}/reenviar`,{});Util.toast('Aviso reenviado');loadAvisos()}catch(err){Util.toast(err.message,true)}};
window.excluirAviso=async avisoId=>{try{await Api.request(`/api/condominios/${id}/avisos/${avisoId}`,{method:'DELETE'});Util.toast('Aviso excluido');loadAvisos();load()}catch(err){Util.toast(err.message,true)}};
window.reenviarAviso=async avisoId=>{if(!await Util.confirm('Deseja reenviar este aviso agora?','Reenviar aviso'))return;try{await Api.post(`/api/condominios/${id}/avisos/${avisoId}/reenviar`,{});Util.toast('Aviso reenviado');loadAvisos()}catch(err){Util.toast(err.message,true)}};
window.excluirAviso=async avisoId=>{if(!await Util.confirm('Tem certeza que deseja excluir este aviso? Esta ação não pode ser desfeita.','Excluir aviso'))return;try{await Api.request(`/api/condominios/${id}/avisos/${avisoId}`,{method:'DELETE'});Util.toast('Aviso excluído');loadAvisos();load()}catch(err){Util.toast(err.message,true)}};
load();