Automatiza destinatarios de assembleias

This commit is contained in:
Tiago
2026-09-30 15:12:39 -03:00
parent 1791e92597
commit 44c04d0b23
3 changed files with 20 additions and 11 deletions
@@ -79,7 +79,7 @@ public final class ClienteHandlers implements HttpHandler {
if(p.length==3&&"condominios".equals(p[0])&&"espacos-comuns".equals(p[2])){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} if("POST".equals(method)) criarEspaco(ex,c,cond,funcionarioId); else if("GET".equals(method)) listarParam(ex,c,"SELECT * FROM espacos_comuns WHERE condominio_id=? ORDER BY nome",cond,"espacos"); else HttpResponses.methodNotAllowed(ex); return; }
if(p.length==4&&"condominios".equals(p[0])&&"espacos-comuns".equals(p[2])&&"PUT".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} atualizarEspaco(ex,c,cond,id(p[3]),funcionarioId); return; }
if(p.length==3&&"condominios".equals(p[0])&&"assembleias".equals(p[2])){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} if("POST".equals(method)) criarAssembleia(ex,c,empresaId,cond,funcionarioId); else if("GET".equals(method)) listarParam(ex,c,"SELECT * FROM assembleias WHERE condominio_id=? ORDER BY data_hora DESC",cond,"assembleias"); else HttpResponses.methodNotAllowed(ex); return; }
if(p.length==4&&"condominios".equals(p[0])&&"assembleias".equals(p[2])&&"PUT".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} atualizarAssembleia(ex,c,cond,id(p[3]),funcionarioId); return; }
if(p.length==4&&"condominios".equals(p[0])&&"assembleias".equals(p[2])&&"PUT".equals(method)){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} atualizarAssembleia(ex,c,empresaId,cond,id(p[3]),funcionarioId); return; }
if(p.length==3&&"condominios".equals(p[0])&&"documentos".equals(p[2])){ long cond=id(p[1]); if(!podeAcessarCondominio(c,cargo,funcionarioId,cond)){HttpResponses.json(ex,403,Map.of("erro","condominio_nao_vinculado"));return;} if("POST".equals(method)) uploadDocumento(ex,c,empresaId,cond,funcionarioId); else if("GET".equals(method)) listarParam(ex,c,"SELECT * FROM documentos WHERE condominio_id=? ORDER BY criado_em DESC",cond,"documentos"); else HttpResponses.methodNotAllowed(ex); return; }
if(p.length==3&&"documentos".equals(p[0])&&"download".equals(p[2])&&"GET".equals(method)){ downloadDocumento(ex,c,id(p[1])); return; }
HttpResponses.json(ex,404,Map.of("erro","endpoint_nao_encontrado"));
@@ -129,7 +129,7 @@ public final class ClienteHandlers implements HttpHandler {
HttpResponses.json(ex,201,Map.of("assembleia",one(c,"SELECT * FROM assembleias WHERE id=?",Arrays.asList(id)),"envio",envio));
}
private Map<String,Object> criarAvisosAssembleia(Connection c,long emp,long cond,long assembleia,long fid,Map<String,String>b,List<Destinatario>dests)throws Exception{
List<Destinatario> destinatarios=dests.isEmpty()?destinatariosPadraoAssembleia(c,cond):dests;
List<Destinatario> destinatarios=dests.isEmpty()?destinatariosPadraoAssembleia(c,emp,cond):dests;
int total=destinatarios.size();
if(total==0)return Map.of("enviados",0,"falhas",0,"mensagem","Nenhum destinatario vinculado ao condominio.");
if(!funcionalidadeAtiva(emp,"whatsapp_envio"))return Map.of("enviados",0,"falhas",1,"mensagem","WhatsApp nao liberado para esta empresa.");
@@ -141,12 +141,19 @@ public final class ClienteHandlers implements HttpHandler {
avisos.add(criarAvisoAutomaticoAssembleia(c,cond,assembleia,fid,b,destinatarios,"Lembrete de assembleia","Lembrete: a assembleia acontece hoje.",data));
return Map.of("aviso_ids",avisos,"destinatarios",total,"status","enfileirado");
}
private List<Destinatario> destinatariosPadraoAssembleia(Connection c,long cond)throws SQLException{
private List<Destinatario> destinatariosPadraoAssembleia(Connection c,long emp,long cond)throws SQLException{
List<Destinatario> out=new ArrayList<>();
for(Map<String,Object> row:rows(c,"SELECT contato_id FROM contato_condominio WHERE condominio_id=? AND status=?",Arrays.asList(cond,"ativo")))out.add(new Destinatario("contato",((Number)row.get("contato_id")).longValue()));
for(Map<String,Object> row:rows(c,"SELECT funcionario_id FROM funcionario_condominio WHERE condominio_id=?",Arrays.asList(cond)))out.add(new Destinatario("funcionario",((Number)row.get("funcionario_id")).longValue()));
Set<String> vistos=new HashSet<>();
for(Map<String,Object> row:rows(c,"SELECT contato_id FROM contato_condominio WHERE condominio_id=? AND status=? AND tipo IN ('sindico','subsindico','conselheiro')",Arrays.asList(cond,"ativo")))addDestinatario(out,vistos,"contato",((Number)row.get("contato_id")).longValue());
try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id FROM funcionarios WHERE empresa_id=? AND cargo='admin_empresa' AND status='ativo'")){
ps.setLong(1,emp);
try(ResultSet rs=ps.executeQuery()){while(rs.next())addDestinatario(out,vistos,"funcionario",rs.getLong(1));}
}
for(Map<String,Object> 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<String,Object> row:rows(c,"SELECT id FROM destinatarios_extra WHERE status='ativo'",Arrays.asList()))addDestinatario(out,vistos,"extra",((Number)row.get("id")).longValue());
return out;
}
private void addDestinatario(List<Destinatario> out,Set<String> vistos,String tipo,long id){if(vistos.add(tipo+":"+id))out.add(new Destinatario(tipo,id));}
private long criarAvisoAutomaticoAssembleia(Connection c,long cond,long assembleia,long fid,Map<String,String>b,List<Destinatario>dests,String titulo,String intro,Timestamp envio)throws SQLException{
Timestamp agora=Timestamp.valueOf(LocalDateTime.now(java.time.ZoneId.of("America/Sao_Paulo")));
Timestamp agendado=envio!=null&&envio.before(agora)?agora:envio;
@@ -163,8 +170,8 @@ public final class ClienteHandlers implements HttpHandler {
if(b.get("pauta")!=null&&!b.get("pauta").isBlank())mensagem.append("\nPauta: ").append(b.get("pauta"));
return mensagem.toString();
}
private void atualizarAssembleia(HttpExchange ex,Connection c,long cond,long aid,long fid)throws Exception{Map<String,String>b=body(ex); Map<String,Object> old=one(c,"SELECT * FROM assembleias WHERE id=? AND condominio_id=?",Arrays.asList(aid,cond)); if(old==null){HttpResponses.json(ex,404,Map.of("erro","assembleia_nao_encontrada"));return;} c.setAutoCommit(false);try{try(PreparedStatement ps=c.prepareStatement("UPDATE assembleias SET tipo=COALESCE(?,tipo), data_hora=COALESCE(?,data_hora), local=COALESCE(?,local), pauta=COALESCE(?,pauta), status=COALESCE(?,status) WHERE id=? AND condominio_id=?")){ps.setString(1,b.get("tipo")); if(b.get("data_hora")==null)ps.setNull(2,Types.TIMESTAMP);else ps.setTimestamp(2,ts(b.get("data_hora"))); ps.setString(3,b.get("local"));ps.setString(4,b.get("pauta"));ps.setString(5,b.get("status"));ps.setLong(6,aid);ps.setLong(7,cond);ps.executeUpdate();} if("cancelada".equalsIgnoreCase(b.get("status"))&&!"cancelada".equalsIgnoreCase(str(old.get("status"))))criarAvisoCancelamentoAssembleia(c,cond,aid,fid,old); audit(c,fid,"update","assembleias",aid,old,b); c.commit();HttpResponses.json(ex,200,Map.of("assembleia",one(c,"SELECT * FROM assembleias WHERE id=?",Arrays.asList(aid))));}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}}
private void criarAvisoCancelamentoAssembleia(Connection c,long cond,long aid,long fid,Map<String,Object>old)throws SQLException{
private void atualizarAssembleia(HttpExchange ex,Connection c,long emp,long cond,long aid,long fid)throws Exception{Map<String,String>b=body(ex); Map<String,Object> old=one(c,"SELECT * FROM assembleias WHERE id=? AND condominio_id=?",Arrays.asList(aid,cond)); if(old==null){HttpResponses.json(ex,404,Map.of("erro","assembleia_nao_encontrada"));return;} c.setAutoCommit(false);try{try(PreparedStatement ps=c.prepareStatement("UPDATE assembleias SET tipo=COALESCE(?,tipo), data_hora=COALESCE(?,data_hora), local=COALESCE(?,local), pauta=COALESCE(?,pauta), status=COALESCE(?,status) WHERE id=? AND condominio_id=?")){ps.setString(1,b.get("tipo")); if(b.get("data_hora")==null)ps.setNull(2,Types.TIMESTAMP);else ps.setTimestamp(2,ts(b.get("data_hora"))); ps.setString(3,b.get("local"));ps.setString(4,b.get("pauta"));ps.setString(5,b.get("status"));ps.setLong(6,aid);ps.setLong(7,cond);ps.executeUpdate();} if("cancelada".equalsIgnoreCase(b.get("status"))&&!"cancelada".equalsIgnoreCase(str(old.get("status"))))criarAvisoCancelamentoAssembleia(c,emp,cond,aid,fid,old); audit(c,fid,"update","assembleias",aid,old,b); c.commit();HttpResponses.json(ex,200,Map.of("assembleia",one(c,"SELECT * FROM assembleias WHERE id=?",Arrays.asList(aid))));}catch(Exception err){c.rollback();throw err;}finally{c.setAutoCommit(true);}}
private void criarAvisoCancelamentoAssembleia(Connection c,long emp,long cond,long aid,long fid,Map<String,Object>old)throws SQLException{
List<Map<String,Object>> avisos=rows(c,"SELECT id FROM avisos WHERE assembleia_id=?",Arrays.asList(aid));
Set<String> vistos=new HashSet<>();
List<Destinatario>dests=new ArrayList<>();
@@ -174,7 +181,11 @@ public final class ClienteHandlers implements HttpHandler {
if(vistos.add(key))dests.add(new Destinatario(tipo,id));
}
}
if(dests.isEmpty())dests=destinatariosPadraoAssembleia(c,cond);
if(dests.isEmpty()){
Map<String,Object> a=one(c,"SELECT condominio_id FROM assembleias WHERE id=?",Arrays.asList(aid));
long cid=a!=null&&a.get("condominio_id")!=null?((Number)a.get("condominio_id")).longValue():cond;
dests=destinatariosPadraoAssembleia(c,emp,cid);
}
try(PreparedStatement ps=c.prepareStatement("UPDATE avisos SET status_envio='cancelado' WHERE assembleia_id=? AND status_envio IN ('nao_enviado','agendado','falhou')")){ps.setLong(1,aid);ps.executeUpdate();}
try(PreparedStatement ps=c.prepareStatement("UPDATE fila_envio SET status='ignorado', ultimo_erro='Assembleia cancelada', locked_at=NULL, atualizado_em=now() WHERE tipo='aviso' AND referencia_id IN (SELECT id FROM avisos WHERE assembleia_id=?) AND status IN ('pendente','processando','falhou')")){ps.setLong(1,aid);ps.executeUpdate();}
Map<String,String>b=new HashMap<>();b.put("data_hora",String.valueOf(old.get("data_hora")));b.put("local",str(old.get("local")));b.put("pauta",str(old.get("pauta")));
@@ -20,7 +20,7 @@ async function safeLoad(section,fn){try{await fn()}catch(e){document.querySelect
async function load(){try{const d=await Api.get(`/api/condominios/${id}`);document.querySelector('#header').innerHTML=`<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)}catch(e){Util.toast(e.message,true)}}
async function loadContatos(){const d=await Api.get(`/api/condominios/${id}/contatos`);contatos=d.contatos||[];document.querySelector('#contatos').innerHTML=`<h2>Contatos</h2><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><button class="btn">Cadastrar</button></form><div class="table-wrap"><table><thead><tr><th>Nome</th><th>Tipo</th><th>Telefone</th><th>Email</th></tr></thead><tbody>${contatos.map(c=>`<tr><td>${c.contato_nome}</td><td>${c.tipo}</td><td>${c.contato_telefone||''}</td><td>${c.contato_email||''}</td></tr>`).join('')}</tbody></table></div>`;document.querySelector('#contato-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/contatos/vincular`,Util.formData(e.target));Util.toast('Contato salvo');loadContatos()}catch(err){Util.toast(err.message,true)}}}
async function loadEspacos(){const d=await Api.get(`/api/condominios/${id}/espacos-comuns`);document.querySelector('#espacos').innerHTML=`<h2>Espacos comuns</h2><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><button class="btn">Cadastrar</button></form><div class="table-wrap"><table><thead><tr><th>Nome</th><th>Valor</th><th>Indice</th><th>Descricao</th></tr></thead><tbody>${(d.espacos||[]).map(x=>`<tr><td>${x.nome}</td><td>${Util.money(x.valor_utilizacao_centavos)}</td><td>${x.indice_correcao||''}</td><td>${x.descricao||''}</td></tr>`).join('')}</tbody></table></div>`;document.querySelector('#espaco-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/espacos-comuns`,Util.formData(e.target));Util.toast('Espaco salvo');loadEspacos()}catch(err){Util.toast(err.message,true)}}}
async function loadAssembleias(){const d=await Api.get(`/api/condominios/${id}/assembleias`);const destContatos=contatos.map(c=>`<label><input type="checkbox" name="dest" value="contato:${c.contato_id}" checked> ${c.contato_nome} (${c.tipo})</label>`).join('');const destFuncionarios=funcionariosVinculados.map(f=>`<label><input type="checkbox" name="dest" value="funcionario:${f.funcionario_id||f.id}" checked> ${f.nome} (funcionario)</label>`).join('');const dest=[destContatos,destFuncionarios].filter(Boolean).join('')||'<p>Nenhum membro cadastrado.</p>';document.querySelector('#assembleias').innerHTML=`<div class="section-head"><h2>Assembleias</h2><button class="icon-btn calendar-open" type="button" onclick="abrirCalendarioAssembleias()" title="Calendário de assembleias" aria-label="Calendário de assembleias"></button></div><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="field"><label>Membros notificados</label><div class="check-list">${dest}</div></div><button class="btn">Cadastrar</button></form><div class="table-wrap"><table><thead><tr><th>Tipo</th><th>Data</th><th>Local</th><th>Status</th></tr></thead><tbody>${(d.assembleias||[]).map(a=>`<tr><td>${a.tipo}</td><td>${Util.date(a.data_hora)}</td><td>${a.local||''}</td><td>${Util.badge(a.status)}</td></tr>`).join('')}</tbody></table></div>`;document.querySelector('#ass-form').onsubmit=async e=>{e.preventDefault();const f=e.target,b=Util.formData(f);b.destinatarios=[...f.querySelectorAll('input[name=dest]:checked')].map(x=>{const [tipo,did]=x.value.split(':');return{tipo_destinatario:tipo,id:Number(did)}});try{await Api.post(`/api/condominios/${id}/assembleias`,b);Util.toast('Assembleia salva');loadAssembleias();load()}catch(err){Util.toast(err.message,true)}}}
async function loadAssembleias(){const d=await Api.get(`/api/condominios/${id}/assembleias`);document.querySelector('#assembleias').innerHTML=`<div class="section-head"><h2>Assembleias</h2><button class="icon-btn calendar-open" type="button" onclick="abrirCalendarioAssembleias()" title="Calendário de assembleias" aria-label="Calendário de assembleias"></button></div><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><button class="btn">Cadastrar</button></form><div class="table-wrap"><table><thead><tr><th>Tipo</th><th>Data</th><th>Local</th><th>Status</th></tr></thead><tbody>${(d.assembleias||[]).map(a=>`<tr><td>${a.tipo}</td><td>${Util.date(a.data_hora)}</td><td>${a.local||''}</td><td>${Util.badge(a.status)}</td></tr>`).join('')}</tbody></table></div>`;document.querySelector('#ass-form').onsubmit=async e=>{e.preventDefault();try{await Api.post(`/api/condominios/${id}/assembleias`,Util.formData(e.target));Util.toast('Assembleia salva');loadAssembleias();load()}catch(err){Util.toast(err.message,true)}}}
async function loadFuncionariosVinculados(){const d=await Api.get(`/api/condominios/${id}/funcionarios-vinculados`);funcionariosVinculados=d.funcionarios||[]}
async function loadExtras(){const d=await Api.get(`/api/extras`);extras=(d.extras||[]).filter(x=>x.status==='ativo')}
async function loadDocs(){const d=await Api.get(`/api/condominios/${id}/documentos`);docs=d.documentos||[];document.querySelector('#documentos').innerHTML=`<h2>Documentos</h2><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><button class="btn">Enviar</button></form><div class="table-wrap"><table><thead><tr><th>ID</th><th>Arquivo</th><th>Tipo</th><th>Download</th></tr></thead><tbody>${docs.map(d=>`<tr><td>${d.id}</td><td>${d.nome_arquivo}</td><td>${d.tipo||''}</td><td><a href="/api/documentos/${d.id}/download">Baixar</a></td></tr>`).join('')}</tbody></table></div>`;document.querySelector('#doc-form').onsubmit=async e=>{e.preventDefault();const fd=new FormData(e.target);try{await Api.post(`/api/condominios/${id}/documentos`,fd);Util.toast('Documento enviado');loadDocs()}catch(err){Util.toast(err.message,true)}}}
-2
View File
@@ -3,6 +3,4 @@
.success-message{display:inline-block;margin:0 0 10px;padding:10px 12px;border:1px solid #bbf7d0;border-radius:8px;background:#dcfce7;color:var(--ok);font-weight:700}
.section-head{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:14px}.section-head h2{margin:0}.icon-btn{position:relative;width:42px;height:42px;border:1px solid var(--line);border-radius:6px;background:#fff;cursor:pointer;display:inline-grid;place-items:center}.icon-btn:hover{border-color:var(--brand);background:#eef8fb}.calendar-open::before{content:"";width:20px;height:18px;border:2px solid var(--brand);border-radius:3px;box-shadow:inset 0 5px 0 #e8f3f7;position:relative}.calendar-open::after{content:"";position:absolute;width:12px;height:2px;background:var(--brand);transform:translateY(-7px)}.calendar-modal{width:min(1120px,100%)}.calendar-layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px}.calendar-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}.calendar-toolbar strong{text-transform:capitalize}.calendar-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));border:1px solid var(--line);border-radius:8px;overflow:hidden;background:#fff}.calendar-weekday{padding:10px;background:#f8fafc;color:var(--muted);font-size:12px;font-weight:700;text-align:center;border-bottom:1px solid var(--line)}.calendar-day{min-height:86px;border:0;border-right:1px solid var(--line);border-bottom:1px solid var(--line);background:#fff;padding:8px;text-align:left;color:var(--text);cursor:pointer;position:relative}.calendar-day:nth-child(7n+7){border-right:0}.calendar-day.other{background:#f8fafc;color:#98a2b3}.calendar-day.busy{background:#fff7ed}.calendar-day.busy span{font-weight:700;color:var(--warn)}.calendar-day.today{box-shadow:inset 0 0 0 2px var(--brand)}.calendar-day.selected{background:#e8f3f7}.calendar-day b{position:absolute;right:8px;bottom:8px;background:var(--brand);color:#fff;border-radius:999px;padding:2px 7px;font-size:12px}.calendar-side{border:1px solid var(--line);border-radius:8px;padding:14px;background:#fff;min-height:420px}.calendar-side h3{margin:0 0 12px}.calendar-event{border:1px solid var(--line);border-radius:8px;padding:10px;margin-bottom:10px;display:grid;gap:6px}.calendar-event span{font-size:13px}.calendar-event p{margin:0;color:var(--muted);font-size:13px}@media(max-width:900px){.calendar-layout{grid-template-columns:1fr}.calendar-day{min-height:64px}.calendar-side{min-height:auto}}
.check-list{display:grid;gap:8px;max-height:150px;overflow:auto;border:1px solid var(--line);border-radius:6px;padding:10px;background:#fff}.check-list label{display:flex;align-items:center;gap:8px;color:var(--text)}.check-list input{width:auto}
@media(max-width:520px){.content{padding:12px}h1{font-size:22px}h2{font-size:17px}.panel{padding:12px;border-radius:6px}.login-page{padding:16px}.login-box{padding:20px}.modal{padding:8px;align-items:start}.modal-panel{max-height:calc(100vh - 16px);padding:14px}.modal-head{align-items:flex-start}.modal-head .btn{width:auto}.calendar-grid{font-size:12px}.calendar-weekday{padding:8px 4px}.calendar-day{min-height:54px;padding:5px}.calendar-day b{right:4px;bottom:4px;padding:1px 5px}.calendar-toolbar .btn{width:auto}.section-head{align-items:flex-start}.icon-btn{width:40px;min-width:40px}.cond-item{display:grid;grid-template-columns:1fr;align-items:stretch}.cond-item .btn{width:100%}}