Files
Fociz/planner.php
T
NiekRabelink e161c72fc4 Enhance email rendering and Jira widget functionality
- Implemented a secure rendering method for email content using a sandboxed iframe to prevent XSS attacks.
- Added thorough HTML sanitization for incoming emails to mitigate potential exploits.
- Refactored email body rendering logic to utilize a dedicated function for better readability and maintainability.
- Updated Jira widget to fetch and display dashboard data, including assigned issues and team issues, with improved filtering options.
- Enhanced the user interface of the Jira widget with a standardized control toolbar for resizing and collapsing widgets.
- Introduced inline search functionality within the Jira widget for quick ticket filtering.
- Improved error handling and logging for Jira data fetching.
2026-09-10 15:44:37 +02:00

3225 lines
191 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Fociz Dagplanning, Focus Modus & Time-blocking
* Gekoppeld aan Microsoft Outlook 365 (Agenda) en Atlassian Jira (Tickets)
*/
// Controleer of dit een AJAX verzoek is voor een geruisloze datumwisseling
$isAjaxRequest = (!empty($_GET['ajax']) && $_GET['ajax'] === '1');
if ($isAjaxRequest) {
require_once __DIR__ . '/includes/auth.php';
$currentUser = require_auth(true);
} else {
$pageTitle = 'Fociz';
require_once __DIR__ . '/includes/header.php';
}
require_once __DIR__ . '/includes/settings.php';
require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/integrations/jira.php';
$db = get_db_connection();
$userId = (int)($currentUser['id'] ?? 0);
$today = date('Y-m-d');
// Indien de datum van vandaag via de URL is meegegeven bij een normale pagina-aanroep, schoon de URL op naar zuiver 'planner.php'
if (!$isAjaxRequest && isset($_GET['date']) && $_GET['date'] === $today) {
header('Location: planner.php', true, 302);
exit;
}
$selectedDate = (!empty($_GET['date']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date'])) ? $_GET['date'] : $today;
$isTodaySelected = ($selectedDate === $today);
$selectedTs = strtotime($selectedDate);
// Haal organisatie werkdag- en AI-standaarden op (Release v1.4 & v1.5)
$workdaySettings = get_workday_settings();
$aiSettings = get_ai_settings();
// Bereken weekgrenzen (Maandag t/m Zondag) rondom de geselecteerde datum
$dayOfWeek = (int)date('N', $selectedTs); // 1 = Maandag, 7 = Zondag
$mondayTs = strtotime('-' . ($dayOfWeek - 1) . ' days', $selectedTs);
$sundayTs = strtotime('+' . (7 - $dayOfWeek) . ' days', $selectedTs);
$weekStart = date('Y-m-d', $mondayTs);
$weekEnd = date('Y-m-d', $sundayTs);
$weekNumber = (int)date('W', $selectedTs);
$yearNumber = (int)date('o', $selectedTs);
// URLs voor weeknavigatie
$prevWeekDate = date('Y-m-d', strtotime('-7 days', $mondayTs));
$nextWeekDate = date('Y-m-d', strtotime('+7 days', $mondayTs));
// Nederlandse vertalingen voor datums
$dutchShortDays = [1 => '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'],
'tasks' => ['id' => 'tasks', 'title' => 'Dagtaken & Prioriteiten', 'category' => 'Planning', 'icon' => 'check-square', 'enabled' => true, 'col_span' => 7, 'order' => 2, 'desc' => 'Volledige takenlijst met P1/P2/P3, tijdvakken en Outlook synchronisatie'],
'timeline' => ['id' => 'timeline', 'title' => 'Tijdlijn & Time-blocking', 'category' => 'Agenda', 'icon' => 'clock', 'enabled' => true, 'col_span' => 5, 'order' => 3, 'desc' => 'Visueel urenoverzicht met lunchpauze, afspraken en AI Slim Indelen'],
'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)) {
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'];
if (isset($item['order'])) $userWidgetLayout[$wId]['order'] = (int)$item['order'];
}
}
}
}
uasort($userWidgetLayout, function($a, $b) {
return ($a['order'] ?? 0) <=> ($b['order'] ?? 0);
});
// 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:30';
$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);
}
?>
<?php if (!$isAjaxRequest): ?>
<style>
.line-clamp-1 { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
</style>
<div id="plannerContentArea" class="space-y-6 max-w-7xl mx-auto transition-opacity duration-200">
<?php else: ?>
<?php ob_start(); ?>
<?php endif; ?>
<!-- Top Banner: Datum, Hoofddoel & Integraties Status -->
<div class="fociz-card p-6 sm:p-7 relative overflow-hidden">
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div>
<div class="flex items-center space-x-2 text-indigo-600 dark:text-indigo-400 text-xs font-bold uppercase tracking-wider mb-1">
<i data-lucide="calendar" class="w-4 h-4"></i>
<span><?= htmlspecialchars($formattedSelectedDate) ?></span>
<?php if ($isTodaySelected): ?>
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-extrabold bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300 ring-1 ring-indigo-500/30">VANDAAG</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 dark:bg-slate-800/80 text-slate-600 dark:text-slate-400">GEKOZEN DAG</span>
<?php endif; ?>
</div>
<h1 class="text-2xl sm:text-3xl font-black tracking-tight text-slate-900 dark:text-white">
Fociz
</h1>
<p class="text-xs sm:text-sm text-slate-500 dark:text-slate-400 mt-0.5">
Jouw centrale werkstation &bull; Taken, Agenda, Mail & Jira
</p>
</div>
<!-- Integratie Status Badges & Widget Beheer Knop -->
<div class="flex flex-wrap items-center gap-2">
<button onclick="openWidgetManagerModal()" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-500/10 dark:hover:bg-indigo-500/20 text-indigo-700 dark:text-indigo-300 border border-indigo-200/80 dark:border-indigo-500/30 text-xs font-bold transition shadow-2xs cursor-pointer">
<i data-lucide="layout-grid" class="w-3.5 h-3.5"></i>
<span>Widgets & Indeling</span>
</button>
<?php if ($isOutlookConnected): ?>
<div id="outlookSyncBadge" class="flex items-center space-x-2 px-3.5 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-semibold 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/80 dark:text-sky-400/70 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: ?>
<a href="/profile.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition shadow-2xs">
<i data-lucide="link-2" class="w-3.5 h-3.5 text-sky-500"></i>
<span>Koppel Outlook</span>
</a>
<?php endif; ?>
<?php if ($isJiraLive): ?>
<a href="/jira.php" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-slate-100 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition shadow-2xs">
<span class="w-2 h-2 rounded-full bg-indigo-500 animate-pulse"></span>
<span>Jira: <?= count($jiraIssues) ?> tickets</span>
</a>
<?php else: ?>
<a href="/profile.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition shadow-2xs">
<i data-lucide="trello" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Koppel Jira</span>
</a>
<?php endif; ?>
</div>
</div>
</div>
<!-- Week Navigatie & Dagselector -->
<div class="fociz-card p-5 sm:p-6">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-4 mb-4 border-b border-slate-100 dark:border-slate-800/60">
<!-- Navigatie controls -->
<div class="flex items-center space-x-2">
<a href="planner.php?date=<?= $prevWeekDate ?>" onclick="switchPlannerDate('<?= $prevWeekDate ?>', event); return false;" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 text-xs font-semibold transition shadow-2xs" title="Vorige week (Maandag <?= date('j M', strtotime($prevWeekDate)) ?>)">
<i data-lucide="chevron-left" class="w-4 h-4"></i>
<span class="hidden sm:inline">Vorige week</span>
</a>
<a href="planner.php" onclick="switchPlannerDate('<?= $today ?>', event); return false;" class="px-3.5 py-1.5 rounded-xl <?= $isTodaySelected ? 'bg-slate-900 text-white dark:bg-white dark:text-slate-900 font-bold shadow-xs' : 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 font-semibold shadow-2xs' ?> text-xs transition" title="Spring direct naar vandaag">
Vandaag
</a>
<a href="planner.php?date=<?= $nextWeekDate ?>" onclick="switchPlannerDate('<?= $nextWeekDate ?>', event); return false;" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 text-xs font-semibold transition shadow-2xs" title="Volgende week (Maandag <?= date('j M', strtotime($nextWeekDate)) ?>)">
<span class="hidden sm:inline">Volgende week</span>
<i data-lucide="chevron-right" class="w-4 h-4"></i>
</a>
</div>
<!-- Week Titel & Datumrange -->
<div class="text-center md:text-left">
<div class="text-xs font-bold uppercase tracking-wider text-indigo-600 dark:text-indigo-400">
Week <?= $weekNumber ?> &bull; <?= $yearNumber ?>
</div>
<div class="text-base font-black text-slate-900 dark:text-white">
<?= htmlspecialchars($formattedWeekRange) ?>
</div>
</div>
<!-- Directe datumselector -->
<div class="flex items-center space-x-2 justify-end">
<label for="jumpDateInput" class="text-xs text-slate-500 dark:text-slate-400 font-medium hidden sm:inline">Datum:</label>
<input
type="date"
id="jumpDateInput"
value="<?= htmlspecialchars($selectedDate) ?>"
onchange="if(this.value) { switchPlannerDate(this.value, event); }"
class="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 font-medium cursor-pointer shadow-2xs"
>
</div>
</div>
<!-- 7 Dagen Weekstrook -->
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-7 gap-3">
<?php foreach ($weekDays as $wd): ?>
<a
href="<?= ($wd['date'] === $today) ? 'planner.php' : ('planner.php?date=' . $wd['date']) ?>"
onclick="switchPlannerDate('<?= $wd['date'] ?>', event); return false;"
class="fociz-card-interactive group relative flex flex-col p-3.5 rounded-2xl border transition-all text-left <?= $wd['is_selected']
? 'bg-gradient-to-b from-indigo-500/15 via-indigo-500/5 to-transparent dark:from-indigo-500/25 dark:via-indigo-500/10 dark:to-transparent border-indigo-500 ring-2 ring-indigo-500/40 shadow-sm'
: 'bg-slate-50/70 dark:bg-[#0c0f17]/50 border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 hover:bg-white dark:hover:bg-[#111624] shadow-2xs' ?>"
>
<div class="flex items-center justify-between mb-1.5">
<span class="text-[11px] font-bold uppercase tracking-wider <?= $wd['is_selected'] ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-500 dark:text-slate-400' ?>">
<?= $wd['short_name'] ?>
</span>
<?php if ($wd['is_today']): ?>
<span class="px-2 py-0.5 rounded-full text-[9px] font-extrabold uppercase bg-indigo-600 text-white shadow-xs">
Nu
</span>
<?php endif; ?>
</div>
<div class="flex items-baseline space-x-1.5 mb-3">
<span class="text-2xl font-black <?= $wd['is_selected'] ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-900 dark:text-white' ?>">
<?= $wd['day_num'] ?>
</span>
<span class="text-[11px] font-semibold text-slate-400 dark:text-slate-500 uppercase">
<?= $wd['month_short'] ?>
</span>
</div>
<!-- Mini chips voor taken en meetings op deze dag -->
<div class="flex flex-col gap-1.5 mt-auto pt-2 border-t border-slate-200/60 dark:border-slate-800/60 text-[10px]">
<div class="flex items-center justify-between px-2 py-1 rounded-lg bg-white/70 dark:bg-slate-900/60 border border-slate-200/50 dark:border-slate-800/50 text-slate-600 dark:text-slate-400">
<span class="flex items-center space-x-1">
<i data-lucide="check-square" class="w-3 h-3 text-indigo-500"></i>
<span>Taken</span>
</span>
<span class="font-bold <?= $wd['task_count'] > 0 ? ($wd['completed_count'] === $wd['task_count'] ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-800 dark:text-slate-200') : 'text-slate-400' ?>">
<?= $wd['completed_count'] ?>/<?= $wd['task_count'] ?>
</span>
</div>
<div class="flex items-center justify-between px-2 py-1 rounded-lg bg-sky-50/70 dark:bg-sky-500/10 border border-sky-200/50 dark:border-sky-500/20 text-sky-800 dark:text-sky-300">
<span class="flex items-center space-x-1">
<i data-lucide="calendar" class="w-3 h-3 text-sky-500"></i>
<span>Agenda</span>
</span>
<span class="font-bold">
<?= $wd['meeting_count'] ?>
</span>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Modular Customizable Widget Grid (Fociz v2.0) -->
<div id="focizWidgetGrid" class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
<?php foreach ($userWidgetLayout as $wId => $widget):
if (empty($widget['enabled'])) continue;
$span = (int)($widget['col_span'] ?? 6);
$colClass = match($span) {
12 => 'lg:col-span-12',
8 => 'lg:col-span-8',
7 => 'lg:col-span-7',
6 => 'lg:col-span-6',
5 => 'lg:col-span-5',
4 => 'lg:col-span-4',
default => 'lg:col-span-6'
};
?>
<div class="<?= $colClass ?> col-span-1 min-w-0 transition-all fociz-widget-wrapper group/widget" data-widget-id="<?= htmlspecialchars($wId) ?>" data-col-span="<?= $span ?>" id="widgetWrapper_<?= htmlspecialchars($wId) ?>">
<!-- Standardized In-Widget Control Toolbar -->
<div class="widget-toolbar flex items-center justify-between px-3 py-1.5 mb-2 rounded-xl bg-slate-100/80 dark:bg-[#0c0f17]/80 border border-slate-200/70 dark:border-slate-800/70 backdrop-blur-xs select-none shadow-2xs">
<div class="flex items-center space-x-2 min-w-0">
<div class="widget-drag-handle flex items-center space-x-1.5 px-2 py-1 rounded-lg cursor-grab active:cursor-grabbing text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-slate-200/60 dark:hover:bg-slate-800/80 transition" title="Sleep om te verplaatsen" draggable="true" data-widget-id="<?= htmlspecialchars($wId) ?>">
<i data-lucide="grip-vertical" class="w-3.5 h-3.5 flex-shrink-0"></i>
<span class="text-xs font-bold text-slate-700 dark:text-slate-200 truncate max-w-[130px] sm:max-w-[200px]"><?= htmlspecialchars($widget['title']) ?></span>
</div>
</div>
<div class="flex items-center space-x-1 sm:space-x-1.5 flex-shrink-0">
<!-- Breedte Knoppen (Inline Resizing) -->
<div class="widget-size-picker inline-flex items-center rounded-lg bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 p-0.5 shadow-2xs">
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 4)" class="widget-span-btn px-1.5 py-0.5 rounded text-[10px] font-bold transition <?= $span == 4 ? 'bg-indigo-600 text-white shadow-2xs' : 'text-slate-500 hover:text-slate-800 dark:hover:text-slate-200' ?>" data-span="4" title="1/3 breedte (4 kolommen)">1/3</button>
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 6)" class="widget-span-btn px-1.5 py-0.5 rounded text-[10px] font-bold transition <?= $span == 6 ? 'bg-indigo-600 text-white shadow-2xs' : 'text-slate-500 hover:text-slate-800 dark:hover:text-slate-200' ?>" data-span="6" title="1/2 breedte (6 kolommen)">1/2</button>
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 8)" class="widget-span-btn px-1.5 py-0.5 rounded text-[10px] font-bold transition <?= $span == 8 ? 'bg-indigo-600 text-white shadow-2xs' : 'text-slate-500 hover:text-slate-800 dark:hover:text-slate-200' ?>" data-span="8" title="2/3 breedte (8 kolommen)">2/3</button>
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 12)" class="widget-span-btn px-1.5 py-0.5 rounded text-[10px] font-bold transition <?= $span == 12 ? 'bg-indigo-600 text-white shadow-2xs' : 'text-slate-500 hover:text-slate-800 dark:hover:text-slate-200' ?>" data-span="12" title="Volledige breedte (12 kolommen)">Vol</button>
</div>
<!-- Collapse / Expand Toggle -->
<button type="button" onclick="toggleWidgetCollapse('<?= $wId ?>')" class="p-1 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-200/60 dark:hover:bg-slate-800/60 transition cursor-pointer" title="Inklappen / Uitklappen">
<i data-lucide="chevron-up" class="w-3.5 h-3.5 widget-collapse-icon transition-transform duration-200" id="collapse_icon_<?= htmlspecialchars($wId) ?>"></i>
</button>
<!-- Verbergen -->
<button type="button" onclick="hideWidgetOnScreen('<?= $wId ?>')" class="p-1 rounded-lg text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-500/10 transition cursor-pointer" title="Verberg widget van dashboard">
<i data-lucide="eye-off" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
<!-- Widget Content Body -->
<div class="widget-content-body transition-all duration-200" id="widget_body_<?= htmlspecialchars($wId) ?>">
<?php if ($wId === 'daily_goal'): ?>
<!-- Widget 1: Dagfocus & Hoofddoel -->
<div class="fociz-card p-5 sm:p-6 border-l-4 border-l-indigo-500 relative overflow-hidden">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div class="flex items-center space-x-3 min-w-0 flex-1">
<div class="w-10 h-10 rounded-2xl bg-indigo-500/10 border border-indigo-500/20 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold shadow-2xs flex-shrink-0">
<i data-lucide="target" class="w-5 h-5"></i>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center space-x-2">
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">Dagfocus & Hoofddoel</h3>
<span class="text-[10px] text-slate-400 font-mono"><?= htmlspecialchars($formattedSelectedDate) ?></span>
</div>
<div class="mt-1 flex items-center space-x-2">
<input
type="text"
id="focusGoalInput"
value="<?= htmlspecialchars($dailyPlan['focus_goal'] ?? '') ?>"
placeholder="Wat is je absolute topprioriteit voor vandaag?"
onkeydown="if(event.key==='Enter'){saveGoal();}"
class="w-full bg-slate-50/80 dark:bg-[#0c0f17]/80 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs sm:text-sm font-semibold text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 transition shadow-2xs"
>
<button
id="saveGoalBtn"
onclick="saveGoal()"
class="px-3.5 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition flex items-center space-x-1.5 flex-shrink-0 cursor-pointer shadow-xs"
>
<i data-lucide="check-circle-2" class="w-3.5 h-3.5"></i>
<span class="hidden sm:inline">Opslaan</span>
</button>
</div>
</div>
</div>
<div class="flex items-center space-x-2 sm:border-l border-slate-100 dark:border-slate-800/80 sm:pl-4 flex-shrink-0">
<div class="text-center px-3 py-1 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/50 border border-slate-200/60 dark:border-slate-800/60">
<span class="text-[10px] uppercase font-bold text-slate-400 block">Taken</span>
<span class="text-xs font-black text-slate-800 dark:text-slate-200"><?= $completedTasksCount ?>/<?= $totalTaskCount ?></span>
</div>
<div class="text-center px-3 py-1 rounded-xl bg-rose-50/70 dark:bg-rose-500/10 border border-rose-200/60 dark:border-rose-500/20">
<span class="text-[10px] uppercase font-bold text-rose-500 block">P1 Focus</span>
<span class="text-xs font-black text-rose-600 dark:text-rose-400"><?= $highPriorityCount ?></span>
</div>
<div class="text-center px-3 py-1 rounded-xl bg-sky-50/70 dark:bg-sky-500/10 border border-sky-200/60 dark:border-sky-500/20">
<span class="text-[10px] uppercase font-bold text-sky-500 block">Meetings</span>
<span class="text-xs font-black text-sky-600 dark:text-sky-400"><?= count($displayedOutlookEvents) ?></span>
</div>
</div>
</div>
</div>
<?php elseif ($wId === 'tasks'): ?>
<!-- Widget 2: Dagtaken & Prioriteiten -->
<div class="fociz-card p-6 sm:p-7">
<div class="flex items-center justify-between mb-5 pb-3 border-b border-slate-100 dark:border-slate-800/60">
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="check-square" class="w-4 h-4 text-indigo-500"></i>
<span>Dagtaken & Prioriteiten</span>
<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300 ring-1 ring-indigo-500/20"><?= count($tasks) ?></span>
</h3>
</div>
<button onclick="toggleAddTaskForm()" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-sm shadow-indigo-600/25 cursor-pointer">
<i data-lucide="plus" class="w-4 h-4"></i>
<span>Nieuwe Taak</span>
</button>
</div>
<!-- Nieuwe taak formulier (verborgen by default) -->
<div id="addTaskForm" class="hidden mb-5 p-5 rounded-2xl bg-slate-50/90 dark:bg-[#0c0f17]/90 border border-indigo-500/30 shadow-xs space-y-3.5 transition">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Taaktitel</label>
<input type="text" id="newTaskTitle" placeholder="Wat moet er vandaag gedaan worden?" class="w-full bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl px-3.5 py-2 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<div>
<label class="block text-[11px] font-semibold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
<select id="newTaskPriority" class="w-full bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="P1">P1 - Brandend / Focus</option>
<option value="P2" selected>P2 - Belangrijk (Standaard)</option>
<option value="P3">P3 - Regulier / Lage druk</option>
</select>
</div>
<div>
<label class="block text-[11px] font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd</label>
<div class="relative">
<input type="text" id="newTaskTime" placeholder="Bijv. 09:30" class="w-full bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-7 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="onNewTaskTimeOrDurationChange('start')">
<i data-lucide="clock" class="w-3.5 h-3.5 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
</div>
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-[11px] font-semibold text-slate-700 dark:text-slate-300">Eindtijd</label>
<span class="text-[9px] text-indigo-600 dark:text-indigo-400 font-medium">Vrij invulbaar</span>
</div>
<div class="relative">
<input type="text" id="newTaskEndTime" placeholder="Bijv. 09:40" class="w-full bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-7 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="onNewTaskTimeOrDurationChange('end')">
<i data-lucide="clock-arrow-down" class="w-3.5 h-3.5 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
</div>
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-[11px] font-semibold text-slate-700 dark:text-slate-300">Tijdsduur</label>
<span id="newTaskDurationDisplay" class="text-[10px] font-mono font-bold text-indigo-600 dark:text-indigo-400">30m</span>
</div>
<div class="flex items-center space-x-1.5">
<?php
$defDuration = (int)($workdaySettings['default_task_duration'] ?? 30);
?>
<select id="newTaskDuration" class="w-full bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500" onchange="onNewTaskTimeOrDurationChange('duration')">
<option value="5" <?= $defDuration === 5 ? 'selected' : '' ?>>5 minuten</option>
<option value="10" <?= $defDuration === 10 ? 'selected' : '' ?>>10 minuten</option>
<option value="15" <?= $defDuration === 15 ? 'selected' : '' ?>>15 minuten</option>
<option value="20" <?= $defDuration === 20 ? 'selected' : '' ?>>20 minuten</option>
<option value="25" <?= $defDuration === 25 ? 'selected' : '' ?>>25 minuten</option>
<option value="30" <?= $defDuration === 30 ? 'selected' : '' ?>>30 minuten</option>
<option value="45" <?= $defDuration === 45 ? 'selected' : '' ?>>45 minuten</option>
<option value="60" <?= $defDuration === 60 ? 'selected' : '' ?>>60 minuten (1 uur)</option>
<option value="90" <?= $defDuration === 90 ? 'selected' : '' ?>>90 minuten (1,5 uur)</option>
<option value="120" <?= $defDuration === 120 ? 'selected' : '' ?>>120 minuten (2 uur)</option>
<option value="custom">Aangepast...</option>
</select>
<input type="number" id="newTaskCustomDuration" min="1" max="720" placeholder="Min" class="hidden w-20 bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-800 rounded-xl px-2 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="onNewTaskTimeOrDurationChange('custom_duration')">
</div>
</div>
</div>
<?php if ($isOutlookConnected): ?>
<div class="p-3 rounded-xl bg-sky-50/80 dark:bg-sky-950/40 border border-sky-200/70 dark:border-sky-800/50">
<label class="flex items-center space-x-2 text-xs text-sky-900 dark:text-sky-200 cursor-pointer select-none">
<input type="checkbox" id="newTaskSyncOutlook" checked class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
<span class="flex items-center space-x-1.5 font-semibold">
<i data-lucide="calendar" class="w-3.5 h-3.5 text-sky-500"></i>
<span>Direct inplannen in Microsoft Outlook agenda</span>
</span>
</label>
</div>
<?php endif; ?>
<div class="flex space-x-2 pt-1">
<button onclick="submitNewTask()" class="px-5 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-xl text-xs transition cursor-pointer shadow-xs">Opslaan</button>
<button onclick="toggleAddTaskForm()" class="px-4 py-2 bg-slate-200 hover:bg-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-400 rounded-xl text-xs transition cursor-pointer">Annuleren</button>
</div>
</div>
<!-- Taken weergave -->
<div class="space-y-2.5" id="tasksContainer">
<?php if (empty($tasks)): ?>
<div class="text-center py-10 text-slate-400 dark:text-slate-500 text-xs">
<i data-lucide="check-circle" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
<p>Nog geen taken gepland voor <?= htmlspecialchars($formattedSelectedDate) ?>.</p>
<p class="text-[11px] mt-0.5">Klik op 'Nieuwe Taak' of importeer uit Jira / Outlook Mail hieronder.</p>
</div>
<?php else: ?>
<?php foreach ($tasks as $task): ?>
<div class="fociz-card-interactive group flex items-center justify-between p-3.5 rounded-2xl border <?= $task['is_completed'] ? 'bg-slate-50/50 dark:bg-slate-950/25 border-slate-200/60 dark:border-slate-800/40 opacity-60' : 'bg-white/95 dark:bg-[#111624]/90 border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-400 dark:hover:border-indigo-500/50 shadow-2xs hover:shadow-xs' ?> <?= $task['priority'] === 'P1' ? 'border-l-4 border-l-rose-500' : ($task['priority'] === 'P2' ? 'border-l-4 border-l-amber-500' : 'border-l-4 border-l-indigo-400 dark:border-l-indigo-500') ?> transition min-w-0" id="task-row-<?= $task['id'] ?>">
<div class="flex items-center space-x-3 truncate mr-2">
<button onclick="toggleTask(<?= $task['id'] ?>, <?= $task['is_completed'] ? 0 : 1 ?>)" class="w-5 h-5 rounded-lg border flex items-center justify-center transition <?= $task['is_completed'] ? 'bg-indigo-600 border-indigo-600 text-white font-bold' : 'border-slate-300 dark:border-slate-700 hover:border-indigo-500 bg-white dark:bg-slate-900' ?>">
<?php if ($task['is_completed']): ?>
<i data-lucide="check" class="w-3.5 h-3.5 stroke-[3]"></i>
<?php endif; ?>
</button>
<div class="truncate">
<div class="flex items-center space-x-1.5 truncate">
<?php if ($task['source'] === 'JIRA'): ?>
<span class="px-1.5 py-0.2 rounded bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-700 dark:text-indigo-400 font-mono text-[9px] font-bold">
JIRA
</span>
<?php elseif ($task['source'] === 'OUTLOOK_MAIL'): ?>
<span class="px-1.5 py-0.2 rounded bg-violet-50 dark:bg-violet-500/10 border border-violet-200 dark:border-violet-500/20 text-violet-700 dark:text-violet-400 font-mono text-[9px] font-bold flex items-center space-x-0.5">
<i data-lucide="mail" class="w-2.5 h-2.5"></i>
<span>MAIL</span>
</span>
<?php elseif ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])): ?>
<span class="px-1.5 py-0.2 rounded bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-700 dark:text-sky-400 font-mono text-[9px] font-bold flex items-center space-x-0.5">
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
<span>OUTLOOK</span>
</span>
<?php endif; ?>
<span class="text-xs font-semibold truncate <?= $task['is_completed'] ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-800 dark:text-slate-200' ?>">
<?= htmlspecialchars($task['title']) ?>
</span>
</div>
<div class="flex items-center space-x-2 mt-0.5">
<?php if (!empty($task['time_block_start'])): ?>
<span class="text-[10px] text-slate-500 dark:text-slate-400 font-mono flex items-center space-x-1">
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
<span><?= htmlspecialchars($task['time_block_start']) ?><?= !empty($task['time_block_end']) ? ' - ' . htmlspecialchars($task['time_block_end']) : '' ?><?= !empty($task['duration_minutes']) ? ' (' . (int)$task['duration_minutes'] . 'm)' : '' ?></span>
</span>
<?php endif; ?>
<?php if (!empty($task['external_url'])): ?>
<a href="<?= htmlspecialchars($task['external_url']) ?>" target="_blank" class="text-[10px] <?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'text-sky-600 dark:text-sky-400' : (($task['source'] === 'OUTLOOK_MAIL') ? 'text-violet-600 dark:text-violet-400' : 'text-indigo-600 dark:text-indigo-400') ?> hover:underline flex items-center space-x-0.5">
<span><?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'Open in Outlook' : (($task['source'] === 'OUTLOOK_MAIL') ? 'Open Mail' : 'Open in Jira') ?></span>
<i data-lucide="external-link" class="w-2.5 h-2.5"></i>
</a>
<?php endif; ?>
</div>
</div>
</div>
<div class="flex items-center space-x-2 flex-shrink-0">
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full <?= $task['priority'] === 'P1' ? 'bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20' : ($task['priority'] === 'P2' ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700') ?>">
<?= htmlspecialchars($task['priority']) ?>
</span>
<?php if ($isOutlookConnected && empty($task['external_id']) && !$task['is_completed']): ?>
<button onclick="syncTaskToOutlook(<?= $task['id'] ?>, this)" title="Synchroniseer deze taak direct naar Outlook Agenda" class="flex items-center space-x-1 text-[10px] font-semibold px-2 py-0.5 rounded-lg bg-sky-50 hover:bg-sky-100 dark:bg-sky-500/10 dark:hover:bg-sky-500/20 text-sky-800 dark:text-sky-300 border border-sky-200 dark:border-sky-500/20 transition cursor-pointer">
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
<span class="hidden sm:inline">Naar Outlook</span>
</button>
<?php endif; ?>
<button onclick="openEditTaskModal(<?= htmlspecialchars(json_encode([
'id' => (int)$task['id'],
'title' => $task['title'],
'priority' => $task['priority'],
'start' => $task['time_block_start'] ?? '',
'end' => $task['time_block_end'] ?? '',
'duration' => (int)($task['duration_minutes'] ?: 30),
'source' => $task['source'],
'external_id' => $task['external_id'] ?? ''
]), ENT_QUOTES, 'UTF-8') ?>)" title="Taak & Tijd aanpassen" class="p-1.5 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-500/10 opacity-0 group-hover:opacity-100 transition cursor-pointer">
<i data-lucide="edit-3" class="w-3.5 h-3.5"></i>
</button>
<button onclick="deleteTask(<?= $task['id'] ?>)" title="Verwijderen" class="p-1.5 rounded-lg text-slate-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-500/10 opacity-0 group-hover:opacity-100 transition cursor-pointer">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php elseif ($wId === 'timeline'): ?>
<!-- Widget 3: Tijdlijn & Time-blocking -->
<div class="fociz-card p-5 sm:p-6 overflow-hidden">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2">
<i data-lucide="clock" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">Tijdlijn & Time-blocking</h3>
</div>
<div class="flex items-center space-x-2">
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
<button onclick="aiSmartSchedule(this)" id="aiScheduleBtn" title="Laat AI Co-Pilot openstaande taken optimaal inplannen om meetings en lunch heen" class="flex items-center space-x-1.5 px-3 py-1 rounded-lg bg-gradient-to-r from-indigo-600 to-sky-600 hover:from-indigo-500 hover:to-sky-500 text-white text-xs font-semibold shadow-xs transition cursor-pointer">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
<span class="hidden sm:inline">AI Slim Indelen</span>
</button>
<?php endif; ?>
<?php if ($isOutlookConnected): ?>
<span class="px-2 py-0.5 rounded-lg bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-700 dark:text-sky-400 text-[10px] font-semibold">Outlook 365</span>
<?php endif; ?>
<span class="text-xs text-slate-500 dark:text-slate-400 font-mono"><?= $hours[0] ?> – <?= end($hours) ?></span>
</div>
</div>
<div class="space-y-2.5">
<?php foreach ($hours as $hour):
$hourInt = (int)explode(':', $hour)[0];
$isLunchSlot = ($hourInt === $lunchHour && !empty($lunchStart) && !empty($lunchEnd));
$slotEvents = array_filter($displayedOutlookEvents, function($e) use ($hourInt) {
if (empty($e['start'])) return false;
return (int)explode(':', $e['start'])[0] === $hourInt;
});
$slotTasks = array_filter($tasks, function($t) use ($hourInt) {
return !empty($t['time_block_start']) && (int)explode(':', $t['time_block_start'])[0] === $hourInt;
});
?>
<div class="flex items-start group min-h-[48px] border-b border-slate-100 dark:border-slate-800/40 pb-2.5 last:border-0 min-w-0">
<div class="w-12 sm:w-14 text-xs font-mono font-semibold text-slate-500 dark:text-slate-400 pt-1.5 flex-shrink-0">
<?= $hour ?>
</div>
<div class="flex-1 space-y-1.5 min-w-0">
<?php if ($isLunchSlot): ?>
<div class="p-3 rounded-xl bg-amber-500/10 dark:bg-amber-500/15 border-l-4 border-l-amber-500 border border-amber-500/20 text-amber-900 dark:text-amber-200 text-xs shadow-2xs flex items-center justify-between min-w-0 transition-all">
<div class="flex items-center space-x-2.5 min-w-0">
<span class="px-2 py-0.5 rounded-md bg-amber-500/20 text-amber-900 dark:text-amber-200 font-mono text-[10px] font-bold flex-shrink-0">
<?= htmlspecialchars($lunchStart) ?> - <?= htmlspecialchars($lunchEnd) ?>
</span>
<span class="font-semibold text-xs flex items-center space-x-1.5 truncate">
<i data-lucide="coffee" class="w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0"></i>
<span class="truncate">Lunchpauze (Gereserveerd)</span>
</span>
</div>
<span class="text-[10px] font-semibold text-amber-700 dark:text-amber-400 flex-shrink-0 ml-2">Pauze</span>
</div>
<?php endif; ?>
<!-- Outlook meetings -->
<?php foreach ($slotEvents as $evt): ?>
<div class="p-3 rounded-xl bg-sky-500/[0.07] dark:bg-sky-500/[0.12] border-l-4 border-l-sky-500 border border-sky-500/20 text-sky-950 dark:text-sky-100 text-xs shadow-2xs min-w-0 overflow-hidden transition-all hover:bg-sky-500/[0.10] dark:hover:bg-sky-500/[0.16]">
<div class="flex items-start justify-between gap-2.5 min-w-0">
<div class="flex items-start space-x-2 min-w-0 flex-1">
<span class="px-2 py-0.5 rounded-md bg-sky-500/15 text-sky-800 dark:text-sky-300 font-mono text-[10px] font-bold flex-shrink-0 whitespace-nowrap mt-0.5">
<?= htmlspecialchars($evt['start']) ?> - <?= htmlspecialchars($evt['end']) ?>
</span>
<span class="font-semibold text-xs leading-snug break-words line-clamp-2 min-w-0 flex-1" title="<?= htmlspecialchars($evt['subject']) ?>">
<?= htmlspecialchars($evt['subject']) ?>
</span>
</div>
<div class="flex items-center space-x-1 flex-shrink-0">
<?php if (!empty($evt['meeting_url'])): ?>
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[11px] text-white bg-indigo-600 hover:bg-indigo-700 px-2.5 py-1 rounded-lg flex-shrink-0 transition font-semibold shadow-xs">
<i data-lucide="video" class="w-3 h-3"></i>
<span class="hidden sm:inline">Deelnemen</span>
</a>
<?php elseif (!empty($evt['web_link'])): ?>
<a href="<?= htmlspecialchars($evt['web_link']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-sky-700 dark:text-sky-300 hover:underline px-1 py-0.5 transition font-medium">
<i data-lucide="external-link" class="w-3 h-3"></i>
<span class="hidden sm:inline">Outlook</span>
</a>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<!-- Geplande taken -->
<?php foreach ($slotTasks as $t): ?>
<div class="p-3 rounded-xl border-l-4 <?= $t['priority'] === 'P1' ? 'border-l-rose-500' : ($t['priority'] === 'P2' ? 'border-l-amber-500' : 'border-l-indigo-400') ?> <?= $t['is_completed'] ? 'bg-slate-100/60 dark:bg-slate-900/40 border border-slate-200 dark:border-slate-800 text-slate-400 dark:text-slate-500 line-through' : 'bg-white/80 dark:bg-[#0c0f17]/70 border border-slate-200/90 dark:border-slate-800 text-slate-900 dark:text-slate-100 shadow-2xs hover:shadow-xs' ?> text-xs min-w-0 overflow-hidden transition-all">
<div class="flex items-start justify-between gap-2.5 min-w-0">
<div class="flex items-start space-x-2 min-w-0 flex-1">
<span class="px-2 py-0.5 rounded-md bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-500/20 font-mono text-[10px] font-bold flex-shrink-0 whitespace-nowrap mt-0.5">
<?= htmlspecialchars($t['time_block_start']) ?><?= !empty($t['time_block_end']) ? ' - ' . htmlspecialchars($t['time_block_end']) : '' ?>
</span>
<span class="font-semibold text-xs leading-snug break-words line-clamp-2 min-w-0 flex-1" title="<?= htmlspecialchars($t['title']) ?>">
<?= htmlspecialchars($t['title']) ?>
</span>
</div>
<div class="flex items-center space-x-1.5 flex-shrink-0">
<?php if (!empty($t['external_id']) && $t['source'] === 'OUTLOOK'): ?>
<span class="text-[9px] font-semibold px-2 py-0.5 rounded bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-500/25 flex items-center space-x-1 whitespace-nowrap" title="In Outlook agenda">
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
<span class="hidden sm:inline">In Outlook</span>
</span>
<?php elseif ($isOutlookConnected && !$t['is_completed']): ?>
<button onclick="syncTaskToOutlook(<?= $t['id'] ?>, this)" title="Synchroniseer naar Outlook Agenda" class="text-[9px] font-semibold px-2 py-0.5 rounded-lg bg-sky-50 hover:bg-sky-100 dark:bg-sky-500/10 dark:hover:bg-sky-500/20 text-sky-800 dark:text-sky-300 border border-sky-200 dark:border-sky-500/20 flex items-center space-x-1 transition cursor-pointer whitespace-nowrap">
<i data-lucide="calendar-plus" class="w-2.5 h-2.5"></i>
<span class="hidden sm:inline">Naar Outlook</span>
</button>
<?php endif; ?>
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full <?= $t['priority'] === 'P1' ? 'bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20' : ($t['priority'] === 'P2' ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700') ?>">
<?= $t['priority'] ?>
</span>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($slotEvents) && empty($slotTasks) && !$isLunchSlot): ?>
<button onclick="quickSchedule('<?= $hour ?>')" class="w-full text-left py-2 px-3 rounded-xl border border-dashed border-slate-200 dark:border-slate-800/80 hover:border-indigo-400 dark:hover:border-indigo-500/50 hover:bg-indigo-500/[0.04] dark:hover:bg-indigo-500/[0.08] text-slate-400 dark:text-slate-500 hover:text-indigo-600 dark:hover:text-indigo-400 text-xs font-medium transition flex items-center justify-between group/slot cursor-pointer">
<span>+ Plan taak om <?= $hour ?></span>
<i data-lucide="plus" class="w-3.5 h-3.5 opacity-0 group-hover/slot:opacity-100 transition-opacity"></i>
</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php elseif ($wId === 'outlook_mail'): ?>
<!-- Widget 4: Outlook E-mail & Inbox -->
<div class="fociz-card p-6" id="plannerMailWidget">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
<div class="flex items-center space-x-2">
<div class="w-7 h-7 rounded-lg bg-sky-500/10 text-sky-600 dark:text-sky-400 flex items-center justify-center font-bold">
<i data-lucide="mail" class="w-4 h-4"></i>
</div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Outlook E-mail
</h3>
<?php if ($mailNeedsReconsent): ?>
<a href="/auth/oauth_microsoft.php?reconsent=1&return_to=/planner.php" class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-amber-50 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/30 hover:underline flex items-center space-x-1" title="Klik om e-mail toestemming te verlenen bij Microsoft">
<i data-lucide="shield-alert" class="w-3 h-3 text-amber-600"></i>
<span>Machtiging vereist &rarr;</span>
</a>
<?php elseif ($unreadEmailCount > 0): ?>
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-rose-50 dark:bg-rose-500/15 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/30">
<?= $unreadEmailCount ?> ongelezen
</span>
<?php endif; ?>
</div>
<a href="/mail.php" class="text-xs text-sky-600 dark:text-sky-400 hover:text-sky-700 dark:hover:text-sky-300 transition flex items-center space-x-1 font-semibold">
<span>Naar Mailbox</span>
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
</a>
</div>
<?php if (empty($recentEmails)): ?>
<div class="text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
<i data-lucide="inbox" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
<p>Geen recente e-mails gevonden in Postvak IN.</p>
</div>
<?php else: ?>
<div class="space-y-2 max-h-72 overflow-y-auto pr-1">
<?php foreach ($recentEmails as $mail): ?>
<div class="fociz-card-interactive flex items-center justify-between p-2.5 rounded-xl <?= !empty($mail['is_read']) ? 'bg-slate-50/70 dark:bg-[#0c0f17]/60' : 'bg-sky-50/40 dark:bg-sky-500/10 border-sky-300/60 dark:border-sky-500/30 font-medium' ?> border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 transition shadow-2xs">
<div class="truncate mr-2 min-w-0 flex-1">
<div class="flex items-center space-x-2">
<?php if (empty($mail['is_read'])): ?>
<span class="w-2 h-2 rounded-full bg-sky-500 flex-shrink-0"></span>
<?php endif; ?>
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
<?= htmlspecialchars($mail['sender_name'] ?? 'Onbekend') ?>
</span>
<span class="text-[10px] text-slate-400 font-mono flex-shrink-0">
<?= !empty($mail['received_at']) ? date('H:i', strtotime($mail['received_at'])) : '' ?>
</span>
</div>
<p class="text-xs text-slate-700 dark:text-slate-300 font-medium truncate mt-0.5">
<?= htmlspecialchars($mail['subject'] ?: '(Geen onderwerp)') ?>
</p>
<p class="text-[11px] text-slate-400 truncate">
<?= htmlspecialchars($mail['body_preview'] ?? '') ?>
</p>
</div>
<div class="flex items-center space-x-1 flex-shrink-0">
<button
onclick="openPlanEmailDirect('<?= htmlspecialchars(addslashes($mail['id'])) ?>', '<?= htmlspecialchars(addslashes($mail['subject'])) ?>', '<?= htmlspecialchars(addslashes($mail['sender_name'])) ?>', '<?= htmlspecialchars(addslashes($mail['web_link'] ?? '')) ?>')"
class="flex items-center space-x-1 text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20 px-2.5 py-1 rounded-lg transition cursor-pointer"
title="Plan deze e-mail direct in als dagtaak"
>
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
<span>Plan</span>
</button>
<a
href="/mail.php?id=<?= urlencode($mail['id']) ?>"
class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition"
title="Open in Mailbox"
>
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php elseif ($wId === 'jira_queue'): ?>
<!-- Widget 5: Jira Tickets & Wachtrijen (Interactive v2.0) -->
<div id="jiraPanel" class="fociz-card p-5 sm:p-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 mb-3 gap-2">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="layers" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Jira Wachtrijen & Tickets
</h3>
</div>
<span id="plannerJiraCountBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold <?= $isJiraLive ? 'bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400' ?>">
<?= count($jiraAllIssues) ?>
</span>
</div>
<a href="/jira.php" class="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition flex items-center space-x-1 font-semibold flex-shrink-0">
<span>Jira Hub</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a>
</div>
<!-- Filter Queue Pills & Quick Search -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2.5 mb-3">
<div class="flex items-center space-x-1 overflow-x-auto pb-1 sm:pb-0 scrollbar-none" id="plannerJiraFilterTabs">
<button type="button" onclick="setPlannerJiraQueue('')" id="pjPill_all" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-indigo-600 text-white shadow-2xs flex-shrink-0 cursor-pointer">
Alle (<span id="pjCount_all"><?= count($jiraAllIssues) ?></span>)
</button>
<button type="button" onclick="setPlannerJiraQueue('my')" id="pjPill_my" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer">
Mijn (<span id="pjCount_my"><?= count($jiraAssignedIssues) ?></span>)
</button>
<button type="button" onclick="setPlannerJiraQueue('DX')" id="pjPill_DX" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer">
DX Queue (<span id="pjCount_DX">0</span>)
</button>
<button type="button" onclick="setPlannerJiraQueue('SD')" id="pjPill_SD" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer">
SD Queue (<span id="pjCount_SD">0</span>)
</button>
</div>
<div class="relative min-w-[140px] sm:w-44 flex-shrink-0">
<i data-lucide="search" class="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400"></i>
<input
type="text"
id="plannerJiraSearch"
oninput="filterPlannerJira()"
placeholder="Filter tickets..."
class="w-full pl-8 pr-2 py-1 text-xs bg-slate-50 dark:bg-[#0c0f17] border border-slate-200/80 dark:border-slate-800/80 rounded-xl text-slate-800 dark:text-slate-200 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
</div>
</div>
<!-- Ticket List Container -->
<div id="plannerJiraListContainer" class="space-y-2 max-h-72 overflow-y-auto pr-1">
<?php if (empty($jiraAllIssues)): ?>
<div class="text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
<i data-lucide="ticket" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
<p>Geen openstaande tickets gevonden in Jira.</p>
</div>
<?php else: ?>
<?php foreach (array_slice($jiraAllIssues, 0, 8) as $issue): ?>
<div class="fociz-card-interactive flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 transition shadow-2xs">
<div class="truncate mr-2 min-w-0 flex-1">
<div class="flex items-center space-x-2">
<span class="font-mono text-[10px] font-bold text-sky-700 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-1.5 py-0.5 rounded">
<?= htmlspecialchars($issue['key'] ?? '') ?>
</span>
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
<?= htmlspecialchars($issue['summary'] ?? '') ?>
</span>
</div>
<span class="text-[10px] text-slate-500 mt-0.5 block">
<?= htmlspecialchars($issue['status'] ?? '') ?> &bull; <?= htmlspecialchars($issue['priority'] ?? '') ?>
</span>
</div>
<button
onclick="quickImportJiraByKey(this, '<?= htmlspecialchars($issue['key'] ?? '') ?>')"
class="flex items-center space-x-1 text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20 px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
title="Plan in als dagtaak"
>
<i data-lucide="plus" class="w-3 h-3"></i>
<span>Plan</span>
</button>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- Lege Toestand (verborgen by default) -->
<div id="plannerJiraEmptyState" class="hidden text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
<i data-lucide="ticket" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
<p>Geen tickets gevonden voor deze filterselectie.</p>
</div>
</div>
<?php elseif ($wId === 'calendar_meetings'): ?>
<!-- Widget 6: Agenda & Teams Meetings -->
<div class="fociz-card p-6" id="plannerMeetingsWidget">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-3">
<div class="flex items-center space-x-2">
<div class="w-7 h-7 rounded-lg bg-teal-500/10 text-teal-600 dark:text-teal-400 flex items-center justify-center font-bold">
<i data-lucide="video" class="w-4 h-4"></i>
</div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Agenda & Teams Meetings
</h3>
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-teal-50 dark:bg-teal-500/15 text-teal-700 dark:text-teal-400 border border-teal-200 dark:border-teal-500/30">
<?= count($displayedOutlookEvents) ?> afspraken
</span>
</div>
<?php if ($isOutlookConnected): ?>
<button onclick="triggerManualSync()" title="Synchroniseer agenda nu" class="text-xs text-teal-600 dark:text-teal-400 hover:underline flex items-center space-x-1 font-semibold cursor-pointer">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
<span>Verversen</span>
</button>
<?php endif; ?>
</div>
<?php if (empty($displayedOutlookEvents)): ?>
<div class="text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
<i data-lucide="calendar-check" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
<p>Geen vergaderingen of afspraken gepland voor <?= htmlspecialchars($formattedSelectedDate) ?>.</p>
</div>
<?php else: ?>
<div class="space-y-2 max-h-72 overflow-y-auto pr-1">
<?php foreach ($displayedOutlookEvents as $evt): ?>
<div class="fociz-card-interactive flex items-center justify-between p-3 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-teal-300 dark:hover:border-teal-600/50 transition shadow-2xs">
<div class="truncate mr-2 min-w-0 flex-1">
<div class="flex items-center space-x-2">
<span class="px-2 py-0.5 rounded-md bg-sky-500/15 text-sky-800 dark:text-sky-300 font-mono text-[10px] font-bold flex-shrink-0">
<?= htmlspecialchars($evt['start'] ?? '') ?> - <?= htmlspecialchars($evt['end'] ?? '') ?>
</span>
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
<?= htmlspecialchars($evt['subject'] ?? 'Afspraak') ?>
</span>
</div>
<?php if (!empty($evt['location'])): ?>
<span class="text-[10px] text-slate-500 mt-0.5 flex items-center space-x-1">
<i data-lucide="map-pin" class="w-3 h-3 text-slate-400"></i>
<span class="truncate"><?= htmlspecialchars($evt['location']) ?></span>
</span>
<?php endif; ?>
</div>
<div class="flex items-center space-x-1 flex-shrink-0">
<?php if (!empty($evt['meeting_url'])): ?>
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[11px] text-white bg-indigo-600 hover:bg-indigo-700 px-2.5 py-1 rounded-lg transition font-semibold shadow-xs">
<i data-lucide="video" class="w-3 h-3"></i>
<span>Deelnemen</span>
</a>
<?php elseif (!empty($evt['web_link'])): ?>
<a href="<?= htmlspecialchars($evt['web_link']) ?>" target="_blank" class="p-1.5 text-sky-600 dark:text-sky-400 hover:text-sky-700 rounded-lg hover:bg-sky-50 dark:hover:bg-sky-500/10 transition" title="Open in Outlook">
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
</a>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php elseif ($wId === 'scratchpad'): ?>
<!-- Widget 7: Dagnotities & Kladblok -->
<div class="fociz-card p-6">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-3">
<div class="flex items-center space-x-2">
<div class="w-7 h-7 rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center justify-center font-bold">
<i data-lucide="edit-3" class="w-4 h-4"></i>
</div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Dagnotities & Kladblok
</h3>
</div>
<span id="scratchpadSaveStatus" class="text-[10px] text-slate-400 font-mono">Automatisch opgeslagen</span>
</div>
<textarea
id="dailyNotesInput"
rows="5"
placeholder="Belangrijke aantekeningen, reflecties of snelle memo's voor vandaag..."
oninput="autoSaveScratchpad()"
onblur="saveGoal()"
class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 dark:border-slate-800 rounded-xl p-3 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 resize-none transition shadow-2xs leading-relaxed"
><?= htmlspecialchars($dailyPlan['notes'] ?? '') ?></textarea>
</div>
<?php elseif ($wId === 'stats'): ?>
<!-- Widget 8: Dagstatistieken & Voortgang -->
<div class="fociz-card p-6">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
<div class="flex items-center space-x-2">
<div class="w-7 h-7 rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="bar-chart-2" class="w-4 h-4"></i>
</div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Dagstatistieken & Voortgang
</h3>
</div>
<span class="text-xs font-mono font-bold text-indigo-600 dark:text-indigo-400"><?= $taskProgressPercent ?>% voltooid</span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div class="p-4 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/70 dark:border-slate-800/70">
<span class="text-[10px] font-bold uppercase text-slate-400 block mb-1">Voltooide Taken</span>
<div class="flex items-baseline space-x-1.5">
<span class="text-2xl font-black text-slate-900 dark:text-white"><?= $completedTasksCount ?></span>
<span class="text-xs text-slate-400 font-semibold">/ <?= $totalTaskCount ?></span>
</div>
<div class="w-full bg-slate-200 dark:bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden">
<div class="h-full bg-emerald-500 rounded-full" style="width: <?= $taskProgressPercent ?>%"></div>
</div>
</div>
<div class="p-4 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/70 dark:border-slate-800/70">
<span class="text-[10px] font-bold uppercase text-rose-500 block mb-1">P1 Focus Taken</span>
<div class="flex items-baseline space-x-1.5">
<span class="text-2xl font-black text-rose-600 dark:text-rose-400"><?= $highPriorityCount ?></span>
<span class="text-xs text-slate-400 font-semibold">openstaand</span>
</div>
<span class="text-[10px] text-slate-400 mt-1 block">Hoogste urgentie</span>
</div>
<div class="p-4 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/70 dark:border-slate-800/70">
<span class="text-[10px] font-bold uppercase text-sky-500 block mb-1">Geplande Tijd</span>
<?php
$hoursPlanned = floor($totalPlannedMinutes / 60);
$minsPlanned = $totalPlannedMinutes % 60;
?>
<div class="flex items-baseline space-x-1.5">
<span class="text-2xl font-black text-sky-600 dark:text-sky-400"><?= $hoursPlanned ?>u <?= $minsPlanned ?>m</span>
</div>
<span class="text-[10px] text-slate-400 mt-1 block">In timeblocks</span>
</div>
<div class="p-4 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/70 dark:border-slate-800/70">
<span class="text-[10px] font-bold uppercase text-teal-500 block mb-1">Agenda Meetings</span>
<div class="flex items-baseline space-x-1.5">
<span class="text-2xl font-black text-teal-600 dark:text-teal-400"><?= count($displayedOutlookEvents) ?></span>
<span class="text-xs text-slate-400 font-semibold">afspraken</span>
</div>
<span class="text-[10px] text-slate-400 mt-1 block">In Outlook 365</span>
</div>
</div>
</div>
<?php elseif ($wId === 'quick_links'): ?>
<!-- Widget 9: Snelle Koppelingen & Tools -->
<div class="fociz-card p-6">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
<div class="flex items-center space-x-2">
<div class="w-7 h-7 rounded-lg bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center font-bold">
<i data-lucide="external-link" class="w-4 h-4"></i>
</div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Snelle Koppelingen & Tools
</h3>
</div>
<span class="text-[10px] text-slate-400 font-mono">Werktools</span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2.5">
<a href="/jira.php" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-400 dark:hover:border-indigo-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
<div class="w-7 h-7 rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold flex-shrink-0">
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
</div>
<div class="truncate">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-indigo-600">Jira Cloud</span>
<span class="text-[10px] text-slate-400">Tickets</span>
</div>
</a>
<a href="/mail.php" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-sky-400 dark:hover:border-sky-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
<div class="w-7 h-7 rounded-lg bg-sky-500/10 text-sky-600 dark:text-sky-400 flex items-center justify-center font-bold flex-shrink-0">
<i data-lucide="mail" class="w-3.5 h-3.5"></i>
</div>
<div class="truncate">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-sky-600">Outlook Mail</span>
<span class="text-[10px] text-slate-400">Postvak IN</span>
</div>
</a>
<a href="https://teams.microsoft.com" target="_blank" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-violet-400 dark:hover:border-violet-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
<div class="w-7 h-7 rounded-lg bg-violet-500/10 text-violet-600 dark:text-violet-400 flex items-center justify-center font-bold flex-shrink-0">
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
</div>
<div class="truncate">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-violet-600">MS Teams</span>
<span class="text-[10px] text-slate-400">Chat & Calls</span>
</div>
</a>
<a href="/board.php" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-amber-400 dark:hover:border-amber-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
<div class="w-7 h-7 rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center justify-center font-bold flex-shrink-0">
<i data-lucide="kanban" class="w-3.5 h-3.5"></i>
</div>
<div class="truncate">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-amber-600">Kanban Bord</span>
<span class="text-[10px] text-slate-400">Sprint & Work</span>
</div>
</a>
<a href="https://portal.azure.com" target="_blank" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-blue-400 dark:hover:border-blue-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
<div class="w-7 h-7 rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400 flex items-center justify-center font-bold flex-shrink-0">
<i data-lucide="cloud" class="w-3.5 h-3.5"></i>
</div>
<div class="truncate">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-blue-600">Azure Portal</span>
<span class="text-[10px] text-slate-400">Cloud & DevOps</span>
</div>
</a>
<a href="/profile.php" class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-teal-400 dark:hover:border-teal-500/50 hover:bg-white dark:hover:bg-[#111624] transition flex items-center space-x-2.5 group shadow-2xs">
<div class="w-7 h-7 rounded-lg bg-teal-500/10 text-teal-600 dark:text-teal-400 flex items-center justify-center font-bold flex-shrink-0">
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
</div>
<div class="truncate">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block truncate group-hover:text-teal-600">Instellingen</span>
<span class="text-[10px] text-slate-400">Profiel & Koppel</span>
</div>
</a>
</div>
</div>
<?php elseif ($wId === 'habit_tracker'): ?>
<!-- Widget 10: Dagelijkse Focus Gewoontes -->
<div class="fociz-card p-6" id="habitTrackerWidget">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
<div class="flex items-center space-x-2">
<div class="w-7 h-7 rounded-lg bg-rose-500/10 text-rose-600 dark:text-rose-400 flex items-center justify-center font-bold">
<i data-lucide="sparkles" class="w-4 h-4"></i>
</div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Dagelijkse Focus Gewoontes
</h3>
</div>
<span id="habitProgressBadge" class="text-xs font-mono font-bold text-rose-600 dark:text-rose-400">0/5 gedaan</span>
</div>
<div class="space-y-2.5">
<?php
$habits = [
['id' => 'water', 'label' => '💧 Voldoende water gedronken (2L)'],
['id' => 'walk', 'label' => '🚶‍♂️ Minimaal 15 minuten bewogen / gewandeld'],
['id' => 'inbox', 'label' => '📭 Postvak IN opgeruimd & acties ingepland'],
['id' => 'standup', 'label' => '👥 Standup & overleg voorbereid'],
['id' => 'reflection', 'label' => '🧘 Dag afgesloten met reflectie & planning morgen'],
];
foreach ($habits as $h): ?>
<label class="flex items-center justify-between p-3 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-rose-300 dark:hover:border-rose-500/30 transition cursor-pointer select-none">
<span class="text-xs font-semibold text-slate-800 dark:text-slate-200"><?= $h['label'] ?></span>
<input type="checkbox" data-habit-id="<?= $h['id'] ?>" onchange="updateHabitCount()" class="w-4 h-4 rounded text-rose-600 focus:ring-rose-500 border-slate-300 dark:border-slate-700 cursor-pointer">
</label>
<?php endforeach; ?>
</div>
</div>
<?php elseif ($wId === 'focus_sound'): ?>
<!-- Widget 11: Focus Sounds & Omgeving -->
<div class="fociz-card p-6" id="focusSoundWidget">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
<div class="flex items-center space-x-2">
<div class="w-7 h-7 rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="headphones" class="w-4 h-4"></i>
</div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Focus Sounds & Rust
</h3>
</div>
<span id="soundStatusText" class="text-[10px] text-slate-400 font-mono">Gepauzeerd</span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2.5">
<button onclick="toggleSound('rain', this)" class="sound-btn p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-400 transition text-center cursor-pointer">
<span class="text-xl block mb-1">🌧️</span>
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block">Zachte Regen</span>
</button>
<button onclick="toggleSound('forest', this)" class="sound-btn p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-emerald-400 transition text-center cursor-pointer">
<span class="text-xl block mb-1">🌲</span>
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block">Bos & Vogels</span>
</button>
<button onclick="toggleSound('cafe', this)" class="sound-btn p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-amber-400 transition text-center cursor-pointer">
<span class="text-xl block mb-1">☕</span>
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block">Koffiebar</span>
</button>
<button onclick="toggleSound('whitenoise', this)" class="sound-btn p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-sky-400 transition text-center cursor-pointer">
<span class="text-xl block mb-1">🌊</span>
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block">Witte Ruis</span>
</button>
</div>
</div>
<?php elseif ($wId === 'team_status'): ?>
<!-- Widget 12: Team & Beschikbaarheid -->
<div class="fociz-card p-6">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-4">
<div class="flex items-center space-x-2">
<div class="w-7 h-7 rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400 flex items-center justify-center font-bold">
<i data-lucide="users" class="w-4 h-4"></i>
</div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Team & Beschikbaarheid
</h3>
</div>
<span class="text-[10px] text-slate-400 font-mono">Vandaag</span>
</div>
<div class="space-y-2.5">
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-full bg-emerald-500/20 text-emerald-700 dark:text-emerald-300 font-bold text-xs flex items-center justify-center">
<?= strtoupper(substr($currentUser['name'] ?? 'Ik', 0, 2)) ?>
</div>
<div>
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block"><?= htmlspecialchars($currentUser['name'] ?? 'Jij') ?> (Jij)</span>
<span class="text-[10px] text-slate-400">Actief in Fociz</span>
</div>
</div>
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-500/30">
Beschikbaar
</span>
</div>
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-full bg-amber-500/20 text-amber-700 dark:text-amber-300 font-bold text-xs flex items-center justify-center">
SD
</div>
<div>
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 block">Servicedesk Team</span>
<span class="text-[10px] text-slate-400">DX & SD Wachtrij</span>
</div>
</div>
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300 border border-amber-200 dark:border-amber-500/30">
Tickets opvolgen
</span>
</div>
</div>
</div>
<?php endif; ?>
</div><!-- /.widget-content-body -->
</div><!-- /.fociz-widget-wrapper -->
<?php endforeach; ?>
</div>
<?php
if ($isAjaxRequest) {
$contentHtml = ob_get_clean();
while (ob_get_level() > 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;
}
?>
<?php if (!$isAjaxRequest): ?>
</div>
<?php endif; ?>
<!-- Modal: Taak & Tijdblok Bewerken (Vrij invullen van tijden & duur) -->
<div id="editTaskModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
<div class="fociz-card p-6 w-full max-w-lg shadow-2xl space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
<i data-lucide="edit-3" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Taak & Tijdblok Bewerken</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400">Pas titel, start-/eindtijd en tijdsduur vrij aan</p>
</div>
</div>
<button onclick="closeEditTaskModal()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<input type="hidden" id="editTaskId">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Taaktitel</label>
<input type="text" id="editTaskTitle" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
<select id="editTaskPriority" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<option value="P1">P1 - Brandend / Focus</option>
<option value="P2">P2 - Belangrijk (Standaard)</option>
<option value="P3">P3 - Regulier / Lage druk</option>
</select>
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Tijdsduur</label>
<span id="editTaskDurationDisplay" class="text-[11px] font-mono font-bold text-indigo-600 dark:text-indigo-400">30m</span>
</div>
<div class="flex items-center space-x-1.5">
<select id="editTaskDuration" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" onchange="onEditTaskTimeOrDurationChange('duration')">
<option value="5">5 minuten</option>
<option value="10">10 minuten</option>
<option value="15">15 minuten</option>
<option value="20">20 minuten</option>
<option value="25">25 minuten</option>
<option value="30">30 minuten</option>
<option value="45">45 minuten</option>
<option value="60">60 minuten (1 uur)</option>
<option value="90">90 minuten (1,5 uur)</option>
<option value="120">120 minuten (2 uur)</option>
<option value="custom">Aangepast...</option>
</select>
<input type="number" id="editTaskCustomDuration" min="1" max="720" placeholder="Min" class="hidden w-20 bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-2 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" oninput="onEditTaskTimeOrDurationChange('custom_duration')">
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd</label>
<div class="relative">
<input type="text" id="editTaskStartTime" placeholder="Bijv. 09:30" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" oninput="onEditTaskTimeOrDurationChange('start')">
<i data-lucide="clock" class="w-4 h-4 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
</div>
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Eindtijd</label>
<span class="text-[9px] text-indigo-600 dark:text-indigo-400 font-medium">Vrij invulbaar</span>
</div>
<div class="relative">
<input type="text" id="editTaskEndTime" placeholder="Bijv. 09:40" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" oninput="onEditTaskTimeOrDurationChange('end')">
<i data-lucide="clock-arrow-down" class="w-4 h-4 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
</div>
</div>
</div>
<div id="editTaskOutlookNotice" class="hidden p-3 rounded-xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-800 dark:text-sky-300 text-[11px] flex items-center space-x-2">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5 flex-shrink-0"></i>
<span>Deze taak is gekoppeld aan Outlook 365. Wijzigingen worden automatisch in uw Outlook agenda gesynchroniseerd.</span>
</div>
<div class="flex justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800/80">
<button type="button" onclick="closeEditTaskModal()" class="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
Annuleren
</button>
<button type="button" onclick="submitEditTask()" id="btnSaveEditTask" class="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer">
Wijzigingen Opslaan
</button>
</div>
</div>
</div>
<!-- Modal: Direct E-mail Inplannen als Dagtaak -->
<div id="planEmailDirectModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
<div class="fociz-card p-6 w-full max-w-lg shadow-2xl space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center">
<i data-lucide="mail" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">E-mail Inplannen in Fociz</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400">Zet deze Outlook mail direct om in een concrete dagtaak</p>
</div>
</div>
<button onclick="closePlanEmailDirectModal()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<input type="hidden" id="planEmailId">
<input type="hidden" id="planEmailWebLink">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Afzender</label>
<input type="text" id="planEmailSender" readonly class="w-full bg-slate-100/80 dark:bg-[#0c0f17]/40 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-500 dark:text-slate-400 text-xs cursor-not-allowed">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Taaktitel</label>
<input type="text" id="planEmailTitle" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
<select id="planEmailPriority" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<option value="P1">P1 - Brandend / Focus</option>
<option value="P2" selected>P2 - Belangrijk (Standaard)</option>
<option value="P3">P3 - Regulier / Lage druk</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Geschatte Duur</label>
<select id="planEmailDuration" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<option value="15">15 minuten</option>
<option value="30" selected>30 minuten</option>
<option value="45">45 minuten</option>
<option value="60">60 minuten (1 uur)</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd (Optioneel)</label>
<input type="text" id="planEmailStartTime" placeholder="Bijv. 14:00" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
</div>
<div class="flex items-center pt-5">
<label class="flex items-center space-x-2 text-xs text-slate-700 dark:text-slate-300 cursor-pointer select-none">
<input type="checkbox" id="planEmailSyncOutlook" class="w-4 h-4 rounded text-sky-600 focus:ring-sky-500 border-slate-300 dark:border-slate-700" <?= $isOutlookConnected ? 'checked' : 'disabled' ?>>
<span>Blokkeer in Outlook Agenda</span>
</label>
</div>
</div>
<div class="flex justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800/80">
<button type="button" onclick="closePlanEmailDirectModal()" class="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
Annuleren
</button>
<button type="button" onclick="submitPlanEmailDirect()" id="btnSubmitPlanEmailDirect" class="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer flex items-center space-x-1.5">
<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i>
<span>Taak Inplannen</span>
</button>
</div>
</div>
</div>
<!-- Modal: Widgets & Raster Indeling Beheren (Fociz v2.0) -->
<div id="widgetManagerModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
<div class="fociz-card p-6 w-full max-w-2xl shadow-2xl space-y-4 max-h-[90vh] flex flex-col">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 flex-shrink-0">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
<i data-lucide="layout-grid" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Fociz Widgets & Indeling Beheren</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400">Activeer, versleep en bepaal de breedte van je werkstation-widgets</p>
</div>
</div>
<button onclick="closeWidgetManagerModal()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto pr-1 space-y-2.5" id="widgetManagerList">
<!-- Dynamisch gevuld via renderWidgetManagerList() -->
</div>
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80 flex-shrink-0">
<button type="button" onclick="resetWidgetLayoutToDefault()" class="text-xs text-slate-500 hover:text-rose-600 dark:text-slate-400 dark:hover:text-rose-400 font-semibold transition cursor-pointer">
Herstel Standaard
</button>
<div class="flex items-center space-x-2">
<button type="button" onclick="closeWidgetManagerModal()" class="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
Annuleren
</button>
<button type="button" onclick="saveWidgetLayoutConfig()" id="btnSaveWidgetLayout" class="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer flex items-center space-x-1.5">
<i data-lucide="save" class="w-3.5 h-3.5"></i>
<span>Indeling Opslaan</span>
</button>
</div>
</div>
</div>
</div>
<script>
// --- Fociz Widget Beheer & Configuratielogica (v2.0) ---
let focizWidgets = <?= json_encode(array_values($userWidgetLayout), JSON_UNESCAPED_UNICODE) ?>;
const defaultWidgetLayout = <?= json_encode(array_values($availableWidgets), JSON_UNESCAPED_UNICODE) ?>;
function openWidgetManagerModal() {
renderWidgetManagerList();
const modal = document.getElementById('widgetManagerModal');
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
}
}
function closeWidgetManagerModal() {
const modal = document.getElementById('widgetManagerModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
}
function renderWidgetManagerList() {
const list = document.getElementById('widgetManagerList');
if (!list) return;
list.innerHTML = focizWidgets.map((w, index) => {
return `
<div class="flex items-center justify-between p-3 rounded-xl border ${w.enabled ? 'bg-slate-50/90 dark:bg-[#0c0f17]/70 border-slate-200/80 dark:border-slate-800/80' : 'bg-slate-100/40 dark:bg-slate-900/30 border-dashed border-slate-200 dark:border-slate-800/60 opacity-60'} transition-all gap-3">
<div class="flex items-center space-x-3 min-w-0 flex-1">
<div class="flex flex-col space-y-0.5">
<button type="button" onclick="moveWidgetOrder(${index}, -1)" ${index === 0 ? 'disabled' : ''} class="p-1 text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 disabled:opacity-20 disabled:cursor-not-allowed rounded transition">
<i data-lucide="chevron-up" class="w-3.5 h-3.5"></i>
</button>
<button type="button" onclick="moveWidgetOrder(${index}, 1)" ${index === focizWidgets.length - 1 ? 'disabled' : ''} class="p-1 text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 disabled:opacity-20 disabled:cursor-not-allowed rounded transition">
<i data-lucide="chevron-down" class="w-3.5 h-3.5"></i>
</button>
</div>
<label class="flex items-center space-x-2.5 cursor-pointer min-w-0 flex-1 select-none">
<input type="checkbox" ${w.enabled ? 'checked' : ''} onchange="toggleWidgetEnabled('${w.id}', this.checked)" class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
<div class="min-w-0 flex-1">
<div class="flex items-center space-x-2">
<span class="text-xs font-bold text-slate-900 dark:text-white truncate">${w.title}</span>
<span class="px-2 py-0.5 text-[9px] font-bold rounded-full bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400">${w.category || 'Algemeen'}</span>
</div>
<p class="text-[10px] text-slate-400 truncate mt-0.5">${w.desc || ''}</p>
</div>
</label>
</div>
<div class="flex items-center space-x-2 flex-shrink-0">
<select onchange="changeWidgetSpan('${w.id}', this.value)" class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="12" ${w.col_span == 12 ? 'selected' : ''}>100% (Hele breedte)</option>
<option value="8" ${w.col_span == 8 ? 'selected' : ''}>66% (Tweederde)</option>
<option value="7" ${w.col_span == 7 ? 'selected' : ''}>58% (Grote helft)</option>
<option value="6" ${w.col_span == 6 ? 'selected' : ''}>50% (Halve breedte)</option>
<option value="5" ${w.col_span == 5 ? 'selected' : ''}>42% (Kleine helft)</option>
<option value="4" ${w.col_span == 4 ? 'selected' : ''}>33% (Eenderde)</option>
</select>
</div>
</div>
`;
}).join('');
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
}
function toggleWidgetEnabled(id, isEnabled) {
const w = focizWidgets.find(item => item.id === id);
if (w) {
w.enabled = isEnabled;
renderWidgetManagerList();
}
}
function changeWidgetSpan(id, span) {
const w = focizWidgets.find(item => item.id === id);
if (w) {
w.col_span = parseInt(span, 10);
}
}
function moveWidgetOrder(index, direction) {
const targetIndex = index + direction;
if (targetIndex < 0 || targetIndex >= focizWidgets.length) return;
const temp = focizWidgets[index];
focizWidgets[index] = focizWidgets[targetIndex];
focizWidgets[targetIndex] = temp;
focizWidgets.forEach((item, idx) => { item.order = idx + 1; });
renderWidgetManagerList();
}
function resetWidgetLayoutToDefault() {
if (!confirm('Standaard raster indeling en alle widgets herstellen?')) return;
focizWidgets = JSON.parse(JSON.stringify(defaultWidgetLayout));
renderWidgetManagerList();
}
async function saveWidgetLayoutConfig() {
const btn = document.getElementById('btnSaveWidgetLayout');
const origHtml = btn.innerHTML;
btn.innerText = 'Opslaan...';
btn.disabled = true;
try {
const payload = focizWidgets.map((w, idx) => ({
id: w.id,
enabled: !!w.enabled,
col_span: parseInt(w.col_span, 10) || 6,
order: idx + 1
}));
const res = await fetch('/api/planner.php?action=save_widget_layout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ layout: payload })
});
const data = await res.json();
if (data.success) {
btn.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5"></i><span>Opgeslagen!</span>';
if (typeof lucide !== 'undefined') lucide.createIcons();
setTimeout(() => {
closeWidgetManagerModal();
window.location.reload();
}, 600);
} else {
alert(data.error || 'Kon widget layout niet opslaan');
btn.innerHTML = origHtml;
btn.disabled = false;
}
} catch (e) {
alert('Netwerkfout bij opslaan widget layout');
btn.innerHTML = origHtml;
btn.disabled = false;
}
}
// --- HTML Escaping Helper (XSS preventie) ---
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// --- Floating Toast Notificatie ---
function showWidgetToast(htmlMessage, type = 'success', duration = 3500) {
let toast = document.getElementById('widgetGlobalToast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'widgetGlobalToast';
toast.className = 'fixed bottom-6 right-6 z-50 transform translate-y-20 opacity-0 transition-all duration-300 pointer-events-auto flex items-center space-x-2.5 px-4 py-2.5 rounded-2xl bg-slate-900/95 dark:bg-white text-white dark:text-slate-900 text-xs font-semibold shadow-2xl border border-slate-700/60 dark:border-slate-200/80';
document.body.appendChild(toast);
}
const icon = type === 'success' ? 'check-circle-2' : 'info';
toast.innerHTML = `
<div class="w-5 h-5 rounded-full ${type === 'success' ? 'bg-emerald-500/20 text-emerald-400 dark:text-emerald-600' : 'bg-indigo-500/20 text-indigo-400 dark:text-indigo-600'} flex items-center justify-center flex-shrink-0">
<i data-lucide="${icon}" class="w-3.5 h-3.5"></i>
</div>
<div class="flex-1">${htmlMessage}</div>
`;
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
toast.classList.remove('translate-y-20', 'opacity-0');
toast.classList.add('translate-y-0', 'opacity-100');
if (window._widgetToastTimeout) clearTimeout(window._widgetToastTimeout);
window._widgetToastTimeout = setTimeout(() => {
toast.classList.add('translate-y-20', 'opacity-0');
toast.classList.remove('translate-y-0', 'opacity-100');
}, duration);
}
// --- Achtergrond Auto-Save voor Widget Layout ---
async function persistWidgetLayoutAuto() {
try {
const payload = focizWidgets.map((w, idx) => ({
id: w.id,
enabled: !!w.enabled,
col_span: parseInt(w.col_span, 10) || 6,
order: idx + 1
}));
const res = await fetch('/api/planner.php?action=save_widget_layout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ layout: payload })
});
const data = await res.json();
return data.success;
} catch (e) {
console.warn('Fout bij automatisch opslaan widget layout:', e);
return false;
}
}
// --- In-Widget Inline Breedte Aanpassen (1/3, 1/2, 2/3, 100%) ---
function setWidgetSpan(wId, span) {
span = parseInt(span, 10);
const wrapper = document.getElementById(`widgetWrapper_${wId}`) || document.querySelector(`.fociz-widget-wrapper[data-widget-id="${wId}"]`);
if (!wrapper) return;
// Verwijder alle mogelijke col-span klassen
const spanClasses = ['lg:col-span-12', 'lg:col-span-8', 'lg:col-span-7', 'lg:col-span-6', 'lg:col-span-5', 'lg:col-span-4'];
spanClasses.forEach(c => wrapper.classList.remove(c));
// Voeg nieuwe klasse toe
wrapper.classList.add(`lg:col-span-${span}`);
wrapper.setAttribute('data-col-span', span);
// Knoppen styling bijwerken binnen deze widget
const buttons = wrapper.querySelectorAll('.widget-span-btn');
buttons.forEach(btn => {
const bSpan = parseInt(btn.getAttribute('data-span'), 10);
if (bSpan === span) {
btn.className = "widget-span-btn px-1.5 py-0.5 rounded text-[10px] font-bold transition bg-indigo-600 text-white shadow-2xs";
} else {
btn.className = "widget-span-btn px-1.5 py-0.5 rounded text-[10px] font-bold transition text-slate-500 hover:text-slate-800 dark:hover:text-slate-200";
}
});
// Update in-memory array
const item = focizWidgets.find(x => x.id === wId);
if (item) {
item.col_span = span;
}
// Trigger resize voor SVG en diagrammen
window.dispatchEvent(new Event('resize'));
// Automatisch opslaan naar database
persistWidgetLayoutAuto();
const spanDescriptions = {
4: '1/3 schermbreedte (33%)',
6: '1/2 schermbreedte (50%)',
8: '2/3 schermbreedte (66%)',
12: 'volledige breedte (100%)'
};
showWidgetToast(`Formaat aangepast naar ${spanDescriptions[span] || span + ' kolommen'}`, 'success');
}
// --- Widget Inklappen / Uitklappen ---
function toggleWidgetCollapse(wId) {
const body = document.getElementById(`widget_body_${wId}`);
const icon = document.getElementById(`collapse_icon_${wId}`);
if (!body) return;
const isNowHidden = body.classList.toggle('hidden');
if (icon) {
if (isNowHidden) {
icon.classList.add('rotate-180');
} else {
icon.classList.remove('rotate-180');
}
}
try {
localStorage.setItem(`fociz_widget_collapsed_${wId}`, isNowHidden ? '1' : '0');
} catch (e) {}
}
// --- Widget Verbergen van Scherm met Undo Toast ---
function hideWidgetOnScreen(wId) {
const wrapper = document.getElementById(`widgetWrapper_${wId}`) || document.querySelector(`.fociz-widget-wrapper[data-widget-id="${wId}"]`);
const item = focizWidgets.find(x => x.id === wId);
const title = item ? item.title : 'Widget';
if (wrapper) {
wrapper.style.transition = 'all 0.25s ease';
wrapper.style.opacity = '0';
wrapper.style.transform = 'scale(0.95)';
setTimeout(() => {
wrapper.remove();
}, 250);
}
if (item) {
item.enabled = false;
}
persistWidgetLayoutAuto();
showWidgetToast(`Widget "${title}" verborgen <button type="button" onclick="undoHideWidget('${wId}')" class="underline font-bold ml-2 text-indigo-400 hover:text-indigo-300 cursor-pointer">Ongedaan maken</button>`, 'info', 6000);
}
function undoHideWidget(wId) {
const item = focizWidgets.find(x => x.id === wId);
if (item) {
item.enabled = true;
}
persistWidgetLayoutAuto();
showWidgetToast('Widget herstellen...', 'success', 2000);
setTimeout(() => {
window.location.reload();
}, 400);
}
// --- Direct On-Screen Drag & Drop Engine (HTML5 Native) ---
let draggedWidgetEl = null;
let dropPlaceholderEl = null;
function initPlannerWidgets() {
const grid = document.getElementById('focizWidgetGrid');
if (!grid) return;
// Herstel eventuele ingeklapte widgets vanuit localStorage
document.querySelectorAll('.fociz-widget-wrapper').forEach(wrapper => {
const wId = wrapper.getAttribute('data-widget-id');
try {
if (localStorage.getItem(`fociz_widget_collapsed_${wId}`) === '1') {
const body = document.getElementById(`widget_body_${wId}`);
const icon = document.getElementById(`collapse_icon_${wId}`);
if (body) body.classList.add('hidden');
if (icon) icon.classList.add('rotate-180');
}
} catch(e) {}
});
if (!dropPlaceholderEl) {
dropPlaceholderEl = document.createElement('div');
dropPlaceholderEl.id = 'widgetDropPlaceholder';
dropPlaceholderEl.className = 'col-span-1 min-w-0 border-2 border-dashed border-indigo-400/80 dark:border-indigo-500/80 bg-indigo-50/50 dark:bg-indigo-950/40 rounded-2xl flex items-center justify-center p-8 text-xs font-bold text-indigo-600 dark:text-indigo-400 pointer-events-none transition-all duration-150';
dropPlaceholderEl.innerHTML = '<div class="flex items-center space-x-2"><i data-lucide="arrow-down-circle" class="w-4 h-4"></i><span>Laat los om hier te plaatsen</span></div>';
}
// Luister naar drag handles
document.querySelectorAll('.widget-drag-handle').forEach(handle => {
handle.setAttribute('draggable', 'true');
handle.ondragstart = (e) => {
const wrapper = handle.closest('.fociz-widget-wrapper');
if (!wrapper) return;
draggedWidgetEl = wrapper;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', wrapper.getAttribute('data-widget-id'));
// Match column span van dragged widget
const curSpan = wrapper.getAttribute('data-col-span') || '6';
dropPlaceholderEl.className = `lg:col-span-${curSpan} col-span-1 min-w-0 border-2 border-dashed border-indigo-400/80 dark:border-indigo-500/80 bg-indigo-50/60 dark:bg-indigo-950/50 rounded-2xl flex items-center justify-center p-8 text-xs font-bold text-indigo-600 dark:text-indigo-400 pointer-events-none transition-all`;
setTimeout(() => {
if (draggedWidgetEl) {
draggedWidgetEl.classList.add('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/50');
}
}, 0);
};
handle.ondragend = (e) => {
if (draggedWidgetEl) {
draggedWidgetEl.classList.remove('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/50');
}
if (dropPlaceholderEl && dropPlaceholderEl.parentNode) {
dropPlaceholderEl.remove();
}
draggedWidgetEl = null;
};
});
// Dragover op grid
grid.ondragover = (e) => {
if (!draggedWidgetEl) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const target = e.target.closest('.fociz-widget-wrapper');
if (target && target !== draggedWidgetEl && target !== dropPlaceholderEl) {
const rect = target.getBoundingClientRect();
const isAfter = (e.clientY - rect.top) > (rect.height / 2) || (e.clientX - rect.left) > (rect.width / 2);
if (isAfter) {
if (target.nextSibling !== dropPlaceholderEl) {
target.after(dropPlaceholderEl);
}
} else {
if (target.previousSibling !== dropPlaceholderEl) {
target.before(dropPlaceholderEl);
}
}
}
};
grid.ondrop = (e) => {
if (!draggedWidgetEl) return;
e.preventDefault();
if (dropPlaceholderEl && dropPlaceholderEl.parentNode) {
dropPlaceholderEl.replaceWith(draggedWidgetEl);
}
if (draggedWidgetEl) {
draggedWidgetEl.classList.remove('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/50');
}
// Volgorde bijwerken
const currentWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
currentWrappers.forEach((el, index) => {
const wId = el.getAttribute('data-widget-id');
const item = focizWidgets.find(x => x.id === wId);
if (item) {
item.order = index + 1;
}
});
persistWidgetLayoutAuto();
showWidgetToast('Widget verplaatst & indeling opgeslagen', 'success');
draggedWidgetEl = null;
};
}
// --- Jira Queue & Tickets Widget Engine (v2.0) ---
const plannerJiraAssigned = <?= json_encode($jiraAssignedIssues, JSON_UNESCAPED_UNICODE) ?>;
const plannerJiraAll = <?= json_encode($jiraAllIssues, JSON_UNESCAPED_UNICODE) ?>;
const plannerTicketStore = new Map();
plannerJiraAll.forEach(iss => {
if (iss && iss.key) plannerTicketStore.set(iss.key, iss);
});
let activePlannerJiraQueue = ''; // '': Alle, 'my': Mijn, 'DX': DX Queue, 'SD': SD Queue
function isIssueInQueue(iss, qCode) {
if (!qCode) return true;
const code = qCode.toUpperCase();
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
const sum = (iss.summary || '').toLowerCase();
const dxTerms = ['koppeling', 'sso', 'endpoint', 'rest', 'datadump', 'uwv', 'migratie', 'api', 'webhook', 'integrat'];
const matchesDx = dxTerms.some(t => ol.includes(t) || sum.includes(t));
if (code === 'DX') return matchesDx;
if (code === 'SD') return !matchesDx;
return true;
}
function setPlannerJiraQueue(queueCode) {
activePlannerJiraQueue = queueCode;
// Tab styling bijwerken
['all', 'my', 'DX', 'SD'].forEach(tab => {
const btn = document.getElementById(`pjPill_${tab}`);
if (!btn) return;
const isActive = (tab === 'all' && !queueCode) || (tab === queueCode);
if (isActive) {
btn.className = "planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-indigo-600 text-white shadow-2xs flex-shrink-0 cursor-pointer";
} else {
btn.className = "planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer";
}
});
filterPlannerJira();
}
function filterPlannerJira() {
const searchVal = (document.getElementById('plannerJiraSearch')?.value || '').toLowerCase().trim();
const listContainer = document.getElementById('plannerJiraListContainer');
const emptyState = document.getElementById('plannerJiraEmptyState');
if (!listContainer) return;
// Tellers bijwerken
let dxCount = 0;
let sdCount = 0;
plannerJiraAll.forEach(iss => {
if (isIssueInQueue(iss, 'DX')) dxCount++;
else sdCount++;
});
const elAll = document.getElementById('pjCount_all');
const elMy = document.getElementById('pjCount_my');
const elDx = document.getElementById('pjCount_DX');
const elSd = document.getElementById('pjCount_SD');
if (elAll) elAll.innerText = plannerJiraAll.length;
if (elMy) elMy.innerText = plannerJiraAssigned.length;
if (elDx) elDx.innerText = dxCount;
if (elSd) elSd.innerText = sdCount;
// Filteren
let filtered = plannerJiraAll.filter(iss => {
if (activePlannerJiraQueue === 'my') {
const isAssigned = plannerJiraAssigned.some(a => a.key === iss.key);
if (!isAssigned) return false;
} else if (activePlannerJiraQueue === 'DX') {
if (!isIssueInQueue(iss, 'DX')) return false;
} else if (activePlannerJiraQueue === 'SD') {
if (!isIssueInQueue(iss, 'SD')) return false;
}
if (searchVal) {
const k = (iss.key || '').toLowerCase();
const s = (iss.summary || '').toLowerCase();
const p = (iss.project || '').toLowerCase();
const ol = (iss.onderwerp_logging || '').toLowerCase();
if (!k.includes(searchVal) && !s.includes(searchVal) && !p.includes(searchVal) && !ol.includes(searchVal)) {
return false;
}
}
return true;
});
// Badge totale resultaten bijwerken
const badge = document.getElementById('plannerJiraCountBadge');
if (badge) badge.innerText = filtered.length;
if (filtered.length === 0) {
listContainer.innerHTML = '';
if (emptyState) emptyState.classList.remove('hidden');
return;
}
if (emptyState) emptyState.classList.add('hidden');
listContainer.innerHTML = filtered.slice(0, 15).map(issue => {
const keyEsc = escapeHtml(issue.key || '');
const summaryEsc = escapeHtml(issue.summary || '');
const statusEsc = escapeHtml(issue.status || 'To Do');
const priorityEsc = escapeHtml(issue.priority || 'Medium');
const isDx = isIssueInQueue(issue, 'DX');
const queuePill = isDx
? '<span class="px-1.5 py-0.5 rounded text-[9px] font-bold bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20">DX</span>'
: '<span class="px-1.5 py-0.5 rounded text-[9px] font-bold bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-200 dark:border-blue-500/20">SD</span>';
return `
<div class="fociz-card-interactive flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 transition shadow-2xs">
<div class="truncate mr-2 min-w-0 flex-1">
<div class="flex items-center space-x-2">
<span class="font-mono text-[10px] font-bold text-sky-700 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-1.5 py-0.5 rounded">
${keyEsc}
</span>
${queuePill}
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
${summaryEsc}
</span>
</div>
<span class="text-[10px] text-slate-500 mt-0.5 block">
${statusEsc} &bull; ${priorityEsc}
</span>
</div>
<button
onclick="quickImportJiraByKey(this, '${keyEsc}')"
class="flex items-center space-x-1 text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20 px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
title="Plan in als dagtaak"
>
<i data-lucide="plus" class="w-3 h-3"></i>
<span>Plan</span>
</button>
</div>
`;
}).join('');
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
}
async function quickImportJiraByKey(btn, issueKey) {
const issue = plannerTicketStore.get(issueKey);
if (!issue) return;
return quickImportJira(btn, issue.summary || issueKey, issue.key, issue.url || '');
}
// --- Direct E-mail Inplannen als Dagtaak ---
function openPlanEmailDirect(mailId, subject, sender, webLink) {
document.getElementById('planEmailId').value = mailId;
document.getElementById('planEmailTitle').value = subject ? `[Mail] ${subject}` : '[Mail] E-mail opvolgen';
document.getElementById('planEmailSender').value = sender || 'Outlook Mail';
document.getElementById('planEmailWebLink').value = webLink || '';
const modal = document.getElementById('planEmailDirectModal');
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
}
}
function closePlanEmailDirectModal() {
const modal = document.getElementById('planEmailDirectModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
}
async function submitPlanEmailDirect() {
const mailId = document.getElementById('planEmailId').value;
const title = document.getElementById('planEmailTitle').value.trim();
const priority = document.getElementById('planEmailPriority').value;
const duration = parseInt(document.getElementById('planEmailDuration').value, 10) || 30;
const startTime = document.getElementById('planEmailStartTime').value.trim();
const syncOutlook = document.getElementById('planEmailSyncOutlook').checked;
const webLink = document.getElementById('planEmailWebLink').value;
if (!title) {
alert('Vul een taaktitel in');
return;
}
const btn = document.getElementById('btnSubmitPlanEmailDirect');
const origHtml = btn.innerHTML;
btn.innerText = 'Inplannen...';
btn.disabled = true;
try {
const res = await fetch('/api/mail.php?action=plan_email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mail_id: mailId,
title: title,
priority: priority,
duration_minutes: duration,
time_block_start: startTime,
sync_outlook: syncOutlook,
date: activePlannerDate
})
});
const data = await res.json();
if (data.success) {
closePlanEmailDirectModal();
switchPlannerDate(activePlannerDate, null, false);
} else {
alert(data.error || 'Kon e-mail niet inplannen');
}
} catch(err) {
alert('Netwerkfout bij inplannen van e-mail');
} finally {
btn.innerHTML = origHtml;
btn.disabled = false;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
// --- Scratchpad Debounced Auto-Save ---
let scratchpadTimer = null;
function autoSaveScratchpad() {
const statusEl = document.getElementById('scratchpadSaveStatus');
if (statusEl) statusEl.innerText = 'Wijzigingen worden opgeslagen...';
clearTimeout(scratchpadTimer);
scratchpadTimer = setTimeout(async () => {
const notes = document.getElementById('dailyNotesInput').value;
try {
await fetch('/api/planner.php?action=save_scratchpad', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notes: notes, date: activePlannerDate })
});
if (statusEl) statusEl.innerText = 'Automatisch opgeslagen';
} catch (e) {
if (statusEl) statusEl.innerText = 'Fout bij opslaan';
}
}, 800);
}
// --- Habit Tracker Logica ---
function updateHabitCount() {
const checkboxes = document.querySelectorAll('#habitTrackerWidget input[type="checkbox"]');
const total = checkboxes.length;
let checked = 0;
checkboxes.forEach(cb => { if (cb.checked) checked++; });
const badge = document.getElementById('habitProgressBadge');
if (badge) {
badge.innerText = `${checked}/${total} gedaan`;
if (checked === total) {
badge.classList.remove('text-rose-600', 'dark:text-rose-400');
badge.classList.add('text-emerald-600', 'dark:text-emerald-400');
} else {
badge.classList.add('text-rose-600', 'dark:text-rose-400');
badge.classList.remove('text-emerald-600', 'dark:text-emerald-400');
}
}
}
// --- Focus Sounds Web Audio Synthesizer ---
let activeSoundAudio = null;
let currentSoundType = null;
function toggleSound(type, btn) {
const statusText = document.getElementById('soundStatusText');
const allBtns = document.querySelectorAll('.sound-btn');
if (currentSoundType === type) {
if (activeSoundAudio) {
activeSoundAudio.pause();
activeSoundAudio = null;
}
currentSoundType = null;
if (statusText) statusText.innerText = 'Gepauzeerd';
allBtns.forEach(b => b.classList.remove('ring-2', 'ring-indigo-500', 'bg-indigo-50/80', 'dark:bg-indigo-500/20'));
return;
}
if (activeSoundAudio) {
activeSoundAudio.pause();
}
allBtns.forEach(b => b.classList.remove('ring-2', 'ring-indigo-500', 'bg-indigo-50/80', 'dark:bg-indigo-500/20'));
currentSoundType = type;
btn.classList.add('ring-2', 'ring-indigo-500', 'bg-indigo-50/80', 'dark:bg-indigo-500/20');
if (statusText) {
const names = { rain: 'Regen speelt...', forest: 'Bos speelt...', cafe: 'Koffiebar speelt...', whitenoise: 'Witte ruis speelt...' };
statusText.innerText = names[type] || 'Focus sound actief';
}
}
// --- Real-Time Custom Events van Notifications.js ---
window.addEventListener('fociz:mail-updated', () => {
if (document.getElementById('plannerMailWidget')) {
switchPlannerDate(activePlannerDate, null, false);
}
});
window.addEventListener('fociz:jira-updated', () => {
if (document.getElementById('jiraPanel')) {
switchPlannerDate(activePlannerDate, null, false);
}
});
window.addEventListener('fociz:calendar-updated', () => {
switchPlannerDate(activePlannerDate, null, false);
});
// Veilige stubs voor verwijderde Focus Modus timer (voorkomt JavaScript runtime fouten)
function selectFocusTask(title) {}
function updateTimerDisplay() {}
function setTimerPreset(min) {}
function toggleTimer() {}
function resetTimer() {}
function saveTimerStateBeforeReload() {}
function restoreTimerStateIfAny() {}
// --- Globale Planner Status & Naadloos AJAX Datumbeheer ---
let activePlannerDate = '<?= $selectedDate ?>';
let weekStartDate = '<?= $weekStart ?>';
let weekEndDate = '<?= $weekEnd ?>';
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
let isDateSwitching = false;
/**
* Wisselt de actieve plannerdatum geruisloos via AJAX
* Behoudt de huidige schermhoogte (scrollpositie) en voorkomt flitsen/herladen
*/
async function switchPlannerDate(newDate, event = null, pushState = true) {
if (event && event.preventDefault) {
event.preventDefault();
}
if (!newDate || isDateSwitching) return;
const container = document.getElementById('plannerContentArea');
if (!container) {
window.location.href = 'planner.php?date=' + encodeURIComponent(newDate);
return;
}
// Onthoud exact de huidige scrollpositie
const currentScrollY = window.scrollY;
isDateSwitching = true;
// Subtiele visuele feedback zonder layout-shift
container.classList.add('opacity-60', 'pointer-events-none');
try {
const response = await fetch(`planner.php?date=${encodeURIComponent(newDate)}&ajax=1`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP fout ${response.status}`);
}
const data = await response.json();
if (!data || !data.success || !data.html) {
throw new Error(data?.error || 'Ongeldige serverrespons');
}
// Vervang de dynamische plannerinhoud geruisloos
container.innerHTML = data.html;
// Werk globale variabelen bij
activePlannerDate = data.selectedDate;
if (data.weekStart) weekStartDate = data.weekStart;
if (data.weekEnd) weekEndDate = data.weekEnd;
if (data.syncHash) currentSyncHash = data.syncHash;
// Werk paginatitel bij
if (data.title) {
document.title = data.title;
}
// Werk URL bij in adresbalk zonder paginaherlaad
if (pushState && window.history && window.history.pushState) {
const todayStr = '<?= $today ?>';
const newUrl = (newDate === todayStr) ? 'planner.php' : `planner.php?date=${encodeURIComponent(newDate)}`;
window.history.pushState({ date: newDate }, data.title || '', newUrl);
}
// Render Lucide SVG-iconen in de nieuwe inhoud
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
initPlannerWidgets();
if (document.getElementById('plannerJiraListContainer')) {
filterPlannerJira();
}
// Garandeer dat het scherm exact op dezelfde scrollhoogte blijft
window.scrollTo({
top: currentScrollY,
behavior: 'instant'
});
} catch (err) {
console.error('Fout bij wisselen van datum via AJAX:', err);
} finally {
isDateSwitching = false;
container.classList.remove('opacity-60', 'pointer-events-none');
}
}
// Browser Terug/Vooruit knoppen opvangen (popstate)
window.addEventListener('popstate', (event) => {
const urlParams = new URLSearchParams(window.location.search);
const dateFromUrl = urlParams.get('date') || '<?= $today ?>';
if (dateFromUrl !== activePlannerDate) {
switchPlannerDate(dateFromUrl, null, false);
}
});
// Schoon direct de browser URL op indien deze ?date=VANDAAG bevat
if (window.history && window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('date') === '<?= $today ?>') {
window.history.replaceState({}, document.title, 'planner.php');
}
}
// --- Dagdoel & Taken AJAX ---
async function saveGoal() {
const focusGoal = document.getElementById('focusGoalInput').value;
const notes = document.getElementById('dailyNotesInput').value;
const btn = document.getElementById('saveGoalBtn');
btn.innerText = 'Opslaan...';
try {
await fetch('/api/planner.php?action=save_goal', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ focus_goal: focusGoal, notes: notes, date: activePlannerDate })
});
btn.innerHTML = '<i data-lucide="check" class="w-4 h-4"></i><span>Opgeslagen</span>';
if (typeof lucide !== 'undefined') lucide.createIcons();
setTimeout(() => {
btn.innerHTML = '<i data-lucide="check-circle-2" class="w-4 h-4"></i><span>Opslaan</span>';
if (typeof lucide !== 'undefined') lucide.createIcons();
}, 1500);
} catch(err) {
btn.innerText = 'Fout';
}
}
// --- Tijdblok Hulpfuncties & Vrije Duur Berekening ---
function parseTimeToMinutes(str) {
if (!str || typeof str !== 'string') return null;
str = str.trim();
if (!str) return null;
let m = str.match(/^(\d{1,2})[:.](\d{2})$/);
if (m) {
const hours = parseInt(m[1], 10);
const mins = parseInt(m[2], 10);
if (hours >= 0 && hours < 24 && mins >= 0 && mins < 60) {
return (hours * 60) + mins;
}
}
m = str.match(/^(\d{1,2})$/);
if (m) {
const hours = parseInt(m[1], 10);
if (hours >= 0 && hours < 24) {
return hours * 60;
}
}
return null;
}
function formatMinutesToTime(totalMins) {
totalMins = (totalMins + (24 * 60)) % (24 * 60);
const h = Math.floor(totalMins / 60);
const m = totalMins % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}
// Tweeweg synchronisatie bij Nieuwe Taak (Starttijd, Eindtijd & Tijdsduur)
function onNewTaskTimeOrDurationChange(source) {
const startInput = document.getElementById('newTaskTime');
const endInput = document.getElementById('newTaskEndTime');
const durationSelect = document.getElementById('newTaskDuration');
const customDurationInput = document.getElementById('newTaskCustomDuration');
const durationDisplay = document.getElementById('newTaskDurationDisplay');
if (!startInput || !endInput || !durationSelect) return;
const startMins = parseTimeToMinutes(startInput.value);
if (source === 'end') {
const endMins = parseTimeToMinutes(endInput.value);
if (startMins !== null && endMins !== null && endMins > startMins) {
const diffMins = endMins - startMins;
setSelectedNewTaskDuration(diffMins);
}
} else if (source === 'duration') {
const val = durationSelect.value;
if (val === 'custom') {
customDurationInput.classList.remove('hidden');
customDurationInput.focus();
const customVal = parseInt(customDurationInput.value, 10);
if (!isNaN(customVal) && customVal > 0 && startMins !== null) {
endInput.value = formatMinutesToTime(startMins + customVal);
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
}
} else {
customDurationInput.classList.add('hidden');
const dur = parseInt(val, 10);
if (!isNaN(dur) && dur > 0) {
if (durationDisplay) durationDisplay.innerText = `${dur}m`;
if (startMins !== null) {
endInput.value = formatMinutesToTime(startMins + dur);
}
}
}
} else if (source === 'custom_duration') {
const customVal = parseInt(customDurationInput.value, 10);
if (!isNaN(customVal) && customVal > 0) {
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
if (startMins !== null) {
endInput.value = formatMinutesToTime(startMins + customVal);
}
}
} else if (source === 'start') {
let dur = getSelectedNewTaskDurationMinutes();
if (startMins !== null) {
if (endInput.value && parseTimeToMinutes(endInput.value) !== null) {
const currentEndMins = parseTimeToMinutes(endInput.value);
if (currentEndMins > startMins) {
setSelectedNewTaskDuration(currentEndMins - startMins);
} else if (dur > 0) {
endInput.value = formatMinutesToTime(startMins + dur);
}
} else if (dur > 0) {
endInput.value = formatMinutesToTime(startMins + dur);
}
}
}
}
function getSelectedNewTaskDurationMinutes() {
const durationSelect = document.getElementById('newTaskDuration');
const customDurationInput = document.getElementById('newTaskCustomDuration');
if (!durationSelect) return 30;
if (durationSelect.value === 'custom') {
const val = parseInt(customDurationInput?.value, 10);
return (!isNaN(val) && val > 0) ? val : 30;
}
const val = parseInt(durationSelect.value, 10);
return (!isNaN(val) && val > 0) ? val : 30;
}
function setSelectedNewTaskDuration(mins) {
const durationSelect = document.getElementById('newTaskDuration');
const customDurationInput = document.getElementById('newTaskCustomDuration');
const durationDisplay = document.getElementById('newTaskDurationDisplay');
if (!durationSelect) return;
let found = false;
for (let i = 0; i < durationSelect.options.length; i++) {
if (durationSelect.options[i].value === String(mins)) {
durationSelect.selectedIndex = i;
found = true;
break;
}
}
if (!found) {
durationSelect.value = 'custom';
if (customDurationInput) {
customDurationInput.value = mins;
customDurationInput.classList.remove('hidden');
}
} else {
if (customDurationInput) customDurationInput.classList.add('hidden');
}
if (durationDisplay) durationDisplay.innerText = `${mins}m`;
}
function toggleAddTaskForm() {
const form = document.getElementById('addTaskForm');
form.classList.toggle('hidden');
if (!form.classList.contains('hidden')) {
document.getElementById('newTaskTitle').focus();
}
}
function quickSchedule(hour) {
const form = document.getElementById('addTaskForm');
if (form && form.classList.contains('hidden')) {
form.classList.remove('hidden');
}
const timeInput = document.getElementById('newTaskTime');
if (timeInput) {
timeInput.value = hour;
onNewTaskTimeOrDurationChange('start');
}
const outlookCheckbox = document.getElementById('newTaskSyncOutlook');
if (outlookCheckbox) outlookCheckbox.checked = true;
document.getElementById('newTaskTitle')?.focus();
}
async function submitNewTask() {
const title = document.getElementById('newTaskTitle').value.trim();
const priority = document.getElementById('newTaskPriority').value;
const time = document.getElementById('newTaskTime').value.trim();
const endTime = document.getElementById('newTaskEndTime') ? document.getElementById('newTaskEndTime').value.trim() : '';
const duration = getSelectedNewTaskDurationMinutes();
const syncOutlook = document.getElementById('newTaskSyncOutlook') ? document.getElementById('newTaskSyncOutlook').checked : false;
if (!title) {
alert('Vul een taaktitel in.');
return;
}
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,
time_block_end: endTime,
duration_minutes: duration,
sync_to_outlook: syncOutlook,
date: activePlannerDate
})
});
const data = await res.json();
if (data.success) {
if (syncOutlook && !data.outlook_synced) {
alert('Let op: De taak is opgeslagen in Fociz, maar kon niet direct naar Microsoft Outlook worden verzonden:\n\n' + (data.outlook_error || 'Controleer uw verbinding in Mijn Profiel.'));
}
toggleAddTaskForm();
const titleInput = document.getElementById('newTaskTitle');
if (titleInput) titleInput.value = '';
switchPlannerDate(activePlannerDate, null, false);
} else {
alert(data.error || 'Fout bij opslaan van de taak');
}
} catch(err) {
alert('Netwerkfout bij opslaan van de taak');
}
}
// --- Taak & Tijdblok Bewerken Modal Logica ---
function openEditTaskModal(task) {
if (!task) return;
document.getElementById('editTaskId').value = task.id;
document.getElementById('editTaskTitle').value = task.title || '';
document.getElementById('editTaskPriority').value = task.priority || 'P2';
document.getElementById('editTaskStartTime').value = task.start || '';
document.getElementById('editTaskEndTime').value = task.end || '';
const dur = task.duration || 30;
setSelectedEditTaskDuration(dur);
const outlookNotice = document.getElementById('editTaskOutlookNotice');
if (outlookNotice) {
if (task.source === 'OUTLOOK' || task.external_id) {
outlookNotice.classList.remove('hidden');
} else {
outlookNotice.classList.add('hidden');
}
}
const modal = document.getElementById('editTaskModal');
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined') lucide.createIcons();
document.getElementById('editTaskTitle')?.focus();
}
}
function closeEditTaskModal() {
const modal = document.getElementById('editTaskModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
}
function onEditTaskTimeOrDurationChange(source) {
const startInput = document.getElementById('editTaskStartTime');
const endInput = document.getElementById('editTaskEndTime');
const durationSelect = document.getElementById('editTaskDuration');
const customDurationInput = document.getElementById('editTaskCustomDuration');
const durationDisplay = document.getElementById('editTaskDurationDisplay');
if (!startInput || !endInput || !durationSelect) return;
const startMins = parseTimeToMinutes(startInput.value);
if (source === 'end') {
const endMins = parseTimeToMinutes(endInput.value);
if (startMins !== null && endMins !== null && endMins > startMins) {
const diffMins = endMins - startMins;
setSelectedEditTaskDuration(diffMins);
}
} else if (source === 'duration') {
const val = durationSelect.value;
if (val === 'custom') {
customDurationInput.classList.remove('hidden');
customDurationInput.focus();
const customVal = parseInt(customDurationInput.value, 10);
if (!isNaN(customVal) && customVal > 0 && startMins !== null) {
endInput.value = formatMinutesToTime(startMins + customVal);
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
}
} else {
customDurationInput.classList.add('hidden');
const dur = parseInt(val, 10);
if (!isNaN(dur) && dur > 0) {
if (durationDisplay) durationDisplay.innerText = `${dur}m`;
if (startMins !== null) {
endInput.value = formatMinutesToTime(startMins + dur);
}
}
}
} else if (source === 'custom_duration') {
const customVal = parseInt(customDurationInput.value, 10);
if (!isNaN(customVal) && customVal > 0) {
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
if (startMins !== null) {
endInput.value = formatMinutesToTime(startMins + customVal);
}
}
} else if (source === 'start') {
let dur = getSelectedEditTaskDurationMinutes();
if (startMins !== null) {
if (endInput.value && parseTimeToMinutes(endInput.value) !== null) {
const currentEndMins = parseTimeToMinutes(endInput.value);
if (currentEndMins > startMins) {
setSelectedEditTaskDuration(currentEndMins - startMins);
} else if (dur > 0) {
endInput.value = formatMinutesToTime(startMins + dur);
}
} else if (dur > 0) {
endInput.value = formatMinutesToTime(startMins + dur);
}
}
}
}
function getSelectedEditTaskDurationMinutes() {
const durationSelect = document.getElementById('editTaskDuration');
const customDurationInput = document.getElementById('editTaskCustomDuration');
if (!durationSelect) return 30;
if (durationSelect.value === 'custom') {
const val = parseInt(customDurationInput?.value, 10);
return (!isNaN(val) && val > 0) ? val : 30;
}
const val = parseInt(durationSelect.value, 10);
return (!isNaN(val) && val > 0) ? val : 30;
}
function setSelectedEditTaskDuration(mins) {
const durationSelect = document.getElementById('editTaskDuration');
const customDurationInput = document.getElementById('editTaskCustomDuration');
const durationDisplay = document.getElementById('editTaskDurationDisplay');
if (!durationSelect) return;
let found = false;
for (let i = 0; i < durationSelect.options.length; i++) {
if (durationSelect.options[i].value === String(mins)) {
durationSelect.selectedIndex = i;
found = true;
break;
}
}
if (!found) {
durationSelect.value = 'custom';
if (customDurationInput) {
customDurationInput.value = mins;
customDurationInput.classList.remove('hidden');
}
} else {
if (customDurationInput) customDurationInput.classList.add('hidden');
}
if (durationDisplay) durationDisplay.innerText = `${mins}m`;
}
async function submitEditTask() {
const taskId = parseInt(document.getElementById('editTaskId').value, 10);
const title = document.getElementById('editTaskTitle').value.trim();
const priority = document.getElementById('editTaskPriority').value;
const startTime = document.getElementById('editTaskStartTime').value.trim();
const endTime = document.getElementById('editTaskEndTime').value.trim();
const duration = getSelectedEditTaskDurationMinutes();
const saveBtn = document.getElementById('btnSaveEditTask');
if (!title) {
alert('Vul een taaktitel in.');
return;
}
if (saveBtn) {
saveBtn.innerText = 'Opslaan...';
saveBtn.disabled = true;
}
try {
const res = await fetch('/api/planner.php?action=update_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
task_id: taskId,
title: title,
priority: priority,
time_block_start: startTime,
time_block_end: endTime,
duration_minutes: duration
})
});
const data = await res.json();
if (data.success) {
closeEditTaskModal();
switchPlannerDate(activePlannerDate, null, false);
} else {
alert(data.error || 'Kon taak niet bijwerken');
if (saveBtn) {
saveBtn.innerText = 'Wijzigingen Opslaan';
saveBtn.disabled = false;
}
}
} catch (e) {
alert('Netwerkfout bij bijwerken van de taak');
if (saveBtn) {
saveBtn.innerText = 'Wijzigingen Opslaan';
saveBtn.disabled = false;
}
}
}
async function syncTaskToOutlook(taskId, btn) {
if (!btn) return;
const originalHtml = btn.innerHTML;
btn.innerHTML = '<span class="text-[9px] animate-pulse font-medium">Syncen...</span>';
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 = '<i data-lucide="check" class="w-3.5 h-3.5 text-teal-500"></i>';
if (typeof lucide !== 'undefined') lucide.createIcons();
setTimeout(() => {
switchPlannerDate(activePlannerDate, null, false);
}, 300);
} 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();
}
}
async function quickImportJira(btn, title, jiraKey, url) {
const originalHtml = btn.innerHTML;
btn.innerText = '...';
btn.disabled = true;
try {
const res = await fetch('/api/planner.php?action=add_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: `[${jiraKey}] ${title}`,
priority: 'P1',
source: 'JIRA',
external_id: jiraKey,
external_url: url,
date: activePlannerDate
})
});
const data = await res.json();
if (data.success) {
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3"></i>';
switchPlannerDate(activePlannerDate, null, false);
} else {
btn.innerHTML = originalHtml;
btn.disabled = false;
alert(data.error || 'Kon taak niet toevoegen');
}
} catch(err) {
btn.innerHTML = originalHtml;
btn.disabled = false;
alert('Netwerkfout bij toevoegen');
}
}
async function toggleTask(taskId, isCompleted) {
await fetch('/api/planner.php?action=toggle_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ task_id: taskId, is_completed: isCompleted })
});
switchPlannerDate(activePlannerDate, null, false);
}
async function deleteTask(taskId) {
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'},
body: JSON.stringify({ task_id: taskId })
});
switchPlannerDate(activePlannerDate, null, false);
}
// --- AI Co-Pilot Slimme Dagplanning (Release v1.5) ---
async function aiSmartSchedule(btn) {
if (!confirm('Wil je AI Co-Pilot jouw openstaande taken voor deze dag slim laten inplannen rondom je Outlook meetings en lunch?')) return;
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.innerHTML = '<span class="text-xs font-bold animate-pulse flex items-center space-x-1"><i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Indelen...</span></span>';
btn.disabled = true;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
try {
const res = await fetch('/api/ai.php?action=smart_schedule', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ date: activePlannerDate })
});
const data = await res.json();
if (data.success) {
alert(`Succes! AI Co-Pilot heeft ${data.scheduled_count} ta(a)k(en) optimaal ingepland op je tijdlijn.`);
switchPlannerDate(activePlannerDate, null, false);
} else {
alert(data.error || 'Kon dagplanning niet genereren via AI');
if (btn) {
btn.innerHTML = origHtml;
btn.disabled = false;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
} catch(err) {
alert('Netwerkfout bij aanroepen van AI Co-Pilot');
if (btn) {
btn.innerHTML = origHtml;
btn.disabled = false;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
}
// --- Outlook 365 Auto-Sync & Achtergrond Controle ---
const isOutlookConnected = <?= $isOutlookConnected ? 'true' : 'false' ?>;
const syncIntervalSeconds = <?= (int)($syncIntervalSeconds ?? 30) ?>;
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...';
await switchPlannerDate(activePlannerDate, null, false);
if (statusText) statusText.innerText = 'Outlook 365 Live';
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();
initPlannerWidgets();
if (document.getElementById('plannerJiraListContainer')) {
filterPlannerJira();
}
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'; ?>