query(" SELECT i.settings FROM integrations i JOIN users u ON i.user_id = u.id WHERE u.role = 'ADMIN' AND i.provider = 'JIRA' AND i.settings IS NOT NULL ORDER BY i.updated_at DESC LIMIT 1 "); $row = $stmt->fetch(); if (!empty($row['settings'])) { $settings = json_decode($row['settings'], true) ?: []; if (!empty($settings['jira_domain'])) { return jira_normalize_domain($settings['jira_domain']); } } // Fallback naar een willekeurige JIRA rij $fallback = $db->query(" SELECT settings FROM integrations WHERE provider = 'JIRA' AND settings LIKE '%jira_domain%' LIMIT 1 ")->fetch(); if (!empty($fallback['settings'])) { $settings = json_decode($fallback['settings'], true) ?: []; if (!empty($settings['jira_domain'])) { return jira_normalize_domain($settings['jira_domain']); } } } catch (Throwable $e) { error_log("jira_get_company_domain error: " . $e->getMessage()); } return ''; } /** * Genereert de Basic Authentication header voor Atlassian REST API */ function jira_get_auth_header(string $email, string $apiToken): string { return 'Basic ' . base64_encode(trim($email) . ':' . trim($apiToken)); } /** * Test de verbinding met Atlassian Jira Cloud via /rest/api/3/myself */ function jira_test_connection(string $domain, string $email, string $apiToken): array { $cleanDomain = jira_normalize_domain($domain); if (empty($cleanDomain) || empty($email) || empty($apiToken)) { return [ 'success' => false, 'error' => 'Vul een Jira domein, e-mailadres en Atlassian API token in.', ]; } $authHeader = jira_get_auth_header($email, $apiToken); $url = $cleanDomain . '/rest/api/3/myself'; $response = fociz_http_request($url, [ 'method' => 'GET', 'headers' => [ 'Authorization' => $authHeader, 'Accept' => 'application/json', ], 'timeout' => 10, ]); // Fallback naar api v2 indien v3 niet beschikbaar is if ($response['status'] === 404) { $urlV2 = $cleanDomain . '/rest/api/2/myself'; $response = fociz_http_request($urlV2, [ 'method' => 'GET', 'headers' => [ 'Authorization' => $authHeader, 'Accept' => 'application/json', ], 'timeout' => 10, ]); } if ($response['status'] === 200 && is_array($response['json'])) { $user = $response['json']; return [ 'success' => true, 'user' => [ 'displayName' => $user['displayName'] ?? 'Atlassian Gebruiker', 'emailAddress' => $user['emailAddress'] ?? $email, 'accountId' => $user['accountId'] ?? ($user['key'] ?? ''), 'active' => $user['active'] ?? true, 'timeZone' => $user['timeZone'] ?? 'Europe/Amsterdam', ], ]; } $errMsg = 'Verbinding mislukt met Jira.'; if ($response['status'] === 401) { $errMsg = 'Authenticatie geweigerd (HTTP 401). Controleer uw e-mailadres en zorg dat u een geldig Atlassian API token gebruikt.'; } elseif ($response['status'] === 403) { $errMsg = 'Toegang geweigerd (HTTP 403). Uw account heeft onvoldoende rechten op dit Jira domein.'; } elseif ($response['status'] === 404) { $errMsg = 'Jira Cloud domein niet gevonden (HTTP 404). Controleer het ingevoerde domein (bijv. uwbedrijf.atlassian.net).'; } elseif (!empty($response['error'])) { $errMsg = "Netwerkfout: {$response['error']}"; } return [ 'success' => false, 'error' => $errMsg, 'status' => $response['status'], ]; } /** * 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 alle gekoppelde wachtrijfilter codes voor een gebruiker op (meervoudig) */ function jira_get_user_queue_preferences(int $userId): array { try { $db = get_db_connection(); $stmt = $db->prepare("SELECT default_jira_filter_codes, default_jira_filter_code FROM users WHERE id = ?"); $stmt->execute([$userId]); $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 []; } } /** * 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 project-specifieke statussen op uit Jira Cloud voor een specifiek project */ function jira_fetch_project_statuses(string $domain, string $email, string $apiToken, string $projectKey): array { $cleanDomain = jira_normalize_domain($domain); $pKey = strtoupper(trim($projectKey)); if (empty($cleanDomain) || empty($email) || empty($apiToken) || empty($pKey)) { return []; } $cacheKey = "jira_proj_statuses_" . $pKey . "_" . md5($cleanDomain); $cached = fociz_cache_get($cacheKey); if (is_array($cached) && !empty($cached)) { return $cached; } $authHeader = jira_get_auth_header($email, $apiToken); $url = $cleanDomain . '/rest/api/3/project/' . rawurlencode($pKey) . '/statuses'; $response = fociz_http_request($url, [ 'method' => 'GET', 'headers' => [ 'Authorization' => $authHeader, 'Accept' => 'application/json', ], 'timeout' => 8, ]); if ($response['status'] === 404 || $response['status'] === 405) { $urlV2 = $cleanDomain . '/rest/api/2/project/' . rawurlencode($pKey) . '/statuses'; $response = fociz_http_request($urlV2, [ 'method' => 'GET', 'headers' => [ 'Authorization' => $authHeader, 'Accept' => 'application/json', ], 'timeout' => 8, ]); } $statuses = []; if ($response['status'] === 200 && is_array($response['json'])) { foreach ($response['json'] as $issueType) { if (!empty($issueType['statuses']) && is_array($issueType['statuses'])) { foreach ($issueType['statuses'] as $st) { $sName = trim($st['name'] ?? ''); if (empty($sName)) continue; $sKey = strtolower($sName); $catKey = strtolower($st['statusCategory']['key'] ?? 'indeterminate'); $catName = $st['statusCategory']['name'] ?? 'In uitvoering'; $statuses[$sKey] = [ 'id' => (string)($st['id'] ?? ''), 'name' => $sName, 'description' => $st['description'] ?? '', 'status_category' => $catKey, 'category_name' => $catName, 'color' => $st['statusCategory']['colorName'] ?? '', ]; } } } $statuses = array_values($statuses); usort($statuses, function($a, $b) { $order = ['new' => 1, 'indeterminate' => 2, 'done' => 3]; $oa = $order[$a['status_category']] ?? 2; $ob = $order[$b['status_category']] ?? 2; if ($oa !== $ob) return $oa - $ob; return strcasecmp($a['name'], $b['name']); }); fociz_cache_set($cacheKey, $statuses, 3600); } return $statuses; } /** * Haalt issues op via een flexibele JQL query via Atlassian REST API */ function jira_fetch_issues(string $domain, string $email, string $apiToken, string $jql, int $maxResults = 50): array { $cleanDomain = jira_normalize_domain($domain); $authHeader = jira_get_auth_header($email, $apiToken); $fieldsList = [ '*navigable', 'summary', 'status', 'priority', 'issuetype', 'parent', 'project', 'assignee', 'reporter', 'duedate', 'updated', 'created', 'description', 'labels', 'components', 'story_points', 'customfield_10016', // Story Points (Jira Cloud standaard) 'customfield_10028', // Story Points (alternatief veld sommige instanties) ]; $allRawIssues = []; $nextPageToken = null; $startAt = 0; $pageSize = 100; $maxPages = (int)ceil($maxResults / $pageSize); $useMethod = 'v3_post'; // 'v3_post', 'v3_get', 'v2_get' for ($page = 0; $page < $maxPages; $page++) { $needed = $maxResults - count($allRawIssues); if ($needed <= 0) break; $currentPageSize = min($pageSize, $needed); $response = null; // 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST if ($useMethod === 'v3_post') { $url = $cleanDomain . '/rest/api/3/search/jql'; $payload = [ 'jql' => $jql, 'maxResults' => $currentPageSize, 'fields' => $fieldsList, ]; if (!empty($nextPageToken)) { $payload['nextPageToken'] = $nextPageToken; } $response = fociz_http_request($url, [ 'method' => 'POST', 'headers' => [ 'Authorization' => $authHeader, 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'body' => $payload, 'timeout' => 12, ]); if ($response['status'] === 405 || $response['status'] === 400) { $useMethod = 'v3_get'; } elseif ($response['status'] === 404) { $useMethod = 'v2_get'; } } // 2. Fallback: GET /rest/api/3/search/jql met query parameters indien de instantie GET vereist if ($useMethod === 'v3_get') { $getParams = [ 'jql' => $jql, 'maxResults' => $currentPageSize, 'fields' => implode(',', $fieldsList), ]; if (!empty($nextPageToken)) { $getParams['nextPageToken'] = $nextPageToken; } elseif ($startAt > 0) { $getParams['startAt'] = $startAt; } $urlGet = $cleanDomain . '/rest/api/3/search/jql?' . http_build_query($getParams); $response = fociz_http_request($urlGet, [ 'method' => 'GET', 'headers' => [ 'Authorization' => $authHeader, 'Accept' => 'application/json', ], 'timeout' => 12, ]); if ($response['status'] === 404) { $useMethod = 'v2_get'; } } // 3. Fallback naar API v2 voor on-premise Jira Server / Data Center installaties if ($useMethod === 'v2_get') { $getParams = [ 'jql' => $jql, 'maxResults' => $currentPageSize, 'startAt' => $startAt, 'fields' => implode(',', $fieldsList), ]; $urlV2 = $cleanDomain . '/rest/api/2/search?' . http_build_query($getParams); $response = fociz_http_request($urlV2, [ 'method' => 'GET', 'headers' => [ 'Authorization' => $authHeader, 'Accept' => 'application/json', ], 'timeout' => 12, ]); } if (!$response || $response['status'] !== 200 || !isset($response['json']['issues']) || !is_array($response['json']['issues'])) { if ($page === 0) { $errorMsg = $response['json']['errorMessages'][0] ?? ($response['json']['message'] ?? ($response['error'] ?? 'Kan Jira tickets niet ophalen')); return [ 'success' => false, 'error' => $errorMsg, 'issues' => [], ]; } break; } $pageIssues = $response['json']['issues']; if (empty($pageIssues)) { break; } foreach ($pageIssues as $pi) { $allRawIssues[] = $pi; } $nextPageToken = $response['json']['nextPageToken'] ?? null; $totalInApi = (int)($response['json']['total'] ?? 0); $startAt += count($pageIssues); // Geen volgende pagina meer if (empty($nextPageToken)) { if ($totalInApi > 0 && $startAt >= $totalInApi) { break; } if ($useMethod === 'v3_post') { break; } if (count($pageIssues) < $currentPageSize) { break; } } } $rawIssues = $allRawIssues; $issues = []; foreach ($rawIssues as $item) { $fields = $item['fields'] ?? []; $key = $item['key'] ?? ''; $summary = $fields['summary'] ?? '(Geen titel)'; $statusName = $fields['status']['name'] ?? 'To Do'; $statusCategory = $fields['status']['statusCategory']['key'] ?? 'indeterminate'; $priorityName = $fields['priority']['name'] ?? 'Medium'; $issueTypeName = $fields['issuetype']['name'] ?? 'Taak'; $issueTypeIcon = $fields['issuetype']['iconUrl'] ?? ''; $projectName = $fields['project']['name'] ?? 'Project'; $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" of routeringsveld $onderwerpLogging = ''; $knownTopics = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie', 'incident', 'vraag', 'algemeen']; foreach ($fields as $fKey => $fVal) { if (str_starts_with($fKey, 'customfield_')) { if (is_array($fVal)) { if (isset($fVal['value']) && is_string($fVal['value'])) { $valStr = trim($fVal['value']); if (!empty($valStr)) { foreach ($knownTopics as $kt) { if (str_contains(strtolower($valStr), $kt)) { $onderwerpLogging = $valStr; break 2; } } if (empty($onderwerpLogging) && (stripos($fKey, 'onderwerp') !== false || stripos($fKey, 'logging') !== false)) { $onderwerpLogging = $valStr; } } } } elseif (is_string($fVal) && !empty($fVal)) { $valStr = trim($fVal); foreach ($knownTopics as $kt) { if (str_contains(strtolower($valStr), $kt)) { $onderwerpLogging = $valStr; break 2; } } } } } if (empty($onderwerpLogging) && !empty($fields['onderwerp_logging'])) { $onderwerpLogging = is_string($fields['onderwerp_logging']) ? $fields['onderwerp_logging'] : ($fields['onderwerp_logging']['value'] ?? ''); } // Fallback: kijk naar labels & componenten if (empty($onderwerpLogging) && !empty($fields['labels']) && is_array($fields['labels'])) { foreach ($fields['labels'] as $lbl) { $lblLower = strtolower(trim($lbl)); foreach ($knownTopics as $kt) { if (str_contains($lblLower, $kt)) { $onderwerpLogging = ucfirst($kt); break 2; } } } } // Fallback: detecteer onderwerp op basis van steekwoorden in summary if (empty($onderwerpLogging)) { $sumLower = strtolower($summary); if (str_contains($sumLower, 'sso')) $onderwerpLogging = 'SSO-koppeling'; elseif (str_contains($sumLower, 'endpoint') || str_contains($sumLower, 'rest') || str_contains($sumLower, 'api')) $onderwerpLogging = 'Rest-endpoints'; elseif (str_contains($sumLower, 'uwv')) $onderwerpLogging = 'UWV-koppeling'; elseif (str_contains($sumLower, 'datadump') || str_contains($sumLower, 'dump')) $onderwerpLogging = 'Datadump'; elseif (str_contains($sumLower, 'migratie')) $onderwerpLogging = 'Migratie'; elseif (str_contains($sumLower, 'koppeling')) $onderwerpLogging = 'Koppelingen'; } // Detectie van story points (customfield_10016 is Jira Cloud standaard, met fallback naar alternatieven) $storyPoints = null; if (isset($fields['customfield_10016']) && is_numeric($fields['customfield_10016'])) { $storyPoints = (float)$fields['customfield_10016']; } elseif (isset($fields['customfield_10028']) && is_numeric($fields['customfield_10028'])) { $storyPoints = (float)$fields['customfield_10028']; } elseif (isset($fields['story_points']) && is_numeric($fields['story_points'])) { $storyPoints = (float)$fields['story_points']; } $issues[] = [ '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}", 'story_points' => $storyPoints, 'source' => 'JIRA_LIVE', ]; } return [ 'success' => true, 'issues' => $issues, 'total' => count($issues), ]; } /** * 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 = 250): array { $jql = 'assignee = currentUser() ORDER BY updated DESC'; return jira_fetch_issues($domain, $email, $apiToken, $jql, $maxResults); } /** * Haalt openstaande team issues op die horen bij de projecten van de gebruiker */ function jira_fetch_team_issues(string $domain, string $email, string $apiToken, $projectKeys = [], int $maxResults = 500): array { if (is_int($projectKeys)) { $maxResults = $projectKeys; $projectKeys = []; } $cleanKeys = array_values(array_filter(array_map('strtoupper', array_map('trim', (array)$projectKeys)))); $projClause = ''; if (!empty($cleanKeys)) { $escaped = array_map(function($k) { return '"' . addslashes($k) . '"'; }, $cleanKeys); $projClause = 'project in (' . implode(', ', $escaped) . ') AND '; } $jql = $projClause . 'statusCategory != Done ORDER BY updated DESC'; return jira_fetch_issues($domain, $email, $apiToken, $jql, $maxResults); } /** * Haalt de complete dashboard data op (persoonlijk + team) inclusief statistieken */ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array { $db = get_db_connection(); $stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA' AND is_enabled = 1"); $stmt->execute([$userId]); $integration = $stmt->fetch(); // 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.", 'status' => 'In behandeling', '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', '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.", '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', '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' => "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', 'project_key' => 'SD', 'assignee_name' => 'Mijzelf', 'assignee_avatar' => '', 'reporter_name' => 'HR Servicedesk', 'reporter_avatar' => '', 'onderwerp_logging' => 'EMPTY', 'due_date' => date('Y-m-d', strtotime('-1 day')), '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', ], ]; $team = [ [ '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.", 'status' => 'In behandeling', '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', 'story_points' => 5, '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', 'story_points' => 3, 'source' => 'SANDBOX', ], [ '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.", 'status' => 'In behandeling', 'status_category' => 'indeterminate', 'priority' => 'Medium', 'issue_type' => 'Task', 'project' => 'Interfaces', 'project_key' => 'IN', '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/IN-210', 'story_points' => 5, 'source' => 'SANDBOX', ], [ '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' => 'Low', 'issue_type' => 'Task', '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' => 'Marketing Director', 'reporter_avatar' => '', 'onderwerp_logging' => 'Licenties', '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('-1 day')), 'url' => 'https://jira.atlassian.net/browse/AAN-110', 'story_points' => 2, 'source' => 'SANDBOX', ], [ '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' => 'Task', 'project' => 'Aanvragen', 'project_key' => 'AAN', 'assignee_name' => 'Bram Meijer', 'assignee_avatar' => '', 'reporter_name' => 'Security Officer', 'reporter_avatar' => '', '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('-5 hours')), 'url' => 'https://jira.atlassian.net/browse/GEN-315', 'story_points' => 8, 'source' => 'SANDBOX', ], ]; $demoProjectStatuses = [ 'SD' => [ ['id' => '1', 'name' => 'To Do', 'status_category' => 'new', 'category_name' => 'Nieuw'], ['id' => '2', 'name' => 'In behandeling', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], ['id' => '3', 'name' => 'Wachten op klant', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], ['id' => '4', 'name' => 'Gesloten', 'status_category' => 'done', 'category_name' => 'Gereed'], ], 'IN' => [ ['id' => '10', 'name' => 'Nieuw', 'status_category' => 'new', 'category_name' => 'Nieuw'], ['id' => '11', 'name' => 'In behandeling', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], ['id' => '12', 'name' => 'In Review', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], ['id' => '13', 'name' => 'Gereed', 'status_category' => 'done', 'category_name' => 'Gereed'], ['id' => '14', 'name' => 'Gesloten', 'status_category' => 'done', 'category_name' => 'Gereed'], ], 'AAN' => [ ['id' => '20', 'name' => 'Nieuw', 'status_category' => 'new', 'category_name' => 'Nieuw'], ['id' => '21', 'name' => 'In behandeling', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], ['id' => '22', 'name' => 'Goedgekeurd', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], ['id' => '23', 'name' => 'Afgewezen', 'status_category' => 'done', 'category_name' => 'Gereed'], ['id' => '24', 'name' => 'Gesloten', 'status_category' => 'done', 'category_name' => 'Gereed'], ], 'GEN' => [ ['id' => '30', 'name' => 'To Do', 'status_category' => 'new', 'category_name' => 'Nieuw'], ['id' => '31', 'name' => 'In Progress', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'], ['id' => '32', 'name' => 'Done', 'status_category' => 'done', 'category_name' => 'Gereed'], ], ]; return [ 'is_demo' => true, 'success' => true, '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, 'user_queue_filters' => $userQueuePrefs, 'available_statuses' => $demoStatuses, 'project_statuses' => $demoProjectStatuses, ]; } $settings = json_decode($integration['settings'] ?? '{}', true) ?: []; $domain = !empty($settings['jira_domain']) ? $settings['jira_domain'] : jira_get_company_domain(); $email = $settings['jira_email'] ?? fociz_env('JIRA_EMAIL', ''); $apiTokenRaw = $settings['jira_api_token'] ?? fociz_env('JIRA_API_TOKEN', ''); $apiToken = fociz_decrypt($apiTokenRaw); $accName = $settings['account_name'] ?? 'Atlassian Gebruiker'; if (empty($domain) || empty($email) || empty($apiToken)) { return [ 'is_demo' => false, 'success' => false, 'error' => 'Jira configuratie is onvolledig. Controleer je instellingen.', 'assigned_issues' => [], 'team_issues' => [], ]; } // Cache controle (60 seconden) $cacheKey = "jira_dashboard_user_{$userId}"; if (!$forceRefresh) { $cachedData = fociz_cache_get($cacheKey); if ($cachedData !== null && is_array($cachedData)) { return array_merge($cachedData, ['from_cache' => true]); } if (session_status() === PHP_SESSION_ACTIVE && !empty($_SESSION[$cacheKey]['data'])) { if (($_SESSION[$cacheKey]['timestamp'] ?? 0) > (time() - 60)) { return array_merge($_SESSION[$cacheKey]['data'], ['from_cache' => true]); } } } // Ophalen van toegewezen taken (max 250) $assignedRes = jira_fetch_assigned_issues($domain, $email, $apiToken, 250); // Bepaal de relevante projecten van de gebruiker om gericht en compleet op te halen $targetProjKeys = []; if (!empty($userQueuePrefs)) { foreach ($userQueuePrefs as $code) { $c = strtoupper(trim($code)); if ($c === 'DX') { $targetProjKeys['SD'] = true; } elseif (!in_array($c, ['MY_QUEUES'], true)) { $foundProj = false; foreach ($queueFilters as $qf) { if (strtoupper(trim($qf['filter_code'] ?? '')) === $c && !empty($qf['project_key'])) { $targetProjKeys[strtoupper(trim($qf['project_key']))] = true; $foundProj = true; break; } } if (!$foundProj && strlen($c) <= 10) { $targetProjKeys[$c] = true; } } } } if (empty($targetProjKeys)) { $targetProjKeys = array_fill_keys(['SD', 'IN', 'AAN', 'GEN'], true); } // Ophalen van teamtaken binnen de relevante projecten (met paginatie tot max 500) $teamRes = jira_fetch_team_issues($domain, $email, $apiToken, array_keys($targetProjKeys), 500); // Ophalen van Jira statussen $availableStatuses = jira_fetch_statuses($domain, $email, $apiToken); if (!$assignedRes['success'] && !$teamRes['success']) { return [ 'is_demo' => false, 'success' => false, 'error' => $assignedRes['error'] ?? 'Kan Jira tickets niet ophalen.', 'assigned_issues' => [], 'team_issues' => [], ]; } $assignedIssues = $assignedRes['success'] ? $assignedRes['issues'] : []; $teamIssues = $teamRes['success'] ? $teamRes['issues'] : []; // Project-specifieke statussen ophalen voor gebruikte projecten $activeProjKeys = ['SD', 'IN', 'AAN', 'GEN']; $allLoaded = array_merge($assignedIssues, $teamIssues); foreach ($allLoaded as $iss) { $pk = strtoupper(trim($iss['project_key'] ?? '')); if (!empty($pk) && !in_array($pk, $activeProjKeys, true)) { $activeProjKeys[] = $pk; } } $projectStatusesMap = []; foreach ($activeProjKeys as $pk) { $pStats = jira_fetch_project_statuses($domain, $email, $apiToken, $pk); if (!empty($pStats)) { $projectStatusesMap[$pk] = $pStats; } } $data = [ 'is_demo' => false, 'success' => true, '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, 'user_queue_filters' => $userQueuePrefs, 'available_statuses' => $availableStatuses, 'project_statuses' => $projectStatusesMap, ]; fociz_cache_set($cacheKey, $data, 60); if (session_status() === PHP_SESSION_ACTIVE) { $_SESSION[$cacheKey] = [ 'timestamp' => time(), 'data' => $data, ]; } // Werk last_sync_at bij in database $stmtUpdate = $db->prepare("UPDATE integrations SET last_sync_at = NOW() WHERE id = ?"); $stmtUpdate->execute([$integration['id']]); return $data; } /** * Haalt de Jira tickets op voor een Fociz gebruiker (backwards compatibility) */ function jira_get_user_issues(int $userId, bool $forceRefresh = false): array { $data = jira_get_dashboard_data($userId, $forceRefresh); return [ 'is_demo' => $data['is_demo'] ?? false, 'success' => $data['success'] ?? false, 'error' => $data['error'] ?? null, 'issues' => $data['assigned_issues'] ?? [], 'from_cache' => $data['from_cache'] ?? false, ]; } /** * Ontkoppelt Jira integratie voor een gebruiker */ function jira_disconnect(int $userId): bool { $db = get_db_connection(); $stmt = $db->prepare("DELETE FROM integrations WHERE user_id = ? AND provider = 'JIRA'"); if (session_status() === PHP_SESSION_NONE) { @session_start(); } unset($_SESSION["jira_issues_user_{$userId}"]); unset($_SESSION["jira_dashboard_user_{$userId}"]); fociz_cache_delete("jira_dashboard_user_{$userId}"); return $stmt->execute([$userId]); }