Outlook changes

This commit is contained in:
2026-09-16 00:32:29 +02:00
parent 460fa967ee
commit 45a883319a
3 changed files with 654 additions and 104 deletions
+272 -87
View File
@@ -14,15 +14,22 @@ $isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$db = get_db_connection();
$userBoards = fociz_get_user_boards_with_lanes($db, $userId, $isAdmin);
// Haal initiële inbox data op (met 45s cache, tenzij ?refresh=1)
// Haal alle beschikbare mailmappen op (exclusief SPAM/Junk, Prullenbak/Deleted, Outbox, Drafts)
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
$foldersData = microsoft_fetch_mail_folders($userId, $forceRefresh);
$mailFolders = $foldersData['folders'] ?? [];
$totalUnreadCount = (int)($foldersData['total_unread'] ?? 0);
// Haal initiële inbox data op (met 45s cache, tenzij ?refresh=1)
$inboxData = microsoft_fetch_inbox_messages($userId, 30, false, $forceRefresh);
$messages = $inboxData['messages'] ?? [];
$isDemo = !empty($inboxData['is_demo']);
$unreadCount = (int)($inboxData['unread_count'] ?? 0);
$warning = $inboxData['warning'] ?? '';
$isDemo = !empty($inboxData['is_demo']) || !empty($foldersData['is_demo']);
$warning = $inboxData['warning'] ?? ($foldersData['warning'] ?? '');
$needsReconsent = !empty($inboxData['needs_reconsent']);
// Bereken actuele ongelezen aantal: neem het hoogste van total_unread over mappen en inbox unread_count
$unreadCount = max($totalUnreadCount, (int)($inboxData['unread_count'] ?? 0));
// Controleer status
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
$stmtInt->execute([$userId]);
@@ -31,6 +38,7 @@ $isOutlookConfigured = !empty($outlookInt['access_token']) && empty($outlookInt[
$isLiveMail = $isOutlookConfigured && !$isDemo;
$jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$jsonFolders = json_encode($mailFolders, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
?>
<div class="max-w-7xl mx-auto space-y-6">
@@ -143,15 +151,15 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
<!-- Twee-kolommen Mail Interface -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 min-h-[600px]">
<!-- Linker kolom: Berichtenlijst (5 kolommen) -->
<!-- Linker kolom: Berichtenlijst & Mappen (5 kolommen) -->
<div id="mailListColumn" class="lg:col-span-5 fociz-card p-3 sm:p-4 flex flex-col h-[720px] overflow-hidden">
<div class="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 px-2 py-1 mb-2 flex items-center justify-between">
<span>Inbox Berichten (<span id="visibleMailCount"><?= count($messages) ?></span>)</span>
<span>Inbox &amp; Mappen (<span id="visibleMailCount"><?= count($messages) ?></span>)</span>
<span class="text-[10px] text-slate-400 font-normal">Sorteer: Nieuwste eerst</span>
</div>
<div class="flex-1 overflow-y-auto space-y-1.5 pr-1" id="mailListContainer">
<!-- Wordt dynamisch gerenderd via JS -->
<div class="flex-1 overflow-y-auto space-y-2.5 pr-1 custom-scrollbar" id="mailFoldersContainer">
<!-- Wordt dynamisch gerenderd via JS (Inbox open, overige mappen inklapbaar) -->
</div>
</div>
@@ -527,7 +535,12 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
</div>
<script>
let allMessages = <?= $jsonMessages ?>;
let mailFolders = <?= $jsonFolders ?>;
let folderMessagesMap = {
'inbox': <?= $jsonMessages ?>
};
let expandedFolders = new Set(['inbox']); // Inbox is standaard geopend, overige mappen ingeklapt
let loadingFolders = new Set();
let activeFilter = 'all'; // 'all' | 'unread'
let selectedMessage = null;
@@ -537,101 +550,234 @@ function setMailFilter(filter) {
const btnUnread = document.getElementById('filterBtn_unread');
if (filter === 'all') {
btnAll.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition";
btnUnread.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition";
btnAll.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition cursor-pointer";
btnUnread.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition cursor-pointer";
} else {
btnUnread.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition";
btnAll.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition";
}
btnUnread.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition cursor-pointer";
btnAll.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition cursor-pointer";
applyMailSearch();
}
function applyMailSearch() {
const q = (document.getElementById('mailSearchInput')?.value || '').toLowerCase().trim();
let filtered = allMessages;
if (activeFilter === 'unread') {
filtered = filtered.filter(m => !m.is_read);
}
if (q) {
filtered = filtered.filter(m => {
const sub = (m.subject || '').toLowerCase();
const from = (m.from_name || '').toLowerCase() + ' ' + (m.from_email || '').toLowerCase();
const prev = (m.body_preview || '').toLowerCase();
return sub.includes(q) || from.includes(q) || prev.includes(q);
// Bij ongelezen filter: klap automatisch alle mappen met ongelezen mails open!
mailFolders.forEach(f => {
const msgs = folderMessagesMap[f.id];
const hasUnread = msgs ? msgs.some(m => !m.is_read) : ((f.unread_count || 0) > 0);
if (hasUnread) {
expandedFolders.add(f.id);
if (!folderMessagesMap[f.id]) {
loadFolderMessagesInBackground(f.id);
}
}
});
}
renderMailList(filtered);
renderFolders();
}
function renderMailList(list) {
const container = document.getElementById('mailListContainer');
const countEl = document.getElementById('visibleMailCount');
if (countEl) countEl.innerText = list.length;
function applyMailSearch() {
renderFolders();
}
if (!list || list.length === 0) {
container.innerHTML = `
<div class="text-center py-12 text-slate-400 text-xs">
<i data-lucide="inbox" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
<p>Geen berichten gevonden.</p>
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
async function loadFolderMessagesInBackground(folderId) {
if (loadingFolders.has(folderId) || folderMessagesMap[folderId]) return;
loadingFolders.add(folderId);
renderFolders();
try {
const res = await fetch(`/api/mail.php?action=get_folder_messages&folder_id=${encodeURIComponent(folderId)}&limit=30`);
const data = await res.json();
if (data.success && Array.isArray(data.messages)) {
folderMessagesMap[folderId] = data.messages;
const fObj = mailFolders.find(f => f.id === folderId);
if (fObj && typeof data.unread_count === 'number') {
fObj.unread_count = data.unread_count;
}
}
} catch (e) {
console.warn('Fout bij ophalen berichten voor map ' + folderId, e);
} finally {
loadingFolders.delete(folderId);
updateTotalCounters();
renderFolders();
}
}
async function toggleFolder(folderId) {
if (expandedFolders.has(folderId)) {
expandedFolders.delete(folderId);
renderFolders();
return;
}
let html = '';
list.forEach(m => {
const isSelected = selectedMessage && selectedMessage.id === m.id;
const isUnread = !m.is_read;
const initials = (m.from_name || 'NL').split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
expandedFolders.add(folderId);
let cardClass = isSelected
? 'bg-sky-50 dark:bg-sky-950/40 border-sky-400 dark:border-sky-700 shadow-xs'
: (isUnread ? 'bg-white dark:bg-[#111624] border-slate-200 dark:border-slate-800 hover:border-slate-300 font-semibold' : 'bg-slate-50/60 dark:bg-[#0c0f17]/60 border-slate-100 dark:border-slate-800/60 text-slate-600 dark:text-slate-400');
if (!folderMessagesMap[folderId]) {
loadingFolders.add(folderId);
renderFolders();
try {
const res = await fetch(`/api/mail.php?action=get_folder_messages&folder_id=${encodeURIComponent(folderId)}&limit=30`);
const data = await res.json();
if (data.success && Array.isArray(data.messages)) {
folderMessagesMap[folderId] = data.messages;
const fObj = mailFolders.find(f => f.id === folderId);
if (fObj && typeof data.unread_count === 'number') {
fObj.unread_count = data.unread_count;
}
} else {
folderMessagesMap[folderId] = [];
}
} catch (e) {
folderMessagesMap[folderId] = [];
} finally {
loadingFolders.delete(folderId);
updateTotalCounters();
renderFolders();
}
} else {
renderFolders();
}
}
function renderFolders() {
const container = document.getElementById('mailFoldersContainer');
if (!container) return;
const q = (document.getElementById('mailSearchInput')?.value || '').toLowerCase().trim();
let totalVisible = 0;
let html = '';
mailFolders.forEach(folder => {
const isInbox = (folder.id === 'inbox' || folder.is_inbox);
const isExpanded = expandedFolders.has(folder.id);
const isLoading = loadingFolders.has(folder.id);
const msgs = folderMessagesMap[folder.id] || [];
let unreadCount = 0;
if (folderMessagesMap[folder.id]) {
unreadCount = msgs.filter(m => !m.is_read).length;
} else {
unreadCount = folder.unread_count || 0;
}
let filteredMsgs = msgs;
if (activeFilter === 'unread') {
filteredMsgs = filteredMsgs.filter(m => !m.is_read);
}
if (q) {
filteredMsgs = filteredMsgs.filter(m => {
const sub = (m.subject || '').toLowerCase();
const from = (m.from_name || '').toLowerCase() + ' ' + (m.from_email || '').toLowerCase();
const prev = (m.body_preview || '').toLowerCase();
return sub.includes(q) || from.includes(q) || prev.includes(q);
});
}
if (isExpanded) {
totalVisible += filteredMsgs.length;
}
const folderTotal = folder.total_count || msgs.length;
html += `
<div onclick="selectMail('${m.id}')" class="group relative p-3 rounded-2xl border transition cursor-pointer ${cardClass}">
<div class="flex items-start justify-between gap-2">
<div class="flex items-center space-x-2 truncate">
<div class="w-6 h-6 rounded-lg ${isUnread ? 'bg-sky-600 text-white' : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400'} text-[10px] font-bold flex items-center justify-center flex-shrink-0">
${initials}
<div class="rounded-2xl border border-slate-200/80 dark:border-slate-800/80 bg-white/70 dark:bg-slate-900/40 overflow-hidden shadow-2xs">
<!-- Folder Header Accordion Knop -->
<button type="button" onclick="toggleFolder('${folder.id}')" class="w-full flex items-center justify-between p-3 select-none transition cursor-pointer ${isInbox ? 'bg-slate-100/90 dark:bg-slate-800/80' : 'bg-slate-50/80 dark:bg-slate-800/40 hover:bg-slate-100 dark:hover:bg-slate-800/70'}">
<div class="flex items-center space-x-2.5 min-w-0">
<div class="w-7 h-7 rounded-xl ${isInbox ? 'bg-sky-600 text-white shadow-xs' : 'bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300'} flex items-center justify-center flex-shrink-0">
<i data-lucide="${isInbox ? 'inbox' : 'folder'}" class="w-3.5 h-3.5"></i>
</div>
<div class="flex items-center space-x-1.5 truncate">
<span class="text-xs font-bold text-slate-900 dark:text-white truncate">${escapeHtml(folder.name)}</span>
<span class="text-[10px] text-slate-400 font-semibold">(${folderTotal})</span>
</div>
<span class="text-xs ${isUnread ? 'font-black text-slate-900 dark:text-white' : 'font-medium text-slate-700 dark:text-slate-300'} truncate">
${escapeHtml(m.from_name)}
</span>
</div>
<div class="flex items-center space-x-1.5 flex-shrink-0">
${isUnread ? '<span class="w-2 h-2 rounded-full bg-sky-500"></span>' : ''}
${m.has_attachments ? '<i data-lucide="paperclip" class="w-3 h-3 text-slate-400"></i>' : ''}
<span class="text-[10px] text-slate-400 font-medium">${escapeHtml(m.received_human || '')}</span>
<div class="flex items-center space-x-2 flex-shrink-0 ml-2">
${unreadCount > 0 ? `<span class="px-2 py-0.5 rounded-full text-[10px] font-black bg-sky-100 dark:bg-sky-950/70 text-sky-700 dark:text-sky-300 ring-1 ring-sky-500/30">${unreadCount} ongelezen</span>` : ''}
<i data-lucide="chevron-down" class="w-4 h-4 text-slate-400 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}"></i>
</div>
</div>
</button>
<div class="mt-1 text-xs ${isUnread ? 'font-bold text-slate-900 dark:text-white' : 'text-slate-800 dark:text-slate-200'} truncate">
${escapeHtml(m.subject)}
</div>
<div class="mt-0.5 text-[11px] text-slate-400 dark:text-slate-500 line-clamp-1 leading-snug">
${escapeHtml(m.body_preview || '')}
<!-- Folder Berichten Inhoud -->
<div class="${isExpanded ? '' : 'hidden'} p-2 space-y-1.5 bg-slate-50/40 dark:bg-[#0c0f17]/30 border-t border-slate-100 dark:border-slate-800/80">
${isLoading ? `
<div class="p-4 text-center text-xs text-slate-400 flex items-center justify-center space-x-1.5">
<i data-lucide="loader-2" class="w-4 h-4 animate-spin text-sky-500"></i>
<span>Berichten ophalen uit Outlook...</span>
</div>
` : (filteredMsgs.length === 0 ? `
<div class="py-4 text-center text-slate-400 text-xs italic">
${activeFilter === 'unread' ? 'Geen ongelezen berichten in deze map.' : 'Geen berichten gevonden.'}
</div>
` : filteredMsgs.map(m => renderMessageCardHtml(m)).join(''))}
</div>
</div>
`;
});
container.innerHTML = html;
const countEl = document.getElementById('visibleMailCount');
if (countEl) countEl.innerText = totalVisible;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function renderMessageCardHtml(m) {
const isSelected = selectedMessage && selectedMessage.id === m.id;
const isUnread = !m.is_read;
const initials = (m.from_name || 'NL').split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
let cardClass = isSelected
? 'bg-sky-50 dark:bg-sky-950/40 border-sky-400 dark:border-sky-700 shadow-xs'
: (isUnread ? 'bg-white dark:bg-[#111624] border-slate-200 dark:border-slate-800 hover:border-slate-300 font-semibold' : 'bg-slate-50/60 dark:bg-[#0c0f17]/60 border-slate-100 dark:border-slate-800/60 text-slate-600 dark:text-slate-400');
return `
<div onclick="selectMail('${m.id}')" class="group relative p-3 rounded-2xl border transition cursor-pointer ${cardClass}">
<div class="flex items-start justify-between gap-2">
<div class="flex items-center space-x-2 truncate">
<div class="w-6 h-6 rounded-lg ${isUnread ? 'bg-sky-600 text-white' : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400'} text-[10px] font-bold flex items-center justify-center flex-shrink-0">
${initials}
</div>
<span class="text-xs ${isUnread ? 'font-black text-slate-900 dark:text-white' : 'font-medium text-slate-700 dark:text-slate-300'} truncate">
${escapeHtml(m.from_name)}
</span>
</div>
<div class="flex items-center space-x-1.5 flex-shrink-0">
${isUnread ? '<span class="w-2 h-2 rounded-full bg-sky-500"></span>' : ''}
${m.has_attachments ? '<i data-lucide="paperclip" class="w-3 h-3 text-slate-400"></i>' : ''}
<span class="text-[10px] text-slate-400 font-medium">${escapeHtml(m.received_human || '')}</span>
</div>
</div>
<div class="mt-1 text-xs ${isUnread ? 'font-bold text-slate-900 dark:text-white' : 'text-slate-800 dark:text-slate-200'} truncate">
${escapeHtml(m.subject)}
</div>
<div class="mt-0.5 text-[11px] text-slate-400 dark:text-slate-500 line-clamp-1 leading-snug">
${escapeHtml(m.body_preview || '')}
</div>
</div>
`;
}
function renderMailList() {
renderFolders();
}
async function selectMail(id) {
const found = allMessages.find(m => m.id === id);
let found = null;
let foundFolderId = null;
for (const fId in folderMessagesMap) {
const m = (folderMessagesMap[fId] || []).find(x => x.id === id);
if (m) {
found = m;
foundFolderId = fId;
break;
}
}
if (!found) return;
selectedMessage = found;
renderMailList(allMessages); // Update actieve styling
renderFolders(); // Update actieve styling
// Toon detail container
document.getElementById('noMailSelectedState').classList.add('hidden');
@@ -659,9 +805,13 @@ async function selectMail(id) {
const data = await res.json();
if (data.success && data.message) {
renderMailBodyContent(bodyEl, data.message.body_html, data.message.body_text || found.body_preview);
found.is_read = true;
updateUnreadCount();
renderMailList(allMessages);
if (!found.is_read) {
found.is_read = true;
const fObj = mailFolders.find(f => f.id === foundFolderId);
if (fObj && fObj.unread_count > 0) fObj.unread_count--;
updateTotalCounters();
renderFolders();
}
} else {
bodyEl.innerHTML = `<div class="p-4 rounded-xl bg-red-50 text-red-700 text-xs">${data.error || 'Kon bericht niet inladen.'}</div>`;
}
@@ -784,9 +934,29 @@ function renderMailBodyContent(container, htmlContent, plainContent) {
}
function updateUnreadCount() {
const unread = allMessages.filter(m => !m.is_read).length;
const el = document.getElementById('countUnread');
if (el) el.innerText = unread;
updateTotalCounters();
}
function updateTotalCounters() {
let totalUnread = 0;
let totalAll = 0;
mailFolders.forEach(f => {
const msgs = folderMessagesMap[f.id];
if (msgs) {
totalUnread += msgs.filter(m => !m.is_read).length;
totalAll += msgs.length;
} else {
totalUnread += (f.unread_count || 0);
totalAll += (f.total_count || 0);
}
});
const elUnread = document.getElementById('countUnread');
if (elUnread) elUnread.innerText = totalUnread;
const elAll = document.getElementById('countAll');
if (elAll) elAll.innerText = totalAll;
}
function closeMobileDetail() {
@@ -1182,17 +1352,30 @@ async function refreshMailbox() {
if (icon) icon.classList.add('animate-spin');
try {
const res = await fetch('/api/mail.php?action=get_inbox&refresh=1');
const res = await fetch('/api/mail.php?action=get_folders&refresh=1');
const data = await res.json();
if (data.success && data.messages) {
allMessages = data.messages;
applyMailSearch();
updateUnreadCount();
if (data.success && Array.isArray(data.folders)) {
mailFolders = data.folders;
}
const promises = [...expandedFolders].map(async (fId) => {
try {
const r = await fetch(`/api/mail.php?action=get_folder_messages&folder_id=${encodeURIComponent(fId)}&limit=30&refresh=1`);
const d = await r.json();
if (d.success && Array.isArray(d.messages)) {
folderMessagesMap[fId] = d.messages;
const fObj = mailFolders.find(f => f.id === fId);
if (fObj && typeof d.unread_count === 'number') fObj.unread_count = d.unread_count;
}
} catch (e) {}
});
await Promise.all(promises);
} catch(err) {
console.warn('Mail refresh error:', err);
} finally {
if (icon) icon.classList.remove('animate-spin');
updateTotalCounters();
renderFolders();
}
}
@@ -1228,9 +1411,11 @@ document.addEventListener('keydown', function(e) {
});
document.addEventListener('DOMContentLoaded', () => {
applyMailSearch();
if (allMessages && allMessages.length > 0) {
selectMail(allMessages[0].id);
updateTotalCounters();
renderFolders();
const inboxMsgs = folderMessagesMap['inbox'] || [];
if (inboxMsgs.length > 0) {
selectMail(inboxMsgs[0].id);
}
// Luister naar live websocket/polling event voor nieuwe inkomende e-mails