'Ma', 2 => 'Di', 3 => 'Wo', 4 => 'Do', 5 => 'Vr', 6 => 'Za', 7 => 'Zo'];
$dutchFullDays = [1 => 'Maandag', 2 => 'Dinsdag', 3 => 'Woensdag', 4 => 'Donderdag', 5 => 'Vrijdag', 6 => 'Zaterdag', 7 => 'Zondag'];
$dutchMonths = ['', 'januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'];
$dutchShortMonths = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
$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 integratiestatussen op
$userIntegrations = [];
try {
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ?");
$stmtInt->execute([$userId]);
foreach ($stmtInt->fetchAll() as $row) {
$userIntegrations[$row['provider']] = $row;
}
} catch (Throwable $e) {
error_log("Fout bij ophalen integraties: " . $e->getMessage());
ensure_tables_exist($db);
}
$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;
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
if ($isOutlookConnected) {
try {
// Haal afspraken op voor het volledige datumbereik van de week (met 60s cache)
$weekOutlookEvents = microsoft_fetch_events_range($userId, $weekStart, $weekEnd, $forceRefresh);
$isLiveOutlook = true;
// Synchroniseer / reconcilieer Fociz taken direct met actuele Outlook data
// Alleen uitvoeren indien events gewijzigd zijn of bij expliciete refresh om overtollige DB load te voorkomen
$eventsHash = md5(json_encode($weekOutlookEvents));
$lastReconciledHash = $_SESSION["last_reconciled_{$userId}"] ?? '';
if ($eventsHash !== $lastReconciledHash || $forceRefresh) {
microsoft_reconcile_tasks_with_outlook($userId, $weekStart, $weekEnd, $weekOutlookEvents);
if (session_status() === PHP_SESSION_ACTIVE) {
$_SESSION["last_reconciled_{$userId}"] = $eventsHash;
}
}
} catch (Throwable $e) {
error_log("Outlook synchronisatiefout in planner: " . $e->getMessage());
$isLiveOutlook = false;
$weekOutlookEvents = [];
}
} else {
// Demo data indien Microsoft Outlook nog niet gekoppeld is
$weekOutlookEvents = [
[
'id' => 'evt-1',
'subject' => 'Voorbeeld: Team Standup & Sync',
'date' => $today,
'start' => '09:00',
'end' => '09:30',
'location' => 'Microsoft Teams',
'meeting_url' => 'https://teams.microsoft.com',
'source' => 'SANDBOX',
],
[
'id' => 'evt-2',
'subject' => 'Voorbeeld: Roadmap Review met Stakeholders',
'date' => $today,
'start' => '11:00',
'end' => '12:00',
'location' => 'Vergaderruimte A & Teams',
'meeting_url' => 'https://teams.microsoft.com',
'source' => 'SANDBOX',
],
[
'id' => 'evt-3',
'subject' => 'Voorbeeld: Architectuur & DB Evaluatie',
'date' => $today,
'start' => '14:30',
'end' => '15:30',
'location' => 'Online Teams Call',
'meeting_url' => 'https://teams.microsoft.com',
'source' => 'SANDBOX',
]
];
}
// 2. Haal dagplanning op of maak aan voor $selectedDate
$dailyPlan = null;
try {
$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 (?, ?, ?)
ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)
");
$stmtCreatePlan->execute([$userId, $selectedDate, $defaultGoal]);
$planId = (int)$db->lastInsertId();
$dailyPlan = ['id' => $planId, 'plan_date' => $selectedDate, 'focus_goal' => $defaultGoal, 'notes' => ''];
}
} catch (Throwable $e) {
error_log("Fout bij dagplanning ophalen/aanmaken: " . $e->getMessage());
ensure_tables_exist($db);
$dailyPlan = ['id' => 0, 'plan_date' => $selectedDate, 'focus_goal' => 'Mijn belangrijkste doel van vandaag behalen', 'notes' => ''];
}
$planId = !empty($dailyPlan['id']) ? (int)$dailyPlan['id'] : 0;
// 3. Haal taken op voor de geselecteerde dag (vers bijgewerkt na eventuele reconciliatie)
$tasks = [];
try {
$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, $planId]);
$tasks = $stmtTasks->fetchAll() ?: [];
} catch (Throwable $e) {
error_log("Fout bij ophalen taken: " . $e->getMessage());
}
// 4. Haal weekstatistieken voor taken op (aantal per dag)
$weekTaskStats = [];
try {
$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]);
foreach ($stmtWeekTasks->fetchAll() as $row) {
$weekTaskStats[$row['plan_date']] = [
'total' => (int)$row['total_tasks'],
'completed' => (int)$row['completed_tasks'],
];
}
} catch (Throwable $e) {
error_log("Fout bij weekstatistieken ophalen: " . $e->getMessage());
}
// Groepeer Outlook afspraken per datum
$outlookEventsByDate = [];
foreach ($weekOutlookEvents as $evt) {
$evtDate = $evt['date'] ?? $selectedDate;
$outlookEventsByDate[$evtDate][] = $evt;
}
// 7 Dagen van de week samenstellen voor de weekselector
$weekDays = [];
for ($i = 0; $i < 7; $i++) {
$dayTs = strtotime("+{$i} days", $mondayTs);
$dayDate = date('Y-m-d', $dayTs);
$dayN = (int)date('N', $dayTs);
$dayM = (int)date('n', $dayTs);
$tStat = $weekTaskStats[$dayDate] ?? ['total' => 0, 'completed' => 0];
$mCount = count($outlookEventsByDate[$dayDate] ?? []);
$weekDays[] = [
'date' => $dayDate,
'day_num' => date('j', $dayTs),
'short_name' => $dutchShortDays[$dayN],
'full_name' => $dutchFullDays[$dayN],
'month_short' => $dutchShortMonths[$dayM],
'is_today' => ($dayDate === $today),
'is_selected' => ($dayDate === $selectedDate),
'task_count' => $tStat['total'],
'completed_count' => $tStat['completed'],
'meeting_count' => $mCount,
];
}
// 5. Haal Jira dashboard data op voor de snelle import widget en wachtrijfiltering
$jiraAssignedIssues = [];
$jiraTeamIssues = [];
$jiraAllIssues = [];
$jiraQueueFilters = [];
$isJiraLive = false;
try {
$jiraDashboardData = jira_get_dashboard_data($userId, $forceRefresh);
$jiraAssignedIssues = $jiraDashboardData['assigned_issues'] ?? [];
$jiraTeamIssues = $jiraDashboardData['team_issues'] ?? [];
$jiraQueueFilters = $jiraDashboardData['queue_filters'] ?? [];
$isJiraLive = !empty($jiraDashboardData['success']) && empty($jiraDashboardData['is_demo']);
$seenJiraKeys = [];
foreach (array_merge($jiraAssignedIssues, $jiraTeamIssues) as $iss) {
if (!empty($iss['key']) && !isset($seenJiraKeys[$iss['key']])) {
$seenJiraKeys[$iss['key']] = true;
$jiraAllIssues[] = $iss;
}
}
} catch (Throwable $e) {
error_log("Fout bij ophalen Jira issues in planner: " . $e->getMessage());
}
$jiraIssues = $jiraAllIssues;
// 5b. Haal recente e-mails op voor de Outlook E-mail Widget
$recentEmails = [];
$unreadEmailCount = 0;
$mailNeedsReconsent = false;
try {
$mailData = microsoft_fetch_inbox_messages($userId, 5, false, $forceRefresh);
$recentEmails = $mailData['messages'] ?? [];
$unreadEmailCount = (int)($mailData['unread_count'] ?? 0);
$mailNeedsReconsent = !empty($mailData['needs_reconsent']);
} catch (Throwable $e) {
error_log("Fout bij ophalen recente e-mails in planner: " . $e->getMessage());
}
// 5c. Widget Catalogus & Raster Indeling (Fociz v2.0)
$availableWidgets = [
'daily_goal' => ['id' => 'daily_goal', 'title' => 'Dagfocus & Hoofddoel', 'category' => 'Planning', 'icon' => 'target', 'enabled' => true, 'col_span' => 12, 'order' => 1, 'desc' => 'Centraal focusdoel voor de dag met snelle tellers'],
'timeline' => ['id' => 'timeline', 'title' => 'Tijdlijn & Dag/Weekoverzicht', 'category' => 'Agenda', 'icon' => 'calendar', 'enabled' => true, 'col_span' => 12, 'order' => 2, 'desc' => 'Gecombineerd weekoverzicht, dagselectie en uren-tijdlijn met time-blocking en AI'],
'tasks' => ['id' => 'tasks', 'title' => 'Dagtaken & Prioriteiten', 'category' => 'Planning', 'icon' => 'check-square', 'enabled' => true, 'col_span' => 7, 'order' => 3, 'desc' => 'Volledige takenlijst met P1/P2/P3, tijdvakken en Outlook synchronisatie'],
'outlook_mail' => ['id' => 'outlook_mail', 'title' => 'Outlook E-mail Widget', 'category' => 'Communicatie', 'icon' => 'mail', 'enabled' => true, 'col_span' => 6, 'order' => 4, 'desc' => 'Inkomende e-mails direct inzien en met 1 klik inplannen als taak'],
'jira_queue' => ['id' => 'jira_queue', 'title' => 'Jira Tickets & Wachtrijen', 'category' => 'Ontwikkeling', 'icon' => 'layers', 'enabled' => true, 'col_span' => 6, 'order' => 5, 'desc' => 'Openstaande Jira tickets (DX/SD wachtrijen) met directe importknop'],
'calendar_meetings' => ['id' => 'calendar_meetings', 'title' => 'Agenda & Teams Meetings', 'category' => 'Agenda', 'icon' => 'video', 'enabled' => true, 'col_span' => 6, 'order' => 6, 'desc' => 'Vandaag geplande afspraken met 1-klik Teams deelnemen knop'],
'scratchpad' => ['id' => 'scratchpad', 'title' => 'Dagnotities & Kladblok', 'category' => 'Productiviteit', 'icon' => 'edit-3', 'enabled' => true, 'col_span' => 6, 'order' => 7, 'desc' => 'Vrij notitieblok met automatische achtergrond-opslag'],
'stats' => ['id' => 'stats', 'title' => 'Productiviteit & KPI\'s', 'category' => 'Inzicht', 'icon' => 'bar-chart-2', 'enabled' => true, 'col_span' => 12, 'order' => 8, 'desc' => 'Voortgangsbalk, voltooide taken, focus uren en meeting totalen'],
'quick_links' => ['id' => 'quick_links', 'title' => 'Snelle Koppelingen & Tools', 'category' => 'Productiviteit', 'icon' => 'external-link', 'enabled' => false, 'col_span' => 6, 'order' => 9, 'desc' => '1-klik snelkoppelingen naar Jira, Teams, Outlook, Azure en documentatie'],
'habit_tracker' => ['id' => 'habit_tracker', 'title' => 'Dagelijkse Focus Gewoontes', 'category' => 'Productiviteit', 'icon' => 'sparkles', 'enabled' => false, 'col_span' => 6, 'order' => 10, 'desc' => 'Houd gezonde werkgewoontes bij (water drinken, 15m bewegen, inbox zero)'],
'focus_sound' => ['id' => 'focus_sound', 'title' => 'Focus Sounds & Omgeving', 'category' => 'Productiviteit', 'icon' => 'headphones', 'enabled' => false, 'col_span' => 6, 'order' => 11, 'desc' => 'Rustgevende focus-geluiden voor concentratie en diep werk'],
'team_status' => ['id' => 'team_status', 'title' => 'Team & Beschikbaarheid', 'category' => 'Team', 'icon' => 'users', 'enabled' => false, 'col_span' => 6, 'order' => 12, 'desc' => 'Overzicht van collega\'s en wie vandaag bereikbaar of in overleg is']
];
$userWidgetLayout = $availableWidgets;
$savedLayoutJson = $currentUser['fociz_widget_layout'] ?? '';
if (!empty($savedLayoutJson)) {
$decoded = json_decode($savedLayoutJson, true);
if (is_array($decoded)) {
$savedOrder = 1;
$seen = [];
foreach ($decoded as $item) {
$wId = $item['id'] ?? '';
if (isset($userWidgetLayout[$wId])) {
$userWidgetLayout[$wId]['enabled'] = !empty($item['enabled']);
if (!empty($item['col_span'])) $userWidgetLayout[$wId]['col_span'] = (int)$item['col_span'];
$userWidgetLayout[$wId]['order'] = isset($item['order']) ? (int)$item['order'] : $savedOrder;
if (!empty($item['custom_title'])) {
$userWidgetLayout[$wId]['custom_title'] = trim($item['custom_title']);
$userWidgetLayout[$wId]['title'] = trim($item['custom_title']);
}
$seen[$wId] = true;
$savedOrder++;
}
}
foreach ($userWidgetLayout as $wId => &$wData) {
if (!isset($seen[$wId])) {
$wData['order'] = $savedOrder++;
}
}
unset($wData);
}
}
uasort($userWidgetLayout, function($a, $b) {
$diff = ($a['order'] ?? 0) <=> ($b['order'] ?? 0);
if ($diff !== 0) return $diff;
return strcmp($a['id'] ?? '', $b['id'] ?? '');
});
// Bereken dagstatistieken
$completedTasksCount = 0;
$pendingTasksCount = 0;
$highPriorityCount = 0;
$totalPlannedMinutes = 0;
foreach ($tasks as $t) {
if (!empty($t['is_completed'])) {
$completedTasksCount++;
} else {
$pendingTasksCount++;
if (($t['priority'] ?? '') === 'P1') $highPriorityCount++;
}
$totalPlannedMinutes += (int)($t['duration_minutes'] ?: 30);
}
$totalTaskCount = count($tasks);
$taskProgressPercent = $totalTaskCount > 0 ? round(($completedTasksCount / $totalTaskCount) * 100) : 0;
// Filter Outlook events die afkomstig zijn van Fociz taken voor de geselecteerde dag om duplicaten op de tijdlijn te voorkomen
$dayOutlookEvents = $outlookEventsByDate[$selectedDate] ?? [];
$syncedTaskExternalIds = [];
foreach ($tasks as $t) {
if (!empty($t['external_id'])) {
$syncedTaskExternalIds[$t['external_id']] = true;
}
}
$displayedOutlookEvents = [];
foreach ($dayOutlookEvents as $evt) {
if (!empty($evt['id']) && isset($syncedTaskExternalIds[$evt['id']])) {
continue;
}
$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:00';
$lunchStart = $workdaySettings['lunch_start'] ?? '12:30';
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
$lunchHour = (int)explode(':', $lunchStart)[0];
$startHour = (int)explode(':', $workdayStartStr)[0];
$endHourParts = explode(':', $workdayEndStr);
$endHour = (int)$endHourParts[0];
if (!empty($endHourParts[1]) && (int)$endHourParts[1] > 0) {
$endHour++;
}
if ($startHour < 6) $startHour = 6;
if ($endHour > 23) $endHour = 23;
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 <= 23) $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 <= 23) $endHour = $h + 1;
}
}
$hours = [];
for ($h = $startHour; $h <= $endHour; $h++) {
$hours[] = sprintf('%02d:00', $h);
}
?>
= htmlspecialchars($formattedSelectedDate) ?>
GEKOZEN DAG
Fociz
Jouw centrale werkstation • Taken, Agenda, Mail & Jira
AI Co-Pilot · Dagbriefing
= date('H:i') ?>
0) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => true,
'selectedDate' => $selectedDate,
'formattedDate' => $formattedSelectedDate,
'weekStart' => $weekStart,
'weekEnd' => $weekEnd,
'weekNumber' => $weekNumber,
'yearNumber' => $yearNumber,
'formattedWeek' => $formattedWeekRange,
'syncHash' => $initialSyncHash,
'focusGoal' => $dailyPlan['focus_goal'] ?? '',
'dailyNotes' => $dailyPlan['notes'] ?? '',
'title' => 'Fociz (' . $formattedSelectedDate . ')',
'html' => $contentHtml,
]);
exit;
}
?>
Inhoud van het item bekijken
Annuleren
Inplannen
Taak & Tijdblok Bewerken
Pas titel, start-/eindtijd en tijdsduur vrij aan
Taaktitel
Prioriteit
P1 - Brandend / Focus
P2 - Belangrijk (Standaard)
P3 - Regulier / Lage druk
Datum & Tijdblok
Plan in open blok
Deze taak is gekoppeld aan Outlook 365. Wijzigingen worden automatisch in uw Outlook agenda gesynchroniseerd.
Annuleren
Wijzigingen Opslaan
Let op! Je plant buiten werktijden
Het ingeplande tijdblok valt buiten de gezette werktijden.
Gekozen tijdblok:
Gezette werktijden:
Kies Nee om het item automatisch in te plannen in het eerstvolgende open blok op de volgende werkdag, of kies Ja om toch nu buiten werktijd te plannen.
Nee (Volgende werkdag)
Ja, toch inplannen
E-mail Inplannen in Fociz
Zet deze Outlook mail direct om in een concrete dagtaak
Afzender
Taaktitel
Prioriteit
P1 - Brandend / Focus
P2 - Belangrijk (Standaard)
P3 - Regulier / Lage druk
Geschatte Duur
15 minuten
30 minuten
45 minuten
60 minuten (1 uur)
Annuleren
Taak Inplannen