From 247fb4cd267a18587049672a43a6df1f71c5d9a6 Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Wed, 9 Sep 2026 12:10:54 +0200 Subject: [PATCH 1/2] feat(outlook): fix calendar sync format and enable bidirectional task-to-outlook scheduling --- api/planner.php | 155 ++++++++++++++++++++-- config/database.php | 7 +- config/schema.sql | 2 +- includes/integrations/microsoft.php | 166 ++++++++++++++++++++++- planner.php | 198 +++++++++++++++++++++++----- 5 files changed, 473 insertions(+), 55 deletions(-) diff --git a/api/planner.php b/api/planner.php index 24a2108..74489c3 100644 --- a/api/planner.php +++ b/api/planner.php @@ -4,6 +4,7 @@ */ require_once __DIR__ . '/../includes/auth.php'; +require_once __DIR__ . '/../includes/integrations/microsoft.php'; $user = require_auth(); $db = get_db_connection(); @@ -36,8 +37,11 @@ try { $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 = $data['time_block_start'] ?? null; - $date = $data['date'] ?? $today; + $timeBlockStart = !empty($data['time_block_start']) ? trim($data['time_block_start']) : null; + $durationMinutes = (int)($data['duration_minutes'] ?? 30); + if ($durationMinutes <= 0) $durationMinutes = 30; + $date = !empty($data['date']) ? trim($data['date']) : $today; + $syncOutlook = !empty($data['sync_to_outlook']); if (empty($title)) { echo json_encode(['error' => 'Titel is verplicht']); @@ -54,14 +58,44 @@ try { $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; + $externalId = !empty($data['external_id']) ? substr(trim($data['external_id']), 0, 255) : null; $externalUrl = !empty($data['external_url']) ? trim($data['external_url']) : null; + $timeBlockEnd = null; + + // Als er een tijdblok is en de gebruiker heeft synchronisatie naar Outlook gekozen (of source is OUTLOOK) + if (($syncOutlook || $source === 'OUTLOOK') && !empty($timeBlockStart) && empty($externalId)) { + $msResult = microsoft_create_event($user['id'], [ + 'subject' => $title, + 'description' => "Gepland via Fociz Dagplanning ({$priority})", + 'date' => $date, + 'start_time' => $timeBlockStart, + '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'; + $timeBlockEnd = $msResult['end_time'] ?? null; + } + } + + // Bereken timeBlockEnd indien nog niet gezet maar wel starttijd bekend + 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 + } + } $stmt = $db->prepare(" - INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, status, source, external_id, external_url) - VALUES (?, ?, ?, ?, ?, 'TODO', ?, ?, ?) + 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, $source, $externalId, $externalUrl]); + $stmt->execute([$user['id'], $planId, $title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $source, $externalId, $externalUrl]); $taskId = (int)$db->lastInsertId(); echo json_encode([ @@ -71,14 +105,102 @@ try { 'title' => $title, 'priority' => $priority, 'time_block_start' => $timeBlockStart, + 'time_block_end' => $timeBlockEnd, + 'duration_minutes' => $durationMinutes, 'is_completed' => 0, - 'status' => 'TODO' - ] + 'status' => 'TODO', + 'source' => $source, + 'external_id' => $externalId, + 'external_url' => $externalUrl, + ], + 'outlook_synced' => ($source === 'OUTLOOK' && !empty($externalId)), ]); exit; } - // 3. Taak status toggelen (voltooid / open) + // 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'); + $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, + '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); @@ -95,11 +217,24 @@ try { exit; } - // 4. Taak verwijderen + // 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']]); diff --git a/config/database.php b/config/database.php index e4caf0a..4469844 100644 --- a/config/database.php +++ b/config/database.php @@ -118,7 +118,12 @@ function ensure_tables_exist(PDO $pdo): void { // Controleer of de hoofdtabel 'users' al bestaat $result = $pdo->query("SHOW TABLES LIKE 'users'"); if ($result && $result->rowCount() > 0) { - // Tabellen bestaan al! Geen actie nodig. + // Tabellen bestaan al. Voer eventuele niet-destructieve kolom-upgrades uit indien nodig + try { + $pdo->exec("ALTER TABLE tasks MODIFY external_id VARCHAR(255) NULL"); + } catch (Throwable $t) { + // Kolom bestaat wellicht al of heeft al de juiste lengte + } return; } diff --git a/config/schema.sql b/config/schema.sql index 6bbb337..eb6bf11 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -57,7 +57,7 @@ CREATE TABLE IF NOT EXISTS `tasks` ( `duration_minutes` INT NULL DEFAULT 30, `is_completed` TINYINT(1) NOT NULL DEFAULT 0, `source` VARCHAR(50) NOT NULL DEFAULT 'FOCIZ', - `external_id` VARCHAR(100) NULL, + `external_id` VARCHAR(255) NULL, `external_url` TEXT NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, diff --git a/includes/integrations/microsoft.php b/includes/integrations/microsoft.php index 72d7be8..6f6326b 100644 --- a/includes/integrations/microsoft.php +++ b/includes/integrations/microsoft.php @@ -269,6 +269,10 @@ function microsoft_get_valid_token(int $userId): ?string { } 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; } @@ -283,9 +287,21 @@ function microsoft_fetch_today_events(int $userId, ?string $date = null): array return []; } - // Venster van 00:00:00 tot 23:59:59 lokale tijd - $startIso = "{$targetDate}T00:00:00"; - $endIso = "{$targetDate}T23:59:59"; + // Bereken het dagvenster in Europe/Amsterdam en converteer naar UTC ISO 8601 (eindigend op 'Z') + // Microsoft Graph vereist strikt een ISO 8601 formaat met tijdzone of 'Z' (bijv. 2026-09-09T00:00:00Z) + try { + $tzLocal = new DateTimeZone('Europe/Amsterdam'); + $tzUtc = new DateTimeZone('UTC'); + + $dtStart = new DateTime("{$targetDate} 00:00:00", $tzLocal); + $dtEnd = new DateTime("{$targetDate} 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 = "{$targetDate}T00:00:00Z"; + $endIso = "{$targetDate}T23:59:59Z"; + } $url = MS_GRAPH_BASE_URL . '/me/calendarView?' . http_build_query([ 'startDateTime' => $startIso, @@ -305,7 +321,12 @@ function microsoft_fetch_today_events(int $userId, ?string $date = null): array 'timeout' => 12, ]); - if ($response['status'] !== 200 || empty($response['json']['value'])) { + if ($response['status'] !== 200) { + error_log("microsoft_fetch_today_events error (HTTP {$response['status']}): " . ($response['error'] ?? json_encode($response['json']))); + return []; + } + + if (empty($response['json']['value'])) { return []; } @@ -321,14 +342,23 @@ function microsoft_fetch_today_events(int $userId, ?string $date = null): array $rawStart = $e['start']['dateTime'] ?? ''; $rawEnd = $e['end']['dateTime'] ?? ''; - // Formatteer naar H:i (bijv. 09:30) + // Formatteer naar H:i (bijv. 09:30). Omdat we 'Prefer: outlook.timezone="Europe/Amsterdam"' + // meesturen, levert Graph de tijden al aan in de Nederlandse tijdzone. $startTime = ''; $endTime = ''; if (!empty($rawStart)) { - $startTime = date('H:i', strtotime($rawStart)); + if (preg_match('/T(\d{2}:\d{2})/', $rawStart, $m)) { + $startTime = $m[1]; + } else { + $startTime = date('H:i', strtotime($rawStart)); + } } if (!empty($rawEnd)) { - $endTime = date('H:i', strtotime($rawEnd)); + if (preg_match('/T(\d{2}:\d{2})/', $rawEnd, $m)) { + $endTime = $m[1]; + } else { + $endTime = date('H:i', strtotime($rawEnd)); + } } // Teams of meeting URL detecteren @@ -353,6 +383,7 @@ function microsoft_fetch_today_events(int $userId, ?string $date = null): array 'end_raw' => $rawEnd, 'location' => $locationName ?: 'Geen locatie', 'meeting_url' => $meetingUrl, + 'web_link' => $e['webLink'] ?? '', 'is_all_day' => !empty($e['isAllDay']), 'source' => 'OUTLOOK_LIVE', ]; @@ -361,6 +392,127 @@ function microsoft_fetch_today_events(int $userId, ?string $date = null): array return $events; } +/** + * 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']; + } + + $subject = trim($eventData['subject'] ?? 'Fociz Dagplanning Taak'); + $description = trim($eventData['description'] ?? 'Gepland via Fociz Dagplanning & Structuur'); + $date = !empty($eventData['date']) ? trim($eventData['date']) : date('Y-m-d'); + $startTime = !empty($eventData['start_time']) ? trim($eventData['start_time']) : '09:00'; + + // Als tijd alleen een uur is (bijv. "9" of "09"), converteer naar "09:00" + if (preg_match('/^(\d{1,2})$/', $startTime, $m)) { + $startTime = sprintf('%02d:00', (int)$m[1]); + } + + // Bepaal de eindtijd + $endTime = !empty($eventData['end_time']) ? trim($eventData['end_time']) : ''; + $duration = (int)($eventData['duration_minutes'] ?? 30); + if ($duration <= 0) $duration = 30; + + if (empty($endTime)) { + 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, + ]); + + 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: {$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]); +} + /** * Ontkoppelt Microsoft integratie voor een gebruiker * Wist uitsluitend de persoonlijke tokens; behoudt de centrale Azure App gegevens indien aanwezig. diff --git a/planner.php b/planner.php index c3294b7..f6dd593 100644 --- a/planner.php +++ b/planner.php @@ -50,15 +50,13 @@ $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)) { + $isLiveOutlook = true; +} else { + // Alleen tonen indien de gebruiker Microsoft Outlook nog niet gekoppeld heeft $outlookEvents = [ [ 'id' => 'evt-1', - 'subject' => 'Daily Team Standup & Focus Sync', + 'subject' => 'Voorbeeld: Team Standup & Sync', 'start' => '09:00', 'end' => '09:30', 'location' => 'Microsoft Teams', @@ -67,7 +65,7 @@ if (!$isOutlookConnected || empty($outlookEvents)) { ], [ 'id' => 'evt-2', - 'subject' => 'Sprint & Roadmap Review met Stakeholders', + 'subject' => 'Voorbeeld: Roadmap Review met Stakeholders', 'start' => '11:00', 'end' => '12:00', 'location' => 'Vergaderruimte A & Teams', @@ -76,7 +74,7 @@ if (!$isOutlookConnected || empty($outlookEvents)) { ], [ 'id' => 'evt-3', - 'subject' => 'Architectuur & MariaDB Database Evaluatie', + 'subject' => 'Voorbeeld: Architectuur & DB Evaluatie', 'start' => '14:30', 'end' => '15:30', 'location' => 'Online Teams Call', @@ -91,16 +89,38 @@ $jiraData = jira_get_user_issues($userId); $jiraIssues = $jiraData['issues'] ?? []; $isJiraLive = !empty($jiraData['success']) && empty($jiraData['is_demo']); +// Filter Outlook events die afkomstig zijn van Fociz taken om duplicaten op de tijdlijn te voorkomen +$syncedTaskExternalIds = []; +foreach ($tasks as $t) { + if (!empty($t['external_id'])) { + $syncedTaskExternalIds[$t['external_id']] = true; + } +} +$displayedOutlookEvents = []; +foreach ($outlookEvents as $evt) { + if (!empty($evt['id']) && isset($syncedTaskExternalIds[$evt['id']])) { + continue; + } + $displayedOutlookEvents[] = $evt; +} + // 5. Tijdlijn uren bepalen $startHour = 8; $endHour = 18; -foreach ($outlookEvents as $e) { +foreach ($displayedOutlookEvents 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; } } +foreach ($tasks as $t) { + if (!empty($t['time_block_start'])) { + $h = (int)explode(':', $t['time_block_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); @@ -254,11 +274,11 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i -
+
@@ -267,7 +287,31 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
+
+ + +
+ + +
+ +
+ +
@@ -296,6 +340,11 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i JIRA + + + + OUTLOOK + @@ -306,12 +355,12 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i - + - - Open in Jira + + @@ -324,6 +373,12 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i + + + +
- - - - Deelnemen - - +
@@ -473,13 +535,26 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
- +
- - - +
+ + + + + + + + + + + +
@@ -597,24 +672,75 @@ function toggleAddTaskForm() { function quickSchedule(hour) { toggleAddTaskForm(); - document.getElementById('newTaskTime').value = hour; + const timeInput = document.getElementById('newTaskTime'); + if (timeInput) timeInput.value = hour; + const outlookCheckbox = document.getElementById('newTaskSyncOutlook'); + if (outlookCheckbox) outlookCheckbox.checked = true; } async function submitNewTask() { const title = document.getElementById('newTaskTitle').value.trim(); const priority = document.getElementById('newTaskPriority').value; const time = document.getElementById('newTaskTime').value.trim(); + const duration = document.getElementById('newTaskDuration') ? parseInt(document.getElementById('newTaskDuration').value) : 30; + const syncOutlook = document.getElementById('newTaskSyncOutlook') ? document.getElementById('newTaskSyncOutlook').checked : false; if (!title) return; - const res = await fetch('/api/planner.php?action=add_task', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ title, priority, time_block_start: time, date: '' }) - }); - const data = await res.json(); - if (data.success) { - location.reload(); + try { + const res = await fetch('/api/planner.php?action=add_task', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + title, + priority, + time_block_start: time, + duration_minutes: duration, + sync_to_outlook: syncOutlook, + date: '' + }) + }); + const data = await res.json(); + if (data.success) { + location.reload(); + } else { + alert(data.error || 'Fout bij opslaan van de taak'); + } + } catch(err) { + alert('Netwerkfout bij opslaan van de taak'); + } +} + +async function syncTaskToOutlook(taskId, btn) { + if (!btn) return; + const originalHtml = btn.innerHTML; + btn.innerHTML = 'Syncen...'; + btn.disabled = true; + + try { + const res = await fetch('/api/planner.php?action=sync_task_to_outlook', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ task_id: taskId }) + }); + const data = await res.json(); + if (data.success) { + btn.innerHTML = ''; + if (typeof lucide !== 'undefined') lucide.createIcons(); + setTimeout(() => { + location.reload(); + }, 600); + } else { + alert(data.error || 'Kon taak niet synchroniseren naar Outlook'); + btn.innerHTML = originalHtml; + btn.disabled = false; + if (typeof lucide !== 'undefined') lucide.createIcons(); + } + } catch(err) { + alert('Netwerkfout bij synchroniseren naar Outlook'); + btn.innerHTML = originalHtml; + btn.disabled = false; + if (typeof lucide !== 'undefined') lucide.createIcons(); } } @@ -662,7 +788,7 @@ async function toggleTask(taskId, isCompleted) { } async function deleteTask(taskId) { - if (!confirm('Taak verwijderen?')) return; + if (!confirm('Taak verwijderen? (Indien gekoppeld aan Outlook wordt ook de agenda-afspraak verwijderd)')) return; await fetch('/api/planner.php?action=delete_task', { method: 'POST', headers: {'Content-Type': 'application/json'}, From 2f336f54a812f2005e4b93243303b5a520bb5fdb Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Wed, 9 Sep 2026 12:27:28 +0200 Subject: [PATCH 2/2] fix(ui): clean header banner and fix timeline event text overflow --- includes/header.php | 48 +++++++------------- planner.php | 108 ++++++++++++++++++++++++-------------------- profile.php | 35 +++++++++++--- 3 files changed, 104 insertions(+), 87 deletions(-) diff --git a/includes/header.php b/includes/header.php index b230bb9..5505b19 100644 --- a/includes/header.php +++ b/includes/header.php @@ -132,39 +132,28 @@ require_once __DIR__ . '/env_badge.php'; - -
- - - - - - - - - + +
-
+
+
+ +
-
@@ -188,11 +177,6 @@ require_once __DIR__ . '/env_badge.php'; Profiel - diff --git a/planner.php b/planner.php index f6dd593..1a1fb6c 100644 --- a/planner.php +++ b/planner.php @@ -131,6 +131,10 @@ $dutchDays = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag' $dutchMonths = ['', 'januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december']; $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(int)date('n')] . ' ' . date('Y'); ?> +
@@ -207,7 +211,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
-
+
@@ -467,8 +471,8 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
-
-
+
+
@@ -482,7 +486,7 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
-
+
-
-
+
+
-
+
-
-
- - - - - - - -
-
- - - - Deelnemen - - - - - Outlook - - + -
-
- - - - -
-
- - - - +
+
+
+ + - - - - - - + + + +
+
+ + + + + + + + + + + +
diff --git a/profile.php b/profile.php index 5a056ff..46862bd 100644 --- a/profile.php +++ b/profile.php @@ -724,13 +724,36 @@ require_once __DIR__ . '/includes/header.php';
-
- -
-
- -

Persoonlijke Gegevens

+
+ +
+
+
+ +
+
+
+

Tweestapsverificatie (2FA) is Actief

+ Beveiligd +
+

+ Uw Fociz account is optimaal beveiligd met een Authenticator app (TOTP) voor maximale privacy- en dataveiligheid. +

+
+
+ + Verplicht & Actief +
+
+ +
+ +
+
+ +

Persoonlijke Gegevens

+