Files
FocusAgenda/src/main/resources/static/calendario.html
T

1330 lines
49 KiB
HTML

<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Focus Agenda</title>
<link rel="icon" href="imagens/icone.png">
<link rel="stylesheet" href="calendario.css">
<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>
<script src="utils.js"></script>
<style>
.notif-wrapper { position: relative; }
.notif-btn {
background: transparent;
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 7px 9px;
cursor: pointer;
color: #374151;
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: background 0.2s, border-color 0.2s;
}
.notif-btn:hover { background: #f9fafb; border-color: #c0392b; }
.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: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0,0,0,0.12);
z-index: 9999;
overflow: hidden;
font-family: 'Poppins', sans-serif;
}
.notif-panel.aberto { display: block; }
.notif-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #f3f4f6;
font-size: 13px;
font-weight: 600;
color: #1f2937;
background: linear-gradient(to right, rgba(192,57,43,0.06), rgba(17,68,85,0.06));
}
.notif-marcar-todas {
background: none; border: none;
color: #c0392b;
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: #9ca3af;
font-size: 13px;
padding: 28px 16px;
}
.notif-item {
display: flex; gap: 10px;
padding: 12px 16px;
border-bottom: 1px solid #e5e7eb;
cursor: pointer;
transition: background 0.15s;
align-items: flex-start;
}
.notif-item:hover { background: #fef2f2; }
.notif-item.nao-lida { background: #fef9f9; }
.notif-item.nao-lida:hover { background: #fee2e2; }
.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: #fff7ed; }
.notif-icone.atrasada { background: #fef2f2; }
.notif-icone.evento { background: #eff6ff; }
.notif-corpo { flex: 1; min-width: 0; }
.notif-titulo-item {
font-size: 13px; font-weight: 600;
color: #111827;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.notif-msg {
font-size: 12px; color: #6b7280;
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: #9ca3af; margin-top: 4px; }
.notif-ponto {
width: 7px; height: 7px;
background: #c0392b;
border-radius: 50%; flex-shrink: 0; margin-top: 6px;
}
.notif-item.lida .notif-ponto { visibility: hidden; }
</style>
</head>
<body>
<div id="header">
<h1 id="title">Focus Agenda</h1>
</div>
<div id="barraesquerda">
<div id="calendario">
<div class="calendariotop">
<div id="mes"></div>
<div id="calendarseta">
<button class="ant" aria-label="Mes anterior">&lsaquo;</button>
<button class="prox" aria-label="Proximo mes">&rsaquo;</button>
</div>
</div>
<table class="calendariodia">
<thead>
<tr>
<th>DOM</th><th>SEG</th><th>TER</th><th>QUA</th>
<th>QUI</th><th>SEX</th><th>SAB</th>
</tr>
</thead>
<tbody id="dias"></tbody>
</table>
</div>
<div id="agenda"></div>
<div id="feriados"></div>
</div>
<div class="main">
<div class="topbar">
<h1>Calendario</h1>
<div class="user-area">
<div class="perfil">
<div class="avatar"></div>
<div class="info">
<span class="nome" id="nomeUsuario">Carregando...</span>
<span class="cargo" id="cursoUsuario">-</span>
</div>
</div>
<div class="notif-wrapper" id="notifWrapper">
<button class="notif-btn" id="btnNotif" title="Notificacoes" aria-label="Notificacoes">
<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>Notificacoes</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 notificacao</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="Configuracoes"><img src="imagens/engrenagem.png" class="config-icon" alt="Configuracoes"></a>
<button id="btnLogout" title="Sair">Sair</button>
</div>
</div>
<div class="calendar-header">
<div class="mes-nav">
<button class="seta antGrande" aria-label="Anterior">&lsaquo;</button>
<span class="titulo-mes">Janeiro, 2025</span>
<button class="seta proxGrande" aria-label="Proximo">&rsaquo;</button>
</div>
<div class="view-switch">
<button type="button" data-view="dia">Dia</button>
<button type="button" data-view="semana">Semana</button>
<button type="button" data-view="mes" class="active">Mes</button>
</div>
<button id="btnNovoEvento">+ Novo Evento</button>
<button id="btnGerenciarDisciplinas">Disciplinas</button>
</div>
<div class="calendar-area month-view" id="calendarArea"></div>
</div>
<div class="modal-overlay" id="modalEvento">
<div class="modal">
<div class="modal-titulo" id="modalEventoTitulo">Novo Evento</div>
<div class="modal-campo">
<label for="evTitulo">Titulo *</label>
<input type="text" id="evTitulo" placeholder="Ex: Prova de Matematica">
</div>
<div class="modal-campo">
<label for="evDescricao">Descricao</label>
<textarea id="evDescricao" placeholder="Detalhes do evento..."></textarea>
</div>
<div class="modal-linha">
<div class="modal-campo">
<label for="evTipo">Tipo *</label>
<select id="evTipo">
<option value="AULA">Aula</option>
<option value="PROVA">Prova</option>
<option value="TRABALHO">Trabalho</option>
<option value="ESTUDO">Estudo</option>
<option value="EXAME">Exame</option>
<option value="OUTRO">Outro</option>
</select>
</div>
<div class="modal-campo">
<label for="evDisciplina">Disciplina</label>
<select id="evDisciplina">
<option value="">Nenhuma</option>
</select>
</div>
</div>
<div class="modal-linha">
<div class="modal-campo">
<label for="evData">Data e Hora *</label>
<input type="datetime-local" id="evData">
</div>
<div class="modal-campo">
<label for="evLocal">Local</label>
<input type="text" id="evLocal" placeholder="Ex: Sala 204">
</div>
</div>
<div class="modal-acoes">
<button class="btn-perigo" id="btnExcluirEvento" style="display:none">Excluir</button>
<button class="btn-secundario" id="btnCancelarEvento">Cancelar</button>
<button class="btn-primario" id="btnSalvarEvento">Salvar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="modalTarefa">
<div class="modal">
<div class="modal-titulo" id="modalTarefaTitulo">Nova Tarefa</div>
<div class="modal-campo">
<label for="tfTitulo">Titulo *</label>
<input type="text" id="tfTitulo" placeholder="Ex: Estudar para a prova">
</div>
<div class="modal-campo">
<label for="tfDescricao">Descricao</label>
<textarea id="tfDescricao" placeholder="Detalhes da tarefa..."></textarea>
</div>
<div class="modal-linha">
<div class="modal-campo">
<label for="tfPrioridade">Prioridade *</label>
<select id="tfPrioridade">
<option value="BAIXA">Baixa</option>
<option value="MEDIA" selected>Media</option>
<option value="ALTA">Alta</option>
<option value="URGENTE">Urgente</option>
</select>
</div>
<div class="modal-campo">
<label for="tfStatus">Status</label>
<select id="tfStatus">
<option value="PENDENTE" selected>Pendente</option>
<option value="EM_ANDAMENTO">Em Andamento</option>
<option value="CONCLUIDA">Concluida</option>
</select>
</div>
</div>
<div class="modal-linha">
<div class="modal-campo">
<label for="tfData">Data de Entrega *</label>
<input type="date" id="tfData">
</div>
<div class="modal-campo">
<label for="tfDisciplina">Disciplina</label>
<select id="tfDisciplina">
<option value="">Nenhuma</option>
</select>
</div>
</div>
<div class="modal-acoes">
<button class="btn-perigo" id="btnExcluirTarefa" style="display:none">Excluir</button>
<button class="btn-secundario" id="btnCancelarTarefa">Cancelar</button>
<button class="btn-primario" id="btnSalvarTarefa">Salvar</button>
</div>
</div>
</div>
<div id="toast"></div>
<div class="modal-overlay" id="modalDisciplina">
<div class="modal">
<div class="modal-disc-header">
<h2 id="tituloDisciplina">Gerenciar Disciplinas</h2>
<button class="modal-fechar-btn" id="btnFecharDisciplina">&times;</button>
</div>
<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 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>
<script>
const token = localStorage.getItem('fa_token');
const userStr = localStorage.getItem('fa_user');
if (!token) {
window.location.href = 'login.html';
}
let usuario = null;
try { usuario = JSON.parse(userStr); } catch(e) {}
function logout() {
localStorage.removeItem('fa_token');
localStorage.removeItem('fa_user');
window.location.href = 'login.html';
}
document.getElementById('btnLogout').addEventListener('click', logout);
async function api(method, path, body) {
const opts = {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
};
if (body) opts.body = JSON.stringify(body);
const res = await fetch(path, opts);
if (res.status === 401 || res.status === 403) {
logout(); return;
}
const json = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(json.message || 'Erro na requisicao');
}
return json.data;
}
const toastEl = document.getElementById('toast');
let toastTimer;
function mostrarToast(msg, tipo = 'sucesso') {
clearTimeout(toastTimer);
toastEl.textContent = msg;
toastEl.className = 'visivel ' + tipo;
toastTimer = setTimeout(() => { toastEl.className = ''; }, 3200);
}
let dataMini = new Date();
let dataGrande = new Date();
let modoAtual = 'mes';
let dataSelecionada = null;
let eventos = [];
let tarefas = [];
let disciplinas = [];
let eventoEditandoId = null;
let tarefaEditandoId = null;
const feriadosCache = {};
const meses = ['Janeiro','Fevereiro','Marco','Abril','Maio','Junho',
'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
const diasCurto = ['Dom','Seg','Ter','Qua','Qui','Sex','Sab'];
const diasLongo = ['Domingo','Segunda','Terca','Quarta','Quinta','Sexta','Sabado'];
async function carregarUsuario() {
try {
const u = await api('GET', '/api/estudantes/me');
usuario = u;
localStorage.setItem('fa_user', JSON.stringify(u));
} catch(e) {}
if (usuario) {
document.getElementById('nomeUsuario').textContent = usuario.nome || 'Usuario';
document.getElementById('cursoUsuario').textContent =
usuario.curso ? (usuario.curso + (usuario.periodo ? ' - ' + usuario.periodo + ' periodo' : '')) : '';
}
}
async function carregarDados() {
if (!usuario) return;
const id = usuario.id;
try {
[eventos, tarefas, disciplinas] = await Promise.all([
api('GET', '/api/eventos/me'),
api('GET', '/api/tarefas/me'),
api('GET', '/api/disciplinas/me')
]);
if (!eventos) eventos = [];
if (!tarefas) tarefas = [];
if (!disciplinas) disciplinas = [];
} catch(e) {
mostrarToast('Erro ao carregar dados: ' + e.message, 'erro');
}
}
function nomeDisciplina(id) {
if (!id) return null;
const d = disciplinas.find(d => d.id === id);
return d ? d.nome : null;
}
function preencherSelectsDisciplina() {
['evDisciplina', 'tfDisciplina'].forEach(selId => {
const sel = document.getElementById(selId);
while (sel.options.length > 1) sel.remove(1);
disciplinas.forEach(d => {
const opt = document.createElement('option');
opt.value = d.id;
opt.textContent = d.nome;
sel.appendChild(opt);
});
});
}
function normalizarData(d) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
function formatarISO(d) {
const a = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2,'0');
const dia = String(d.getDate()).padStart(2,'0');
return `${a}-${m}-${dia}`;
}
function formatarCurta(d) {
return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}`;
}
function adicionarDias(d, n) {
const r = new Date(d);
r.setDate(r.getDate() + n);
return r;
}
function inicioSemana(d) {
const r = normalizarData(d);
r.setDate(r.getDate() - r.getDay());
return r;
}
function dataDoEvento(ev) {
const raw = ev.dataHora || ev.dataEntrega;
if (!raw) return null;
return raw.substring(0, 10);
}
function eventosDaData(iso) {
const evs = eventos.filter(e => dataDoEvento(e) === iso);
const tfs = tarefas.filter(t => t.dataEntrega === iso);
return { evs, tfs };
}
const corPorTipo = { PROVA: 'amarelo', AULA: 'azul', TRABALHO: 'verde', EXAME: 'amarelo', OUTRO: '' };
function renderMini() {
const mesEl = document.getElementById('mes');
const diasEl = document.getElementById('dias');
const ano = dataMini.getFullYear();
const mes = dataMini.getMonth();
mesEl.textContent = `${meses[mes]} ${ano}`;
diasEl.replaceChildren();
const primeiroDia = new Date(ano, mes, 1).getDay();
const ultimoDia = new Date(ano, mes + 1, 0).getDate();
const ultimoAnterior = new Date(ano, mes, 0).getDate();
const totalCelulas = Math.ceil((primeiroDia + ultimoDia) / 7) * 7;
let diaAtual = 1, diaPros = 1;
for (let s = 0; s < totalCelulas / 7; s++) {
const linha = document.createElement('tr');
for (let ds = 0; ds < 7; ds++) {
const pos = s * 7 + ds;
const cel = document.createElement('td');
if (pos < primeiroDia) {
cel.textContent = ultimoAnterior - (primeiroDia - pos - 1);
cel.className = 'outromes';
} else if (diaAtual <= ultimoDia) {
cel.textContent = diaAtual;
const hoje = new Date();
if (diaAtual === hoje.getDate() && mes === hoje.getMonth() && ano === hoje.getFullYear()) {
cel.className = 'today';
}
const d = diaAtual;
cel.addEventListener('click', () => {
dataSelecionada = new Date(ano, mes, d);
dataGrande = new Date(dataSelecionada);
renderCalendarioGrande();
renderMini();
});
diaAtual++;
} else {
cel.textContent = diaPros++;
cel.className = 'outromes';
}
linha.appendChild(cel);
}
diasEl.appendChild(linha);
}
}
function renderAgenda() {
const container = document.getElementById('agenda');
container.replaceChildren();
const header = document.createElement('div');
header.className = 'agenda-header';
header.innerHTML = '<strong>HOJE</strong> ' +
new Intl.DateTimeFormat('pt-BR').format(new Date());
container.appendChild(header);
const hoje = formatarISO(normalizarData(new Date()));
const { evs, tfs } = eventosDaData(hoje);
const tudo = [
...evs.map(e => ({ titulo: e.titulo, hora: e.dataHora ? e.dataHora.substring(11,16) : '', tipo: 'evento' })),
...tfs.map(t => ({ titulo: t.titulo, hora: '', tipo: 'tarefa' }))
];
if (tudo.length === 0) {
const vazio = document.createElement('div');
vazio.className = 'agenda-empty';
vazio.textContent = 'Sem eventos para hoje.';
container.appendChild(vazio);
return;
}
tudo.forEach(item => {
const ev = document.createElement('div');
ev.className = 'evento';
ev.innerHTML = `<div class="hora">${item.hora || (item.tipo === 'tarefa' ? 'Tarefa' : '')}</div>
<div class="titulo">${item.titulo}</div>`;
container.appendChild(ev);
});
}
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);
const lista = await res.json();
feriadosCache[ano] = lista;
return lista;
} catch(e) {
feriadosCache[ano] = [
{ date: `${ano}-01-01`, name: 'Confraternizacao Universal' },
{ date: `${ano}-04-21`, name: 'Tiradentes' },
{ date: `${ano}-05-01`, name: 'Dia do Trabalho' },
{ date: `${ano}-09-07`, name: 'Independencia do Brasil' },
{ date: `${ano}-10-12`, name: 'Nossa Sra. Aparecida' },
{ date: `${ano}-11-02`, name: 'Finados' },
{ date: `${ano}-11-15`, name: 'Proclamacao da Republica' },
{ date: `${ano}-12-25`, name: 'Natal' },
];
return feriadosCache[ano];
}
}
async function renderFeriados() {
const container = document.getElementById('feriados');
container.replaceChildren();
const header = document.createElement('div');
header.className = 'feriados-header';
header.textContent = 'FERIADOS NACIONAIS';
container.appendChild(header);
const loading = document.createElement('div');
loading.className = 'feriado';
loading.innerHTML = '<span style="opacity:0.5;font-size:11px">Carregando...</span>';
container.appendChild(loading);
const ano = dataMini.getFullYear();
const mes = dataMini.getMonth();
const todosFeriados = await buscarFeriadosAno(ano);
container.replaceChildren();
container.appendChild(header);
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;
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 mes</span>';
container.appendChild(item);
return;
}
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>${dia} ${nomeMes} - ${f.name}</span>`;
container.appendChild(item);
});
}
function atualizarTituloGrande() {
const titulo = document.querySelector('.titulo-mes');
if (modoAtual === 'mes') {
titulo.textContent = `${meses[dataGrande.getMonth()]}, ${dataGrande.getFullYear()}`;
} else if (modoAtual === 'semana') {
const ini = inicioSemana(dataGrande);
const fim = adicionarDias(ini, 6);
titulo.textContent = `Semana ${formatarCurta(ini)} - ${formatarCurta(fim)}/${fim.getFullYear()}`;
} else {
titulo.textContent = `${diasLongo[dataGrande.getDay()]}, ${formatarCurta(dataGrande)}/${dataGrande.getFullYear()}`;
}
}
function configurarArea(classe) {
const area = document.getElementById('calendarArea');
area.className = 'calendar-area ' + classe;
area.replaceChildren();
return area;
}
function criarCardEvento(ev, isTarefa) {
const card = document.createElement('div');
const cor = isTarefa ? 'verde' : (corPorTipo[ev.tipo] || '');
card.className = `calendar-event ${cor}`;
card.innerHTML = `
<div class="calendar-event-hora">${isTarefa ? 'Tarefa' : (ev.dataHora ? ev.dataHora.substring(11,16) : '')}</div>
<div class="calendar-event-titulo">${ev.titulo}</div>`;
card.addEventListener('click', (e) => {
e.stopPropagation();
if (isTarefa) abrirModalTarefa(ev);
else abrirModalEvento(ev);
});
return card;
}
function renderMes() {
const area = configurarArea('month-view');
atualizarTituloGrande();
diasCurto.forEach(d => {
const el = document.createElement('div');
el.className = 'dia-semana';
el.textContent = d;
area.appendChild(el);
});
const ano = dataGrande.getFullYear(), mes = dataGrande.getMonth();
const primeiroDia = new Date(ano, mes, 1).getDay();
const ultimoDia = new Date(ano, mes + 1, 0).getDate();
const anteriorUltimo = new Date(ano, mes, 0).getDate();
const hoje = normalizarData(new Date());
for (let i = 0; i < 42; i++) {
const box = document.createElement('div');
box.className = 'dia-box';
let dNum, dData, outroMes = false;
if (i < primeiroDia) {
dNum = anteriorUltimo - (primeiroDia - i - 1);
dData = new Date(ano, mes - 1, dNum);
outroMes = true;
} else if (i < primeiroDia + ultimoDia) {
dNum = i - primeiroDia + 1;
dData = new Date(ano, mes, dNum);
} else {
dNum = i - primeiroDia - ultimoDia + 1;
dData = new Date(ano, mes + 1, dNum);
outroMes = true;
}
if (outroMes) box.classList.add('outro-mes');
const iso = formatarISO(dData);
const isHoje = !outroMes && normalizarData(dData).getTime() === hoje.getTime();
if (isHoje) box.classList.add('today');
if (dataSelecionada && iso === formatarISO(normalizarData(dataSelecionada))) {
box.classList.add('selecionado');
}
const numEl = document.createElement('div');
numEl.className = 'num-dia';
numEl.textContent = dNum;
box.appendChild(numEl);
const { evs, tfs } = eventosDaData(iso);
const todos = [...evs.map(e => ({ ...e, _tipo: 'evento' })), ...tfs.map(t => ({ ...t, _tipo: 'tarefa' }))];
const limite = 2;
todos.slice(0, limite).forEach(item => {
const mini = document.createElement('div');
const isTarefa = item._tipo === 'tarefa';
mini.className = 'evento-mini ' + (isTarefa ? 'verde' : (corPorTipo[item.tipo] || ''));
mini.textContent = item.titulo;
box.appendChild(mini);
});
if (todos.length > limite) {
const mais = document.createElement('div');
mais.className = 'mais-eventos';
mais.textContent = `+${todos.length - limite} mais`;
box.appendChild(mais);
}
box.addEventListener('click', () => {
dataSelecionada = dData;
dataGrande = new Date(dData);
renderCalendarioGrande();
renderMini();
});
area.appendChild(box);
}
}
function renderSemana() {
const area = configurarArea('week-view');
atualizarTituloGrande();
const ini = inicioSemana(dataGrande);
const hoje = normalizarData(new Date());
for (let i = 0; i < 7; i++) {
const d = adicionarDias(ini, i);
const iso = formatarISO(d);
const { evs, tfs } = eventosDaData(iso);
const col = document.createElement('div');
col.className = 'week-col';
if (normalizarData(d).getTime() === hoje.getTime()) col.classList.add('today');
const head = document.createElement('div');
head.className = 'week-col-head';
head.textContent = diasCurto[d.getDay()];
const dt = document.createElement('span');
dt.className = 'week-col-date';
dt.textContent = `${formatarCurta(d)}/${d.getFullYear()}`;
head.appendChild(dt);
const lista = document.createElement('div');
lista.className = 'week-events';
const todos = [...evs.map(e => ({ ...e, _tipo: 'evento' })), ...tfs.map(t => ({ ...t, _tipo: 'tarefa' }))];
if (todos.length === 0) {
const vazio = document.createElement('div');
vazio.className = 'week-empty';
vazio.textContent = 'Sem eventos';
lista.appendChild(vazio);
} else {
todos.forEach(item => lista.appendChild(criarCardEvento(item, item._tipo === 'tarefa')));
}
col.appendChild(head);
col.appendChild(lista);
area.appendChild(col);
}
}
function renderDia() {
const area = configurarArea('day-view');
atualizarTituloGrande();
const iso = formatarISO(normalizarData(dataGrande));
const { evs, tfs } = eventosDaData(iso);
const painel = document.createElement('div');
painel.className = 'day-panel';
const head = document.createElement('div');
head.className = 'day-panel-header';
head.textContent = `Agenda de ${formatarCurta(dataGrande)}/${dataGrande.getFullYear()}`;
painel.appendChild(head);
const lista = document.createElement('div');
lista.className = 'day-events';
const todos = [...evs.map(e => ({ ...e, _tipo: 'evento' })), ...tfs.map(t => ({ ...t, _tipo: 'tarefa' }))];
if (todos.length === 0) {
const vazio = document.createElement('div');
vazio.className = 'day-empty';
vazio.textContent = 'Nenhum evento para este dia.';
lista.appendChild(vazio);
} else {
todos.forEach(item => lista.appendChild(criarCardEvento(item, item._tipo === 'tarefa')));
}
painel.appendChild(lista);
area.appendChild(painel);
}
function renderCalendarioGrande() {
if (modoAtual === 'dia') renderDia();
else if (modoAtual === 'semana') renderSemana();
else renderMes();
}
function moverPeriodo(dir) {
if (modoAtual === 'dia') dataGrande.setDate(dataGrande.getDate() + dir);
else if (modoAtual === 'semana') dataGrande.setDate(dataGrande.getDate() + dir * 7);
else dataGrande.setMonth(dataGrande.getMonth() + dir);
renderCalendarioGrande();
atualizarTituloGrande();
}
function abrirModalEvento(ev) {
eventoEditandoId = ev ? ev.id : null;
const titulo = document.getElementById('modalEventoTitulo');
const btnExcluir = document.getElementById('btnExcluirEvento');
titulo.textContent = ev ? 'Editar Evento' : 'Novo Evento';
btnExcluir.style.display = ev ? 'inline-block' : 'none';
document.getElementById('evTitulo').value = ev ? ev.titulo : '';
document.getElementById('evDescricao').value = ev ? (ev.descricao || '') : '';
document.getElementById('evTipo').value = ev ? (ev.tipo || 'OUTRO') : 'OUTRO';
document.getElementById('evLocal').value = ev ? (ev.local || '') : '';
if (ev && ev.dataHora) {
document.getElementById('evData').value = ev.dataHora.substring(0, 16);
} else if (dataSelecionada) {
const iso = formatarISO(normalizarData(dataSelecionada));
document.getElementById('evData').value = iso + 'T08:00';
} else {
document.getElementById('evData').value = '';
}
document.getElementById('evDisciplina').value = ev ? (ev.disciplinaId || '') : '';
document.getElementById('modalEvento').classList.add('aberto');
document.getElementById('evTitulo').focus();
}
document.getElementById('btnNovoEvento').addEventListener('click', () => abrirModalEvento(null));
document.getElementById('btnGerenciarDisciplinas').addEventListener('click', () => abrirGerenciarDisciplinas());
let disciplinaEditandoId = null;
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 || '') : '';
document.getElementById('discProfessor').value = disc ? (disc.professor || '') : '';
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';
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);
});
}
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; }
const btn = document.getElementById('btnSalvarDisciplina');
setButtonLoading(btn, true, 'Salvar');
const payload = {
nome,
professor: document.getElementById('discProfessor').value.trim() || null,
sala: document.getElementById('discSala').value.trim() || null,
cor: document.getElementById('discCor').value
};
try {
if (disciplinaEditandoId) {
const res = await apiFetch(`/api/disciplinas/${disciplinaEditandoId}`, { method: 'PUT', body: JSON.stringify(payload) });
const updated = res.data;
const idx = disciplinas.findIndex(d => d.id === disciplinaEditandoId);
if (idx !== -1) disciplinas[idx] = updated;
showToast('Disciplina atualizada!', 'sucesso');
} else {
const res = await apiFetch('/api/disciplinas', { method: 'POST', body: JSON.stringify(payload) });
const novo = res.data;
disciplinas.push(novo);
showToast('Disciplina criada!', 'sucesso');
}
preencherSelectsDisciplina();
mostrarPainelLista();
renderCalendarioGrande();
renderAgenda();
} catch(e) {
showToast(e.message, 'erro');
} finally {
setButtonLoading(btn, false, 'Salvar');
}
}
async function excluirDisciplina() {
if (!disciplinaEditandoId) return;
if (!confirm('Tem certeza que deseja excluir esta disciplina?')) return;
try {
await apiFetch(`/api/disciplinas/${disciplinaEditandoId}`, { method: 'DELETE' });
disciplinas = disciplinas.filter(d => d.id !== disciplinaEditandoId);
mostrarPainelLista();
showToast('Disciplina excluida!', 'sucesso');
renderCalendarioGrande();
renderAgenda();
} catch(e) {
showToast(e.message, 'erro');
}
}
document.getElementById('btnCancelarEvento').addEventListener('click', () => {
document.getElementById('modalEvento').classList.remove('aberto');
});
document.getElementById('btnSalvarEvento').addEventListener('click', async () => {
const titulo = document.getElementById('evTitulo').value.trim();
const dataHoraRaw = document.getElementById('evData').value;
if (!titulo) { showToast('Titulo e obrigatorio.', 'erro'); return; }
if (!dataHoraRaw) { showToast('Data e hora sao obrigatorias.', 'erro'); return; }
const btn = document.getElementById('btnSalvarEvento');
setButtonLoading(btn, true, 'Salvar');
const payload = {
titulo,
descricao: document.getElementById('evDescricao').value.trim() || null,
tipo: document.getElementById('evTipo').value,
local: document.getElementById('evLocal').value.trim() || null,
disciplinaId: document.getElementById('evDisciplina').value || null,
dataHora: dataHoraRaw + ':00'
};
try {
if (eventoEditandoId) {
const res = await apiFetch(`/api/eventos/${eventoEditandoId}`, { method: 'PUT', body: JSON.stringify(payload) });
const updated = res.data;
const idx = eventos.findIndex(e => e.id === eventoEditandoId);
if (idx !== -1) eventos[idx] = updated;
showToast('Evento atualizado!', 'sucesso');
} else {
const res = await apiFetch('/api/eventos', { method: 'POST', body: JSON.stringify(payload) });
const novo = res.data;
eventos.push(novo);
showToast('Evento criado!', 'sucesso');
}
document.getElementById('modalEvento').classList.remove('aberto');
renderCalendarioGrande();
renderAgenda();
} catch(e) {
showToast(e.message, 'erro');
} finally {
setButtonLoading(btn, false, 'Salvar');
}
});
document.getElementById('btnExcluirEvento').addEventListener('click', async () => {
if (!confirm('Excluir este evento?')) return;
try {
await apiFetch(`/api/eventos/${eventoEditandoId}?estudanteId=${usuario.id}`, { method: 'DELETE' });
eventos = eventos.filter(e => e.id !== eventoEditandoId);
document.getElementById('modalEvento').classList.remove('aberto');
showToast('Evento excluido.', 'sucesso');
renderCalendarioGrande();
renderAgenda();
} catch(e) {
showToast(e.message, 'erro');
}
});
function abrirModalTarefa(tf) {
tarefaEditandoId = tf ? tf.id : null;
const titulo = document.getElementById('modalTarefaTitulo');
const btnExcluir = document.getElementById('btnExcluirTarefa');
titulo.textContent = tf ? 'Editar Tarefa' : 'Nova Tarefa';
btnExcluir.style.display = tf ? 'inline-block' : 'none';
document.getElementById('tfTitulo').value = tf ? tf.titulo : '';
document.getElementById('tfDescricao').value = tf ? (tf.descricao || '') : '';
document.getElementById('tfPrioridade').value = tf ? (tf.prioridade || 'MEDIA') : 'MEDIA';
document.getElementById('tfStatus').value = tf ? (tf.status || 'PENDENTE') : 'PENDENTE';
document.getElementById('tfDisciplina').value = tf ? (tf.disciplinaId || '') : '';
if (tf && tf.dataEntrega) {
document.getElementById('tfData').value = tf.dataEntrega;
} else if (dataSelecionada) {
document.getElementById('tfData').value = formatarISO(normalizarData(dataSelecionada));
} else {
document.getElementById('tfData').value = '';
}
document.getElementById('modalTarefa').classList.add('aberto');
document.getElementById('tfTitulo').focus();
}
document.getElementById('btnCancelarTarefa').addEventListener('click', () => {
document.getElementById('modalTarefa').classList.remove('aberto');
});
document.getElementById('btnSalvarTarefa').addEventListener('click', async () => {
const titulo = document.getElementById('tfTitulo').value.trim();
const dataEntrega = document.getElementById('tfData').value;
if (!titulo) { showToast('Titulo e obrigatorio.', 'erro'); return; }
if (!dataEntrega) { showToast('Data de entrega e obrigatoria.', 'erro'); return; }
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
const dataSelecionada = new Date(dataEntrega + 'T00:00:00');
if (dataSelecionada < hoje) { showToast('Data de entrega nao pode ser no passado.', 'erro'); return; }
const btn = document.getElementById('btnSalvarTarefa');
setButtonLoading(btn, true, 'Salvar');
const payload = {
titulo,
descricao: document.getElementById('tfDescricao').value.trim() || null,
prioridade: document.getElementById('tfPrioridade').value,
status: document.getElementById('tfStatus').value,
dataEntrega,
disciplinaId: document.getElementById('tfDisciplina').value || null,
estudanteId: usuario.id
};
try {
if (tarefaEditandoId) {
const res = await apiFetch(`/api/tarefas/${tarefaEditandoId}`, { method: 'PUT', body: JSON.stringify(payload) });
const updated = res.data;
const idx = tarefas.findIndex(t => t.id === tarefaEditandoId);
if (idx !== -1) tarefas[idx] = updated;
showToast('Tarefa atualizada!', 'sucesso');
} else {
const res = await apiFetch('/api/tarefas', { method: 'POST', body: JSON.stringify(payload) });
const nova = res.data;
tarefas.push(nova);
showToast('Tarefa criada!', 'sucesso');
}
document.getElementById('modalTarefa').classList.remove('aberto');
renderCalendarioGrande();
renderAgenda();
} catch(e) {
showToast(e.message, 'erro');
} finally {
setButtonLoading(btn, false, 'Salvar');
}
});
document.getElementById('btnExcluirTarefa').addEventListener('click', async () => {
if (!confirm('Excluir esta tarefa?')) return;
try {
await apiFetch(`/api/tarefas/${tarefaEditandoId}`, { method: 'DELETE' });
tarefas = tarefas.filter(t => t.id !== tarefaEditandoId);
document.getElementById('modalTarefa').classList.remove('aberto');
showToast('Tarefa excluida.', 'sucesso');
renderCalendarioGrande();
renderAgenda();
} catch(e) {
showToast(e.message, 'erro');
}
});
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.classList.remove('aberto');
});
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.aberto').forEach(m => m.classList.remove('aberto'));
}
});
document.querySelector('.prox').onclick = () => { dataMini.setMonth(dataMini.getMonth() + 1); renderMini(); renderFeriados(); };
document.querySelector('.ant').onclick = () => { dataMini.setMonth(dataMini.getMonth() - 1); renderMini(); renderFeriados(); };
document.querySelector('.antGrande').onclick = () => moverPeriodo(-1);
document.querySelector('.proxGrande').onclick = () => moverPeriodo(1);
document.querySelectorAll('.view-switch button').forEach(btn => {
btn.addEventListener('click', () => {
modoAtual = btn.dataset.view;
document.querySelectorAll('.view-switch button').forEach(b => b.classList.toggle('active', b === btn));
renderCalendarioGrande();
});
});
document.getElementById('calendarArea').addEventListener('dblclick', (e) => {
abrirModalEvento(null);
});
async function init() {
document.getElementById('calendarArea').innerHTML =
'<div class="loading"><div class="spinner"></div>Carregando...</div>';
await carregarUsuario();
await carregarDados();
preencherSelectsDisciplina();
renderMini();
renderCalendarioGrande();
renderAgenda();
await renderFeriados();
}
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 atras';
if (diff < 86400) return Math.floor(diff / 3600) + 'h atras';
return Math.floor(diff / 86400) + 'd atras';
}
function iconeNotif(tipo) {
if (tipo === 'PRAZO_PROXIMO') return { cls: 'prazo', emoji: '\u23F0' };
if (tipo === 'TAREFA_ATRASADA') return { cls: 'atrasada', emoji: '\uD83D\uDD34' };
if (tipo === 'EVENTO_PROXIMO') return { cls: 'evento', emoji: '\uD83D\uDCC5' };
return { cls: 'evento', emoji: '\uD83D\uDD14' };
}
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 notificacao</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 || 'Notificacao'}</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 notificacoes:', 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) {
showToast('Erro ao marcar notificacoes', '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');
}
});
setInterval(carregarNotificacoes, 60000);
carregarNotificacoes();
init();
</script>
</body>
</html>