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 */ /** * 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 = [ 'summary', 'status', 'priority', 'issuetype', 'parent', 'project', 'assignee', 'reporter', 'duedate', 'updated' ]; // 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; $assigneeName = $fields['assignee']['displayName'] ?? 'Niet toegewezen'; $assigneeAvatar = $fields['assignee']['avatarUrls']['48x48'] ?? ($fields['assignee']['avatarUrls']['32x32'] ?? ''); $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', ]; } 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(); // 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' => 'FOC-102', 'summary' => 'Interactief whiteboard canvas voor brainstormsessies', 'status' => 'To Do', 'status_category' => 'new', 'priority' => 'High', 'issue_type' => 'Feature', 'project' => 'Fociz Platform', 'project_key' => 'FOC', 'assignee_name' => 'Mijzelf', 'assignee_avatar' => '', 'due_date' => date('Y-m-d', strtotime('+4 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', 'issue_type' => 'Task', 'project' => 'Integrations Hub', 'project_key' => 'INT', 'assignee_name' => 'Mijzelf', 'assignee_avatar' => '', 'due_date' => date('Y-m-d'), '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', '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', ], ]; $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' => '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' => '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' => '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' => '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' => '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' => '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-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', ], ]; 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, ]; } $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' => [], ]; } // Sessiecache voor 60 seconden if (session_status() === PHP_SESSION_NONE) { @session_start(); } $cacheKey = "jira_dashboard_user_{$userId}"; $cached = $_SESSION[$cacheKey] ?? null; if (!$forceRefresh && $cached && is_array($cached) && ($cached['timestamp'] ?? 0) > (time() - 60)) { return array_merge($cached['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, ]; $_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}"]); return $stmt->execute([$userId]); }