b1dd3a8b01
meco meram, meco meram, meco meram
961 lines
35 KiB
HTML
961 lines
35 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">
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Nixie+One&display=swap');
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- HEADER -->
|
||
<div id="header">
|
||
<h1 id="title">Focus Agenda</h1>
|
||
</div>
|
||
|
||
<!-- BARRA ESQUERDA -->
|
||
<div id="barraesquerda">
|
||
<div id="calendario">
|
||
<div class="calendariotop">
|
||
<div id="mes"></div>
|
||
<div id="calendarseta">
|
||
<button class="ant" aria-label="Mês anterior">‹</button>
|
||
<button class="prox" aria-label="Próximo mês">›</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>
|
||
|
||
<!-- CONTEUDO PRINCIPAL -->
|
||
<div class="main">
|
||
<div class="topbar">
|
||
<h1>Calendário</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>
|
||
<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="Próximo">›</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">Mês</button>
|
||
</div>
|
||
|
||
<button id="btnNovoEvento">+ Novo Evento</button>
|
||
</div>
|
||
|
||
<div class="calendar-area month-view" id="calendarArea"></div>
|
||
</div>
|
||
|
||
<!-- MODAL DE EVENTO -->
|
||
<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">Título *</label>
|
||
<input type="text" id="evTitulo" placeholder="Ex: Prova de Matemática">
|
||
</div>
|
||
|
||
<div class="modal-campo">
|
||
<label for="evDescricao">Descrição</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>
|
||
|
||
<!-- MODAL DE TAREFA -->
|
||
<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">Título *</label>
|
||
<input type="text" id="tfTitulo" placeholder="Ex: Estudar para a prova">
|
||
</div>
|
||
|
||
<div class="modal-campo">
|
||
<label for="tfDescricao">Descrição</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>Média</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">Concluída</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>
|
||
|
||
<!-- TOAST -->
|
||
<div id="toast"></div>
|
||
|
||
<script>
|
||
// =============================================
|
||
// AUTENTICAÇÃO & BOOTSTRAP
|
||
// =============================================
|
||
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);
|
||
|
||
// =============================================
|
||
// API HELPERS
|
||
// =============================================
|
||
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) {
|
||
// Token expirado ou inválido
|
||
if (res.status === 401) { logout(); return; }
|
||
}
|
||
|
||
const json = await res.json().catch(() => ({}));
|
||
if (!res.ok) {
|
||
throw new Error(json.message || 'Erro na requisição');
|
||
}
|
||
return json.data;
|
||
}
|
||
|
||
// =============================================
|
||
// TOAST
|
||
// =============================================
|
||
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);
|
||
}
|
||
|
||
// =============================================
|
||
// ESTADO GLOBAL
|
||
// =============================================
|
||
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 meses = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho',
|
||
'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
|
||
const diasCurto = ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'];
|
||
const diasLongo = ['Domingo','Segunda','Terça','Quarta','Quinta','Sexta','Sábado'];
|
||
|
||
// =============================================
|
||
// CARREGA DADOS DO USUÁRIO
|
||
// =============================================
|
||
async function carregarUsuario() {
|
||
try {
|
||
const u = await api('GET', '/api/estudantes/me');
|
||
usuario = u;
|
||
localStorage.setItem('fa_user', JSON.stringify(u));
|
||
} catch(e) { /* usa cache */ }
|
||
|
||
if (usuario) {
|
||
document.getElementById('nomeUsuario').textContent = usuario.nome || 'Usuário';
|
||
document.getElementById('cursoUsuario').textContent =
|
||
usuario.curso ? (usuario.curso + (usuario.periodo ? ' – ' + usuario.periodo + 'º período' : '')) : '';
|
||
}
|
||
}
|
||
|
||
// =============================================
|
||
// CARREGA DADOS DA API
|
||
// =============================================
|
||
async function carregarDados() {
|
||
if (!usuario) return;
|
||
const id = usuario.id;
|
||
|
||
try {
|
||
[eventos, tarefas, disciplinas] = await Promise.all([
|
||
api('GET', `/api/eventos/estudante/${id}`),
|
||
api('GET', `/api/tarefas/estudante/${id}`),
|
||
api('GET', `/api/disciplinas/estudante/${id}`)
|
||
]);
|
||
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;
|
||
}
|
||
|
||
// Preenche selects de disciplina nos modais
|
||
function preencherSelectsDisciplina() {
|
||
['evDisciplina', 'tfDisciplina'].forEach(selId => {
|
||
const sel = document.getElementById(selId);
|
||
// Remove options exceto a primeira
|
||
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);
|
||
});
|
||
});
|
||
}
|
||
|
||
// =============================================
|
||
// NORMALIZAÇÃO DE DATAS
|
||
// =============================================
|
||
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;
|
||
}
|
||
|
||
// Pega a data ISO de um evento (pode ser dataHora ou dataEntrega)
|
||
function dataDoEvento(ev) {
|
||
const raw = ev.dataHora || ev.dataEntrega;
|
||
if (!raw) return null;
|
||
return raw.substring(0, 10); // 'YYYY-MM-DD'
|
||
}
|
||
|
||
function eventosDaData(iso) {
|
||
const evs = eventos.filter(e => dataDoEvento(e) === iso);
|
||
const tfs = tarefas.filter(t => t.dataEntrega === iso);
|
||
return { evs, tfs };
|
||
}
|
||
|
||
// Cor baseada no tipo do evento
|
||
const corPorTipo = { PROVA: 'amarelo', AULA: 'azul', TRABALHO: 'verde', EXAME: 'amarelo', OUTRO: '' };
|
||
|
||
// =============================================
|
||
// MINI CALENDÁRIO
|
||
// =============================================
|
||
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);
|
||
}
|
||
}
|
||
|
||
// =============================================
|
||
// AGENDA DO DIA (barra esquerda)
|
||
// =============================================
|
||
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);
|
||
});
|
||
}
|
||
|
||
// =============================================
|
||
// FERIADOS (fixos Brasil)
|
||
// =============================================
|
||
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 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());
|
||
|
||
if (feriadosMes.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>';
|
||
container.appendChild(item);
|
||
return;
|
||
}
|
||
|
||
feriadosMes.forEach(f => {
|
||
const item = document.createElement('div');
|
||
item.className = 'feriado';
|
||
item.innerHTML = `<span class="dot"></span><span>${f.texto}</span>`;
|
||
container.appendChild(item);
|
||
});
|
||
}
|
||
|
||
// =============================================
|
||
// CALENDÁRIO GRANDE
|
||
// =============================================
|
||
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;
|
||
}
|
||
|
||
/* MÊS */
|
||
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);
|
||
|
||
// Mostra até 3 eventos
|
||
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);
|
||
}
|
||
}
|
||
|
||
/* SEMANA */
|
||
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);
|
||
}
|
||
}
|
||
|
||
/* DIA */
|
||
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();
|
||
}
|
||
|
||
// =============================================
|
||
// MODAL DE EVENTO
|
||
// =============================================
|
||
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';
|
||
|
||
// Preenche ou limpa campos
|
||
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) {
|
||
// dataHora vem como "2025-04-01T14:00:00" – datetime-local precisa de "2025-04-01T14:00"
|
||
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('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) { mostrarToast('Título é obrigatório.', 'erro'); return; }
|
||
if (!dataHoraRaw) { mostrarToast('Data e hora são obrigatórias.', 'erro'); return; }
|
||
|
||
const btn = document.getElementById('btnSalvarEvento');
|
||
btn.disabled = true;
|
||
|
||
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', // adiciona segundos
|
||
estudanteId: usuario.id
|
||
};
|
||
|
||
try {
|
||
if (eventoEditandoId) {
|
||
const updated = await api('PUT', `/api/eventos/${eventoEditandoId}`, payload);
|
||
const idx = eventos.findIndex(e => e.id === eventoEditandoId);
|
||
if (idx !== -1) eventos[idx] = updated;
|
||
mostrarToast('Evento atualizado!');
|
||
} else {
|
||
const novo = await api('POST', '/api/eventos', payload);
|
||
eventos.push(novo);
|
||
mostrarToast('Evento criado!');
|
||
}
|
||
document.getElementById('modalEvento').classList.remove('aberto');
|
||
renderCalendarioGrande();
|
||
renderAgenda();
|
||
} catch(e) {
|
||
mostrarToast('Erro: ' + e.message, 'erro');
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
});
|
||
|
||
document.getElementById('btnExcluirEvento').addEventListener('click', async () => {
|
||
if (!confirm('Excluir este evento?')) return;
|
||
try {
|
||
await api('DELETE', `/api/eventos/${eventoEditandoId}?estudanteId=${usuario.id}`);
|
||
eventos = eventos.filter(e => e.id !== eventoEditandoId);
|
||
document.getElementById('modalEvento').classList.remove('aberto');
|
||
mostrarToast('Evento excluído.', 'sucesso');
|
||
renderCalendarioGrande();
|
||
renderAgenda();
|
||
} catch(e) {
|
||
mostrarToast('Erro: ' + e.message, 'erro');
|
||
}
|
||
});
|
||
|
||
// =============================================
|
||
// MODAL DE TAREFA
|
||
// =============================================
|
||
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) { mostrarToast('Título é obrigatório.', 'erro'); return; }
|
||
if (!dataEntrega) { mostrarToast('Data de entrega é obrigatória.', 'erro'); return; }
|
||
|
||
const btn = document.getElementById('btnSalvarTarefa');
|
||
btn.disabled = true;
|
||
|
||
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 updated = await api('PUT', `/api/tarefas/${tarefaEditandoId}`, payload);
|
||
const idx = tarefas.findIndex(t => t.id === tarefaEditandoId);
|
||
if (idx !== -1) tarefas[idx] = updated;
|
||
mostrarToast('Tarefa atualizada!');
|
||
} else {
|
||
const nova = await api('POST', '/api/tarefas', payload);
|
||
tarefas.push(nova);
|
||
mostrarToast('Tarefa criada!');
|
||
}
|
||
document.getElementById('modalTarefa').classList.remove('aberto');
|
||
renderCalendarioGrande();
|
||
renderAgenda();
|
||
} catch(e) {
|
||
mostrarToast('Erro: ' + e.message, 'erro');
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
});
|
||
|
||
document.getElementById('btnExcluirTarefa').addEventListener('click', async () => {
|
||
if (!confirm('Excluir esta tarefa?')) return;
|
||
try {
|
||
await api('DELETE', `/api/tarefas/${tarefaEditandoId}`);
|
||
tarefas = tarefas.filter(t => t.id !== tarefaEditandoId);
|
||
document.getElementById('modalTarefa').classList.remove('aberto');
|
||
mostrarToast('Tarefa excluída.', 'sucesso');
|
||
renderCalendarioGrande();
|
||
renderAgenda();
|
||
} catch(e) {
|
||
mostrarToast('Erro: ' + e.message, 'erro');
|
||
}
|
||
});
|
||
|
||
// Fecha modais clicando no overlay
|
||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) overlay.classList.remove('aberto');
|
||
});
|
||
});
|
||
|
||
// Fecha modais com ESC
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape') {
|
||
document.querySelectorAll('.modal-overlay.aberto').forEach(m => m.classList.remove('aberto'));
|
||
}
|
||
});
|
||
|
||
// =============================================
|
||
// NAVEGAÇÃO E VIEW SWITCH
|
||
// =============================================
|
||
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();
|
||
});
|
||
});
|
||
|
||
// Duplo clique em dia abre modal de novo evento
|
||
document.getElementById('calendarArea').addEventListener('dblclick', (e) => {
|
||
abrirModalEvento(null);
|
||
});
|
||
|
||
// =============================================
|
||
// INICIALIZAÇÃO
|
||
// =============================================
|
||
async function init() {
|
||
// Mostra spinner enquanto carrega
|
||
document.getElementById('calendarArea').innerHTML =
|
||
'<div class="loading"><div class="spinner"></div>Carregando...</div>';
|
||
|
||
await carregarUsuario();
|
||
await carregarDados();
|
||
preencherSelectsDisciplina();
|
||
|
||
renderMini();
|
||
renderCalendarioGrande();
|
||
renderAgenda();
|
||
renderFeriados();
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|