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 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 */ 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) ]; // 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST $url = $cleanDomain . '/rest/api/3/search/jql'; $payload = [ 'jql' => $jql, 'maxResults' => min($maxResults, 100), 'fields' => $fieldsList, ]; $response = fociz_http_request($url, [ 'method' => 'POST', 'headers' => [ 'Authorization' => $authHeader, 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'body' => $payload, 'timeout' => 12, ]); // 2. Fallback: GET /rest/api/3/search/jql met query parameters indien de instantie GET vereist if ($response['status'] === 405 || $response['status'] === 400) { $getParams = [ 'jql' => $jql, 'maxResults' => min($maxResults, 100), 'fields' => implode(',', $fieldsList), ]; $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, ]); } // 3. Fallback naar API v2 voor on-premise Jira Server / Data Center installaties (waar /rest/api/2/search actief is) if ($response['status'] === 404) { $getParams = [ 'jql' => $jql, 'maxResults' => min($maxResults, 100), '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['status'] !== 200 || !isset($response['json']['issues'])) { $errorMsg = $response['json']['errorMessages'][0] ?? ($response['json']['message'] ?? ($response['error'] ?? 'Kan Jira tickets niet ophalen')); return [ 'success' => false, 'error' => $errorMsg, 'issues' => [], ]; } $rawIssues = $response['json']['issues']; $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 = 50): 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, int $maxResults = 50): array { $jql = 'assignee != currentUser() AND 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 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' => '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' => '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', 'description' => "Synchronisatie van dagplanning blokken naar Microsoft 365 Exchange Online agenda met real-time conflictcontrole.", 'status' => 'In Review', 'status_category' => 'indeterminate', 'priority' => 'Medium', 'issue_type' => 'Task', 'project' => 'Integrations Hub', 'project_key' => 'INT', 'assignee_name' => 'Mijzelf', 'assignee_avatar' => '', 'reporter_name' => 'Architect', 'reporter_avatar' => '', 'onderwerp_logging' => 'SSO-koppeling', 'due_date' => date('Y-m-d'), 'created' => date('Y-m-d H:i:s', strtotime('-6 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-4 hours')), 'url' => 'https://jira.atlassian.net/browse/FOC-103', 'source' => 'SANDBOX', ], [ 'key' => 'FOC-108', 'summary' => 'CSS scrollbar glitch oplossen in Kanban lane op Firefox', 'description' => "Op Windows Firefox versies 128+ verspringt de horizontale scrollbalk bij het slepen van kaarten tussen kolommen.", 'status' => 'In Progress', 'status_category' => 'indeterminate', 'priority' => 'Highest', 'issue_type' => 'Bug', 'project' => 'UI/UX Redesign', 'project_key' => 'UI', 'assignee_name' => 'Mijzelf', 'assignee_avatar' => '', 'reporter_name' => 'QA Lead', 'reporter_avatar' => '', 'onderwerp_logging' => '', 'due_date' => date('Y-m-d', strtotime('+1 day')), 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-30 minutes')), 'url' => 'https://jira.atlassian.net/browse/FOC-108', 'source' => 'SANDBOX', ], [ 'key' => 'FOC-112', 'summary' => 'Avatar upload beveiliging en compressie optimalisatie', 'description' => "Server-side image re-encoding en EXIF stripping voor geüploade gebruikersavatars.", 'status' => 'Done', 'status_category' => 'done', 'priority' => 'Medium', 'issue_type' => 'Task', 'project' => 'Security', 'project_key' => 'SEC', 'assignee_name' => 'Mijzelf', 'assignee_avatar' => '', 'reporter_name' => 'SecOps', 'reporter_avatar' => '', 'onderwerp_logging' => '', 'due_date' => date('Y-m-d', strtotime('-1 day')), 'created' => date('Y-m-d H:i:s', strtotime('-7 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-5 hours')), 'url' => 'https://jira.atlassian.net/browse/FOC-112', '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.\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' => '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' => 'SD-416', 'summary' => 'Migratie van oude klantportaal database naar MariaDB 11 cluster', 'description' => "Voorbereiden en uitvoeren van de dataschemamigratie van het oude portaal naar de nieuwe MariaDB 11 clusteromgeving inclusief regressietests.", 'status' => 'To Do', 'status_category' => 'new', 'priority' => 'High', 'issue_type' => 'Story', 'project' => 'Servicedesk', 'project_key' => 'SD', 'assignee_name' => 'Bram Meijer', 'assignee_avatar' => '', 'reporter_name' => 'Lead Engineer', 'reporter_avatar' => '', 'onderwerp_logging' => 'Migratie', 'due_date' => date('Y-m-d', strtotime('+8 days')), 'created' => date('Y-m-d H:i:s', strtotime('-4 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')), 'url' => 'https://jira.atlassian.net/browse/SD-416', 'source' => 'SANDBOX', ], [ 'key' => 'SD-420', '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' => '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' => '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-95', 'summary' => 'Sessie timeout notificatie verschijnt niet tijdig bij inactieve tab', 'description' => "Wanneer een gebruiker de browser 2 uur niet aanraakt, vervalt de PHP sessie zonder dat het login-dialoogvenster netjes openspringt.", 'status' => 'In Progress', 'status_category' => 'indeterminate', 'priority' => 'Highest', 'issue_type' => 'Bug', 'project' => 'Fociz Platform', 'project_key' => 'FOC', 'assignee_name' => 'Lisa Bakker', 'assignee_avatar' => '', 'reporter_name' => 'Product QA', 'reporter_avatar' => '', 'onderwerp_logging' => '', 'due_date' => date('Y-m-d', strtotime('+1 day')), 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')), 'url' => 'https://jira.atlassian.net/browse/FOC-95', 'source' => 'SANDBOX', ], [ 'key' => 'FOC-98', 'summary' => 'Automatische e-mail triggers voor taakdeadlines en uitnodigingen', 'description' => "Cronjob instellen die 24 uur voor de deadline van een taak een herinneringsmail stuurt via SMTP.", 'status' => 'To Do', 'status_category' => 'new', 'priority' => 'High', 'issue_type' => 'Story', 'project' => 'Notifications', 'project_key' => 'NOTIF', 'assignee_name' => 'Daan van Dijk', 'assignee_avatar' => '', 'reporter_name' => 'Operations', 'reporter_avatar' => '', 'onderwerp_logging' => '', '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, 'queue_filters' => $queueFilters, 'default_queue_filter' => $userQueuePref, ]; } $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', ''); $apiToken = $settings['jira_api_token'] ?? fociz_env('JIRA_API_TOKEN', ''); $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 $assignedRes = jira_fetch_assigned_issues($domain, $email, $apiToken, 50); // Ophalen van teamtaken $teamRes = jira_fetch_team_issues($domain, $email, $apiToken, 50); 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'] : []; $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, ]; 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]); }