Files
Fociz/mail.php
T
2026-09-16 12:22:27 +02:00

1446 lines
76 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Fociz Microsoft Outlook 365 E-mail Hub
* Volwaardige inbox, berichtdetail, beantwoorden, doorsturen en direct inplannen in Fociz.
*/
$pageTitle = 'Fociz — Microsoft Outlook Mail';
require_once __DIR__ . '/includes/header.php';
require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/boards.php';
$userId = (int)($currentUser['id'] ?? 0);
$isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$db = get_db_connection();
$userBoards = fociz_get_user_boards_with_lanes($db, $userId, $isAdmin);
// Geef direct het PHP sessieslot vrij zodat achtergrondverzoeken en paginanavigatie nooit blokkeren
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
// 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']) || !empty($foldersData['is_demo']);
$warning = $inboxData['warning'] ?? ($foldersData['warning'] ?? ($foldersData['error'] ?? ''));
$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]);
$outlookInt = $stmtInt->fetch();
$isOutlookConfigured = !empty($outlookInt['access_token']) && empty($outlookInt['is_demo_mode']);
$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">
<?php if (!empty($_GET['success'])): ?>
<div class="p-4 rounded-2xl bg-emerald-50 dark:bg-emerald-950/40 border border-emerald-200 dark:border-emerald-800/60 flex items-center space-x-2.5 text-xs text-emerald-800 dark:text-emerald-200 shadow-2xs">
<i data-lucide="check-circle-2" class="w-4 h-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i>
<span><?= htmlspecialchars($_GET['success']) ?></span>
</div>
<?php elseif (!empty($_GET['error'])): ?>
<div class="p-4 rounded-2xl bg-rose-50 dark:bg-rose-950/40 border border-rose-200 dark:border-rose-800/60 flex items-center space-x-2.5 text-xs text-rose-800 dark:text-rose-200 shadow-2xs">
<i data-lucide="alert-circle" class="w-4 h-4 text-rose-600 dark:text-rose-400 flex-shrink-0"></i>
<span><?= htmlspecialchars($_GET['error']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($warning)): ?>
<div class="p-4 rounded-2xl bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800/60 flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs text-amber-800 dark:text-amber-200 shadow-2xs">
<div class="flex items-center space-x-2.5">
<i data-lucide="alert-triangle" class="w-4 h-4 text-amber-600 dark:text-amber-400 flex-shrink-0"></i>
<span><?= htmlspecialchars($warning) ?></span>
</div>
<?php if (!empty($needsReconsent)): ?>
<a href="/auth/oauth_microsoft.php?reconsent=1&return_to=/mail.php" class="px-3 py-1.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white font-bold text-xs shadow-xs transition whitespace-nowrap flex-shrink-0 text-center">Machtigingen Bijwerken</a>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Top Banner: Status & Snelle Acties -->
<div class="fociz-card p-6 sm:p-7 relative overflow-hidden">
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div>
<div class="flex items-center space-x-2 text-sky-600 dark:text-sky-400 text-xs font-bold uppercase tracking-wider mb-1">
<i data-lucide="mail" class="w-4 h-4"></i>
<span>Microsoft Outlook 365 &bull; Inbox</span>
<?php if ($isLiveMail): ?>
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-extrabold bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300 ring-1 ring-emerald-500/30">LIVE VERBONDEN</span>
<?php elseif ($isOutlookConfigured && $isDemo): ?>
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-extrabold bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300 ring-1 ring-amber-500/30">E-MAIL MACHTIGING VEREIST</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300 ring-1 ring-amber-500/30">DEMO INBOX</span>
<?php endif; ?>
</div>
<h1 class="text-2xl sm:text-3xl font-black tracking-tight text-slate-900 dark:text-white">
E-mail & Communicatie
</h1>
<p class="text-xs sm:text-sm text-slate-500 dark:text-slate-400 mt-1">
Bekijk binnenkomende e-mails, beantwoord direct en plan belangrijke berichten in één klik in op jouw tijdlijn.
</p>
</div>
<!-- Knoppen rechts -->
<div class="flex flex-wrap items-center gap-2">
<button onclick="refreshMailbox()" id="refreshMailBtn" class="flex items-center space-x-1.5 px-3.5 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer shadow-2xs">
<i data-lucide="refresh-cw" id="refreshMailIcon" class="w-3.5 h-3.5"></i>
<span>Vernieuwen</span>
</button>
<?php if (!$isLiveMail): ?>
<a href="/auth/oauth_microsoft.php?reconsent=1&return_to=/mail.php" class="flex items-center space-x-1.5 px-4 py-2 rounded-xl bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-500 hover:to-indigo-500 text-white text-xs font-bold transition shadow-sm shadow-sky-600/20">
<i data-lucide="link-2" class="w-3.5 h-3.5"></i>
<span><?= $isOutlookConfigured ? 'Machtigingen Bijwerken' : 'Live Outlook Koppelen' ?></span>
</a>
<?php else: ?>
<a href="https://outlook.office.com/mail/" target="_blank" rel="noopener noreferrer" class="flex items-center space-x-1.5 px-3.5 py-2 rounded-xl bg-sky-50 dark:bg-sky-950/40 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-800/60 text-xs font-bold transition hover:bg-sky-100">
<span>Open in Outlook Web</span>
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
</a>
<?php endif; ?>
</div>
</div>
<?php if ($isOutlookConfigured && $isDemo): ?>
<div class="mt-5 p-4 rounded-2xl bg-amber-50/90 dark:bg-amber-950/40 border border-amber-300 dark:border-amber-700/60 flex flex-col sm:flex-row sm:items-center justify-between gap-4 text-xs text-amber-900 dark:text-amber-200 shadow-2xs">
<div class="flex items-start sm:items-center space-x-3">
<div class="w-8 h-8 rounded-xl bg-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center flex-shrink-0 font-bold">
<i data-lucide="shield-alert" class="w-4 h-4"></i>
</div>
<div>
<span class="font-bold block text-amber-950 dark:text-amber-100">E-mail permissie ontbreekt (Herautorisatie vereist)</span>
<span class="text-[11px] text-amber-800 dark:text-amber-300">
Je Microsoft account is gekoppeld voor je agenda, maar het huidige token heeft nog geen toestemming voor e-mail (<code class="font-mono bg-amber-100 dark:bg-amber-900/60 px-1 py-0.5 rounded text-[10px]">Mail.ReadWrite</code>). Klik op <strong>Machtigingen Bijwerken</strong> om eenmalig toestemming te verlenen.
</span>
</div>
</div>
<a href="/auth/oauth_microsoft.php?reconsent=1&return_to=/mail.php" class="px-4 py-2 rounded-xl bg-amber-600 hover:bg-amber-500 text-white font-bold text-xs shadow-xs transition whitespace-nowrap flex-shrink-0 text-center cursor-pointer">
Machtigingen Bijwerken &rarr;
</a>
</div>
<?php elseif (!$isOutlookConfigured): ?>
<div class="mt-5 p-3.5 rounded-2xl bg-amber-50/80 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-900/40 flex items-center justify-between gap-3 text-xs text-amber-900 dark:text-amber-200">
<div class="flex items-center space-x-2.5">
<i data-lucide="info" class="w-4 h-4 text-amber-600 dark:text-amber-400 flex-shrink-0"></i>
<span>Je bekijkt momenteel een interactieve demonstratieset. Koppel je zakelijke Microsoft 365 account om al je live zakelijke e-mails in te laden.</span>
</div>
<a href="/auth/oauth_microsoft.php?return_to=/mail.php" class="font-bold underline hover:text-amber-700 dark:hover:text-amber-100 flex-shrink-0">Nu koppelen &rarr;</a>
</div>
<?php endif; ?>
</div>
<!-- Filterbalk & Zoeken -->
<div class="fociz-card p-4 flex flex-col sm:flex-row items-center justify-between gap-3">
<!-- Filter tabs: Alle / Ongelezen -->
<div class="flex items-center space-x-1.5 bg-slate-100/90 dark:bg-slate-800/60 p-1 rounded-xl border border-slate-200/70 dark:border-slate-700/60 text-xs font-bold w-full sm:w-auto">
<button onclick="setMailFilter('all')" id="filterBtn_all" class="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">
<span>Alle Berichten</span>
<span class="ml-1 px-1.5 py-0.5 rounded-full text-[10px] bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400" id="countAll"><?= count($messages) ?></span>
</button>
<button onclick="setMailFilter('unread')" id="filterBtn_unread" class="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">
<span>Ongelezen</span>
<span class="ml-1 px-1.5 py-0.5 rounded-full text-[10px] bg-sky-100 dark:bg-sky-900/60 text-sky-700 dark:text-sky-300" id="countUnread"><?= $unreadCount ?></span>
</button>
</div>
<!-- Zoekveld -->
<div class="relative w-full sm:w-72">
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-3 pointer-events-none"></i>
<input type="text" id="mailSearchInput" oninput="applyMailSearch()" placeholder="Zoek op afzender, onderwerp..." class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl pl-9 pr-3 py-2 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-sky-500">
</div>
</div>
<!-- Twee-kolommen Mail Interface -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 min-h-[600px]">
<!-- 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 &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-2.5 pr-1 custom-scrollbar" id="mailFoldersContainer">
<!-- Wordt dynamisch gerenderd via JS (Inbox open, overige mappen inklapbaar) -->
</div>
</div>
<!-- Rechter kolom: Bericht Detail & Acties (7 kolommen) -->
<div id="mailDetailColumn" class="lg:col-span-7 fociz-card p-6 sm:p-7 flex flex-col h-[720px] overflow-hidden">
<!-- Empty state wanneer geen mail geselecteerd is -->
<div id="noMailSelectedState" class="flex-1 flex flex-col items-center justify-center text-center p-8 text-slate-400">
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800/80 flex items-center justify-center mb-3 text-slate-400 dark:text-slate-500">
<i data-lucide="mail-open" class="w-7 h-7"></i>
</div>
<h4 class="text-sm font-bold text-slate-700 dark:text-slate-300">Geen e-mail geselecteerd</h4>
<p class="text-xs text-slate-400 dark:text-slate-500 max-w-sm mt-1">
Selecteer een bericht aan de linkerkant om de volledige inhoud te lezen, te beantwoorden of in te plannen in je dagagenda.
</p>
</div>
<!-- Actief geselecteerd bericht -->
<div id="mailActiveContainer" class="hidden flex-1 flex flex-col min-h-0">
<!-- Mobiel terug knop -->
<button type="button" onclick="closeMobileDetail()" class="lg:hidden flex items-center space-x-1 text-xs font-bold text-slate-500 mb-3 hover:text-slate-800">
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
<span>Terug naar lijst</span>
</button>
<!-- Detail Header -->
<div class="pb-4 border-b border-slate-100 dark:border-slate-800/80 space-y-3">
<div class="flex items-start justify-between gap-3">
<h2 id="detailSubject" class="text-lg sm:text-xl font-black text-slate-900 dark:text-white leading-snug"></h2>
<div class="flex items-center space-x-1.5 flex-shrink-0">
<!-- Plan in Fociz knop -->
<button type="button" onclick="openPlanMailModal()" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-sm shadow-indigo-600/20 cursor-pointer">
<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i>
<span>Plan in Fociz</span>
</button>
<!-- Beantwoorden knop -->
<button type="button" onclick="openReplyModal()" class="flex items-center space-x-1 px-2.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
<i data-lucide="reply" class="w-3.5 h-3.5"></i>
<span class="hidden sm:inline">Antwoord</span>
</button>
<!-- Doorsturen knop -->
<button type="button" onclick="openForwardModal()" class="flex items-center space-x-1 px-2.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
<i data-lucide="corner-up-right" class="w-3.5 h-3.5"></i>
<span class="hidden sm:inline">Doorsturen</span>
</button>
</div>
</div>
<!-- Afzender & Ontvangers info -->
<div class="flex items-center justify-between text-xs pt-1">
<div class="flex items-center space-x-3">
<div id="detailSenderAvatar" class="w-9 h-9 rounded-xl bg-sky-100 dark:bg-sky-950/60 text-sky-700 dark:text-sky-300 font-bold flex items-center justify-center text-xs">
NL
</div>
<div>
<div class="font-bold text-slate-900 dark:text-white" id="detailSenderName"></div>
<div class="text-[11px] text-slate-400 font-mono" id="detailSenderEmail"></div>
</div>
</div>
<div class="text-right">
<div class="text-xs font-semibold text-slate-600 dark:text-slate-300" id="detailReceivedHuman"></div>
<div class="text-[10px] text-slate-400 font-mono" id="detailReceivedExact"></div>
</div>
</div>
</div>
<!-- Detail Body (Scrollable) -->
<div class="flex-1 overflow-y-auto py-5 text-sm text-slate-800 dark:text-slate-200 leading-relaxed font-normal" id="detailBody">
<!-- HTML body content -->
</div>
<!-- Footer met web link -->
<div class="pt-3 border-t border-slate-100 dark:border-slate-800 flex items-center justify-between text-xs text-slate-400">
<span id="detailFooterBadge">Outlook 365 Bericht</span>
<a id="detailWebLink" href="#" target="_blank" rel="noopener noreferrer" class="text-sky-600 dark:text-sky-400 hover:underline flex items-center space-x-1 font-semibold">
<span>Bekijk in Outlook Web App</span>
<i data-lucide="external-link" class="w-3 h-3"></i>
</a>
</div>
</div>
</div>
</div>
</div>
<!-- MODAL 1: E-mail Inplannen in Fociz Dagplanning -->
<div id="planMailModal" onclick="if(event.target === this) closePlanMailModal()" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
<div class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-3xl max-w-lg w-full p-6 sm:p-7 shadow-2xl space-y-5 animate-in fade-in zoom-in-95 duration-150">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2.5">
<div class="w-9 h-9 rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
<i data-lucide="calendar-plus" class="w-5 h-5"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Plan E-mail in als Taak</h3>
<p class="text-[11px] text-slate-400">Plaats dit actiepunt direct op jouw Fociz tijdlijn</p>
</div>
</div>
<button onclick="closePlanMailModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1.5 rounded-lg transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<form onsubmit="submitPlanEmail(event)" class="space-y-4">
<input type="hidden" id="planMailId" value="">
<input type="hidden" id="planMailWebLink" value="">
<input type="hidden" id="planMailSender" value="">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Taaktitel</label>
<input type="text" id="planMailSubject" required class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white font-semibold focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<!-- Datum & Slimme "Plan in open blok" knop -->
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
<button type="button" onclick="planInNextOpenBlockMail()" id="btnMailPlanOpenBlock" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-gradient-to-r from-indigo-600 to-sky-600 hover:from-indigo-500 hover:to-sky-500 text-white text-xs font-bold transition shadow-xs cursor-pointer active:scale-95" title="Vind automatisch het eerstvolgende vrije kwartierblok binnen werktijden">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
<span>Plan in open blok</span>
</button>
</div>
<!-- Open blok banner -->
<div id="mailPlanOpenBlockNotice" class="hidden p-2.5 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-800 dark:text-emerald-300 text-[11px] flex items-center justify-between">
<div class="flex items-center space-x-1.5 min-w-0 flex-1 mr-2">
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i>
<span id="mailPlanOpenBlockNoticeText" class="truncate font-semibold"></span>
</div>
<button type="button" onclick="document.getElementById('mailPlanOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded cursor-pointer">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Datum</label>
<input type="date" id="planMailDate" value="<?= date('Y-m-d') ?>" required class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Starttijd (optioneel)</label>
<span id="planMailEndTimeBadge" class="text-[10px] text-indigo-600 dark:text-indigo-400 font-mono font-semibold"></span>
</div>
<input type="text" id="planMailStartTime" oninput="updatePlanMailEndTimeBadge()" placeholder="bijv. 10:30" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs font-mono text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Tijdsduur</label>
<select id="planMailDuration" onchange="updatePlanMailEndTimeBadge()" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer">
<option value="15">15 minuten</option>
<option value="30" selected>30 minuten</option>
<option value="45">45 minuten</option>
<option value="60">60 minuten (1 uur)</option>
<option value="90">90 minuten</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
<select id="planMailPriority" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer">
<option value="P1">P1 - Urgent / Brandend</option>
<option value="P2" selected>P2 - Belangrijk</option>
<option value="P3">P3 - Regulier</option>
</select>
</div>
</div>
<!-- Optie: Tevens toevoegen aan Bord & Tijdlijn -->
<div class="p-3 rounded-xl bg-slate-50 dark:bg-slate-900/40 border border-slate-200 dark:border-slate-800 space-y-2.5">
<label class="flex items-center space-x-2.5 cursor-pointer select-none">
<input type="checkbox" id="mailPlanAddToBoard" onchange="toggleMailBoardOptions(this.checked)" class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 dark:bg-slate-900 cursor-pointer">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5">
<i data-lucide="layout-grid" class="w-3.5 h-3.5 text-indigo-600 dark:text-indigo-400"></i>
<span>Tevens toevoegen aan Bord &amp; Tijdlijn</span>
</span>
</label>
<div id="mailBoardOptionsWrapper" class="hidden space-y-2.5 pt-2.5 border-t border-slate-200/70 dark:border-slate-800">
<?php if (empty($userBoards)): ?>
<p class="text-[11px] text-slate-500 dark:text-slate-400">Er zijn geen actieve borden gevonden.</p>
<?php else: ?>
<div class="grid grid-cols-2 gap-2">
<div>
<label for="mailPlanBoardSelect" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Bord</label>
<select id="mailPlanBoardSelect" onchange="onMailBoardSelectChange(this.value)" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<?php foreach ($userBoards as $b): ?>
<option value="<?= (int)$b['id'] ?>"><?= htmlspecialchars($b['title']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label for="mailPlanLaneSelect" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Kolom</label>
<select id="mailPlanLaneSelect" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<!-- Wordt gevuld via JS -->
</select>
</div>
</div>
<div class="pt-1">
<label class="flex items-center space-x-2 cursor-pointer select-none">
<input type="checkbox" id="mailPlanScheduleOnTimeline" checked onchange="toggleMailTimelineDates(this.checked)" class="w-3.5 h-3.5 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 dark:bg-slate-900 cursor-pointer">
<span class="text-xs font-medium text-slate-700 dark:text-slate-300">Inplannen op de Project Tijdlijn</span>
</label>
<div id="mailTimelineDatesWrapper" class="grid grid-cols-2 gap-2 mt-2">
<div>
<label for="mailTimelineStartDate" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Startdatum</label>
<input type="date" id="mailTimelineStartDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label for="mailTimelineEndDate" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Einddatum</label>
<input type="date" id="mailTimelineEndDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800">
<button type="button" onclick="closePlanMailModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
Annuleren
</button>
<button type="submit" id="planMailSubmitBtn" class="flex items-center space-x-1.5 px-5 py-2 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-500 text-white transition shadow-sm shadow-indigo-600/30 cursor-pointer">
<i data-lucide="check" class="w-3.5 h-3.5"></i>
<span>Inplannen</span>
</button>
</div>
</form>
</div>
</div>
<!-- Modal: Waarschuwing Buiten Werktijden (Mail) -->
<div id="outsideWorkhoursModalMail" onclick="if(event.target === this) closeOutsideWorkhoursModalMail('CANCEL')" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
<div class="bg-white dark:bg-[#111624] border border-amber-500/30 rounded-2xl w-full max-w-md shadow-2xl p-6 space-y-4">
<div class="flex items-start space-x-3">
<div class="w-10 h-10 rounded-2xl bg-amber-500/10 border border-amber-500/20 text-amber-500 flex items-center justify-center flex-shrink-0">
<i data-lucide="alert-triangle" class="w-5 h-5"></i>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base font-bold text-slate-900 dark:text-white leading-tight">
Let op! Je plant buiten werktijden
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">
Het ingeplande tijdblok valt buiten de gezette werktijden.
</p>
</div>
<button type="button" onclick="closeOutsideWorkhoursModalMail('CANCEL')" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg transition cursor-pointer flex-shrink-0">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800/40 rounded-xl p-3.5 text-xs text-amber-800 dark:text-amber-300 space-y-1.5">
<div class="flex items-center justify-between text-[11px] font-semibold text-amber-900 dark:text-amber-200">
<span>Gekozen tijdblok:</span>
<span id="outsideWorkhoursSelectedSlotMail" class="font-mono font-bold"></span>
</div>
<div class="flex items-center justify-between text-[11px] text-amber-700 dark:text-amber-400">
<span>Gezette werktijden:</span>
<span id="outsideWorkhoursBoundsMail" class="font-mono font-semibold">08:30 – 17:00</span>
</div>
<p class="text-[11px] text-amber-700/80 dark:text-amber-400/80 pt-1 border-t border-amber-200/60 dark:border-amber-800/30">
Kies <strong>Nee</strong> om het item automatisch in te plannen in het eerstvolgende open blok op de volgende werkdag, of kies <strong>Ja</strong> om toch nu buiten werktijd te plannen.
</p>
</div>
<div class="flex items-center space-x-2.5 pt-2">
<button type="button" id="btnOutsideWorkhoursNoMail" onclick="closeOutsideWorkhoursModalMail('NEE')" class="flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition shadow-xs flex items-center justify-center space-x-1.5 cursor-pointer">
<i data-lucide="calendar-arrow-up" class="w-4 h-4"></i>
<span>Nee (Volgende werkdag)</span>
</button>
<button type="button" id="btnOutsideWorkhoursYesMail" onclick="closeOutsideWorkhoursModalMail('JA')" class="px-4 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 font-semibold text-xs transition cursor-pointer">
<span>Ja, toch inplannen</span>
</button>
</div>
</div>
</div>
<!-- MODAL 2: E-mail Beantwoorden -->
<div id="replyModal" onclick="if(event.target === this) closeReplyModal()" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4">
<div class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-3xl max-w-lg w-full p-6 sm:p-7 shadow-2xl space-y-5 animate-in fade-in zoom-in-95 duration-150">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2.5">
<div class="w-9 h-9 rounded-xl bg-sky-500/10 text-sky-600 dark:text-sky-400 flex items-center justify-center">
<i data-lucide="reply" class="w-5 h-5"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Beantwoord Bericht</h3>
<p class="text-[11px] text-slate-400" id="replyToLabel"></p>
</div>
</div>
<button onclick="closeReplyModal()" class="text-slate-400 hover:text-slate-600 p-1.5 rounded-lg transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<form onsubmit="submitReply(event)" class="space-y-4">
<input type="hidden" id="replyMailId" value="">
<div class="flex items-center space-x-3 text-xs">
<label class="flex items-center space-x-2 cursor-pointer text-slate-700 dark:text-slate-300">
<input type="checkbox" id="replyAllCheckbox" class="w-4 h-4 rounded text-sky-600 focus:ring-sky-500 border-slate-300 dark:border-slate-700">
<span>Allen beantwoorden (Reply All)</span>
</label>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Jouw Reactie</label>
<textarea id="replyContent" rows="6" required placeholder="Typ hier jouw bericht..." class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl p-3 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-sky-500"></textarea>
</div>
<div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800">
<button type="button" onclick="closeReplyModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
Annuleren
</button>
<button type="submit" id="replySubmitBtn" class="flex items-center space-x-1.5 px-5 py-2 rounded-xl text-xs font-bold bg-sky-600 hover:bg-sky-500 text-white transition shadow-sm shadow-sky-600/30 cursor-pointer">
<i data-lucide="send" class="w-3.5 h-3.5"></i>
<span>Versturen via Outlook</span>
</button>
</div>
</form>
</div>
</div>
<!-- MODAL 3: E-mail Doorsturen -->
<div id="forwardModal" onclick="if(event.target === this) closeForwardModal()" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4">
<div class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-3xl max-w-lg w-full p-6 sm:p-7 shadow-2xl space-y-5 animate-in fade-in zoom-in-95 duration-150">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2.5">
<div class="w-9 h-9 rounded-xl bg-violet-500/10 text-violet-600 dark:text-violet-400 flex items-center justify-center">
<i data-lucide="corner-up-right" class="w-5 h-5"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">E-mail Doorsturen</h3>
<p class="text-[11px] text-slate-400" id="forwardSubjectLabel"></p>
</div>
</div>
<button onclick="closeForwardModal()" class="text-slate-400 hover:text-slate-600 p-1.5 rounded-lg transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<form onsubmit="submitForward(event)" class="space-y-4">
<input type="hidden" id="forwardMailId" value="">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Ontvanger(s) e-mailadres</label>
<input type="text" id="forwardTo" required placeholder="collega@fociz.nl (meerdere scheiden met komma)" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-violet-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Toelichting (optioneel)</label>
<textarea id="forwardComment" rows="4" placeholder="Eventuele toelichting voor de ontvanger..." class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl p-3 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-violet-500"></textarea>
</div>
<div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800">
<button type="button" onclick="closeForwardModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
Annuleren
</button>
<button type="submit" id="forwardSubmitBtn" class="flex items-center space-x-1.5 px-5 py-2 rounded-xl text-xs font-bold bg-violet-600 hover:bg-violet-500 text-white transition shadow-sm shadow-violet-600/30 cursor-pointer">
<i data-lucide="send" class="w-3.5 h-3.5"></i>
<span>Doorsturen via Outlook</span>
</button>
</div>
</form>
</div>
</div>
<script>
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;
function setMailFilter(filter) {
activeFilter = filter;
const btnAll = document.getElementById('filterBtn_all');
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 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 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";
// 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);
}
}
});
}
renderFolders();
}
function applyMailSearch() {
renderFolders();
}
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;
}
expandedFolders.add(folderId);
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 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 ${folder.is_child ? 'pl-3' : ''}">
<div class="w-7 h-7 rounded-xl ${isInbox ? 'bg-sky-600 text-white shadow-xs' : (folder.is_child ? 'bg-slate-100 dark:bg-slate-800 text-slate-500' : '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.is_child ? 'corner-down-right' : '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>
</div>
<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>
</button>
<!-- 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) {
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;
renderFolders(); // Update actieve styling
// Toon detail container
document.getElementById('noMailSelectedState').classList.add('hidden');
const activeCont = document.getElementById('mailActiveContainer');
activeCont.classList.remove('hidden');
// Velden invullen met preview
document.getElementById('detailSubject').innerText = found.subject;
document.getElementById('detailSenderName').innerText = found.from_name;
document.getElementById('detailSenderEmail').innerText = found.from_email;
document.getElementById('detailReceivedHuman').innerText = found.received_human;
document.getElementById('detailReceivedExact').innerText = found.received_at;
document.getElementById('detailWebLink').href = found.web_link || 'https://outlook.office.com/mail/';
const initials = (found.from_name || 'NL').split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
document.getElementById('detailSenderAvatar').innerText = initials;
// Body laden
const bodyEl = document.getElementById('detailBody');
bodyEl.innerHTML = `<div class="p-8 text-center text-slate-400"><i data-lucide="loader-2" class="w-5 h-5 animate-spin mx-auto mb-2"></i><span>Bericht laden...</span></div>`;
if (typeof lucide !== 'undefined') lucide.createIcons();
try {
const res = await fetch(`/api/mail.php?action=get_message&id=${encodeURIComponent(id)}`);
const data = await res.json();
if (data.success && data.message) {
renderMailBodyContent(bodyEl, data.message.body_html, data.message.body_text || found.body_preview);
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>`;
}
} catch(err) {
renderMailBodyContent(bodyEl, null, found.body_preview);
}
}
// Grondige HTML sanitization voor inkomende e-mails (XSS & exploit preventie)
function sanitizeEmailHtml(html) {
if (!html) return '';
// 1. Verwijder alle script tags en inhoud
let cleaned = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
// 2. Verwijder potentieel gevaarlijke tags
cleaned = cleaned.replace(/<\/?(object|embed|applet|meta|base|form|input|button|textarea|select)\b[^>]*>/gi, '');
// 3. Verwijder inline JavaScript event handlers (onload, onerror, onclick, onmouseover, etc.)
cleaned = cleaned.replace(/\s+on[a-z0-9_-]+\s*=\s*(?:'[^']*'|"[^"]*"|[^\s>]+)/gi, '');
// 4. Neutraliseer javascript: en data:text/html pseudo-protocollen in links en afbeeldingen
cleaned = cleaned.replace(/(href|src)\s*=\s*['"]\s*(?:javascript|data:text\/html):[^'"]*['"]/gi, '$1="#"');
return cleaned;
}
// Veilig renderen van e-mail HTML via een geïsoleerde sandboxed iframe
function renderMailBodyContent(container, htmlContent, plainContent) {
container.innerHTML = '';
if (!htmlContent) {
const pre = document.createElement('pre');
pre.className = 'font-sans whitespace-pre-wrap text-sm text-slate-800 dark:text-slate-200 leading-relaxed p-2';
pre.textContent = plainContent || '(Geen inhoud)';
container.appendChild(pre);
return;
}
const safeHtml = sanitizeEmailHtml(htmlContent);
const isDark = document.documentElement.classList.contains('dark');
// Sandboxed iframe: strikte isolatie (geen allow-scripts)
const iframe = document.createElement('iframe');
iframe.id = 'mailBodyIframe';
iframe.setAttribute('sandbox', 'allow-same-origin allow-popups');
iframe.className = 'w-full border-0 rounded-xl transition-all';
iframe.style.width = '100%';
iframe.style.minHeight = '350px';
iframe.style.border = 'none';
iframe.style.background = isDark ? '#0c0f17' : '#ffffff';
iframe.style.display = 'block';
container.appendChild(iframe);
const docHtml = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base target="_blank">
<style>
*, *::before, *::after {
box-sizing: border-box;
}
html {
color-scheme: ${isDark ? 'dark' : 'light'};
}
body {
margin: 0;
padding: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
color: ${isDark ? '#f1f5f9' : '#0f172a'};
background-color: ${isDark ? '#0c0f17' : '#ffffff'};
word-break: break-word;
}
img {
max-width: 100% !important;
height: auto !important;
}
table {
max-width: 100% !important;
}
pre, code {
white-space: pre-wrap;
word-break: break-all;
}
a {
color: #4f46e5;
}
</style>
</head>
<body>
${safeHtml}
</body>
</html>`;
iframe.srcdoc = docHtml;
iframe.onload = () => {
try {
const innerDoc = iframe.contentDocument || iframe.contentWindow.document;
if (innerDoc) {
const updateHeight = () => {
const scrollH = innerDoc.documentElement.scrollHeight || innerDoc.body.scrollHeight;
iframe.style.height = Math.max(scrollH + 35, 280) + 'px';
};
updateHeight();
if (window.ResizeObserver) {
const observer = new ResizeObserver(updateHeight);
observer.observe(innerDoc.body);
}
}
} catch (e) {
iframe.style.height = '600px';
}
};
}
function updateUnreadCount() {
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() {
document.getElementById('mailActiveContainer').classList.add('hidden');
document.getElementById('noMailSelectedState').classList.remove('hidden');
}
// Modal open/close helpers & Open blok inplannen
let _outsideWorkhoursPendingMail = null;
function updatePlanMailEndTimeBadge() {
const startInput = document.getElementById('planMailStartTime');
const durationInput = document.getElementById('planMailDuration');
const badge = document.getElementById('planMailEndTimeBadge');
if (!startInput || !durationInput || !badge) return;
const startVal = (startInput.value || '').trim();
const duration = parseInt(durationInput.value || '30', 10);
if (!startVal || !startVal.includes(':') || isNaN(duration)) {
badge.innerText = '';
return;
}
const [hStr, mStr] = startVal.split(':');
const totalMinutes = (parseInt(hStr, 10) * 60 + parseInt(mStr, 10)) + duration;
const endH = Math.floor((totalMinutes / 60) % 24);
const endM = totalMinutes % 60;
const endStr = `${String(endH).padStart(2, '0')}:${String(endM).padStart(2, '0')}`;
badge.innerText = `(tot ${endStr})`;
}
async function planInNextOpenBlockMail(startFromNextWorkday = false) {
const durationInput = document.getElementById('planMailDuration');
const duration = parseInt(durationInput?.value || '30', 10);
const btn = document.getElementById('btnMailPlanOpenBlock');
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.disabled = true;
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Zoeken...</span>`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
try {
let url = `/api/planner.php?action=find_next_free_slot&duration=${duration}`;
if (startFromNextWorkday) {
url += '&start_from_next_workday=1';
}
const resp = await fetch(url);
const data = await resp.json();
if (data.success && data.slot) {
const slot = data.slot;
const dateInput = document.getElementById('planMailDate');
const startInput = document.getElementById('planMailStartTime');
if (dateInput) dateInput.value = slot.date;
if (startInput) startInput.value = slot.start;
updatePlanMailEndTimeBadge();
const notice = document.getElementById('mailPlanOpenBlockNotice');
const noticeText = document.getElementById('mailPlanOpenBlockNoticeText');
if (notice && noticeText) {
const formattedDate = new Date(slot.date + 'T00:00:00').toLocaleDateString('nl-NL', { weekday: 'short', day: 'numeric', month: 'short' });
noticeText.innerText = `Vrij blok gevonden: ${formattedDate} om ${slot.start} – ${slot.end} (${duration} min)`;
notice.classList.remove('hidden');
}
} else {
alert(data.error || 'Geen beschikbaar tijdblok gevonden binnen de komende 14 dagen.');
}
} catch (err) {
alert('Netwerkfout bij zoeken van open blok: ' + err.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = origHtml;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
}
function closeOutsideWorkhoursModalMail(choice) {
const modal = document.getElementById('outsideWorkhoursModalMail');
if (modal) modal.classList.add('hidden');
if (choice === 'JA') {
if (_outsideWorkhoursPendingMail) {
_outsideWorkhoursPendingMail.bypassWorkhours = true;
submitPlanEmail(null, _outsideWorkhoursPendingMail);
}
} else if (choice === 'NEE') {
_outsideWorkhoursPendingMail = null;
planInNextOpenBlockMail(true);
} else {
_outsideWorkhoursPendingMail = null;
}
}
const availableUserBoards = <?= json_encode($userBoards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?> || [];
function toggleMailBoardOptions(show) {
const wrapper = document.getElementById('mailBoardOptionsWrapper');
if (wrapper) {
if (show) {
wrapper.classList.remove('hidden');
const boardSelect = document.getElementById('mailPlanBoardSelect');
if (boardSelect && boardSelect.value) {
onMailBoardSelectChange(boardSelect.value);
}
const planDate = document.getElementById('planMailDate')?.value || '';
const startInput = document.getElementById('mailTimelineStartDate');
const endInput = document.getElementById('mailTimelineEndDate');
if (startInput && !startInput.value) startInput.value = planDate;
if (endInput && !endInput.value) endInput.value = planDate;
} else {
wrapper.classList.add('hidden');
}
}
}
function onMailBoardSelectChange(boardId) {
const laneSelect = document.getElementById('mailPlanLaneSelect');
if (!laneSelect) return;
laneSelect.innerHTML = '';
const board = availableUserBoards.find(b => String(b.id) === String(boardId));
if (board && board.lanes && board.lanes.length > 0) {
board.lanes.forEach((lane, idx) => {
const opt = document.createElement('option');
opt.value = lane.id;
opt.textContent = lane.title;
if (idx === 0) opt.selected = true;
laneSelect.appendChild(opt);
});
} else {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Geen kolommen beschikbaar';
laneSelect.appendChild(opt);
}
}
function toggleMailTimelineDates(show) {
const wrapper = document.getElementById('mailTimelineDatesWrapper');
if (wrapper) {
if (show) wrapper.classList.remove('hidden');
else wrapper.classList.add('hidden');
}
}
function openPlanMailModal() {
if (!selectedMessage) return;
document.getElementById('planMailId').value = selectedMessage.id;
document.getElementById('planMailSubject').value = selectedMessage.subject;
document.getElementById('planMailWebLink').value = selectedMessage.web_link || '';
document.getElementById('planMailSender').value = selectedMessage.from_name || '';
const notice = document.getElementById('mailPlanOpenBlockNotice');
if (notice) notice.classList.add('hidden');
const planDate = document.getElementById('planMailDate')?.value || '';
const addToBoardCb = document.getElementById('mailPlanAddToBoard');
if (addToBoardCb) {
addToBoardCb.checked = false;
toggleMailBoardOptions(false);
}
const schedTimelineCb = document.getElementById('mailPlanScheduleOnTimeline');
if (schedTimelineCb) {
schedTimelineCb.checked = true;
toggleMailTimelineDates(true);
}
const tStart = document.getElementById('mailTimelineStartDate');
const tEnd = document.getElementById('mailTimelineEndDate');
if (tStart) tStart.value = planDate;
if (tEnd) tEnd.value = planDate;
updatePlanMailEndTimeBadge();
const modal = document.getElementById('planMailModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closePlanMailModal() {
const modal = document.getElementById('planMailModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
async function submitPlanEmail(e, options = {}) {
if (e) e.preventDefault();
const messageId = document.getElementById('planMailId').value;
const subject = document.getElementById('planMailSubject').value;
const planDate = document.getElementById('planMailDate').value;
const timeBlockStart = (document.getElementById('planMailStartTime')?.value || '').trim();
const durationMinutes = parseInt(document.getElementById('planMailDuration').value, 10);
const priority = document.getElementById('planMailPriority').value;
const webLink = document.getElementById('planMailWebLink').value;
const sender = document.getElementById('planMailSender').value;
// Controleer of de geplande tijd buiten de gezette werktijden valt (standaard 08:30 - 17:00)
if (!options.bypassWorkhours && timeBlockStart && timeBlockStart.includes(':')) {
const [shStr, smStr] = timeBlockStart.split(':');
const startMin = parseInt(shStr, 10) * 60 + parseInt(smStr, 10);
const endMin = startMin + durationMinutes;
const workStartMin = 8 * 60 + 30; // 08:30
const workEndMin = 17 * 60; // 17:00
if (startMin < workStartMin || endMin > workEndMin) {
_outsideWorkhoursPendingMail = { messageId, subject, planDate, timeBlockStart, durationMinutes, priority, webLink, sender };
const endH = Math.floor((endMin / 60) % 24);
const endM = endMin % 60;
const endStr = `${String(endH).padStart(2, '0')}:${String(endM).padStart(2, '0')}`;
const slotEl = document.getElementById('outsideWorkhoursSelectedSlotMail');
if (slotEl) slotEl.innerText = `${timeBlockStart} – ${endStr} (${durationMinutes} min)`;
const outsideModal = document.getElementById('outsideWorkhoursModalMail');
if (outsideModal) {
outsideModal.classList.remove('hidden');
outsideModal.classList.add('flex');
setTimeout(() => {
const btnNo = document.getElementById('btnOutsideWorkhoursNoMail');
if (btnNo) btnNo.focus();
}, 50);
if (typeof lucide !== 'undefined') lucide.createIcons();
}
return;
}
}
const btn = document.getElementById('planMailSubmitBtn');
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Inplannen...</span>`;
btn.disabled = true;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
const addToBoard = document.getElementById('mailPlanAddToBoard')?.checked || false;
const boardId = parseInt(document.getElementById('mailPlanBoardSelect')?.value || '0', 10);
const laneId = parseInt(document.getElementById('mailPlanLaneSelect')?.value || '0', 10);
const scheduleOnTimeline = document.getElementById('mailPlanScheduleOnTimeline')?.checked || false;
const timelineStartDate = document.getElementById('mailTimelineStartDate')?.value || planDate;
const timelineEndDate = document.getElementById('mailTimelineEndDate')?.value || timelineStartDate;
try {
const res = await fetch('/api/mail.php?action=plan_email', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
message_id: messageId,
subject: subject,
plan_date: planDate,
time_block_start: timeBlockStart,
duration_minutes: durationMinutes,
priority: priority,
web_link: webLink,
sender: sender,
add_to_board: addToBoard,
board_id: boardId,
lane_id: laneId,
schedule_on_timeline: scheduleOnTimeline,
timeline_start_date: timelineStartDate,
timeline_end_date: timelineEndDate
})
});
const data = await res.json();
if (data.success) {
closePlanMailModal();
alert(`Succes! Het e-mailbericht is als taak ingepland voor ${data.plan_date}.`);
} else {
alert(data.error || 'Kon e-mail niet inplannen');
}
} catch(err) {
alert('Netwerkfout: ' + err.message);
} finally {
if (btn) {
btn.innerHTML = origHtml;
btn.disabled = false;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
}
function openReplyModal() {
if (!selectedMessage) return;
document.getElementById('replyMailId').value = selectedMessage.id;
document.getElementById('replyToLabel').innerText = `Aan: ${selectedMessage.from_name} (${selectedMessage.from_email})`;
document.getElementById('replyContent').value = '';
const modal = document.getElementById('replyModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeReplyModal() {
const modal = document.getElementById('replyModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
async function submitReply(e) {
e.preventDefault();
const btn = document.getElementById('replySubmitBtn');
const origHtml = btn.innerHTML;
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Versturen...</span>`;
btn.disabled = true;
try {
const res = await fetch('/api/mail.php?action=reply', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: document.getElementById('replyMailId').value,
comment: document.getElementById('replyContent').value,
reply_all: document.getElementById('replyAllCheckbox').checked
})
});
const data = await res.json();
if (data.success) {
closeReplyModal();
alert('Antwoord is succesvol verstuurd via Outlook!');
} else {
alert(data.error || 'Kon reactie niet versturen.');
}
} catch(err) {
alert('Netwerkfout: ' + err.message);
} finally {
btn.innerHTML = origHtml;
btn.disabled = false;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
function openForwardModal() {
if (!selectedMessage) return;
document.getElementById('forwardMailId').value = selectedMessage.id;
document.getElementById('forwardSubjectLabel').innerText = `Fwd: ${selectedMessage.subject}`;
document.getElementById('forwardTo').value = '';
document.getElementById('forwardComment').value = '';
const modal = document.getElementById('forwardModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeForwardModal() {
const modal = document.getElementById('forwardModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
async function submitForward(e) {
e.preventDefault();
const btn = document.getElementById('forwardSubmitBtn');
const origHtml = btn.innerHTML;
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Doorsturen...</span>`;
btn.disabled = true;
try {
const res = await fetch('/api/mail.php?action=forward', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: document.getElementById('forwardMailId').value,
to: document.getElementById('forwardTo').value,
comment: document.getElementById('forwardComment').value
})
});
const data = await res.json();
if (data.success) {
closeForwardModal();
alert('E-mail is succesvol doorgestuurd via Outlook!');
} else {
alert(data.error || 'Kon e-mail niet doorsturen.');
}
} catch(err) {
alert('Netwerkfout: ' + err.message);
} finally {
btn.innerHTML = origHtml;
btn.disabled = false;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
async function refreshMailbox() {
const icon = document.getElementById('refreshMailIcon');
if (icon) icon.classList.add('animate-spin');
try {
const res = await fetch('/api/mail.php?action=get_folders&refresh=1');
const data = await res.json();
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();
}
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// Toetsenbordnavigatie voor alle modals (Escape & Enter)
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const outsideModal = document.getElementById('outsideWorkhoursModalMail');
if (outsideModal && !outsideModal.classList.contains('hidden')) {
closeOutsideWorkhoursModalMail('CANCEL');
return;
}
closePlanMailModal();
closeReplyModal();
closeForwardModal();
} else if (e.key === 'Enter') {
const outsideModal = document.getElementById('outsideWorkhoursModalMail');
if (outsideModal && !outsideModal.classList.contains('hidden')) {
e.preventDefault();
closeOutsideWorkhoursModalMail('NEE');
return;
}
}
});
document.addEventListener('DOMContentLoaded', () => {
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
window.addEventListener('fociz:mail-updated', () => {
refreshMailbox();
});
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>