Add Outlook synchronization features: implement real-time sync check, save sync preferences, and reconcile tasks with Outlook events
This commit is contained in:
+108
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
+218
-48
@@ -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++) {
|
||||
<!-- Integratie Status Badges -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<?php if ($isOutlookConnected): ?>
|
||||
<div class="flex items-center space-x-2 px-3 py-1.5 rounded-xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200/80 dark:border-sky-500/20 text-sky-800 dark:text-sky-300 text-xs font-medium shadow-2xs">
|
||||
<span class="w-2 h-2 rounded-full bg-sky-500 animate-pulse"></span>
|
||||
<span>Outlook 365 Live</span>
|
||||
<button onclick="location.reload()" title="Agenda Verversen" class="p-0.5 text-sky-600 hover:text-sky-900 dark:text-sky-400 dark:hover:text-white rounded transition">
|
||||
<i data-lucide="refresh-cw" class="w-3 h-3"></i>
|
||||
<div id="outlookSyncBadge" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200/80 dark:border-sky-500/20 text-sky-800 dark:text-sky-300 text-xs font-medium shadow-2xs transition">
|
||||
<span id="outlookSyncDot" class="w-2 h-2 rounded-full bg-sky-500"></span>
|
||||
<span id="outlookSyncStatusText">Outlook 365 Live</span>
|
||||
<span id="outlookLastSyncText" class="text-[10px] text-sky-600/70 dark:text-sky-400/60 font-mono hidden sm:inline">(synced <span id="syncTimeDisplay"><?= date('H:i') ?></span>)</span>
|
||||
<button id="manualSyncBtn" onclick="triggerManualSync()" title="Nu handmatig synchroniseren met Outlook" class="p-0.5 hover:bg-sky-100 dark:hover:bg-sky-500/20 text-sky-600 hover:text-sky-900 dark:text-sky-400 dark:hover:text-white rounded transition cursor-pointer">
|
||||
<i id="manualSyncIcon" data-lucide="refresh-cw" class="w-3 h-3"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
@@ -844,6 +875,7 @@ function toggleTimer() {
|
||||
clearInterval(timerInterval);
|
||||
isRunning = false;
|
||||
document.getElementById('timerToggleText').innerText = 'Hervatten';
|
||||
saveTimerStateBeforeReload();
|
||||
} else {
|
||||
isRunning = true;
|
||||
document.getElementById('timerToggleText').innerText = 'Pauzeren';
|
||||
@@ -854,6 +886,7 @@ function toggleTimer() {
|
||||
} else {
|
||||
clearInterval(timerInterval);
|
||||
isRunning = false;
|
||||
sessionStorage.removeItem('fociz_timer_state');
|
||||
document.getElementById('timerToggleText').innerText = 'Start Focus';
|
||||
alert('Focus sessie voltooid! Neem even een korte pauze.');
|
||||
}
|
||||
@@ -865,10 +898,59 @@ function resetTimer() {
|
||||
clearInterval(timerInterval);
|
||||
isRunning = false;
|
||||
timerSeconds = totalDuration;
|
||||
sessionStorage.removeItem('fociz_timer_state');
|
||||
document.getElementById('timerToggleText').innerText = 'Start Focus';
|
||||
updateTimerDisplay();
|
||||
}
|
||||
|
||||
function saveTimerStateBeforeReload() {
|
||||
try {
|
||||
if (isRunning || timerSeconds < totalDuration) {
|
||||
const state = {
|
||||
isRunning: isRunning,
|
||||
timerSeconds: timerSeconds,
|
||||
totalDuration: totalDuration,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
sessionStorage.setItem('fociz_timer_state', JSON.stringify(state));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Kon timer status niet opslaan:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreTimerStateIfAny() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('fociz_timer_state');
|
||||
if (!raw) return;
|
||||
sessionStorage.removeItem('fociz_timer_state');
|
||||
const state = JSON.parse(raw);
|
||||
if (state && typeof state.timerSeconds === 'number') {
|
||||
totalDuration = state.totalDuration || (pomodoroFocusTime * 60);
|
||||
let remaining = state.timerSeconds;
|
||||
if (state.isRunning && state.timestamp) {
|
||||
const elapsed = Math.floor((Date.now() - state.timestamp) / 1000);
|
||||
remaining = Math.max(0, remaining - elapsed);
|
||||
}
|
||||
timerSeconds = remaining;
|
||||
updateTimerDisplay();
|
||||
|
||||
if (state.isRunning && timerSeconds > 0) {
|
||||
toggleTimer(); // Hervat timer
|
||||
} else if (!state.isRunning && timerSeconds < totalDuration) {
|
||||
const toggleBtnText = document.getElementById('timerToggleText');
|
||||
if (toggleBtnText) toggleBtnText.innerText = 'Hervatten';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Kon timer status niet herstellen:', e);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
saveTimerStateBeforeReload();
|
||||
});
|
||||
|
||||
function selectFocusTask(title) {
|
||||
document.getElementById('activeTaskDisplay').innerText = title;
|
||||
}
|
||||
@@ -1076,6 +1158,94 @@ async function aiSmartSchedule(btn) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- Outlook 365 Auto-Sync & Achtergrond Controle ---
|
||||
const isOutlookConnected = <?= $isOutlookConnected ? 'true' : 'false' ?>;
|
||||
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
|
||||
const syncIntervalSeconds = <?= (int)($syncIntervalSeconds ?? 30) ?>;
|
||||
const weekStartDate = '<?= $weekStart ?>';
|
||||
const weekEndDate = '<?= $weekEnd ?>';
|
||||
let isSyncCheckActive = false;
|
||||
|
||||
async function checkOutlookChanges(isManual = false) {
|
||||
if (!isOutlookConnected || isSyncCheckActive) return;
|
||||
isSyncCheckActive = true;
|
||||
|
||||
const icon = document.getElementById('manualSyncIcon');
|
||||
const dot = document.getElementById('outlookSyncDot');
|
||||
const statusText = document.getElementById('outlookSyncStatusText');
|
||||
|
||||
if (icon) icon.classList.add('animate-spin');
|
||||
if (dot) {
|
||||
dot.classList.remove('bg-sky-500');
|
||||
dot.classList.add('bg-amber-500', 'animate-ping');
|
||||
}
|
||||
if (isManual && statusText) {
|
||||
statusText.innerText = 'Synchroniseren...';
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/planner.php?action=check_outlook_sync&plan_date=${encodeURIComponent(activePlannerDate)}&week_start=${encodeURIComponent(weekStartDate)}&week_end=${encodeURIComponent(weekEndDate)}&last_hash=${encodeURIComponent(currentSyncHash)}`);
|
||||
if (!res.ok) throw new Error('Netwerkfout bij synchronisatie controle');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.connected) {
|
||||
if (data.last_sync) {
|
||||
const timeEl = document.getElementById('syncTimeDisplay');
|
||||
if (timeEl) timeEl.innerText = data.last_sync.substring(0, 5);
|
||||
}
|
||||
|
||||
if (data.has_changes) {
|
||||
if (statusText) statusText.innerText = 'Wijzigingen bijwerken...';
|
||||
saveTimerStateBeforeReload();
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 400);
|
||||
return;
|
||||
} else {
|
||||
currentSyncHash = data.hash;
|
||||
if (statusText) statusText.innerText = 'Outlook 365 Live';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Outlook achtergrond controle:', err);
|
||||
if (statusText) statusText.innerText = 'Outlook 365 Live';
|
||||
} finally {
|
||||
isSyncCheckActive = false;
|
||||
if (icon) icon.classList.remove('animate-spin');
|
||||
if (dot) {
|
||||
dot.classList.remove('bg-amber-500', 'animate-ping');
|
||||
dot.classList.add('bg-sky-500');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function triggerManualSync() {
|
||||
checkOutlookChanges(true);
|
||||
}
|
||||
|
||||
// Initialisatie bij pagina-inladen
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
restoreTimerStateIfAny();
|
||||
|
||||
if (isOutlookConnected) {
|
||||
if (syncIntervalSeconds > 0) {
|
||||
setInterval(() => {
|
||||
checkOutlookChanges(false);
|
||||
}, syncIntervalSeconds * 1000);
|
||||
}
|
||||
|
||||
// Direct controleren wanneer gebruiker terugkeert naar het tabblad / venster
|
||||
window.addEventListener('focus', () => {
|
||||
checkOutlookChanges(false);
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
checkOutlookChanges(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
+43
@@ -48,6 +48,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2b. Outlook synchronisatievoorkeuren opslaan
|
||||
elseif ($action === 'save_outlook_settings') {
|
||||
$syncInterval = (int)($_POST['sync_interval_seconds'] ?? 30);
|
||||
|
||||
$stmtExisting = $db->prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
|
||||
$stmtExisting->execute([$userId]);
|
||||
$existing = $stmtExisting->fetch();
|
||||
$settings = json_decode($existing['settings'] ?? '{}', true) ?: [];
|
||||
$settings['sync_interval_seconds'] = $syncInterval;
|
||||
|
||||
$stmtUp = $db->prepare("UPDATE integrations SET settings = ? WHERE user_id = ? AND provider = 'OUTLOOK'");
|
||||
$stmtUp->execute([json_encode($settings), $userId]);
|
||||
|
||||
$message = "Uw Microsoft Outlook synchronisatievoorkeur is succesvol opgeslagen.";
|
||||
$messageType = 'success';
|
||||
}
|
||||
|
||||
// 3. Jira Opslaan & Testen
|
||||
elseif ($action === 'save_jira') {
|
||||
$companyDomain = jira_get_company_domain();
|
||||
@@ -462,6 +479,32 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="mt-4 p-4 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<input type="hidden" name="action" value="save_outlook_settings">
|
||||
<div class="space-y-1">
|
||||
<label for="sync_interval_seconds" class="text-xs font-bold text-slate-900 dark:text-white flex items-center space-x-1.5">
|
||||
<i data-lucide="refresh-cw" class="w-3.5 h-3.5 text-sky-500"></i>
|
||||
<span>Automatische Agenda Synchronisatie</span>
|
||||
</label>
|
||||
<p class="text-[11px] text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
Bepaalt hoe vaak het dagplanningscherm in de achtergrond controleert op gewijzigde of verwijderde Outlook afspraken.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<?php $currentInterval = (int)($outlookSettings['sync_interval_seconds'] ?? 30); ?>
|
||||
<select id="sync_interval_seconds" name="sync_interval_seconds" class="px-3 py-2 rounded-xl border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white text-xs focus:ring-2 focus:ring-sky-500 focus:outline-hidden">
|
||||
<option value="15" <?= $currentInterval === 15 ? 'selected' : '' ?>>Elke 15 seconden (Real-time)</option>
|
||||
<option value="30" <?= $currentInterval === 30 ? 'selected' : '' ?>>Elke 30 seconden (Aanbevolen)</option>
|
||||
<option value="60" <?= $currentInterval === 60 ? 'selected' : '' ?>>Elke 60 seconden (1 minuut)</option>
|
||||
<option value="120" <?= $currentInterval === 120 ? 'selected' : '' ?>>Elke 2 minuten</option>
|
||||
<option value="0" <?= $currentInterval === 0 ? 'selected' : '' ?>>Handmatig (en bij vensterfocus)</option>
|
||||
</select>
|
||||
<button type="submit" class="px-4 py-2 rounded-xl bg-sky-600 hover:bg-sky-500 text-white font-semibold text-xs transition shadow-2xs whitespace-nowrap cursor-pointer">
|
||||
Opslaan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 p-3.5 rounded-xl bg-teal-50 dark:bg-teal-500/10 border border-teal-200 dark:border-teal-500/20 text-teal-800 dark:text-teal-300 text-xs flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="check-circle" class="w-4 h-4 text-teal-600 dark:text-teal-400 flex-shrink-0"></i>
|
||||
|
||||
Reference in New Issue
Block a user