1330 lines
49 KiB
HTML
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">‹</button>
|
|
<button class="prox" aria-label="Proximo mes">›</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">‹</button>
|
|
<span class="titulo-mes">Janeiro, 2025</span>
|
|
<button class="seta proxGrande" aria-label="Proximo">›</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">×</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>
|