diff --git a/admin.php b/admin.php index 5041a67..3c85d83 100644 --- a/admin.php +++ b/admin.php @@ -171,15 +171,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } - // 2d. Teamleden toewijzen aan Jira Wachtrijfilter + // 2d. Teamleden toewijzen aan Jira Wachtrijfilters (meervoudig) elseif ($action === 'save_user_jira_assignments') { $activeTab = 'jira'; - $assignments = $_POST['user_filter'] ?? []; - if (is_array($assignments)) { - $upd = $db->prepare("UPDATE users SET default_jira_filter_code = ? WHERE id = ?"); - foreach ($assignments as $uId => $fCode) { - $fCode = trim($fCode); - $upd->execute([$fCode !== '' ? $fCode : null, (int)$uId]); + $assignments = $_POST['user_filters'] ?? []; + $submittedUserIds = isset($_POST['user_ids']) && is_array($_POST['user_ids']) ? array_map('intval', $_POST['user_ids']) : []; + + // Fallback als user_ids niet meegestuurd werd (bv. oude formulier submission met user_filter) + if (empty($submittedUserIds) && !empty($_POST['user_filter']) && is_array($_POST['user_filter'])) { + $submittedUserIds = array_map('intval', array_keys($_POST['user_filter'])); + foreach ($_POST['user_filter'] as $uId => $sCode) { + $sCode = trim($sCode); + $assignments[$uId] = $sCode !== '' ? [$sCode] : []; + } + } + + if (!empty($submittedUserIds)) { + $upd = $db->prepare("UPDATE users SET default_jira_filter_codes = ?, default_jira_filter_code = ? WHERE id = ?"); + foreach ($submittedUserIds as $uId) { + $rawList = isset($assignments[$uId]) && is_array($assignments[$uId]) ? $assignments[$uId] : []; + $cleanCodes = array_values(array_unique(array_filter(array_map('trim', $rawList)))); + $jsonVal = !empty($cleanCodes) ? json_encode($cleanCodes) : null; + $firstVal = !empty($cleanCodes) ? $cleanCodes[0] : null; + $upd->execute([$jsonVal, $firstVal, (int)$uId]); } fociz_log_audit($userId, 'SETTINGS_UPDATED', 'jira_assignments', null, [], 'SUCCESS', $adminUser['email']); $message = "Standaard Jira wachtrijtoewijzingen voor het team zijn succesvol opgeslagen."; @@ -511,7 +525,7 @@ try { $allUsersForJira = []; $allJiraFilters = []; try { - $allUsersForJira = $db->query("SELECT id, name, email, role, avatar_url, default_jira_filter_code FROM users ORDER BY name ASC")->fetchAll() ?: []; + $allUsersForJira = $db->query("SELECT id, name, email, role, avatar_url, default_jira_filter_code, default_jira_filter_codes FROM users ORDER BY name ASC")->fetchAll() ?: []; $allJiraFilters = $db->query("SELECT * FROM jira_queue_filters ORDER BY sort_order ASC, id ASC")->fetchAll() ?: []; } catch (Throwable $e) { error_log("Fout bij ophalen jira filters/users: " . $e->getMessage()); @@ -1373,10 +1387,10 @@ require_once __DIR__ . '/includes/header.php';

- Team Wachtrijmatrix: Standaard Wachtrij per Collega + Team Wachtrijmatrix: Gekoppelde Wachtrijen per Collega

- Wijs collega's toe aan hun primaire wachtrij (bijv. DX of SD). Wanneer zij de Jira pagina openen, wordt deze wachtrij direct als standaard getoond. + Koppel collega's aan één of meerdere wachtrijfilters (bijv. SD, IN, AAN, GEN of DX). Collega's zien in de Jira Hub direct de tickets van hun gekoppelde wachtrijen.

@@ -1393,13 +1407,25 @@ require_once __DIR__ . '/includes/header.php'; Teamlid E-mailadres Rol - Standaard Wachtrijfilter + Gekoppelde Wachtrijen (Meervoudig) - + +
Avatar @@ -1423,16 +1449,18 @@ require_once __DIR__ . '/includes/header.php'; - class="w-3.5 h-3.5 rounded text-indigo-600 border-slate-300 dark:border-slate-700 focus:ring-indigo-500 cursor-pointer"> + + - +
diff --git a/config/database.php b/config/database.php index dc0e9e7..ac5746f 100644 --- a/config/database.php +++ b/config/database.php @@ -162,6 +162,8 @@ function ensure_tables_exist(PDO $pdo): void { "ALTER TABLE board_card_schedules ADD UNIQUE KEY uk_sched_card (card_id)", "CREATE TABLE IF NOT EXISTS `password_resets` (`id` INT AUTO_INCREMENT PRIMARY KEY, `email` VARCHAR(191) NOT NULL, `token` VARCHAR(64) NOT NULL UNIQUE, `expires_at` DATETIME NOT NULL, `used_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`email`), INDEX (`token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", "CREATE TABLE IF NOT EXISTS `jira_queue_filters` (`id` INT AUTO_INCREMENT PRIMARY KEY, `filter_code` VARCHAR(50) NOT NULL UNIQUE, `filter_name` VARCHAR(120) NOT NULL, `project_key` VARCHAR(50) NULL, `jql_clause` TEXT NOT NULL, `description` VARCHAR(255) NULL, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `sort_order` INT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + // Jira: Meerdere wachtrijfilters per teamlid + "ALTER TABLE users ADD COLUMN default_jira_filter_codes TEXT NULL AFTER default_jira_filter_code", // V2.0: Subtaken & Checklists "CREATE TABLE IF NOT EXISTS `task_subtasks` (`id` INT AUTO_INCREMENT PRIMARY KEY, `task_id` INT NOT NULL, `user_id` INT NOT NULL, `title` VARCHAR(255) NOT NULL, `is_completed` TINYINT(1) NOT NULL DEFAULT 0, `position` INT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX (`task_id`, `position`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", // Beveiliging: IP tracking & locatie-verificatie @@ -191,30 +193,61 @@ function ensure_tables_exist(PDO $pdo): void { } } - // Zorg dat de standaard Jira Servicedesk wachtrijfilters (DX en SD) aanwezig zijn + // Zorg dat de standaard Jira wachtrijfilters (SD, IN, AAN, GEN, DX) aanwezig zijn try { - $filterCount = (int)$pdo->query("SELECT COUNT(*) FROM jira_queue_filters")->fetchColumn(); - if ($filterCount === 0) { - $insertFilterStmt = $pdo->prepare(" - INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order) - VALUES (?, ?, ?, ?, ?, 1, ?) - "); - $insertFilterStmt->execute([ - 'DX', - 'DX — Koppelingen, SSO & Migratie', - 'SD', - '"Onderwerp logging[dropdown]" in (Koppelingen, SSO-koppeling, Rest-endpoints, Datadump, UWV-koppeling, Migratie)', - 'Servicedesk tickets met betrekking tot koppelingen, endpoints en datamigraties.', - 1 - ]); - $insertFilterStmt->execute([ - 'SD', - 'SD — Regulier Servicedesk', - 'SD', - '("Onderwerp logging[Dropdown]" = EMPTY OR "Onderwerp logging[Dropdown]" != Koppelingen AND "Onderwerp logging[Dropdown]" != Migratie AND "Onderwerp logging[Dropdown]" != SSO-koppeling AND "Onderwerp logging[Dropdown]" != UWV-koppeling AND "Onderwerp logging[Dropdown]" != Rest-endpoints AND "Onderwerp logging[Dropdown]" != Datadump)', - 'Reguliere servicedesk incidenten, algemene meldingen en gebruikersvragen.', - 2 - ]); + $existingCodes = $pdo->query("SELECT filter_code FROM jira_queue_filters")->fetchAll(PDO::FETCH_COLUMN) ?: []; + $defaultFilters = [ + [ + 'code' => 'SD', + 'name' => 'SD — Servicedesk', + 'project' => 'SD', + 'jql' => 'project = "SD"', + 'desc' => 'Servicedesk supportvragen en operationele meldingen.', + 'order' => 1 + ], + [ + 'code' => 'IN', + 'name' => 'IN — Interfaces', + 'project' => 'IN', + 'jql' => 'project = "IN"', + 'desc' => 'Interface-koppelingen, API-endpoints en data-uitwisselingen.', + 'order' => 2 + ], + [ + 'code' => 'AAN', + 'name' => 'AAN — Aanvragen', + 'project' => 'AAN', + 'jql' => 'project = "AAN"', + 'desc' => 'Klant- en gebruikersaanvragen en wijzigingsverzoeken.', + 'order' => 3 + ], + [ + 'code' => 'GEN', + 'name' => 'GEN — General', + 'project' => 'GEN', + 'jql' => 'project = "GEN"', + 'desc' => 'Algemene taken, organisatie en platformprojecten.', + 'order' => 4 + ], + [ + 'code' => 'DX', + 'name' => 'DX — Koppelingen & SSO', + 'project' => 'SD', + 'jql' => '"Onderwerp logging[dropdown]" in (Koppelingen, SSO-koppeling, Rest-endpoints, Datadump, UWV-koppeling, Migratie)', + 'desc' => 'Servicedesk tickets specifiek voor koppelingen en migraties.', + 'order' => 5 + ] + ]; + $insertFilterStmt = $pdo->prepare(" + INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order) + VALUES (?, ?, ?, ?, ?, 1, ?) + "); + foreach ($defaultFilters as $df) { + if (!in_array($df['code'], $existingCodes)) { + $insertFilterStmt->execute([ + $df['code'], $df['name'], $df['project'], $df['jql'], $df['desc'], $df['order'] + ]); + } } } catch (Throwable $e) { // Stil negeren indien tabel nog in opbouw is diff --git a/includes/integrations/jira.php b/includes/integrations/jira.php index 18f6479..af84df2 100644 --- a/includes/integrations/jira.php +++ b/includes/integrations/jira.php @@ -226,20 +226,111 @@ function jira_get_queue_filters(bool $onlyActive = true): array { } /** - * Haalt de standaard wachtrijfilter voorkeur van een gebruiker op + * Haalt alle gekoppelde wachtrijfilter codes voor een gebruiker op (meervoudig) */ -function jira_get_user_queue_preference(int $userId): ?string { +function jira_get_user_queue_preferences(int $userId): array { try { $db = get_db_connection(); - $stmt = $db->prepare("SELECT default_jira_filter_code FROM users WHERE id = ?"); + $stmt = $db->prepare("SELECT default_jira_filter_codes, default_jira_filter_code FROM users WHERE id = ?"); $stmt->execute([$userId]); - $res = $stmt->fetchColumn(); - return $res ? (string)$res : null; + $row = $stmt->fetch(); + if (!$row) return []; + + if (!empty($row['default_jira_filter_codes'])) { + $decoded = json_decode($row['default_jira_filter_codes'], true); + if (is_array($decoded)) { + return array_values(array_filter(array_map('strtoupper', array_map('trim', $decoded)))); + } + $parts = explode(',', $row['default_jira_filter_codes']); + return array_values(array_filter(array_map('strtoupper', array_map('trim', $parts)))); + } + + if (!empty($row['default_jira_filter_code'])) { + return [strtoupper(trim($row['default_jira_filter_code']))]; + } + + return []; } catch (Throwable $e) { - return null; + return []; } } +/** + * Haalt de eerste standaard wachtrijfilter voorkeur van een gebruiker op (backwards compatibility) + */ +function jira_get_user_queue_preference(int $userId): ?string { + $prefs = jira_get_user_queue_preferences($userId); + return !empty($prefs) ? $prefs[0] : null; +} + +/** + * Haalt alle officiële statussen op uit de Atlassian Jira Cloud instantie + */ +function jira_fetch_statuses(string $domain, string $email, string $apiToken): array { + $cleanDomain = jira_normalize_domain($domain); + if (empty($cleanDomain) || empty($email) || empty($apiToken)) { + return []; + } + + $cacheKey = "jira_statuses_" . 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/status'; + + $response = fociz_http_request($url, [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => $authHeader, + 'Accept' => 'application/json', + ], + 'timeout' => 10, + ]); + + if ($response['status'] === 404 || $response['status'] === 405) { + $urlV2 = $cleanDomain . '/rest/api/2/status'; + $response = fociz_http_request($urlV2, [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => $authHeader, + 'Accept' => 'application/json', + ], + 'timeout' => 10, + ]); + } + + $statuses = []; + if ($response['status'] === 200 && is_array($response['json'])) { + foreach ($response['json'] as $st) { + $catKey = strtolower($st['statusCategory']['key'] ?? 'indeterminate'); + $catName = $st['statusCategory']['name'] ?? 'In uitvoering'; + $statuses[] = [ + 'id' => (string)($st['id'] ?? ''), + 'name' => $st['name'] ?? '', + 'description' => $st['description'] ?? '', + 'status_category' => $catKey, // 'new', 'indeterminate', 'done' + 'category_name' => $catName, + 'color' => $st['statusCategory']['colorName'] ?? '', + ]; + } + // Sorteer: To Do eerst, dan In Progress, dan Done + 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 */ @@ -464,7 +555,7 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri /** * Haalt alle actieve issues op die zijn toegewezen aan de ingelogde Jira gebruiker */ -function jira_fetch_assigned_issues(string $domain, string $email, string $apiToken, int $maxResults = 50): array { +function jira_fetch_assigned_issues(string $domain, string $email, string $apiToken, int $maxResults = 100): array { $jql = 'assignee = currentUser() ORDER BY updated DESC'; return jira_fetch_issues($domain, $email, $apiToken, $jql, $maxResults); } @@ -472,7 +563,7 @@ function jira_fetch_assigned_issues(string $domain, string $email, string $apiTo /** * Haalt openstaande team issues op die horen bij de projecten van de gebruiker */ -function jira_fetch_team_issues(string $domain, string $email, string $apiToken, int $maxResults = 50): array { +function jira_fetch_team_issues(string $domain, string $email, string $apiToken, int $maxResults = 100): array { $jql = 'assignee != currentUser() AND statusCategory != Done ORDER BY updated DESC'; return jira_fetch_issues($domain, $email, $apiToken, $jql, $maxResults); } @@ -486,18 +577,29 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array $stmt->execute([$userId]); $integration = $stmt->fetch(); - // Ophalen van actieve wachtrijfilters en voorkeur van gebruiker + // Ophalen van actieve wachtrijfilters en voorkeuren van gebruiker $queueFilters = jira_get_queue_filters(true); $userQueuePref = jira_get_user_queue_preference($userId); + $userQueuePrefs = jira_get_user_queue_preferences($userId); // Sandbox / Demo dataset indien niet gekoppeld of expliciet in demomodus if (!$integration || !empty($integration['is_demo_mode'])) { + $demoStatuses = [ + ['id' => '1', 'name' => 'To Do', 'status_category' => 'new', 'category_name' => 'Nieuw'], + ['id' => '2', 'name' => 'Nieuw', 'status_category' => 'new', 'category_name' => 'Nieuw'], + ['id' => '3', 'name' => 'In behandeling', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], + ['id' => '4', 'name' => 'Wachten op klant', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], + ['id' => '5', 'name' => 'In Review', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], + ['id' => '6', 'name' => 'Gereed', 'status_category' => 'done', 'category_name' => 'Gereed'], + ['id' => '7', 'name' => 'Gesloten', 'status_category' => 'done', 'category_name' => 'Gereed'], + ]; + $assigned = [ [ 'key' => 'SD-401', 'summary' => 'AFAS Profit SSO-koppeling geeft 401 fout bij token refresh', - 'description' => "Gebruikers die via AFAS SSO inloggen krijgen periodiek een HTTP 401 foutcode bij de geautomatiseerde token verversing.\n\nHet endpoint `/oauth/token` reageert met `invalid_grant` zodra de refresh token ouder is dan 24 uur. Er dient een retry-mechanisme met fallback authenticatie ingebouwd te worden.", - 'status' => 'In Progress', + 'description' => "Gebruikers die via AFAS SSO inloggen krijgen periodiek een HTTP 401 foutcode bij de geautomatiseerde token verversing.\n\nHet endpoint `/oauth/token` reageert met `invalid_grant` zodra de refresh token ouder is dan 24 uur.", + 'status' => 'In behandeling', 'status_category' => 'indeterminate', 'priority' => 'Highest', 'issue_type' => 'Bug', @@ -512,12 +614,13 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array 'created' => date('Y-m-d H:i:s', strtotime('-1 day')), 'updated' => date('Y-m-d H:i:s', strtotime('-45 minutes')), 'url' => 'https://jira.atlassian.net/browse/SD-401', + 'story_points' => 3, 'source' => 'SANDBOX', ], [ 'key' => 'SD-405', 'summary' => 'REST-endpoints voor medewerker synchronisatie beveiligen met rate limiting', - 'description' => "De openbare REST-endpoints voor medewerkerssynchronisatie dienen conform security richtlijnen te worden voorzien van rate limiting (max 120 calls per minuut per IP).\n\nBij overschrijding moet een duidelijke HTTP 429 Retry-After header worden geretourneerd.", + 'description' => "De openbare REST-endpoints voor medewerkerssynchronisatie dienen conform security richtlijnen te worden voorzien van rate limiting.", 'status' => 'To Do', 'status_category' => 'new', 'priority' => 'High', @@ -533,14 +636,147 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')), 'url' => 'https://jira.atlassian.net/browse/SD-405', + 'story_points' => 5, + 'source' => 'SANDBOX', + ], + [ + 'key' => 'IN-201', + 'summary' => 'Exact Online REST API webhook connectie configureren', + 'description' => "Configuratie van de webhook triggers voor realtime debiteuren synchronisatie tussen Fociz en Exact Online.", + 'status' => 'In behandeling', + 'status_category' => 'indeterminate', + 'priority' => 'High', + 'issue_type' => 'Story', + 'project' => 'Interfaces', + 'project_key' => 'IN', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'Financieel Beheer', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Koppelingen', + 'due_date' => date('Y-m-d', strtotime('+2 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-30 minutes')), + 'url' => 'https://jira.atlassian.net/browse/IN-201', + 'story_points' => 8, + 'source' => 'SANDBOX', + ], + [ + 'key' => 'IN-205', + 'summary' => 'OAuth 2.0 PKCE handshake voor mobiele koppeling afronden', + 'description' => "Migreer de verouderde implicit flow naar de PKCE authorisatie flow voor alle mobiele apparaten.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'Medium', + 'issue_type' => 'Task', + 'project' => 'Interfaces', + 'project_key' => 'IN', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'Tech Lead', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'SSO-koppeling', + 'due_date' => date('Y-m-d', strtotime('+4 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-4 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'url' => 'https://jira.atlassian.net/browse/IN-205', + 'story_points' => 5, + 'source' => 'SANDBOX', + ], + [ + 'key' => 'AAN-102', + 'summary' => 'Aanvraag nieuwe laptop en ergonomische thuiswerkplek beeldschermen', + 'description' => "Nieuwe onboarding developer heeft MacBook Pro 16 inch en 2x 4K schermen nodig conform standaardpakket.", + 'status' => 'Wachten op klant', + 'status_category' => 'indeterminate', + 'priority' => 'Medium', + 'issue_type' => 'Task', + 'project' => 'Aanvragen', + 'project_key' => 'AAN', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'HR Onboarding', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Hardware', + 'due_date' => date('Y-m-d', strtotime('+5 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-3 hours')), + 'url' => 'https://jira.atlassian.net/browse/AAN-102', + 'story_points' => 2, + 'source' => 'SANDBOX', + ], + [ + 'key' => 'AAN-108', + 'summary' => 'Autorisatieverzoek rechten AFAS financieel beheer teamleider', + 'description' => "Goedkeuring benodigd voor toekennen van geautoriseerde rollen in AFAS Profit.", + 'status' => 'In behandeling', + 'status_category' => 'indeterminate', + 'priority' => 'High', + 'issue_type' => 'Task', + 'project' => 'Aanvragen', + 'project_key' => 'AAN', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'Teamleider Financiën', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Rechten', + 'due_date' => date('Y-m-d'), + 'created' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')), + 'url' => 'https://jira.atlassian.net/browse/AAN-108', + 'story_points' => 1, + 'source' => 'SANDBOX', + ], + [ + 'key' => 'GEN-301', + 'summary' => 'Jaarlijkse ISO-27001 interne security review voorbereiden', + 'description' => "Samenstellen van security checklists, access reviews en back-up logs voor auditcommissie.", + 'status' => 'In behandeling', + 'status_category' => 'indeterminate', + 'priority' => 'High', + 'issue_type' => 'Task', + 'project' => 'General', + 'project_key' => 'GEN', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'CISO', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Beveiliging', + 'due_date' => date('Y-m-d', strtotime('+6 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-5 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-4 hours')), + 'url' => 'https://jira.atlassian.net/browse/GEN-301', + 'story_points' => 13, + 'source' => 'SANDBOX', + ], + [ + 'key' => 'GEN-305', + 'summary' => 'Kantoorruimte vergadertafel opstelling aanpassen voor hybride team', + 'description' => "Herinrichting van de videovergaderruimte met Yealink meetingbar.", + 'status' => 'Gesloten', + 'status_category' => 'done', + 'priority' => 'Low', + 'issue_type' => 'Task', + 'project' => 'General', + 'project_key' => 'GEN', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'Facility Management', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Facilitair', + 'due_date' => date('Y-m-d', strtotime('-2 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-8 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'url' => 'https://jira.atlassian.net/browse/GEN-305', + 'story_points' => 1, 'source' => 'SANDBOX', ], [ 'key' => 'SD-380', 'summary' => 'Wachtwoord reset e-mail komt niet aan bij nieuwe medewerker', - 'description' => "Een nieuwe medewerker ontvangt geen welkomstmail en wachtwoord reset via het selfservice portaal.\n\nControleer de SMTP-verbinding van de mailserver mail.fociz.nl en controleer of het mailadres in de spamfilter is beland.", - 'status' => 'In Progress', - 'status_category' => 'indeterminate', + 'description' => "Controleer de SMTP-verbinding van de mailserver mail.fociz.nl en controleer of het mailadres in de spamfilter is beland.", + 'status' => 'Gesloten', + 'status_category' => 'done', 'priority' => 'High', 'issue_type' => 'Task', 'project' => 'Servicedesk', @@ -550,136 +786,11 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array 'reporter_name' => 'HR Servicedesk', 'reporter_avatar' => '', 'onderwerp_logging' => 'EMPTY', - 'due_date' => date('Y-m-d'), - 'created' => date('Y-m-d H:i:s', strtotime('-3 hours')), - 'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')), - 'url' => 'https://jira.atlassian.net/browse/SD-380', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'SD-385', - 'summary' => 'Rechten aanpassen voor rol Projectleider in urenregistratie', - 'description' => "Projectleiders hebben momenteel alleen leesrechten op ingediende urenstaten. Graag de rol uitbreiden zodat zij declaraties direct kunnen goedkeuren of afkeuren.", - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'Medium', - 'issue_type' => 'Task', - 'project' => 'Servicedesk', - 'project_key' => 'SD', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'reporter_name' => 'Marianne Willems', - 'reporter_avatar' => '', - 'onderwerp_logging' => 'Algemeen', - 'due_date' => date('Y-m-d', strtotime('+5 days')), - 'created' => date('Y-m-d H:i:s', strtotime('-3 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), - 'url' => 'https://jira.atlassian.net/browse/SD-385', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'FOC-101', - 'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren', - 'description' => "Ontwikkeling van een complete Google Authenticator / Microsoft Authenticator TOTP verificatie flow inclusief 8 cijferige noodcodes en QR-code weergave in het gebruikersprofiel.", - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'High', - 'issue_type' => 'Story', - 'project' => 'Fociz Platform', - 'project_key' => 'FOC', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'reporter_name' => 'Product Owner', - 'reporter_avatar' => '', - 'onderwerp_logging' => 'Beveiliging', - 'due_date' => date('Y-m-d', strtotime('+2 days')), - 'created' => date('Y-m-d H:i:s', strtotime('-4 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-101', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'FOC-102', - 'summary' => 'Interactief whiteboard canvas voor brainstormsessies', - 'description' => "Gebruikers moeten live post-its kunnen slepen, kleuren en stemmen tijdens kwartaal brainstormsessies.", - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'High', - 'issue_type' => 'Feature', - 'project' => 'Fociz Platform', - 'project_key' => 'FOC', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'reporter_name' => 'Scrum Master', - 'reporter_avatar' => '', - 'onderwerp_logging' => '', - 'due_date' => date('Y-m-d', strtotime('+4 days')), - 'created' => date('Y-m-d H:i:s', strtotime('-5 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), - 'url' => 'https://jira.atlassian.net/browse/FOC-102', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'FOC-103', - 'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API', - 'description' => "Synchronisatie van dagplanning blokken naar Microsoft 365 Exchange Online agenda met real-time conflictcontrole.", - 'status' => 'In Review', - 'status_category' => 'indeterminate', - 'priority' => 'Medium', - 'issue_type' => 'Task', - 'project' => 'Integrations Hub', - 'project_key' => 'INT', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'reporter_name' => 'Architect', - 'reporter_avatar' => '', - 'onderwerp_logging' => 'SSO-koppeling', - 'due_date' => date('Y-m-d'), - 'created' => date('Y-m-d H:i:s', strtotime('-6 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-4 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-103', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'FOC-108', - 'summary' => 'CSS scrollbar glitch oplossen in Kanban lane op Firefox', - 'description' => "Op Windows Firefox versies 128+ verspringt de horizontale scrollbalk bij het slepen van kaarten tussen kolommen.", - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'Highest', - 'issue_type' => 'Bug', - 'project' => 'UI/UX Redesign', - 'project_key' => 'UI', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'reporter_name' => 'QA Lead', - 'reporter_avatar' => '', - 'onderwerp_logging' => '', - 'due_date' => date('Y-m-d', strtotime('+1 day')), - 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-30 minutes')), - 'url' => 'https://jira.atlassian.net/browse/FOC-108', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'FOC-112', - 'summary' => 'Avatar upload beveiliging en compressie optimalisatie', - 'description' => "Server-side image re-encoding en EXIF stripping voor geüploade gebruikersavatars.", - 'status' => 'Done', - 'status_category' => 'done', - 'priority' => 'Medium', - 'issue_type' => 'Task', - 'project' => 'Security', - 'project_key' => 'SEC', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'reporter_name' => 'SecOps', - 'reporter_avatar' => '', - 'onderwerp_logging' => '', 'due_date' => date('Y-m-d', strtotime('-1 day')), - 'created' => date('Y-m-d H:i:s', strtotime('-7 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-5 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-112', + 'created' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')), + 'url' => 'https://jira.atlassian.net/browse/SD-380', + 'story_points' => 2, 'source' => 'SANDBOX', ], ]; @@ -688,8 +799,8 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array [ 'key' => 'SD-408', 'summary' => 'UWV-koppeling polisadministratie import loopt vast op datumformaat', - 'description' => "De XML batch import vanuit het UWV portaal faalt op records met afwijkende geboortedatumnotatie.\n\nDe parser dient flexibel zowel ISO-8601 (YYYY-MM-DD) als traditioneel formaat (DD-MM-YYYY) te accepteren.", - 'status' => 'In Progress', + 'description' => "De XML batch import vanuit het UWV portaal faalt op records met afwijkende geboortedatumnotatie.", + 'status' => 'In behandeling', 'status_category' => 'indeterminate', 'priority' => 'High', 'issue_type' => 'Bug', @@ -704,6 +815,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')), 'url' => 'https://jira.atlassian.net/browse/SD-408', + 'story_points' => 5, 'source' => 'SANDBOX', ], [ @@ -725,39 +837,19 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array 'created' => date('Y-m-d H:i:s', strtotime('-3 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-4 hours')), 'url' => 'https://jira.atlassian.net/browse/SD-412', + 'story_points' => 3, 'source' => 'SANDBOX', ], [ - 'key' => 'SD-416', - 'summary' => 'Migratie van oude klantportaal database naar MariaDB 11 cluster', - 'description' => "Voorbereiden en uitvoeren van de dataschemamigratie van het oude portaal naar de nieuwe MariaDB 11 clusteromgeving inclusief regressietests.", - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'High', - 'issue_type' => 'Story', - 'project' => 'Servicedesk', - 'project_key' => 'SD', - 'assignee_name' => 'Bram Meijer', - 'assignee_avatar' => '', - 'reporter_name' => 'Lead Engineer', - 'reporter_avatar' => '', - 'onderwerp_logging' => 'Migratie', - 'due_date' => date('Y-m-d', strtotime('+8 days')), - 'created' => date('Y-m-d H:i:s', strtotime('-4 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')), - 'url' => 'https://jira.atlassian.net/browse/SD-416', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'SD-420', + 'key' => 'IN-210', 'summary' => 'Koppelingen API webhook notificaties afleveren met retry-mechanisme', - 'description' => "Webhooks die naar externe partnersystemen worden verzonden moeten bij een tijdelijke 5xx fout automatisch 3x opnieuw worden aangeboden (exponential backoff).", - 'status' => 'In Review', + 'description' => "Webhooks die naar externe partnersystemen worden verzonden moeten bij een tijdelijke 5xx fout automatisch 3x opnieuw worden aangeboden.", + 'status' => 'In behandeling', 'status_category' => 'indeterminate', 'priority' => 'Medium', 'issue_type' => 'Task', - 'project' => 'Servicedesk', - 'project_key' => 'SD', + 'project' => 'Interfaces', + 'project_key' => 'IN', 'assignee_name' => 'Sophie de Vries', 'assignee_avatar' => '', 'reporter_name' => 'Integratie Specialist', @@ -766,112 +858,118 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array 'due_date' => date('Y-m-d', strtotime('+4 days')), 'created' => date('Y-m-d H:i:s', strtotime('-5 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-3 hours')), - 'url' => 'https://jira.atlassian.net/browse/SD-420', + 'url' => 'https://jira.atlassian.net/browse/IN-210', + 'story_points' => 5, 'source' => 'SANDBOX', ], [ - 'key' => 'SD-392', - 'summary' => 'Outlook Agenda synchronisatie toont dubbele afspraken op mobiel', - 'description' => "Bij enkele gebruikers met de iOS Outlook app worden terugkerende agenda-afspraken tweemaal weergegeven na het synchroniseren.", - 'status' => 'In Progress', + 'key' => 'IN-215', + 'summary' => 'Specificatie documentatie voor externe partner API opvragen', + 'description' => "Opvragen van de actuele OpenAPI v3 JSON specificatie bij leverancier.", + 'status' => 'Wachten op klant', 'status_category' => 'indeterminate', - 'priority' => 'Medium', - 'issue_type' => 'Bug', - 'project' => 'Servicedesk', - 'project_key' => 'SD', - 'assignee_name' => 'Lisa Bakker', - 'assignee_avatar' => '', - 'reporter_name' => 'Servicedesk Support', - 'reporter_avatar' => '', - 'onderwerp_logging' => 'Incident', - 'due_date' => date('Y-m-d', strtotime('+3 days')), - 'created' => date('Y-m-d H:i:s', strtotime('-3 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')), - 'url' => 'https://jira.atlassian.net/browse/SD-392', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'SD-399', - 'summary' => 'Vraag over exportmogelijkheden naar Excel rapportage', - 'description' => "Klant informeert of het mogelijk is om een wekelijkse CSV / Excel export van de urenstaten automatisch per mail te ontvangen.", - 'status' => 'To Do', - 'status_category' => 'new', 'priority' => 'Low', 'issue_type' => 'Task', - 'project' => 'Servicedesk', - 'project_key' => 'SD', + 'project' => 'Interfaces', + 'project_key' => 'IN', + 'assignee_name' => 'Sophie de Vries', + 'assignee_avatar' => '', + 'reporter_name' => 'Partner Manager', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Koppelingen', + 'due_date' => date('Y-m-d', strtotime('+7 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-4 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')), + 'url' => 'https://jira.atlassian.net/browse/IN-215', + 'story_points' => 1, + 'source' => 'SANDBOX', + ], + [ + 'key' => 'AAN-110', + 'summary' => 'Aanvraag extra Microsoft 365 E5 licenties voor marketing team', + 'description' => "Bestelling en activering van 5 aanvullende E5 licenties voor het datateam.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'Medium', + 'issue_type' => 'Task', + 'project' => 'Aanvragen', + 'project_key' => 'AAN', 'assignee_name' => 'Thomas Jansen', 'assignee_avatar' => '', - 'reporter_name' => 'Financieel Directeur', + 'reporter_name' => 'Marketing Director', 'reporter_avatar' => '', - 'onderwerp_logging' => 'Vraag', - 'due_date' => date('Y-m-d', strtotime('+10 days')), - 'created' => date('Y-m-d H:i:s', strtotime('-5 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), - 'url' => 'https://jira.atlassian.net/browse/SD-399', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'FOC-95', - 'summary' => 'Sessie timeout notificatie verschijnt niet tijdig bij inactieve tab', - 'description' => "Wanneer een gebruiker de browser 2 uur niet aanraakt, vervalt de PHP sessie zonder dat het login-dialoogvenster netjes openspringt.", - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'Highest', - 'issue_type' => 'Bug', - 'project' => 'Fociz Platform', - 'project_key' => 'FOC', - 'assignee_name' => 'Lisa Bakker', - 'assignee_avatar' => '', - 'reporter_name' => 'Product QA', - 'reporter_avatar' => '', - 'onderwerp_logging' => '', - 'due_date' => date('Y-m-d', strtotime('+1 day')), - 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')), - 'url' => 'https://jira.atlassian.net/browse/FOC-95', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'FOC-98', - 'summary' => 'Automatische e-mail triggers voor taakdeadlines en uitnodigingen', - 'description' => "Cronjob instellen die 24 uur voor de deadline van een taak een herinneringsmail stuurt via SMTP.", - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'High', - 'issue_type' => 'Story', - 'project' => 'Notifications', - 'project_key' => 'NOTIF', - 'assignee_name' => 'Daan van Dijk', - 'assignee_avatar' => '', - 'reporter_name' => 'Operations', - 'reporter_avatar' => '', - 'onderwerp_logging' => '', + 'onderwerp_logging' => 'Licenties', 'due_date' => date('Y-m-d', strtotime('+3 days')), - 'created' => date('Y-m-d H:i:s', strtotime('-3 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-2 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-98', + 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'url' => 'https://jira.atlassian.net/browse/AAN-110', + 'story_points' => 2, 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-110', - 'summary' => 'Real-time websocket synchronisatie voor gelijktijdig bewerken van borden', - 'description' => "Voorkom overschrijfconflicten wanneer meerdere collega's gelijktijdig kaarten verplaatsen op hetzelfde Kanban bord.", - 'status' => 'To Do', - 'status_category' => 'new', + 'key' => 'AAN-115', + 'summary' => 'Aanvraag tijdelijke VPN toegang externe auditor KPMG', + 'description' => "Toegang configureren met verloopdatum 30 september en MFA verplichting.", + 'status' => 'Gesloten', + 'status_category' => 'done', 'priority' => 'High', - 'issue_type' => 'Story', - 'project' => 'Realtime Engine', - 'project_key' => 'RT', + 'issue_type' => 'Task', + 'project' => 'Aanvragen', + 'project_key' => 'AAN', 'assignee_name' => 'Bram Meijer', 'assignee_avatar' => '', - 'reporter_name' => 'Architect', + 'reporter_name' => 'Security Officer', 'reporter_avatar' => '', - 'onderwerp_logging' => '', - 'due_date' => date('Y-m-d', strtotime('+6 days')), + 'onderwerp_logging' => 'Rechten', + 'due_date' => date('Y-m-d', strtotime('-3 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-7 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'url' => 'https://jira.atlassian.net/browse/AAN-115', + 'story_points' => 2, + 'source' => 'SANDBOX', + ], + [ + 'key' => 'GEN-310', + 'summary' => 'Organisatiebeleid hybride werken 2026 actualiseren', + 'description' => "Aanpassing van reiskostenvergoeding en thuiswerkvergoeding conform nieuwe cao-afspraken.", + 'status' => 'In behandeling', + 'status_category' => 'indeterminate', + 'priority' => 'Medium', + 'issue_type' => 'Story', + 'project' => 'General', + 'project_key' => 'GEN', + 'assignee_name' => 'Lisa Bakker', + 'assignee_avatar' => '', + 'reporter_name' => 'Directie', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Algemeen', + 'due_date' => date('Y-m-d', strtotime('+10 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-6 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'url' => 'https://jira.atlassian.net/browse/GEN-310', + 'story_points' => 5, + 'source' => 'SANDBOX', + ], + [ + 'key' => 'GEN-315', + 'summary' => 'Kwartaalplanning en doelstellingen inventarisatie Q4', + 'description' => "Verzamel OKR's van alle teams voor de centrale kwartaalpresentatie.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'High', + 'issue_type' => 'Story', + 'project' => 'General', + 'project_key' => 'GEN', + 'assignee_name' => 'Bram Meijer', + 'assignee_avatar' => '', + 'reporter_name' => 'Operations Lead', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Algemeen', + 'due_date' => date('Y-m-d', strtotime('+8 days')), 'created' => date('Y-m-d H:i:s', strtotime('-4 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-110', + 'updated' => date('Y-m-d H:i:s', strtotime('-5 hours')), + 'url' => 'https://jira.atlassian.net/browse/GEN-315', + 'story_points' => 8, 'source' => 'SANDBOX', ], ]; @@ -887,6 +985,8 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array 'team_issues' => $team, 'queue_filters' => $queueFilters, 'default_queue_filter' => $userQueuePref, + 'user_queue_filters' => $userQueuePrefs, + 'available_statuses' => $demoStatuses, ]; } @@ -921,10 +1021,12 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array } } - // Ophalen van toegewezen taken - $assignedRes = jira_fetch_assigned_issues($domain, $email, $apiToken, 50); - // Ophalen van teamtaken - $teamRes = jira_fetch_team_issues($domain, $email, $apiToken, 50); + // Ophalen van toegewezen taken (max 100) + $assignedRes = jira_fetch_assigned_issues($domain, $email, $apiToken, 100); + // Ophalen van teamtaken (max 100) + $teamRes = jira_fetch_team_issues($domain, $email, $apiToken, 100); + // Ophalen van Jira statussen + $availableStatuses = jira_fetch_statuses($domain, $email, $apiToken); if (!$assignedRes['success'] && !$teamRes['success']) { return [ @@ -950,6 +1052,8 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array 'team_issues' => $teamIssues, 'queue_filters' => $queueFilters, 'default_queue_filter' => $userQueuePref, + 'user_queue_filters' => $userQueuePrefs, + 'available_statuses' => $availableStatuses, ]; fociz_cache_set($cacheKey, $data, 60); diff --git a/jira.php b/jira.php index bfc4c05..c6fadcc 100644 --- a/jira.php +++ b/jira.php @@ -80,12 +80,24 @@ foreach ($allIssues as $iss) { $allStatuses[$iss['status']] = $iss['status']; } } + +// Statussen uitbreiden met officiële Jira instantie statussen +$availableStatuses = $jiraData['available_statuses'] ?? []; +if (!empty($availableStatuses)) { + foreach ($availableStatuses as $st) { + if (!empty($st['name'])) { + $allStatuses[$st['name']] = $st['name']; + } + } +} + ksort($allProjects); ksort($allTypes); ksort($allStatuses); $queueFilters = $jiraData['queue_filters'] ?? jira_get_queue_filters(true); $defaultQueueFilter = $jiraData['default_queue_filter'] ?? jira_get_user_queue_preference($userId); +$userQueueFilters = $jiraData['user_queue_filters'] ?? jira_get_user_queue_preferences($userId); $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); @@ -252,97 +264,11 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT - +
-
-
- - Jira Projecten -
- projecten -
-
- - $pData): ?> - - -
-
- - -
-
-
- - Servicedesk Wachtrijen - - - Jouw voorkeur: - - -
-
- - - - - - - - - - - -
-
-
- - -
-
+
- - -
- -
- +
-
- - -
-
@@ -384,62 +300,227 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
- -
- -
- + +
+ + +
+
+
+ +
+
+ Projecten +

Direct filteren op projectcode

+
+
- +
+ - + + - + + + + + + + + + + $pData): + $pKeyNorm = strtoupper($pKey); + $pDataKeyNorm = strtoupper($pData['key'] ?? ''); + if (in_array($pKeyNorm, ['SD', 'IN', 'AAN', 'GEN']) || in_array($pDataKeyNorm, ['SD', 'IN', 'AAN', 'GEN'])) continue; + $displayKey = !empty($pData['key']) ? $pData['key'] : $pKey; + ?> + + +
- -
-
- - - + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + + + + + +
- +
@@ -794,11 +875,24 @@ const availableBoards = ; let currentTab = 'assigned'; // 'assigned', 'team', 'in_progress', 'done' let currentProject = ''; -let currentQueue = jiraData.default_queue_filter || ''; +let currentQueue = (jiraData.user_queue_filters && jiraData.user_queue_filters.length > 0) ? 'MY_QUEUES' : (jiraData.default_queue_filter || ''); +let hideClosed = true; // Standaard gesloten tickets verbergen conform gebruikerswens! +let onlyInProgress = false; let activeUrgentOnly = false; let expandedKeys = new Set(); const ticketStore = new Map(); +function isDoneStatus(st) { + const s = (st || '').toLowerCase().trim(); + return s.includes('done') || s.includes('gesloten') || s.includes('afgerond') || s.includes('gereed') || s.includes('closed') || s.includes('resolved'); +} + +function isInProgressStatus(st, sc) { + const s = (st || '').toLowerCase().trim(); + const c = (sc || '').toLowerCase().trim(); + return c === 'indeterminate' || s.includes('progress') || s.includes('bezig') || s.includes('review') || s.includes('uitvoering') || s.includes('behandeling') || s.includes('wachten'); +} + // Bereken statussen en KPI statistieken function calculateKpis() { const assigned = jiraData.assigned_issues || []; @@ -814,10 +908,10 @@ function calculateKpis() { const sc = (iss.status_category || '').toLowerCase(); const p = (iss.priority || '').toLowerCase(); - if (s.includes('progress') || s.includes('bezig') || s.includes('review') || sc === 'indeterminate') { + if (isInProgressStatus(s, sc)) { inProgressCount++; } - if (s.includes('done') || s.includes('afgerond') || s.includes('gesloten') || sc === 'done') { + if (isDoneStatus(s) || sc === 'done') { doneCount++; } if (p === 'highest' || p === 'high') { @@ -825,15 +919,28 @@ function calculateKpis() { } }); - document.getElementById('statAssignedCount').innerText = assigned.length; - document.getElementById('statTeamCount').innerText = team.length; - document.getElementById('statInProgressCount').innerText = inProgressCount; - document.getElementById('statHighPriorityCount').innerText = highPriorityCount; + const elStatAssigned = document.getElementById('statAssignedCount'); + const elStatTeam = document.getElementById('statTeamCount'); + const elStatInProg = document.getElementById('statInProgressCount'); + const elStatHigh = document.getElementById('statHighPriorityCount'); - document.getElementById('tabCount_assigned').innerText = assigned.length; - document.getElementById('tabCount_team').innerText = team.length; - document.getElementById('tabCount_in_progress').innerText = inProgressCount; - document.getElementById('tabCount_done').innerText = doneCount; + if (elStatAssigned) elStatAssigned.innerText = assigned.length; + if (elStatTeam) elStatTeam.innerText = team.length; + if (elStatInProg) elStatInProg.innerText = inProgressCount; + if (elStatHigh) elStatHigh.innerText = highPriorityCount; + + const elTabAssigned = document.getElementById('tabCount_assigned'); + const elTabTeam = document.getElementById('tabCount_team'); + const elTabInProg = document.getElementById('tabCount_in_progress'); + const elTabDone = document.getElementById('tabCount_done'); + + if (elTabAssigned) elTabAssigned.innerText = assigned.length; + if (elTabTeam) elTabTeam.innerText = team.length; + if (elTabInProg) elTabInProg.innerText = inProgressCount; + if (elTabDone) elTabDone.innerText = doneCount; + + updateProjectCounts(); + updateQueueCounts(); } // Uitklappen en inklappen van de filters & statistieken drawer @@ -861,10 +968,11 @@ function updateActiveFilterBadge() { let activeCount = 0; if (currentProject) activeCount++; - if (currentQueue) activeCount++; + if (currentQueue && currentQueue !== 'MY_QUEUES') activeCount++; if (document.getElementById('filterType')?.value) activeCount++; if (document.getElementById('filterPriority')?.value) activeCount++; if (document.getElementById('filterStatus')?.value) activeCount++; + if (onlyInProgress) activeCount++; if (activeCount > 0) { badge.innerText = activeCount; @@ -885,57 +993,122 @@ function switchTab(tab) { const btn = document.getElementById(`tabBtn_${t}`); if (!btn) return; if (t === tab) { - btn.className = "flex items-center space-x-1.5 px-3.5 py-2 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"; + 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.5 py-2 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"; + 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"; } }); - updateQueueCounts(); + applyFilters(); +} + +// Toggle: Verberg Gesloten +function toggleHideClosed() { + hideClosed = !hideClosed; + updateHideClosedButton(); + applyFilters(); +} + +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(); +} + +// Toggle: Snelfilter Alleen In Behandeling +function toggleOnlyInProgress() { + onlyInProgress = !onlyInProgress; + updateOnlyInProgressButton(); + applyFilters(); +} + +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 { + 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(); +} + +// Status dropdown verandering +function onStatusSelectChange(val) { + if (val && isDoneStatus(val)) { + hideClosed = false; + updateHideClosedButton(); + } applyFilters(); } // Project selectie via pill tabs function selectProject(proj) { - currentProject = proj; - const pSelect = document.getElementById('filterProject'); - if (pSelect) { - pSelect.value = proj; - } + currentProject = (proj || '').trim(); updateProjectPills(); - updateQueueCounts(); - applyFilters(); -} - -// Project selectie via dropdown -function onProjectSelectChange(proj) { - currentProject = proj; - updateProjectPills(); - updateQueueCounts(); applyFilters(); } function updateProjectPills() { const pills = document.querySelectorAll('.project-pill'); - const curNorm = (currentProject || '').trim().toLowerCase(); + const curNorm = (currentProject || '').toUpperCase().trim(); pills.forEach(p => { - const pKey = (p.getAttribute('data-project') || '').trim().toLowerCase(); - if (pKey === curNorm) { - p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0"; + const pKey = (p.getAttribute('data-project') || '').toUpperCase().trim(); + const isSelected = (pKey === curNorm); + + if (pKey === '') { + p.className = isSelected + ? "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-900 dark:bg-white text-white dark:text-slate-900 shadow-xs flex-shrink-0" + : "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0"; + } else if (pKey === 'SD') { + p.className = isSelected + ? "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-sky-600 text-white shadow-xs flex-shrink-0" + : "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-sky-50 dark:bg-sky-950/40 text-sky-800 dark:text-sky-300 hover:bg-sky-100 dark:hover:bg-sky-900/60 border border-sky-300 dark:border-sky-800 flex-shrink-0"; + } else if (pKey === 'IN') { + p.className = isSelected + ? "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-amber-600 text-white shadow-xs flex-shrink-0" + : "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 border border-amber-300 dark:border-amber-800 flex-shrink-0"; + } else if (pKey === 'AAN') { + p.className = isSelected + ? "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-600 text-white shadow-xs flex-shrink-0" + : "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/60 border border-emerald-300 dark:border-emerald-800 flex-shrink-0"; + } else if (pKey === 'GEN') { + p.className = isSelected + ? "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs flex-shrink-0" + : "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/40 text-indigo-800 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/60 border border-indigo-300 dark:border-indigo-800 flex-shrink-0"; } else { - p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0"; + p.className = isSelected + ? "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs flex-shrink-0" + : "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white 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 flex-shrink-0"; } }); } -// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort (DX, SD of aangepast) +// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort (SD, IN, AAN, GEN, DX of MY_QUEUES) function isIssueInQueue(iss, qCode) { if (!qCode) return true; - const code = qCode.toUpperCase(); + const code = qCode.toUpperCase().trim(); const ol = (iss.onderwerp_logging || '').toLowerCase().trim(); const sum = (iss.summary || '').toLowerCase(); const pKey = (iss.project_key || '').toUpperCase().trim(); + const pName = (iss.project || '').toLowerCase().trim(); + // Multi-queue matching voor de ingelogde gebruiker + if (code === 'MY_QUEUES') { + const myQueues = jiraData.user_queue_filters || []; + if (myQueues.length === 0) return true; + return myQueues.some(q => isIssueInQueue(iss, q)); + } + const dxTerms = ['koppeling', 'sso', 'endpoint', 'rest', 'datadump', 'uwv', 'migratie', 'api', 'webhook', 'integrat']; const matchesDx = dxTerms.some(t => ol.includes(t) || sum.includes(t)); @@ -943,12 +1116,20 @@ function isIssueInQueue(iss, qCode) { return matchesDx; } if (code === 'SD') { - // SD is regulier Servicedesk (alle tickets die niet tot DX routering behoren) - return !matchesDx; + return (pKey === 'SD' || pName.includes('servicedesk') || !matchesDx); + } + if (code === 'IN') { + return (pKey === 'IN' || pName.includes('interface') || matchesDx || ol.includes('interface') || sum.includes('interface')); + } + if (code === 'AAN') { + return (pKey === 'AAN' || pName.includes('aanvraag') || ol.includes('aanvraag') || sum.includes('aanvraag')); + } + if (code === 'GEN') { + return (pKey === 'GEN' || pName.includes('general') || pName.includes('algemeen') || ol.includes('general') || ol.includes('algemeen')); } // Aangepaste wachtrijfilter uit de database - const fObj = (jiraData.queue_filters || []).find(f => f.filter_code.toUpperCase() === code); + const fObj = (jiraData.queue_filters || []).find(f => f.filter_code && f.filter_code.toUpperCase() === code); if (fObj) { if (fObj.project_key && pKey && fObj.project_key.toUpperCase() !== pKey) { return false; @@ -958,6 +1139,7 @@ function isIssueInQueue(iss, qCode) { return true; } if (clause.includes('koppeling') && matchesDx) return true; + if (clause.includes(pKey.toLowerCase())) return true; } return true; } @@ -973,15 +1155,56 @@ function updateQueuePills() { const pills = document.querySelectorAll('.queue-pill'); pills.forEach(p => { const qCode = p.getAttribute('data-queue') || ''; - if (qCode === currentQueue) { + const isSelected = (qCode === currentQueue); + if (isSelected) { p.className = "queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0"; } else { - p.className = "queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0"; + p.className = "queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0"; } }); } -// Update de tellers op de DX / SD wachtrijfilter knoppen +// Update de tellers op de project buttons +function updateProjectCounts() { + const assigned = jiraData.assigned_issues || []; + const team = jiraData.team_issues || []; + const all = [...assigned, ...team]; + + 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'); + if (elAll) elAll.innerText = currentList.length; + + ['SD', 'IN', 'AAN', 'GEN'].forEach(key => { + const el = document.getElementById(`projCount_${key}`); + if (el) { + const cnt = currentList.filter(iss => { + const pk = (iss.project_key || '').toUpperCase().trim(); + const pn = (iss.project || '').toLowerCase().trim(); + if (pk === key) return true; + if (key === 'SD' && pn.includes('servicedesk')) return true; + if (key === 'IN' && pn.includes('interface')) return true; + if (key === 'AAN' && pn.includes('aanvraag')) return true; + if (key === 'GEN' && (pn.includes('general') || pn.includes('algemeen'))) return true; + return false; + }).length; + el.innerText = cnt; + } + }); +} + +// Update de tellers op de wachtrijfilter knoppen function updateQueueCounts() { const assigned = jiraData.assigned_issues || []; const team = jiraData.team_issues || []; @@ -991,52 +1214,45 @@ function updateQueueCounts() { if (currentTab === 'assigned') currentList = assigned; else if (currentTab === 'team') currentList = team; else if (currentTab === 'in_progress') { - currentList = all.filter(iss => { - const s = (iss.status || '').toLowerCase(); - const sc = (iss.status_category || '').toLowerCase(); - return s.includes('progress') || s.includes('bezig') || s.includes('review') || sc === 'indeterminate'; - }); + currentList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category)); } else if (currentTab === 'done') { - currentList = all.filter(iss => { - const s = (iss.status || '').toLowerCase(); - const sc = (iss.status_category || '').toLowerCase(); - return s.includes('done') || s.includes('afgerond') || s.includes('gesloten') || sc === '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) { - const pNorm = currentProject.trim().toLowerCase(); + const pNorm = currentProject.trim().toUpperCase(); currentList = currentList.filter(iss => { - const pName = (iss.project || '').trim().toLowerCase(); - const pKey = (iss.project_key || '').trim().toLowerCase(); - return pName === pNorm || pKey === pNorm; + const pk = (iss.project_key || '').trim().toUpperCase(); + const pn = (iss.project || '').trim().toLowerCase(); + return pk === pNorm || pn.includes(pNorm.toLowerCase()); }); } - let dxCount = 0; - let sdCount = 0; + const elAll = document.getElementById('queueCount_all'); + if (elAll) elAll.innerText = currentList.length; - currentList.forEach(iss => { - if (isIssueInQueue(iss, 'DX')) { - dxCount++; - } else { - sdCount++; + const elMy = document.getElementById('queueCount_MY_QUEUES'); + if (elMy) { + elMy.innerText = currentList.filter(iss => isIssueInQueue(iss, 'MY_QUEUES')).length; + } + + ['SD', 'IN', 'AAN', 'GEN', 'DX'].forEach(code => { + const el = document.getElementById(`queueCount_${code}`); + if (el) { + el.innerText = currentList.filter(iss => isIssueInQueue(iss, code)).length; } }); - const elAll = document.getElementById('queueCount_all'); - const elDx = document.getElementById('queueCount_DX'); - const elSd = document.getElementById('queueCount_SD'); - - if (elAll) elAll.innerText = currentList.length; - if (elDx) elDx.innerText = dxCount; - if (elSd) elSd.innerText = sdCount; - (jiraData.queue_filters || []).forEach(f => { - const el = document.getElementById(`queueCount_${f.filter_code}`); - if (el && f.filter_code !== 'DX' && f.filter_code !== 'SD') { - const cnt = currentList.filter(iss => isIssueInQueue(iss, f.filter_code)).length; - el.innerText = cnt; + if (!['SD', 'IN', 'AAN', 'GEN', 'DX'].includes(f.filter_code)) { + const el = document.getElementById(`queueCount_${f.filter_code}`); + if (el) { + el.innerText = currentList.filter(iss => isIssueInQueue(iss, f.filter_code)).length; + } } }); } @@ -1064,29 +1280,31 @@ function clearSearch() { function resetAllFilters() { currentProject = ''; currentQueue = ''; + hideClosed = true; + onlyInProgress = false; + const searchInput = document.getElementById('jiraSearchInput'); - const projectSelect = document.getElementById('filterProject'); const typeSelect = document.getElementById('filterType'); const prioritySelect = document.getElementById('filterPriority'); const statusSelect = document.getElementById('filterStatus'); if (searchInput) searchInput.value = ''; - if (projectSelect) projectSelect.value = ''; if (typeSelect) typeSelect.value = ''; if (prioritySelect) prioritySelect.value = ''; if (statusSelect) statusSelect.value = ''; document.getElementById('clearSearchBtn')?.classList.add('hidden'); + updateHideClosedButton(); + updateOnlyInProgressButton(); updateProjectPills(); updateQueuePills(); - updateQueueCounts(); applyFilters(); } // Filter en render logic function applyFilters() { const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase(); - const projectVal = currentProject || (document.getElementById('filterProject')?.value || ''); + const projectVal = (currentProject || '').toUpperCase().trim(); const typeVal = (document.getElementById('filterType')?.value || '').trim().toLowerCase(); const priorityVal = (document.getElementById('filterPriority')?.value || '').trim(); const statusVal = (document.getElementById('filterStatus')?.value || '').trim().toLowerCase(); @@ -1107,52 +1325,65 @@ function applyFilters() { } else if (currentTab === 'team') { sourceList = team; } else if (currentTab === 'in_progress') { - sourceList = all.filter(iss => { - const s = (iss.status || '').toLowerCase(); - const sc = (iss.status_category || '').toLowerCase(); - return s.includes('progress') || s.includes('bezig') || s.includes('review') || sc === 'indeterminate'; - }); + sourceList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category)); } else if (currentTab === 'done') { - sourceList = all.filter(iss => { - const s = (iss.status || '').toLowerCase(); - const sc = (iss.status_category || '').toLowerCase(); - return s.includes('done') || s.includes('afgerond') || s.includes('gesloten') || sc === 'done'; - }); + sourceList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done'); } - // Pas zoek-, project-, wachtrij-, type-, prioriteit- en statusfilters toe + // Pas filters toe const filtered = sourceList.filter(iss => { - if (searchVal) { - const keyMatch = (iss.key || '').toLowerCase().includes(searchVal); - const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal); - const prjMatch = (iss.project || '').toLowerCase().includes(searchVal); - const prjKeyMatch = (iss.project_key || '').toLowerCase().includes(searchVal); - const descMatch = (iss.description || '').toLowerCase().includes(searchVal); - const repMatch = (iss.reporter_name || '').toLowerCase().includes(searchVal); - const assMatch = (iss.assignee_name || '').toLowerCase().includes(searchVal); - const olMatch = (iss.onderwerp_logging || '').toLowerCase().includes(searchVal); - if (!keyMatch && !sumMatch && !prjMatch && !prjKeyMatch && !descMatch && !repMatch && !assMatch && !olMatch) return false; - } + 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(); - if (projectVal) { - const pNorm = projectVal.trim().toLowerCase(); - const pName = (iss.project || '').trim().toLowerCase(); - const pKey = (iss.project_key || '').trim().toLowerCase(); - if (pName !== pNorm && pKey !== pNorm) { + // 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; } } + // 2. Snelfilter "Alleen in behandeling" + if (onlyInProgress) { + if (!isInProgressStatus(issStatus, issCat)) { + return false; + } + } + + // 3. Project Filter (SD, IN, AAN, GEN of ander) + if (projectVal) { + let matchesProj = (issPKey === projectVal); + if (!matchesProj) { + if (projectVal === 'SD' && issPName.includes('servicedesk')) matchesProj = true; + else if (projectVal === 'IN' && issPName.includes('interface')) matchesProj = true; + else if (projectVal === 'AAN' && issPName.includes('aanvraag')) matchesProj = true; + else if (projectVal === 'GEN' && (issPName.includes('general') || issPName.includes('algemeen'))) matchesProj = true; + else if (issPName === projectVal.toLowerCase()) matchesProj = true; + } + if (!matchesProj) return false; + } + + // 4. Wachtrijfilter if (currentQueue) { if (!isIssueInQueue(iss, currentQueue)) { return false; } } + // 5. Specifieke status selectie + if (statusVal) { + if (issStatus !== statusVal) { + return false; + } + } + + // 6. Type filter if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) { return false; } + // 7. Prioriteit filter if (priorityVal) { const pVal = priorityVal.toLowerCase(); const ip = (iss.priority || '').toLowerCase(); @@ -1167,14 +1398,25 @@ function applyFilters() { } } - if (statusVal && (iss.status || '').toLowerCase() !== statusVal) { - return false; + // 8. Zoekterm + if (searchVal) { + const keyMatch = (iss.key || '').toLowerCase().includes(searchVal); + const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal); + const prjMatch = issPName.includes(searchVal); + const prjKeyMatch = issPKey.includes(searchVal.toUpperCase()); + const descMatch = (iss.description || '').toLowerCase().includes(searchVal); + const repMatch = (iss.reporter_name || '').toLowerCase().includes(searchVal); + const assMatch = (iss.assignee_name || '').toLowerCase().includes(searchVal); + const olMatch = (iss.onderwerp_logging || '').toLowerCase().includes(searchVal); + if (!keyMatch && !sumMatch && !prjMatch && !prjKeyMatch && !descMatch && !repMatch && !assMatch && !olMatch) return false; } return true; }); updateActiveFilterBadge(); + updateProjectCounts(); + updateQueueCounts(); renderTickets(filtered); } @@ -1262,20 +1504,46 @@ function renderTickets(tickets) { prioIcon = 'chevron-down'; } + // Project badge styling (SD, IN, AAN, GEN) + const pKey = (ticket.project_key || '').toUpperCase().trim(); + const pName = (ticket.project || '').toLowerCase().trim(); + let projBadgeClass = 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 border border-slate-200/80 dark:border-slate-700/80'; + let projLabel = escapeHtml(ticket.project || 'Project'); + + if (pKey === 'SD' || pName.includes('servicedesk')) { + projBadgeClass = 'bg-sky-50 dark:bg-sky-950/40 text-sky-700 dark:text-sky-300 border border-sky-300 dark:border-sky-800/80'; + projLabel = 'SD • Servicedesk'; + } else if (pKey === 'IN' || pName.includes('interface')) { + projBadgeClass = 'bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-300 border border-amber-300 dark:border-amber-800/80'; + projLabel = 'IN • Interfaces'; + } else if (pKey === 'AAN' || pName.includes('aanvraag')) { + projBadgeClass = 'bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80'; + projLabel = 'AAN • Aanvragen'; + } else if (pKey === 'GEN' || pName.includes('general') || pName.includes('algemeen')) { + projBadgeClass = 'bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border border-indigo-300 dark:border-indigo-800/80'; + projLabel = 'GEN • General'; + } + // Status badge styling let statusBadgeClass = 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 border-slate-200 dark:border-slate-700'; let statusDot = ''; const sLower = (ticket.status || '').toLowerCase(); const scLower = (ticket.status_category || '').toLowerCase(); - if (sLower.includes('progress') || sLower.includes('bezig') || scLower === 'indeterminate') { + if (sLower.includes('progress') || sLower.includes('bezig') || sLower.includes('behandeling') || sLower.includes('uitvoering') || scLower === 'indeterminate') { statusBadgeClass = 'bg-blue-50 text-blue-700 dark:bg-blue-950/40 dark:text-blue-300 border-blue-200 dark:border-blue-800/60'; statusDot = ''; + } else if (sLower.includes('wachten') || sLower.includes('klant') || sLower.includes('waiting') || sLower.includes('pending')) { + statusBadgeClass = 'bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300 border-amber-200 dark:border-amber-800/60'; + statusDot = ''; } else if (sLower.includes('review') || sLower.includes('test')) { statusBadgeClass = 'bg-purple-50 text-purple-700 dark:bg-purple-950/40 dark:text-purple-300 border-purple-200 dark:border-purple-800/60'; statusDot = ''; - } else if (sLower.includes('done') || sLower.includes('afgerond') || scLower === 'done') { + } else if (sLower.includes('done') || sLower.includes('afgerond') || sLower.includes('gesloten') || sLower.includes('gereed') || sLower.includes('closed') || scLower === 'done') { statusBadgeClass = 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300 border-emerald-200 dark:border-emerald-800/60'; statusDot = ''; + } else if (sLower.includes('to do') || sLower.includes('open') || sLower.includes('nieuw') || scLower === 'new') { + statusBadgeClass = 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 border-slate-200 dark:border-slate-700'; + statusDot = ''; } // Due date badge @@ -1358,10 +1626,10 @@ function renderTickets(tickets) { - - - - ${escapeHtml(ticket.project || 'Project')} + + + + ${projLabel} @@ -1998,6 +2266,8 @@ document.addEventListener('DOMContentLoaded', () => { } calculateKpis(); + updateHideClosedButton(); + updateOnlyInProgressButton(); updateProjectPills(); updateQueueCounts(); if (currentQueue) {