From ce0dc64e76b35e40feb96bb20914af5d0eceaeff Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Tue, 8 Sep 2026 14:31:15 +0200 Subject: [PATCH] Implementeer live Microsoft 365 Outlook en Atlassian Jira integraties met agendasync en taakimport --- .env.example | 10 + api/integrations.php | 113 +++++ api/planner.php | 10 +- auth/callback_microsoft.php | 97 ++++ auth/oauth_microsoft.php | 42 ++ includes/http.php | 158 ++++++ includes/integrations/jira.php | 310 ++++++++++++ includes/integrations/microsoft.php | 259 ++++++++++ integrations.php | 741 +++++++++++++++++++++------- planner.php | 295 ++++++++--- 10 files changed, 1802 insertions(+), 233 deletions(-) create mode 100644 api/integrations.php create mode 100644 auth/callback_microsoft.php create mode 100644 auth/oauth_microsoft.php create mode 100644 includes/http.php create mode 100644 includes/integrations/jira.php create mode 100644 includes/integrations/microsoft.php diff --git a/.env.example b/.env.example index a7eaf40..9290c45 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,13 @@ APP_URL="https://uw-domein.nl" # Applicatie Geheime Sleutel (willekeurige tekenreeks) APP_SECRET="fociz-maria-db-super-secret-key-998877" +# Microsoft 365 / Outlook (Optionele globale defaults; kan ook via web UI worden ingesteld) +MICROSOFT_CLIENT_ID="" +MICROSOFT_CLIENT_SECRET="" +MICROSOFT_TENANT_ID="common" + +# Atlassian Jira Cloud (Optionele globale defaults; kan ook via web UI worden ingesteld) +JIRA_DOMAIN="" +JIRA_EMAIL="" +JIRA_API_TOKEN="" + diff --git a/api/integrations.php b/api/integrations.php new file mode 100644 index 0000000..1eea488 --- /dev/null +++ b/api/integrations.php @@ -0,0 +1,113 @@ +prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'JIRA'"); + $stmt->execute([$user['id']]); + $saved = json_decode($stmt->fetchColumn() ?: '{}', true); + $apiToken = $saved['jira_api_token'] ?? ''; + if (empty($domain)) $domain = $saved['jira_domain'] ?? ''; + if (empty($email)) $email = $saved['jira_email'] ?? ''; + } + + $res = jira_test_connection($domain, $email, $apiToken); + + if ($res['success'] && $save) { + $jiraUser = $res['user']; + $settings = json_encode([ + 'jira_domain' => jira_normalize_domain($domain), + 'jira_email' => $email, + 'jira_api_token' => $apiToken, + 'account_name' => $jiraUser['displayName'] ?? '', + 'account_id' => $jiraUser['accountId'] ?? '', + ]); + + $stmt = $db->prepare(" + INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at) + VALUES (?, 'JIRA', 1, 0, ?, NOW()) + ON DUPLICATE KEY UPDATE + is_enabled = 1, + is_demo_mode = 0, + settings = VALUES(settings), + last_sync_at = NOW() + "); + $stmt->execute([$user['id'], $settings]); + + // Wis eventuele oude sessie cache + unset($_SESSION["jira_issues_user_{$user['id']}"]); + } + + echo json_encode($res); + exit; + } + + // 2. Jira Issues Ophalen (Toegewezen aan de gebruiker) + if ($action === 'get_jira_issues') { + $force = !empty($_GET['force']) || !empty($_POST['force']); + $res = jira_get_user_issues($user['id'], $force); + echo json_encode($res); + exit; + } + + // 3. Outlook Agenda Afspraken Ophalen + if ($action === 'get_outlook_events') { + $date = $_GET['date'] ?? date('Y-m-d'); + $events = microsoft_fetch_today_events($user['id'], $date); + echo json_encode([ + 'success' => true, + 'date' => $date, + 'events' => $events, + 'count' => count($events), + ]); + exit; + } + + // 4. Integratie Ontkoppelen + if ($action === 'disconnect') { + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $provider = strtoupper(trim($input['provider'] ?? '')); + + if ($provider === 'OUTLOOK') { + microsoft_disconnect($user['id']); + echo json_encode(['success' => true, 'message' => 'Microsoft Outlook ontkoppeld']); + exit; + } + + if ($provider === 'JIRA') { + jira_disconnect($user['id']); + echo json_encode(['success' => true, 'message' => 'Atlassian Jira ontkoppeld']); + exit; + } + + echo json_encode(['error' => 'Onbekende provider']); + exit; + } + + echo json_encode(['error' => 'Onbekende actie']); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} diff --git a/api/planner.php b/api/planner.php index 3e839cc..24a2108 100644 --- a/api/planner.php +++ b/api/planner.php @@ -53,11 +53,15 @@ try { $stmtPlan->execute([$user['id'], $date]); $planId = $db->lastInsertId(); + $source = !empty($data['source']) ? substr(trim($data['source']), 0, 50) : 'FOCIZ'; + $externalId = !empty($data['external_id']) ? substr(trim($data['external_id']), 0, 100) : null; + $externalUrl = !empty($data['external_url']) ? trim($data['external_url']) : null; + $stmt = $db->prepare(" - INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status) - VALUES (?, ?, ?, ?, ?, 'TODO') + INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status, source, external_id, external_url) + VALUES (?, ?, ?, ?, ?, 'TODO', ?, ?, ?) "); - $stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart]); + $stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart, $source, $externalId, $externalUrl]); $taskId = (int)$db->lastInsertId(); echo json_encode([ diff --git a/auth/callback_microsoft.php b/auth/callback_microsoft.php new file mode 100644 index 0000000..5afff06 --- /dev/null +++ b/auth/callback_microsoft.php @@ -0,0 +1,97 @@ +prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'"); +$stmt->execute([$user['id']]); +$row = $stmt->fetch(); +$savedSettings = json_decode($row['settings'] ?? '{}', true) ?: []; + +$clientId = !empty($savedSettings['client_id']) ? $savedSettings['client_id'] : fociz_env('MICROSOFT_CLIENT_ID', ''); +$clientSecret = !empty($savedSettings['client_secret']) ? $savedSettings['client_secret'] : fociz_env('MICROSOFT_CLIENT_SECRET', ''); +$tenantId = !empty($savedSettings['tenant_id']) ? $savedSettings['tenant_id'] : fociz_env('MICROSOFT_TENANT_ID', 'common'); + +if (empty($clientId) || empty($clientSecret)) { + header('Location: /integrations.php?error=' . urlencode('Client ID of Client Secret ontbreekt in Fociz. Vul deze eerst in bij de Outlook instellingen.')); + exit; +} + +$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php'; + +// 5. Code omwisselen voor access & refresh tokens +$tokenResult = microsoft_exchange_code($code, $clientId, $clientSecret, $redirectUri, $tenantId); + +if (!$tokenResult['success']) { + header('Location: /integrations.php?error=' . urlencode('Token uitwisseling mislukt: ' . $tokenResult['error'])); + exit; +} + +$accessToken = $tokenResult['access_token']; +$refreshToken = $tokenResult['refresh_token']; +$expiresIn = $tokenResult['expires_in']; +$expiresAt = time() + $expiresIn; + +// 6. Microsoft gebruikersprofiel ophalen +$profile = microsoft_get_user_profile($accessToken); +$accountEmail = $profile['mail'] ?? ($profile['userPrincipalName'] ?? $user['email']); +$accountName = $profile['displayName'] ?? 'Microsoft 365 Account'; + +// 7. Opslaan in MariaDB +$newSettings = array_merge($savedSettings, [ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'tenant_id' => $tenantId, + 'expires_at' => $expiresAt, + 'account_email' => $accountEmail, + 'account_name' => $accountName, +]); + +$saveStmt = $db->prepare(" + INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, access_token, refresh_token, settings, last_sync_at) + VALUES (?, 'OUTLOOK', 1, 0, ?, ?, ?, NOW()) + ON DUPLICATE KEY UPDATE + is_enabled = 1, + is_demo_mode = 0, + access_token = VALUES(access_token), + refresh_token = VALUES(refresh_token), + settings = VALUES(settings), + last_sync_at = NOW() +"); +$saveStmt->execute([$user['id'], $accessToken, $refreshToken, json_encode($newSettings)]); + +// 8. Succesvol terugkeren naar de Integraties Hub +header('Location: /integrations.php?success=' . urlencode("Microsoft Outlook succesvol gekoppeld als {$accountEmail}!")); +exit; diff --git a/auth/oauth_microsoft.php b/auth/oauth_microsoft.php new file mode 100644 index 0000000..74f18a8 --- /dev/null +++ b/auth/oauth_microsoft.php @@ -0,0 +1,42 @@ +prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'"); +$stmt->execute([$user['id']]); +$row = $stmt->fetch(); +$savedSettings = json_decode($row['settings'] ?? '{}', true) ?: []; + +$clientId = !empty($_GET['client_id']) + ? trim($_GET['client_id']) + : (!empty($savedSettings['client_id']) ? $savedSettings['client_id'] : fociz_env('MICROSOFT_CLIENT_ID', '')); + +$tenantId = !empty($_GET['tenant_id']) + ? trim($_GET['tenant_id']) + : (!empty($savedSettings['tenant_id']) ? $savedSettings['tenant_id'] : fociz_env('MICROSOFT_TENANT_ID', 'common')); + +if (empty($clientId)) { + header('Location: /integrations.php?error=missing_ms_client_id'); + exit; +} + +// Bepaal de dynamische of geconfigureerde redirect URI +$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php'; + +// Genereer unieke CSRF state +$state = bin2hex(random_bytes(16)); +$_SESSION['ms_oauth_state'] = $state; +$_SESSION['ms_oauth_user_id'] = $user['id']; + +// Stuur gebruiker door naar Microsoft login +$authUrl = microsoft_get_auth_url($clientId, $redirectUri, $tenantId, $state); +header("Location: {$authUrl}"); +exit; diff --git a/includes/http.php b/includes/http.php new file mode 100644 index 0000000..6d3822f --- /dev/null +++ b/includes/http.php @@ -0,0 +1,158 @@ + 'GET'|'POST'|'PUT'|'DELETE', + * 'headers' => ['Header-Name: Value'] of ['Header-Name' => 'Value'], + * 'body' => string|array, + * 'timeout' => int (seconden, default 12), + * 'verify_ssl' => bool (default true) + * ] + * @return array ['status' => int, 'body' => string, 'json' => ?array, 'error' => ?string] + */ + function fociz_http_request(string $url, array $options = []): array { + $method = strtoupper($options['method'] ?? 'GET'); + $timeout = (int)($options['timeout'] ?? 12); + $verifySsl = $options['verify_ssl'] ?? true; + + // Normaliseer headers naar 'Name: Value' regels + $rawHeaders = $options['headers'] ?? []; + $headerLines = []; + foreach ($rawHeaders as $k => $v) { + if (is_numeric($k)) { + $headerLines[] = $v; + } else { + $headerLines[] = "{$k}: {$v}"; + } + } + + // Bereid payload voor + $body = $options['body'] ?? null; + if (is_array($body)) { + $hasJsonHeader = false; + foreach ($headerLines as $h) { + if (stripos($h, 'application/json') !== false) { + $hasJsonHeader = true; + break; + } + } + if ($hasJsonHeader) { + $body = json_encode($body); + } else { + $body = http_build_query($body); + $headerLines[] = 'Content-Type: application/x-www-form-urlencoded'; + } + } + + // Voeg standaard User-Agent toe als die er niet is + $hasUserAgent = false; + foreach ($headerLines as $h) { + if (stripos($h, 'User-Agent:') !== false) { + $hasUserAgent = true; + break; + } + } + if (!$hasUserAgent) { + $headerLines[] = 'User-Agent: Fociz-Integrations/1.0'; + } + + // 1. Probeer cURL indien beschikbaar + if (extension_loaded('curl')) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 6); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headerLines); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifySsl); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifySsl ? 2 : 0); + + if ($body !== null && in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + + $responseBody = curl_exec($ch); + $curlError = curl_error($ch); + $statusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($responseBody === false && !empty($curlError)) { + return [ + 'status' => $statusCode ?: 0, + 'body' => '', + 'json' => null, + 'error' => "cURL fout: {$curlError}" + ]; + } + + $parsedJson = json_decode((string)$responseBody, true); + + return [ + 'status' => $statusCode, + 'body' => (string)$responseBody, + 'json' => is_array($parsedJson) ? $parsedJson : null, + 'error' => null + ]; + } + + // 2. Fallback: PHP Streams (file_get_contents) + $contextOptions = [ + 'http' => [ + 'method' => $method, + 'header' => implode("\r\n", $headerLines), + 'timeout' => $timeout, + 'ignore_errors' => true // Zorgt dat we ook bij 4xx / 5xx responses de body en statuscode kunnen lezen + ], + 'ssl' => [ + 'verify_peer' => $verifySsl, + 'verify_peer_name' => $verifySsl + ] + ]; + + if ($body !== null && in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) { + $contextOptions['http']['content'] = $body; + } + + $context = stream_context_create($contextOptions); + $responseBody = @file_get_contents($url, false, $context); + + $statusCode = 0; + if (!empty($http_response_header)) { + // Zoek de laatste HTTP status regel (bij eventuele redirects) + foreach (array_reverse($http_response_header) as $hdr) { + if (preg_match('#HTTP/\d\.\d\s+(\d{3})#i', $hdr, $matches)) { + $statusCode = (int)$matches[1]; + break; + } + } + } + + if ($responseBody === false) { + $lastErr = error_get_last(); + return [ + 'status' => $statusCode ?: 0, + 'body' => '', + 'json' => null, + 'error' => 'Stream fout: ' . ($lastErr['message'] ?? 'Kan verbinding niet voltooien') + ]; + } + + $parsedJson = json_decode((string)$responseBody, true); + + return [ + 'status' => $statusCode, + 'body' => (string)$responseBody, + 'json' => is_array($parsedJson) ? $parsedJson : null, + 'error' => null + ]; + } +} diff --git a/includes/integrations/jira.php b/includes/integrations/jira.php new file mode 100644 index 0000000..87c925d --- /dev/null +++ b/includes/integrations/jira.php @@ -0,0 +1,310 @@ + 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 + */ +function jira_fetch_assigned_issues(string $domain, string $email, string $apiToken, int $maxResults = 50): array { + $cleanDomain = jira_normalize_domain($domain); + $authHeader = jira_get_auth_header($email, $apiToken); + + // JQL: Alle openstaande taken toegewezen aan mij, gesorteerd op recent bijgewerkt + $jql = 'assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC'; + + $params = [ + 'jql' => $jql, + 'maxResults' => min($maxResults, 100), + 'fields' => 'summary,status,priority,issuetype,parent,project,duedate,updated', + ]; + + $url = $cleanDomain . '/rest/api/3/search?' . http_build_query($params); + + $response = fociz_http_request($url, [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => $authHeader, + 'Accept' => 'application/json', + ], + 'timeout' => 12, + ]); + + // Fallback naar api v2 + if ($response['status'] === 404) { + $urlV2 = $cleanDomain . '/rest/api/2/search?' . http_build_query($params); + $response = fociz_http_request($urlV2, [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => $authHeader, + 'Accept' => 'application/json', + ], + 'timeout' => 12, + ]); + } + + if ($response['status'] !== 200 || !isset($response['json']['issues'])) { + return [ + 'success' => false, + 'error' => $response['json']['errorMessages'][0] ?? ($response['error'] ?? 'Kan Jira tickets niet ophalen'), + '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'; + $priorityName = $fields['priority']['name'] ?? 'Medium'; + $issueTypeName = $fields['issuetype']['name'] ?? 'Taak'; + $projectName = $fields['project']['name'] ?? 'Project'; + $dueDate = $fields['duedate'] ?? null; + + $issues[] = [ + 'key' => $key, + 'summary' => $summary, + 'status' => $statusName, + 'priority' => $priorityName, + 'issue_type' => $issueTypeName, + 'project' => $projectName, + 'due_date' => $dueDate, + 'url' => "{$cleanDomain}/browse/{$key}", + 'source' => 'JIRA_LIVE', + ]; + } + + return [ + 'success' => true, + 'issues' => $issues, + 'total' => count($issues), + ]; +} + +/** + * Haalt de Jira tickets op voor een Fociz gebruiker + * Schakelt naadloos tussen live Jira tickets en sandbox demotickets + */ +function jira_get_user_issues(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(); + + // Als de integratie niet is ingesteld of in demo mode staat, geef interactieve fallback issues terug + if (!$integration || !empty($integration['is_demo_mode'])) { + return [ + 'is_demo' => true, + 'success' => true, + 'issues' => [ + [ + 'key' => 'FOC-101', + 'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren', + 'status' => 'In Progress', + 'priority' => 'High', + 'issue_type' => 'Story', + 'project' => 'Fociz Platform', + 'due_date' => date('Y-m-d', strtotime('+2 days')), + 'url' => '#', + 'source' => 'SANDBOX', + ], + [ + 'key' => 'FOC-102', + 'summary' => 'Interactief whiteboard canvas voor brainstormsessies', + 'status' => 'To Do', + 'priority' => 'High', + 'issue_type' => 'Feature', + 'project' => 'Fociz Platform', + 'due_date' => date('Y-m-d', strtotime('+4 days')), + 'url' => '#', + 'source' => 'SANDBOX', + ], + [ + 'key' => 'FOC-103', + 'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API', + 'status' => 'In Review', + 'priority' => 'Medium', + 'issue_type' => 'Task', + 'project' => 'Integrations Hub', + 'due_date' => date('Y-m-d'), + 'url' => '#', + 'source' => 'SANDBOX', + ], + [ + 'key' => 'FOC-104', + 'summary' => 'Roadmap kwartaalweergave en voortgangsindicatoren', + 'status' => 'To Do', + 'priority' => 'Medium', + 'issue_type' => 'Story', + 'project' => 'Roadmap Module', + 'due_date' => date('Y-m-d', strtotime('+7 days')), + 'url' => '#', + 'source' => 'SANDBOX', + ], + ], + ]; + } + + $settings = json_decode($integration['settings'] ?? '{}', true) ?: []; + $domain = $settings['jira_domain'] ?? fociz_env('JIRA_DOMAIN', ''); + $email = $settings['jira_email'] ?? fociz_env('JIRA_EMAIL', ''); + $apiToken = $settings['jira_api_token'] ?? fociz_env('JIRA_API_TOKEN', ''); + + if (empty($domain) || empty($email) || empty($apiToken)) { + return [ + 'is_demo' => false, + 'success' => false, + 'error' => 'Jira configuratie is onvolledig.', + 'issues' => [], + ]; + } + + // Sessiecache voor 90 seconden om onnodige Jira API calls bij snelle pagina refreshes te beperken + if (session_status() === PHP_SESSION_NONE) { + @session_start(); + } + $cacheKey = "jira_issues_user_{$userId}"; + $cached = $_SESSION[$cacheKey] ?? null; + + if (!$forceRefresh && $cached && is_array($cached) && ($cached['timestamp'] ?? 0) > (time() - 90)) { + return [ + 'is_demo' => false, + 'success' => true, + 'issues' => $cached['issues'], + 'from_cache' => true, + ]; + } + + $result = jira_fetch_assigned_issues($domain, $email, $apiToken, 50); + + if ($result['success']) { + $_SESSION[$cacheKey] = [ + 'timestamp' => time(), + 'issues' => $result['issues'], + ]; + + // Werk last_sync_at bij + $stmtUpdate = $db->prepare("UPDATE integrations SET last_sync_at = NOW() WHERE id = ?"); + $stmtUpdate->execute([$integration['id']]); + + return [ + 'is_demo' => false, + 'success' => true, + 'issues' => $result['issues'], + ]; + } + + return [ + 'is_demo' => false, + 'success' => false, + 'error' => $result['error'], + 'issues' => [], + ]; +} + +/** + * 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}"]); + return $stmt->execute([$userId]); +} diff --git a/includes/integrations/microsoft.php b/includes/integrations/microsoft.php new file mode 100644 index 0000000..6616486 --- /dev/null +++ b/includes/integrations/microsoft.php @@ -0,0 +1,259 @@ + trim($clientId), + 'response_type' => 'code', + 'redirect_uri' => trim($redirectUri), + 'response_mode' => 'query', + 'scope' => MS_DEFAULT_SCOPES, + 'state' => $state, + 'prompt' => 'select_account', + ]; + + return $baseUrl . '?' . http_build_query($params); +} + +/** + * Wisselt de ontvangen autorisatiecode om voor access- en refresh tokens + */ +function microsoft_exchange_code(string $code, string $clientId, string $clientSecret, string $redirectUri, string $tenantId = 'common'): array { + $tenant = !empty($tenantId) ? trim($tenantId) : 'common'; + $tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode($tenant)); + + $payload = [ + 'client_id' => trim($clientId), + 'client_secret' => trim($clientSecret), + 'code' => trim($code), + 'redirect_uri' => trim($redirectUri), + 'grant_type' => 'authorization_code', + 'scope' => MS_DEFAULT_SCOPES, + ]; + + $response = fociz_http_request($tokenUrl, [ + 'method' => 'POST', + 'body' => $payload, + 'timeout' => 15, + ]); + + if ($response['status'] !== 200 || empty($response['json']['access_token'])) { + $errorMsg = $response['json']['error_description'] ?? ($response['error'] ?? 'Onbekende fout bij token uitwisseling'); + return [ + 'success' => false, + 'error' => $errorMsg, + ]; + } + + return [ + 'success' => true, + 'access_token' => $response['json']['access_token'], + 'refresh_token' => $response['json']['refresh_token'] ?? null, + 'expires_in' => (int)($response['json']['expires_in'] ?? 3600), + ]; +} + +/** + * Haalt het Microsoft gebruikersprofiel op via Graph API + */ +function microsoft_get_user_profile(string $accessToken): ?array { + $response = fociz_http_request(MS_GRAPH_BASE_URL . '/me', [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => "Bearer {$accessToken}", + 'Accept' => 'application/json', + ], + 'timeout' => 10, + ]); + + if ($response['status'] === 200 && is_array($response['json'])) { + return [ + 'displayName' => $response['json']['displayName'] ?? 'Microsoft 365 Gebruiker', + 'mail' => $response['json']['mail'] ?? ($response['json']['userPrincipalName'] ?? ''), + 'id' => $response['json']['id'] ?? '', + ]; + } + + return null; +} + +/** + * Haalt een geldige access_token op voor de gebruiker; vernieuwt automatisch indien verlopen + */ +function microsoft_get_valid_token(int $userId): ?string { + $db = get_db_connection(); + $stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1"); + $stmt->execute([$userId]); + $integration = $stmt->fetch(); + + if (!$integration || empty($integration['access_token'])) { + return null; + } + + $settings = json_decode($integration['settings'] ?? '{}', true) ?: []; + $expiresAt = (int)($settings['expires_at'] ?? 0); + + // Als token nog minstens 4 minuten geldig is, direct retourneren + if ($expiresAt > (time() + 240)) { + return $integration['access_token']; + } + + // Token is verlopen of verloopt bijna; refresh met refresh_token + $refreshToken = $integration['refresh_token'] ?? ''; + $clientId = $settings['client_id'] ?? fociz_env('MICROSOFT_CLIENT_ID', ''); + $clientSecret = $settings['client_secret'] ?? fociz_env('MICROSOFT_CLIENT_SECRET', ''); + $tenantId = $settings['tenant_id'] ?? fociz_env('MICROSOFT_TENANT_ID', 'common'); + + if (empty($refreshToken) || empty($clientId) || empty($clientSecret)) { + return null; + } + + $tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode(!empty($tenantId) ? $tenantId : 'common')); + $payload = [ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + 'scope' => MS_DEFAULT_SCOPES, + ]; + + $response = fociz_http_request($tokenUrl, [ + 'method' => 'POST', + 'body' => $payload, + 'timeout' => 15, + ]); + + if ($response['status'] === 200 && !empty($response['json']['access_token'])) { + $newAccessToken = $response['json']['access_token']; + $newRefreshToken = $response['json']['refresh_token'] ?? $refreshToken; + $expiresIn = (int)($response['json']['expires_in'] ?? 3600); + $settings['expires_at'] = time() + $expiresIn; + + $updateStmt = $db->prepare(" + UPDATE integrations + SET access_token = ?, refresh_token = ?, settings = ?, last_sync_at = NOW() + WHERE id = ? + "); + $updateStmt->execute([$newAccessToken, $newRefreshToken, json_encode($settings), $integration['id']]); + + return $newAccessToken; + } + + error_log("Microsoft token refresh mislukt voor user {$userId}: " . ($response['error'] ?? json_encode($response['json']))); + return null; +} + +/** + * Haalt agenda-afspraken van Microsoft Outlook op voor de opgegeven datum + */ +function microsoft_fetch_today_events(int $userId, ?string $date = null): array { + $targetDate = $date ?: date('Y-m-d'); + $token = microsoft_get_valid_token($userId); + + if (!$token) { + return []; + } + + // Venster van 00:00:00 tot 23:59:59 lokale tijd + $startIso = "{$targetDate}T00:00:00"; + $endIso = "{$targetDate}T23:59:59"; + + $url = MS_GRAPH_BASE_URL . '/me/calendarView?' . http_build_query([ + 'startDateTime' => $startIso, + 'endDateTime' => $endIso, + '$select' => 'id,subject,bodyPreview,start,end,location,isAllDay,webLink,onlineMeeting,isCancelled', + '$orderby' => 'start/dateTime', + '$top' => '50', + ]); + + $response = fociz_http_request($url, [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => "Bearer {$token}", + 'Accept' => 'application/json', + 'Prefer' => 'outlook.timezone="Europe/Amsterdam"', + ], + 'timeout' => 12, + ]); + + if ($response['status'] !== 200 || empty($response['json']['value'])) { + return []; + } + + $rawEvents = $response['json']['value']; + $events = []; + + foreach ($rawEvents as $e) { + // Annuleringen overslaan + if (!empty($e['isCancelled'])) { + continue; + } + + $rawStart = $e['start']['dateTime'] ?? ''; + $rawEnd = $e['end']['dateTime'] ?? ''; + + // Formatteer naar H:i (bijv. 09:30) + $startTime = ''; + $endTime = ''; + if (!empty($rawStart)) { + $startTime = date('H:i', strtotime($rawStart)); + } + if (!empty($rawEnd)) { + $endTime = date('H:i', strtotime($rawEnd)); + } + + // Teams of meeting URL detecteren + $meetingUrl = $e['onlineMeeting']['joinUrl'] ?? ($e['webLink'] ?? 'https://outlook.office.com'); + if (empty($meetingUrl) && !empty($e['bodyPreview'])) { + if (preg_match('#https://teams\.microsoft\.com/l/meetup-join/[^\s"\'<>]+#i', $e['bodyPreview'], $m)) { + $meetingUrl = $m[0]; + } + } + + $locationName = $e['location']['displayName'] ?? ''; + if (empty($locationName) && !empty($e['onlineMeeting']['joinUrl'])) { + $locationName = 'Microsoft Teams'; + } + + $events[] = [ + 'id' => $e['id'] ?? uniqid('ms_'), + 'subject' => !empty($e['subject']) ? $e['subject'] : '(Geen onderwerp)', + 'start' => $startTime ?: '09:00', + 'end' => $endTime ?: '10:00', + 'start_raw' => $rawStart, + 'end_raw' => $rawEnd, + 'location' => $locationName ?: 'Geen locatie', + 'meeting_url' => $meetingUrl, + 'is_all_day' => !empty($e['isAllDay']), + 'source' => 'OUTLOOK_LIVE', + ]; + } + + return $events; +} + +/** + * Ontkoppelt Microsoft integratie voor een gebruiker + */ +function microsoft_disconnect(int $userId): bool { + $db = get_db_connection(); + $stmt = $db->prepare("DELETE FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'"); + return $stmt->execute([$userId]); +} diff --git a/integrations.php b/integrations.php index 7ce94ff..80fa5b5 100644 --- a/integrations.php +++ b/integrations.php @@ -1,34 +1,151 @@ $_POST['client_id'] ?? '', - 'client_secret' => $_POST['client_secret'] ?? '', - 'tenant_id' => $_POST['tenant_id'] ?? '', - 'jira_domain' => $_POST['jira_domain'] ?? '', - 'jira_email' => $_POST['jira_email'] ?? '', - 'jira_api_token' => $_POST['jira_api_token'] ?? '', - ]); +$messageType = 'success'; + +// Query parameters (bijv. na OAuth redirect) +if (!empty($_GET['success'])) { + $message = $_GET['success']; + $messageType = 'success'; +} elseif (!empty($_GET['error'])) { + $message = $_GET['error']; + $messageType = 'error'; +} + +// POST Verwerking +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $provider = strtoupper(trim($_POST['provider'] ?? '')); + $action = $_POST['action'] ?? 'save'; + + // 1. Ontkoppelen + if ($action === 'disconnect') { + if ($provider === 'OUTLOOK') { + microsoft_disconnect($userId); + $message = "Microsoft Outlook is succesvol ontkoppeld."; + } elseif ($provider === 'JIRA') { + jira_disconnect($userId); + $message = "Atlassian Jira is succesvol ontkoppeld."; + } + } + + // 2. Outlook Opslaan + elseif ($provider === 'OUTLOOK') { + $clientId = trim($_POST['client_id'] ?? ''); + $clientSecret = trim($_POST['client_secret'] ?? ''); + $tenantId = trim($_POST['tenant_id'] ?? 'common'); + $isDemo = isset($_POST['is_demo_mode']) ? 1 : 0; + + // Haal bestaande tokens op + $stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'"); + $stmtExisting->execute([$userId]); + $existing = $stmtExisting->fetch(); + $existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: []; + + // Als geheim veld leeg is gelaten, behoud het bestaande geheim + if (empty($clientSecret) && !empty($existingSettings['client_secret'])) { + $clientSecret = $existingSettings['client_secret']; + } + + $newSettings = array_merge($existingSettings, [ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'tenant_id' => $tenantId, + ]); - if (in_array($provider, ['OUTLOOK', 'TEAMS', 'JIRA'])) { $stmt = $db->prepare(" INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at) - VALUES (?, ?, 1, ?, ?, NOW()) - ON DUPLICATE KEY UPDATE is_demo_mode = VALUES(is_demo_mode), settings = VALUES(settings), last_sync_at = NOW() + VALUES (?, 'OUTLOOK', 1, ?, ?, NOW()) + ON DUPLICATE KEY UPDATE + is_demo_mode = VALUES(is_demo_mode), + settings = VALUES(settings), + last_sync_at = NOW() "); - $stmt->execute([$userId, $provider, $isDemo, $settings]); - $message = "Instellingen voor {$provider} succesvol opgeslagen."; + $stmt->execute([$userId, $isDemo, json_encode($newSettings)]); + $message = "Instellingen voor Microsoft Outlook zijn opgeslagen."; + + // Als gebruiker direct op 'Koppel nu' klikte + if ($action === 'connect_oauth') { + header("Location: /auth/oauth_microsoft.php?client_id=" . urlencode($clientId) . "&tenant_id=" . urlencode($tenantId)); + exit; + } + } + + // 3. Teams Opslaan + elseif ($provider === 'TEAMS') { + $isDemo = isset($_POST['is_demo_mode']) ? 1 : 0; + $stmt = $db->prepare(" + INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, last_sync_at) + VALUES (?, 'TEAMS', 1, ?, NOW()) + ON DUPLICATE KEY UPDATE is_demo_mode = VALUES(is_demo_mode), last_sync_at = NOW() + "); + $stmt->execute([$userId, $isDemo]); + $message = "Instellingen voor Microsoft Teams zijn bijgewerkt."; + } + + // 4. Jira Opslaan & Testen + elseif ($provider === 'JIRA') { + $jiraDomain = jira_normalize_domain($_POST['jira_domain'] ?? ''); + $jiraEmail = trim($_POST['jira_email'] ?? ''); + $jiraToken = trim($_POST['jira_api_token'] ?? ''); + $isDemo = isset($_POST['is_demo_mode']) ? 1 : 0; + + // Haal bestaande instellingen op + $stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA'"); + $stmtExisting->execute([$userId]); + $existing = $stmtExisting->fetch(); + $existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: []; + + if (empty($jiraToken) && !empty($existingSettings['jira_api_token'])) { + $jiraToken = $existingSettings['jira_api_token']; + } + + // Als niet in demo mode, test verbinding + $accountName = $existingSettings['account_name'] ?? ''; + $accountId = $existingSettings['account_id'] ?? ''; + if (!$isDemo && !empty($jiraDomain) && !empty($jiraEmail) && !empty($jiraToken)) { + $testRes = jira_test_connection($jiraDomain, $jiraEmail, $jiraToken); + if (!$testRes['success']) { + $message = "Jira Verbindingsfout: " . $testRes['error']; + $messageType = 'error'; + } else { + $accountName = $testRes['user']['displayName'] ?? 'Jira Gebruiker'; + $accountId = $testRes['user']['accountId'] ?? ''; + $message = "Verbinding met Jira geslaagd! Ingelogd als: {$accountName}"; + } + } else { + $message = "Jira instellingen opgeslagen."; + } + + $newSettings = [ + 'jira_domain' => $jiraDomain, + 'jira_email' => $jiraEmail, + 'jira_api_token' => $jiraToken, + 'account_name' => $accountName, + 'account_id' => $accountId, + ]; + + $stmt = $db->prepare(" + INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at) + VALUES (?, 'JIRA', 1, ?, ?, NOW()) + ON DUPLICATE KEY UPDATE + is_demo_mode = VALUES(is_demo_mode), + settings = VALUES(settings), + last_sync_at = NOW() + "); + $stmt->execute([$userId, $isDemo, json_encode($newSettings)]); + + // Wis cache + unset($_SESSION["jira_issues_user_{$userId}"]); } } @@ -42,263 +159,543 @@ foreach ($configsRaw as $c) { $configs[$c['provider']]['settings_parsed'] = json_decode($c['settings'] ?? '{}', true) ?: []; } -// Jira demo issues -$jiraIssues = [ - [ - 'key' => 'FOC-101', - 'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren', - 'status' => 'In Progress', - 'priority' => 'High', - 'epic' => 'Security & Access', - ], - [ - 'key' => 'FOC-102', - 'summary' => 'Interactief whiteboard canvas voor brainstormsessies', - 'status' => 'To Do', - 'priority' => 'High', - 'epic' => 'Brainstorming Module', - ], - [ - 'key' => 'FOC-103', - 'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API', - 'status' => 'In Review', - 'priority' => 'Medium', - 'epic' => 'Integrations Hub', - ], - [ - 'key' => 'FOC-104', - 'summary' => 'Roadmap kwartaalweergave en voortgangsindicatoren', - 'status' => 'To Do', - 'priority' => 'Medium', - 'epic' => 'Roadmap Module', - ] -]; +// Bepaal statussen +$isOutlookConnected = !empty($configs['OUTLOOK']['access_token']) && empty($configs['OUTLOOK']['is_demo_mode']); +$outlookSettings = $configs['OUTLOOK']['settings_parsed'] ?? []; + +$isJiraConnected = !empty($configs['JIRA']) && empty($configs['JIRA']['is_demo_mode']) && !empty($configs['JIRA']['settings_parsed']['jira_api_token']); +$jiraSettings = $configs['JIRA']['settings_parsed'] ?? []; + +// Haal actuele Jira issues op +$forceRefresh = isset($_GET['refresh_jira']); +$jiraData = jira_get_user_issues($userId, $forceRefresh); +$jiraIssues = $jiraData['issues'] ?? []; +$jiraIsDemo = !empty($jiraData['is_demo']); + +// Callback redirect URI +$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php'; ?>
-
-
- - Connectiviteit & Synchronisatie +
+
+
+
+ + Live Koppelingen & Data Synchronisatie +
+

+ Integraties Hub +

+

+ Koppel Microsoft 365 Outlook (voor realtime agenda & Teams meetings) en Atlassian Jira Cloud (om direct uw toegewezen tickets in te plannen). +

+
+ + +
+
+ + Outlook: +
+
+ + Jira: +
+
-

- Integraties Hub -

-

- Beheer uw koppelingen met Microsoft Outlook (agenda & taken), Microsoft Teams (meetings) en Atlassian Jira (issues & roadmap). -

+ -
- - +
+
+ + +
+
- +
- -
-
-
+ +
+
+
- - - Actief - + + + + Live Verbonden + + + + Sandbox Modus + + + + Niet Gekoppeld + +
-

Microsoft Outlook

-

- Synchroniseert uw agenda-afspraken en e-mails automatisch naar uw dagplanning en time-blocks. -

- -
-
- Status: - Gekoppeld -
-
- Sync frequentie: - Elke 15 min -
+
+

Microsoft Outlook (365)

+

+ Synchroniseert automatisch uw zakelijke agenda-afspraken en meetings naar de dagplanner tijdlijn. +

+ + +
+
+ Account: + + + +
+
+ Naam: + +
+
+ Laatste Sync: + +
+
+ + + +
+ + + + Azure App Registratie Instructies + + + +
+

1. Open Azure App Registraties en maak een nieuwe registratie.

+

2. Kies Accounts in elke organisatiemap (...) en persoonlijke Microsoft-accounts.

+

3. Voeg een Web Omleidings-URI toe:

+
+ + +
+

4. Maak een geheim aan onder Certificaten en geheimen.

+

5. Voeg API-machtigingen toe: Calendars.Read, User.Read, offline_access.

+
+
-
+ - -
-
-
-
+
+
+
- Actief + Inbegrepen bij Outlook
-

Microsoft Teams

-

- Toont vandaag geplande Teams meetings met directe 'Deelnemen' links en kanaalupdates. -

+
+

Microsoft Teams

+

+ Detecteert Teams vergaderingen automatisch vanuit uw Outlook agenda en toont directe 'Deelnemen' knoppen. +

+
-
+
Direct Deelnemen: Ondersteund
- Meldingen: - Ingeschakeld + Meeting detectie: + Via Graph API
+
+ Status: + +
+
+ +
+

+ Teams vergaderingen worden automatisch uitgelezen zodra Microsoft Outlook is gekoppeld. U hoeft voor Teams geen aparte koppeling te configureren! +

-
+ +
-
-
-
+
+
+
- - - Actief - + + + + Live Verbonden + + + + Sandbox Modus + + + + Niet Gekoppeld + +
-

Atlassian Jira

-

- Koppelt Jira Epics en Issues direct aan uw roadmap items en takenlijst. -

+
+

Atlassian Jira Cloud

+

+ Koppelt uw Jira tickets rechtstreeks aan de Fociz dagplanning en roadmap. +

+
-
-
- Two-way Status Sync: - Ondersteund + +
+
+ Ingelogd als: + + + +
+
+ Domein: + +
+
+ Tickets Toegewezen: + open taken +
-
- Roadmap Linkage: - Actief + + +
+
+ Atlassian API Token nodig? + + Genereer Token + +
-
+ + - +
- - + + +
+ +
+ + +
+ +
+ +
+ + +
- +
+ +
+ + + +
-
+

Jira Issues Toegewezen aan U

+ + Sandbox + + Live Jira + +
+ + - issues
-
- -
-
-
- - - - - - + +
+ Geen openstaande tickets gevonden die aan u zijn toegewezen in Jira. +
+ +
+ 'text-red-400 bg-red-500/10 border-red-500/20', + 'medium', 'gemiddeld' => 'text-amber-400 bg-amber-500/10 border-amber-500/20', + default => 'text-slate-400 bg-slate-800/60 border-slate-700', + }; + ?> +
+
+
+
+ + + + + + +
+ + + +
+ +

+ +

+ +
+ Type: + + Vervaldatum: + +
-

- -

- Epic: -
-
- Priority - +
+ + + + +
+ + + + + + + + +
+
-
- -
+ +
+
- diff --git a/planner.php b/planner.php index f1d05af..f4c0ce4 100644 --- a/planner.php +++ b/planner.php @@ -1,15 +1,18 @@ prepare("SELECT * FROM daily_plans WHERE user_id = ? AND plan_date = ?"); $stmtPlan->execute([$userId, $today]); $dailyPlan = $stmtPlan->fetch(); @@ -24,7 +27,7 @@ if (!$dailyPlan) { $dailyPlan = ['id' => $planId, 'plan_date' => $today, 'focus_goal' => 'Mijn belangrijkste doel van vandaag behalen', 'notes' => '']; } -// Haal taken op voor vandaag +// 2. Haal taken op voor vandaag $stmtTasks = $db->prepare(" SELECT * FROM tasks WHERE user_id = ? AND (daily_plan_id = ? OR daily_plan_id IS NULL) @@ -33,35 +36,75 @@ $stmtTasks = $db->prepare(" $stmtTasks->execute([$userId, $dailyPlan['id']]); $tasks = $stmtTasks->fetchAll(); -// Outlook demo / gesynchroniseerde events -$outlookEvents = [ - [ - 'id' => 'evt-1', - 'subject' => 'Daily Team Standup & Focus Sync', - 'start' => '09:00', - 'end' => '09:30', - 'location' => 'Microsoft Teams', - 'meeting_url' => 'https://teams.microsoft.com', - ], - [ - 'id' => 'evt-2', - 'subject' => 'Sprint & Roadmap Review met Stakeholders', - 'start' => '11:00', - 'end' => '12:00', - 'location' => 'Vergaderruimte A & Teams', - 'meeting_url' => 'https://teams.microsoft.com', - ], - [ - 'id' => 'evt-3', - 'subject' => 'Architectuur & MariaDB Database Evaluatie', - 'start' => '14:30', - 'end' => '15:30', - 'location' => 'Online Teams Call', - 'meeting_url' => 'https://teams.microsoft.com', - ] -]; +// 3. Haal integratiestatussen op +$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ?"); +$stmtInt->execute([$userId]); +$userIntegrations = []; +foreach ($stmtInt->fetchAll() as $row) { + $userIntegrations[$row['provider']] = $row; +} -$hours = ["08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00"]; +$isOutlookConnected = !empty($userIntegrations['OUTLOOK']['access_token']) && empty($userIntegrations['OUTLOOK']['is_demo_mode']); +$outlookEvents = []; +$isLiveOutlook = false; + +if ($isOutlookConnected) { + $outlookEvents = microsoft_fetch_today_events($userId, $today); + $isLiveOutlook = !empty($outlookEvents) || true; // Live koppeling is actief +} + +// Fallback naar sandbox data indien Outlook niet live gekoppeld is +if (!$isOutlookConnected || empty($outlookEvents)) { + $outlookEvents = [ + [ + 'id' => 'evt-1', + 'subject' => 'Daily Team Standup & Focus Sync', + 'start' => '09:00', + 'end' => '09:30', + 'location' => 'Microsoft Teams', + 'meeting_url' => 'https://teams.microsoft.com', + 'source' => 'SANDBOX', + ], + [ + 'id' => 'evt-2', + 'subject' => 'Sprint & Roadmap Review met Stakeholders', + 'start' => '11:00', + 'end' => '12:00', + 'location' => 'Vergaderruimte A & Teams', + 'meeting_url' => 'https://teams.microsoft.com', + 'source' => 'SANDBOX', + ], + [ + 'id' => 'evt-3', + 'subject' => 'Architectuur & MariaDB Database Evaluatie', + 'start' => '14:30', + 'end' => '15:30', + 'location' => 'Online Teams Call', + 'meeting_url' => 'https://teams.microsoft.com', + 'source' => 'SANDBOX', + ] + ]; +} + +// 4. Haal Jira issues op voor de snelle import widget +$jiraData = jira_get_user_issues($userId); +$jiraIssues = $jiraData['issues'] ?? []; +$isJiraLive = !empty($jiraData['success']) && empty($jiraData['is_demo']); + +// 5. Tijdlijn uren bepalen +$startHour = 8; +$endHour = 18; +foreach ($outlookEvents as $e) { + if (!empty($e['start'])) { + $h = (int)explode(':', $e['start'])[0]; + if ($h < $startHour && $h >= 6) $startHour = $h; + if ($h > $endHour && $h <= 22) $endHour = $h + 1; + } +} +$hours = []; +for ($h = $startHour; $h <= $endHour; $h++) { + $hours[] = sprintf('%02d:00', $h); +} // Nederlandse datumformattering $dutchDays = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag']; @@ -70,7 +113,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i ?>
- +
@@ -83,11 +126,34 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
- @@ -117,7 +183,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
- +
@@ -212,7 +278,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
- Nog geen taken gepland voor vandaag. Klik op 'Nieuwe Taak' om te beginnen. + Nog geen taken gepland voor vandaag. Klik op 'Nieuwe Taak' of importeer een Jira ticket hieronder.
@@ -225,15 +291,31 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
- - - - - - - +
+ + + JIRA + + + + - +
+ +
+ + + + + + + + + Open in Jira + + + +
@@ -258,6 +340,61 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
+ +
+
+
+ +

+ Mijn Jira Tickets +

+ + + +
+ + + Beheren + + +
+ + +
+ Geen openstaande tickets gevonden in Jira. +
+ +
+ +
+
+
+ + + + + + +
+ + • + +
+ + +
+ +
+ +
+
@@ -282,19 +419,25 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i

Tijdlijn & Time-blocking

- 08:00 – 18:00 +
+ + Outlook 365 + + – +
-
+
- - + - + + + -
- - - Deelnemen - + + + + Deelnemen + +
@@ -374,7 +521,6 @@ function setTimerPreset(minutes) { totalDuration = minutes * 60; updateTimerDisplay(); - // Toggle styling document.getElementById('timerToggleText').innerText = 'Start Focus'; document.getElementById('btnPreset25').className = minutes === 25 ? 'px-2.5 py-1 rounded-lg font-bold bg-emerald-500 text-slate-950' : 'px-2.5 py-1 rounded-lg text-slate-400 hover:text-slate-200'; document.getElementById('btnPreset50').className = minutes === 50 ? 'px-2.5 py-1 rounded-lg font-bold bg-emerald-500 text-slate-950' : 'px-2.5 py-1 rounded-lg text-slate-400 hover:text-slate-200'; @@ -470,6 +616,40 @@ async function submitNewTask() { } } +async function quickImportJira(btn, title, jiraKey, url) { + const originalHtml = btn.innerHTML; + btn.innerText = '...'; + btn.disabled = true; + + try { + const res = await fetch('/api/planner.php?action=add_task', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + title: `[${jiraKey}] ${title}`, + priority: 'P1', + source: 'JIRA', + external_id: jiraKey, + external_url: url, + date: '' + }) + }); + const data = await res.json(); + if (data.success) { + btn.innerHTML = ''; + location.reload(); + } else { + btn.innerHTML = originalHtml; + btn.disabled = false; + alert(data.error || 'Kon taak niet toevoegen'); + } + } catch(err) { + btn.innerHTML = originalHtml; + btn.disabled = false; + alert('Netwerkfout bij toevoegen'); + } +} + async function toggleTask(taskId, isCompleted) { await fetch('/api/planner.php?action=toggle_task', { method: 'POST', @@ -491,4 +671,3 @@ async function deleteTask(taskId) { -