Files
Fociz/planner.php
T
NiekRabelink 5ce08443cc feat: Add story points support and AI daily briefing feature
- Enhanced JIRA integration to fetch story points from multiple custom fields.
- Updated task display to show story points and their estimated duration.
- Introduced AI daily briefing banner with dynamic content loading.
- Implemented subtasks functionality with add, toggle, and delete capabilities.
- Improved task management UI with better accessibility and interaction.
2026-09-10 22:38:06 +02:00

3872 lines
226 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'],
'timeline' => ['id' => 'timeline', 'title' => 'Tijdlijn & Dag/Weekoverzicht', 'category' => 'Agenda', 'icon' => 'calendar', 'enabled' => true, 'col_span' => 12, 'order' => 2, 'desc' => 'Gecombineerd weekoverzicht, dagselectie en uren-tijdlijn met time-blocking en AI'],
'tasks' => ['id' => 'tasks', 'title' => 'Dagtaken & Prioriteiten', 'category' => 'Planning', 'icon' => 'check-square', 'enabled' => true, 'col_span' => 7, 'order' => 3, 'desc' => 'Volledige takenlijst met P1/P2/P3, tijdvakken en Outlook synchronisatie'],
'outlook_mail' => ['id' => 'outlook_mail', 'title' => 'Outlook E-mail Widget', 'category' => 'Communicatie', 'icon' => 'mail', 'enabled' => true, 'col_span' => 6, 'order' => 4, 'desc' => 'Inkomende e-mails direct inzien en met 1 klik inplannen als taak'],
'jira_queue' => ['id' => 'jira_queue', 'title' => 'Jira Tickets & Wachtrijen', 'category' => 'Ontwikkeling', 'icon' => 'layers', 'enabled' => true, 'col_span' => 6, 'order' => 5, 'desc' => 'Openstaande Jira tickets (DX/SD wachtrijen) met directe importknop'],
'calendar_meetings' => ['id' => 'calendar_meetings', 'title' => 'Agenda & Teams Meetings', 'category' => 'Agenda', 'icon' => 'video', 'enabled' => true, 'col_span' => 6, 'order' => 6, 'desc' => 'Vandaag geplande afspraken met 1-klik Teams deelnemen knop'],
'scratchpad' => ['id' => 'scratchpad', 'title' => 'Dagnotities & Kladblok', 'category' => 'Productiviteit', 'icon' => 'edit-3', 'enabled' => true, 'col_span' => 6, 'order' => 7, 'desc' => 'Vrij notitieblok met automatische achtergrond-opslag'],
'stats' => ['id' => 'stats', 'title' => 'Productiviteit & KPI\'s', 'category' => 'Inzicht', 'icon' => 'bar-chart-2', 'enabled' => true, 'col_span' => 12, 'order' => 8, 'desc' => 'Voortgangsbalk, voltooide taken, focus uren en meeting totalen'],
'quick_links' => ['id' => 'quick_links', 'title' => 'Snelle Koppelingen & Tools', 'category' => 'Productiviteit', 'icon' => 'external-link', 'enabled' => false, 'col_span' => 6, 'order' => 9, 'desc' => '1-klik snelkoppelingen naar Jira, Teams, Outlook, Azure en documentatie'],
'habit_tracker' => ['id' => 'habit_tracker', 'title' => 'Dagelijkse Focus Gewoontes', 'category' => 'Productiviteit', 'icon' => 'sparkles', 'enabled' => false, 'col_span' => 6, 'order' => 10, 'desc' => 'Houd gezonde werkgewoontes bij (water drinken, 15m bewegen, inbox zero)'],
'focus_sound' => ['id' => 'focus_sound', 'title' => 'Focus Sounds & Omgeving', 'category' => 'Productiviteit', 'icon' => 'headphones', 'enabled' => false, 'col_span' => 6, 'order' => 11, 'desc' => 'Rustgevende focus-geluiden voor concentratie en diep werk'],
'team_status' => ['id' => 'team_status', 'title' => 'Team & Beschikbaarheid', 'category' => 'Team', 'icon' => 'users', 'enabled' => false, 'col_span' => 6, 'order' => 12, 'desc' => 'Overzicht van collega\'s en wie vandaag bereikbaar of in overleg is']
];
$userWidgetLayout = $availableWidgets;
$savedLayoutJson = $currentUser['fociz_widget_layout'] ?? '';
if (!empty($savedLayoutJson)) {
$decoded = json_decode($savedLayoutJson, true);
if (is_array($decoded)) {
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>
<!-- AI Dagbriefing Banner (async geladen, alleen vandaag) -->
<?php if ($isTodaySelected): ?>
<div id="aiBriefingBanner" class="fociz-card p-5 relative overflow-hidden border-l-4 border-l-indigo-500 hidden">
<!-- Skeleton laadindicator -->
<div id="aiBriefingLoading" class="flex items-center space-x-3">
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center flex-shrink-0 animate-pulse">
<i data-lucide="sparkles" class="w-4 h-4"></i>
</div>
<div class="flex-1 space-y-1.5">
<div class="h-3 bg-slate-200 dark:bg-slate-800 rounded-lg animate-pulse w-3/4"></div>
<div class="h-2.5 bg-slate-200 dark:bg-slate-800 rounded-lg animate-pulse w-1/2"></div>
</div>
</div>
<!-- Inhoud (gevuld via JS) -->
<div id="aiBriefingContent" class="hidden">
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center flex-shrink-0 mt-0.5">
<i data-lucide="sparkles" class="w-4 h-4"></i>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2 mb-1">
<span class="text-[10px] font-black uppercase tracking-wider text-indigo-600 dark:text-indigo-400">AI Co-Pilot · Dagbriefing</span>
<span class="text-[10px] text-slate-400 font-mono"><?= date('H:i') ?></span>
</div>
<p id="aiBriefingGreeting" class="text-sm font-semibold text-slate-900 dark:text-white"></p>
<div id="aiBriefingDetails" class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-600 dark:text-slate-400">
<span id="aiBriefingFocusTask" class="hidden flex items-center space-x-1">
<i data-lucide="zap" class="w-3 h-3 text-amber-500 flex-shrink-0"></i>
<span id="aiBriefingFocusText"></span>
</span>
<span id="aiBriefingTip" class="hidden flex items-center space-x-1">
<i data-lucide="lightbulb" class="w-3 h-3 text-emerald-500 flex-shrink-0"></i>
<span id="aiBriefingTipText"></span>
</span>
</div>
<div id="aiBriefingWarnings" class="mt-2 space-y-1 hidden"></div>
</div>
<button onclick="document.getElementById('aiBriefingBanner').classList.add('hidden')" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0" title="Sluiten">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
</div>
<?php endif; ?>
<!-- Modular Customizable Widget Grid Container (Fociz v2.0) -->
<div class="relative w-full" id="focizWidgetGridContainer">
<!-- Blueprint Achtergrond Raster (zichtbaar zodra een widget wordt opgepakt) -->
<div id="widgetGridBlueprint" class="absolute inset-0 hidden pointer-events-none z-0 rounded-3xl border-2 border-dashed border-indigo-400/30 dark:border-indigo-500/25 bg-indigo-500/[0.02] p-3 transition-all duration-300">
<div class="grid grid-cols-1 lg:grid-cols-12 h-full gap-6">
<?php for ($col = 1; $col <= 12; $col++): ?>
<div class="h-full min-h-[360px] rounded-2xl border border-dashed border-indigo-400/25 dark:border-indigo-400/15 bg-indigo-50/20 dark:bg-indigo-950/20 flex flex-col items-center justify-between py-4 select-none">
<span class="text-[10px] font-mono font-black text-indigo-500/60 dark:text-indigo-400/50 tracking-wider">KOLOM <?= $col ?></span>
<div class="w-2 h-2 rounded-full bg-indigo-400/30 dark:bg-indigo-500/30"></div>
<span class="text-[10px] font-mono font-black text-indigo-500/40 dark:text-indigo-400/30">1/12</span>
</div>
<?php endfor; ?>
</div>
</div>
<!-- Widget Raster (Fociz v2.0) -->
<div id="focizWidgetGrid" class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start relative z-10 min-h-[300px]">
<?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 duration-300 fociz-widget-wrapper group/widget" data-widget-id="<?= htmlspecialchars($wId) ?>" data-col-span="<?= $span ?>" id="widgetWrapper_<?= htmlspecialchars($wId) ?>">
<!-- Bovenste rand van de widget: Prachtig gestyled en direct geïntegreerd in de widget -->
<div class="widget-header-bar flex items-center justify-between px-3.5 py-1.5 bg-slate-50/90 dark:bg-[#0c0f17]/90 border border-b-0 border-slate-200/80 dark:border-slate-800/80 rounded-t-2xl select-none backdrop-blur-xs transition-colors group-hover/widget:border-indigo-400/50 dark:group-hover/widget:border-indigo-500/30">
<!-- Links: Drag Handle Grip + Widget Titel -->
<div class="widget-drag-handle flex items-center space-x-1.5 cursor-grab active:cursor-grabbing text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 py-0.5 px-1 -ml-1 rounded-lg hover:bg-slate-200/50 dark:hover:bg-slate-800/50 transition group/drag touch-none" data-widget-id="<?= htmlspecialchars($wId) ?>" title="Sleep om te verplaatsen">
<i data-lucide="grip-vertical" class="w-3.5 h-3.5 text-slate-400 group-hover/drag:text-indigo-500 transition-colors flex-shrink-0"></i>
<span class="text-xs font-bold text-slate-700 dark:text-slate-300 tracking-wide truncate max-w-[160px] sm:max-w-[240px]"><?= htmlspecialchars($widget['title']) ?></span>
</div>
<!-- Rechts: Tandwieltje (Formaat & Verbergen) & Inklappen -->
<div class="flex items-center space-x-1 relative">
<!-- Tandwieltje Knop met Dropdown -->
<div class="relative">
<button type="button" onclick="toggleWidgetSettingsMenu('<?= $wId ?>', event)" class="widget-settings-btn 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="Widget instellingen (formaat en verbergen)">
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
</button>
<!-- Dropdown Popover Menu -->
<div id="widgetSettingsMenu_<?= htmlspecialchars($wId) ?>" class="widget-settings-dropdown hidden absolute right-0 top-full mt-1.5 z-50 w-52 p-2.5 rounded-2xl bg-white/95 dark:bg-[#111624]/95 backdrop-blur-md border border-slate-200 dark:border-slate-800 shadow-2xl space-y-2.5">
<div>
<span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 block mb-1.5">Breedte instellen</span>
<div class="grid grid-cols-4 gap-1">
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 4)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 4 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="4" title="1/3 breedte (4 kolommen)">1/3</button>
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 6)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 6 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="6" title="1/2 breedte (6 kolommen)">1/2</button>
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 8)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 8 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="8" title="2/3 breedte (8 kolommen)">2/3</button>
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 12)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 12 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="12" title="Volledige breedte (12 kolommen)">Vol</button>
</div>
</div>
<div class="pt-1.5 border-t border-slate-100 dark:border-slate-800/80">
<button type="button" onclick="hideWidgetOnScreen('<?= $wId ?>')" class="w-full flex items-center space-x-2 px-2 py-1.5 rounded-xl text-slate-600 dark: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 text-left text-xs font-semibold">
<i data-lucide="eye-off" class="w-3.5 h-3.5 flex-shrink-0"></i>
<span>Widget verbergen</span>
</button>
</div>
</div>
</div>
<!-- Inklappen / Uitklappen Knop -->
<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>
</div>
</div>
<!-- Widget Content Body: sluit naadloos aan op de bovenste rand -->
<div class="widget-content-body transition-all duration-200 [&>.fociz-card]:rounded-t-none [&>.fociz-card]:border-t-0" 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="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 shadow-2xs' ?> <?= $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'] ?>">
<!-- Hoofd taakrij -->
<div class="fociz-card-interactive group flex items-center justify-between p-3.5 <?= $task['is_completed'] ? '' : 'hover:border-indigo-400 dark:hover:border-indigo-500/50' ?>">
<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; ?>
<!-- Subtaak toggle knop -->
<button onclick="toggleSubtaskPanel(<?= $task['id'] ?>)" title="Subtaken / Checklist" 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" id="subtask-btn-<?= $task['id'] ?>">
<i data-lucide="list-checks" class="w-3.5 h-3.5"></i>
</button>
<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>
<!-- Subtaken panel (verborgen by default, uitklapbaar) -->
<div id="subtask-panel-<?= $task['id'] ?>" class="hidden border-t border-slate-100 dark:border-slate-800/60 px-4 pb-3 pt-2.5 space-y-2 bg-slate-50/50 dark:bg-slate-950/20 rounded-b-2xl">
<!-- Subtaak lijst (dynamisch gevuld) -->
<div id="subtask-list-<?= $task['id'] ?>" class="space-y-1.5"></div>
<!-- Nieuw subtaak invoer -->
<div class="flex items-center space-x-2 mt-1.5">
<input
type="text"
id="subtask-input-<?= $task['id'] ?>"
placeholder="Subtaak toevoegen…"
class="flex-1 bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onkeydown="if(event.key==='Enter'){addSubtask(<?= $task['id'] ?>);}"
>
<button onclick="addSubtask(<?= $task['id'] ?>)" class="flex items-center space-x-1 px-2.5 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-[10px] font-bold rounded-xl transition cursor-pointer">
<i data-lucide="plus" class="w-3 h-3"></i>
<span>Toevoegen</span>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php elseif ($wId === 'timeline'): ?>
<!-- Widget: Tijdlijn, Weekoverzicht & Dagselector (1 Geïntegreerde Widget) -->
<div class="fociz-card p-5 sm:p-6 overflow-hidden space-y-5">
<!-- 1. Week Navigatie & Directe Datumkeuze -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3 pb-3 border-b border-slate-100 dark:border-slate-800/80">
<!-- 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 cursor-pointer" 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 cursor-pointer" 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 cursor-pointer" 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-[11px] font-bold uppercase tracking-wider text-indigo-600 dark:text-indigo-400">
Week <?= $weekNumber ?> &bull; <?= $yearNumber ?>
</div>
<div class="text-sm sm:text-base font-black text-slate-900 dark:text-white">
<?= htmlspecialchars($formattedWeekRange) ?>
</div>
</div>
<!-- Rechterkant: AI Slim Indelen & Directe datumselector -->
<div class="flex flex-wrap items-center space-x-2 justify-end">
<?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.5 rounded-xl 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="hidden sm:inline-flex px-2 py-1 rounded-xl 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; ?>
<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-2.5 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>
<!-- 2. 7 Dagen Weekstrook (Dagselector) -->
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-7 gap-2.5">
<?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 rounded-xl 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">
<span class="text-[10px] 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-1.5 py-0.2 rounded-full text-[8px] font-extrabold uppercase bg-indigo-600 text-white shadow-xs">
Nu
</span>
<?php endif; ?>
</div>
<div class="flex items-baseline space-x-1 mb-2">
<span class="text-xl 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-[10px] 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 mt-auto pt-1.5 border-t border-slate-200/60 dark:border-slate-800/60 text-[9px]">
<div class="flex items-center justify-between px-1.5 py-0.5 rounded-md 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-2.5 h-2.5 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-1.5 py-0.5 rounded-md 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-2.5 h-2.5 text-sky-500"></i>
<span>Agenda</span>
</span>
<span class="font-bold">
<?= $wd['meeting_count'] ?>
</span>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
<!-- 3. Sub-header van de Tijdlijn voor de geselecteerde dag -->
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2">
<div class="w-7 h-7 rounded-lg bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
</div>
<div>
<h4 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Tijdlijn & Time-blocking voor <?= htmlspecialchars($formattedSelectedDate) ?>
</h4>
<p class="text-[10px] text-slate-400">Urenindeling & synchronisatie met Outlook</p>
</div>
</div>
<span class="text-xs text-slate-500 dark:text-slate-400 font-mono font-semibold bg-slate-100 dark:bg-slate-800/70 px-2.5 py-1 rounded-lg">
<?= $hours[0] ?> – <?= end($hours) ?>
</span>
</div>
<!-- 4. Capaciteitsbar: Geplande minuten vs. werkdagcapaciteit -->
<?php
$workdayCapacityMinutes = max(1, ($endHour - $startHour) * 60);
$lunchMinutes = 0;
if (!empty($lunchStart) && !empty($lunchEnd)) {
$lunchMinutes = (int)((strtotime("2000-01-01 {$lunchEnd}") - strtotime("2000-01-01 {$lunchStart}")) / 60);
}
$effectiveCapacity = max(1, $workdayCapacityMinutes - $lunchMinutes);
$meetingMinutes = 0;
foreach ($displayedOutlookEvents as $ev) {
if (!empty($ev['start']) && !empty($ev['end'])) {
$meetingMinutes += max(0, (int)((strtotime("2000-01-01 {$ev['end']}") - strtotime("2000-01-01 {$ev['start']}")) / 60));
}
}
$usedMinutes = $totalPlannedMinutes + $meetingMinutes;
$capPercent = min(100, round(($usedMinutes / $effectiveCapacity) * 100));
$taskOnlyPercent = min(100, round(($totalPlannedMinutes / $effectiveCapacity) * 100));
$meetingPercent = min(100 - $taskOnlyPercent, round(($meetingMinutes / $effectiveCapacity) * 100));
$capColor = $capPercent > 100 ? 'bg-rose-500' : ($capPercent >= 80 ? 'bg-amber-500' : 'bg-indigo-500');
$capLabel = $capPercent > 100 ? 'Overbelast' : ($capPercent >= 80 ? 'Bijna vol' : 'Goed ingepland');
$capLabelColor = $capPercent > 100 ? 'text-rose-600 dark:text-rose-400' : ($capPercent >= 80 ? 'text-amber-600 dark:text-amber-400' : 'text-indigo-600 dark:text-indigo-400');
$freeMinutes = max(0, $effectiveCapacity - $usedMinutes);
$freeH = intdiv($freeMinutes, 60);
$freeM = $freeMinutes % 60;
$freeLabel = $freeH > 0 ? "{$freeH}u " : '';
$freeLabel .= $freeM > 0 ? "{$freeM}m" : '';
$freeLabel = $freeLabel ?: '0m';
?>
<div class="space-y-1.5">
<div class="flex items-center justify-between text-[10px]">
<div class="flex items-center space-x-2">
<span class="font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">Dagcapaciteit</span>
<span class="font-mono font-black <?= $capLabelColor ?>"><?= $capPercent ?>% · <?= $capLabel ?></span>
</div>
<div class="flex items-center space-x-3 font-mono text-slate-500 dark:text-slate-400">
<?php if ($meetingMinutes > 0): ?>
<span class="flex items-center space-x-1">
<span class="w-2 h-2 rounded-sm bg-sky-400 dark:bg-sky-500 inline-block"></span>
<span><?= $meetingMinutes ?>m meetings</span>
</span>
<?php endif; ?>
<?php if ($totalPlannedMinutes > 0): ?>
<span class="flex items-center space-x-1">
<span class="w-2 h-2 rounded-sm bg-indigo-500 inline-block"></span>
<span><?= $totalPlannedMinutes ?>m taken</span>
</span>
<?php endif; ?>
<span class="text-emerald-600 dark:text-emerald-400 font-semibold"><?= $freeLabel ?> vrij</span>
</div>
</div>
<div class="h-2.5 w-full bg-slate-100 dark:bg-slate-900/60 rounded-full overflow-hidden flex">
<?php if ($meetingPercent > 0): ?>
<div class="h-full bg-sky-400 dark:bg-sky-500 transition-all duration-500 rounded-l-full" style="width: <?= $meetingPercent ?>%"></div>
<?php endif; ?>
<?php if ($taskOnlyPercent > 0): ?>
<div class="h-full <?= $capColor ?> transition-all duration-500 <?= $meetingPercent === 0 ? 'rounded-l-full' : '' ?> <?= ($meetingPercent + $taskOnlyPercent) >= 100 ? 'rounded-r-full' : '' ?>" style="width: <?= $taskOnlyPercent ?>%"></div>
<?php endif; ?>
</div>
</div>
<!-- 5. De urentijdlijn rijen -->
<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>
</div><!-- /#focizWidgetGridContainer -->
<?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) ?>;
// --- AI Dagbriefing (laden bij pagina-open, alleen voor vandaag) ---
<?php if ($isTodaySelected): ?>
const _aiBriefingDate = '<?= $selectedDate ?>';
let _aiBriefingLoaded = false;
async function loadDailyBriefing() {
if (_aiBriefingLoaded) return;
const banner = document.getElementById('aiBriefingBanner');
if (!banner) return;
// Toon banner met skeleton
banner.classList.remove('hidden');
try {
const resp = await fetch('/api/ai.php?action=daily_briefing', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date: _aiBriefingDate })
});
const data = await resp.json();
if (!data.success || data.error) {
// Geen AI key of fout: banner verbergen, stil falen
banner.classList.add('hidden');
return;
}
// Vul content
document.getElementById('aiBriefingGreeting').textContent = data.greeting || '';
const focusEl = document.getElementById('aiBriefingFocusTask');
const focusTextEl = document.getElementById('aiBriefingFocusText');
if (data.focus_task) {
focusTextEl.textContent = data.focus_task;
focusEl.classList.remove('hidden');
focusEl.style.display = 'flex';
}
const tipEl = document.getElementById('aiBriefingTip');
const tipTextEl = document.getElementById('aiBriefingTipText');
if (data.tip) {
tipTextEl.textContent = data.tip;
tipEl.classList.remove('hidden');
tipEl.style.display = 'flex';
}
if (data.warnings && data.warnings.length > 0) {
const warnContainer = document.getElementById('aiBriefingWarnings');
warnContainer.innerHTML = data.warnings.map(w =>
`<div class="flex items-center space-x-1.5 text-[11px] text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800/50 rounded-lg px-2.5 py-1">
<i data-lucide="alert-triangle" class="w-3 h-3 flex-shrink-0"></i>
<span>${w}</span>
</div>`
).join('');
warnContainer.classList.remove('hidden');
}
// Wissel skeleton ↔ content
document.getElementById('aiBriefingLoading').classList.add('hidden');
document.getElementById('aiBriefingContent').classList.remove('hidden');
_aiBriefingLoaded = true;
if (typeof lucide !== 'undefined') lucide.createIcons();
} catch (err) {
// Netwerk- of serverfout: banner verbergen
banner.classList.add('hidden');
}
}
// Laad de briefing 1,5 seconden na paginalading (niet blokkeren)
window.addEventListener('DOMContentLoaded', () => {
setTimeout(loadDailyBriefing, 1500);
});
<?php endif; ?>
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 het instellingen dropdown menu
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 py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer bg-indigo-600 border-indigo-600 text-white shadow-2xs";
} else {
btn.className = "widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700";
}
});
// Update in-memory array
const item = focizWidgets.find(x => x.id === wId);
if (item) {
item.col_span = span;
}
// Sluit het instellingen menu
document.getElementById(`widgetSettingsMenu_${wId}`)?.classList.add('hidden');
// 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 Instellingen Popover Menu (Tandwieltje) ---
function toggleWidgetSettingsMenu(wId, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
const menu = document.getElementById(`widgetSettingsMenu_${wId}`);
if (!menu) return;
const isCurrentlyHidden = menu.classList.contains('hidden');
// Sluit alle open menu's eerst
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
if (isCurrentlyHidden) {
menu.classList.remove('hidden');
}
}
// --- 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) {
document.getElementById(`widgetSettingsMenu_${wId}`)?.classList.add('hidden');
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 (Hover Ghost, Blueprint Grid & Auto-Scroll) ---
let activeDraggedWrapper = null;
let dragCandidate = null;
let isDraggingActive = false;
let floatingGhost = null;
let dropPlaceholder = null;
let grabOffsetX = 0;
let grabOffsetY = 0;
let currentPointerX = 0;
let currentPointerY = 0;
let autoScrollAnimId = null;
let dragInitialIndex = -1;
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) {}
});
// Luister naar drag handles met Pointer Events voor soepele hovering en scroll
document.querySelectorAll('.widget-drag-handle').forEach(handle => {
handle.setAttribute('draggable', 'false'); // Uitschakelen van krakkemikkige native browser drag
handle.style.touchAction = 'none';
handle.onpointerdown = (e) => {
// Alleen primaire muisklik (links)
if (e.button !== 0) return;
// Negeer als er op knoppen in de buurt geklikt wordt
if (e.target.closest('.widget-settings-btn') || e.target.closest('.widget-settings-dropdown')) return;
const wrapper = handle.closest('.fociz-widget-wrapper');
if (!wrapper) return;
dragCandidate = {
wrapper: wrapper,
handle: handle,
startX: e.clientX,
startY: e.clientY
};
};
});
}
// Globale pointer listeners (blijven soepel actief zelfs als cursor snel beweegt)
window.addEventListener('pointermove', (e) => {
currentPointerX = e.clientX;
currentPointerY = e.clientY;
if (!isDraggingActive && dragCandidate) {
// Controleer of de gebruiker minimaal 5px heeft bewogen (voorkomt valse triggers bij simpele kliks)
const dist = Math.hypot(e.clientX - dragCandidate.startX, e.clientY - dragCandidate.startY);
if (dist > 5) {
startWidgetDragging(dragCandidate.wrapper, dragCandidate.startX, dragCandidate.startY);
}
}
if (isDraggingActive) {
updateFloatingGhostPosition();
updateDropTargetFromPointer();
}
});
window.addEventListener('pointerup', (e) => {
if (isDraggingActive) {
finishWidgetDragging();
}
dragCandidate = null;
});
window.addEventListener('pointercancel', () => {
if (isDraggingActive) {
cancelWidgetDragging();
}
dragCandidate = null;
});
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isDraggingActive) {
cancelWidgetDragging();
}
});
// Start het oppakken & slepen van de widget
function startWidgetDragging(wrapper, startX, startY) {
const grid = document.getElementById('focizWidgetGrid');
if (!grid || !wrapper) return;
isDraggingActive = true;
activeDraggedWrapper = wrapper;
// Sluit openstaande popovers
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
// Bepaal beginpositie
const allWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
dragInitialIndex = allWrappers.indexOf(wrapper);
// 1. Toon het 12-koloms Blueprint Achtergrond Raster
const blueprint = document.getElementById('widgetGridBlueprint');
if (blueprint) {
blueprint.classList.remove('hidden');
blueprint.classList.add('block');
}
// 2. Creëer de zwevende Hover Ghost kaart
const rect = wrapper.getBoundingClientRect();
grabOffsetX = startX - rect.left;
grabOffsetY = startY - rect.top;
floatingGhost = wrapper.cloneNode(true);
floatingGhost.id = 'widgetFloatingGhost';
floatingGhost.removeAttribute('data-widget-id');
floatingGhost.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
floatingGhost.querySelectorAll('.widget-settings-dropdown').forEach(d => d.remove());
floatingGhost.style.position = 'fixed';
floatingGhost.style.left = `${rect.left}px`;
floatingGhost.style.top = `${rect.top}px`;
floatingGhost.style.width = `${rect.width}px`;
floatingGhost.style.height = `${rect.height}px`;
floatingGhost.style.pointerEvents = 'none';
floatingGhost.style.zIndex = '999999';
floatingGhost.style.transform = 'scale(1.02) rotate(1.2deg)';
floatingGhost.style.opacity = '0.94';
floatingGhost.style.transition = 'transform 0.15s ease, box-shadow 0.15s ease';
floatingGhost.style.boxShadow = '0 25px 60px -10px rgba(79, 70, 229, 0.45), 0 0 0 2px rgba(99, 102, 241, 0.7)';
floatingGhost.style.cursor = 'grabbing';
document.body.appendChild(floatingGhost);
// 3. Plaats een duidelijke Drop Slot Placeholder in het raster
const span = wrapper.getAttribute('data-col-span') || '6';
dropPlaceholder = document.createElement('div');
dropPlaceholder.id = 'widgetDropPlaceholder';
dropPlaceholder.className = `lg:col-span-${span} col-span-1 min-w-0 rounded-2xl border-2 border-dashed border-indigo-500 dark:border-indigo-400 bg-indigo-50/70 dark:bg-indigo-950/50 flex flex-col items-center justify-center p-6 transition-all duration-200 shadow-inner`;
dropPlaceholder.style.minHeight = `${Math.max(130, Math.min(rect.height, 280))}px`;
dropPlaceholder.innerHTML = `
<div class="flex items-center space-x-2 text-indigo-600 dark:text-indigo-400 font-bold text-xs animate-pulse">
<svg class="w-5 h-5 animate-bounce" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path></svg>
<span>Laat los om hier te plaatsen</span>
</div>
`;
wrapper.after(dropPlaceholder);
wrapper.style.display = 'none';
document.body.classList.add('select-none', 'cursor-grabbing');
// 4. Start automatische soepele scroll bij nadering van schermranden
startAutoScroller();
}
function updateFloatingGhostPosition() {
if (!floatingGhost) return;
floatingGhost.style.left = `${currentPointerX - grabOffsetX}px`;
floatingGhost.style.top = `${currentPointerY - grabOffsetY}px`;
}
function updateDropTargetFromPointer() {
const grid = document.getElementById('focizWidgetGrid');
if (!grid || !dropPlaceholder || !activeDraggedWrapper) return;
const wrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper')).filter(w => w !== activeDraggedWrapper);
let targetFound = false;
for (const wrap of wrappers) {
const rect = wrap.getBoundingClientRect();
if (currentPointerY >= rect.top - 15 && currentPointerY <= rect.bottom + 15 &&
currentPointerX >= rect.left - 15 && currentPointerX <= rect.right + 15) {
const midY = rect.top + rect.height / 2;
const midX = rect.left + rect.width / 2;
const isAfter = (currentPointerY > midY) || (Math.abs(currentPointerY - midY) < rect.height * 0.35 && currentPointerX > midX);
if (isAfter) {
if (wrap.nextElementSibling !== dropPlaceholder) {
wrap.after(dropPlaceholder);
}
} else {
if (wrap.previousElementSibling !== dropPlaceholder) {
wrap.before(dropPlaceholder);
}
}
targetFound = true;
break;
}
}
if (!targetFound && wrappers.length > 0) {
const firstRect = wrappers[0].getBoundingClientRect();
const lastRect = wrappers[wrappers.length - 1].getBoundingClientRect();
if (currentPointerY < firstRect.top) {
if (grid.firstElementChild !== dropPlaceholder) {
grid.prepend(dropPlaceholder);
}
} else if (currentPointerY > lastRect.bottom) {
if (grid.lastElementChild !== dropPlaceholder) {
grid.appendChild(dropPlaceholder);
}
}
}
}
// Auto-scroller wanneer je met de widget naar de boven- of onderkant van het scherm beweegt
function startAutoScroller() {
stopAutoScroller();
function scrollStep() {
if (!isDraggingActive) return;
const viewportHeight = window.innerHeight;
const threshold = 110;
if (currentPointerY < threshold && window.scrollY > 0) {
const factor = (threshold - currentPointerY) / threshold;
const speed = Math.max(3, Math.round(factor * 26));
window.scrollBy(0, -speed);
updateDropTargetFromPointer();
} else if (currentPointerY > viewportHeight - threshold) {
const factor = (currentPointerY - (viewportHeight - threshold)) / threshold;
const speed = Math.max(3, Math.round(factor * 26));
window.scrollBy(0, speed);
updateDropTargetFromPointer();
}
autoScrollAnimId = requestAnimationFrame(scrollStep);
}
autoScrollAnimId = requestAnimationFrame(scrollStep);
}
function stopAutoScroller() {
if (autoScrollAnimId) {
cancelAnimationFrame(autoScrollAnimId);
autoScrollAnimId = null;
}
}
// Voltooi het slepen (loslaten & AJAX opslaan)
function finishWidgetDragging() {
isDraggingActive = false;
stopAutoScroller();
document.body.classList.remove('select-none', 'cursor-grabbing');
// Verberg blueprint raster
const blueprint = document.getElementById('widgetGridBlueprint');
if (blueprint) {
blueprint.classList.add('hidden');
blueprint.classList.remove('block');
}
// Verwijder zwevende ghost
if (floatingGhost) {
floatingGhost.remove();
floatingGhost = null;
}
// Plaats originele widget terug op de plek van de placeholder
if (dropPlaceholder && activeDraggedWrapper) {
dropPlaceholder.replaceWith(activeDraggedWrapper);
activeDraggedWrapper.style.display = '';
dropPlaceholder = null;
// Subtiel succes flitsje op de neergezette widget
activeDraggedWrapper.classList.add('ring-2', 'ring-indigo-500', 'shadow-xl');
setTimeout(() => {
activeDraggedWrapper?.classList.remove('ring-2', 'ring-indigo-500', 'shadow-xl');
}, 400);
}
// Controleer of de volgorde gewijzigd is en sla op via AJAX
saveCurrentGridOrderIfChanged();
activeDraggedWrapper = null;
}
// Annuleer slepen (bijv. via Escape)
function cancelWidgetDragging() {
isDraggingActive = false;
stopAutoScroller();
document.body.classList.remove('select-none', 'cursor-grabbing');
const blueprint = document.getElementById('widgetGridBlueprint');
if (blueprint) {
blueprint.classList.add('hidden');
blueprint.classList.remove('block');
}
if (floatingGhost) {
floatingGhost.remove();
floatingGhost = null;
}
if (dropPlaceholder && activeDraggedWrapper) {
dropPlaceholder.remove();
dropPlaceholder = null;
activeDraggedWrapper.style.display = '';
}
activeDraggedWrapper = null;
}
// Controleer gewijzigde volgorde en sla asynchroon op via AJAX
function saveCurrentGridOrderIfChanged() {
const grid = document.getElementById('focizWidgetGrid');
if (!grid) return;
const currentWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
const currentIndex = activeDraggedWrapper ? currentWrappers.indexOf(activeDraggedWrapper) : -1;
let hasOrderChanged = false;
currentWrappers.forEach((el, index) => {
const wId = el.getAttribute('data-widget-id');
const item = focizWidgets.find(x => x.id === wId);
if (item) {
if (item.order !== index + 1) {
item.order = index + 1;
hasOrderChanged = true;
}
}
});
if (hasOrderChanged || (dragInitialIndex !== -1 && currentIndex !== dragInitialIndex)) {
// Geruisloos opslaan via AJAX
persistWidgetLayoutAuto();
showWidgetToast('Widget verplaatst & indeling opgeslagen', 'success');
}
}
// Sluit popup menu's als er buiten wordt geklikt
document.addEventListener('click', (e) => {
if (!e.target.closest('.widget-settings-dropdown') && !e.target.closest('.widget-settings-btn')) {
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
}
});
// --- 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);
}
// --- V2.0: Subtaken & Checklists ---
const _subtaskCache = {}; // task_id -> subtasks array
async function toggleSubtaskPanel(taskId) {
const panel = document.getElementById(`subtask-panel-${taskId}`);
if (!panel) return;
const isHidden = panel.classList.contains('hidden');
if (isHidden) {
panel.classList.remove('hidden');
// Laad subtaken als nog niet geladen
if (!_subtaskCache[taskId]) {
await loadSubtasks(taskId);
} else {
renderSubtasks(taskId);
}
// Focus invoerveld
const inp = document.getElementById(`subtask-input-${taskId}`);
if (inp) setTimeout(() => inp.focus(), 50);
} else {
panel.classList.add('hidden');
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
async function loadSubtasks(taskId) {
try {
const resp = await fetch(`/api/planner.php?action=get_subtasks&task_id=${taskId}`);
const data = await resp.json();
if (data.success) {
_subtaskCache[taskId] = data.subtasks || [];
renderSubtasks(taskId);
}
} catch (e) {
console.error('Subtaken laden mislukt:', e);
}
}
function renderSubtasks(taskId) {
const list = document.getElementById(`subtask-list-${taskId}`);
if (!list) return;
const subtasks = _subtaskCache[taskId] || [];
if (subtasks.length === 0) {
list.innerHTML = '<p class="text-[11px] text-slate-400 italic">Nog geen subtaken. Voeg hieronder toe.</p>';
return;
}
const doneCount = subtasks.filter(s => s.is_completed == 1).length;
const totalCount = subtasks.length;
list.innerHTML = `
<div class="flex items-center justify-between mb-1">
<span class="text-[10px] font-bold text-slate-500 dark:text-slate-400">${doneCount}/${totalCount} afgerond</span>
<div class="flex-1 mx-2 h-1 bg-slate-200 dark:bg-slate-800 rounded-full overflow-hidden">
<div class="h-full bg-indigo-500 transition-all duration-300" style="width: ${totalCount > 0 ? Math.round((doneCount/totalCount)*100) : 0}%"></div>
</div>
</div>
${subtasks.map(s => `
<div class="flex items-center space-x-2 group/sub py-1" id="subtask-item-${s.id}">
<button onclick="toggleSubtask(${s.id}, ${s.is_completed ? 0 : 1}, ${taskId})"
class="w-4 h-4 rounded border flex items-center justify-center flex-shrink-0 transition ${s.is_completed ? 'bg-indigo-600 border-indigo-600 text-white' : 'border-slate-300 dark:border-slate-700 hover:border-indigo-500 bg-white dark:bg-slate-900'}">
${s.is_completed ? '<svg xmlns="http://www.w3.org/2000/svg" class="w-2.5 h-2.5 stroke-[3]" viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg>' : ''}
</button>
<span class="text-xs flex-1 ${s.is_completed ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-700 dark:text-slate-300'}">${escapeHtml(s.title)}</span>
<button onclick="deleteSubtask(${s.id}, ${taskId})"
class="p-0.5 text-slate-300 hover:text-red-500 dark:text-slate-700 dark:hover:text-red-400 opacity-0 group-hover/sub:opacity-100 transition cursor-pointer rounded">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
</button>
</div>
`).join('')}
`;
}
async function addSubtask(taskId) {
const inp = document.getElementById(`subtask-input-${taskId}`);
if (!inp) return;
const title = inp.value.trim();
if (!title) return;
inp.disabled = true;
try {
const resp = await fetch('/api/planner.php?action=add_subtask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: taskId, title })
});
const data = await resp.json();
if (data.success && data.subtask) {
if (!_subtaskCache[taskId]) _subtaskCache[taskId] = [];
_subtaskCache[taskId].push(data.subtask);
renderSubtasks(taskId);
inp.value = '';
} else {
alert(data.error || 'Kan subtaak niet toevoegen');
}
} catch (e) {
alert('Netwerkfout bij toevoegen subtaak');
} finally {
inp.disabled = false;
inp.focus();
}
}
async function toggleSubtask(subtaskId, newState, taskId) {
try {
await fetch('/api/planner.php?action=toggle_subtask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subtask_id: subtaskId, is_completed: newState })
});
// Update cache
if (_subtaskCache[taskId]) {
const sub = _subtaskCache[taskId].find(s => s.id == subtaskId);
if (sub) sub.is_completed = newState;
}
renderSubtasks(taskId);
} catch (e) {
console.error('Subtaak toggle mislukt:', e);
}
}
async function deleteSubtask(subtaskId, taskId) {
try {
await fetch('/api/planner.php?action=delete_subtask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subtask_id: subtaskId })
});
if (_subtaskCache[taskId]) {
_subtaskCache[taskId] = _subtaskCache[taskId].filter(s => s.id != subtaskId);
}
renderSubtasks(taskId);
} catch (e) {
console.error('Subtaak verwijderen mislukt:', e);
}
}
// Helper: XSS-safe escaping voor subtaak titels in JS
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// --- 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'; ?>