/* Shared calendar JS Usage: call initCalendar(containerId, opts) opts: workerId: optional worker id leaves: array of {start:'YYYY-MM-DD', end:'YYYY-MM-DD'} requestUrl: url to use for requesting leave (GET will be used with query params) mode: 'inline' or other */ (function(){ // Keep a registry of calendar instances so external code (eg. modal handlers) // can clear selections when needed. window._calendars = window._calendars || {}; function iso(y,m,d){ return `${y}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}` } function initCalendar(containerId, opts){ const container = document.getElementById(containerId); if(!container) return; const workerId = opts.workerId || container.dataset.workerId || ''; let leaves = opts.leaves || []; const leavesUrl = opts.leavesUrl || null; const requestUrl = opts.requestUrl || (window.location.origin + '/leave/request/'); let viewYear = new Date().getFullYear(); let viewMonth = new Date().getMonth(); let selStart = null; let selEnd = null; let previewEnd = null; function inLeaves(isoStr){ for(const L of leaves){ if(!L.start) continue; if(L.start<=isoStr && isoStr<=L.end) return true } return false } function buildControls(){ let controls = document.getElementById('calendar-controls'); if(!controls){ controls = document.createElement('div'); controls.id='calendar-controls'; controls.innerHTML = `No dates selected `; container.parentNode.insertBefore(controls, container.nextSibling); document.getElementById('sel-clear').onclick = function(){ selStart = null; selEnd = null; previewEnd = null; render(); }; document.getElementById('sel-confirm').onclick = function(){ if(!selStart) return; if(!selEnd) selEnd = selStart; const startInput = document.querySelector('input[name="start_date"]'); const endInput = document.querySelector('input[name="end_date"]'); if(startInput) startInput.value = selStart; if(endInput) endInput.value = selEnd; if(typeof initFlatpickr === 'function') initFlatpickr(); const form = document.querySelector('form'); if(form) form.scrollIntoView({behavior:'smooth'}); }; } const label = document.getElementById('sel-label'); if(selStart && selEnd) label.textContent = `Selected: ${selStart} → ${selEnd}`; else if(selStart) label.textContent = `Start: ${selStart}`; else label.textContent = 'No dates selected'; document.getElementById('sel-confirm').disabled = !selStart; } function render(){ container.innerHTML=''; const first = new Date(viewYear, viewMonth, 1); const startDay = first.getDay(); const days = new Date(viewYear, viewMonth+1, 0).getDate(); let cur = 1 - startDay; const header = document.createElement('div'); header.style.display='flex'; header.style.justifyContent='space-between'; header.style.alignItems='center'; header.style.marginBottom='0.5rem'; const title = document.createElement('div'); title.textContent = first.toLocaleString(undefined,{month:'long', year:'numeric'}); const prev = document.createElement('button'); prev.textContent='◀'; prev.className='button'; prev.onclick=()=>{ viewMonth--; if(viewMonth<0){ viewMonth=11; viewYear--; } render(); }; const next = document.createElement('button'); next.textContent='▶'; next.className='button'; next.onclick=()=>{ viewMonth++; if(viewMonth>11){ viewMonth=0; viewYear++; } render(); }; header.appendChild(prev); header.appendChild(title); header.appendChild(next); container.appendChild(header); // Day-of-week header row const dow = document.createElement('div'); dow.className='week'; dow.style.fontWeight='bold'; const dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; for(const dn of dayNames){ const dh = document.createElement('div'); dh.className='day day-header'; dh.style.textAlign='center'; dh.style.padding='0.25rem'; dh.textContent = dn; dow.appendChild(dh); } container.appendChild(dow); while(cur<=days){ const week = document.createElement('div'); week.className='week'; for(let i=0;i<7;i++){ const dayDiv = document.createElement('div'); dayDiv.className='day'; if(cur<1 || cur>days){ dayDiv.classList.add('other-month'); dayDiv.textContent=''; } else{ const isoStr = iso(viewYear, viewMonth, cur); dayDiv.textContent = cur; if(inLeaves(isoStr)) dayDiv.classList.add('leave'); const rangeEnd = selEnd || previewEnd; if(selStart && rangeEnd){ if(selStart<=isoStr && isoStr<=rangeEnd) dayDiv.classList.add('in-range'); } else if(selStart && selStart===isoStr) dayDiv.classList.add('selected'); dayDiv.dataset.iso = isoStr; dayDiv.onclick = function(){ const clicked = this.dataset.iso; if(!selStart){ selStart = clicked; selEnd = null; } else if(selStart && !selEnd){ if(clicked < selStart){ selEnd = selStart; selStart = clicked } else selEnd = clicked } else { selStart = clicked; selEnd = null } previewEnd = null; render(); // Auto-confirm on range completion: open request URL with params if(selStart && selEnd){ const url = requestUrl + '?worker_id=' + encodeURIComponent(workerId) + '&start_date=' + selStart + '&end_date=' + selEnd; try{ htmx.ajax('GET', url, {target:'#modal', swap:'innerHTML'}); }catch(e){ window.location = url; } } }; dayDiv.onmouseover = function(){ if(selStart && !selEnd){ previewEnd = this.dataset.iso; render(); } }; dayDiv.onmouseout = function(){ if(selStart && !selEnd){ previewEnd = null; render(); } }; } week.appendChild(dayDiv); cur++; } container.appendChild(week); } buildControls(); } // expose a simple API to clear the current selection from outside. function clearSelection(){ selStart = null; selEnd = null; previewEnd = null; render(); } function setLeaves(newLeaves){ leaves = newLeaves || []; render(); } async function refreshLeaves(){ if(leavesUrl){ try{ const res = await fetch(leavesUrl, { credentials: 'same-origin' }); if(res.ok){ const data = await res.json(); if(data && Array.isArray(data.leaves)){ setLeaves(data.leaves.map(l=>({start:l.start, end:l.end, reason:l.reason}))); return; } } }catch(e){ console.error('refreshLeaves failed', e); } } // fallback: do nothing } window._calendars[containerId] = { clearSelection, setLeaves, refreshLeaves }; render(); // If a server leavesUrl is provided, refresh from server to ensure authoritative data if(leavesUrl){ // don't await here — fire-and-forget refresh refreshLeaves(); } } // provide a global helper to clear selections across all calendars window.clearAllCalendarSelections = function(){ try{ if(window._calendars){ Object.values(window._calendars).forEach(c => c.clearSelection && c.clearSelection()); } }catch(e){ console.error('clearAllCalendarSelections', e); } }; // expose window.initCalendar = initCalendar; })();