diff --git a/admin.php b/admin.php index 2173752..53a4ffd 100644 --- a/admin.php +++ b/admin.php @@ -536,9 +536,34 @@ try { error_log("Fout bij ophalen jira filters/users: " . $e->getMessage()); } +// Audit Logs ophalen met 3 maanden bewaartermijn en paginering per 40 items +$auditPerPage = 40; +$auditPage = max(1, (int)($_GET['audit_page'] ?? 1)); +$totalAuditLogs = 0; +$totalAuditPages = 1; $auditLogs = []; + try { - $auditLogs = $db->query("SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 50")->fetchAll() ?: []; + // Bewaartermijn: verwijder logs ouder dan 3 maanden (90 dagen) + $db->exec("DELETE FROM audit_logs WHERE created_at < NOW() - INTERVAL 90 DAY"); + + $totalAuditLogs = (int)$db->query("SELECT COUNT(*) FROM audit_logs WHERE created_at >= NOW() - INTERVAL 90 DAY")->fetchColumn(); + $totalAuditPages = max(1, (int)ceil($totalAuditLogs / $auditPerPage)); + if ($auditPage > $totalAuditPages) { + $auditPage = $totalAuditPages; + } + $offset = ($auditPage - 1) * $auditPerPage; + + $stmtLogs = $db->prepare(" + SELECT * FROM audit_logs + WHERE created_at >= NOW() - INTERVAL 90 DAY + ORDER BY created_at DESC + LIMIT :limit OFFSET :offset + "); + $stmtLogs->bindValue(':limit', $auditPerPage, PDO::PARAM_INT); + $stmtLogs->bindValue(':offset', $offset, PDO::PARAM_INT); + $stmtLogs->execute(); + $auditLogs = $stmtLogs->fetchAll() ?: []; } catch (Throwable $e) { error_log("Fout bij ophalen audit logs: " . $e->getMessage()); } @@ -1746,10 +1771,13 @@ require_once __DIR__ . '/includes/header.php'; Onwijzigbare Audit Trail

- Centrale registratie van alle inlogpogingen, 2FA-verificaties, IP-detecties en beheerdersacties conform NIST SP 800-63B. + Centrale registratie van alle inlogpogingen, 2FA-verificaties, IP-detecties en beheerdersacties conform NIST SP 800-63B (bewaartermijn: 3 maanden).

+ + + Live Audit Logging Actief • NL Tijd @@ -1842,6 +1870,68 @@ require_once __DIR__ . '/includes/header.php';
+ + + 0): ?> +
+
+ Toont t/m + van + gebeurtenissen (bewaartermijn: 3 maanden) +
+ + 1): ?> +
+ + 1): ?> + + + Vorige + + + + + Vorige + + + + + 1) { + echo '1'; + if ($startPage > 2) echo '…'; + } + for ($p = $startPage; $p <= $endPage; $p++) { + if ($p === $auditPage) { + echo '' . $p . ''; + } else { + echo '' . $p . ''; + } + } + if ($endPage < $totalAuditPages) { + if ($endPage < $totalAuditPages - 1) echo '…'; + echo '' . $totalAuditPages . ''; + } + ?> + + + + + Volgende + + + + + Volgende + + + +
+ +
+ diff --git a/includes/auth.php b/includes/auth.php index 04f9297..96f5c75 100644 --- a/includes/auth.php +++ b/includes/auth.php @@ -129,6 +129,12 @@ function fociz_log_audit(?int $userId, string $action, string $resourceType = '' $statusNormalized, $createdAt ]); + + // Bewaartermijn audit logs: 3 maanden (90 dagen) + // Voer periodiek (1 op de 20 log-acties) een automatische opschoning uit + if (mt_rand(1, 20) === 1) { + $db->exec("DELETE FROM audit_logs WHERE created_at < NOW() - INTERVAL 90 DAY"); + } } catch (Throwable $e) { // Audit log mag NOOIT de applicatiewerking breken error_log("fociz_log_audit failure: " . $e->getMessage()); @@ -419,6 +425,36 @@ function fociz_get_device_name(?string $userAgent): string { return "{$browser} op {$platform}"; } +/** + * Ruimt verlopen sessies op en beperkt het aantal sessies per gebruiker tot maximaal 20 (FIFO / oldest out) + */ +function fociz_cleanup_user_sessions(PDO $db, int $userId): void { + try { + // 1. Markeer sessies waarvan de geldigheidsduur is verstreken direct als inactief + $stmtExp = $db->prepare("UPDATE user_sessions SET is_active = 0 WHERE user_id = ? AND is_active = 1 AND expires_at < NOW()"); + $stmtExp->execute([$userId]); + + // 2. Beperk tot maximaal 20 sessies per gebruiker (FIFO: behoud de 20 meest recente, ruim oudere op) + $stmtLimit = $db->prepare(" + SELECT id FROM user_sessions + WHERE user_id = ? + ORDER BY last_activity_at DESC, id DESC + LIMIT 20, 200 + "); + $stmtLimit->execute([$userId]); + $excessIds = $stmtLimit->fetchAll(PDO::FETCH_COLUMN); + if (!empty($excessIds)) { + $inClause = implode(',', array_map('intval', $excessIds)); + $db->exec("DELETE FROM user_sessions WHERE id IN ($inClause)"); + } + + // 3. Verwijder inactieve/verlopen sessies ouder dan 7 dagen over de gehele tabel + $db->exec("DELETE FROM user_sessions WHERE is_active = 0 AND (expires_at < NOW() - INTERVAL 7 DAY OR last_activity_at < NOW() - INTERVAL 7 DAY)"); + } catch (Throwable $e) { + error_log("fociz_cleanup_user_sessions error: " . $e->getMessage()); + } +} + /** * Registreert of vernieuwt een actieve sessie in user_sessions (SOC 2 CC6.1 / ISO 27001 A.9.4.2) */ @@ -447,8 +483,8 @@ function fociz_register_active_session(int $userId, string $sessionId, int $maxI "); $stmt->execute([$userId, $hash, $ip, $ua, $deviceName, $expiresAt]); - // Verwijder oude verlopen sessies (> 7 dagen geleden verlopen) - $db->exec("DELETE FROM user_sessions WHERE expires_at < NOW() - INTERVAL 7 DAY"); + // Ruim verlopen sessies op en handhaaf max 20 sessies + fociz_cleanup_user_sessions($db, $userId); } catch (Throwable $e) { error_log("fociz_register_active_session error: " . $e->getMessage()); } @@ -505,7 +541,14 @@ function fociz_is_session_active(string $sessionId, int $userId): bool { fociz_register_active_session($userId, $sessionId, $maxIdleSeconds); return true; } - return (!empty($row['is_active'])); + if (empty($row['is_active'])) { + return false; + } + if (!empty($row['expires_at']) && strtotime($row['expires_at']) < time()) { + $db->prepare("UPDATE user_sessions SET is_active = 0 WHERE session_id_hash = ?")->execute([$hash]); + return false; + } + return true; } catch (Throwable $e) { return true; // Fallback op toegestaan bij databaseverstoring } @@ -567,7 +610,7 @@ function fociz_revoke_session_by_id(int $userId, int $sessionDbId): bool { } /** - * Haalt alle actieve sessies van een gebruiker op voor de profielpagina + * Haalt alle actieve sessies van een gebruiker op voor de profielpagina (maximaal 20, actieve expiratiedatum) */ function fociz_get_user_active_sessions(int $userId, string $currentSessionId): array { try { @@ -575,14 +618,35 @@ function fociz_get_user_active_sessions(int $userId, string $currentSessionId): ensure_sessions_table($db); $currentHash = hash('sha256', $currentSessionId); + // Voer direct opschoning uit: deactiveer verlopen sessies en handhaaf limiet van max 20 + fociz_cleanup_user_sessions($db, $userId); + $stmt = $db->prepare(" SELECT id, ip_address, user_agent, device_name, last_activity_at, created_at, (session_id_hash = ?) AS is_current FROM user_sessions - WHERE user_id = ? AND is_active = 1 + WHERE user_id = ? AND is_active = 1 AND expires_at >= NOW() ORDER BY is_current DESC, last_activity_at DESC + LIMIT 20 "); $stmt->execute([$currentHash, $userId]); - return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; + $sessions = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; + + // Zorg dat het huidige apparaat altijd geregistreerd staat als actief + $hasCurrent = false; + foreach ($sessions as $s) { + if (!empty($s['is_current'])) { + $hasCurrent = true; + break; + } + } + if (!$hasCurrent && !empty($currentSessionId)) { + $maxIdleSeconds = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800; + fociz_register_active_session($userId, $currentSessionId, $maxIdleSeconds); + $stmt->execute([$currentHash, $userId]); + $sessions = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; + } + + return $sessions; } catch (Throwable $e) { return []; } diff --git a/includes/integrations/jira.php b/includes/integrations/jira.php index af84df2..75a799d 100644 --- a/includes/integrations/jira.php +++ b/includes/integrations/jira.php @@ -331,6 +331,82 @@ function jira_fetch_statuses(string $domain, string $email, string $apiToken): a return $statuses; } +/** + * Haalt project-specifieke statussen op uit Jira Cloud voor een specifiek project + */ +function jira_fetch_project_statuses(string $domain, string $email, string $apiToken, string $projectKey): array { + $cleanDomain = jira_normalize_domain($domain); + $pKey = strtoupper(trim($projectKey)); + if (empty($cleanDomain) || empty($email) || empty($apiToken) || empty($pKey)) { + return []; + } + + $cacheKey = "jira_proj_statuses_" . $pKey . "_" . md5($cleanDomain); + $cached = fociz_cache_get($cacheKey); + if (is_array($cached) && !empty($cached)) { + return $cached; + } + + $authHeader = jira_get_auth_header($email, $apiToken); + $url = $cleanDomain . '/rest/api/3/project/' . rawurlencode($pKey) . '/statuses'; + + $response = fociz_http_request($url, [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => $authHeader, + 'Accept' => 'application/json', + ], + 'timeout' => 8, + ]); + + if ($response['status'] === 404 || $response['status'] === 405) { + $urlV2 = $cleanDomain . '/rest/api/2/project/' . rawurlencode($pKey) . '/statuses'; + $response = fociz_http_request($urlV2, [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => $authHeader, + 'Accept' => 'application/json', + ], + 'timeout' => 8, + ]); + } + + $statuses = []; + if ($response['status'] === 200 && is_array($response['json'])) { + foreach ($response['json'] as $issueType) { + if (!empty($issueType['statuses']) && is_array($issueType['statuses'])) { + foreach ($issueType['statuses'] as $st) { + $sName = trim($st['name'] ?? ''); + if (empty($sName)) continue; + $sKey = strtolower($sName); + $catKey = strtolower($st['statusCategory']['key'] ?? 'indeterminate'); + $catName = $st['statusCategory']['name'] ?? 'In uitvoering'; + $statuses[$sKey] = [ + 'id' => (string)($st['id'] ?? ''), + 'name' => $sName, + 'description' => $st['description'] ?? '', + 'status_category' => $catKey, + 'category_name' => $catName, + 'color' => $st['statusCategory']['colorName'] ?? '', + ]; + } + } + } + $statuses = array_values($statuses); + usort($statuses, function($a, $b) { + $order = ['new' => 1, 'indeterminate' => 2, 'done' => 3]; + $oa = $order[$a['status_category']] ?? 2; + $ob = $order[$b['status_category']] ?? 2; + if ($oa !== $ob) return $oa - $ob; + return strcasecmp($a['name'], $b['name']); + }); + + fociz_cache_set($cacheKey, $statuses, 3600); + } + + return $statuses; +} + /** * Haalt issues op via een flexibele JQL query via Atlassian REST API */ @@ -974,6 +1050,34 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array ], ]; + $demoProjectStatuses = [ + 'SD' => [ + ['id' => '1', 'name' => 'To Do', 'status_category' => 'new', 'category_name' => 'Nieuw'], + ['id' => '2', 'name' => 'In behandeling', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], + ['id' => '3', 'name' => 'Wachten op klant', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], + ['id' => '4', 'name' => 'Gesloten', 'status_category' => 'done', 'category_name' => 'Gereed'], + ], + 'IN' => [ + ['id' => '10', 'name' => 'Nieuw', 'status_category' => 'new', 'category_name' => 'Nieuw'], + ['id' => '11', 'name' => 'In behandeling', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], + ['id' => '12', 'name' => 'In Review', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], + ['id' => '13', 'name' => 'Gereed', 'status_category' => 'done', 'category_name' => 'Gereed'], + ['id' => '14', 'name' => 'Gesloten', 'status_category' => 'done', 'category_name' => 'Gereed'], + ], + 'AAN' => [ + ['id' => '20', 'name' => 'Nieuw', 'status_category' => 'new', 'category_name' => 'Nieuw'], + ['id' => '21', 'name' => 'In behandeling', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], + ['id' => '22', 'name' => 'Goedgekeurd', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], + ['id' => '23', 'name' => 'Afgewezen', 'status_category' => 'done', 'category_name' => 'Gereed'], + ['id' => '24', 'name' => 'Gesloten', 'status_category' => 'done', 'category_name' => 'Gereed'], + ], + 'GEN' => [ + ['id' => '30', 'name' => 'To Do', 'status_category' => 'new', 'category_name' => 'Nieuw'], + ['id' => '31', 'name' => 'In Progress', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], + ['id' => '32', 'name' => 'Done', 'status_category' => 'done', 'category_name' => 'Gereed'], + ], + ]; + return [ 'is_demo' => true, 'success' => true, @@ -987,6 +1091,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array 'default_queue_filter' => $userQueuePref, 'user_queue_filters' => $userQueuePrefs, 'available_statuses' => $demoStatuses, + 'project_statuses' => $demoProjectStatuses, ]; } @@ -1041,6 +1146,23 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array $assignedIssues = $assignedRes['success'] ? $assignedRes['issues'] : []; $teamIssues = $teamRes['success'] ? $teamRes['issues'] : []; + // Project-specifieke statussen ophalen voor gebruikte projecten + $activeProjKeys = ['SD', 'IN', 'AAN', 'GEN']; + $allLoaded = array_merge($assignedIssues, $teamIssues); + foreach ($allLoaded as $iss) { + $pk = strtoupper(trim($iss['project_key'] ?? '')); + if (!empty($pk) && !in_array($pk, $activeProjKeys, true)) { + $activeProjKeys[] = $pk; + } + } + $projectStatusesMap = []; + foreach ($activeProjKeys as $pk) { + $pStats = jira_fetch_project_statuses($domain, $email, $apiToken, $pk); + if (!empty($pStats)) { + $projectStatusesMap[$pk] = $pStats; + } + } + $data = [ 'is_demo' => false, 'success' => true, @@ -1054,6 +1176,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array 'default_queue_filter' => $userQueuePref, 'user_queue_filters' => $userQueuePrefs, 'available_statuses' => $availableStatuses, + 'project_statuses' => $projectStatusesMap, ]; fociz_cache_set($cacheKey, $data, 60); diff --git a/jira.php b/jira.php index 1cb22d6..fa4bbc1 100644 --- a/jira.php +++ b/jira.php @@ -56,20 +56,37 @@ foreach ($boards as &$b) { } unset($b); +// Verzamel unieke projecten en types voor filter dropdowns en project switcher // Verzamel unieke projecten en types voor filter dropdowns en project switcher $allIssues = array_merge($assignedIssues, $teamIssues); $allProjects = []; $allTypes = []; $allPriorities = ['Highest', 'High', 'Medium', 'Low']; -$allStatuses = []; $projectCounts = []; +// Core projecten met styling en namen +$coreProjects = [ + 'SD' => ['key' => 'SD', 'name' => 'Servicedesk', 'color' => 'sky'], + 'IN' => ['key' => 'IN', 'name' => 'Interfaces', 'color' => 'amber'], + 'AAN' => ['key' => 'AAN', 'name' => 'Aanvragen', 'color' => 'emerald'], + 'GEN' => ['key' => 'GEN', 'name' => 'General', 'color' => 'indigo'], +]; + +$usedProjectKeys = []; foreach ($allIssues as $iss) { + $pKey = strtoupper(trim($iss['project_key'] ?? '')); + if (empty($pKey) && !empty($iss['key']) && preg_match('/^([A-Z0-9]+)-/i', $iss['key'], $m)) { + $pKey = strtoupper($m[1]); + } + if (!empty($pKey)) { + $usedProjectKeys[$pKey] = true; + } + if (!empty($iss['project'])) { $p = $iss['project']; $allProjects[$p] = [ 'name' => $p, - 'key' => $iss['project_key'] ?? '', + 'key' => $pKey ?: ($iss['project_key'] ?? ''), ]; $projectCounts[$p] = ($projectCounts[$p] ?? 0) + 1; } @@ -77,60 +94,20 @@ foreach ($allIssues as $iss) { $allTypes[$iss['issue_type']] = $iss['issue_type']; } if (!empty($iss['status'])) { - $sName = trim($iss['status']); - $sKey = strtolower($sName); - $allStatuses[$sKey] = [ - 'name' => $sName, - 'status_category' => $iss['status_category'] ?? 'indeterminate', - 'category_name' => '', - ]; + $allStatuses[$iss['status']] = $iss['status']; } } -// Statussen uitbreiden met ALLE officiële Jira Cloud instantie statussen +// Statussen uitbreiden met officiële Jira instantie statussen $availableStatuses = $jiraData['available_statuses'] ?? []; if (!empty($availableStatuses)) { foreach ($availableStatuses as $st) { if (!empty($st['name'])) { - $sName = trim($st['name']); - $sKey = strtolower($sName); - $cat = $st['status_category'] ?? ($allStatuses[$sKey]['status_category'] ?? 'indeterminate'); - $allStatuses[$sKey] = [ - 'name' => $sName, - 'status_category' => $cat, - 'category_name' => $st['category_name'] ?? '', - ]; + $allStatuses[$st['name']] = $st['name']; } } } -// Fallback statussen als Jira nog geen verbinding heeft -if (empty($allStatuses)) { - $fallbackList = [ - ['name' => 'To Do', 'status_category' => 'new'], - ['name' => 'In behandeling', 'status_category' => 'indeterminate'], - ['name' => 'In Progress', 'status_category' => 'indeterminate'], - ['name' => 'Wachten op klant', 'status_category' => 'indeterminate'], - ['name' => 'In Review', 'status_category' => 'indeterminate'], - ['name' => 'Gesloten', 'status_category' => 'done'], - ['name' => 'Done', 'status_category' => 'done'], - ]; - foreach ($fallbackList as $fb) { - $allStatuses[strtolower($fb['name'])] = $fb; - } -} - -// Sorteren: To Do / Nieuw (1), In Uitvoering / Behandeling (2), Gesloten / Gereed (3), daarbinnen alfabetisch -uasort($allStatuses, function($a, $b) { - $order = ['new' => 1, 'indeterminate' => 2, 'done' => 3]; - $oa = $order[$a['status_category'] ?? 'indeterminate'] ?? 2; - $ob = $order[$b['status_category'] ?? 'indeterminate'] ?? 2; - if ($oa !== $ob) return $oa - $ob; - return strcasecmp($a['name'], $b['name']); -}); - -$allStatusesList = array_values($allStatuses); - ksort($allProjects); ksort($allTypes); @@ -140,7 +117,6 @@ $userQueueFilters = $jiraData['user_queue_filters'] ?? jira_get_user_queue_prefe $jiraDataJson = json_encode($jiraData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); -$statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>
@@ -341,7 +317,7 @@ $statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSO
-
+
@@ -470,55 +446,51 @@ $statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSO
- -
- - + +
+ +
+ + + + + + + +
+ +
-
- - - - -
+ +
-
+
@@ -901,22 +873,6 @@ let selectedStatuses = new Set(); let activeUrgentOnly = false; let expandedKeys = new Set(); const ticketStore = new Map(); -const allJiraStatuses = ; - -// Hulpfunctie voor deduplicatie van tickets (assigned + team) -function getDeduplicatedIssues() { - const assigned = jiraData.assigned_issues || []; - const team = jiraData.team_issues || []; - const seen = new Set(); - const list = []; - [...assigned, ...team].forEach(iss => { - if (iss && iss.key && !seen.has(iss.key)) { - seen.add(iss.key); - list.push(iss); - } - }); - return list; -} function isDoneStatus(st) { const s = (st || '').toLowerCase().trim(); @@ -983,7 +939,7 @@ function toggleJiraFiltersDrawer(forceOpen) { } } -// Bepaal aantal actieve filters voor badge +// Bepaal aantal actieve filters voor de badge in de rechter onderhoek function updateActiveFilterBadge() { const badge = document.getElementById('activeFilterBadge'); if (!badge) return; @@ -993,7 +949,8 @@ function updateActiveFilterBadge() { if (currentQueue && currentQueue !== 'MY_QUEUES') activeCount++; if (document.getElementById('filterType')?.value) activeCount++; if (document.getElementById('filterPriority')?.value) activeCount++; - if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) activeCount++; + if (document.getElementById('filterStatus')?.value) activeCount++; + if (onlyInProgress) activeCount++; if (activeCount > 0) { badge.innerText = activeCount; @@ -1003,236 +960,73 @@ function updateActiveFilterBadge() { } } -// ========================================== -// STATUS MULTI-SELECT CHECKBOX DROPDOWN LOGICA -// ========================================== +// Wissel tussen tabs +function switchTab(tab) { + currentTab = tab; + activeUrgentOnly = false; -function renderStatusCheckboxes() { - const container = document.getElementById('statusCheckboxList'); - if (!container) return; - - if (!allJiraStatuses || allJiraStatuses.length === 0) { - container.innerHTML = '
Geen Jira statussen gevonden
'; - return; - } - - let html = ''; - allJiraStatuses.forEach((st, idx) => { - const sNorm = (st.name || '').toLowerCase().trim(); - const isChecked = selectedStatuses.has(sNorm); - const cat = (st.status_category || '').toLowerCase().trim(); - - let dotColor = 'bg-blue-500'; - if (cat === 'new' || sNorm.includes('to do') || sNorm.includes('open') || sNorm.includes('nieuw')) { - dotColor = 'bg-slate-400'; - } else if (cat === 'done' || isDoneStatus(sNorm)) { - dotColor = 'bg-emerald-500'; - } else if (sNorm.includes('wacht') || sNorm.includes('klant') || sNorm.includes('pending')) { - dotColor = 'bg-amber-500'; - } - - const safeId = 'chkStatus_' + idx; - - html += ` - - `; - }); - - container.innerHTML = html; - updateStatusCounts(); -} - -function updateStatusCounts() { - const all = getDeduplicatedIssues(); - allJiraStatuses.forEach((st, idx) => { - const sNorm = (st.name || '').toLowerCase().trim(); - const el = document.getElementById(`statusCount_${idx}`); - if (el) { - let currentList = all; - if (currentProject) { - const pNorm = currentProject.trim().toUpperCase(); - currentList = currentList.filter(iss => { - const pk = (iss.project_key || '').trim().toUpperCase(); - const pn = (iss.project || '').trim().toLowerCase(); - return pk === pNorm || pn.includes(pNorm.toLowerCase()); - }); - } - if (currentQueue) { - currentList = currentList.filter(iss => isIssueInQueue(iss, currentQueue)); - } - const cnt = currentList.filter(iss => (iss.status || '').toLowerCase().trim() === sNorm).length; - el.innerText = cnt; + // Update tab knoppen styling + const tabs = ['assigned', 'team', 'in_progress', 'done']; + tabs.forEach(t => { + const btn = document.getElementById(`tabBtn_${t}`); + if (!btn) return; + if (t === tab) { + btn.className = "flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800/60 flex-shrink-0"; + } else { + btn.className = "flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0"; } }); -} -function toggleStatusDropdown(force) { - const menu = document.getElementById('statusDropdownMenu'); - const chevron = document.getElementById('statusDropdownChevron'); - const btn = document.getElementById('statusDropdownBtn'); - if (!menu) return; - - const willOpen = (typeof force === 'boolean') ? force : menu.classList.contains('hidden'); - if (willOpen) { - menu.classList.remove('hidden'); - if (chevron) chevron.classList.add('rotate-180'); - if (btn) btn.setAttribute('aria-expanded', 'true'); - updateStatusCounts(); - } else { - menu.classList.add('hidden'); - if (chevron) chevron.classList.remove('rotate-180'); - if (btn) btn.setAttribute('aria-expanded', 'false'); - } -} - -document.addEventListener('click', (e) => { - const wrapper = document.getElementById('statusDropdownWrapper'); - if (wrapper && !wrapper.contains(e.target)) { - toggleStatusDropdown(false); - } -}); - -function onStatusCheckboxChange() { - selectedStatuses.clear(); - const checked = document.querySelectorAll('.status-checkbox:checked'); - checked.forEach(chk => { - if (chk.value) selectedStatuses.add(chk.value.toLowerCase().trim()); - }); - - try { - localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses])); - } catch (e) {} - - updateStatusDropdownLabel(); applyFilters(); } -function selectAllStatuses(selectAll) { - selectedStatuses.clear(); - const chks = document.querySelectorAll('.status-checkbox'); - chks.forEach(chk => { - chk.checked = !!selectAll; - if (selectAll && chk.value) { - selectedStatuses.add(chk.value.toLowerCase().trim()); - } - }); - - try { - localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses])); - } catch (e) {} - - updateStatusDropdownLabel(); +// Toggle: Verberg Gesloten +function toggleHideClosed() { + hideClosed = !hideClosed; + updateHideClosedButton(); applyFilters(); } -function selectActiveStatusesOnly() { - selectedStatuses.clear(); - const chks = document.querySelectorAll('.status-checkbox'); - chks.forEach(chk => { - const val = (chk.value || '').toLowerCase().trim(); - const isDone = isDoneStatus(val); - chk.checked = !isDone; - if (!isDone) { - selectedStatuses.add(val); - } - }); +function updateHideClosedButton() { + const btn = document.getElementById('btnHideClosed'); + if (!btn) return; + if (hideClosed) { + btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80 shadow-2xs"; + btn.innerHTML = `Verberg Gesloten`; + } else { + btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700"; + btn.innerHTML = `Toon Ook Gesloten`; + } + if (typeof lucide !== 'undefined') lucide.createIcons(); +} - try { - localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses])); - } catch (e) {} - - updateStatusDropdownLabel(); +// Toggle: Snelfilter Alleen In Behandeling +function toggleOnlyInProgress() { + onlyInProgress = !onlyInProgress; + updateOnlyInProgressButton(); applyFilters(); } -function updateStatusDropdownLabel() { - const labelEl = document.getElementById('statusDropdownLabel'); - const badgeEl = document.getElementById('statusDropdownBadge'); - if (!labelEl) return; - - const total = allJiraStatuses.length; - const selected = selectedStatuses.size; - - if (selected === total || (total === 0 && selected === 0)) { - labelEl.innerText = "Alle statussen"; - if (badgeEl) badgeEl.classList.add('hidden'); - } else if (selected === 0) { - labelEl.innerText = "Geen geselecteerd"; - if (badgeEl) { - badgeEl.innerText = "0"; - badgeEl.classList.remove('hidden'); - badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-rose-100 dark:bg-rose-950/60 text-rose-700 dark:text-rose-300"; - } - } else if (selected === 1) { - const singleNorm = [...selectedStatuses][0]; - const match = allJiraStatuses.find(st => (st.name || '').toLowerCase().trim() === singleNorm); - labelEl.innerText = match ? match.name : singleNorm; - if (badgeEl) { - badgeEl.innerText = "1"; - badgeEl.classList.remove('hidden'); - badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300"; - } +function updateOnlyInProgressButton() { + const btn = document.getElementById('btnOnlyInProgress'); + if (!btn) return; + if (onlyInProgress) { + btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-blue-600 text-white shadow-xs"; + btn.innerHTML = `In behandeling (Actief)`; } else { - labelEl.innerText = `${selected} statussen`; - if (badgeEl) { - badgeEl.innerText = `${selected}/${total}`; - badgeEl.classList.remove('hidden'); - badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300"; - } - } - - const btnReset = document.getElementById('btnResetFiltersInline'); - if (btnReset) { - const isCustom = selected < total || currentProject !== '' || (currentQueue !== '' && currentQueue !== 'MY_QUEUES') || document.getElementById('jiraSearchInput')?.value; - if (isCustom) btnReset.classList.remove('hidden'); - else btnReset.classList.add('hidden'); + btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700"; + btn.innerHTML = `In behandeling`; } + if (typeof lucide !== 'undefined') lucide.createIcons(); } -function initStatusSelection() { - selectedStatuses = new Set(); - let saved = null; - try { - const raw = localStorage.getItem('fociz_jira_selected_statuses'); - if (raw) saved = JSON.parse(raw); - } catch (e) {} - - if (Array.isArray(saved) && saved.length > 0) { - saved.forEach(s => { - if (s) selectedStatuses.add(String(s).toLowerCase().trim()); - }); - } else { - // Standaard alle actieve statussen selecteren, gesloten uit conform gebruikerswens - allJiraStatuses.forEach(st => { - const sNorm = (st.name || '').toLowerCase().trim(); - const cat = (st.status_category || '').toLowerCase().trim(); - if (cat !== 'done' && !isDoneStatus(sNorm)) { - selectedStatuses.add(sNorm); - } - }); - if (selectedStatuses.size === 0) { - allJiraStatuses.forEach(st => { - selectedStatuses.add((st.name || '').toLowerCase().trim()); - }); - } +// Status dropdown verandering +function onStatusSelectChange(val) { + if (val && isDoneStatus(val)) { + hideClosed = false; + updateHideClosedButton(); } - - renderStatusCheckboxes(); - updateStatusDropdownLabel(); + applyFilters(); } // Project selectie via pill tabs @@ -1348,13 +1142,21 @@ function updateQueuePills() { } function updateProjectCounts() { - const all = getDeduplicatedIssues(); - let currentList = all; + const assigned = jiraData.assigned_issues || []; + const team = jiraData.team_issues || []; + const all = [...assigned, ...team]; - if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) { - currentList = currentList.filter(iss => selectedStatuses.has((iss.status || '').toLowerCase().trim())); - } else if (selectedStatuses.size === 0) { - currentList = []; + let currentList = all; + if (currentTab === 'assigned') currentList = assigned; + else if (currentTab === 'team') currentList = team; + else if (currentTab === 'in_progress') { + currentList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category)); + } else if (currentTab === 'done') { + currentList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done'); + } + + if (hideClosed && currentTab !== 'done') { + currentList = currentList.filter(iss => !isDoneStatus(iss.status) && (iss.status_category || '').toLowerCase() !== 'done'); } const elAll = document.getElementById('projCount_all'); @@ -1379,13 +1181,21 @@ function updateProjectCounts() { } function updateQueueCounts() { - const all = getDeduplicatedIssues(); - let currentList = all; + const assigned = jiraData.assigned_issues || []; + const team = jiraData.team_issues || []; + const all = [...assigned, ...team]; - if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) { - currentList = currentList.filter(iss => selectedStatuses.has((iss.status || '').toLowerCase().trim())); - } else if (selectedStatuses.size === 0) { - currentList = []; + let currentList = all; + if (currentTab === 'assigned') currentList = assigned; + else if (currentTab === 'team') currentList = team; + else if (currentTab === 'in_progress') { + currentList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category)); + } else if (currentTab === 'done') { + currentList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done'); + } + + if (hideClosed && currentTab !== 'done') { + currentList = currentList.filter(iss => !isDoneStatus(iss.status) && (iss.status_category || '').toLowerCase() !== 'done'); } if (currentProject) { @@ -1494,10 +1304,11 @@ function applyFilters() { const issPKey = (iss.project_key || '').toUpperCase().trim(); const issPName = (iss.project || '').toLowerCase().trim(); const issStatus = (iss.status || '').toLowerCase().trim(); + const issCat = (iss.status_category || '').toLowerCase().trim(); - // 1. Status Filter via Checkboxes - if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) { - if (!selectedStatuses.has(issStatus)) { + // 1. Verberg Gesloten filter (als tab niet 'done' is en er niet expliciet een gesloten status gekozen is) + if (currentTab !== 'done' && hideClosed && !isDoneStatus(statusVal)) { + if (issCat === 'done' || isDoneStatus(issStatus)) { return false; } } else if (selectedStatuses.size === 0) { diff --git a/profile.php b/profile.php index 7ab2eeb..cc39f99 100644 --- a/profile.php +++ b/profile.php @@ -1284,7 +1284,7 @@ require_once __DIR__ . '/includes/header.php';

- Centraal overzicht van alle actieve inlogapparaten conform ISO 27001 en SOC 2 richtlijnen. + Centraal overzicht van alle actieve inlogapparaten conform ISO 27001 en SOC 2 richtlijnen (maximaal 20 actieve sessies).