prepare(" INSERT INTO daily_plans (user_id, plan_date, focus_goal, notes) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE focus_goal = VALUES(focus_goal), notes = VALUES(notes) "); $stmt->execute([$user['id'], $date, $focusGoal, $notes]); echo json_encode(['success' => true]); exit; } // 1b. Widget layout opslaan if ($action === 'save_widget_layout') { $data = json_decode(file_get_contents('php://input'), true); $layout = $data['layout'] ?? null; if (!is_array($layout)) { echo json_encode(['success' => false, 'error' => 'Ongeldig layout formaat']); exit; } // Valideer en sanitize layout items inclusief optionele custom widget titel $cleanedLayout = []; foreach ($layout as $item) { if (!is_array($item) || empty($item['id'])) continue; $cleaned = [ 'id' => substr(trim((string)$item['id']), 0, 64), 'enabled' => !empty($item['enabled']), 'col_span' => in_array((int)($item['col_span'] ?? 6), [4, 5, 6, 7, 8, 12]) ? (int)$item['col_span'] : 6, 'order' => (int)($item['order'] ?? 1), ]; if (!empty($item['custom_title'])) { $customTitle = trim(strip_tags((string)$item['custom_title'])); if ($customTitle !== '') { $cleaned['custom_title'] = substr($customTitle, 0, 50); } } $cleanedLayout[] = $cleaned; } $stmt = $db->prepare("UPDATE users SET fociz_widget_layout = ? WHERE id = ?"); $stmt->execute([json_encode($cleanedLayout, JSON_UNESCAPED_UNICODE), $user['id']]); echo json_encode(['success' => true, 'message' => 'Widget indeling succesvol opgeslagen']); exit; } // 1c. Scratchpad snelle notities opslaan if ($action === 'save_scratchpad') { $data = json_decode(file_get_contents('php://input'), true); $date = $data['date'] ?? $today; $notes = $data['notes'] ?? ''; $stmt = $db->prepare(" INSERT INTO daily_plans (user_id, plan_date, notes) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE notes = VALUES(notes) "); $stmt->execute([$user['id'], $date, $notes]); echo json_encode(['success' => true]); exit; } // 2. Taak toevoegen if ($action === 'add_task') { $data = json_decode(file_get_contents('php://input'), true); $title = trim($data['title'] ?? ''); $priority = in_array($data['priority'] ?? '', ['P1', 'P2', 'P3']) ? $data['priority'] : 'P2'; $timeBlockStart = !empty($data['time_block_start']) ? trim($data['time_block_start']) : null; $timeBlockEnd = !empty($data['time_block_end']) ? trim($data['time_block_end']) : null; $durationMinutes = (int)($data['duration_minutes'] ?? 0); $date = !empty($data['date']) ? trim($data['date']) : $today; $syncOutlook = !empty($data['sync_to_outlook']); if (empty($title)) { echo json_encode(['error' => 'Titel is verplicht']); exit; } // Als zowel start- als eindtijd zijn opgegeven, bereken exact de minuten if (!empty($timeBlockStart) && !empty($timeBlockEnd)) { $t1 = strtotime("2000-01-01 {$timeBlockStart}"); $t2 = strtotime("2000-01-01 {$timeBlockEnd}"); if ($t1 !== false && $t2 !== false && $t2 > $t1) { $durationMinutes = (int)(($t2 - $t1) / 60); } } if ($durationMinutes <= 0) { $durationMinutes = 30; } // Als starttijd bekend is maar eindtijd nog niet, bereken eindtijd if (!empty($timeBlockStart) && empty($timeBlockEnd)) { try { $dtEnd = new DateTime("{$date} {$timeBlockStart}:00", new DateTimeZone('Europe/Amsterdam')); $dtEnd->modify("+{$durationMinutes} minutes"); $timeBlockEnd = $dtEnd->format('H:i'); } catch (Throwable $e) { // negeer } } // Haal of maak daily plan op $stmtPlan = $db->prepare(" INSERT INTO daily_plans (user_id, plan_date) VALUES (?, ?) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id) "); $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, 255) : null; $externalUrl = !empty($data['external_url']) ? trim($data['external_url']) : null; $outlookSyncError = null; // Als synchronisatie naar Outlook gewenst is (of source is OUTLOOK) en nog geen extern ID heeft if (($syncOutlook || $source === 'OUTLOOK') && empty($externalId)) { // Als er geen starttijd is ingevuld maar wel sync gewenst is, gebruik 09:00 als veilige standaard if (empty($timeBlockStart)) { $timeBlockStart = '09:00'; } $msResult = microsoft_create_event($user['id'], [ 'subject' => $title, 'description' => "Gepland via Fociz Dagplanning ({$priority})", 'date' => $date, 'start_time' => $timeBlockStart, 'end_time' => $timeBlockEnd, 'duration_minutes' => $durationMinutes, ]); if (!empty($msResult['success']) && !empty($msResult['event_id'])) { $source = 'OUTLOOK'; $externalId = $msResult['event_id']; $externalUrl = $msResult['web_link'] ?? 'https://outlook.office.com'; $timeBlockStart = $msResult['start_time'] ?? $timeBlockStart; $timeBlockEnd = $msResult['end_time'] ?? $timeBlockEnd; } else { $outlookSyncError = $msResult['error'] ?? 'Kon afspraak niet aanmaken in Microsoft Outlook'; } } $stmt = $db->prepare(" INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, time_block_end, duration_minutes, status, source, external_id, external_url) VALUES (?, ?, ?, ?, ?, ?, ?, 'TODO', ?, ?, ?) "); $stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $source, $externalId, $externalUrl]); $taskId = (int)$db->lastInsertId(); echo json_encode([ 'success' => true, 'task' => [ 'id' => $taskId, 'title' => $title, 'priority' => $priority, 'time_block_start' => $timeBlockStart, 'time_block_end' => $timeBlockEnd, 'duration_minutes' => $durationMinutes, 'is_completed' => 0, 'status' => 'TODO', 'source' => $source, 'external_id' => $externalId, 'external_url' => $externalUrl, ], 'outlook_synced' => ($source === 'OUTLOOK' && !empty($externalId)), 'outlook_error' => $outlookSyncError, ]); exit; } // 2b. Taak bewerken (titel, prioriteit, starttijd, eindtijd, duur) if ($action === 'update_task') { $data = json_decode(file_get_contents('php://input'), true); $taskId = (int)($data['task_id'] ?? 0); $title = trim($data['title'] ?? ''); $priority = in_array($data['priority'] ?? '', ['P1', 'P2', 'P3']) ? $data['priority'] : 'P2'; $timeBlockStart = !empty($data['time_block_start']) ? trim($data['time_block_start']) : null; $timeBlockEnd = !empty($data['time_block_end']) ? trim($data['time_block_end']) : null; $durationMinutes = (int)($data['duration_minutes'] ?? 0); if (empty($title)) { echo json_encode(['error' => 'Titel mag niet leeg zijn']); exit; } // Haal bestaande taak op $stmtTask = $db->prepare("SELECT * FROM tasks WHERE id = ? AND user_id = ?"); $stmtTask->execute([$taskId, $user['id']]); $existingTask = $stmtTask->fetch(); if (!$existingTask) { echo json_encode(['error' => 'Taak niet gevonden']); exit; } if (!empty($timeBlockStart) && !empty($timeBlockEnd)) { $t1 = strtotime("2000-01-01 {$timeBlockStart}"); $t2 = strtotime("2000-01-01 {$timeBlockEnd}"); if ($t1 !== false && $t2 !== false && $t2 > $t1) { $durationMinutes = (int)(($t2 - $t1) / 60); } } if ($durationMinutes <= 0) { $durationMinutes = (int)($existingTask['duration_minutes'] ?: 30); } if (!empty($timeBlockStart) && empty($timeBlockEnd)) { try { $dtEnd = new DateTime("2000-01-01 {$timeBlockStart}:00", new DateTimeZone('Europe/Amsterdam')); $dtEnd->modify("+{$durationMinutes} minutes"); $timeBlockEnd = $dtEnd->format('H:i'); } catch (Throwable $e) {} } $targetDate = !empty($data['date']) ? trim($data['date']) : null; $planDate = null; $newPlanId = null; if ($targetDate && preg_match('/^\d{4}-\d{2}-\d{2}$/', $targetDate)) { $stmtPlan = $db->prepare(" INSERT INTO daily_plans (user_id, plan_date) VALUES (?, ?) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id) "); $stmtPlan->execute([$user['id'], $targetDate]); $newPlanId = (int)$db->lastInsertId(); $planDate = $targetDate; } else { $stmtPlan = $db->prepare("SELECT plan_date FROM daily_plans WHERE id = ?"); $stmtPlan->execute([$existingTask['daily_plan_id']]); $planDate = $stmtPlan->fetchColumn() ?: date('Y-m-d'); } // Indien gekoppeld aan Outlook, werk afspraak bij via Graph API if (!empty($existingTask['external_id']) && $existingTask['source'] === 'OUTLOOK') { try { microsoft_update_event($user['id'], $existingTask['external_id'], [ 'subject' => $title, 'date' => $planDate, 'start_time' => $timeBlockStart ?: '09:00', 'end_time' => $timeBlockEnd ?: '09:30', ]); } catch (Throwable $t) { error_log("Fout bij bijwerken Outlook event: " . $t->getMessage()); } } if ($newPlanId) { $stmtUpdate = $db->prepare(" UPDATE tasks SET title = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ?, daily_plan_id = ? WHERE id = ? AND user_id = ? "); $stmtUpdate->execute([$title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $newPlanId, $taskId, $user['id']]); } else { $stmtUpdate = $db->prepare(" UPDATE tasks SET title = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ? WHERE id = ? AND user_id = ? "); $stmtUpdate->execute([$title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $taskId, $user['id']]); } echo json_encode([ 'success' => true, 'time_block_start' => $timeBlockStart, 'time_block_end' => $timeBlockEnd, 'duration_minutes' => $durationMinutes, 'date' => $planDate ]); exit; } // 3. Bestaande taak handmatig naar Microsoft Outlook synchroniseren if ($action === 'sync_task_to_outlook') { $data = json_decode(file_get_contents('php://input'), true); $taskId = (int)($data['task_id'] ?? 0); if (!$taskId) { echo json_encode(['error' => 'Geen taak-ID opgegeven']); exit; } // Haal taak inclusief plan datum op $stmtTask = $db->prepare(" SELECT t.*, dp.plan_date FROM tasks t LEFT JOIN daily_plans dp ON t.daily_plan_id = dp.id WHERE t.id = ? AND t.user_id = ? "); $stmtTask->execute([$taskId, $user['id']]); $task = $stmtTask->fetch(); if (!$task) { echo json_encode(['error' => 'Taak niet gevonden']); exit; } // Als al gesynchroniseerd if (!empty($task['external_id']) && $task['source'] === 'OUTLOOK') { echo json_encode([ 'success' => true, 'already_synced' => true, 'external_id' => $task['external_id'], 'external_url' => $task['external_url'] ]); exit; } $planDate = $task['plan_date'] ?: $today; $startTime = !empty($task['time_block_start']) ? $task['time_block_start'] : ($data['time_block_start'] ?? '09:00'); $endTime = !empty($task['time_block_end']) ? $task['time_block_end'] : null; $duration = (int)($task['duration_minutes'] ?: 30); $msResult = microsoft_create_event($user['id'], [ 'subject' => $task['title'], 'description' => "Gepland via Fociz Dagplanning (" . $task['priority'] . ")", 'date' => $planDate, 'start_time' => $startTime, 'end_time' => $endTime, 'duration_minutes' => $duration, ]); if (empty($msResult['success']) || empty($msResult['event_id'])) { echo json_encode(['error' => $msResult['error'] ?? 'Kon afspraak niet aanmaken in Microsoft Outlook']); exit; } // Sla Outlook ID en bron op in de taak $stmtUp = $db->prepare(" UPDATE tasks SET source = 'OUTLOOK', external_id = ?, external_url = ?, time_block_start = COALESCE(time_block_start, ?), time_block_end = ? WHERE id = ? AND user_id = ? "); $stmtUp->execute([ $msResult['event_id'], $msResult['web_link'] ?? 'https://outlook.office.com', $msResult['start_time'], $msResult['end_time'], $taskId, $user['id'] ]); echo json_encode([ 'success' => true, 'external_id' => $msResult['event_id'], 'external_url' => $msResult['web_link'] ?? 'https://outlook.office.com', 'start_time' => $msResult['start_time'], 'end_time' => $msResult['end_time'], ]); exit; } // 4. Taak status toggelen (voltooid / open) if ($action === 'toggle_task') { $data = json_decode(file_get_contents('php://input'), true); $taskId = (int)($data['task_id'] ?? 0); $isCompleted = !empty($data['is_completed']) ? 1 : 0; $status = $isCompleted ? 'DONE' : 'TODO'; $stmt = $db->prepare(" UPDATE tasks SET is_completed = ?, status = ? WHERE id = ? AND user_id = ? "); $stmt->execute([$isCompleted, $status, $taskId, $user['id']]); echo json_encode(['success' => true]); exit; } // 5. Taak verwijderen (verwijder ook eventueel gekoppelde Outlook afspraak) if ($action === 'delete_task') { $data = json_decode(file_get_contents('php://input'), true); $taskId = (int)($data['task_id'] ?? 0); // Haal eerst op of deze taak aan Outlook was gekoppeld $stmtCheck = $db->prepare("SELECT external_id, source FROM tasks WHERE id = ? AND user_id = ?"); $stmtCheck->execute([$taskId, $user['id']]); $taskToDelete = $stmtCheck->fetch(); if ($taskToDelete && !empty($taskToDelete['external_id']) && $taskToDelete['source'] === 'OUTLOOK') { try { microsoft_delete_event($user['id'], $taskToDelete['external_id']); } catch (Throwable $t) { error_log("Fout bij verwijderen Outlook afspraak voor taak {$taskId}: " . $t->getMessage()); } } $stmt = $db->prepare("DELETE FROM tasks WHERE id = ? AND user_id = ?"); $stmt->execute([$taskId, $user['id']]); echo json_encode(['success' => true]); 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; } // --- V2.0: Subtaken & Checklists --- // 7. Subtaak toevoegen aan een taak if ($action === 'add_subtask') { $data = json_decode(file_get_contents('php://input'), true); $taskId = (int)($data['task_id'] ?? 0); $title = trim($data['title'] ?? ''); if (!$taskId || empty($title)) { echo json_encode(['error' => 'Taak-ID en titel zijn verplicht']); exit; } // Controleer of de taak van deze gebruiker is $stmtCheck = $db->prepare("SELECT id FROM tasks WHERE id = ? AND user_id = ?"); $stmtCheck->execute([$taskId, $user['id']]); if (!$stmtCheck->fetchColumn()) { echo json_encode(['error' => 'Taak niet gevonden']); exit; } // Bereken positie (achteraan) $stmtPos = $db->prepare("SELECT COALESCE(MAX(position), -1) + 1 FROM task_subtasks WHERE task_id = ?"); $stmtPos->execute([$taskId]); $position = (int)$stmtPos->fetchColumn(); $stmt = $db->prepare(" INSERT INTO task_subtasks (task_id, user_id, title, is_completed, position) VALUES (?, ?, ?, 0, ?) "); $stmt->execute([$taskId, $user['id'], substr($title, 0, 255), $position]); $subtaskId = (int)$db->lastInsertId(); echo json_encode([ 'success' => true, 'subtask' => [ 'id' => $subtaskId, 'task_id' => $taskId, 'title' => $title, 'is_completed' => 0, 'position' => $position, ] ]); exit; } // 8. Subtaak afvinken / openzetten if ($action === 'toggle_subtask') { $data = json_decode(file_get_contents('php://input'), true); $subtaskId = (int)($data['subtask_id'] ?? 0); $isCompleted = !empty($data['is_completed']) ? 1 : 0; if (!$subtaskId) { echo json_encode(['error' => 'Subtaak-ID ontbreekt']); exit; } $stmt = $db->prepare(" UPDATE task_subtasks SET is_completed = ? WHERE id = ? AND user_id = ? "); $stmt->execute([$isCompleted, $subtaskId, $user['id']]); echo json_encode(['success' => true, 'is_completed' => $isCompleted]); exit; } // 9. Subtaak verwijderen if ($action === 'delete_subtask') { $data = json_decode(file_get_contents('php://input'), true); $subtaskId = (int)($data['subtask_id'] ?? 0); if (!$subtaskId) { echo json_encode(['error' => 'Subtaak-ID ontbreekt']); exit; } $stmt = $db->prepare("DELETE FROM task_subtasks WHERE id = ? AND user_id = ?"); $stmt->execute([$subtaskId, $user['id']]); echo json_encode(['success' => true]); exit; } // 10. Subtaken ophalen voor een taak if ($action === 'get_subtasks') { $taskId = (int)($_GET['task_id'] ?? 0); if (!$taskId) { echo json_encode(['error' => 'Taak-ID ontbreekt']); exit; } // Controleer eigenaarschap $stmtCheck = $db->prepare("SELECT id FROM tasks WHERE id = ? AND user_id = ?"); $stmtCheck->execute([$taskId, $user['id']]); if (!$stmtCheck->fetchColumn()) { echo json_encode(['error' => 'Taak niet gevonden']); exit; } $stmt = $db->prepare(" SELECT id, task_id, title, is_completed, position FROM task_subtasks WHERE task_id = ? AND user_id = ? ORDER BY position ASC, created_at ASC "); $stmt->execute([$taskId, $user['id']]); $subtasks = $stmt->fetchAll(); echo json_encode(['success' => true, 'subtasks' => $subtasks]); exit; } // 11. Zoek eerstvolgende vrije open blok binnen werktijden (Release v2.1) if ($action === 'find_next_free_slot') { $rawInput = json_decode(file_get_contents('php://input'), true); $data = is_array($rawInput) ? $rawInput : $_GET; $duration = max(5, (int)($data['duration_minutes'] ?? 30)); $excludeTaskId = (int)($data['task_id'] ?? 0); $startFromNextWorkday = !empty($data['start_from_next_workday']); $workdaySettings = get_workday_settings(); $workdayStartStr = $workdaySettings['workday_start'] ?? '08:30'; $workdayEndStr = $workdaySettings['workday_end'] ?? '17:00'; $lunchStartStr = $workdaySettings['lunch_start'] ?? '12:30'; $lunchEndStr = $workdaySettings['lunch_end'] ?? '13:00'; $toMins = function(string $timeStr): int { $parts = explode(':', trim($timeStr)); return ((int)($parts[0] ?? 0) * 60) + (int)($parts[1] ?? 0); }; $toTimeStr = function(int $mins): string { $h = (int)floor($mins / 60); $m = $mins % 60; return sprintf('%02d:%02d', $h, $m); }; $workdayStartMin = $toMins($workdayStartStr); $workdayEndMin = $toMins($workdayEndStr); $lunchStartMin = $toMins($lunchStartStr); $lunchEndMin = $toMins($lunchEndStr); $tz = new DateTimeZone('Europe/Amsterdam'); $now = new DateTime('now', $tz); // Huidige tijd afronden naar hele kwartieren $nowH = (int)$now->format('G'); $nowM = (int)$now->format('i'); if ($nowM % 15 !== 0) { $nowM = (int)(ceil($nowM / 15) * 15); if ($nowM >= 60) { $nowH += (int)floor($nowM / 60); $nowM = $nowM % 60; } } $currentQuarterMin = $nowH * 60 + $nowM; $dutchDays = [1 => 'Maandag', 2 => 'Dinsdag', 3 => 'Woensdag', 4 => 'Donderdag', 5 => 'Vrijdag', 6 => 'Zaterdag', 7 => 'Zondag']; $dutchMonths = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec']; $checkDate = clone $now; if ($startFromNextWorkday) { $checkDate->modify('+1 day'); while ((int)$checkDate->format('N') >= 6) { $checkDate->modify('+1 day'); } } $foundSlot = null; $maxDays = 14; $checkedDays = 0; // Bepaal het totale zoekvenster voor batch-ophalen (O(1) database & Graph API schaalbaarheid) $rangeStartDate = $checkDate->format('Y-m-d'); $rangeEndObj = clone $checkDate; $rangeEndObj->modify('+21 days'); // ruim venster incl. weekenden $rangeEndDate = $rangeEndObj->format('Y-m-d'); // 1. Batch SQL: haal alle bestaande taken van de gebruiker in 1 geoptimaliseerde query op $tasksByDate = []; $stmtTasks = $db->prepare(" SELECT t.id, dp.plan_date, t.time_block_start, t.time_block_end, t.duration_minutes FROM tasks t INNER JOIN daily_plans dp ON t.daily_plan_id = dp.id WHERE t.user_id = ? AND dp.plan_date BETWEEN ? AND ? AND t.time_block_start IS NOT NULL AND t.time_block_start != '' AND t.status != 'CANCELLED' "); $stmtTasks->execute([$user['id'], $rangeStartDate, $rangeEndDate]); while ($t = $stmtTasks->fetch()) { if ($excludeTaskId > 0 && (int)$t['id'] === $excludeTaskId) { continue; } $tasksByDate[$t['plan_date']][] = $t; } // 2. Batch Microsoft Graph: haal alle Outlook events in 1 API call op voor het gehele venster $outlookByDate = []; if (function_exists('microsoft_fetch_events_range')) { try { $allOutlookEvts = microsoft_fetch_events_range($user['id'], $rangeStartDate, $rangeEndDate, false); foreach ($allOutlookEvts as $oe) { if (!empty($oe['date'])) { $outlookByDate[$oe['date']][] = $oe; } } } catch (Throwable $e) {} } while ($checkedDays < $maxDays) { $dayOfWeek = (int)$checkDate->format('N'); // 1 = Ma, 7 = Zo // Weekend overslaan if ($dayOfWeek === 6 || $dayOfWeek === 7) { $checkDate->modify('+1 day'); continue; } $dateStr = $checkDate->format('Y-m-d'); $isToday = ($dateStr === $now->format('Y-m-d')) && !$startFromNextWorkday; // Bepaal vroegste startminuut voor deze dag (op hele kwartieren) if ($isToday) { $earliestMin = max($workdayStartMin, $currentQuarterMin); if ($earliestMin % 15 !== 0) { $earliestMin = (int)(ceil($earliestMin / 15) * 15); } } else { $earliestMin = $workdayStartMin; if ($earliestMin % 15 !== 0) { $earliestMin = (int)(ceil($earliestMin / 15) * 15); } } // Alleen zoeken als er binnen de werkdag nog voldoende tijd is voor duration if ($earliestMin + $duration <= $workdayEndMin) { $busyIntervals = []; // Lunchpauze if ($lunchEndMin > $lunchStartMin) { $busyIntervals[] = [$lunchStartMin, $lunchEndMin]; } // Bestaande taken voor deze datum uit het geheugen $dayTasks = $tasksByDate[$dateStr] ?? []; foreach ($dayTasks as $t) { $tStart = $toMins($t['time_block_start']); $tDur = (int)($t['duration_minutes'] ?: 30); $tEnd = !empty($t['time_block_end']) ? $toMins($t['time_block_end']) : ($tStart + $tDur); if ($tEnd > $tStart) { $busyIntervals[] = [$tStart, $tEnd]; } } // Outlook afspraken voor deze datum uit het geheugen $dayOutlook = $outlookByDate[$dateStr] ?? []; foreach ($dayOutlook as $oe) { if (!empty($oe['is_all_day'])) continue; if (empty($oe['start']) || empty($oe['end'])) continue; $oStart = $toMins($oe['start']); $oEnd = $toMins($oe['end']); if ($oEnd > $oStart) { $busyIntervals[] = [$oStart, $oEnd]; } } // Doorzoek kwartierslots for ($slotStart = $earliestMin; ($slotStart + $duration) <= $workdayEndMin; $slotStart += 15) { $slotEnd = $slotStart + $duration; $hasCollision = false; foreach ($busyIntervals as [$bStart, $bEnd]) { if ($slotStart < $bEnd && $slotEnd > $bStart) { $hasCollision = true; break; } } if (!$hasCollision) { $foundSlot = [ 'date' => $dateStr, 'start' => $toTimeStr($slotStart), 'end' => $toTimeStr($slotEnd), 'duration_minutes' => $duration, 'is_today' => $isToday, 'formatted_date' => $isToday ? 'Vandaag (' . date('j', strtotime($dateStr)) . ' ' . $dutchMonths[(int)date('n', strtotime($dateStr))] . ')' : $dutchDays[(int)$checkDate->format('N')] . ' ' . date('j', strtotime($dateStr)) . ' ' . $dutchMonths[(int)date('n', strtotime($dateStr))], ]; break 2; } } } $checkDate->modify('+1 day'); $checkedDays++; } if ($foundSlot) { echo json_encode([ 'success' => true, 'slot' => $foundSlot, 'workday_start' => $workdayStartStr, 'workday_end' => $workdayEndStr, ]); } else { echo json_encode([ 'success' => false, 'workday_start' => $workdayStartStr, 'workday_end' => $workdayEndStr, 'error' => "Geen vrij blok van {$duration} minuten gevonden binnen de gezette werktijden ({$workdayStartStr} - {$workdayEndStr}) voor de komende {$maxDays} werkdagen.", ]); } exit; } echo json_encode(['error' => 'Onbekende actie']); } catch (Exception $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); }