query(" SELECT i.settings, u.email as admin_email FROM integrations i JOIN users u ON i.user_id = u.id WHERE u.role = 'ADMIN' AND i.provider = 'OUTLOOK' AND i.settings IS NOT NULL ORDER BY i.updated_at DESC LIMIT 5 "); $rows = $stmt->fetchAll(); foreach ($rows as $row) { if (!empty($row['admin_email']) && empty($adminOrgDomain)) { $emailParts = explode('@', $row['admin_email']); $domain = strtolower(trim(end($emailParts))); if (!in_array($domain, ['gmail.com', 'outlook.com', 'hotmail.com', 'live.com', 'yahoo.com', 'icloud.com'])) { $adminOrgDomain = $domain; } } if (!empty($row['settings'])) { $saved = json_decode($row['settings'], true) ?: []; if (empty($clientId) && !empty($saved['client_id'])) { $clientId = trim($saved['client_id']); } if (empty($clientSecret) && !empty($saved['client_secret'])) { $clientSecret = trim($saved['client_secret']); } if ((empty($tenantId) || $tenantId === 'common') && !empty($saved['tenant_id']) && $saved['tenant_id'] !== 'common') { $tenantId = trim($saved['tenant_id']); } if (!empty($clientId) && !empty($clientSecret) && !empty($tenantId) && $tenantId !== 'common') { break; } } } // Fallback: zoek een willekeurige OUTLOOK rij met ingevulde client_id if (empty($clientId) || empty($clientSecret) || empty($tenantId) || $tenantId === 'common') { $stmtFallback = $db->query(" SELECT settings FROM integrations WHERE provider = 'OUTLOOK' AND settings LIKE '%client_id%' ORDER BY updated_at DESC LIMIT 3 "); $fallbackRows = $stmtFallback->fetchAll(); foreach ($fallbackRows as $fbRow) { if (!empty($fbRow['settings'])) { $saved = json_decode($fbRow['settings'], true) ?: []; if (empty($clientId) && !empty($saved['client_id'])) { $clientId = trim($saved['client_id']); } if (empty($clientSecret) && !empty($saved['client_secret'])) { $clientSecret = trim($saved['client_secret']); } if ((empty($tenantId) || $tenantId === 'common') && !empty($saved['tenant_id']) && $saved['tenant_id'] !== 'common') { $tenantId = trim($saved['tenant_id']); } } } } // Als tenantId nog leeg of 'common' is, leid het domein af van de beheerder if (empty($tenantId) || $tenantId === 'common') { if (empty($adminOrgDomain)) { $firstAdminEmail = $db->query("SELECT email FROM users WHERE role = 'ADMIN' ORDER BY id ASC LIMIT 1")->fetchColumn(); if ($firstAdminEmail) { $parts = explode('@', $firstAdminEmail); $domain = strtolower(trim(end($parts))); if (!in_array($domain, ['gmail.com', 'outlook.com', 'hotmail.com', 'live.com', 'yahoo.com', 'icloud.com'])) { $adminOrgDomain = $domain; } } } if (!empty($adminOrgDomain)) { $tenantId = $adminOrgDomain; } } } catch (Throwable $e) { error_log("microsoft_get_app_config error: " . $e->getMessage()); } // Default tenantId voor de organisatie if (empty($tenantId) || $tenantId === 'common') { $tenantId = 'planningsagenda.nl'; } return [ 'client_id' => $clientId ?: '', 'client_secret' => $clientSecret ?: '', 'tenant_id' => $tenantId, 'is_configured' => !empty($clientId) && !empty($clientSecret), ]; } /** * Genereert de Microsoft OAuth2 login URL */ function microsoft_get_auth_url(string $clientId, string $redirectUri, string $tenantId = '', string $state = ''): string { $tenant = (!empty($tenantId) && $tenantId !== 'common') ? trim($tenantId) : 'planningsagenda.nl'; $baseUrl = sprintf(MS_OAUTH_AUTHORIZE_URL, urlencode($tenant)); $params = [ 'client_id' => 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 = ''): array { $tenant = (!empty($tenantId) && $tenantId !== 'common') ? trim($tenantId) : 'planningsagenda.nl'; $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, bool $forceRefresh = false): ?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 én we niet forceren, direct retourneren if (!$forceRefresh && $expiresAt > (time() + 240)) { return $integration['access_token']; } // Token is verlopen of forceRefresh gevraagd; refresh met refresh_token $refreshToken = $integration['refresh_token'] ?? ''; $appConfig = microsoft_get_app_config(); $clientId = !empty($settings['client_id']) ? $settings['client_id'] : $appConfig['client_id']; $clientSecret = !empty($settings['client_secret']) ? $settings['client_secret'] : $appConfig['client_secret']; $tenantId = !empty($settings['tenant_id']) ? $settings['tenant_id'] : $appConfig['tenant_id']; if (empty($refreshToken) || empty($clientId) || empty($clientSecret)) { return !$forceRefresh ? ($integration['access_token'] ?? null) : null; } $cleanTenant = (!empty($tenantId) && $tenantId !== 'common') ? $tenantId : 'planningsagenda.nl'; $tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode($cleanTenant)); $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']))); // Fallback: indien het huidige token nog aanwezig is, probeer dit toch te gebruiken if (!empty($integration['access_token'])) { return $integration['access_token']; } return null; } /** * Haalt agenda-afspraken van Microsoft Outlook op voor een datumbereik (bijv. een hele week) */ function microsoft_fetch_events_range(int $userId, string $startDate, string $endDate): array { $token = microsoft_get_valid_token($userId); if (!$token) { return []; } try { $tzLocal = new DateTimeZone('Europe/Amsterdam'); $tzUtc = new DateTimeZone('UTC'); $dtStart = new DateTime("{$startDate} 00:00:00", $tzLocal); $dtEnd = new DateTime("{$endDate} 23:59:59", $tzLocal); $startIso = $dtStart->setTimezone($tzUtc)->format('Y-m-d\TH:i:s\Z'); $endIso = $dtEnd->setTimezone($tzUtc)->format('Y-m-d\TH:i:s\Z'); } catch (Throwable $e) { $startIso = "{$startDate}T00:00:00Z"; $endIso = "{$endDate}T23:59:59Z"; } $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' => '100', ]); $response = fociz_http_request($url, [ 'method' => 'GET', 'headers' => [ 'Authorization' => "Bearer {$token}", 'Accept' => 'application/json', 'Prefer' => 'outlook.timezone="Europe/Amsterdam"', ], 'timeout' => 12, ]); // Retry bij 401 Unauthorized if ($response['status'] === 401) { $freshToken = microsoft_get_valid_token($userId, true); if ($freshToken && $freshToken !== $token) { $token = $freshToken; $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'])) { if ($response['status'] !== 200) { error_log("microsoft_fetch_events_range error (HTTP {$response['status']}): " . ($response['error'] ?? json_encode($response['json']))); } return []; } $rawEvents = $response['json']['value']; $events = []; foreach ($rawEvents as $e) { if (!empty($e['isCancelled'])) { continue; } $rawStart = $e['start']['dateTime'] ?? ''; $rawEnd = $e['end']['dateTime'] ?? ''; $eventDate = ''; $startTime = ''; $endTime = ''; if (!empty($rawStart)) { $eventDate = substr($rawStart, 0, 10); if (preg_match('/T(\d{2}:\d{2})/', $rawStart, $m)) { $startTime = $m[1]; } else { $startTime = date('H:i', strtotime($rawStart)); } } if (!empty($rawEnd)) { if (preg_match('/T(\d{2}:\d{2})/', $rawEnd, $m)) { $endTime = $m[1]; } else { $endTime = date('H:i', strtotime($rawEnd)); } } $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)', 'date' => $eventDate ?: $startDate, 'start' => $startTime ?: '09:00', 'end' => $endTime ?: '10:00', 'start_raw' => $rawStart, 'end_raw' => $rawEnd, 'location' => $locationName ?: 'Geen locatie', 'meeting_url' => $meetingUrl, 'web_link' => $e['webLink'] ?? '', 'is_all_day' => !empty($e['isAllDay']), 'source' => 'OUTLOOK_LIVE', ]; } return $events; } /** * 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'); return microsoft_fetch_events_range($userId, $targetDate, $targetDate); } /** * Maakt een nieuwe afspraak aan in de Microsoft Outlook agenda via Graph API * * @param int $userId * @param array $eventData [ * 'subject' => string, * 'description' => ?string, * 'date' => ?string (Y-m-d), * 'start_time' => ?string (H:i), * 'end_time' => ?string (H:i), * 'duration_minutes' => ?int, * ] * @return array ['success' => bool, 'event_id' => ?string, 'web_link' => ?string, 'error' => ?string] */ function microsoft_create_event(int $userId, array $eventData): array { $token = microsoft_get_valid_token($userId); if (!$token) { return ['success' => false, 'error' => 'Geen geldige Outlook 365 verbinding gevonden. Koppel uw account via Mijn Profiel.']; } $subject = trim($eventData['subject'] ?? 'Fociz Dagplanning Taak'); $description = trim($eventData['description'] ?? 'Gepland via Fociz Dagplanning & Structuur'); // Normaliseer datum naar strikt YYYY-MM-DD $date = !empty($eventData['date']) ? trim($eventData['date']) : date('Y-m-d'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { $date = date('Y-m-d', strtotime($date) ?: time()); } // Normaliseer starttijd naar strikt HH:mm met 2-cijferig uur $rawStart = !empty($eventData['start_time']) ? trim($eventData['start_time']) : '09:00'; if (preg_match('/^(\d{1,2})[:.](\d{2})(?::\d{2})?$/', $rawStart, $m)) { $startTime = sprintf('%02d:%02d', (int)$m[1], (int)$m[2]); } elseif (preg_match('/^(\d{1,2})$/', $rawStart, $m)) { $startTime = sprintf('%02d:00', (int)$m[1]); } else { $startTime = '09:00'; } // Duur $duration = (int)($eventData['duration_minutes'] ?? 30); if ($duration <= 0) $duration = 30; // Normaliseer eindtijd naar strikt HH:mm $rawEnd = !empty($eventData['end_time']) ? trim($eventData['end_time']) : ''; if (!empty($rawEnd) && preg_match('/^(\d{1,2})[:.](\d{2})(?::\d{2})?$/', $rawEnd, $m)) { $endTime = sprintf('%02d:%02d', (int)$m[1], (int)$m[2]); } else { try { $dt = new DateTime("{$date} {$startTime}:00", new DateTimeZone('Europe/Amsterdam')); $dt->modify("+{$duration} minutes"); $endTime = $dt->format('H:i'); } catch (Throwable $e) { $endTime = '10:00'; } } $startDateTimeStr = "{$date}T{$startTime}:00"; $endDateTimeStr = "{$date}T{$endTime}:00"; $payload = [ 'subject' => $subject, 'body' => [ 'contentType' => 'HTML', 'content' => nl2br(htmlspecialchars($description)) . '

Aangemaakt via Fociz Dagplanning', ], 'start' => [ 'dateTime' => $startDateTimeStr, 'timeZone' => 'Europe/Amsterdam', ], 'end' => [ 'dateTime' => $endDateTimeStr, 'timeZone' => 'Europe/Amsterdam', ], 'showAs' => 'busy', 'isReminderOn' => true, 'reminderMinutesBeforeStart' => 15, ]; $response = fociz_http_request(MS_GRAPH_BASE_URL . '/me/events', [ 'method' => 'POST', 'headers' => [ 'Authorization' => "Bearer {$token}", 'Content-Type' => 'application/json', 'Accept' => 'application/json', ], 'body' => $payload, 'timeout' => 15, ]); // Retry bij 401 Unauthorized (token verlopen) if ($response['status'] === 401) { $freshToken = microsoft_get_valid_token($userId, true); if ($freshToken && $freshToken !== $token) { $token = $freshToken; $response = fociz_http_request(MS_GRAPH_BASE_URL . '/me/events', [ 'method' => 'POST', 'headers' => [ 'Authorization' => "Bearer {$token}", 'Content-Type' => 'application/json', 'Accept' => 'application/json', ], 'body' => $payload, 'timeout' => 15, ]); } } if ($response['status'] === 201 && is_array($response['json'])) { return [ 'success' => true, 'event_id' => $response['json']['id'] ?? null, 'web_link' => $response['json']['webLink'] ?? null, 'start_time'=> $startTime, 'end_time' => $endTime, ]; } $errMsg = $response['json']['error']['message'] ?? ($response['error'] ?? "Fout bij aanmaken Outlook afspraak (HTTP {$response['status']})"); error_log("microsoft_create_event fout (user {$userId}): {$errMsg}"); return [ 'success' => false, 'error' => $errMsg, ]; } /** * Verwijdert een afspraak uit Microsoft Outlook via Graph API */ function microsoft_delete_event(int $userId, string $eventId): bool { if (empty($eventId)) { return false; } $token = microsoft_get_valid_token($userId); if (!$token) { return false; } $url = MS_GRAPH_BASE_URL . '/me/events/' . urlencode($eventId); $response = fociz_http_request($url, [ 'method' => 'DELETE', 'headers' => [ 'Authorization' => "Bearer {$token}", ], 'timeout' => 12, ]); return in_array($response['status'], [200, 204, 404]); } /** * Werkt een bestaande afspraak bij in Microsoft Outlook via Graph API (PATCH) */ function microsoft_update_event(int $userId, string $eventId, array $eventData): bool { if (empty($eventId)) { return false; } $token = microsoft_get_valid_token($userId); if (!$token) { return false; } $payload = []; if (!empty($eventData['subject'])) { $payload['subject'] = $eventData['subject']; } $date = !empty($eventData['date']) ? $eventData['date'] : date('Y-m-d'); $startTime = !empty($eventData['start_time']) ? $eventData['start_time'] : '09:00'; $endTime = !empty($eventData['end_time']) ? $eventData['end_time'] : '09:30'; if (preg_match('/^(\d{1,2})[:.](\d{2})$/', $startTime, $m)) { $startTime = sprintf('%02d:%02d', (int)$m[1], (int)$m[2]); } if (preg_match('/^(\d{1,2})[:.](\d{2})$/', $endTime, $m)) { $endTime = sprintf('%02d:%02d', (int)$m[1], (int)$m[2]); } $payload['start'] = [ 'dateTime' => "{$date}T{$startTime}:00", 'timeZone' => 'Europe/Amsterdam', ]; $payload['end'] = [ 'dateTime' => "{$date}T{$endTime}:00", 'timeZone' => 'Europe/Amsterdam', ]; $url = MS_GRAPH_BASE_URL . '/me/events/' . urlencode($eventId); $response = fociz_http_request($url, [ 'method' => 'PATCH', 'headers' => [ 'Authorization' => "Bearer {$token}", 'Content-Type' => 'application/json', 'Accept' => 'application/json', ], 'body' => $payload, 'timeout' => 12, ]); if ($response['status'] === 401) { $freshToken = microsoft_get_valid_token($userId, true); if ($freshToken && $freshToken !== $token) { $response = fociz_http_request($url, [ 'method' => 'PATCH', 'headers' => [ 'Authorization' => "Bearer {$freshToken}", 'Content-Type' => 'application/json', 'Accept' => 'application/json', ], 'body' => $payload, 'timeout' => 12, ]); } } return in_array($response['status'], [200, 204]); } /** * Ontkoppelt Microsoft integratie voor een gebruiker * Wist uitsluitend de persoonlijke tokens; behoudt de centrale Azure App gegevens indien aanwezig. */ function microsoft_disconnect(int $userId): bool { $db = get_db_connection(); $stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'"); $stmt->execute([$userId]); $integration = $stmt->fetch(); if (!$integration) { return true; } $settings = json_decode($integration['settings'] ?? '{}', true) ?: []; // Verwijder gebruikers-specifieke token gegevens unset($settings['account_email'], $settings['account_name'], $settings['expires_at']); // Als er centrale app credentials in staan (bijv. van beheerder), behouden we deze netjes if (!empty($settings['client_id']) || !empty($settings['client_secret'])) { $updateStmt = $db->prepare(" UPDATE integrations SET access_token = NULL, refresh_token = NULL, settings = ?, last_sync_at = NULL, is_enabled = 1, is_demo_mode = 0 WHERE id = ? "); return $updateStmt->execute([json_encode($settings), $integration['id']]); } else { $delStmt = $db->prepare("DELETE FROM integrations WHERE id = ?"); return $delStmt->execute([$integration['id']]); } } /** * Haalt één specifieke afspraak op uit Microsoft Outlook via Graph API. * Retourneert: * - array met event data indien gevonden (HTTP 200) * - false indien definitief verwijderd / geannuleerd (HTTP 404 of isCancelled) * - null indien er een netwerk/server error optreedt (beschermt taken tegen onterecht wissen) */ function microsoft_get_single_event(int $userId, string $eventId): false|array|null { if (empty($eventId)) { return false; } $token = microsoft_get_valid_token($userId); if (!$token) { return null; } $url = MS_GRAPH_BASE_URL . '/me/events/' . urlencode($eventId) . '?' . http_build_query([ '$select' => 'id,subject,start,end,isCancelled', ]); $response = fociz_http_request($url, [ 'method' => 'GET', 'headers' => [ 'Authorization' => "Bearer {$token}", 'Accept' => 'application/json', 'Prefer' => 'outlook.timezone="Europe/Amsterdam"', ], 'timeout' => 8, ]); if ($response['status'] === 401) { $freshToken = microsoft_get_valid_token($userId, true); if ($freshToken && $freshToken !== $token) { $token = $freshToken; $response = fociz_http_request($url, [ 'method' => 'GET', 'headers' => [ 'Authorization' => "Bearer {$token}", 'Accept' => 'application/json', 'Prefer' => 'outlook.timezone="Europe/Amsterdam"', ], 'timeout' => 8, ]); } } if ($response['status'] === 404) { return false; // Definitief niet meer gevonden in Outlook } if ($response['status'] === 200 && is_array($response['json'])) { $e = $response['json']; if (!empty($e['isCancelled'])) { return false; // Geannuleerd } $rawStart = $e['start']['dateTime'] ?? ''; $rawEnd = $e['end']['dateTime'] ?? ''; $eventDate = !empty($rawStart) ? substr($rawStart, 0, 10) : ''; $startTime = ''; $endTime = ''; if (!empty($rawStart) && preg_match('/T(\d{2}:\d{2})/', $rawStart, $m)) { $startTime = $m[1]; } if (!empty($rawEnd) && preg_match('/T(\d{2}:\d{2})/', $rawEnd, $m)) { $endTime = $m[1]; } return [ 'id' => $e['id'], 'subject' => !empty($e['subject']) ? $e['subject'] : '(Geen onderwerp)', 'date' => $eventDate, 'start' => $startTime ?: '09:00', 'end' => $endTime ?: '10:00', 'start_raw' => $rawStart, 'end_raw' => $rawEnd, ]; } return null; } /** * Synchroniseert en reconciled Fociz taken met de live Microsoft Outlook agenda. * * - Als een gekoppelde afspraak in Outlook is verwijderd of geannuleerd, wordt de taak in Fociz gewist. * - Als een afspraak in Outlook van tijd, datum of titel is gewijzigd, wordt de taak in Fociz bijgewerkt. * * @param int $userId * @param string $startDate Datumbereik begin (Y-m-d) * @param string $endDate Datumbereik einde (Y-m-d) * @param array $liveEvents Reeds opgehaalde Outlook events via microsoft_fetch_events_range * @return array ['deleted' => int, 'updated' => int] */ function microsoft_reconcile_tasks_with_outlook(int $userId, string $startDate, string $endDate, array $liveEvents): array { $db = get_db_connection(); $deletedCount = 0; $updatedCount = 0; // 1. Indexeer live events op id $liveEventMap = []; foreach ($liveEvents as $evt) { if (!empty($evt['id'])) { $liveEventMap[$evt['id']] = $evt; } } // 2. Haal alle taken op voor deze gebruiker in dit datumbereik met een external_id (gekoppeld aan Outlook) $stmt = $db->prepare(" SELECT t.id, t.title, t.time_block_start, t.time_block_end, t.duration_minutes, t.external_id, dp.plan_date, dp.id as daily_plan_id FROM tasks t JOIN daily_plans dp ON t.daily_plan_id = dp.id WHERE t.user_id = ? AND t.source = 'OUTLOOK' AND t.external_id IS NOT NULL AND t.external_id != '' AND dp.plan_date BETWEEN ? AND ? "); $stmt->execute([$userId, $startDate, $endDate]); $syncedTasks = $stmt->fetchAll(); foreach ($syncedTasks as $task) { $extId = $task['external_id']; if (isset($liveEventMap[$extId])) { // Het event bestaat in het huidige week/datumbereik! $evt = $liveEventMap[$extId]; $dateChanged = (!empty($evt['date']) && $evt['date'] !== $task['plan_date']); $titleChanged = (!empty($evt['subject']) && $evt['subject'] !== $task['title']); $startChanged = (!empty($evt['start']) && $evt['start'] !== $task['time_block_start']); $endChanged = (!empty($evt['end']) && $evt['end'] !== $task['time_block_end']); if ($dateChanged || $titleChanged || $startChanged || $endChanged) { $targetPlanId = (int)$task['daily_plan_id']; // Als datum gewijzigd is, verplaats naar de daily_plan van de nieuwe datum if ($dateChanged) { $stmtPlan = $db->prepare(" INSERT INTO daily_plans (user_id, plan_date) VALUES (?, ?) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id) "); $stmtPlan->execute([$userId, $evt['date']]); $targetPlanId = (int)$db->lastInsertId(); } // Bereken duur $newStart = !empty($evt['start']) ? $evt['start'] : $task['time_block_start']; $newEnd = !empty($evt['end']) ? $evt['end'] : $task['time_block_end']; $newDuration = (int)$task['duration_minutes']; if (!empty($newStart) && !empty($newEnd)) { $durCalc = (strtotime("2000-01-01 {$newEnd}") - strtotime("2000-01-01 {$newStart}")) / 60; if ($durCalc > 0) { $newDuration = (int)$durCalc; } } $newTitle = !empty($evt['subject']) ? $evt['subject'] : $task['title']; $updateStmt = $db->prepare(" UPDATE tasks SET daily_plan_id = ?, title = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ? WHERE id = ? AND user_id = ? "); $updateStmt->execute([$targetPlanId, $newTitle, $newStart, $newEnd, $newDuration, $task['id'], $userId]); $updatedCount++; } } else { // Event niet gevonden in het weekbereik! // Controleer specifiek of het event in Outlook gewist is of naar een andere week verplaatst $singleEvent = microsoft_get_single_event($userId, $extId); if ($singleEvent === false) { // Event is definitief verwijderd of geannuleerd in Outlook! $delStmt = $db->prepare("DELETE FROM tasks WHERE id = ? AND user_id = ?"); $delStmt->execute([$task['id'], $userId]); $deletedCount++; } elseif (is_array($singleEvent)) { // Event bestaat nog, maar staat op een andere datum (buiten huidig bereik) $newDate = $singleEvent['date']; if (!empty($newDate)) { $stmtPlan = $db->prepare(" INSERT INTO daily_plans (user_id, plan_date) VALUES (?, ?) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id) "); $stmtPlan->execute([$userId, $newDate]); $newPlanId = (int)$db->lastInsertId(); $newStart = $singleEvent['start']; $newEnd = $singleEvent['end']; $newDuration = (int)$task['duration_minutes']; if (!empty($newStart) && !empty($newEnd)) { $durCalc = (strtotime("2000-01-01 {$newEnd}") - strtotime("2000-01-01 {$newStart}")) / 60; if ($durCalc > 0) $newDuration = (int)$durCalc; } $updateStmt = $db->prepare(" UPDATE tasks SET daily_plan_id = ?, title = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ? WHERE id = ? AND user_id = ? "); $updateStmt->execute([$newPlanId, $singleEvent['subject'], $newStart, $newEnd, $newDuration, $task['id'], $userId]); $updatedCount++; } } } } return [ 'deleted' => $deletedCount, 'updated' => $updatedCount, ]; }