Dev #84
@@ -536,9 +536,34 @@ try {
|
|||||||
error_log("Fout bij ophalen jira filters/users: " . $e->getMessage());
|
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 = [];
|
$auditLogs = [];
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (Throwable $e) {
|
||||||
error_log("Fout bij ophalen audit logs: " . $e->getMessage());
|
error_log("Fout bij ophalen audit logs: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
@@ -1746,10 +1771,13 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
Onwijzigbare Audit Trail
|
Onwijzigbare Audit Trail
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
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).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2 flex-shrink-0">
|
<div class="flex items-center space-x-2 flex-shrink-0">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/60">
|
||||||
|
<?= $totalAuditLogs ?> <?= $totalAuditLogs === 1 ? 'gebeurtenis' : 'gebeurtenissen' ?>
|
||||||
|
</span>
|
||||||
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20" title="Tijdzone: Europe/Amsterdam (UTC+2 in zomertijd / UTC+1 in wintertijd)">
|
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20" title="Tijdzone: Europe/Amsterdam (UTC+2 in zomertijd / UTC+1 in wintertijd)">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5 animate-pulse"></span>
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5 animate-pulse"></span>
|
||||||
Live Audit Logging Actief • NL Tijd
|
Live Audit Logging Actief • NL Tijd
|
||||||
@@ -1842,6 +1870,68 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Paginering Navigatie (40 items per pagina, 3 maanden retentie) -->
|
||||||
|
<?php if ($totalAuditLogs > 0): ?>
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-4 border-t border-slate-200 dark:border-slate-800 text-xs">
|
||||||
|
<div class="text-slate-500 dark:text-slate-400">
|
||||||
|
Toont <span class="font-semibold text-slate-800 dark:text-slate-200"><?= ($offset + 1) ?></span> t/m
|
||||||
|
<span class="font-semibold text-slate-800 dark:text-slate-200"><?= min($totalAuditLogs, $offset + count($auditLogs)) ?></span> van
|
||||||
|
<span class="font-semibold text-slate-800 dark:text-slate-200"><?= $totalAuditLogs ?></span> gebeurtenissen (bewaartermijn: 3 maanden)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($totalAuditPages > 1): ?>
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<!-- Vorige knop -->
|
||||||
|
<?php if ($auditPage > 1): ?>
|
||||||
|
<a href="?tab=security&audit_page=<?= ($auditPage - 1) ?>#tab-pane-security" class="inline-flex items-center space-x-1 px-3 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/80 font-medium transition cursor-pointer">
|
||||||
|
<i data-lucide="chevron-left" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Vorige</span>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="inline-flex items-center space-x-1 px-3 py-1.5 rounded-lg border border-slate-100 dark:border-slate-800 text-slate-300 dark:text-slate-600 bg-slate-50/50 dark:bg-slate-900/30 cursor-not-allowed">
|
||||||
|
<i data-lucide="chevron-left" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Vorige</span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Paginanummers -->
|
||||||
|
<?php
|
||||||
|
$startPage = max(1, $auditPage - 2);
|
||||||
|
$endPage = min($totalAuditPages, $auditPage + 2);
|
||||||
|
if ($startPage > 1) {
|
||||||
|
echo '<a href="?tab=security&audit_page=1#tab-pane-security" class="px-2.5 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 font-medium transition cursor-pointer">1</a>';
|
||||||
|
if ($startPage > 2) echo '<span class="px-1 text-slate-400">…</span>';
|
||||||
|
}
|
||||||
|
for ($p = $startPage; $p <= $endPage; $p++) {
|
||||||
|
if ($p === $auditPage) {
|
||||||
|
echo '<span class="px-2.5 py-1.5 rounded-lg font-bold bg-indigo-600 text-white shadow-xs">' . $p . '</span>';
|
||||||
|
} else {
|
||||||
|
echo '<a href="?tab=security&audit_page=' . $p . '#tab-pane-security" class="px-2.5 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 font-medium transition cursor-pointer">' . $p . '</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($endPage < $totalAuditPages) {
|
||||||
|
if ($endPage < $totalAuditPages - 1) echo '<span class="px-1 text-slate-400">…</span>';
|
||||||
|
echo '<a href="?tab=security&audit_page=' . $totalAuditPages . '#tab-pane-security" class="px-2.5 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 font-medium transition cursor-pointer">' . $totalAuditPages . '</a>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Volgende knop -->
|
||||||
|
<?php if ($auditPage < $totalAuditPages): ?>
|
||||||
|
<a href="?tab=security&audit_page=<?= ($auditPage + 1) ?>#tab-pane-security" class="inline-flex items-center space-x-1 px-3 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/80 font-medium transition cursor-pointer">
|
||||||
|
<span>Volgende</span>
|
||||||
|
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="inline-flex items-center space-x-1 px-3 py-1.5 rounded-lg border border-slate-100 dark:border-slate-800 text-slate-300 dark:text-slate-600 bg-slate-50/50 dark:bg-slate-900/30 cursor-not-allowed">
|
||||||
|
<span>Volgende</span>
|
||||||
|
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+70
-6
@@ -129,6 +129,12 @@ function fociz_log_audit(?int $userId, string $action, string $resourceType = ''
|
|||||||
$statusNormalized,
|
$statusNormalized,
|
||||||
$createdAt
|
$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) {
|
} catch (Throwable $e) {
|
||||||
// Audit log mag NOOIT de applicatiewerking breken
|
// Audit log mag NOOIT de applicatiewerking breken
|
||||||
error_log("fociz_log_audit failure: " . $e->getMessage());
|
error_log("fociz_log_audit failure: " . $e->getMessage());
|
||||||
@@ -419,6 +425,36 @@ function fociz_get_device_name(?string $userAgent): string {
|
|||||||
return "{$browser} op {$platform}";
|
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)
|
* 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]);
|
$stmt->execute([$userId, $hash, $ip, $ua, $deviceName, $expiresAt]);
|
||||||
|
|
||||||
// Verwijder oude verlopen sessies (> 7 dagen geleden verlopen)
|
// Ruim verlopen sessies op en handhaaf max 20 sessies
|
||||||
$db->exec("DELETE FROM user_sessions WHERE expires_at < NOW() - INTERVAL 7 DAY");
|
fociz_cleanup_user_sessions($db, $userId);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
error_log("fociz_register_active_session error: " . $e->getMessage());
|
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);
|
fociz_register_active_session($userId, $sessionId, $maxIdleSeconds);
|
||||||
return true;
|
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) {
|
} catch (Throwable $e) {
|
||||||
return true; // Fallback op toegestaan bij databaseverstoring
|
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 {
|
function fociz_get_user_active_sessions(int $userId, string $currentSessionId): array {
|
||||||
try {
|
try {
|
||||||
@@ -575,14 +618,35 @@ function fociz_get_user_active_sessions(int $userId, string $currentSessionId):
|
|||||||
ensure_sessions_table($db);
|
ensure_sessions_table($db);
|
||||||
$currentHash = hash('sha256', $currentSessionId);
|
$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("
|
$stmt = $db->prepare("
|
||||||
SELECT id, ip_address, user_agent, device_name, last_activity_at, created_at, (session_id_hash = ?) AS is_current
|
SELECT id, ip_address, user_agent, device_name, last_activity_at, created_at, (session_id_hash = ?) AS is_current
|
||||||
FROM user_sessions
|
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
|
ORDER BY is_current DESC, last_activity_at DESC
|
||||||
|
LIMIT 20
|
||||||
");
|
");
|
||||||
$stmt->execute([$currentHash, $userId]);
|
$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) {
|
} catch (Throwable $e) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,6 +331,82 @@ function jira_fetch_statuses(string $domain, string $email, string $apiToken): a
|
|||||||
return $statuses;
|
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
|
* 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 [
|
return [
|
||||||
'is_demo' => true,
|
'is_demo' => true,
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@@ -987,6 +1091,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
|
|||||||
'default_queue_filter' => $userQueuePref,
|
'default_queue_filter' => $userQueuePref,
|
||||||
'user_queue_filters' => $userQueuePrefs,
|
'user_queue_filters' => $userQueuePrefs,
|
||||||
'available_statuses' => $demoStatuses,
|
'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'] : [];
|
$assignedIssues = $assignedRes['success'] ? $assignedRes['issues'] : [];
|
||||||
$teamIssues = $teamRes['success'] ? $teamRes['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 = [
|
$data = [
|
||||||
'is_demo' => false,
|
'is_demo' => false,
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@@ -1054,6 +1176,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
|
|||||||
'default_queue_filter' => $userQueuePref,
|
'default_queue_filter' => $userQueuePref,
|
||||||
'user_queue_filters' => $userQueuePrefs,
|
'user_queue_filters' => $userQueuePrefs,
|
||||||
'available_statuses' => $availableStatuses,
|
'available_statuses' => $availableStatuses,
|
||||||
|
'project_statuses' => $projectStatusesMap,
|
||||||
];
|
];
|
||||||
|
|
||||||
fociz_cache_set($cacheKey, $data, 60);
|
fociz_cache_set($cacheKey, $data, 60);
|
||||||
|
|||||||
@@ -56,20 +56,37 @@ foreach ($boards as &$b) {
|
|||||||
}
|
}
|
||||||
unset($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
|
// Verzamel unieke projecten en types voor filter dropdowns en project switcher
|
||||||
$allIssues = array_merge($assignedIssues, $teamIssues);
|
$allIssues = array_merge($assignedIssues, $teamIssues);
|
||||||
$allProjects = [];
|
$allProjects = [];
|
||||||
$allTypes = [];
|
$allTypes = [];
|
||||||
$allPriorities = ['Highest', 'High', 'Medium', 'Low'];
|
$allPriorities = ['Highest', 'High', 'Medium', 'Low'];
|
||||||
$allStatuses = [];
|
|
||||||
$projectCounts = [];
|
$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) {
|
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'])) {
|
if (!empty($iss['project'])) {
|
||||||
$p = $iss['project'];
|
$p = $iss['project'];
|
||||||
$allProjects[$p] = [
|
$allProjects[$p] = [
|
||||||
'name' => $p,
|
'name' => $p,
|
||||||
'key' => $iss['project_key'] ?? '',
|
'key' => $pKey ?: ($iss['project_key'] ?? ''),
|
||||||
];
|
];
|
||||||
$projectCounts[$p] = ($projectCounts[$p] ?? 0) + 1;
|
$projectCounts[$p] = ($projectCounts[$p] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
@@ -77,60 +94,20 @@ foreach ($allIssues as $iss) {
|
|||||||
$allTypes[$iss['issue_type']] = $iss['issue_type'];
|
$allTypes[$iss['issue_type']] = $iss['issue_type'];
|
||||||
}
|
}
|
||||||
if (!empty($iss['status'])) {
|
if (!empty($iss['status'])) {
|
||||||
$sName = trim($iss['status']);
|
$allStatuses[$iss['status']] = $iss['status'];
|
||||||
$sKey = strtolower($sName);
|
|
||||||
$allStatuses[$sKey] = [
|
|
||||||
'name' => $sName,
|
|
||||||
'status_category' => $iss['status_category'] ?? 'indeterminate',
|
|
||||||
'category_name' => '',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statussen uitbreiden met ALLE officiële Jira Cloud instantie statussen
|
// Statussen uitbreiden met officiële Jira instantie statussen
|
||||||
$availableStatuses = $jiraData['available_statuses'] ?? [];
|
$availableStatuses = $jiraData['available_statuses'] ?? [];
|
||||||
if (!empty($availableStatuses)) {
|
if (!empty($availableStatuses)) {
|
||||||
foreach ($availableStatuses as $st) {
|
foreach ($availableStatuses as $st) {
|
||||||
if (!empty($st['name'])) {
|
if (!empty($st['name'])) {
|
||||||
$sName = trim($st['name']);
|
$allStatuses[$st['name']] = $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'] ?? '',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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($allProjects);
|
||||||
ksort($allTypes);
|
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);
|
$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);
|
$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);
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="space-y-6 w-full">
|
<div class="space-y-6 w-full">
|
||||||
@@ -341,7 +317,7 @@ $statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSO
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hoofdbalk: Project Switcher, Wachtrijen, Tabs & Status Filtering -->
|
<!-- Hoofdbalk: Project Switcher, Wachtrijen, Tabs & Status Filtering -->
|
||||||
<div class="fociz-card p-4 sm:p-5 space-y-4 shadow-sm border border-slate-200/80 dark:border-slate-800">
|
<div class="fociz-card relative z-30 p-4 sm:p-5 space-y-4 shadow-sm border border-slate-200/80 dark:border-slate-800 overflow-visible">
|
||||||
|
|
||||||
<!-- Rij 1: Prominente Project Switcher (SD, IN, AAN, GEN, Alle) -->
|
<!-- Rij 1: Prominente Project Switcher (SD, IN, AAN, GEN, Alle) -->
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3 pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3 pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||||||
@@ -470,56 +446,52 @@ $statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSO
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rij 3: Status Multi-select Dropdown & Zoekveld -->
|
<!-- Rij 3: Tabs, Verberg Gesloten, In behandeling & Zoekveld -->
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-3">
|
||||||
|
<!-- Tabs Navigatie -->
|
||||||
|
<div class="flex items-center space-x-1 sm:space-x-1.5 overflow-x-auto pb-1 lg:pb-0">
|
||||||
|
<button onclick="switchTab('assigned')" id="tabBtn_assigned" class="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">
|
||||||
|
<i data-lucide="user-check" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Aan mij</span>
|
||||||
|
<span id="tabCount_assigned" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-200/70 dark:bg-indigo-800/60 text-indigo-800 dark:text-indigo-200">
|
||||||
|
<?= count($assignedIssues) ?>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Linkerkant: Statussen multi-select dropdown -->
|
<button onclick="switchTab('team')" id="tabBtn_team" class="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">
|
||||||
|
<i data-lucide="users" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Team & Projecten</span>
|
||||||
|
<span id="tabCount_team" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
|
||||||
|
<?= count($teamIssues) ?>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="switchTab('in_progress')" id="tabBtn_in_progress" class="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">
|
||||||
|
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>In Uitvoering</span>
|
||||||
|
<span id="tabCount_in_progress" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="switchTab('done')" id="tabBtn_done" class="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">
|
||||||
|
<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Afgerond</span>
|
||||||
|
<span id="tabCount_done" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Acties: Verberg Gesloten + Dropdown + Zoeken -->
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<div class="relative" id="statusDropdownWrapper">
|
<!-- Toggle: Verberg Gesloten (Standaard Actief!) -->
|
||||||
<button
|
<button type="button" onclick="toggleHideClosed()" id="btnHideClosed" class="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" title="Verberg afgeronde en gesloten tickets">
|
||||||
type="button"
|
<i data-lucide="eye-off" id="iconHideClosed" class="w-3.5 h-3.5 text-emerald-600"></i>
|
||||||
onclick="toggleStatusDropdown()"
|
<span>Verberg Gesloten</span>
|
||||||
id="statusDropdownBtn"
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
||||||
class="flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-semibold bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 text-slate-800 dark:text-slate-200 transition cursor-pointer shadow-2xs"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i data-lucide="check-square" class="w-3.5 h-3.5 text-indigo-500"></i>
|
|
||||||
<span>Status:</span>
|
|
||||||
<span id="statusDropdownLabel" class="font-bold text-indigo-600 dark:text-indigo-400">Alle statussen</span>
|
|
||||||
<span id="statusDropdownBadge" class="hidden 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"></span>
|
|
||||||
<i data-lucide="chevron-down" id="statusDropdownChevron" class="w-3.5 h-3.5 text-slate-400 transition-transform duration-200"></i>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Dropdown Menu met Checkboxes -->
|
|
||||||
<div
|
|
||||||
id="statusDropdownMenu"
|
|
||||||
class="hidden absolute left-0 top-full mt-2 w-72 max-w-[90vw] bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 py-3 px-3.5 z-50 text-xs space-y-2.5"
|
|
||||||
>
|
|
||||||
<!-- Quick actions header -->
|
|
||||||
<div class="flex items-center justify-between pb-2 border-b border-slate-100 dark:border-slate-800">
|
|
||||||
<span class="font-bold text-slate-900 dark:text-white text-[11px] uppercase tracking-wider">Jira Statussen</span>
|
|
||||||
<div class="flex items-center space-x-1.5">
|
|
||||||
<button type="button" onclick="selectAllStatuses(true)" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer">
|
|
||||||
Alles
|
|
||||||
</button>
|
|
||||||
<span class="text-slate-300 dark:text-slate-700">•</span>
|
|
||||||
<button type="button" onclick="selectActiveStatusesOnly()" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-950/60 transition cursor-pointer" title="Alleen actieve taken (geen gesloten)">
|
|
||||||
Actief
|
|
||||||
</button>
|
|
||||||
<span class="text-slate-300 dark:text-slate-700">•</span>
|
|
||||||
<button type="button" onclick="selectAllStatuses(false)" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
|
||||||
Geen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status Checkboxes List (scrollable) -->
|
|
||||||
<div class="max-h-64 overflow-y-auto space-y-1 pr-1 custom-scrollbar" id="statusCheckboxList">
|
|
||||||
<!-- Dynamisch gevuld via renderStatusCheckboxes() -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actieve filters reset knop als er iets gefilterd is -->
|
<!-- Actieve filters reset knop als er iets gefilterd is -->
|
||||||
<button type="button" onclick="resetAllFilters()" id="btnResetFiltersInline" class="hidden flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-semibold text-slate-500 hover:text-slate-800 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
<button type="button" onclick="resetAllFilters()" id="btnResetFiltersInline" class="hidden flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-semibold text-slate-500 hover:text-slate-800 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||||
<i data-lucide="rotate-ccw" class="w-3 h-3"></i>
|
<i data-lucide="rotate-ccw" class="w-3 h-3"></i>
|
||||||
@@ -546,7 +518,7 @@ $statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSO
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tickets Container -->
|
<!-- Tickets Container -->
|
||||||
<div id="ticketsContainer" class="space-y-3">
|
<div id="ticketsContainer" class="relative z-10 space-y-3">
|
||||||
<!-- Wordt dynamisch gerenderd via JavaScript -->
|
<!-- Wordt dynamisch gerenderd via JavaScript -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -901,22 +873,6 @@ let selectedStatuses = new Set();
|
|||||||
let activeUrgentOnly = false;
|
let activeUrgentOnly = false;
|
||||||
let expandedKeys = new Set();
|
let expandedKeys = new Set();
|
||||||
const ticketStore = new Map();
|
const ticketStore = new Map();
|
||||||
const allJiraStatuses = <?= $statusesJson ?>;
|
|
||||||
|
|
||||||
// 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) {
|
function isDoneStatus(st) {
|
||||||
const s = (st || '').toLowerCase().trim();
|
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() {
|
function updateActiveFilterBadge() {
|
||||||
const badge = document.getElementById('activeFilterBadge');
|
const badge = document.getElementById('activeFilterBadge');
|
||||||
if (!badge) return;
|
if (!badge) return;
|
||||||
@@ -993,7 +949,8 @@ function updateActiveFilterBadge() {
|
|||||||
if (currentQueue && currentQueue !== 'MY_QUEUES') activeCount++;
|
if (currentQueue && currentQueue !== 'MY_QUEUES') activeCount++;
|
||||||
if (document.getElementById('filterType')?.value) activeCount++;
|
if (document.getElementById('filterType')?.value) activeCount++;
|
||||||
if (document.getElementById('filterPriority')?.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) {
|
if (activeCount > 0) {
|
||||||
badge.innerText = activeCount;
|
badge.innerText = activeCount;
|
||||||
@@ -1003,236 +960,73 @@ function updateActiveFilterBadge() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// Wissel tussen tabs
|
||||||
// STATUS MULTI-SELECT CHECKBOX DROPDOWN LOGICA
|
function switchTab(tab) {
|
||||||
// ==========================================
|
currentTab = tab;
|
||||||
|
activeUrgentOnly = false;
|
||||||
|
|
||||||
function renderStatusCheckboxes() {
|
// Update tab knoppen styling
|
||||||
const container = document.getElementById('statusCheckboxList');
|
const tabs = ['assigned', 'team', 'in_progress', 'done'];
|
||||||
if (!container) return;
|
tabs.forEach(t => {
|
||||||
|
const btn = document.getElementById(`tabBtn_${t}`);
|
||||||
if (!allJiraStatuses || allJiraStatuses.length === 0) {
|
if (!btn) return;
|
||||||
container.innerHTML = '<div class="py-2 text-center text-slate-400 italic text-[11px]">Geen Jira statussen gevonden</div>';
|
if (t === tab) {
|
||||||
return;
|
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";
|
||||||
}
|
|
||||||
|
|
||||||
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 += `
|
|
||||||
<label for="${safeId}" class="flex items-center justify-between px-2.5 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/80 cursor-pointer transition select-none group">
|
|
||||||
<div class="flex items-center space-x-2.5 min-w-0 flex-1">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="${safeId}"
|
|
||||||
value="${escapeHtml(sNorm)}"
|
|
||||||
${isChecked ? 'checked' : ''}
|
|
||||||
onchange="onStatusCheckboxChange()"
|
|
||||||
class="status-checkbox w-4 h-4 rounded text-indigo-600 border-slate-300 dark:border-slate-700 focus:ring-indigo-500 cursor-pointer flex-shrink-0"
|
|
||||||
>
|
|
||||||
<span class="w-2 h-2 rounded-full ${dotColor} flex-shrink-0"></span>
|
|
||||||
<span class="font-semibold text-slate-800 dark:text-slate-200 truncate text-xs group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition">${escapeHtml(st.name)}</span>
|
|
||||||
</div>
|
|
||||||
<span id="statusCount_${idx}" class="text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 ml-2 flex-shrink-0">0</span>
|
|
||||||
</label>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
menu.classList.add('hidden');
|
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";
|
||||||
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();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAllStatuses(selectAll) {
|
// Toggle: Verberg Gesloten
|
||||||
selectedStatuses.clear();
|
function toggleHideClosed() {
|
||||||
const chks = document.querySelectorAll('.status-checkbox');
|
hideClosed = !hideClosed;
|
||||||
chks.forEach(chk => {
|
updateHideClosedButton();
|
||||||
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();
|
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectActiveStatusesOnly() {
|
function updateHideClosedButton() {
|
||||||
selectedStatuses.clear();
|
const btn = document.getElementById('btnHideClosed');
|
||||||
const chks = document.querySelectorAll('.status-checkbox');
|
if (!btn) return;
|
||||||
chks.forEach(chk => {
|
if (hideClosed) {
|
||||||
const val = (chk.value || '').toLowerCase().trim();
|
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";
|
||||||
const isDone = isDoneStatus(val);
|
btn.innerHTML = `<i data-lucide="eye-off" class="w-3.5 h-3.5 text-emerald-600"></i><span>Verberg Gesloten</span><span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>`;
|
||||||
chk.checked = !isDone;
|
} else {
|
||||||
if (!isDone) {
|
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";
|
||||||
selectedStatuses.add(val);
|
btn.innerHTML = `<i data-lucide="eye" class="w-3.5 h-3.5 text-slate-400"></i><span>Toon Ook Gesloten</span>`;
|
||||||
}
|
}
|
||||||
});
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
// Toggle: Snelfilter Alleen In Behandeling
|
||||||
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
|
function toggleOnlyInProgress() {
|
||||||
} catch (e) {}
|
onlyInProgress = !onlyInProgress;
|
||||||
|
updateOnlyInProgressButton();
|
||||||
updateStatusDropdownLabel();
|
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatusDropdownLabel() {
|
function updateOnlyInProgressButton() {
|
||||||
const labelEl = document.getElementById('statusDropdownLabel');
|
const btn = document.getElementById('btnOnlyInProgress');
|
||||||
const badgeEl = document.getElementById('statusDropdownBadge');
|
if (!btn) return;
|
||||||
if (!labelEl) 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";
|
||||||
const total = allJiraStatuses.length;
|
btn.innerHTML = `<i data-lucide="play" class="w-3 h-3 text-white fill-white"></i><span>In behandeling (Actief)</span>`;
|
||||||
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";
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
labelEl.innerText = `${selected} statussen`;
|
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";
|
||||||
if (badgeEl) {
|
btn.innerHTML = `<i data-lucide="play" class="w-3 h-3 text-blue-500"></i><span>In behandeling</span>`;
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initStatusSelection() {
|
// Status dropdown verandering
|
||||||
selectedStatuses = new Set();
|
function onStatusSelectChange(val) {
|
||||||
let saved = null;
|
if (val && isDoneStatus(val)) {
|
||||||
try {
|
hideClosed = false;
|
||||||
const raw = localStorage.getItem('fociz_jira_selected_statuses');
|
updateHideClosedButton();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
applyFilters();
|
||||||
if (selectedStatuses.size === 0) {
|
|
||||||
allJiraStatuses.forEach(st => {
|
|
||||||
selectedStatuses.add((st.name || '').toLowerCase().trim());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderStatusCheckboxes();
|
|
||||||
updateStatusDropdownLabel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project selectie via pill tabs
|
// Project selectie via pill tabs
|
||||||
@@ -1348,13 +1142,21 @@ function updateQueuePills() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateProjectCounts() {
|
function updateProjectCounts() {
|
||||||
const all = getDeduplicatedIssues();
|
const assigned = jiraData.assigned_issues || [];
|
||||||
let currentList = all;
|
const team = jiraData.team_issues || [];
|
||||||
|
const all = [...assigned, ...team];
|
||||||
|
|
||||||
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
|
let currentList = all;
|
||||||
currentList = currentList.filter(iss => selectedStatuses.has((iss.status || '').toLowerCase().trim()));
|
if (currentTab === 'assigned') currentList = assigned;
|
||||||
} else if (selectedStatuses.size === 0) {
|
else if (currentTab === 'team') currentList = team;
|
||||||
currentList = [];
|
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');
|
const elAll = document.getElementById('projCount_all');
|
||||||
@@ -1379,13 +1181,21 @@ function updateProjectCounts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateQueueCounts() {
|
function updateQueueCounts() {
|
||||||
const all = getDeduplicatedIssues();
|
const assigned = jiraData.assigned_issues || [];
|
||||||
let currentList = all;
|
const team = jiraData.team_issues || [];
|
||||||
|
const all = [...assigned, ...team];
|
||||||
|
|
||||||
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
|
let currentList = all;
|
||||||
currentList = currentList.filter(iss => selectedStatuses.has((iss.status || '').toLowerCase().trim()));
|
if (currentTab === 'assigned') currentList = assigned;
|
||||||
} else if (selectedStatuses.size === 0) {
|
else if (currentTab === 'team') currentList = team;
|
||||||
currentList = [];
|
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) {
|
if (currentProject) {
|
||||||
@@ -1494,10 +1304,11 @@ function applyFilters() {
|
|||||||
const issPKey = (iss.project_key || '').toUpperCase().trim();
|
const issPKey = (iss.project_key || '').toUpperCase().trim();
|
||||||
const issPName = (iss.project || '').toLowerCase().trim();
|
const issPName = (iss.project || '').toLowerCase().trim();
|
||||||
const issStatus = (iss.status || '').toLowerCase().trim();
|
const issStatus = (iss.status || '').toLowerCase().trim();
|
||||||
|
const issCat = (iss.status_category || '').toLowerCase().trim();
|
||||||
|
|
||||||
// 1. Status Filter via Checkboxes
|
// 1. Verberg Gesloten filter (als tab niet 'done' is en er niet expliciet een gesloten status gekozen is)
|
||||||
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
|
if (currentTab !== 'done' && hideClosed && !isDoneStatus(statusVal)) {
|
||||||
if (!selectedStatuses.has(issStatus)) {
|
if (issCat === 'done' || isDoneStatus(issStatus)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (selectedStatuses.size === 0) {
|
} else if (selectedStatuses.size === 0) {
|
||||||
|
|||||||
+1
-1
@@ -1284,7 +1284,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
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).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user