const PID_IC_WEEKDAYS = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"]; function pidIsoDate(year, month, day) { const mm = String(month + 1).padStart(2, "0"); const dd = String(day).padStart(2, "0"); return `${year}-${mm}-${dd}`; } /** Categoria de exibição/filtro de um compromisso — não é só `ev.visibilidade`: * um "somente_eu" que não é meu só pode ter chegado por eu liderar o dono (o * backend só devolve isso pra gerente/coordenador), então vira "equipe". */ function pidEventoCategoria(ev) { if (ev.eh_evento) return "evento"; if (ev.visibilidade === "somente_eu") { return ev.sou_dono ? "somente_eu" : "equipe"; } return ev.visibilidade; } const PID_IC_VISIBILIDADE_LABELS = { somente_eu: "Somente eu", departamento: "Meu departamento", todos: "Todos", }; const PID_IC_MODALIDADE_LABELS = { presencial: "Presencial", remoto: "Remoto", hibrido: "Híbrido", }; document.addEventListener("DOMContentLoaded", async () => { const grid = document.getElementById("ic-grid"); if (!grid) return; const me = await pidGetMe(); if (!me) return; const titleEl = document.getElementById("ic-title"); const prevBtn = document.getElementById("ic-prev"); const nextBtn = document.getElementById("ic-next"); const todayBtn = document.getElementById("ic-today-btn"); const newBtn = document.getElementById("ic-new-btn"); const modal = document.getElementById("ic-modal"); const modalTitle = document.getElementById("ic-modal-title"); const modalTitleInput = document.getElementById("ic-event-title"); const modalDateInput = document.getElementById("ic-event-date"); const modalTimeInput = document.getElementById("ic-event-time"); const modalVisibilityField = document.getElementById("ic-event-visibility-field"); const modalVisibilitySelect = document.getElementById("ic-event-visibility"); const modalDepartmentField = document.getElementById("ic-event-department-field"); const modalDepartmentSelect = document.getElementById("ic-event-department"); const modalCategoriaSelect = document.getElementById("ic-event-categoria"); const modalCategoriaAddBtn = document.getElementById("ic-event-categoria-add-btn"); const modalEhEventoField = document.getElementById("ic-event-eh-evento-field"); const modalEhEventoCheckbox = document.getElementById("ic-event-eh-evento"); const modalLocalInput = document.getElementById("ic-event-local"); const modalModalidadeSelect = document.getElementById("ic-event-modalidade"); const modalDescricaoInput = document.getElementById("ic-event-descricao"); const modalReminderSelect = document.getElementById("ic-event-reminder"); const modalError = document.getElementById("ic-event-error"); const modalSaveBtn = document.getElementById("ic-event-save-btn"); const modalCancelBtn = document.getElementById("ic-event-cancel-btn"); const modalDeleteBtn = document.getElementById("ic-event-delete-btn"); const categoriaModal = document.getElementById("ic-categoria-modal"); const categoriaNomeInput = document.getElementById("ic-categoria-nome"); const categoriaCorInput = document.getElementById("ic-categoria-cor"); const categoriaError = document.getElementById("ic-categoria-error"); const categoriaSaveBtn = document.getElementById("ic-categoria-save-btn"); const categoriaCancelBtn = document.getElementById("ic-categoria-cancel-btn"); const viewModal = document.getElementById("ic-view-modal"); const viewTitle = document.getElementById("ic-view-title"); const viewDatetime = document.getElementById("ic-view-datetime"); const viewOwner = document.getElementById("ic-view-owner"); const viewVisibility = document.getElementById("ic-view-visibility"); const viewDepartmentField = document.getElementById("ic-view-department-field"); const viewDepartment = document.getElementById("ic-view-department"); const viewCategoriaField = document.getElementById("ic-view-categoria-field"); const viewCategoriaDot = document.getElementById("ic-view-categoria-dot"); const viewCategoria = document.getElementById("ic-view-categoria"); const viewLocalField = document.getElementById("ic-view-local-field"); const viewLocal = document.getElementById("ic-view-local"); const viewModalidadeField = document.getElementById("ic-view-modalidade-field"); const viewModalidade = document.getElementById("ic-view-modalidade"); const viewDescricaoField = document.getElementById("ic-view-descricao-field"); const viewDescricao = document.getElementById("ic-view-descricao"); const viewCloseBtn = document.getElementById("ic-view-close-btn"); const podeCriarEventoAmplo = Boolean( me.permissoes_efetivas?.["calendario-individual"]?.apps?.["calendario-individual-criar-evento"] ); modalCategoriaAddBtn.hidden = !podeCriarEventoAmplo; modalEhEventoField.hidden = !podeCriarEventoAmplo; modalVisibilitySelect.querySelectorAll('option[value="departamento"], option[value="todos"]').forEach((opt) => { opt.disabled = !podeCriarEventoAmplo; }); function updateEhEventoFieldsVisibility() { if (modalEhEventoCheckbox.checked) { modalVisibilitySelect.value = "todos"; modalVisibilityField.hidden = true; } else { modalVisibilityField.hidden = false; } updateDepartmentFieldVisibility(); } modalEhEventoCheckbox.addEventListener("change", updateEhEventoFieldsVisibility); // Sem categorias o calendário continua funcionando; o select avisa que a // lista não veio, em vez de parecer que não há categoria cadastrada. let categorias = []; let categoriasFalharam = false; try { categorias = await pidFetchCategoriasEvento(); } catch (e) { categoriasFalharam = true; } function popularSelectCategorias(selecionarId) { const rotuloVazio = categoriasFalharam ? "Nenhuma (categorias indisponíveis, recarregue a página)" : "Nenhuma"; modalCategoriaSelect.innerHTML = `` + categorias.map((c) => ``).join(""); if (selecionarId) modalCategoriaSelect.value = String(selecionarId); } function confirmarSairSemSalvar() { return pidConfirm("Sair sem salvar as alterações?", { titulo: "Sair sem salvar", textoConfirmar: "Sair sem salvar", perigoso: true, }); } popularSelectCategorias(); const holidayModal = document.getElementById("ic-holiday-modal"); const holidayDate = document.getElementById("ic-holiday-date"); const holidayName = document.getElementById("ic-holiday-name"); const holidayCloseBtn = document.getElementById("ic-holiday-close-btn"); const dayModal = document.getElementById("ic-day-modal"); const dayModalTitle = document.getElementById("ic-day-title"); const dayModalList = document.getElementById("ic-day-list"); const dayModalEmpty = document.getElementById("ic-day-empty"); const dayModalAddBtn = document.getElementById("ic-day-add-btn"); const dayModalCloseBtn = document.getElementById("ic-day-close-btn"); let dayModalIso = null; const meusDepartamentos = me.departamentos || []; modalDepartmentSelect.innerHTML = meusDepartamentos .map((d) => ``) .join(""); function updateDepartmentFieldVisibility() { modalDepartmentField.hidden = modalVisibilitySelect.value !== "departamento"; } modalVisibilitySelect.addEventListener("change", updateDepartmentFieldVisibility); // Retrato do modal de categoria na abertura (só pergunta ao sair se mudou). let retratoCategoria = null; function retratoCategoriaAtual() { return JSON.stringify([categoriaNomeInput.value, categoriaCorInput.value]); } async function tentarFecharCategoriaModal() { if (retratoCategoria !== null && retratoCategoriaAtual() !== retratoCategoria && !(await confirmarSairSemSalvar())) return; categoriaModal.hidden = true; retratoCategoria = null; } modalCategoriaAddBtn.addEventListener("click", () => { categoriaNomeInput.value = ""; categoriaCorInput.value = "#7c4dff"; categoriaError.textContent = ""; retratoCategoria = retratoCategoriaAtual(); categoriaModal.hidden = false; categoriaNomeInput.focus(); }); categoriaCancelBtn.addEventListener("click", tentarFecharCategoriaModal); categoriaModal.addEventListener("click", (event) => { if (event.target === categoriaModal) tentarFecharCategoriaModal(); }); categoriaSaveBtn.addEventListener("click", async () => { const nome = categoriaNomeInput.value.trim(); if (!nome) { categoriaError.textContent = "Informe o nome da categoria."; categoriaNomeInput.focus(); return; } // Trava contra clique duplo (criaria a categoria duas vezes). const textoOriginal = categoriaSaveBtn.textContent; categoriaSaveBtn.disabled = true; categoriaSaveBtn.textContent = "Salvando…"; try { const nova = await pidCreateCategoriaEvento({ nome, cor: categoriaCorInput.value }); categorias.push(nova); popularSelectCategorias(nova.id); categoriaModal.hidden = true; retratoCategoria = null; } catch (e) { categoriaError.textContent = e.message; } finally { categoriaSaveBtn.disabled = false; categoriaSaveBtn.textContent = textoOriginal; } }); const filtersContainer = document.getElementById("ic-filters"); const filterEquipeChip = document.getElementById("ic-filter-equipe"); filterEquipeChip.hidden = !me.lideranca; const activeFilters = new Set(); function selecionarTodosOsFiltros() { activeFilters.clear(); filtersContainer.querySelectorAll(".filter-chip").forEach((c) => { const visivel = !c.hidden; c.classList.toggle("is-active", visivel); if (visivel) activeFilters.add(c.dataset.filter); }); } selecionarTodosOsFiltros(); filtersContainer.addEventListener("click", (event) => { const chip = event.target.closest(".filter-chip"); if (!chip) return; const filtro = chip.dataset.filter; if (event.shiftKey) { // Shift mantido: acrescenta/remove esse filtro aos já selecionados. if (activeFilters.has(filtro)) { activeFilters.delete(filtro); chip.classList.remove("is-active"); } else { activeFilters.add(filtro); chip.classList.add("is-active"); } } else if (activeFilters.size === 1 && activeFilters.has(filtro)) { // Clicar de novo no único filtro já selecionado volta pra visualização padrão (todos selecionados). selecionarTodosOsFiltros(); } else { // Clique simples: troca a seleção pra só esse filtro. activeFilters.clear(); activeFilters.add(filtro); filtersContainer.querySelectorAll(".filter-chip").forEach((c) => { c.classList.toggle("is-active", c.dataset.filter === filtro); }); } render(); }); const today = new Date(); let viewYear = today.getFullYear(); let viewMonth = today.getMonth(); let editingEventId = null; let events = []; // Retrato do formulário de compromisso na abertura. let retratoCompromisso = null; function retratoCompromissoAtual() { return JSON.stringify([ modalTitleInput.value, modalDateInput.value, modalTimeInput.value, modalVisibilitySelect.value, modalDepartmentSelect.value, modalReminderSelect.value, modalCategoriaSelect.value, modalLocalInput.value, modalModalidadeSelect.value, modalDescricaoInput.value, modalEhEventoCheckbox.checked, ]); } function openModal(prefillDate, existingEvent) { if (existingEvent) { editingEventId = existingEvent.id; modalTitle.textContent = "Editar Compromisso"; modalTitleInput.value = existingEvent.titulo; modalDateInput.value = existingEvent.data; modalTimeInput.value = existingEvent.horario ? existingEvent.horario.slice(0, 5) : ""; modalVisibilitySelect.value = existingEvent.visibilidade || "somente_eu"; modalDepartmentSelect.value = existingEvent.departamento_compartilhado || ""; modalReminderSelect.value = existingEvent.lembrete_antecedencia || ""; popularSelectCategorias(existingEvent.categoria); modalLocalInput.value = existingEvent.local || ""; modalModalidadeSelect.value = existingEvent.modalidade || ""; modalDescricaoInput.value = existingEvent.descricao || ""; modalEhEventoCheckbox.checked = Boolean(existingEvent.eh_evento); modalDeleteBtn.hidden = false; } else { editingEventId = null; modalTitle.textContent = "Novo Compromisso"; modalTitleInput.value = ""; modalDateInput.value = prefillDate || pidIsoDate(today.getFullYear(), today.getMonth(), today.getDate()); modalTimeInput.value = ""; modalVisibilitySelect.value = "somente_eu"; modalDepartmentSelect.value = meusDepartamentos[0] ? String(meusDepartamentos[0].id) : ""; modalReminderSelect.value = ""; popularSelectCategorias(); modalLocalInput.value = ""; modalModalidadeSelect.value = ""; modalDescricaoInput.value = ""; modalEhEventoCheckbox.checked = false; modalDeleteBtn.hidden = true; } updateEhEventoFieldsVisibility(); modalError.textContent = ""; retratoCompromisso = retratoCompromissoAtual(); modal.hidden = false; modalTitleInput.focus(); } function closeModal() { modal.hidden = true; retratoCompromisso = null; } async function tentarFecharModal() { if (retratoCompromisso !== null && retratoCompromissoAtual() !== retratoCompromisso && !(await confirmarSairSemSalvar())) return; closeModal(); } function openViewModal(ev) { const horario = ev.horario ? ev.horario.slice(0, 5) : ""; viewTitle.textContent = ev.titulo; viewDatetime.textContent = horario ? `${new Date(`${ev.data}T00:00:00`).toLocaleDateString("pt-BR")} às ${horario}` : new Date(`${ev.data}T00:00:00`).toLocaleDateString("pt-BR"); viewOwner.textContent = ev.dono_nome; viewVisibility.textContent = PID_IC_VISIBILIDADE_LABELS[ev.visibilidade] || ev.visibilidade; viewDepartmentField.hidden = ev.visibilidade !== "departamento"; viewDepartment.textContent = ev.departamento_compartilhado_nome || ""; viewCategoriaField.hidden = !ev.categoria_nome; viewCategoriaDot.style.background = ev.categoria_cor || "transparent"; viewCategoria.textContent = ev.categoria_nome || ""; viewLocalField.hidden = !ev.local; viewLocal.textContent = ev.local || ""; viewModalidadeField.hidden = !ev.modalidade; viewModalidade.textContent = PID_IC_MODALIDADE_LABELS[ev.modalidade] || ev.modalidade || ""; viewDescricaoField.hidden = !ev.descricao; viewDescricao.textContent = ev.descricao || ""; viewModal.hidden = false; } viewCloseBtn.addEventListener("click", () => { viewModal.hidden = true; }); function openHolidayModal(iso, nome) { dayModal.hidden = true; holidayDate.textContent = new Date(`${iso}T00:00:00`).toLocaleDateString("pt-BR"); holidayName.textContent = nome; holidayModal.hidden = false; } holidayCloseBtn.addEventListener("click", () => { holidayModal.hidden = true; }); /** Pill de compromisso reaproveitado tanto na grade mensal quanto na agenda * do dia (popup aberto ao clicar no número do dia) — mesmo visual, mesmo * comportamento de clique (editar o próprio, ver detalhes dos demais). */ function criarPillCompromisso(ev) { const categoria = pidEventoCategoria(ev).replace("_", "-"); const horario = ev.horario ? ev.horario.slice(0, 5) : ""; const pill = document.createElement("span"); pill.className = `calendar-event calendar-event--${categoria}`; pill.textContent = horario ? `${horario} ${ev.titulo}` : ev.titulo; pill.title = ev.sou_dono ? `${ev.titulo}${horario ? ` às ${horario}` : ""} (clique para editar)` : `${ev.titulo} — ${ev.dono_nome} (clique para ver detalhes)`; if (ev.sou_dono) { pill.addEventListener("click", () => { dayModal.hidden = true; openModal(ev.data, ev); }); } else { pill.addEventListener("click", () => { dayModal.hidden = true; openViewModal(ev); }); } return pill; } function criarPillFeriado(iso, nome) { const holidayLabel = document.createElement("span"); holidayLabel.className = "calendar-day__holiday"; holidayLabel.textContent = nome; holidayLabel.title = `${nome} (clique para ver o nome completo)`; holidayLabel.addEventListener("click", () => openHolidayModal(iso, nome)); return holidayLabel; } function openDayModal(iso, cellDate, nomeFeriado, eventosDoDia) { dayModalIso = iso; dayModalTitle.textContent = cellDate.toLocaleDateString("pt-BR", { day: "numeric", month: "long", year: "numeric", }); dayModalList.innerHTML = ""; if (nomeFeriado) dayModalList.appendChild(criarPillFeriado(iso, nomeFeriado)); eventosDoDia.forEach((ev) => dayModalList.appendChild(criarPillCompromisso(ev))); dayModalEmpty.hidden = Boolean(nomeFeriado) || eventosDoDia.length > 0; dayModal.hidden = false; } dayModalAddBtn.addEventListener("click", () => { dayModal.hidden = true; openModal(dayModalIso, null); }); dayModalCloseBtn.addEventListener("click", () => { dayModal.hidden = true; }); modal.addEventListener("click", (event) => { if (event.target === modal) tentarFecharModal(); }); viewModal.addEventListener("click", (event) => { if (event.target === viewModal) viewModal.hidden = true; }); holidayModal.addEventListener("click", (event) => { if (event.target === holidayModal) holidayModal.hidden = true; }); dayModal.addEventListener("click", (event) => { if (event.target === dayModal) dayModal.hidden = true; }); // Nunca lança: na falha avisa e mantém a lista anterior, pra grade // continuar desenhada (e um save que já deu certo não parecer erro). async function reloadEvents() { try { events = await pidFetchEvents(); return true; } catch (e) { await pidAlert(`${e.message} Recarregue a página; se persistir, contate a Inovação.`, { titulo: "Não foi possível carregar os compromissos", }); return false; } } const feriadosPorAno = new Map(); // Feriado é complemento: se /api/feriados/ falhar, a grade sai sem eles // em vez de sumir. A falha não fica em cache (tenta de novo na próxima). async function carregarFeriados(ano) { if (!feriadosPorAno.has(ano)) { try { const lista = await pidApiRequest(`/feriados/?ano=${ano}`); feriadosPorAno.set(ano, new Map((lista || []).map((f) => [f.data, f.nome]))); } catch (e) { return new Map(); } } return feriadosPorAno.get(ano); } async function render() { // Os dias "fora do mês" no início/fim da grade podem cair no ano anterior/seguinte // (ex.: dezembro exibe dias de janeiro do ano seguinte) — só busca esses anos extras // quando o mês exibido realmente encosta numa dessas bordas. const anosNecessarios = new Set([viewYear]); if (viewMonth === 0) anosNecessarios.add(viewYear - 1); if (viewMonth === 11) anosNecessarios.add(viewYear + 1); const mapasFeriados = await Promise.all(Array.from(anosNecessarios).map(carregarFeriados)); const feriados = new Map(); mapasFeriados.forEach((mapa) => mapa.forEach((nome, data) => feriados.set(data, nome))); const firstWeekday = new Date(viewYear, viewMonth, 1).getDay(); const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); const daysInPrevMonth = new Date(viewYear, viewMonth, 0).getDate(); const isCurrentMonth = viewYear === today.getFullYear() && viewMonth === today.getMonth(); // Só a primeira letra maiúscula ("Setembro de 2026", não "Setembro De 2026"). const tituloMes = new Date(viewYear, viewMonth, 1).toLocaleDateString("pt-BR", { month: "long", year: "numeric", }); titleEl.textContent = tituloMes.charAt(0).toUpperCase() + tituloMes.slice(1); const cells = []; for (let i = firstWeekday - 1; i >= 0; i -= 1) { cells.push({ day: daysInPrevMonth - i, outside: true, monthOffset: -1 }); } for (let d = 1; d <= daysInMonth; d += 1) { cells.push({ day: d, outside: false, monthOffset: 0 }); } while (cells.length % 7 !== 0) { cells.push({ day: cells.length - (firstWeekday + daysInMonth) + 1, outside: true, monthOffset: 1 }); } grid.querySelectorAll(".calendar-day").forEach((el) => el.remove()); cells.forEach((cell) => { const cellDate = new Date(viewYear, viewMonth + cell.monthOffset, cell.day); const iso = pidIsoDate(cellDate.getFullYear(), cellDate.getMonth(), cellDate.getDate()); const nomeFeriado = feriados.get(iso); const eventosDoDia = events .filter((ev) => ev.data === iso && activeFilters.has(pidEventoCategoria(ev))) .sort((a, b) => (a.horario || "").localeCompare(b.horario || "")); const dayEl = document.createElement("div"); dayEl.className = "calendar-day" + (cell.outside ? " is-outside" : ""); if (!cell.outside && isCurrentMonth && cell.day === today.getDate()) { dayEl.classList.add("is-today"); } if (nomeFeriado) { dayEl.classList.add("is-feriado"); } const header = document.createElement("div"); header.className = "calendar-day__header"; const number = document.createElement("span"); number.className = "calendar-day__number"; number.textContent = cell.day; number.title = "Ver agenda completa do dia"; number.addEventListener("click", () => openDayModal(iso, cellDate, nomeFeriado, eventosDoDia)); header.appendChild(number); const addBtn = document.createElement("button"); addBtn.type = "button"; addBtn.className = "calendar-day__add"; addBtn.setAttribute("aria-label", "Novo compromisso"); addBtn.innerHTML = ''; addBtn.addEventListener("click", () => openModal(iso)); header.appendChild(addBtn); dayEl.appendChild(header); if (nomeFeriado) { dayEl.appendChild(criarPillFeriado(iso, nomeFeriado)); } const eventsEl = document.createElement("div"); eventsEl.className = "calendar-day__events"; eventosDoDia.forEach((ev) => eventsEl.appendChild(criarPillCompromisso(ev))); dayEl.appendChild(eventsEl); grid.appendChild(dayEl); }); } PID_IC_WEEKDAYS.forEach((label) => { const el = document.createElement("div"); el.className = "calendar-grid__weekday"; el.textContent = label; grid.appendChild(el); }); newBtn.addEventListener("click", () => openModal(null, null)); modalCancelBtn.addEventListener("click", tentarFecharModal); modalDeleteBtn.addEventListener("click", async () => { if (!editingEventId) return; if (await pidConfirm("Excluir este compromisso?", { perigoso: true })) { modalDeleteBtn.disabled = true; try { await pidDeleteEvent(editingEventId); } catch (e) { modalError.textContent = e.message; return; } finally { modalDeleteBtn.disabled = false; } closeModal(); await reloadEvents(); render(); } }); modalSaveBtn.addEventListener("click", async () => { const titulo = modalTitleInput.value.trim(); const data = modalDateInput.value; const horario = modalTimeInput.value; const visibilidade = modalVisibilitySelect.value; if (!titulo || !data) { modalError.textContent = "Informe o título e a data do compromisso."; (titulo ? modalDateInput : modalTitleInput).focus(); return; } if (visibilidade === "departamento" && !modalDepartmentSelect.value) { modalError.textContent = "Selecione um departamento para compartilhar o compromisso, ou você não tem nenhum departamento vinculado."; return; } const payload = { titulo, data, horario: horario || null, visibilidade, departamento_compartilhado: visibilidade === "departamento" ? Number(modalDepartmentSelect.value) : null, lembrete_antecedencia: modalReminderSelect.value, categoria: modalCategoriaSelect.value ? Number(modalCategoriaSelect.value) : null, eh_evento: modalEhEventoCheckbox.checked, local: modalLocalInput.value.trim(), modalidade: modalModalidadeSelect.value, descricao: modalDescricaoInput.value.trim(), }; // Trava contra clique duplo (criaria o compromisso duas vezes). const textoOriginal = modalSaveBtn.textContent; modalSaveBtn.disabled = true; modalSaveBtn.textContent = "Salvando…"; try { if (editingEventId) { await pidUpdateEvent(editingEventId, payload); } else { await pidCreateEvent(payload); } } catch (e) { modalError.textContent = e.message; return; } finally { modalSaveBtn.disabled = false; modalSaveBtn.textContent = textoOriginal; } closeModal(); await reloadEvents(); render(); }); prevBtn.addEventListener("click", () => { viewMonth -= 1; if (viewMonth < 0) { viewMonth = 11; viewYear -= 1; } render(); }); nextBtn.addEventListener("click", () => { viewMonth += 1; if (viewMonth > 11) { viewMonth = 0; viewYear += 1; } render(); }); todayBtn.addEventListener("click", () => { viewYear = today.getFullYear(); viewMonth = today.getMonth(); render(); }); await reloadEvents(); render(); });