1446 lines
76 KiB
PHP
1446 lines
76 KiB
PHP
<?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 • 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 →
|
||
</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 →</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 & 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 & 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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// 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'; ?>
|