diff --git a/api/planner.php b/api/planner.php index d2e4b5e..889ca81 100644 --- a/api/planner.php +++ b/api/planner.php @@ -252,6 +252,114 @@ try { exit; } + // 6. Realtime Outlook synchronisatie & controle op gewijzigde/gewiste items + if ($action === 'check_outlook_sync') { + $rawInput = file_get_contents('php://input'); + $data = !empty($rawInput) ? (json_decode($rawInput, true) ?: []) : []; + if (empty($data)) { + $data = $_GET; + } + + $planDate = !empty($data['plan_date']) ? trim($data['plan_date']) : $today; + $weekStart = !empty($data['week_start']) ? trim($data['week_start']) : date('Y-m-d', strtotime('monday this week', strtotime($planDate))); + $weekEnd = !empty($data['week_end']) ? trim($data['week_end']) : date('Y-m-d', strtotime('sunday this week', strtotime($planDate))); + $lastHash = !empty($data['last_hash']) ? trim($data['last_hash']) : ''; + + // Controleer of Outlook gekoppeld is + $stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1"); + $stmtInt->execute([$user['id']]); + $integration = $stmtInt->fetch(); + + if (!$integration || empty($integration['access_token']) || !empty($integration['is_demo_mode'])) { + echo json_encode([ + 'success' => true, + 'connected' => false, + 'has_changes' => false, + 'last_sync' => date('H:i:s'), + ]); + exit; + } + + // Haal live afspraken op voor de hele week + $weekLiveEvents = microsoft_fetch_events_range($user['id'], $weekStart, $weekEnd); + + // Voer reconciliatie uit met de Fociz tasks tabel + $reconcileRes = microsoft_reconcile_tasks_with_outlook($user['id'], $weekStart, $weekEnd, $weekLiveEvents); + + // Haal de geactualiseerde taken voor deze dag op + $stmtPlan = $db->prepare("SELECT id FROM daily_plans WHERE user_id = ? AND plan_date = ?"); + $stmtPlan->execute([$user['id'], $planDate]); + $planId = $stmtPlan->fetchColumn(); + + $tasks = []; + if ($planId) { + $stmtTasks = $db->prepare(" + SELECT id, title, priority, time_block_start, time_block_end, duration_minutes, is_completed, status, source, external_id + FROM tasks + WHERE user_id = ? AND (daily_plan_id = ? " . ($planDate === $today ? "OR daily_plan_id IS NULL" : "") . ") + ORDER BY is_completed ASC, priority ASC, time_block_start ASC, created_at DESC + "); + $stmtTasks->execute([$user['id'], $planId]); + $tasks = $stmtTasks->fetchAll(); + } + + // Filter events voor de geselecteerde dag die geen gesynchroniseerde Fociz taak zijn + $syncedIds = []; + foreach ($tasks as $t) { + if (!empty($t['external_id'])) { + $syncedIds[$t['external_id']] = true; + } + } + + $dayEvents = []; + foreach ($weekLiveEvents as $e) { + if (($e['date'] ?? '') === $planDate) { + if (!empty($e['id']) && isset($syncedIds[$e['id']])) { + continue; + } + $dayEvents[] = [ + 'id' => $e['id'], + 'start' => $e['start'], + 'end' => $e['end'], + 'subject' => $e['subject'], + ]; + } + } + + // Bereken status hash voor de dag + $hashData = [ + 'events' => $dayEvents, + 'tasks' => array_map(function($t) { + return [ + 'id' => $t['id'], + 'title' => $t['title'], + 'time_block_start' => $t['time_block_start'], + 'time_block_end' => $t['time_block_end'], + 'is_completed' => $t['is_completed'], + ]; + }, $tasks) + ]; + $currentHash = md5(json_encode($hashData)); + + $hasChanges = ($reconcileRes['deleted'] > 0 || $reconcileRes['updated'] > 0); + if (!empty($lastHash) && $lastHash !== $currentHash) { + $hasChanges = true; + } + + echo json_encode([ + 'success' => true, + 'connected' => true, + 'has_changes' => $hasChanges, + 'hash' => $currentHash, + 'deleted_count' => $reconcileRes['deleted'], + 'updated_count' => $reconcileRes['updated'], + 'events_count' => count($dayEvents), + 'tasks_count' => count($tasks), + 'last_sync' => date('H:i:s'), + ]); + exit; + } + echo json_encode(['error' => 'Onbekende actie']); } catch (Exception $e) { http_response_code(500); diff --git a/includes/integrations/microsoft.php b/includes/integrations/microsoft.php index 4234b7a..99165f6 100644 --- a/includes/integrations/microsoft.php +++ b/includes/integrations/microsoft.php @@ -594,3 +594,223 @@ function microsoft_disconnect(int $userId): bool { } } +/** + * 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, + ]; +} + + diff --git a/planner.php b/planner.php index d8043a1..bf84da4 100644 --- a/planner.php +++ b/planner.php @@ -42,49 +42,7 @@ $dutchShortMonths = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', $formattedSelectedDate = $dutchFullDays[(int)date('N', $selectedTs)] . ' ' . date('j', $selectedTs) . ' ' . $dutchMonths[(int)date('n', $selectedTs)] . ' ' . date('Y', $selectedTs); $formattedWeekRange = date('j', $mondayTs) . ' ' . $dutchShortMonths[(int)date('n', $mondayTs)] . ' – ' . date('j', $sundayTs) . ' ' . $dutchShortMonths[(int)date('n', $sundayTs)] . ' ' . date('Y', $sundayTs); -// 1. Haal dagplanning op of maak aan voor $selectedDate -$stmtPlan = $db->prepare("SELECT * FROM daily_plans WHERE user_id = ? AND plan_date = ?"); -$stmtPlan->execute([$userId, $selectedDate]); -$dailyPlan = $stmtPlan->fetch(); - -if (!$dailyPlan) { - $defaultGoal = ($selectedDate === $today) ? 'Mijn belangrijkste doel van vandaag behalen' : 'Focusdoel voor deze dag bepalen'; - $stmtCreatePlan = $db->prepare(" - INSERT INTO daily_plans (user_id, plan_date, focus_goal) - VALUES (?, ?, ?) - "); - $stmtCreatePlan->execute([$userId, $selectedDate, $defaultGoal]); - $planId = (int)$db->lastInsertId(); - $dailyPlan = ['id' => $planId, 'plan_date' => $selectedDate, 'focus_goal' => $defaultGoal, 'notes' => '']; -} - -// 2. Haal taken op voor de geselecteerde dag -$stmtTasks = $db->prepare(" - SELECT * FROM tasks - WHERE user_id = ? AND (daily_plan_id = ? " . ($selectedDate === $today ? "OR daily_plan_id IS NULL" : "") . ") - ORDER BY is_completed ASC, priority ASC, time_block_start ASC, created_at DESC -"); -$stmtTasks->execute([$userId, $dailyPlan['id']]); -$tasks = $stmtTasks->fetchAll(); - -// 3. Haal weekstatistieken voor taken op (aantal per dag) -$stmtWeekTasks = $db->prepare(" - SELECT dp.plan_date, COUNT(t.id) as total_tasks, SUM(CASE WHEN t.is_completed = 1 THEN 1 ELSE 0 END) as completed_tasks - FROM daily_plans dp - LEFT JOIN tasks t ON t.daily_plan_id = dp.id AND t.user_id = ? - WHERE dp.user_id = ? AND dp.plan_date BETWEEN ? AND ? - GROUP BY dp.plan_date -"); -$stmtWeekTasks->execute([$userId, $userId, $weekStart, $weekEnd]); -$weekTaskStats = []; -foreach ($stmtWeekTasks->fetchAll() as $row) { - $weekTaskStats[$row['plan_date']] = [ - 'total' => (int)$row['total_tasks'], - 'completed' => (int)$row['completed_tasks'], - ]; -} - -// 4. Haal integratiestatussen op +// 1. Haal integratiestatussen op $stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ?"); $stmtInt->execute([$userId]); $userIntegrations = []; @@ -93,6 +51,9 @@ foreach ($stmtInt->fetchAll() as $row) { } $isOutlookConnected = !empty($userIntegrations['OUTLOOK']['access_token']) && empty($userIntegrations['OUTLOOK']['is_demo_mode']); +$outlookIntegrationSettings = json_decode($userIntegrations['OUTLOOK']['settings'] ?? '{}', true) ?: []; +$syncIntervalSeconds = isset($outlookIntegrationSettings['sync_interval_seconds']) ? (int)$outlookIntegrationSettings['sync_interval_seconds'] : 30; + $weekOutlookEvents = []; $isLiveOutlook = false; @@ -100,6 +61,9 @@ if ($isOutlookConnected) { // Haal afspraken op voor het volledige datumbereik van de week $weekOutlookEvents = microsoft_fetch_events_range($userId, $weekStart, $weekEnd); $isLiveOutlook = true; + + // Synchroniseer / reconcilieer Fociz taken direct met actuele Outlook data + microsoft_reconcile_tasks_with_outlook($userId, $weekStart, $weekEnd, $weekOutlookEvents); } else { // Demo data indien Microsoft Outlook nog niet gekoppeld is $weekOutlookEvents = [ @@ -136,6 +100,48 @@ if ($isOutlookConnected) { ]; } +// 2. Haal dagplanning op of maak aan voor $selectedDate +$stmtPlan = $db->prepare("SELECT * FROM daily_plans WHERE user_id = ? AND plan_date = ?"); +$stmtPlan->execute([$userId, $selectedDate]); +$dailyPlan = $stmtPlan->fetch(); + +if (!$dailyPlan) { + $defaultGoal = ($selectedDate === $today) ? 'Mijn belangrijkste doel van vandaag behalen' : 'Focusdoel voor deze dag bepalen'; + $stmtCreatePlan = $db->prepare(" + INSERT INTO daily_plans (user_id, plan_date, focus_goal) + VALUES (?, ?, ?) + "); + $stmtCreatePlan->execute([$userId, $selectedDate, $defaultGoal]); + $planId = (int)$db->lastInsertId(); + $dailyPlan = ['id' => $planId, 'plan_date' => $selectedDate, 'focus_goal' => $defaultGoal, 'notes' => '']; +} + +// 3. Haal taken op voor de geselecteerde dag (vers bijgewerkt na eventuele reconciliatie) +$stmtTasks = $db->prepare(" + SELECT * FROM tasks + WHERE user_id = ? AND (daily_plan_id = ? " . ($selectedDate === $today ? "OR daily_plan_id IS NULL" : "") . ") + ORDER BY is_completed ASC, priority ASC, time_block_start ASC, created_at DESC +"); +$stmtTasks->execute([$userId, $dailyPlan['id']]); +$tasks = $stmtTasks->fetchAll(); + +// 4. Haal weekstatistieken voor taken op (aantal per dag) +$stmtWeekTasks = $db->prepare(" + SELECT dp.plan_date, COUNT(t.id) as total_tasks, SUM(CASE WHEN t.is_completed = 1 THEN 1 ELSE 0 END) as completed_tasks + FROM daily_plans dp + LEFT JOIN tasks t ON t.daily_plan_id = dp.id AND t.user_id = ? + WHERE dp.user_id = ? AND dp.plan_date BETWEEN ? AND ? + GROUP BY dp.plan_date +"); +$stmtWeekTasks->execute([$userId, $userId, $weekStart, $weekEnd]); +$weekTaskStats = []; +foreach ($stmtWeekTasks->fetchAll() as $row) { + $weekTaskStats[$row['plan_date']] = [ + 'total' => (int)$row['total_tasks'], + 'completed' => (int)$row['completed_tasks'], + ]; +} + // Groepeer Outlook afspraken per datum $outlookEventsByDate = []; foreach ($weekOutlookEvents as $evt) { @@ -188,6 +194,30 @@ foreach ($dayOutlookEvents as $evt) { $displayedOutlookEvents[] = $evt; } +// Bereken initiële status-hash voor achtergrondsynchronisatie +$dayEventsForHash = []; +foreach ($displayedOutlookEvents as $e) { + $dayEventsForHash[] = [ + 'id' => $e['id'] ?? '', + 'start' => $e['start'] ?? '', + 'end' => $e['end'] ?? '', + 'subject' => $e['subject'] ?? '', + ]; +} +$tasksForHash = array_map(function($t) { + return [ + 'id' => $t['id'], + 'title' => $t['title'], + 'time_block_start' => $t['time_block_start'], + 'time_block_end' => $t['time_block_end'], + 'is_completed' => $t['is_completed'], + ]; +}, $tasks); +$initialSyncHash = md5(json_encode([ + 'events' => $dayEventsForHash, + 'tasks' => $tasksForHash +])); + // 6. Tijdlijn uren & Werkdag Standaarden (Release v1.4) $workdayStartStr = $workdaySettings['workday_start'] ?? '08:30'; $workdayEndStr = $workdaySettings['workday_end'] ?? '17:30'; @@ -250,11 +280,12 @@ for ($h = $startHour; $h <= $endHour; $h++) {