/* Página del trabajador: registro + calendario de la ronda con estados. */ (() => { const token = location.pathname.split('/').pop(); const api = (p) => `/api/round/${token}${p}`; const $ = (id) => document.getElementById(id); const MONTHS = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre']; const DOW = ['L','M','X','J','V','S','D']; let state = null; // respuesta del servidor let todayIso = ''; // hoy según el servidor (zona de España) let blockedSet = new Set(); // días bloqueados por el admin (no elegibles) let myStatus = new Map(); // fecha → 'approved' | 'rejected' | 'pending' (guardado) let savedPending = new Set();// pendientes ya guardadas en el servidor let pending = new Set(); // pendientes en edición (con cambios sin guardar) // ---------- utilidades ---------- const toast = (msg, isError = false) => { const el = $('toast'); el.textContent = msg; el.classList.toggle('error', isError); el.classList.add('show'); clearTimeout(el._t); el._t = setTimeout(() => el.classList.remove('show'), 2600); }; const fetchJSON = async (url, opts) => { const res = await fetch(url, opts && { ...opts, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(opts.body), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'Error de conexión'); return data; }; const iso = (y, m, d) => `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; // Reconstruye el estado local a partir de las solicitudes del servidor. function setRequests(requests) { myStatus = new Map(requests.map((r) => [r.date, r.status])); savedPending = new Set(requests.filter((r) => r.status === 'pending').map((r) => r.date)); pending = new Set(savedPending); } // ---------- registro ---------- function renderJoin() { $('join-card').classList.remove('hidden'); const grid = $('role-grid'); grid.innerHTML = ''; for (const [value, label] of Object.entries(state.roles)) { const opt = document.createElement('label'); opt.className = 'role-option'; opt.innerHTML = `${label}`; grid.appendChild(opt); } $('join-form').addEventListener('submit', async (e) => { e.preventDefault(); const err = $('join-error'); err.classList.remove('show'); try { const role = document.querySelector('input[name=role]:checked')?.value; await fetchJSON(api('/join'), { method: 'POST', body: { name: $('name').value, role } }); await load(); } catch (ex) { err.textContent = ex.message; err.classList.add('show'); } }); } // ---------- calendario ---------- // Calcula cómo se ve y se comporta un día concreto para este trabajador. function dayMeta(date) { const role = state.me.role; const status = myStatus.get(date); const inPending = pending.has(date); const others = state.counts[date]?.[role] || 0; const limit = state.limits[role]; const full = limit ? others >= limit : false; const blocked = blockedSet.has(date); const past = date <= todayIso; const open = state.round.status === 'open'; let cls = 'day'; let disabled = !open; let clickable = false; if (status === 'approved') { cls += ' approved'; disabled = true; } else if (status === 'rejected') { cls += ' rejected'; disabled = true; } else if (inPending && savedPending.has(date)) { cls += ' pending'; if (past) disabled = true; else clickable = !disabled; } else if (inPending) { cls += ' choosing'; clickable = !disabled; } else { // Día libre para este trabajador. if (past) { cls += ' past'; disabled = true; } else if (blocked) { cls += ' blocked'; disabled = true; } else if (full) { cls += ' full'; disabled = true; } else clickable = !disabled; } return { cls, disabled, clickable, others, full }; } // Aplica el aspecto y el badge de compañeros a un botón de día. function styleDay(btn) { const date = btn.dataset.date; const m = dayMeta(date); btn.className = m.cls; btn.disabled = m.disabled; btn.querySelector('.count')?.remove(); if (m.others) { const b = document.createElement('span'); b.className = 'count' + (m.full ? ' is-full' : ''); b.textContent = m.others; btn.appendChild(b); } } function renderCalendar() { $('join-card').classList.add('hidden'); $('calendar-section').classList.remove('hidden'); const { me, round } = state; const open = round.status === 'open'; $('me-name').textContent = me.name; $('me-role').textContent = state.roles[me.role] || me.role; $('closed-banner').classList.toggle('hidden', open); $('calendar-hint').classList.toggle('hidden', !open); $('calendar-legend').classList.remove('hidden'); const cal = $('calendar'); cal.innerHTML = ''; for (let m = 0; m < 12; m++) { const month = document.createElement('section'); month.className = 'month'; month.style.animationDelay = `${m * 40}ms`; month.innerHTML = `

${MONTHS[m]} ${round.year}

` + `
${DOW.map((d) => `${d}`).join('')}
`; const days = document.createElement('div'); days.className = 'days'; const first = new Date(round.year, m, 1); const blanks = (first.getDay() + 6) % 7; // semana empieza en lunes for (let i = 0; i < blanks; i++) days.appendChild(document.createElement('span')); const total = new Date(round.year, m + 1, 0).getDate(); for (let d = 1; d <= total; d++) { const date = iso(round.year, m, d); const btn = document.createElement('button'); btn.type = 'button'; btn.dataset.date = date; btn.textContent = d; styleDay(btn); if (!btn.disabled) btn.addEventListener('click', () => toggle(btn)); days.appendChild(btn); } month.appendChild(days); cal.appendChild(month); } updateBars(); } function toggle(btn) { const date = btn.dataset.date; if (pending.has(date)) pending.delete(date); else pending.add(date); styleDay(btn); updateBars(); } function updateBars() { const approved = [...myStatus.values()].filter((s) => s === 'approved').length; $('me-count').textContent = approved + pending.size; const dirty = pending.size !== savedPending.size || [...pending].some((d) => !savedPending.has(d)); $('savebar').classList.toggle('show', dirty && state.round.status === 'open'); const n = pending.size; $('save-label').textContent = n === 1 ? '1 día pendiente' : `${n} días pendientes`; } $('save-btn').addEventListener('click', async () => { const btn = $('save-btn'); btn.disabled = true; try { const data = await fetchJSON(api('/requests'), { method: 'PUT', body: { dates: [...pending] }, }); setRequests(data.requests); state.counts = data.counts; renderCalendar(); toast('✓ Petición guardada'); } catch (ex) { toast(ex.message, true); // Si el servidor rechazó por días completos o bloqueados, recargamos para ver la realidad. if (/complet|bloquead/i.test(ex.message)) load(); } finally { btn.disabled = false; } }); // ---------- carga inicial ---------- async function load() { try { state = await fetchJSON(api('')); } catch { $('round-title').textContent = 'Ronda no encontrada'; return; } todayIso = state.today || new Date().toISOString().slice(0, 10); blockedSet = new Set(state.blocked || []); $('round-title').innerHTML = `${state.round.name} ${state.round.year}`; $('round-sub').textContent = state.me ? 'Marca en el calendario los días que quieres pedir' : 'Pide tus días de vacaciones en un minuto'; document.title = `Vacaciones · ${state.round.name} ${state.round.year}`; if (state.me) { setRequests(state.me.requests || []); renderCalendar(); } else if (state.round.status !== 'open') { $('closed-banner').classList.remove('hidden'); $('round-sub').textContent = 'Esta ronda ya no admite peticiones.'; } else { renderJoin(); } } load(); })();