// Calendar week and day views: a time grid with all-day strip, overlapping events side by side and a "now" line. // Uses the calendar globals from app.js (calState, loadRange, evStore, ymd, fmtTime, esc, icon, newEvent, viewCalendar). const HOUR_PX = 48; /** Lay out one day's timed events in columns so overlapping ones sit side by side. */ function layoutDay(events) { const evs = events.map(e => ({ e, s: e.sm, en: Math.max(e.em, e.sm + 20) })).sort((a, b) => a.s - b.s || b.en - a.en); const out = []; let cluster = [], clusterEnd = -1; const flush = () => { const cols = []; for (const it of cluster) { let c = cols.findIndex(end => end <= it.s); if (c < 0) { c = cols.length; cols.push(0); } cols[c] = it.en; it.col = c; } cluster.forEach(it => { it.cols = cols.length; out.push(it); }); cluster = []; }; for (const it of evs) { if (cluster.length && it.s >= clusterEnd) { flush(); clusterEnd = -1; } cluster.push(it); clusterEnd = Math.max(clusterEnd, it.en); } if (cluster.length) flush(); return out; } async function renderTimeGrid(nDays) { const sel = new Date(calState.sel + 'T00:00:00'), start = new Date(sel); if (nDays === 7) start.setDate(start.getDate() - start.getDay()); const { byDay, errors } = await loadRange(start, nDays); if (!location.hash.startsWith('#/calendar') || !$('#cbody')) return; evStore.length = 0; const today = ymd(new Date()), days = [...Array(nDays)].map((_, i) => { const d = new Date(start); d.setDate(d.getDate() + i); return d; }); const title = nDays === 1 ? sel.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }) : days[0].toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' – ' + days[6].toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }); const allDay = days.map(d => (byDay.get(ymd(d)) || []).filter(e => e.allDay)); const cols = days.map(d => { const k = ymd(d), t0 = new Date(k + 'T00:00:00').getTime(), t1 = t0 + 864e5; const timed = (byDay.get(k) || []).filter(e => !e.allDay).map(e => ({ ...e, sm: (Math.max(e.start, t0) - t0) / 6e4, em: (Math.min(e.end, t1) - t0) / 6e4, orig: e })); return layoutDay(timed).map(({ e, s, en, col, cols: n }) => { const i = evStore.push(e.orig) - 1, h = Math.max(18, (en - s) / 60 * HOUR_PX - 2); return ``; }).join(''); }); const hours = [...Array(24)].map((_, h) => `
${h === 0 ? '' : new Date(2000, 0, 1, h).toLocaleTimeString([], { hour: 'numeric' })}
`).join(''); const now = new Date(), nowTop = (now.getHours() * 60 + now.getMinutes()) / 60 * HOUR_PX; $('#cbody').innerHTML = `

${esc(title)}

${days.map(d => ``).join('')}
${allDay.some(a => a.length) ? `
all day${allDay.map(a => `
${a.map(e => { const i = evStore.push(e) - 1; return ``; }).join('')}
`).join('')}
` : ''}
${hours}
${days.map((d, i) => `
${cols[i]}${ymd(d) === today ? `` : ''}
`).join('')}
${errors.map(e => `

${esc(e)}

`).join('')}`; const body = $('#tgbody'); if (body) { const wrap = body.parentElement.parentElement; wrap.scrollTop = Math.max(0, ((days.some(d => ymd(d) === today) ? now.getHours() - 1 : 7) * HOUR_PX)); } const shift = n => { const d = new Date(sel); d.setDate(d.getDate() + n * (nDays === 1 ? 1 : 7)); calState.sel = ymd(d); calState.y = d.getFullYear(); calState.m = d.getMonth(); renderTimeGrid(nDays); }; $('#tp').onclick = () => shift(-1); $('#tn').onclick = () => shift(1); $('#tt').onclick = () => { const d = new Date(); calState.sel = ymd(d); calState.y = d.getFullYear(); calState.m = d.getMonth(); renderTimeGrid(nDays); }; $('#cbody').querySelectorAll('.tgd').forEach(b => b.onclick = () => { if (nDays === 7) { calState.sel = b.dataset.day; store.set('cal_view', 'day'); viewCalendar(); } }); $('#cbody').querySelectorAll('.tgcol').forEach(c => c.onclick = e => { if (e.target === c) newEvent(c.dataset.day); }); }