Fix speed and error #94

Merged
NiekRabelink merged 1 commits from dev into main 2026-09-16 10:23:33 +00:00
5 changed files with 149 additions and 54 deletions
Showing only changes of commit bda7b311e3 - Show all commits
+5
View File
@@ -7,6 +7,11 @@ require_once __DIR__ . '/auth.php';
$currentUser = $currentUser ?? require_auth(); $currentUser = $currentUser ?? require_auth();
$currentPage = basename($_SERVER['PHP_SELF']); $currentPage = basename($_SERVER['PHP_SELF']);
$currentUserTheme = !empty($currentUser['id']) ? get_user_theme((int)$currentUser['id']) : 'dark'; $currentUserTheme = !empty($currentUser['id']) ? get_user_theme((int)$currentUser['id']) : 'dark';
// Geef direct het PHP sessieslot vrij voor getoonde pagina's zodat parallelle requests en navigatie nooit blokkeren
if (session_status() === PHP_SESSION_ACTIVE && ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') {
session_write_close();
}
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="nl" class="dark"> <html lang="nl" class="dark">
+7 -5
View File
@@ -401,7 +401,9 @@ function jira_fetch_project_statuses(string $domain, string $email, string $apiT
return strcasecmp($a['name'], $b['name']); return strcasecmp($a['name'], $b['name']);
}); });
fociz_cache_set($cacheKey, $statuses, 3600); fociz_cache_set($cacheKey, $statuses, 86400); // 24 uur cache
} else {
fociz_cache_set($cacheKey, [], 1800); // 30 min negatieve cache
} }
return $statuses; return $statuses;
@@ -1195,7 +1197,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
return array_merge($cachedData, ['from_cache' => true]); return array_merge($cachedData, ['from_cache' => true]);
} }
if (session_status() === PHP_SESSION_ACTIVE && !empty($_SESSION[$cacheKey]['data'])) { if (session_status() === PHP_SESSION_ACTIVE && !empty($_SESSION[$cacheKey]['data'])) {
if (($_SESSION[$cacheKey]['timestamp'] ?? 0) > (time() - 60)) { if (($_SESSION[$cacheKey]['timestamp'] ?? 0) > (time() - 180)) {
return array_merge($_SESSION[$cacheKey]['data'], ['from_cache' => true]); return array_merge($_SESSION[$cacheKey]['data'], ['from_cache' => true]);
} }
} }
@@ -1230,8 +1232,8 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
$targetProjKeys = array_fill_keys(['SD', 'IN', 'AAN', 'GEN'], true); $targetProjKeys = array_fill_keys(['SD', 'IN', 'AAN', 'GEN'], true);
} }
// Ophalen van teamtaken binnen de relevante projecten (met paginatie tot max 500) // Ophalen van teamtaken binnen de relevante projecten (met paginatie tot max 200)
$teamRes = jira_fetch_team_issues($domain, $email, $apiToken, array_keys($targetProjKeys), 500); $teamRes = jira_fetch_team_issues($domain, $email, $apiToken, array_keys($targetProjKeys), 200);
// Ophalen van Jira statussen // Ophalen van Jira statussen
$availableStatuses = jira_fetch_statuses($domain, $email, $apiToken); $availableStatuses = jira_fetch_statuses($domain, $email, $apiToken);
@@ -1281,7 +1283,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'project_statuses' => $projectStatusesMap, 'project_statuses' => $projectStatusesMap,
]; ];
fociz_cache_set($cacheKey, $data, 60); fociz_cache_set($cacheKey, $data, 180); // 3 minuten cache
if (session_status() === PHP_SESSION_ACTIVE) { if (session_status() === PHP_SESSION_ACTIVE) {
$_SESSION[$cacheKey] = [ $_SESSION[$cacheKey] = [
+97 -29
View File
@@ -1187,7 +1187,7 @@ function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false):
$cacheKey = "ms_folders_{$userId}"; $cacheKey = "ms_folders_{$userId}";
if (!$forceRefresh) { if (!$forceRefresh) {
$cached = fociz_cache_get($cacheKey); $cached = fociz_cache_get($cacheKey);
if ($cached !== null && is_array($cached)) { if ($cached !== null && is_array($cached) && !empty($cached['success'])) {
return $cached; return $cached;
} }
} }
@@ -1206,6 +1206,8 @@ function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false):
'unread_count' => 2, 'unread_count' => 2,
'total_count' => 5, 'total_count' => 5,
'is_inbox' => true, 'is_inbox' => true,
'parent_id' => null,
'is_child' => false,
], ],
[ [
'id' => 'demo-folder-dx', 'id' => 'demo-folder-dx',
@@ -1213,6 +1215,8 @@ function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false):
'unread_count' => 1, 'unread_count' => 1,
'total_count' => 3, 'total_count' => 3,
'is_inbox' => false, 'is_inbox' => false,
'parent_id' => null,
'is_child' => false,
], ],
[ [
'id' => 'demo-folder-klanten', 'id' => 'demo-folder-klanten',
@@ -1220,6 +1224,8 @@ function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false):
'unread_count' => 0, 'unread_count' => 0,
'total_count' => 2, 'total_count' => 2,
'is_inbox' => false, 'is_inbox' => false,
'parent_id' => null,
'is_child' => false,
], ],
[ [
'id' => 'demo-folder-fin', 'id' => 'demo-folder-fin',
@@ -1227,6 +1233,8 @@ function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false):
'unread_count' => 1, 'unread_count' => 1,
'total_count' => 2, 'total_count' => 2,
'is_inbox' => false, 'is_inbox' => false,
'parent_id' => null,
'is_child' => false,
], ],
]; ];
$totalUnread = array_sum(array_column($demoFolders, 'unread_count')); $totalUnread = array_sum(array_column($demoFolders, 'unread_count'));
@@ -1243,12 +1251,23 @@ function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false):
return [ return [
'success' => false, 'success' => false,
'error' => 'Geen geldig Microsoft 365 token beschikbaar.', 'error' => 'Geen geldig Microsoft 365 token beschikbaar.',
'folders' => [], 'folders' => [
[
'id' => 'inbox',
'name' => 'Postvak IN',
'unread_count' => 0,
'total_count' => 0,
'is_inbox' => true,
'parent_id' => null,
'is_child' => false,
]
],
'total_unread' => 0, 'total_unread' => 0,
]; ];
} }
$url = MS_GRAPH_BASE_URL . '/me/mailFolders?$top=100&$select=id,displayName,parentFolderId,childFolderCount,unreadItemCount,totalItemCount,wellKnownName'; // 1. Haal de hoofdmappen op ZONDER restrictief $select (voorkomt 400 Bad Request op wellKnownName)
$url = MS_GRAPH_BASE_URL . '/me/mailFolders?$top=100';
$res = fociz_http_request($url, [ $res = fociz_http_request($url, [
'method' => 'GET', 'method' => 'GET',
'headers' => [ 'headers' => [
@@ -1271,73 +1290,117 @@ function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false):
'unread_count' => 0, 'unread_count' => 0,
'total_count' => 0, 'total_count' => 0,
'is_inbox' => true, 'is_inbox' => true,
'parent_id' => null,
'is_child' => false,
] ]
], ],
'total_unread' => 0, 'total_unread' => 0,
]; ];
} }
$rawFolders = $res['json']['value']; $rawFolders = is_array($res['json']['value']) ? $res['json']['value'] : [];
// Haal eventuele submappen op voor mappen met childFolderCount > 0 // 2. Haal ALTIJD expliciet de submappen van Postvak IN op
foreach ($res['json']['value'] as $f) { // (In Outlook bevinden 95% van de door gebruikers aangemaakte mappen zich direct onder Inbox)
if (!empty($f['childFolderCount']) && (int)$f['childFolderCount'] > 0 && !empty($f['id'])) { $inboxChildUrl = MS_GRAPH_BASE_URL . '/me/mailFolders/inbox/childFolders?$top=100';
$childUrl = MS_GRAPH_BASE_URL . '/me/mailFolders/' . urlencode($f['id']) . '/childFolders?$top=50&$select=id,displayName,parentFolderId,childFolderCount,unreadItemCount,totalItemCount,wellKnownName'; $inboxChildRes = fociz_http_request($inboxChildUrl, [
$cRes = fociz_http_request($childUrl, [
'method' => 'GET', 'method' => 'GET',
'headers' => [ 'headers' => [
'Authorization' => "Bearer {$token}", 'Authorization' => "Bearer {$token}",
'Accept' => 'application/json', 'Accept' => 'application/json',
], ],
'timeout' => 10, 'timeout' => 8,
]); ]);
if ($cRes['status'] === 200 && !empty($cRes['json']['value'])) { if ($inboxChildRes['status'] === 200 && !empty($inboxChildRes['json']['value']) && is_array($inboxChildRes['json']['value'])) {
foreach ($cRes['json']['value'] as $child) { foreach ($inboxChildRes['json']['value'] as $child) {
$rawFolders[] = $child; $rawFolders[] = $child;
// Als een eigen submap onder Inbox zelf ook weer submappen heeft: haal die ook op
$cfc = (int)($child['childFolderCount'] ?? 0);
$cId = $child['id'] ?? '';
if ($cfc > 0 && !empty($cId)) {
$subRes = fociz_http_request(MS_GRAPH_BASE_URL . '/me/mailFolders/' . rawurlencode($cId) . '/childFolders?$top=100', [
'method' => 'GET',
'headers' => [
'Authorization' => "Bearer {$token}",
'Accept' => 'application/json',
],
'timeout' => 6,
]);
if ($subRes['status'] === 200 && !empty($subRes['json']['value']) && is_array($subRes['json']['value'])) {
foreach ($subRes['json']['value'] as $subChild) {
$rawFolders[] = $subChild;
}
} }
} }
} }
} }
// Uitsluitingen conform gebruikerswens: SPAM / Ongewenst, Prullenbak / Deleted, Outbox en Drafts // 4. Uitsluitingen conform gebruikerswens:
$excludedWellKnown = ['junkemail', 'deleteditems', 'outbox', 'drafts', 'conflicts', 'localfailures', 'serverfailures', 'syncissues']; // "de SPAM , Ongewenste email en Prullenbak hoeven er niet in."
$excludedTerms = ['junk', 'spam', 'ongewenst', 'ongewenste e-mail', 'ongewenste email', 'deleted items', 'verwijderde items', 'prullenbak', 'trash', 'verwijderd', 'outbox', 'postvak uit', 'drafts', 'concepten']; // Ook Outbox, Drafts en technische sync logs uitsluiten
$excludedWellKnown = [
'junkemail', 'deleteditems', 'outbox', 'drafts',
'conflicts', 'localfailures', 'serverfailures', 'syncissues', 'conversationhistory',
'clutter', 'recoverableitemsdeletions'
];
$excludedExactNames = [
// Nederlands
'ongewenste e-mail', 'ongewenste email', 'ongewenst', 'spam', 'junk',
'verwijderde items', 'prullenbak', 'trash', 'verwijderd',
'postvak uit', 'outbox',
'concepten', 'drafts',
'gespreksgeschiedenis', 'conversation history',
'problemen met synchronisatie', 'synchronisatieproblemen', 'conflicten',
// Engels
'junk email', 'deleted items', 'sync issues', 'conflicts', 'local failures', 'server failures'
];
$folders = []; $folders = [];
$seenIds = []; $seenIds = [];
foreach ($rawFolders as $rf) { foreach ($rawFolders as $rf) {
$fId = $rf['id'] ?? ''; $fId = trim($rf['id'] ?? '');
if (empty($fId) || isset($seenIds[$fId])) continue; if (empty($fId) || isset($seenIds[$fId])) {
$seenIds[$fId] = true; continue;
}
// Sla verborgen mappen over
if (!empty($rf['isHidden'])) {
continue;
}
$wk = strtolower(trim($rf['wellKnownName'] ?? '')); $wk = strtolower(trim($rf['wellKnownName'] ?? ''));
if (in_array($wk, $excludedWellKnown, true)) continue; if (in_array($wk, $excludedWellKnown, true)) {
continue;
}
$dName = trim($rf['displayName'] ?? ''); $dName = trim($rf['displayName'] ?? '');
$dNameLower = strtolower($dName); $dNameLower = strtolower($dName);
$skip = false; if (in_array($dNameLower, $excludedExactNames, true)) {
foreach ($excludedTerms as $term) { continue;
if ($dNameLower === $term || strpos($dNameLower, $term) !== false) {
$skip = true;
break;
} }
if (strpos($dNameLower, 'problemen met synchronisatie') === 0 || strpos($dNameLower, 'sync issues') === 0) {
continue;
} }
if ($skip) continue;
$isInbox = ($wk === 'inbox' || $dNameLower === 'inbox' || $dNameLower === 'postvak in'); $isInbox = ($wk === 'inbox' || $dNameLower === 'inbox' || $dNameLower === 'postvak in');
$seenIds[$fId] = true;
$folders[] = [ $folders[] = [
'id' => $isInbox ? 'inbox' : $fId, 'id' => $isInbox ? 'inbox' : $fId,
'name' => $dName ?: 'Map', 'name' => $dName ?: ($isInbox ? 'Postvak IN' : 'Map'),
'unread_count' => (int)($rf['unreadItemCount'] ?? 0), 'unread_count' => (int)($rf['unreadItemCount'] ?? 0),
'total_count' => (int)($rf['totalItemCount'] ?? 0), 'total_count' => (int)($rf['totalItemCount'] ?? 0),
'is_inbox' => $isInbox, 'is_inbox' => $isInbox,
'parent_id' => $rf['parentFolderId'] ?? null,
'is_child' => !empty($rf['parentFolderId']) && !$isInbox,
]; ];
} }
// Zorg dat Inbox altijd vooraan staat // Garandeer dat Inbox altijd aanwezig is
$hasInbox = false; $hasInbox = false;
foreach ($folders as $f) { foreach ($folders as $f) {
if (!empty($f['is_inbox'])) { if (!empty($f['is_inbox'])) {
@@ -1352,9 +1415,12 @@ function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false):
'unread_count' => 0, 'unread_count' => 0,
'total_count' => 0, 'total_count' => 0,
'is_inbox' => true, 'is_inbox' => true,
'parent_id' => null,
'is_child' => false,
]); ]);
} }
// Sorteer: Inbox altijd bovenaan, daarna alfabetisch
usort($folders, function($a, $b) { usort($folders, function($a, $b) {
if (!empty($a['is_inbox'])) return -1; if (!empty($a['is_inbox'])) return -1;
if (!empty($b['is_inbox'])) return 1; if (!empty($b['is_inbox'])) return 1;
@@ -1369,7 +1435,9 @@ function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false):
'folders' => $folders, 'folders' => $folders,
'total_unread' => $totalUnread, 'total_unread' => $totalUnread,
]; ];
fociz_cache_set($cacheKey, $result, 60); // 60 seconden cache
fociz_cache_set($cacheKey, $result, 300); // 5 minuten cache
return $result; return $result;
} }
@@ -1428,7 +1496,7 @@ function microsoft_fetch_folder_messages(int $userId, string $folderId = 'inbox'
$queryParams['$filter'] = 'isRead eq false'; $queryParams['$filter'] = 'isRead eq false';
} }
$folderEndpoint = ($cleanFolderId === 'inbox') ? 'inbox' : urlencode($cleanFolderId); $folderEndpoint = ($cleanFolderId === 'inbox') ? 'inbox' : rawurlencode($cleanFolderId);
$url = MS_GRAPH_BASE_URL . '/me/mailFolders/' . $folderEndpoint . '/messages?' . http_build_query($queryParams); $url = MS_GRAPH_BASE_URL . '/me/mailFolders/' . $folderEndpoint . '/messages?' . http_build_query($queryParams);
$res = fociz_http_request($url, [ $res = fociz_http_request($url, [
'method' => 'GET', 'method' => 'GET',
+11 -8
View File
@@ -13,6 +13,11 @@ $userId = (int)($currentUser['id'] ?? 0);
$isAdmin = (($currentUser['role'] ?? '') === 'ADMIN'); $isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$forceRefresh = isset($_GET['refresh']) && $_GET['refresh'] === '1'; $forceRefresh = isset($_GET['refresh']) && $_GET['refresh'] === '1';
// Geef direct het PHP sessieslot vrij zodat achtergrondverzoeken en paginanavigatie nooit blokkeren
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
// Haal Jira data op // Haal Jira data op
$jiraData = jira_get_dashboard_data($userId, $forceRefresh); $jiraData = jira_get_dashboard_data($userId, $forceRefresh);
$isDemo = !empty($jiraData['is_demo']); $isDemo = !empty($jiraData['is_demo']);
@@ -72,6 +77,12 @@ $defaultQueueFilter = $jiraData['default_queue_filter'] ?? jira_get_user_queue_p
$userQueueFilters = $jiraData['user_queue_filters'] ?? jira_get_user_queue_preferences($userId); $userQueueFilters = $jiraData['user_queue_filters'] ?? jira_get_user_queue_preferences($userId);
$userQueueFilters = array_values(array_filter(array_map('strtoupper', array_map('trim', $userQueueFilters ?? [])))); $userQueueFilters = array_values(array_filter(array_map('strtoupper', array_map('trim', $userQueueFilters ?? []))));
$jiraData['user_queue_filters'] = $userQueueFilters; $jiraData['user_queue_filters'] = $userQueueFilters;
$hasUserAssignments = !empty($userQueueFilters);
$isCodeVisible = function(string $code) use ($hasUserAssignments, $userQueueFilters): bool {
if (!$hasUserAssignments) return true;
return in_array(strtoupper(trim($code)), $userQueueFilters, true);
};
// Hulpfunctie om te controleren of een ticket behoort tot een wachtrij / project // Hulpfunctie om te controleren of een ticket behoort tot een wachtrij / project
$issueMatchesQueue = function(array $iss, string $code) use ($queueFilters): bool { $issueMatchesQueue = function(array $iss, string $code) use ($queueFilters): bool {
@@ -597,14 +608,6 @@ $currentUserEmailJson = json_encode($currentUserEmail, JSON_HEX_TAG | JSON_HEX_A
<!-- Project Knoppen met direct ingebouwde dropdowns voor statusinstellingen & uitvoerder --> <!-- Project Knoppen met direct ingebouwde dropdowns voor statusinstellingen & uitvoerder -->
<div class="flex flex-wrap items-center gap-2 relative z-50" id="projectPillsContainer"> <div class="flex flex-wrap items-center gap-2 relative z-50" id="projectPillsContainer">
<?php
$hasUserAssignments = !empty($userQueueFilters);
$isCodeVisible = function(string $code) use ($hasUserAssignments, $userQueueFilters): bool {
if (!$hasUserAssignments) return true;
return in_array(strtoupper(trim($code)), $userQueueFilters, true);
};
?>
<!-- Alle Projecten --> <!-- Alle Projecten -->
<div class="relative inline-flex items-stretch rounded-xl shadow-xs group flex-shrink-0 project-pill-container" data-project="" id="projGroup_all"> <div class="relative inline-flex items-stretch rounded-xl shadow-xs group flex-shrink-0 project-pill-container" data-project="" id="projGroup_all">
<button type="button" onclick="selectProject('')" data-project="" class="project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-slate-900 dark:bg-white text-white dark:text-slate-900"> <button type="button" onclick="selectProject('')" data-project="" class="project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-slate-900 dark:bg-white text-white dark:text-slate-900">
+21 -4
View File
@@ -14,6 +14,11 @@ $isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$db = get_db_connection(); $db = get_db_connection();
$userBoards = fociz_get_user_boards_with_lanes($db, $userId, $isAdmin); $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) // Haal alle beschikbare mailmappen op (exclusief SPAM/Junk, Prullenbak/Deleted, Outbox, Drafts)
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1'; $forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
$foldersData = microsoft_fetch_mail_folders($userId, $forceRefresh); $foldersData = microsoft_fetch_mail_folders($userId, $forceRefresh);
@@ -24,7 +29,7 @@ $totalUnreadCount = (int)($foldersData['total_unread'] ?? 0);
$inboxData = microsoft_fetch_inbox_messages($userId, 30, false, $forceRefresh); $inboxData = microsoft_fetch_inbox_messages($userId, 30, false, $forceRefresh);
$messages = $inboxData['messages'] ?? []; $messages = $inboxData['messages'] ?? [];
$isDemo = !empty($inboxData['is_demo']) || !empty($foldersData['is_demo']); $isDemo = !empty($inboxData['is_demo']) || !empty($foldersData['is_demo']);
$warning = $inboxData['warning'] ?? ($foldersData['warning'] ?? ''); $warning = $inboxData['warning'] ?? ($foldersData['warning'] ?? ($foldersData['error'] ?? ''));
$needsReconsent = !empty($inboxData['needs_reconsent']); $needsReconsent = !empty($inboxData['needs_reconsent']);
// Bereken actuele ongelezen aantal: neem het hoogste van total_unread over mappen en inbox unread_count // Bereken actuele ongelezen aantal: neem het hoogste van total_unread over mappen en inbox unread_count
@@ -55,6 +60,18 @@ $jsonFolders = json_encode($mailFolders, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX
</div> </div>
<?php endif; ?> <?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 --> <!-- Top Banner: Status & Snelle Acties -->
<div class="fociz-card p-6 sm:p-7 relative overflow-hidden"> <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 class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
@@ -681,9 +698,9 @@ function renderFolders() {
<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"> <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 --> <!-- 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'}"> <button type="button" onclick="toggleFolder('${folder.id}')" class="w-full flex items-center justify-between p-3 select-none transition cursor-pointer ${isInbox ? 'bg-slate-100/90 dark:bg-slate-800/80' : 'bg-slate-50/80 dark:bg-slate-800/40 hover:bg-slate-100 dark:hover:bg-slate-800/70'}">
<div class="flex items-center space-x-2.5 min-w-0"> <div class="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' : 'bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300'} flex items-center justify-center flex-shrink-0"> <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'}" class="w-3.5 h-3.5"></i> <i data-lucide="${isInbox ? 'inbox' : (folder.is_child ? 'corner-down-right' : 'folder')}" class="w-3.5 h-3.5"></i>
</div> </div>
<div class="flex items-center space-x-1.5 truncate"> <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-xs font-bold text-slate-900 dark:text-white truncate">${escapeHtml(folder.name)}</span>