so arrumar o botão e o bgl de diciplina

This commit is contained in:
2026-05-12 20:26:41 -03:00
parent aaa2b25ad7
commit ea3ccd57dd
3 changed files with 486 additions and 70 deletions
@@ -4,6 +4,7 @@ import com.agendaestudantil.filtro.FiltroJwt;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -12,6 +13,7 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
@@ -44,6 +46,8 @@ public class ConfiguracaoSeguranca {
"/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
.anyRequest().authenticated())
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(filtroJwt, UsernamePasswordAuthenticationFilter.class);
+99
View File
@@ -157,6 +157,19 @@ body {
font-family: inherit;
}
#btnNovoEvento:hover { background: #a03224; }
#btnGerenciarDisciplinas {
background: transparent;
color: #c0392b;
border: 2px solid #c0392b;
border-radius: 8px;
padding: 6px 16px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, color 0.2s;
font-family: inherit;
}
#btnGerenciarDisciplinas:hover { background: #c0392b; color: #fff; }
/* Calendar area */
.calendar-area { background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); overflow: hidden; }
@@ -444,3 +457,89 @@ body {
[data-theme="dark"] #toast { background: #2a2a2a; }
[data-theme="dark"] .mais-eventos { color: var(--text-secondary); }
/* ===== DISCIPLINE BUTTON DARK MODE ===== */
[data-theme="dark"] #btnGerenciarDisciplinas {
color: #e57373;
border-color: #e57373;
}
[data-theme="dark"] #btnGerenciarDisciplinas:hover {
background: #e57373;
color: #fff;
}
/* ===== MODAL DISCIPLINA PANELS ===== */
.modal-disc-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.modal-disc-header h2 {
font-size: 18px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
[data-theme="dark"] .modal-disc-header h2 { color: var(--text-primary); }
.modal-fechar-btn {
background: none;
border: none;
font-size: 22px;
line-height: 1;
cursor: pointer;
color: #9ca3af;
padding: 2px 7px;
border-radius: 6px;
transition: background 0.15s, color 0.15s;
}
.modal-fechar-btn:hover { background: #f3f4f6; color: #374151; }
[data-theme="dark"] .modal-fechar-btn:hover { background: var(--hover-bg); color: var(--text-primary); }
.modal-corpo-lista {
max-height: 310px;
overflow-y: auto;
margin-bottom: 4px;
}
.modal-corpo-form { margin-bottom: 4px; }
/* Discipline list item */
.disc-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 8px;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
border-bottom: 1px solid #e5e7eb;
}
.disc-item:last-child { border-bottom: none; }
.disc-item:hover { background: #f9fafb; }
[data-theme="dark"] .disc-item { border-bottom-color: var(--border-color); }
[data-theme="dark"] .disc-item:hover { background: var(--hover-bg); }
.disc-cor {
width: 13px;
height: 13px;
border-radius: 50%;
flex-shrink: 0;
}
.disc-nome {
font-weight: 600;
font-size: 14px;
flex: 1;
color: #111827;
}
[data-theme="dark"] .disc-nome { color: var(--text-primary); }
.disc-info {
font-size: 12px;
color: #6b7280;
}
[data-theme="dark"] .disc-info { color: var(--text-secondary); }
.disc-seta {
color: #9ca3af;
font-size: 20px;
line-height: 1;
}
+383 -70
View File
@@ -9,6 +9,115 @@
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="theme.js"></script>
<style>
/* ===== NOTIFICACOES ===== */
.notif-wrapper { position: relative; }
.notif-btn {
background: none;
border: 1px solid var(--borda, #333);
border-radius: 8px;
padding: 7px 9px;
cursor: pointer;
color: var(--texto, #fff);
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: background 0.2s;
}
.notif-btn:hover { background: var(--hover, rgba(255,255,255,0.08)); }
.notif-badge {
position: absolute;
top: -5px; right: -5px;
background: #ef4444;
color: #fff;
font-size: 10px;
font-weight: 700;
min-width: 17px;
height: 17px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 3px;
pointer-events: none;
}
.notif-panel {
display: none;
position: absolute;
top: calc(100% + 10px);
right: 0;
width: 320px;
background: var(--fundo-card, #1e1e2e);
border: 1px solid var(--borda, #333);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.35);
z-index: 9999;
overflow: hidden;
}
.notif-panel.aberto { display: block; }
.notif-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--borda, #333);
font-weight: 600;
font-size: 14px;
color: var(--texto, #fff);
}
.notif-marcar-todas {
background: none; border: none;
color: var(--primario, #7c3aed);
font-size: 12px; cursor: pointer; padding: 0;
}
.notif-marcar-todas:hover { text-decoration: underline; }
.notif-lista { max-height: 340px; overflow-y: auto; }
.notif-vazia {
text-align: center;
color: var(--texto2, #888);
font-size: 13px;
padding: 28px 16px;
}
.notif-item {
display: flex; gap: 10px;
padding: 12px 16px;
border-bottom: 1px solid var(--borda, #2a2a3a);
cursor: pointer;
transition: background 0.15s;
align-items: flex-start;
}
.notif-item:hover { background: var(--hover, rgba(255,255,255,0.04)); }
.notif-item.nao-lida { background: rgba(124,58,237,0.08); }
.notif-item.nao-lida:hover { background: rgba(124,58,237,0.14); }
.notif-icone {
width: 32px; height: 32px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 15px; flex-shrink: 0; margin-top: 1px;
}
.notif-icone.prazo { background: rgba(245,158,11,0.18); }
.notif-icone.atrasada { background: rgba(239,68,68,0.18); }
.notif-icone.evento { background: rgba(59,130,246,0.18); }
.notif-corpo { flex: 1; min-width: 0; }
.notif-titulo-item {
font-size: 13px; font-weight: 600;
color: var(--texto, #fff);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.notif-msg {
font-size: 12px; color: var(--texto2, #aaa);
margin-top: 2px; line-height: 1.4;
display: -webkit-box; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; overflow: hidden;
}
.notif-tempo { font-size: 11px; color: var(--texto2, #888); margin-top: 4px; }
.notif-ponto {
width: 7px; height: 7px;
background: var(--primario, #7c3aed);
border-radius: 50%; flex-shrink: 0; margin-top: 6px;
}
.notif-item.lida .notif-ponto { visibility: hidden; }
</style>
</head>
<body>
@@ -54,6 +163,21 @@
<span class="cargo" id="cursoUsuario"></span>
</div>
</div>
<div class="notif-wrapper" id="notifWrapper">
<button class="notif-btn" id="btnNotif" title="Notificações" aria-label="Notificações">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="notif-badge" id="notifBadge" style="display:none">0</span>
</button>
<div class="notif-panel" id="notifPanel">
<div class="notif-panel-header">
<span>Notificações</span>
<button class="notif-marcar-todas" id="btnMarcarTodas">Marcar todas como lidas</button>
</div>
<div class="notif-lista" id="notifLista">
<div class="notif-vazia">Nenhuma notificação</div>
</div>
</div>
</div>
<button class="theme-toggle-btn" onclick="toggleTheme()" title="Tema Escuro"><img src="imagens/moon-svgrepo-com.svg" class="theme-icon" alt="Tema"></button>
<a href="configuracoes.html" id="btnConfig" title="Configurações"><img src="imagens/engrenagem.png" class="config-icon" alt="Configurações"></a>
<button id="btnLogout" title="Sair">Sair</button>
@@ -196,34 +320,44 @@
<!-- MODAL DE DISCIPLINA -->
<div class="modal-overlay" id="modalDisciplina">
<div class="modal">
<div class="modal-header">
<div class="modal-disc-header">
<h2 id="tituloDisciplina">Gerenciar Disciplinas</h2>
<button class="modal-fechar" id="btnFecharDisciplina">&times;</button>
<button class="modal-fechar-btn" id="btnFecharDisciplina">&times;</button>
</div>
<div class="modal-corpo">
<div class="lista-itens" id="listaDisciplinas"></div>
<hr style="margin: 15px 0; border: none; border-top: 1px solid var(--borda);">
<div class="modal-campo">
<label for="discNome">Nome *</label>
<input type="text" id="discNome" placeholder="Ex: Calculo I">
</div>
<div class="modal-campo">
<label for="discProfessor">Professor</label>
<input type="text" id="discProfessor" placeholder="Nome do professor">
</div>
<div class="modal-campo">
<label for="discSala">Sala</label>
<input type="text" id="discSala" placeholder="Ex: Sala 204">
</div>
<div class="modal-campo">
<label for="discCor">Cor</label>
<input type="color" id="discCor" value="#4a90e2">
<!-- Painel 1: Lista de disciplinas -->
<div id="painelListaDisciplinas">
<div class="modal-corpo-lista" id="listaDisciplinas"></div>
<div class="modal-acoes">
<button class="btn-primario" id="btnNovaDisciplina">+ Nova Disciplina</button>
</div>
</div>
<div class="modal-acoes">
<button class="btn-perigo" id="btnExcluirDisciplina" style="display:none">Excluir</button>
<button class="btn-secundario" id="btnCancelarDisciplina">Cancelar</button>
<button class="btn-primario" id="btnSalvarDisciplina">Salvar</button>
<!-- Painel 2: Formulário add/edit -->
<div id="painelFormDisciplina" style="display:none">
<div class="modal-corpo-form">
<div class="modal-campo">
<label for="discNome">Nome *</label>
<input type="text" id="discNome" placeholder="Ex: Calculo I">
</div>
<div class="modal-campo">
<label for="discProfessor">Professor</label>
<input type="text" id="discProfessor" placeholder="Nome do professor">
</div>
<div class="modal-campo">
<label for="discSala">Sala</label>
<input type="text" id="discSala" placeholder="Ex: Sala 204">
</div>
<div class="modal-campo">
<label for="discCor">Cor</label>
<input type="color" id="discCor" value="#4a90e2">
</div>
</div>
<div class="modal-acoes">
<button class="btn-perigo" id="btnExcluirDisciplina" style="display:none">Excluir</button>
<button class="btn-secundario" id="btnVoltarDisciplina">← Voltar</button>
<button class="btn-primario" id="btnSalvarDisciplina">Salvar</button>
</div>
</div>
</div>
</div>
@@ -266,8 +400,7 @@ async function api(method, path, body) {
const res = await fetch(path, opts);
if (res.status === 401 || res.status === 403) {
// Token expirado ou inválido
if (res.status === 401) { logout(); return; }
logout(); return;
}
const json = await res.json().catch(() => ({}));
@@ -305,6 +438,8 @@ let disciplinas = [];
let eventoEditandoId = null;
let tarefaEditandoId = null;
const feriadosCache = {};
const meses = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho',
'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
const diasCurto = ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'];
@@ -505,9 +640,34 @@ function renderAgenda() {
}
// =============================================
// FERIADOS (fixos Brasil)
// FERIADOS BrasilAPI (gratuita, CORS habilitado)
// =============================================
function renderFeriados() {
async function buscarFeriadosAno(ano) {
if (feriadosCache[ano]) return feriadosCache[ano];
try {
const res = await fetch(`https://brasilapi.com.br/api/feriados/v1/${ano}`);
if (!res.ok) throw new Error('Erro BrasilAPI: ' + res.status);
// Retorna: [{date:"YYYY-MM-DD", name:"...", type:"national"}]
const lista = await res.json();
feriadosCache[ano] = lista;
return lista;
} catch(e) {
console.warn('Fallback para feriados fixos:', e.message);
feriadosCache[ano] = [
{ date: `${ano}-01-01`, name: 'Confraternização Universal' },
{ date: `${ano}-04-21`, name: 'Tiradentes' },
{ date: `${ano}-05-01`, name: 'Dia do Trabalho' },
{ date: `${ano}-09-07`, name: 'Independência do Brasil' },
{ date: `${ano}-10-12`, name: 'Nossa Sra. Aparecida' },
{ date: `${ano}-11-02`, name: 'Finados' },
{ date: `${ano}-11-15`, name: 'Proclamação da República' },
{ date: `${ano}-12-25`, name: 'Natal' },
];
return feriadosCache[ano];
}
}
async function renderFeriados() {
const container = document.getElementById('feriados');
container.replaceChildren();
@@ -516,18 +676,27 @@ function renderFeriados() {
header.textContent = 'FERIADOS NACIONAIS';
container.appendChild(header);
const feriadosMes = [
{ mes: 0, dia: 1, texto: '01 Jan Ano Novo' },
{ mes: 3, dia: 21, texto: '21 Abr Tiradentes' },
{ mes: 4, dia: 1, texto: '01 Mai Dia do Trabalho' },
{ mes: 8, dia: 7, texto: '07 Set Independência' },
{ mes: 9, dia: 12, texto: '12 Out N. Sra. Aparecida' },
{ mes: 10, dia: 2, texto: '02 Nov Finados' },
{ mes: 10, dia: 15, texto: '15 Nov República' },
{ mes: 11, dia: 25, texto: '25 Dez Natal' },
].filter(f => f.mes === dataMini.getMonth());
const loading = document.createElement('div');
loading.className = 'feriado';
loading.innerHTML = '<span style="opacity:0.5;font-size:11px">Carregando...</span>';
container.appendChild(loading);
if (feriadosMes.length === 0) {
const ano = dataMini.getFullYear();
const mes = dataMini.getMonth(); // 0-11
const todosFeriados = await buscarFeriadosAno(ano);
container.replaceChildren();
container.appendChild(header);
// BrasilAPI usa date no formato "YYYY-MM-DD"
const nomeMeses = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'];
const doMes = todosFeriados.filter(f => {
if (!f.date) return false;
const m = parseInt(f.date.substring(5, 7)) - 1; // 0-indexed
return m === mes;
});
if (doMes.length === 0) {
const item = document.createElement('div');
item.className = 'feriado';
item.innerHTML = '<span style="opacity:0.6;font-size:12px">Nenhum este mês</span>';
@@ -535,10 +704,12 @@ function renderFeriados() {
return;
}
feriadosMes.forEach(f => {
doMes.forEach(f => {
const dia = f.date.substring(8, 10);
const nomeMes = nomeMeses[mes];
const item = document.createElement('div');
item.className = 'feriado';
item.innerHTML = `<span class="dot"></span><span>${f.texto}</span>`;
item.innerHTML = `<span class="dot"></span><span>${dia} ${nomeMes} ${f.name}</span>`;
container.appendChild(item);
});
}
@@ -789,34 +960,21 @@ function abrirModalEvento(ev) {
}
document.getElementById('btnNovoEvento').addEventListener('click', () => abrirModalEvento(null));
document.getElementById('btnGerenciarDisciplinas').addEventListener('click', () => { renderListaDisciplinas(); abrirModalDisciplina(null); });
function renderListaDisciplinas() {
const lista = document.getElementById('listaDisciplinas');
lista.innerHTML = '';
if (!disciplinas.length) {
lista.innerHTML = '<p style="color:var(--texto2); text-align:center;">Nenhuma disciplina cadastrada</p>';
return;
}
disciplinas.forEach(d => {
const div = document.createElement('div');
div.className = 'item-lista';
div.style.cssText = 'display:flex; align-items:center; justify-content:space-between; padding:10px; border-bottom:1px solid var(--borda); cursor:pointer;';
div.innerHTML = `<span style="display:flex; align-items:center; gap:8px;"><span style="width:12px; height:12px; border-radius:50%; background:${d.cor||'#4a90e2'}"></span><b>${d.nome||''}</b></span>`;
div.onclick = () => abrirModalDisciplina(d);
lista.appendChild(div);
});
}
// Event Listeners Disciplina
document.getElementById('btnFecharDisciplina').addEventListener('click', () => document.getElementById('modalDisciplina').classList.remove('aberto'));
document.getElementById('btnCancelarDisciplina').addEventListener('click', () => document.getElementById('modalDisciplina').classList.remove('aberto'));
document.getElementById('btnSalvarDisciplina').addEventListener('click', salvarDisciplina);
document.getElementById('btnExcluirDisciplina').addEventListener('click', excluirDisciplina);
document.getElementById('btnGerenciarDisciplinas').addEventListener('click', () => abrirGerenciarDisciplinas());
// =============================================
// MODAL DE DISCIPLINA — dois painéis
// =============================================
let disciplinaEditandoId = null;
function abrirModalDisciplina(disc) {
function mostrarPainelLista() {
document.getElementById('tituloDisciplina').textContent = 'Gerenciar Disciplinas';
document.getElementById('painelListaDisciplinas').style.display = '';
document.getElementById('painelFormDisciplina').style.display = 'none';
renderListaDisciplinas();
}
function mostrarPainelForm(disc) {
disciplinaEditandoId = disc ? disc.id : null;
document.getElementById('tituloDisciplina').textContent = disc ? 'Editar Disciplina' : 'Nova Disciplina';
document.getElementById('discNome').value = disc ? (disc.nome || '') : '';
@@ -824,10 +982,43 @@ function abrirModalDisciplina(disc) {
document.getElementById('discSala').value = disc ? (disc.sala || '') : '';
document.getElementById('discCor').value = disc && disc.cor ? disc.cor : '#4a90e2';
document.getElementById('btnExcluirDisciplina').style.display = disc ? 'inline-block' : 'none';
renderListaDisciplinas();
document.getElementById('painelListaDisciplinas').style.display = 'none';
document.getElementById('painelFormDisciplina').style.display = '';
setTimeout(() => document.getElementById('discNome').focus(), 50);
}
function abrirGerenciarDisciplinas() {
mostrarPainelLista();
document.getElementById('modalDisciplina').classList.add('aberto');
}
function renderListaDisciplinas() {
const lista = document.getElementById('listaDisciplinas');
lista.innerHTML = '';
if (!disciplinas.length) {
lista.innerHTML = '<p style="color:var(--texto2); text-align:center; padding: 20px 0;">Nenhuma disciplina cadastrada</p>';
return;
}
disciplinas.forEach(d => {
const div = document.createElement('div');
div.className = 'disc-item';
div.innerHTML = `
<span class="disc-cor" style="background:${d.cor||'#4a90e2'}"></span>
<span class="disc-nome">${d.nome||''}</span>
${d.professor ? `<span class="disc-info">${d.professor}</span>` : ''}
<span class="disc-seta"></span>`;
div.onclick = () => mostrarPainelForm(d);
lista.appendChild(div);
});
}
// Event Listeners Disciplina
document.getElementById('btnFecharDisciplina').addEventListener('click', () => document.getElementById('modalDisciplina').classList.remove('aberto'));
document.getElementById('btnVoltarDisciplina').addEventListener('click', () => mostrarPainelLista());
document.getElementById('btnNovaDisciplina').addEventListener('click', () => mostrarPainelForm(null));
document.getElementById('btnSalvarDisciplina').addEventListener('click', salvarDisciplina);
document.getElementById('btnExcluirDisciplina').addEventListener('click', excluirDisciplina);
async function salvarDisciplina() {
const nome = document.getElementById('discNome').value.trim();
if (!nome) { mostrarToast('Nome e obrigatorio.', 'erro'); return; }
@@ -853,7 +1044,8 @@ async function salvarDisciplina() {
disciplinas.push(novo);
mostrarToast('Disciplina criada!');
}
document.getElementById('modalDisciplina').classList.remove('aberto');
preencherSelectsDisciplina();
mostrarPainelLista();
renderCalendarioGrande();
renderAgenda();
} catch(e) {
@@ -870,7 +1062,7 @@ async function excluirDisciplina() {
try {
await api('DELETE', `/api/disciplinas/${disciplinaEditandoId}`);
disciplinas = disciplinas.filter(d => d.id !== disciplinaEditandoId);
document.getElementById('modalDisciplina').classList.remove('aberto');
mostrarPainelLista();
mostrarToast('Disciplina excluida!');
renderCalendarioGrande();
renderAgenda();
@@ -1075,9 +1267,130 @@ async function init() {
renderMini();
renderCalendarioGrande();
renderAgenda();
renderFeriados();
await renderFeriados();
}
// =============================================
// NOTIFICACOES
// =============================================
let notificacoes = [];
let notifPanelAberto = false;
function tempoRelativo(dataStr) {
if (!dataStr) return '';
const agora = new Date();
const data = new Date(dataStr);
const diff = Math.floor((agora - data) / 1000);
if (diff < 60) return 'agora';
if (diff < 3600) return Math.floor(diff / 60) + 'min atrás';
if (diff < 86400) return Math.floor(diff / 3600) + 'h atrás';
return Math.floor(diff / 86400) + 'd atrás';
}
function iconeNotif(tipo) {
if (tipo === 'PRAZO_PROXIMO') return { cls: 'prazo', emoji: '⏰' };
if (tipo === 'TAREFA_ATRASADA') return { cls: 'atrasada', emoji: '🔴' };
if (tipo === 'EVENTO_PROXIMO') return { cls: 'evento', emoji: '📅' };
return { cls: 'evento', emoji: '🔔' };
}
function renderNotificacoes() {
const lista = document.getElementById('notifLista');
const badge = document.getElementById('notifBadge');
const naoLidas = notificacoes.filter(n => !n.lida);
const qtd = naoLidas.length;
if (qtd > 0) {
badge.textContent = qtd > 99 ? '99+' : qtd;
badge.style.display = 'flex';
} else {
badge.style.display = 'none';
}
if (!notificacoes.length) {
lista.innerHTML = '<div class="notif-vazia">Nenhuma notificação</div>';
return;
}
const ordenadas = [...notificacoes].sort((a, b) => {
if (!a.lida && b.lida) return -1;
if (a.lida && !b.lida) return 1;
return new Date(b.dataGeracao || 0) - new Date(a.dataGeracao || 0);
});
lista.innerHTML = ordenadas.map(n => {
const { cls, emoji } = iconeNotif(n.tipo);
const lidaCls = n.lida ? 'lida' : 'nao-lida';
return `<div class="notif-item ${lidaCls}" data-id="${n.id}" onclick="clicarNotificacao('${n.id}')">
<div class="notif-icone ${cls}">${emoji}</div>
<div class="notif-corpo">
<div class="notif-titulo-item">${n.titulo || 'Notificação'}</div>
<div class="notif-msg">${n.mensagem || ''}</div>
<div class="notif-tempo">${tempoRelativo(n.dataGeracao)}</div>
</div>
<div class="notif-ponto"></div>
</div>`;
}).join('');
}
async function carregarNotificacoes() {
if (!token) return;
try {
const dados = await api('GET', '/api/notificacoes/me');
notificacoes = dados || [];
renderNotificacoes();
} catch(e) {
console.error('Erro ao carregar notificações:', e);
}
}
async function clicarNotificacao(id) {
const n = notificacoes.find(x => x.id === id);
if (!n) return;
if (!n.lida) {
try {
await api('PATCH', `/api/notificacoes/${id}/ler`);
n.lida = true;
renderNotificacoes();
} catch(e) { console.error(e); }
}
}
async function marcarTodasLidas() {
if (!notificacoes.some(n => !n.lida)) return;
try {
await api('PATCH', '/api/notificacoes/me/ler-todas');
notificacoes.forEach(n => n.lida = true);
renderNotificacoes();
} catch(e) {
mostrarToast('Erro ao marcar notificações', 'erro');
}
}
document.getElementById('btnNotif').addEventListener('click', (e) => {
e.stopPropagation();
const panel = document.getElementById('notifPanel');
panel.classList.toggle('aberto');
});
document.getElementById('btnMarcarTodas').addEventListener('click', (e) => {
e.stopPropagation();
marcarTodasLidas();
});
document.addEventListener('click', (e) => {
const wrapper = document.getElementById('notifWrapper');
if (!wrapper.contains(e.target)) {
document.getElementById('notifPanel').classList.remove('aberto');
}
});
// Polling a cada 60 segundos
setInterval(carregarNotificacoes, 60000);
carregarNotificacoes();
init();
</script>
</body>