// ============================================================ // MAIL MANAGEMENT // ============================================================ let mailsData = []; let mailFormsData = []; let mailFormFilter = ''; let mailSortField = 'received'; let mailSortDir = 'desc'; async function loadMails() { try { if (!currentSettings) { await loadSettings(); } const response = await fetch('api.php?action=load-mails'); const result = await response.json(); if (result.success) { if (Array.isArray(result.data)) { mailsData = result.data; mailFormsData = []; } else { mailsData = result.data.mails || []; mailFormsData = result.data.forms || []; } populateMailFormFilter(); renderMails(); updateMailBadge(); updateMailConfigBanner(); updateMailStorageBanner(); } else { showToast(result.message, 'error'); } } catch (error) { showToast(t('toast.error_loading', {message: error.message}), 'error'); } } function renderMails() { const tbody = document.getElementById('mailsList'); if (!tbody) return; tbody.innerHTML = ''; const visibleMails = getFilteredMails(); if (!visibleMails || visibleMails.length === 0) { rjRenderEmptyRow(tbody, 6, t('mails.no_messages')); renderAdminListFooter('mailsListFooter', 'mails', 0, getDashboardPageSize(), renderMails, 'mailsListFooterTop'); updateMailBulkActions(); updateMailSortIndicators(); return; } const sortedMails = getSortedMails(visibleMails); const pageSize = getDashboardPageSize(); const paged = pageSlice(sortedMails, 'mails', pageSize); renderAdminListFooter('mailsListFooter', 'mails', sortedMails.length, pageSize, renderMails, 'mailsListFooterTop'); paged.items.forEach(mail => { const date = new Date(mail.timestamp); const dateStr = rjFormatDate(date, 'date'); const timeStr = rjFormatDate(date, 'time'); const isUnread = !mail.read; const isStarred = !!mail.starred; const formLabel = mail.formLabel || mail.formId || t('forms.contact_default'); const tr = document.createElement('tr'); tr.className = 'page-list-row'; if (isUnread) tr.classList.add('mail-row-unread'); const tdRead = document.createElement('td'); tdRead.className = 'mail-cell-flag'; const readBtn = document.createElement('button'); readBtn.type = 'button'; readBtn.className = 'mail-flag-btn mail-read-toggle' + (isUnread ? ' is-active' : ''); readBtn.title = isUnread ? t('mails.mark_read') : t('mails.mark_unread'); readBtn.setAttribute('aria-label', readBtn.title); readBtn.innerHTML = ``; readBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); setMailFlags(mail.id, { read: isUnread }); }; tdRead.appendChild(readBtn); const tdStar = document.createElement('td'); tdStar.className = 'mail-cell-flag'; const starBtn = document.createElement('button'); starBtn.type = 'button'; starBtn.className = 'mail-flag-btn mail-star-toggle' + (isStarred ? ' is-active' : ''); starBtn.title = isStarred ? t('mails.unstar') : t('mails.star'); starBtn.setAttribute('aria-label', starBtn.title); starBtn.textContent = isStarred ? '★' : '☆'; starBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); setMailFlags(mail.id, { starred: !isStarred }); }; tdStar.appendChild(starBtn); const tdForm = document.createElement('td'); tdForm.className = 'mail-cell-form'; tdForm.textContent = formLabel; const tdFrom = document.createElement('td'); tdFrom.className = 'page-list-cell-title'; const fromLink = document.createElement('a'); fromLink.href = '#'; fromLink.className = 'page-list-title-link'; fromLink.textContent = mail.name; fromLink.onclick = (e) => { e.preventDefault(); openMailDetail(mail.id); }; tdFrom.appendChild(fromLink); const actions = document.createElement('div'); actions.className = 'page-list-row-actions'; const viewLink = document.createElement('a'); viewLink.href = '#'; viewLink.className = 'page-list-row-action'; viewLink.innerHTML = icon('eye', 12, '2') + ' ' + t('pages.view'); viewLink.onclick = (e) => { e.preventDefault(); openMailDetail(mail.id); }; actions.appendChild(viewLink); const sep = document.createElement('span'); sep.className = 'page-list-row-action-sep'; sep.textContent = '|'; actions.appendChild(sep); const trashLink = document.createElement('a'); trashLink.href = '#'; trashLink.className = 'page-list-row-action page-list-row-action--danger'; trashLink.innerHTML = icon('trash', 12, '2') + ' ' + t('btn.delete'); trashLink.onclick = (e) => { e.preventDefault(); deleteMail(mail.id); }; actions.appendChild(trashLink); tdFrom.appendChild(actions); const tdSubject = document.createElement('td'); tdSubject.textContent = mail.occasion || ''; const tdDate = document.createElement('td'); tdDate.className = 'page-list-cell-date'; tdDate.textContent = `${dateStr} ${timeStr}`; tr.appendChild(tdRead); tr.appendChild(tdStar); tr.appendChild(tdForm); tr.appendChild(tdFrom); tr.appendChild(tdSubject); tr.appendChild(tdDate); tbody.appendChild(tr); }); updateMailBulkActions(); updateMailSortIndicators(); } function populateMailFormFilter() { const select = document.getElementById('mailFormFilter'); if (!select) return; const current = mailFormFilter; const byId = new Map(); (mailFormsData || []).forEach(form => { if (form.id) byId.set(form.id, form.label || form.id); }); (mailsData || []).forEach(mail => { const id = mail.formId || 'contact'; if (!byId.has(id)) byId.set(id, mail.formLabel || id); }); select.innerHTML = ``; Array.from(byId.entries()) .sort((a, b) => String(a[1]).localeCompare(String(b[1]), undefined, { sensitivity: 'base' })) .forEach(([id, label]) => { const option = document.createElement('option'); option.value = id; option.textContent = label; select.appendChild(option); }); select.value = byId.has(current) ? current : ''; mailFormFilter = select.value; } function setMailFormFilter(value) { mailFormFilter = value || ''; dashboardListPages.mails = 1; renderMails(); } function getFilteredMails() { const list = Array.isArray(mailsData) ? mailsData : []; if (!mailFormFilter) return list; return list.filter(mail => (mail.formId || 'contact') === mailFormFilter); } function getSortedMails(list) { return [...(list || mailsData)].sort((a, b) => { let cmp = 0; if (mailSortField === 'read') { cmp = Number(!!a.read) - Number(!!b.read); } else if (mailSortField === 'starred') { cmp = Number(!!a.starred) - Number(!!b.starred); } else if (mailSortField === 'form') { cmp = String(a.formLabel || a.formId || '').localeCompare(String(b.formLabel || b.formId || ''), undefined, { sensitivity: 'base' }); } else if (mailSortField === 'from') { cmp = String(a.name || '').localeCompare(String(b.name || ''), undefined, { sensitivity: 'base' }); } else if (mailSortField === 'subject') { cmp = String(a.occasion || '').localeCompare(String(b.occasion || ''), undefined, { sensitivity: 'base' }); } else { cmp = new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime(); } return mailSortDir === 'asc' ? cmp : -cmp; }); } function sortMails(field) { if (mailSortField === field) { mailSortDir = mailSortDir === 'asc' ? 'desc' : 'asc'; } else { mailSortField = field; mailSortDir = field === 'read' || field === 'from' || field === 'subject' || field === 'form' ? 'asc' : 'desc'; } dashboardListPages.mails = 1; renderMails(); } function updateMailSortIndicators() { document.querySelectorAll('#mailsListTable .page-list-sortable').forEach(th => { const iconEl = th.querySelector('.page-list-sort-icon'); if (!iconEl) return; if (th.dataset.mailSort === mailSortField) { th.classList.add('sorted'); iconEl.textContent = mailSortDir === 'asc' ? '▲' : '▼'; } else { th.classList.remove('sorted'); iconEl.textContent = ''; } }); } function updateMailBadge() { const unreadCount = mailsData.filter(m => !m.read).length; const badge = document.getElementById('mailBadge'); if (!badge) return; if (unreadCount > 0) { badge.textContent = unreadCount; badge.classList.remove('mail-badge--hidden'); } else { badge.classList.add('mail-badge--hidden'); } } function updateMailBulkActions() { const btn = document.getElementById('deleteReadMailsBtn'); if (!btn) return; const readCount = (mailsData || []).filter(m => m.read).length; btn.disabled = readCount === 0; btn.textContent = readCount > 0 ? t('mails.delete_read_count', {count: readCount}) : t('mails.delete_read'); } function updateMailConfigBanner() { const banner = document.getElementById('mailConfigBanner'); if (!banner) return; const title = document.getElementById('mailConfigBannerTitle'); const text = document.getElementById('mailConfigBannerText'); const email = (currentSettings && currentSettings.email) || {}; const method = email.method || 'inactive'; let titleText = ''; let bodyText = ''; if (method === 'inactive') { titleText = t('mails.email_inactive_title'); bodyText = t('mails.email_inactive_text'); } else if (!email.recipientEmail) { titleText = t('mails.email_recipient_missing_title'); bodyText = t('mails.email_recipient_missing_text'); } if (!titleText) { banner.hidden = true; return; } title.textContent = titleText; text.textContent = bodyText; banner.hidden = false; } // This list only shows messages stored locally. Warn when no form that is switched on stores them. function updateMailStorageBanner() { const banner = document.getElementById('mailStorageBanner'); if (!banner) return; const forms = mailFormsData || []; const enabled = forms.filter(form => form.enabled); let key = ''; if (!enabled.length) key = 'forms_disabled'; else if (!enabled.some(form => form.store)) key = 'storage_off'; if (!key) { banner.hidden = true; return; } document.getElementById('mailStorageBannerTitle').textContent = t('mails.' + key + '_title'); document.getElementById('mailStorageBannerText').textContent = t('mails.' + key + '_text'); banner.hidden = false; } function openFormsSettings() { switchTab('settings'); const formsTab = document.querySelector('[data-settings-tab="forms"]'); if (formsTab) formsTab.click(); } function openEmailSettings() { switchTab('settings'); const emailTab = document.querySelector('[data-settings-tab="email"]'); if (emailTab) emailTab.click(); } async function loadUnreadCount() { try { const response = await fetch('api.php?action=unread-mail-count'); const result = await response.json(); if (result.success) { const badge = document.getElementById('mailBadge'); if (!badge) return; if (result.data.count > 0) { badge.textContent = result.data.count; badge.classList.remove('mail-badge--hidden'); } } } catch (error) { // Badge is non-critical; ignore transient fetch aborts during navigation/tests. } } function openMailDetail(mailId) { const mail = mailsData.find(m => m.id === mailId); if (!mail) return; const date = new Date(mail.timestamp); const dateStr = rjFormatDate(date, 'date'); const timeStr = rjFormatDate(date, 'time'); const formLabel = mail.formLabel || mail.formId || t('forms.contact_default'); const fields = Array.isArray(mail.fields) ? mail.fields.filter(field => field && field.value !== '') : []; const fieldsHtml = fields.length ? `
${fields.map(field => `
${escapeHtml(field.label || field.key || '')} ${escapeHtml(field.value || '').replace(/\n/g, '
')}
`).join('')}
` : ''; document.getElementById('mailDetailTitle').textContent = mail.occasion; document.getElementById('mailDetailContent').innerHTML = `
${escapeHtml(formLabel)}
${dateStr} ${timeStr}
${escapeHtml(mail.name)}
${escapeHtml(mail.email)}
${mail.phone ? `
${escapeHtml(mail.phone)}
` : ''} ${mail.date ? `
${rjFormatDate(mail.date, 'date')}
` : ''}
${escapeHtml(mail.message).replace(/\n/g, '
')}
${fieldsHtml} `; document.getElementById('mailDeleteBtn').onclick = () => deleteMail(mailId); document.getElementById('mailsListView').style.display = 'none'; document.getElementById('mailDetailView').style.display = ''; if (!mail.read) { markMailRead(mailId); } } function closeMailDetail() { document.getElementById('mailDetailView').style.display = 'none'; document.getElementById('mailsListView').style.display = ''; } async function markMailRead(mailId) { return setMailFlags(mailId, { read: true }, { silent: true }); } async function setMailFlags(mailId, flags, options = {}) { try { const formData = new FormData(); formData.append('action', 'update-mail-flags'); formData.append('mail_id', mailId); formData.append('csrf_token', CSRF_TOKEN); Object.entries(flags).forEach(([key, value]) => { formData.append(key, value ? '1' : '0'); }); const response = await fetch('api.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { const mail = mailsData.find(m => m.id === mailId); if (mail) { Object.assign(mail, flags); } renderMails(); updateMailBadge(); } else if (!options.silent) { showToast(result.message, 'error'); } } catch (error) { if (options.silent) { console.error('Error updating mail flags:', error); } else { showToast(t('toast.error_generic', {message: error.message}), 'error'); } } } async function markAllMailsRead() { try { const formData = new FormData(); formData.append('action', 'mark-all-mails-read'); formData.append('csrf_token', CSRF_TOKEN); const response = await fetch('api.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { mailsData.forEach(m => m.read = true); renderMails(); updateMailBadge(); showToast(t('toast.all_read'), 'success'); } else { showToast(result.message, 'error'); } } catch (error) { showToast(t('toast.error_generic', {message: error.message}), 'error'); } } function deleteReadMails() { const readCount = (mailsData || []).filter(m => m.read).length; if (readCount === 0) return; showModal(t('modal.delete_read_messages'), t('modal.delete_read_messages_confirm', {count: readCount}), async () => { try { const formData = new FormData(); formData.append('action', 'delete-read-mails'); formData.append('csrf_token', CSRF_TOKEN); const response = await fetch('api.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { mailsData = mailsData.filter(m => !m.read); renderMails(); updateMailBadge(); closeModal(); showToast(t('toast.read_messages_deleted', {count: result.data?.deleted || readCount}), 'success'); } else { showToast(result.message, 'error'); } } catch (error) { showToast(t('toast.error_generic', {message: error.message}), 'error'); } }); } function deleteMail(mailId) { showModal(t('modal.delete_message'), t('modal.delete_message_confirm'), async () => { try { const formData = new FormData(); formData.append('action', 'delete-mail'); formData.append('mail_id', mailId); formData.append('csrf_token', CSRF_TOKEN); const response = await fetch('api.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { mailsData = mailsData.filter(m => m.id !== mailId); renderMails(); updateMailBadge(); closeMailDetail(); closeModal(); showToast(t('toast.message_deleted'), 'success'); } else { showToast(result.message, 'error'); } } catch (error) { showToast(t('toast.error_generic', {message: error.message}), 'error'); } }); } // Load badge on startup loadUnreadCount();