diff --git a/admin.php b/admin.php index d96e99e..1dcb0b1 100644 --- a/admin.php +++ b/admin.php @@ -93,6 +93,74 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $messageType = 'success'; } + // 2b. Jira Wachtrijfilter toevoegen of bewerken + elseif ($action === 'save_jira_queue_filter') { + $filterId = (int)($_POST['filter_id'] ?? 0); + $filterCode = strtoupper(trim($_POST['filter_code'] ?? '')); + $filterName = trim($_POST['filter_name'] ?? ''); + $projectKey = strtoupper(trim($_POST['project_key'] ?? '')); + $jqlClause = trim($_POST['jql_clause'] ?? ''); + $description = trim($_POST['description'] ?? ''); + $isActive = isset($_POST['is_active']) ? 1 : 0; + $sortOrder = (int)($_POST['sort_order'] ?? 0); + + if (empty($filterCode) || empty($filterName) || empty($jqlClause)) { + $message = "Filtercode, naam en JQL-clausule zijn verplicht."; + $messageType = 'error'; + } else { + if ($filterId > 0) { + $stmt = $db->prepare(" + UPDATE jira_queue_filters + SET filter_code = ?, filter_name = ?, project_key = ?, jql_clause = ?, description = ?, is_active = ?, sort_order = ?, updated_at = NOW() + WHERE id = ? + "); + $stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder, $filterId]); + $message = "Wachtrijfilter '{$filterName}' is succesvol bijgewerkt."; + } else { + $stmt = $db->prepare(" + INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + filter_name = VALUES(filter_name), + project_key = VALUES(project_key), + jql_clause = VALUES(jql_clause), + description = VALUES(description), + is_active = VALUES(is_active), + sort_order = VALUES(sort_order), + updated_at = NOW() + "); + $stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]); + $message = "Nieuw wachtrijfilter '{$filterName}' is succesvol toegevoegd."; + } + $messageType = 'success'; + } + } + + // 2c. Jira Wachtrijfilter verwijderen + elseif ($action === 'delete_jira_queue_filter') { + $filterId = (int)($_POST['filter_id'] ?? 0); + if ($filterId > 0) { + $stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?"); + $stmt->execute([$filterId]); + $message = "Wachtrijfilter is succesvol verwijderd."; + $messageType = 'success'; + } + } + + // 2d. Teamleden toewijzen aan Jira Wachtrijfilter + elseif ($action === 'save_user_jira_assignments') { + $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]); + } + $message = "Standaard Jira wachtrijtoewijzingen voor het team zijn succesvol opgeslagen."; + $messageType = 'success'; + } + } + // 3. Organisatie Werkdag Standaarden opslaan (Release v1.4) elseif ($action === 'save_workday_settings') { $workdayStart = trim($_POST['workday_start'] ?? '08:30'); @@ -215,6 +283,15 @@ try { error_log("Fout bij ophalen recente gebruikers: " . $e->getMessage()); } +$allUsersForJira = []; +$allJiraFilters = []; +try { + $allUsersForJira = $db->query("SELECT id, name, email, role, avatar_url, default_jira_filter_code 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()); +} + $pageTitle = 'Systeembeheer & Integraties — Fociz'; require_once __DIR__ . '/includes/header.php'; ?> @@ -497,6 +574,286 @@ require_once __DIR__ . '/includes/header.php'; + +
+
+
+
+ +
+
+
+

Jira Wachtrijfilters & Toewijzingen

+ + ServiceDesk & Teams + +
+

+ Beheer aangepaste JQL-wachtrijfilters (zoals DX en SD) en wijs teamleden toe aan hun standaard wachtrij. +

+
+
+ +
+ + +
+
+

+ + Geconfigureerde Wachtrijfilters () +

+ Worden getoond als snelle filtertabs op de Jira pagina +
+ + +
+

Nog geen wachtrijfilters ingesteld. Klik op "Nieuw Wachtrijfilter" om te beginnen.

+
+ + +
+ + +
+
+
+ + + + + + + + + Project: + + + + Alle projecten + + + + + Inactief + + +
+ + +

+ +

+ + + +
+ JQL: +
+ + +
+ Standaard gekoppeld aan: + + + + + + + + + + Nog niemand specifiek toegewezen + +
+
+ + +
+ +
+ + + +
+
+
+ +
+ +
+ + +
+
+
+

+ + Team Wachtrijmatrix: Standaard Wachtrij 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. +

+
+
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + +
TeamlidE-mailadresRolStandaard Wachtrijfilter
+
+ + Avatar + +
+ +
+ + +
+
+ + + + + + + +
+
+ +
+ +
+
+
+
+ + + +
@@ -860,6 +1217,49 @@ function toggleAdminGuide() { arrow.classList.toggle('rotate-180'); } } + +// Jira Wachtrijfilter Modalfuncties +function openJiraFilterModal() { + document.getElementById('jiraFilterModalTitle').innerText = 'Nieuw Wachtrijfilter Toevoegen'; + document.getElementById('filterModal_id').value = '0'; + document.getElementById('filterModal_code').value = ''; + document.getElementById('filterModal_name').value = ''; + document.getElementById('filterModal_project').value = 'SD'; + document.getElementById('filterModal_jql').value = ''; + document.getElementById('filterModal_desc').value = ''; + document.getElementById('filterModal_active').checked = true; + + const modal = document.getElementById('jiraFilterModal'); + modal.classList.remove('hidden'); + modal.classList.add('flex'); +} + +function editJiraFilter(filter) { + document.getElementById('jiraFilterModalTitle').innerText = `Wachtrijfilter '${filter.filter_name}' Bewerken`; + document.getElementById('filterModal_id').value = filter.id || '0'; + document.getElementById('filterModal_code').value = filter.filter_code || ''; + document.getElementById('filterModal_name').value = filter.filter_name || ''; + document.getElementById('filterModal_project').value = filter.project_key || ''; + document.getElementById('filterModal_jql').value = filter.jql_clause || ''; + document.getElementById('filterModal_desc').value = filter.description || ''; + document.getElementById('filterModal_active').checked = (filter.is_active != 0); + + const modal = document.getElementById('jiraFilterModal'); + modal.classList.remove('hidden'); + modal.classList.add('flex'); +} + +function closeJiraFilterModal() { + const modal = document.getElementById('jiraFilterModal'); + modal.classList.add('hidden'); + modal.classList.remove('flex'); +} + +document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + closeJiraFilterModal(); + } +}); diff --git a/api/integrations.php b/api/integrations.php index b8f78e7..7c8656c 100644 --- a/api/integrations.php +++ b/api/integrations.php @@ -272,6 +272,107 @@ try { exit; } + // 5a. Jira Wachtrijfilters Ophalen + if ($action === 'get_jira_queue_filters') { + $onlyActive = !isset($_GET['all']) && !isset($_POST['all']); + $filters = jira_get_queue_filters($onlyActive); + echo json_encode(['success' => true, 'filters' => $filters]); + exit; + } + + // 5b. Jira Wachtrijfilter Opslaan / Wijzigen (Alleen Beheerders) + if ($action === 'save_jira_queue_filter') { + if (($user['role'] ?? '') !== 'ADMIN') { + echo json_encode(['error' => 'Alleen beheerders mogen wachtrijfilters aanpassen.']); + exit; + } + + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $id = (int)($input['id'] ?? 0); + $filterCode = strtoupper(trim($input['filter_code'] ?? '')); + $filterName = trim($input['filter_name'] ?? ''); + $projectKey = strtoupper(trim($input['project_key'] ?? '')); + $jqlClause = trim($input['jql_clause'] ?? ''); + $description = trim($input['description'] ?? ''); + $isActive = isset($input['is_active']) ? (int)$input['is_active'] : 1; + $sortOrder = (int)($input['sort_order'] ?? 0); + + if (empty($filterCode) || empty($filterName) || empty($jqlClause)) { + echo json_encode(['error' => 'Code, naam en JQL-clausule zijn verplicht.']); + exit; + } + + if ($id > 0) { + $stmt = $db->prepare(" + UPDATE jira_queue_filters + SET filter_code = ?, filter_name = ?, project_key = ?, jql_clause = ?, description = ?, is_active = ?, sort_order = ?, updated_at = NOW() + WHERE id = ? + "); + $stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder, $id]); + } else { + $stmt = $db->prepare(" + INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + filter_name = VALUES(filter_name), + project_key = VALUES(project_key), + jql_clause = VALUES(jql_clause), + description = VALUES(description), + is_active = VALUES(is_active), + sort_order = VALUES(sort_order), + updated_at = NOW() + "); + $stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]); + } + + echo json_encode(['success' => true, 'message' => "Wachtrijfilter '{$filterName}' is succesvol opgeslagen."]); + exit; + } + + // 5c. Jira Wachtrijfilter Verwijderen (Alleen Beheerders) + if ($action === 'delete_jira_queue_filter') { + if (($user['role'] ?? '') !== 'ADMIN') { + echo json_encode(['error' => 'Alleen beheerders mogen wachtrijfilters verwijderen.']); + exit; + } + + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $id = (int)($input['id'] ?? 0); + $filterCode = trim($input['filter_code'] ?? ''); + + if ($id > 0) { + $stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?"); + $stmt->execute([$id]); + } elseif (!empty($filterCode)) { + $stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE filter_code = ?"); + $stmt->execute([$filterCode]); + } else { + echo json_encode(['error' => 'Filter ID of code ontbreekt.']); + exit; + } + + echo json_encode(['success' => true, 'message' => 'Wachtrijfilter is verwijderd.']); + exit; + } + + // 5d. Standaard Wachtrijfilter Toewijzen aan Gebruiker (Beheerders of Gebruiker zelf) + if ($action === 'set_user_jira_filter') { + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $targetUserId = (int)($input['user_id'] ?? $user['id']); + $filterCode = trim($input['filter_code'] ?? ''); + + if ($targetUserId !== (int)$user['id'] && ($user['role'] ?? '') !== 'ADMIN') { + echo json_encode(['error' => 'Geen toestemming om instellingen van andere gebruikers te wijzigen.']); + exit; + } + + $stmt = $db->prepare("UPDATE users SET default_jira_filter_code = ? WHERE id = ?"); + $stmt->execute([$filterCode ?: null, $targetUserId]); + + echo json_encode(['success' => true, 'message' => 'Wachtrijvoorkeur is succesvol bijgewerkt.']); + exit; + } + echo json_encode(['error' => 'Onbekende actie']); } catch (Exception $e) { http_response_code(500); diff --git a/config/database.php b/config/database.php index 77d4cd4..88ea16d 100644 --- a/config/database.php +++ b/config/database.php @@ -147,8 +147,10 @@ function ensure_tables_exist(PDO $pdo): void { "ALTER TABLE users ADD COLUMN theme VARCHAR(20) NOT NULL DEFAULT 'dark' AFTER notification_settings", "ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER theme", "ALTER TABLE users ADD COLUMN backup_codes TEXT NULL AFTER two_factor_enabled", + "ALTER TABLE users ADD COLUMN default_jira_filter_code VARCHAR(50) NULL AFTER avatar_url", "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", ]; foreach ($colUpgrades as $sql) { try { @@ -158,6 +160,35 @@ function ensure_tables_exist(PDO $pdo): void { } } + // Zorg dat de standaard Jira Servicedesk wachtrijfilters (DX en SD) 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 + ]); + } + } catch (Throwable $e) { + // Stil negeren indien tabel nog in opbouw is + } + // Zorg dat er ten minste één standaard bord met standaard lanes aanwezig is try { $boardCount = (int)$pdo->query("SELECT COUNT(*) FROM boards")->fetchColumn(); diff --git a/config/schema.sql b/config/schema.sql index d49f3c2..e855a8c 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS `users` ( `remember_token_expires_at` DATETIME NULL, `notification_settings` TEXT NULL, `avatar_url` VARCHAR(255) NULL, + `default_jira_filter_code` VARCHAR(50) NULL, `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; @@ -218,3 +219,16 @@ CREATE TABLE IF NOT EXISTS `board_card_schedules` ( UNIQUE KEY `uk_sched_card` (`card_id`), CONSTRAINT `fk_bsched_card` FOREIGN KEY (`card_id`) REFERENCES `board_cards` (`id`) ON DELETE CASCADE ) 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; diff --git a/includes/integrations/jira.php b/includes/integrations/jira.php index 46dedf1..c9caade 100644 --- a/includes/integrations/jira.php +++ b/includes/integrations/jira.php @@ -142,6 +142,76 @@ function jira_test_connection(string $domain, string $email, string $apiToken): /** * Haalt alle actieve issues op die zijn toegewezen aan de ingelogde Jira gebruiker */ +/** + * Converteert Atlassian Document Format (ADF) of tekst naar platte leesbare tekst + */ +function jira_adf_to_text($adf): string { + if (empty($adf)) { + return ''; + } + if (is_string($adf)) { + return trim($adf); + } + if (!is_array($adf)) { + return ''; + } + + $text = ''; + $type = $adf['type'] ?? ''; + + if ($type === 'text') { + return $adf['text'] ?? ''; + } + + if (!empty($adf['content']) && is_array($adf['content'])) { + foreach ($adf['content'] as $child) { + $childText = jira_adf_to_text($child); + $childType = $child['type'] ?? ''; + + if ($childType === 'paragraph' || $childType === 'heading') { + $text .= $childText . "\n\n"; + } elseif ($childType === 'listItem') { + $text .= "• " . trim($childText) . "\n"; + } elseif ($childType === 'codeBlock') { + $text .= "\n" . trim($childText) . "\n\n"; + } else { + $text .= $childText; + } + } + } + + return trim($text); +} + +/** + * Haalt alle geconfigureerde Jira wachtrijfilters op uit de database + */ +function jira_get_queue_filters(bool $onlyActive = true): array { + try { + $db = get_db_connection(); + $sql = "SELECT * FROM jira_queue_filters " . ($onlyActive ? "WHERE is_active = 1 " : "") . "ORDER BY sort_order ASC, id ASC"; + return $db->query($sql)->fetchAll() ?: []; + } catch (Throwable $e) { + error_log("jira_get_queue_filters error: " . $e->getMessage()); + return []; + } +} + +/** + * Haalt de standaard wachtrijfilter voorkeur van een gebruiker op + */ +function jira_get_user_queue_preference(int $userId): ?string { + try { + $db = get_db_connection(); + $stmt = $db->prepare("SELECT default_jira_filter_code FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $res = $stmt->fetchColumn(); + return $res ? (string)$res : null; + } catch (Throwable $e) { + return null; + } +} + /** * Haalt issues op via een flexibele JQL query via Atlassian REST API */ @@ -159,7 +229,9 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri 'assignee', 'reporter', 'duedate', - 'updated' + 'updated', + 'created', + 'description', ]; // 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST @@ -242,26 +314,60 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri $projectKey = $fields['project']['key'] ?? ''; $dueDate = $fields['duedate'] ?? null; $updated = $fields['updated'] ?? null; + $created = $fields['created'] ?? null; $assigneeName = $fields['assignee']['displayName'] ?? 'Niet toegewezen'; $assigneeAvatar = $fields['assignee']['avatarUrls']['48x48'] ?? ($fields['assignee']['avatarUrls']['32x32'] ?? ''); + $reporterName = $fields['reporter']['displayName'] ?? 'Onbekend'; + $reporterAvatar = $fields['reporter']['avatarUrls']['48x48'] ?? ($fields['reporter']['avatarUrls']['32x32'] ?? ''); + + $descriptionText = jira_adf_to_text($fields['description'] ?? ''); + + // Detectie van custom field "Onderwerp logging" + $onderwerpLogging = ''; + foreach ($fields as $fKey => $fVal) { + if (str_starts_with($fKey, 'customfield_')) { + if (is_array($fVal) && isset($fVal['value']) && is_string($fVal['value'])) { + $valStr = trim($fVal['value']); + $known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie', 'incident', 'vraag', 'algemeen']; + if (in_array(strtolower($valStr), $known)) { + $onderwerpLogging = $valStr; + } + } elseif (is_string($fVal) && !empty($fVal)) { + $valStr = trim($fVal); + $known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; + if (in_array(strtolower($valStr), $known)) { + $onderwerpLogging = $valStr; + } + } + } + } + if (empty($onderwerpLogging) && !empty($fields['onderwerp_logging'])) { + $onderwerpLogging = is_string($fields['onderwerp_logging']) ? $fields['onderwerp_logging'] : ($fields['onderwerp_logging']['value'] ?? ''); + } + $issues[] = [ - 'key' => $key, - 'summary' => $summary, - 'status' => $statusName, - 'status_category' => $statusCategory, - 'priority' => $priorityName, - 'issue_type' => $issueTypeName, - 'issue_type_icon' => $issueTypeIcon, - 'project' => $projectName, - 'project_key' => $projectKey, - 'assignee_name' => $assigneeName, - 'assignee_avatar' => $assigneeAvatar, - 'due_date' => $dueDate, - 'updated' => $updated, - 'url' => "{$cleanDomain}/browse/{$key}", - 'source' => 'JIRA_LIVE', + 'key' => $key, + 'summary' => $summary, + 'description' => $descriptionText, + 'status' => $statusName, + 'status_category' => $statusCategory, + 'priority' => $priorityName, + 'issue_type' => $issueTypeName, + 'issue_type_icon' => $issueTypeIcon, + 'project' => $projectName, + 'project_key' => $projectKey, + 'assignee_name' => $assigneeName, + 'assignee_avatar' => $assigneeAvatar, + 'reporter_name' => $reporterName, + 'reporter_avatar' => $reporterAvatar, + 'onderwerp_logging' => $onderwerpLogging, + 'due_date' => $dueDate, + 'created' => $created, + 'updated' => $updated, + 'url' => "{$cleanDomain}/browse/{$key}", + 'source' => 'JIRA_LIVE', ]; } @@ -297,247 +403,407 @@ 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 + $queueFilters = jira_get_queue_filters(true); + $userQueuePref = jira_get_user_queue_preference($userId); + // Sandbox / Demo dataset indien niet gekoppeld of expliciet in demomodus if (!$integration || !empty($integration['is_demo_mode'])) { $assigned = [ [ - 'key' => 'FOC-101', - 'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren', - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'High', - 'issue_type' => 'Story', - 'project' => 'Fociz Platform', - 'project_key' => 'FOC', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+2 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-101', - 'source' => 'SANDBOX', + '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', + 'status_category' => 'indeterminate', + 'priority' => 'Highest', + 'issue_type' => 'Bug', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'Kees van de Berg (AFAS Beheer)', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'SSO-koppeling', + 'due_date' => date('Y-m-d', strtotime('+1 day')), + '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', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-102', - 'summary' => 'Interactief whiteboard canvas voor brainstormsessies', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'High', + '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.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'High', + 'issue_type' => 'Task', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'Security Team', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Rest-endpoints', + 'due_date' => date('Y-m-d', strtotime('+3 days')), + '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', + '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', + 'priority' => 'High', + 'issue_type' => 'Task', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + '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', - 'status' => 'In Review', - 'status_category' => 'indeterminate', - 'priority' => 'Medium', + '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-104', - 'summary' => 'Roadmap kwartaalweergave en voortgangsindicatoren', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'Medium', - 'issue_type' => 'Story', - 'project' => 'Roadmap Module', - 'project_key' => 'ROAD', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+7 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-3 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-104', - '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-108', - 'summary' => 'CSS scrollbar glitch oplossen in Kanban lane op Firefox', - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'Highest', - 'issue_type' => 'Bug', - 'project' => 'UI/UX Redesign', - 'project_key' => 'UI', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+1 day')), - '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', - 'status' => 'Done', - 'status_category' => 'done', - 'priority' => 'Medium', - 'issue_type' => 'Task', - 'project' => 'Security', - 'project_key' => 'SEC', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('-1 day')), - 'updated' => date('Y-m-d H:i:s', strtotime('-5 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-112', - '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', + 'source' => 'SANDBOX', ], ]; $team = [ [ - 'key' => 'FOC-95', - 'summary' => 'Sessie timeout notificatie verschijnt niet tijdig bij inactieve tab', - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'Highest', - 'issue_type' => 'Bug', - 'project' => 'Fociz Platform', - 'project_key' => 'FOC', - 'assignee_name' => 'Lisa Bakker', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+1 day')), - 'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')), - 'url' => 'https://jira.atlassian.net/browse/FOC-95', - 'source' => 'SANDBOX', + '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', + 'status_category' => 'indeterminate', + 'priority' => 'High', + 'issue_type' => 'Bug', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Lisa Bakker', + 'assignee_avatar' => '', + 'reporter_name' => 'Servicedesk L2', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'UWV-koppeling', + 'due_date' => date('Y-m-d', strtotime('+2 days')), + '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', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-98', - 'summary' => 'Automatische e-mail triggers voor taakdeadlines en uitnodigingen', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'High', - 'issue_type' => 'Story', - 'project' => 'Notifications', - 'project_key' => 'NOTIF', - 'assignee_name' => 'Daan van Dijk', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+3 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-2 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-98', - 'source' => 'SANDBOX', + 'key' => 'SD-412', + 'summary' => 'Datadump archief export voor externe accountant audit 2025', + 'description' => "Genereer een beveiligde, versleutelde datadump van alle audit-logs en urenstaten voor de periodieke controle.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'Medium', + 'issue_type' => 'Story', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Daan van Dijk', + 'assignee_avatar' => '', + 'reporter_name' => 'Compliance Officer', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Datadump', + 'due_date' => date('Y-m-d', strtotime('+6 days')), + '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', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-105', - 'summary' => 'Database indexering optimaliseren voor grote audit logs', - 'status' => 'In Review', - 'status_category' => 'indeterminate', - 'priority' => 'Medium', - 'issue_type' => 'Task', - 'project' => 'Database & Core', - 'project_key' => 'CORE', - 'assignee_name' => 'Sophie de Vries', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+5 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), - 'url' => 'https://jira.atlassian.net/browse/FOC-105', - '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' => 'FOC-107', - 'summary' => 'Exporteer wekelijkse team rapportage naar PDF en Excel spreadsheet', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'Low', - 'issue_type' => 'Feature', - 'project' => 'Reporting', - 'project_key' => 'REP', - 'assignee_name' => 'Thomas Jansen', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+10 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-4 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-107', - 'source' => 'SANDBOX', + 'key' => 'SD-420', + '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', + 'status_category' => 'indeterminate', + 'priority' => 'Medium', + 'issue_type' => 'Task', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Sophie de Vries', + 'assignee_avatar' => '', + 'reporter_name' => 'Integratie Specialist', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Koppelingen', + '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', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-110', - 'summary' => 'Real-time websocket synchronisatie voor gelijktijdig bewerken van borden', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'High', - 'issue_type' => 'Story', - 'project' => 'Realtime Engine', - 'project_key' => 'RT', - 'assignee_name' => 'Bram Meijer', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+6 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-110', - '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', + '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' => 'FOC-114', - 'summary' => 'Datum picker springt terug naar 1970 bij lege invoer', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'Medium', - 'issue_type' => 'Bug', - 'project' => 'UI/UX Redesign', - 'project_key' => 'UI', - 'assignee_name' => 'Niet toegewezen', - 'assignee_avatar' => '', - 'due_date' => null, - 'updated' => date('Y-m-d H:i:s', strtotime('-2 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-114', - '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', + 'assignee_name' => 'Thomas Jansen', + 'assignee_avatar' => '', + 'reporter_name' => 'Financieel Directeur', + '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-89', - 'summary' => 'GDPR / AVG compliancy en geautomatiseerd data retentiebeleid', - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'Medium', - 'issue_type' => 'Epic', - 'project' => 'Security', - 'project_key' => 'SEC', - 'assignee_name' => 'Lisa Bakker', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+14 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-3 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-89', - '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-82', - 'summary' => 'Performance optimalisatie van vector avatar rendering in navigatiebalk', - 'status' => 'Done', - 'status_category' => 'done', - 'priority' => 'Low', - 'issue_type' => 'Task', - 'project' => 'UI/UX Redesign', - 'project_key' => 'UI', - 'assignee_name' => 'Sophie de Vries', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('-3 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-2 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-82', - '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' => '', + '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', + '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', + 'priority' => 'High', + 'issue_type' => 'Story', + 'project' => 'Realtime Engine', + 'project_key' => 'RT', + 'assignee_name' => 'Bram Meijer', + 'assignee_avatar' => '', + 'reporter_name' => 'Architect', + 'reporter_avatar' => '', + 'onderwerp_logging' => '', + 'due_date' => date('Y-m-d', strtotime('+6 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', + 'source' => 'SANDBOX', ], ]; return [ - 'is_demo' => true, - 'success' => true, - 'domain' => 'sandbox.atlassian.net', - 'email' => 'demo@fociz.nl', - 'account_name' => 'Demo Account', - 'last_sync_at' => date('Y-m-d H:i:s'), - 'assigned_issues' => $assigned, - 'team_issues' => $team, + 'is_demo' => true, + 'success' => true, + 'domain' => 'sandbox.atlassian.net', + 'email' => 'demo@fociz.nl', + 'account_name' => 'Demo Account', + 'last_sync_at' => date('Y-m-d H:i:s'), + 'assigned_issues' => $assigned, + 'team_issues' => $team, + 'queue_filters' => $queueFilters, + 'default_queue_filter' => $userQueuePref, ]; } @@ -587,14 +853,16 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array $teamIssues = $teamRes['success'] ? $teamRes['issues'] : []; $data = [ - 'is_demo' => false, - 'success' => true, - 'domain' => $domain, - 'email' => $email, - 'account_name' => $accName, - 'last_sync_at' => date('Y-m-d H:i:s'), - 'assigned_issues' => $assignedIssues, - 'team_issues' => $teamIssues, + 'is_demo' => false, + 'success' => true, + 'domain' => $domain, + 'email' => $email, + 'account_name' => $accName, + 'last_sync_at' => date('Y-m-d H:i:s'), + 'assigned_issues' => $assignedIssues, + 'team_issues' => $teamIssues, + 'queue_filters' => $queueFilters, + 'default_queue_filter' => $userQueuePref, ]; $_SESSION[$cacheKey] = [ diff --git a/jira.php b/jira.php index f4d1648..d63d25e 100644 --- a/jira.php +++ b/jira.php @@ -56,16 +56,22 @@ foreach ($boards as &$b) { } unset($b); -// Verzamel unieke projecten en types voor filter dropdowns +// Verzamel unieke projecten en types voor filter dropdowns en project switcher $allIssues = array_merge($assignedIssues, $teamIssues); $allProjects = []; $allTypes = []; $allPriorities = ['Highest', 'High', 'Medium', 'Low']; $allStatuses = []; +$projectCounts = []; foreach ($allIssues as $iss) { if (!empty($iss['project'])) { - $allProjects[$iss['project']] = $iss['project']; + $p = $iss['project']; + $allProjects[$p] = [ + 'name' => $p, + 'key' => $iss['project_key'] ?? '', + ]; + $projectCounts[$p] = ($projectCounts[$p] ?? 0) + 1; } if (!empty($iss['issue_type'])) { $allTypes[$iss['issue_type']] = $iss['issue_type']; @@ -78,6 +84,9 @@ 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); + $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); ?> @@ -229,6 +238,97 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
+ +
+
+
+
+ +
+
+

Jira Projecten

+

Klik op een project om direct te filteren

+
+
+ +
+ +
+ + + + + $pData): ?> + + +
+
+ + +
+
+
+
+ +
+
+
+ Servicedesk Wachtrijfilters + + + Jouw voorkeur: + + +
+

+ Filtert op onderwerp logging routering (DX voor koppelingen & endpoints, SD voor reguliere support) +

+
+
+ + +
+ + + + + + + + + + + +
+
+
+
@@ -250,10 +350,10 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
- - - + $pData): ?> +
@@ -573,7 +673,10 @@ const jiraData = ; const availableBoards = ; let currentTab = 'assigned'; // 'assigned', 'team', 'in_progress', 'done' +let currentProject = ''; +let currentQueue = jiraData.default_queue_filter || ''; let activeUrgentOnly = false; +let expandedKeys = new Set(); // Bereken statussen en KPI statistieken function calculateKpis() { @@ -629,9 +732,97 @@ function switchTab(tab) { } }); + updateQueueCounts(); applyFilters(); } +// Project selectie via pill tabs +function selectProject(proj) { + currentProject = proj; + const pSelect = document.getElementById('filterProject'); + if (pSelect) { + pSelect.value = proj; + } + updateProjectPills(); + updateQueueCounts(); + applyFilters(); +} + +// Project selectie via dropdown +function onProjectSelectChange(proj) { + currentProject = proj; + updateProjectPills(); + updateQueueCounts(); + applyFilters(); +} + +function updateProjectPills() { + const pills = document.querySelectorAll('.project-pill'); + pills.forEach(p => { + const pKey = p.getAttribute('data-project') || ''; + if (pKey === currentProject) { + 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"; + } 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"; + } + }); +} + +// Wachtrijfilter selectie +function selectQueueFilter(code) { + currentQueue = code; + updateQueuePills(); + applyFilters(); +} + +function updateQueuePills() { + const pills = document.querySelectorAll('.queue-pill'); + pills.forEach(p => { + const qCode = p.getAttribute('data-queue') || ''; + if (qCode === currentQueue) { + 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"; + } + }); +} + +// Update de tellers op de DX / SD wachtrijfilter knoppen +function updateQueueCounts() { + 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; + + if (currentProject) { + currentList = currentList.filter(iss => iss.project === currentProject); + } + + const dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; + let dxCount = 0; + let sdCount = 0; + + currentList.forEach(iss => { + const ol = (iss.onderwerp_logging || '').toLowerCase().trim(); + if (dxValues.includes(ol)) { + dxCount++; + } else { + sdCount++; + } + }); + + 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; +} + // Snelkoppeling KPI-kaart Urgent function filterByUrgent() { const pSelect = document.getElementById('filterPriority'); @@ -653,6 +844,8 @@ function clearSearch() { // Alle filters herstellen function resetAllFilters() { + currentProject = ''; + currentQueue = ''; const searchInput = document.getElementById('jiraSearchInput'); const projectSelect = document.getElementById('filterProject'); const typeSelect = document.getElementById('filterType'); @@ -665,14 +858,17 @@ function resetAllFilters() { if (prioritySelect) prioritySelect.value = ''; if (statusSelect) statusSelect.value = ''; - document.getElementById('clearSearchBtn').classList.add('hidden'); + document.getElementById('clearSearchBtn')?.classList.add('hidden'); + updateProjectPills(); + updateQueuePills(); + updateQueueCounts(); applyFilters(); } // Filter en render logic function applyFilters() { const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase(); - const projectVal = document.getElementById('filterProject')?.value || ''; + const projectVal = currentProject || (document.getElementById('filterProject')?.value || ''); const typeVal = document.getElementById('filterType')?.value || ''; const priorityVal = document.getElementById('filterPriority')?.value || ''; const statusVal = document.getElementById('filterStatus')?.value || ''; @@ -706,19 +902,42 @@ function applyFilters() { }); } - // Pas zoek- en selectiefilters toe + const dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; + + // Pas zoek-, project-, wachtrij-, type-, prioriteit- en statusfilters 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); - if (!keyMatch && !sumMatch && !prjMatch) return false; + const descMatch = (iss.description || '').toLowerCase().includes(searchVal); + const repMatch = (iss.reporter_name || '').toLowerCase().includes(searchVal); + const olMatch = (iss.onderwerp_logging || '').toLowerCase().includes(searchVal); + if (!keyMatch && !sumMatch && !prjMatch && !descMatch && !repMatch && !olMatch) return false; } if (projectVal && iss.project !== projectVal) { return false; } + if (currentQueue) { + const qCode = currentQueue.toUpperCase(); + const ol = (iss.onderwerp_logging || '').toLowerCase().trim(); + + if (qCode === 'DX') { + if (!dxValues.includes(ol)) return false; + } else if (qCode === 'SD') { + if (ol !== '' && ol !== 'empty' && dxValues.includes(ol)) return false; + } else { + const fObj = (jiraData.queue_filters || []).find(f => f.filter_code.toUpperCase() === qCode); + if (fObj) { + if (ol && !ol.includes(qCode.toLowerCase()) && !fObj.jql_clause.toLowerCase().includes(ol)) { + return false; + } + } + } + } + if (typeVal && iss.issue_type !== typeVal) { return false; } @@ -741,6 +960,36 @@ function applyFilters() { renderTickets(filtered); } +// Uitklappen en inklappen van ticket details +function toggleTicketDetails(key, event) { + if (event) { + event.stopPropagation(); + } + const details = document.getElementById(`details_${key}`); + const chevron = document.getElementById(`chevron_${key}`); + if (!details) return; + + if (details.classList.contains('hidden')) { + details.classList.remove('hidden'); + if (chevron) chevron.classList.add('rotate-180'); + expandedKeys.add(key); + } else { + details.classList.add('hidden'); + if (chevron) chevron.classList.remove('rotate-180'); + expandedKeys.delete(key); + } + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + +function copyTicketKey(key) { + navigator.clipboard.writeText(key).then(() => { + showToast({ + message: `Ticket ${key} gekopieerd!`, + subMessage: 'De ticketsleutel is direct naar je klembord gekopieerd.' + }); + }); +} + // Renders the list of tickets function renderTickets(tickets) { const container = document.getElementById('ticketsContainer'); @@ -842,11 +1091,38 @@ function renderTickets(tickets) { `; } + // Onderwerp logging badge + let onderwerpBadge = 'Niet ingevuld'; + if (ticket.onderwerp_logging) { + const olLower = ticket.onderwerp_logging.toLowerCase(); + const dxList = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; + const isDx = dxList.includes(olLower); + if (isDx) { + onderwerpBadge = ` + + + DX: ${escapeHtml(ticket.onderwerp_logging)} + + `; + } else { + onderwerpBadge = ` + + + SD: ${escapeHtml(ticket.onderwerp_logging)} + + `; + } + } + + const isExpanded = expandedKeys.has(ticket.key); + const detailsClass = isExpanded ? '' : 'hidden'; + const chevronRotateClass = isExpanded ? 'rotate-180' : ''; + // JSON stringified ticket data voor modalaanroep const ticketSafeJson = JSON.stringify(ticket).replace(/"/g, '"'); html += ` -
+
@@ -888,23 +1164,29 @@ function renderTickets(tickets) { ${dueDateHtml}
- -

- + +

+ ${escapeHtml(ticket.summary || '(Geen titel)')} - +

- +
${assigneeHtml} ${ticket.updated ? `Gewijzigd: ${escapeHtml(ticket.updated)}` : ''} + ${ticket.onderwerp_logging ? onderwerpBadge : ''}

- +
+ +
+ + +
+ + +
+
+ + Volledige Ticket Omschrijving +
+
+ ${escapeHtml(ticket.description || 'Geen uitgebreide omschrijving beschikbaar in Jira.')} +
+
+ + +
+
+ Aangemeld door (Reporter): +
+
+ ${escapeHtml((ticket.reporter_name || 'J').charAt(0).toUpperCase())} +
+ ${escapeHtml(ticket.reporter_name || 'Onbekend')} +
+
+ +
+ Aangemaakt op: + + ${ticket.created ? escapeHtml(ticket.created) : 'Niet opgegeven'} + +
+ +
+ Laatst gewijzigd: + + ${ticket.updated ? escapeHtml(ticket.updated) : 'Recent'} + +
+ +
+ Onderwerp logging: + ${onderwerpBadge} +
+
+ + +
+
+ + + + Open in Jira Cloud + +
+
+ + +
+
+ +
+
`; }); @@ -1191,6 +1546,11 @@ document.addEventListener('keydown', function(e) { // Initiële opstart document.addEventListener('DOMContentLoaded', () => { calculateKpis(); + updateProjectPills(); + updateQueueCounts(); + if (currentQueue) { + updateQueuePills(); + } applyFilters(); });