// ── 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 ``;
}).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 += ``);
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 += ``;
}
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'));
}