// ── Contacts & Calendar ───────────────────────────────────────────────────── let _currentView = 'mail'; // ======== VIEW SWITCHING ======== // Uses data-view attribute on #app-root to switch panels via CSS, // avoiding direct style manipulation of elements that may not exist. function _setView(view) { _currentView = view; // Update nav item active states ['nav-unified','nav-starred','nav-contacts','nav-calendar'].forEach(id => { document.getElementById(id)?.classList.remove('active'); }); // Show/hide panels const mail1 = document.getElementById('message-list-panel'); const mail2 = document.getElementById('message-detail'); const contacts = document.getElementById('contacts-panel'); const calendar = document.getElementById('calendar-panel'); if (mail1) mail1.style.display = view === 'mail' ? '' : 'none'; if (mail2) mail2.style.display = view === 'mail' ? '' : 'none'; if (contacts) contacts.style.display = view === 'contacts' ? 'flex' : 'none'; if (calendar) calendar.style.display = view === 'calendar' ? 'flex' : 'none'; } function showMail() { _setView('mail'); document.getElementById('nav-unified')?.classList.add('active'); } function showContacts() { _setView('contacts'); document.getElementById('nav-contacts')?.classList.add('active'); if (typeof mobCloseNav === 'function') { mobCloseNav(); mobSetView('list'); } loadContacts(); } function showCalendar() { _setView('calendar'); document.getElementById('nav-calendar')?.classList.add('active'); if (typeof mobCloseNav === 'function') { mobCloseNav(); mobSetView('list'); } calRender(); } // Patch selectFolder — called from app.js sidebar click handlers. // When a mail folder is clicked while contacts/calendar is showing, switch back to mail first. // Avoids infinite recursion by checking _currentView before doing anything. (function() { const _orig = window.selectFolder; window.selectFolder = function(folderId, folderName) { if (_currentView !== 'mail') { showMail(); // Give the DOM a tick to re-show the mail panels before loading setTimeout(function() { _orig && _orig(folderId, folderName); }, 10); return; } _orig && _orig(folderId, folderName); }; })(); // ======== CONTACTS ======== let _contacts = []; let _editingContactId = null; async function loadContacts() { const data = await api('GET', '/contacts'); _contacts = data || []; renderContacts(_contacts); } function renderContacts(list) { const el = document.getElementById('contacts-list'); if (!el) return; if (!list || list.length === 0) { el.innerHTML = `

No contacts yet. Click "+ New Contact" to add one.

`; return; } el.innerHTML = list.map(c => { const initials = (c.display_name || c.email || '?').split(' ').map(w => w[0]).join('').substring(0,2).toUpperCase(); const color = c.avatar_color || '#6b7280'; const meta = [c.email, c.company].filter(Boolean).join(' · '); return `
${esc(initials)}
${esc(c.display_name || c.email)}
${esc(meta)}
`; }).join(''); } function filterContacts(q) { if (!q) { renderContacts(_contacts); return; } const lower = q.toLowerCase(); renderContacts(_contacts.filter(c => (c.display_name||'').toLowerCase().includes(lower) || (c.email||'').toLowerCase().includes(lower) || (c.company||'').toLowerCase().includes(lower) )); } function composeToContact(email) { showMail(); setTimeout(() => { if (typeof openCompose === 'function') openCompose(); setTimeout(() => { if (typeof addTag === 'function') addTag('compose-to', email); }, 100); }, 50); } function openContactForm(id) { _editingContactId = id || null; const delBtn = document.getElementById('cf-delete-btn'); if (id) { document.getElementById('contact-modal-title').textContent = 'Edit Contact'; if (delBtn) delBtn.style.display = ''; const c = _contacts.find(x => x.id === id); if (c) { document.getElementById('cf-name').value = c.display_name || ''; document.getElementById('cf-email').value = c.email || ''; document.getElementById('cf-phone').value = c.phone || ''; document.getElementById('cf-company').value = c.company || ''; document.getElementById('cf-notes').value = c.notes || ''; } } else { document.getElementById('contact-modal-title').textContent = 'New Contact'; if (delBtn) delBtn.style.display = 'none'; ['cf-name','cf-email','cf-phone','cf-company','cf-notes'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); } openModal('contact-modal'); } async function saveContact() { const body = { display_name: document.getElementById('cf-name').value.trim(), email: document.getElementById('cf-email').value.trim(), phone: document.getElementById('cf-phone').value.trim(), company: document.getElementById('cf-company').value.trim(), notes: document.getElementById('cf-notes').value.trim(), }; if (!body.display_name && !body.email) { toast('Name or email is required','error'); return; } if (_editingContactId) { await api('PUT', `/contacts/${_editingContactId}`, body); } else { await api('POST', '/contacts', body); } closeModal('contact-modal'); await loadContacts(); toast(_editingContactId ? 'Contact updated' : 'Contact saved', 'success'); } async function deleteContact() { if (!_editingContactId) return; if (!confirm('Delete this contact?')) return; await api('DELETE', `/contacts/${_editingContactId}`); closeModal('contact-modal'); await loadContacts(); toast('Contact deleted', 'success'); } // ======== CALENDAR ======== const CAL = { view: 'month', cursor: new Date(), events: [], }; function calSetView(v) { CAL.view = v; document.getElementById('cal-btn-month')?.classList.toggle('active', v === 'month'); document.getElementById('cal-btn-week')?.classList.toggle('active', v === 'week'); calRender(); } function calNav(dir) { if (CAL.view === 'month') { CAL.cursor = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() + dir, 1); } else { CAL.cursor = new Date(CAL.cursor.getTime() + dir * 7 * 86400000); } calRender(); } function calGoToday() { CAL.cursor = new Date(); calRender(); } async function calRender() { const gridEl = document.getElementById('cal-grid'); if (!gridEl) return; let from, to; if (CAL.view === 'month') { from = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth(), 1); to = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() + 1, 0); from = new Date(from.getTime() - from.getDay() * 86400000); to = new Date(to.getTime() + (6 - to.getDay()) * 86400000); } else { const dow = CAL.cursor.getDay(); from = new Date(CAL.cursor.getTime() - dow * 86400000); to = new Date(from.getTime() + 6 * 86400000); } const fmt = d => d.toISOString().split('T')[0]; const data = await api('GET', `/calendar/events?from=${fmt(from)}&to=${fmt(to)}`); CAL.events = data || []; const months = ['January','February','March','April','May','June','July','August','September','October','November','December']; const titleEl = document.getElementById('cal-title'); if (CAL.view === 'month') { if (titleEl) titleEl.textContent = `${months[CAL.cursor.getMonth()]} ${CAL.cursor.getFullYear()}`; calRenderMonth(from, to); } else { if (titleEl) titleEl.textContent = `${months[from.getMonth()]} ${from.getDate()} – ${months[to.getMonth()]} ${to.getDate()}, ${to.getFullYear()}`; calRenderWeek(from); } } function calRenderMonth(from, to) { const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; const today = new Date(); today.setHours(0,0,0,0); let html = `
`; days.forEach(d => html += `
${d}
`); const cur = new Date(from); const curMonth = CAL.cursor.getMonth(); while (cur <= to) { const dateStr = cur.toISOString().split('T')[0]; const isToday = cur.getTime() === today.getTime(); const isOther = cur.getMonth() !== curMonth; const dayEvents = CAL.events.filter(e => e.start_time && e.start_time.startsWith(dateStr)); const shown = dayEvents.slice(0, 3); const more = dayEvents.length - 3; html += `
${cur.getDate()}
${shown.map(ev=>`
${esc(ev.title)}
`).join('')} ${more>0?`
+${more} more
`:''}
`; cur.setDate(cur.getDate() + 1); } html += `
`; document.getElementById('cal-grid').innerHTML = html; } function calRenderWeek(weekStart) { const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; const today = new Date(); today.setHours(0,0,0,0); let html = `
`; html += `
`; for (let i=0;i<7;i++) { const d = new Date(weekStart.getTime()+i*86400000); const isT = d.getTime()===today.getTime(); html += `
${days[d.getDay()]} ${d.getDate()}
`; } for (let h=0;h<24;h++) { const label = h===0?'12am':h<12?`${h}am`:h===12?'12pm':`${h-12}pm`; html += `
${label}
`; for (let i=0;i<7;i++) { const d = new Date(weekStart.getTime()+i*86400000); const dateStr = d.toISOString().split('T')[0]; const slotEvs = CAL.events.filter(ev => { if (!ev.start_time) return false; return ev.start_time.startsWith(dateStr) && parseInt((ev.start_time.split('T')[1]||'').split(':')[0]||'0') === h; }); const isT = d.getTime()===today.getTime(); html += `
${slotEvs.map(ev=>`
${esc(ev.title)}
`).join('')}
`; } } html += `
`; document.getElementById('cal-grid').innerHTML = html; } // ======== EVENT FORM ======== let _editingEventId = null; let _selectedEvColor = '#0078D4'; function selectEvColor(el) { _selectedEvColor = el.dataset.color; document.querySelectorAll('#ev-colors span').forEach(s => s.style.borderColor = 'transparent'); el.style.borderColor = 'white'; } function openEventForm(id, defaultStart) { _editingEventId = id || null; const delBtn = document.getElementById('ev-delete-btn'); _selectedEvColor = '#0078D4'; document.querySelectorAll('#ev-colors span').forEach((s,i) => s.style.borderColor = i===0?'white':'transparent'); if (id) { document.getElementById('event-modal-title').textContent = 'Edit Event'; if (delBtn) delBtn.style.display = ''; const ev = CAL.events.find(e => e.id === id); if (ev) { document.getElementById('ev-title').value = ev.title||''; document.getElementById('ev-start').value = (ev.start_time||'').replace(' ','T').substring(0,16); document.getElementById('ev-end').value = (ev.end_time||'').replace(' ','T').substring(0,16); document.getElementById('ev-allday').checked = !!ev.all_day; document.getElementById('ev-location').value = ev.location||''; document.getElementById('ev-desc').value = ev.description||''; _selectedEvColor = ev.color||'#0078D4'; document.querySelectorAll('#ev-colors span').forEach(s => { s.style.borderColor = s.dataset.color===_selectedEvColor ? 'white' : 'transparent'; }); } } else { document.getElementById('event-modal-title').textContent = 'New Event'; if (delBtn) delBtn.style.display = 'none'; document.getElementById('ev-title').value = ''; const start = defaultStart || new Date().toISOString().substring(0,16); document.getElementById('ev-start').value = start; const endDate = new Date(start); endDate.setHours(endDate.getHours()+1); document.getElementById('ev-end').value = endDate.toISOString().substring(0,16); document.getElementById('ev-allday').checked = false; document.getElementById('ev-location').value = ''; document.getElementById('ev-desc').value = ''; } openModal('event-modal'); } async function saveEvent() { const title = document.getElementById('ev-title').value.trim(); if (!title) { toast('Title is required','error'); return; } const body = { title, start_time: document.getElementById('ev-start').value.replace('T',' '), end_time: document.getElementById('ev-end').value.replace('T',' '), all_day: document.getElementById('ev-allday').checked, location: document.getElementById('ev-location').value.trim(), description:document.getElementById('ev-desc').value.trim(), color: _selectedEvColor, status: 'confirmed', }; if (_editingEventId) { await api('PUT', `/calendar/events/${_editingEventId}`, body); } else { await api('POST', '/calendar/events', body); } closeModal('event-modal'); await calRender(); toast(_editingEventId ? 'Event updated' : 'Event created', 'success'); } async function deleteEvent() { if (!_editingEventId) return; if (!confirm('Delete this event?')) return; await api('DELETE', `/calendar/events/${_editingEventId}`); closeModal('event-modal'); await calRender(); toast('Event deleted', 'success'); } // ======== CALDAV ======== async function showCalDAVSettings() { openModal('caldav-modal'); await loadCalDAVTokens(); } async function loadCalDAVTokens() { const tokens = await api('GET', '/caldav/tokens') || []; const el = document.getElementById('caldav-tokens-list'); if (!el) return; if (!tokens.length) { el.innerHTML = '

No tokens yet.

'; return; } el.innerHTML = tokens.map(t => { const url = `${location.origin}/caldav/${t.token}/calendar.ics`; return `
${esc(t.label)}
${url}
Created: ${t.created_at}${t.last_used?' · Last used: '+t.last_used:''}
`; }).join(''); } async function createCalDAVToken() { const label = document.getElementById('caldav-label').value.trim() || 'CalDAV token'; await api('POST', '/caldav/tokens', { label }); document.getElementById('caldav-label').value = ''; await loadCalDAVTokens(); toast('Token created', 'success'); } async function revokeCalDAVToken(id) { if (!confirm('Revoke this token?')) return; await api('DELETE', `/caldav/tokens/${id}`); await loadCalDAVTokens(); toast('Token revoked', 'success'); } function copyCalDAVUrl(url) { navigator.clipboard.writeText(url).then(() => toast('URL copied','success')); }