Files
Fociz/planner.php
T
2026-10-05 11:01:25 +02:00

8392 lines
465 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');
require_once __DIR__ . '/includes/auth.php';
$currentUser = require_auth($isAjaxRequest);
require_once __DIR__ . '/includes/settings.php';
require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/integrations/jira.php';
require_once __DIR__ . '/includes/boards.php';
$db = get_db_connection();
$userId = (int)($currentUser['id'] ?? 0);
$isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$today = date('Y-m-d');
$userBoards = fociz_get_user_boards_with_lanes($db, $userId, $isAdmin);
// 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();
$calendarBlockReasons = get_calendar_block_reasons();
$activeWorkdays = $workdaySettings['workdays'] ?? [1, 2, 3, 4, 5];
if (!is_array($activeWorkdays) || empty($activeWorkdays)) {
$activeWorkdays = [1, 2, 3, 4, 5];
}
$selectedDayN = (int)date('N', $selectedTs);
$isSelectedDateWorkday = in_array($selectedDayN, $activeWorkdays, true);
$isSelectedDateWeekend = !$isSelectedDateWorkday;
// 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);
// Haal actieve kalenderblokkades (vakanties / verlof / afwezigheid) op vóór header render
$allCalendarBlocks = [];
$calendarBlocksByDate = [];
try {
$stmtBlocks = $db->prepare("
SELECT * FROM calendar_blocks
WHERE user_id = ? AND start_date <= ? AND end_date >= ?
ORDER BY start_date ASC
");
$stmtBlocks->execute([$userId, date('Y-m-d', strtotime('+35 days', $sundayTs)), date('Y-m-d', strtotime('-14 days', $mondayTs))]);
$allCalendarBlocks = $stmtBlocks->fetchAll() ?: [];
foreach ($allCalendarBlocks as $cb) {
$cbStart = strtotime($cb['start_date']);
$cbEnd = strtotime($cb['end_date']);
for ($d = $cbStart; $d <= $cbEnd; $d += 86400) {
$dStr = date('Y-m-d', $d);
$calendarBlocksByDate[$dStr][] = $cb;
}
}
} catch (Throwable $e) {
error_log("Fout bij ophalen calendar_blocks in planner: " . $e->getMessage());
}
$selectedDayBlock = $calendarBlocksByDate[$selectedDate][0] ?? null;
$isSelectedDateBlocked = !empty($selectedDayBlock);
// 1. Haal integratiestatussen op vóór header render
$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;
if (!$isAjaxRequest) {
$pageTitle = 'Fociz';
require_once __DIR__ . '/includes/header.php';
}
$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));
$cacheReconcileKey = "last_reconciled_{$userId}";
$lastReconciledHash = fociz_cache_get($cacheReconcileKey, '');
if ($eventsHash !== $lastReconciledHash || $forceRefresh) {
microsoft_reconcile_tasks_with_outlook($userId, $weekStart, $weekEnd, $weekOutlookEvents);
fociz_cache_set($cacheReconcileKey, $eventsHash, 86400);
}
} 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());
}
// 3b. Haal alle reeds ingeplande externe items op (Jira keys & Outlook Mail IDs) inclusief taakgegevens voor herplannen
$plannedExternalIds = [];
try {
$stmtPlannedExt = $db->prepare("
SELECT t.id as task_id, t.external_id, t.title, t.priority, t.time_block_start, t.time_block_end,
t.duration_minutes, dp.plan_date
FROM tasks t
LEFT JOIN daily_plans dp ON dp.id = t.daily_plan_id
WHERE t.user_id = ? AND t.external_id IS NOT NULL AND t.external_id != ''
ORDER BY t.id DESC
");
$stmtPlannedExt->execute([$userId]);
foreach ($stmtPlannedExt->fetchAll() as $row) {
$extId = $row['external_id'];
if (!isset($plannedExternalIds[$extId])) {
$plannedExternalIds[$extId] = [
'task_id' => (int)$row['task_id'],
'title' => $row['title'],
'priority' => $row['priority'],
'time_block_start' => $row['time_block_start'] ? substr($row['time_block_start'], 0, 5) : '',
'time_block_end' => $row['time_block_end'] ? substr($row['time_block_end'], 0, 5) : '',
'duration_minutes' => (int)($row['duration_minutes'] ?: 30),
'date' => $row['plan_date'] ?: date('Y-m-d')
];
}
}
} catch (Throwable $e) {
error_log("Fout bij ophalen planned external IDs: " . $e->getMessage());
}
// 3c. Haal snelle bullet notities / todo's op voor de geselecteerde dag
$dashboardBullets = [];
$pastOpenBulletsCount = 0;
try {
$stmtBullets = $db->prepare("
SELECT b.*, t.time_block_start as live_start, t.time_block_end as live_end, t.status as task_status
FROM dashboard_bullets b
LEFT JOIN tasks t ON b.scheduled_task_id = t.id
WHERE b.user_id = ? AND b.plan_date = ?
ORDER BY b.position ASC, b.id ASC
");
$stmtBullets->execute([$userId, $selectedDate]);
$dashboardBullets = $stmtBullets->fetchAll() ?: [];
// Tel openstaande items van eerdere dagen
$stmtPastBullets = $db->prepare("
SELECT COUNT(*) FROM dashboard_bullets
WHERE user_id = ? AND plan_date < ? AND is_completed = 0
");
$stmtPastBullets->execute([$userId, $selectedDate]);
$pastOpenBulletsCount = (int)$stmtPastBullets->fetchColumn();
} catch (Throwable $e) {
error_log("Fout bij ophalen dashboard bullets: " . $e->getMessage());
ensure_tables_exist($db);
}
// 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] ?? []);
$isWorkday = in_array($dayN, $activeWorkdays, true);
$dayBlock = $calendarBlocksByDate[$dayDate][0] ?? null;
$isBlocked = !empty($dayBlock);
$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),
'is_workday' => $isWorkday,
'is_weekend' => !$isWorkday,
'is_blocked' => $isBlocked,
'block_info' => $dayBlock,
'task_count' => $tStat['total'],
'completed_count' => $tStat['completed'],
'meeting_count' => $mCount,
];
}
// 5. 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', 'category' => 'Ontwikkeling', 'icon' => 'layers', 'enabled' => true, 'col_span' => 6, 'order' => 5, 'desc' => 'Openstaande Jira tickets 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' => 'Todo\'s & Notities', 'category' => 'Productiviteit', 'icon' => 'check-square', 'enabled' => true, 'col_span' => 6, 'order' => 7, 'desc' => 'Snel actiepunten en notities vastleggen en inplannen in een agendablok'],
'stats' => ['id' => 'stats', 'title' => 'Productiviteit & KPI\'s', 'category' => 'Inzicht', 'icon' => 'bar-chart-2', 'enabled' => true, 'col_span' => 12, 'order' => 8, 'desc' => 'Voortgangsbalk, voltooide taken, focus uren en meeting totalen'],
'quick_links' => ['id' => 'quick_links', 'title' => 'Snelle Koppelingen & Tools', 'category' => 'Productiviteit', 'icon' => 'external-link', 'enabled' => false, 'col_span' => 6, 'order' => 9, 'desc' => '1-klik snelkoppelingen naar Jira, Teams, Outlook, Azure en documentatie'],
'habit_tracker' => ['id' => 'habit_tracker', 'title' => 'Dagelijkse Focus Gewoontes', 'category' => 'Productiviteit', 'icon' => 'sparkles', 'enabled' => false, 'col_span' => 6, 'order' => 10, 'desc' => 'Houd gezonde werkgewoontes bij (water drinken, 15m bewegen, inbox zero)'],
'focus_sound' => ['id' => 'focus_sound', 'title' => 'Focus Sounds & Omgeving', 'category' => 'Productiviteit', 'icon' => 'headphones', 'enabled' => false, 'col_span' => 6, 'order' => 11, 'desc' => 'Rustgevende focus-geluiden voor concentratie en diep werk'],
'team_status' => ['id' => 'team_status', 'title' => 'Team & Beschikbaarheid', 'category' => 'Team', 'icon' => 'users', 'enabled' => false, 'col_span' => 6, 'order' => 12, 'desc' => 'Overzicht van collega\'s en wie vandaag bereikbaar of in overleg is']
];
$userWidgetLayout = $availableWidgets;
$savedLayoutJson = $currentUser['fociz_widget_layout'] ?? '';
if (!empty($savedLayoutJson)) {
$decoded = json_decode($savedLayoutJson, true);
if (is_array($decoded)) {
$savedOrder = 1;
$seen = [];
foreach ($decoded as $item) {
$wId = $item['id'] ?? '';
if (isset($userWidgetLayout[$wId])) {
$userWidgetLayout[$wId]['enabled'] = !empty($item['enabled']);
if (!empty($item['col_span'])) $userWidgetLayout[$wId]['col_span'] = (int)$item['col_span'];
$userWidgetLayout[$wId]['order'] = isset($item['order']) ? (int)$item['order'] : $savedOrder;
if (!empty($item['custom_title'])) {
$userWidgetLayout[$wId]['custom_title'] = trim($item['custom_title']);
$userWidgetLayout[$wId]['title'] = trim($item['custom_title']);
}
$seen[$wId] = true;
$savedOrder++;
}
}
foreach ($userWidgetLayout as $wId => &$wData) {
if (!isset($seen[$wId])) {
$wData['order'] = $savedOrder++;
}
}
unset($wData);
}
}
uasort($userWidgetLayout, function($a, $b) {
$diff = ($a['order'] ?? 0) <=> ($b['order'] ?? 0);
if ($diff !== 0) return $diff;
return strcmp($a['id'] ?? '', $b['id'] ?? '');
});
// 5b. Haal Jira dashboard data op voor de snelle import widget (indien ingeschakeld)
$jiraAssignedIssues = [];
$jiraTeamIssues = [];
$jiraAllIssues = [];
$jiraQueueFilters = [];
$isJiraLive = false;
$isJiraWidgetEnabled = !empty($userWidgetLayout['jira_queue']['enabled']);
if ($isJiraWidgetEnabled) {
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;
// 5c. Haal recente e-mails op voor de Outlook E-mail Widget (indien ingeschakeld)
$recentEmails = [];
$unreadEmailCount = 0;
$mailNeedsReconsent = false;
$isMailWidgetEnabled = !empty($userWidgetLayout['outlook_mail']['enabled']);
if ($isMailWidgetEnabled) {
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());
}
}
// Bereken dagstatistieken
$completedTasksCount = 0;
$pendingTasksCount = 0;
$highPriorityCount = 0;
$totalPlannedMinutes = 0;
foreach ($tasks as $t) {
if (!empty($t['is_completed'])) {
$completedTasksCount++;
} else {
$pendingTasksCount++;
if (($t['priority'] ?? '') === 'P1') $highPriorityCount++;
}
$totalPlannedMinutes += (int)($t['duration_minutes'] ?: 30);
}
$totalTaskCount = count($tasks);
$taskProgressPercent = $totalTaskCount > 0 ? round(($completedTasksCount / $totalTaskCount) * 100) : 0;
// Filter Outlook events die afkomstig zijn van Fociz taken voor de geselecteerde dag om duplicaten op de tijdlijn te voorkomen
$dayOutlookEvents = $outlookEventsByDate[$selectedDate] ?? [];
$syncedTaskExternalIds = [];
foreach ($tasks as $t) {
if (!empty($t['external_id'])) {
$syncedTaskExternalIds[$t['external_id']] = true;
}
}
$displayedOutlookEvents = [];
foreach ($dayOutlookEvents as $evt) {
if (!empty($evt['id']) && isset($syncedTaskExternalIds[$evt['id']])) {
continue;
}
$displayedOutlookEvents[] = $evt;
}
// Bereken initiële status-hash voor achtergrondsynchronisatie
$dayEventsForHash = [];
foreach ($displayedOutlookEvents as $e) {
$dayEventsForHash[] = [
'id' => $e['id'] ?? '',
'start' => $e['start'] ?? '',
'end' => $e['end'] ?? '',
'subject' => $e['subject'] ?? '',
];
}
$tasksForHash = array_map(function($t) {
return [
'id' => $t['id'],
'title' => $t['title'],
'time_block_start' => $t['time_block_start'],
'time_block_end' => $t['time_block_end'],
'is_completed' => $t['is_completed'],
];
}, $tasks);
$initialSyncHash = md5(json_encode([
'events' => $dayEventsForHash,
'tasks' => $tasksForHash
]));
// 6. Tijdlijn uren & Werkdag Standaarden (Release v1.4)
$workdayStartStr = $workdaySettings['workday_start'] ?? '08:30';
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:00';
$lunchStart = $workdaySettings['lunch_start'] ?? '12:30';
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
$lunchHour = (int)explode(':', $lunchStart)[0];
$startHour = (int)explode(':', $workdayStartStr)[0];
$endHourParts = explode(':', $workdayEndStr);
$endHour = (int)$endHourParts[0];
if (!empty($endHourParts[1]) && (int)$endHourParts[1] > 0) {
$endHour++;
}
if ($startHour < 6) $startHour = 6;
if ($endHour > 23) $endHour = 23;
foreach ($displayedOutlookEvents as $e) {
if (!empty($e['start'])) {
$h = (int)explode(':', $e['start'])[0];
if ($h < $startHour && $h >= 6) $startHour = $h;
if ($h > $endHour && $h <= 23) $endHour = $h + 1;
}
}
foreach ($tasks as $t) {
if (!empty($t['time_block_start'])) {
$h = (int)explode(':', $t['time_block_start'])[0];
if ($h < $startHour && $h >= 6) $startHour = $h;
if ($h > $endHour && $h <= 23) $endHour = $h + 1;
}
}
$hours = [];
for ($h = $startHour; $h <= $endHour; $h++) {
$hours[] = sprintf('%02d:00', $h);
}
?>
<?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; ?>
<!-- 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 id="widgetTitleText_<?= htmlspecialchars($wId) ?>" 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, Naam & 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 (naam, 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-64 p-3 rounded-2xl bg-white/95 dark:bg-[#111624]/95 backdrop-blur-md border border-slate-200 dark:border-slate-800 shadow-2xl space-y-3">
<!-- 1. Naam / Titel Aanpassen -->
<div class="space-y-1.5 pb-2.5 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center justify-between">
<label for="widgetTitleInput_<?= htmlspecialchars($wId) ?>" class="text-[10px] font-bold uppercase tracking-wider text-slate-400">Naam aanpassen</label>
<button type="button" onclick="resetWidgetTitle('<?= $wId ?>', '<?= htmlspecialchars(addslashes($availableWidgets[$wId]['title'] ?? $widget['title']), ENT_QUOTES) ?>')" class="text-[10px] text-indigo-500 hover:text-indigo-600 dark:hover:text-indigo-400 font-medium cursor-pointer" title="Standaardnaam herstellen">Herstellen</button>
</div>
<div class="flex items-center space-x-1.5">
<input type="text"
id="widgetTitleInput_<?= htmlspecialchars($wId) ?>"
value="<?= htmlspecialchars($widget['title']) ?>"
data-default-title="<?= htmlspecialchars($availableWidgets[$wId]['title'] ?? $widget['title']) ?>"
placeholder="<?= htmlspecialchars($availableWidgets[$wId]['title'] ?? 'Widget titel') ?>"
maxlength="50"
onclick="event.stopPropagation();"
onkeydown="if(event.key==='Enter'){event.preventDefault();saveWidgetTitle('<?= $wId ?>');}"
class="flex-1 min-w-0 px-2.5 py-1.5 text-xs rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-slate-800 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition">
<button type="button"
onclick="saveWidgetTitle('<?= $wId ?>')"
class="px-2.5 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition flex-shrink-0 cursor-pointer shadow-2xs flex items-center justify-center"
title="Naam opslaan">
<i data-lucide="check" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
<!-- 2. Breedte instellen -->
<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>
<!-- 3. Widget verbergen -->
<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):
$taskJson = [
'id' => (int)$task['id'],
'title' => $task['title'],
'description' => $task['description'] ?? '',
'priority' => $task['priority'],
'start' => $task['time_block_start'] ?? '',
'end' => $task['time_block_end'] ?? '',
'duration' => (int)($task['duration_minutes'] ?: 30),
'is_completed' => (bool)$task['is_completed'],
'source' => $task['source'],
'external_id' => $task['external_id'] ?? '',
'external_url' => $task['external_url'] ?? '',
'date' => $selectedDate
];
?>
<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'] ?>" oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($taskJson), ENT_QUOTES, 'UTF-8') ?>)">
<!-- 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' ?>" title="Klik met de rechtermuisknop voor meer opties (openen, bewerken, verwijderen)">
<div class="flex items-center space-x-3 truncate mr-2 flex-1 cursor-pointer" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($taskJson), ENT_QUOTES, 'UTF-8') ?>)">
<button type="button" onclick="event.stopPropagation(); 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.5 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.5 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.5 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" onclick="event.stopPropagation()" 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-1.5 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="event.stopPropagation(); 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; ?>
<!-- Volledige inhoud / Details knop -->
<button onclick="event.stopPropagation(); openTaskDetailModal(<?= htmlspecialchars(json_encode($taskJson), ENT_QUOTES, 'UTF-8') ?>)" title="Volledige inhoud openen" 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="eye" class="w-3.5 h-3.5"></i>
</button>
<!-- Subtaak toggle knop -->
<button onclick="event.stopPropagation(); 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="event.stopPropagation(); openEditTaskModal(<?= htmlspecialchars(json_encode($taskJson), 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="event.stopPropagation(); 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 gap-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>
<!-- Inklap / Uitklap Pijltje rechts direct boven de laatste dag van de weekstrook -->
<div class="flex justify-end -mb-2">
<button
type="button"
onclick="toggleDaySelectionCollapse()"
id="btnToggleDaySelection"
class="p-1 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer"
title="Dagen inklappen / tonen"
>
<i data-lucide="chevron-up" id="iconToggleDaySelection" class="w-4 h-4 transition-transform duration-200"></i>
</button>
</div>
<!-- 2. 7 Dagen Weekstrook (Dagselector met Rechtermuisknop Blokkade Opties) -->
<div id="weekDaysStripContainer" class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-7 gap-2.5 transition-all duration-200">
<?php foreach ($weekDays as $wd): ?>
<a
href="<?= ($wd['date'] === $today) ? 'planner.php' : ('planner.php?date=' . $wd['date']) ?>"
onclick="switchPlannerDate('<?= $wd['date'] ?>', event); return false;"
oncontextmenu="openDayContextMenu(event, '<?= $wd['date'] ?>', <?= $wd['is_blocked'] ? (int)$wd['block_info']['id'] : 0 ?>, '<?= $wd['is_blocked'] ? addslashes(htmlspecialchars($wd['block_info']['title'], ENT_QUOTES)) : '' ?>'); return false;"
data-date="<?= $wd['date'] ?>"
data-blocked="<?= $wd['is_blocked'] ? '1' : '0' ?>"
class="fociz-card-interactive group relative flex flex-col p-3 rounded-xl border transition-all text-left <?= $wd['is_blocked']
? ($wd['is_selected']
? 'bg-amber-100/90 dark:bg-amber-950/60 border-amber-400 dark:border-amber-600 ring-2 ring-amber-400/50 shadow-sm'
: 'bg-amber-50/80 dark:bg-amber-950/30 border-amber-300/80 dark:border-amber-800/70 hover:border-amber-400 dark:hover:border-amber-600 shadow-2xs')
: ($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'
: ($wd['is_workday']
? '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'
: 'bg-slate-100/60 dark:bg-[#080b11]/60 border-dashed border-slate-200/80 dark:border-slate-800/80 hover:border-slate-300 dark:hover:border-slate-700 hover:bg-white dark:hover:bg-[#0f141f] shadow-2xs')) ?>"
title="<?= $wd['is_blocked'] ? ('Geblokkeerd: ' . htmlspecialchars($wd['block_info']['title']) . ' • Rechtermuisknop voor opties') : 'Klik om te selecteren • Rechtermuisknop om dag te blokkeren' ?>"
>
<div class="flex items-center justify-between mb-1">
<span class="text-[10px] font-bold uppercase tracking-wider <?= $wd['is_blocked'] ? 'text-amber-800 dark:text-amber-300' : ($wd['is_selected'] ? 'text-indigo-600 dark:text-indigo-400' : ($wd['is_workday'] ? 'text-slate-500 dark:text-slate-400' : 'text-slate-400 dark:text-slate-500')) ?>">
<?= $wd['short_name'] ?>
</span>
<div class="flex items-center gap-1">
<?php if ($wd['is_blocked']): ?>
<span class="px-2 py-0.5 rounded-md text-[9px] font-bold uppercase bg-amber-200/90 dark:bg-amber-900/80 text-amber-950 dark:text-amber-200 border border-amber-300 dark:border-amber-700/80 inline-flex items-center gap-1 leading-none shadow-2xs" title="<?= htmlspecialchars($wd['block_info']['title']) ?>">
<i data-lucide="palmtree" class="w-2.5 h-2.5 flex-shrink-0"></i>
<span class="max-w-[80px] truncate"><?= htmlspecialchars($wd['block_info']['title']) ?></span>
</span>
<?php elseif ($wd['is_weekend']): ?>
<span class="px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase bg-slate-200/60 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-300/60 dark:border-slate-700/60">
Weekend
</span>
<?php endif; ?>
<?php if ($wd['is_today']): ?>
<span class="px-1.5 py-0.5 rounded-full text-[8px] font-extrabold uppercase bg-indigo-600 text-white shadow-xs">
Nu
</span>
<?php endif; ?>
</div>
</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 <?= ($isSelectedDateBlocked || $isSelectedDateWeekend) ? 'bg-amber-50 dark:bg-amber-950/50 text-amber-600 dark:text-amber-400' : '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="<?= $isSelectedDateBlocked ? 'palmtree' : ($isSelectedDateWeekend ? 'coffee' : 'clock') ?>" class="w-3.5 h-3.5"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h4 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Agenda voor <?= htmlspecialchars($formattedSelectedDate) ?>
</h4>
<?php if ($isSelectedDateBlocked): ?>
<span class="px-2 py-0.5 rounded-full text-[9px] font-extrabold uppercase bg-amber-100 text-amber-900 dark:bg-amber-950/80 dark:text-amber-200 border border-amber-300 dark:border-amber-700/80">
<?= htmlspecialchars($selectedDayBlock['title']) ?> (Geblokkeerd)
</span>
<?php elseif ($isSelectedDateWeekend): ?>
<span class="px-2 py-0.5 rounded-full text-[9px] font-extrabold uppercase bg-amber-50 text-amber-700 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-200/80 dark:border-amber-800/60">
Weekend / Vrije Dag
</span>
<?php endif; ?>
</div>
</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>
<?php if ($isSelectedDateWeekend): ?>
<span class="px-1.5 py-0.5 rounded text-[9px] font-extrabold uppercase bg-amber-100 text-amber-700 dark:bg-amber-950/70 dark:text-amber-400">Weekend</span>
<?php endif; ?>
<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="timeline-hour-slot 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 transition-all duration-150 rounded-xl p-1"
data-hour="<?= $hour ?>"
ondragover="handleTimelineSlotDragOver(event)"
ondragleave="handleTimelineSlotDragLeave(event)"
ondrop="handleTimelineSlotDrop(event, '<?= $hour ?>')">
<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 select-none">
<?= $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):
$evtJson = [
'id' => $evt['id'] ?? '',
'title' => $evt['subject'] ?? 'Afspraak',
'subject' => $evt['subject'] ?? 'Afspraak',
'start' => $evt['start'] ?? '',
'end' => $evt['end'] ?? '',
'date' => $evt['date'] ?? $selectedDate,
'location' => $evt['location'] ?? '',
'meeting_url' => $evt['meeting_url'] ?? '',
'web_link' => $evt['web_link'] ?? '',
'is_all_day' => !empty($evt['is_all_day']),
'source' => 'OUTLOOK_LIVE',
'is_outlook_event' => true,
];
?>
<div id="timeline-evt-<?= htmlspecialchars(preg_replace('/[^a-zA-Z0-9_-]/', '', $evt['id'] ?? '')) ?>"
draggable="true"
ondragstart="handleTimelineItemDragStart(event, 'outlook', <?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)"
ondragend="handleTimelineItemDragEnd(event)"
oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)"
title="Sleep naar ander tijdblok, of rechtermuisklik voor opties"
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] cursor-grab active:cursor-grabbing group/evt">
<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" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)">
<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" onclick="event.stopPropagation()">
<?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):
$slotTaskJson = [
'id' => (int)$t['id'],
'title' => $t['title'],
'description' => $t['description'] ?? '',
'priority' => $t['priority'],
'start' => $t['time_block_start'] ?? '',
'end' => $t['time_block_end'] ?? '',
'duration' => (int)($t['duration_minutes'] ?: 30),
'is_completed' => (bool)$t['is_completed'],
'source' => $t['source'],
'external_id' => $t['external_id'] ?? '',
'external_url' => $t['external_url'] ?? '',
'date' => $selectedDate
];
?>
<div id="timeline-task-<?= $t['id'] ?>"
draggable="true"
ondragstart="handleTimelineItemDragStart(event, 'task', <?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)"
ondragend="handleTimelineItemDragEnd(event)"
oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)"
title="Sleep naar ander tijdblok, of rechtermuisklik voor opties"
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 cursor-grab active:cursor-grabbing group/task">
<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 cursor-pointer" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)">
<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="event.stopPropagation(); 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>
<button onclick="event.stopPropagation(); openTaskDetailModal(<?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)" title="Volledige inhoud openen" class="p-1 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="eye" class="w-3 h-3"></i>
</button>
<button onclick="event.stopPropagation(); openEditTaskModal(<?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)" title="Taak & Tijdblok bewerken" class="p-1 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="edit-3" class="w-3 h-3"></i>
</button>
<button onclick="event.stopPropagation(); deleteTask(<?= $t['id'] ?>)" title="Verwijderen" class="p-1 rounded-lg text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer">
<i data-lucide="trash-2" class="w-3 h-3"></i>
</button>
</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">
<?php $isMailPlanned = !empty($plannedExternalIds[$mail['id']]); ?>
<button
onclick="handleEmailPlanClick(this, event)"
data-mail-id="<?= htmlspecialchars($mail['id'] ?? '', ENT_QUOTES) ?>"
data-mail-subject="<?= htmlspecialchars($mail['subject'] ?? '', ENT_QUOTES) ?>"
data-mail-sender="<?= htmlspecialchars($mail['sender_name'] ?? 'Outlook Mail', ENT_QUOTES) ?>"
data-mail-link="<?= htmlspecialchars($mail['web_link'] ?? '', ENT_QUOTES) ?>"
data-mail-preview="<?= htmlspecialchars(mb_substr($mail['body_preview'] ?? '', 0, 300), ENT_QUOTES) ?>"
class="plan-btn-mail flex items-center space-x-1 text-[11px] <?= $isMailPlanned ? 'font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs' : '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="<?= $isMailPlanned ? 'Deze e-mail is al ingepland als dagtaak (klik om opnieuw/anders in te plannen)' : 'Plan deze e-mail direct in als dagtaak' ?>"
>
<?php if ($isMailPlanned): ?>
<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i>
<span>Ingepland</span>
<?php else: ?>
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
<span>Plan</span>
<?php endif; ?>
</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 (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 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 flex-wrap items-center gap-1.5" 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 (<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 (<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 cursor-pointer" onclick="openJiraTicketDetail('<?= htmlspecialchars($issue['key'] ?? '') ?>')" title="Klik voor ticketdetails">
<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>
<?php $isJiraPlanned = !empty($plannedExternalIds[$issue['key']]); ?>
<button
onclick="quickImportJiraByKey(this, '<?= htmlspecialchars($issue['key'] ?? '') ?>', event)"
data-jira-key="<?= htmlspecialchars($issue['key'] ?? '') ?>"
data-jira-summary="<?= htmlspecialchars($issue['summary'] ?? '') ?>"
data-jira-status="<?= htmlspecialchars($issue['status'] ?? 'To Do') ?>"
data-jira-priority="<?= htmlspecialchars($issue['priority'] ?? 'Medium') ?>"
class="plan-btn-jira flex items-center space-x-1 text-[11px] <?= $isJiraPlanned ? 'font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs' : '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="<?= $isJiraPlanned ? 'Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)' : 'Plan in als dagtaak' ?>"
>
<?php if ($isJiraPlanned): ?>
<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i>
<span>Ingepland</span>
<?php else: ?>
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
<span>Plan</span>
<?php endif; ?>
</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):
$widgetEvtJson = [
'id' => $evt['id'] ?? '',
'title' => $evt['subject'] ?? 'Afspraak',
'subject' => $evt['subject'] ?? 'Afspraak',
'start' => $evt['start'] ?? '',
'end' => $evt['end'] ?? '',
'date' => $evt['date'] ?? $selectedDate,
'location' => $evt['location'] ?? '',
'meeting_url' => $evt['meeting_url'] ?? '',
'web_link' => $evt['web_link'] ?? '',
'is_all_day' => !empty($evt['is_all_day']),
'source' => 'OUTLOOK_LIVE',
'is_outlook_event' => true,
];
?>
<div oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($widgetEvtJson), ENT_QUOTES, 'UTF-8') ?>)" title="Rechtermuisklik voor opties (openen, bewerken, verwijderen)" 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 cursor-pointer group/evt">
<div class="truncate mr-2 min-w-0 flex-1" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($widgetEvtJson), ENT_QUOTES, 'UTF-8') ?>)">
<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" title="<?= htmlspecialchars($evt['subject'] ?? 'Afspraak') ?>">
<?= 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" onclick="event.stopPropagation()">
<?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: Todo's & Notities -->
<div class="fociz-card p-6" id="dashboardBulletWidget">
<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="check-square" class="w-4 h-4"></i>
</div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
<?= htmlspecialchars($wConfig['custom_title'] ?? 'Todo\'s & Notities') ?>
</h3>
</div>
<!-- Tab Switcher: Bullet Todo's vs Vrij Kladblok -->
<div class="flex items-center space-x-1 bg-slate-100 dark:bg-slate-800/80 p-0.5 rounded-xl text-[11px] font-semibold">
<button type="button" onclick="switchBulletView('bullets')" id="btnTabBullets" class="px-2.5 py-1 rounded-lg transition flex items-center space-x-1.5 bg-white dark:bg-[#111624] text-indigo-600 dark:text-indigo-400 shadow-2xs font-bold cursor-pointer">
<i data-lucide="list-todo" class="w-3.5 h-3.5"></i>
<span>Bullets</span>
<span id="bulletCountBadge" class="ml-1 px-1.5 py-0.2 rounded-full text-[9px] <?= count($dashboardBullets) > 0 ? 'bg-indigo-100 dark:bg-indigo-500/20 text-indigo-700 dark:text-indigo-300' : 'hidden' ?>">
<?= count($dashboardBullets) ?>
</span>
</button>
<button type="button" onclick="switchBulletView('notes')" id="btnTabNotes" class="px-2.5 py-1 rounded-lg transition flex items-center space-x-1.5 text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200 cursor-pointer">
<i data-lucide="file-text" class="w-3.5 h-3.5"></i>
<span>Kladblok</span>
</button>
</div>
</div>
<!-- 1. Bullet Todo Lijst Weergave -->
<div id="bulletListViewArea" class="space-y-3">
<!-- Snelle Toevoegen Regel -->
<div class="flex items-center space-x-2">
<div class="relative flex-1">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400">
<i data-lucide="circle-dot" class="w-3.5 h-3.5 text-indigo-500"></i>
</div>
<input
type="text"
id="newBulletInput"
placeholder="Snel item toevoegen... (bijv. Bespreken met collega)"
onkeydown="if(event.key==='Enter'){event.preventDefault();addBulletItem();}"
class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 dark:border-slate-800 rounded-xl pl-9 pr-3 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 shadow-2xs transition"
>
</div>
<button
type="button"
onclick="addBulletItem()"
id="btnAddBullet"
class="px-3.5 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition flex items-center space-x-1 flex-shrink-0 cursor-pointer shadow-xs active:scale-95"
>
<i data-lucide="plus" class="w-3.5 h-3.5"></i>
<span class="hidden sm:inline">Toevoegen</span>
</button>
</div>
<!-- Carry-over Banner (als er openstaande notities van eerdere dagen zijn) -->
<div id="bulletCarryoverBanner" class="<?= $pastOpenBulletsCount > 0 ? '' : 'hidden' ?> p-2.5 rounded-xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-between text-xs text-indigo-900 dark:text-indigo-200">
<div class="flex items-center space-x-2 min-w-0">
<i data-lucide="inbox" class="w-4 h-4 text-indigo-500 flex-shrink-0"></i>
<span class="truncate">
<strong id="pastOpenBulletCountText"><?= $pastOpenBulletsCount ?></strong> openstaande memo's van eerdere dagen
</span>
</div>
<button
type="button"
onclick="carryOverOpenBullets()"
class="px-2.5 py-1 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-[11px] font-bold flex-shrink-0 transition cursor-pointer shadow-2xs"
>
Naar vandaag verplaatsen
</button>
</div>
<!-- De Bullet Items Lijst -->
<div class="space-y-1.5 max-h-72 overflow-y-auto pr-0.5" id="bulletItemsContainer">
<?php if (empty($dashboardBullets)): ?>
<div id="bulletEmptyState" class="py-8 text-center text-slate-400 dark:text-slate-500">
<div class="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800/60 flex items-center justify-center mx-auto mb-2 text-slate-400">
<i data-lucide="check-circle" class="w-5 h-5 opacity-60"></i>
</div>
<p class="text-xs font-medium text-slate-600 dark:text-slate-400">Geen notities of actiepunten voor deze dag.</p>
<p class="text-[11px] opacity-75 mt-0.5">Typ hierboven een punt, bijv. "Bespreken met collega".</p>
</div>
<?php else: ?>
<div id="bulletEmptyState" class="hidden py-8 text-center text-slate-400 dark:text-slate-500">
<div class="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800/60 flex items-center justify-center mx-auto mb-2 text-slate-400">
<i data-lucide="check-circle" class="w-5 h-5 opacity-60"></i>
</div>
<p class="text-xs font-medium text-slate-600 dark:text-slate-400">Geen notities of actiepunten voor deze dag.</p>
</div>
<?php foreach ($dashboardBullets as $b):
$bId = (int)$b['id'];
$isComp = !empty($b['is_completed']);
$isSched = !empty($b['scheduled_task_id']) || !empty($b['scheduled_start_time']);
$schedTime = !empty($b['scheduled_start_time']) ? htmlspecialchars($b['scheduled_start_time']) : (!empty($b['live_start']) ? htmlspecialchars($b['live_start']) : '');
$schedEndTime = !empty($b['scheduled_end_time']) ? htmlspecialchars($b['scheduled_end_time']) : (!empty($b['live_end']) ? htmlspecialchars($b['live_end']) : '');
$schedDate = !empty($b['scheduled_date']) ? $b['scheduled_date'] : $selectedDate;
?>
<div
id="bullet-row-<?= $bId ?>"
class="group/bullet flex items-center justify-between p-2.5 rounded-xl border <?= $isComp ? 'bg-slate-50/60 dark:bg-slate-900/30 border-slate-200/60 dark:border-slate-800/50' : 'bg-white/80 dark:bg-[#0c0f17]/70 border-slate-200/90 dark:border-slate-800' ?> transition shadow-2xs hover:border-indigo-300 dark:hover:border-indigo-500/40"
>
<div class="flex items-center space-x-2.5 min-w-0 flex-1 mr-2">
<!-- Checkbox / Toggle -->
<button
type="button"
onclick="toggleBulletItem(<?= $bId ?>, <?= $isComp ? 0 : 1 ?>)"
title="<?= $isComp ? 'Markeren als niet afgerond' : 'Afvinken' ?>"
class="w-5 h-5 rounded-lg flex items-center justify-center flex-shrink-0 transition cursor-pointer <?= $isComp ? 'bg-emerald-500 text-white' : 'border border-slate-300 dark:border-slate-700 hover:border-indigo-500 text-transparent hover:text-indigo-400' ?>"
>
<i data-lucide="check" class="w-3.5 h-3.5 stroke-[3]"></i>
</button>
<!-- Tekst & Agenda Badge -->
<div class="min-w-0 flex-1">
<div class="flex items-center flex-wrap gap-1.5">
<span
id="bullet-text-<?= $bId ?>"
ondblclick="startEditBullet(<?= $bId ?>)"
class="text-xs font-medium select-text break-words <?= $isComp ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-900 dark:text-slate-100' ?>"
><?= htmlspecialchars($b['content']) ?></span>
<?php if ($isSched): ?>
<button
type="button"
onclick="openBulletScheduleModal(<?= $bId ?>, '<?= htmlspecialchars(addslashes($b['content'])) ?>', '<?= $schedDate ?>', '<?= $schedTime ?>', '<?= $schedEndTime ?>')"
title="Ingepland op agenda: <?= $schedDate ?> <?= $schedTime ?><?= $schedEndTime ? ' - ' . $schedEndTime : '' ?> (klik om te wijzigen)"
class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md bg-indigo-50 dark:bg-indigo-500/15 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-500/30 text-[10px] font-mono font-bold hover:bg-indigo-100 transition cursor-pointer"
>
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
<span><?= ($schedDate === $today ? 'Vandaag' : $schedDate) ?> <?= $schedTime ?><?= $schedEndTime ? '-' . $schedEndTime : '' ?></span>
</button>
<?php endif; ?>
</div>
</div>
</div>
<!-- Knoppen: Inplannen, Bewerken, Verwijderen -->
<div class="flex items-center space-x-1 opacity-80 sm:opacity-0 sm:group-hover/bullet:opacity-100 transition-opacity flex-shrink-0">
<!-- Knop: In agenda plannen (Vrij blok) -->
<button
type="button"
onclick="openBulletScheduleModal(<?= $bId ?>, '<?= htmlspecialchars(addslashes($b['content'])) ?>', '<?= $schedDate ?>', '<?= $schedTime ?>', '<?= $schedEndTime ?>')"
title="In agenda inplannen (kies een vrij blok)"
class="p-1.5 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-500/20 transition cursor-pointer"
>
<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i>
</button>
<!-- Knop: Bewerken -->
<button
type="button"
onclick="startEditBullet(<?= $bId ?>)"
title="Tekst bewerken"
class="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer"
>
<i data-lucide="pencil" class="w-3.5 h-3.5"></i>
</button>
<!-- Knop: Verwijderen -->
<button
type="button"
onclick="deleteBulletItem(<?= $bId ?>)"
title="Verwijderen"
class="p-1.5 rounded-lg text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-500/20 transition cursor-pointer"
>
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<!-- 2. Vrij Kladblok Weergave (Oorspronkelijke textarea, volledig bewaard) -->
<div id="bulletNotesViewArea" class="hidden space-y-2">
<div class="flex items-center justify-between text-[11px] text-slate-400">
<span>Vrije notities & memo's voor deze dag</span>
<span id="scratchpadSaveStatus" class="font-mono">Automatisch opgeslagen</span>
</div>
<textarea
id="dailyNotesInput"
rows="6"
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>
</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,
'isToday' => (bool)$isTodaySelected,
'isWorkday' => (bool)$isSelectedDateWorkday,
'isBlocked' => (bool)$isSelectedDateBlocked,
'selectedDayBlock'=> $selectedDayBlock ?? null,
'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: Snelle Bullet Notitie Inplannen in Agenda (Vrij Blok Kiezen) -->
<div id="bulletScheduleModal" onclick="if(event.target === this) closeBulletScheduleModal()" 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 max-h-[90vh] overflow-y-auto shadow-2xl space-y-4">
<!-- Header -->
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Notitie Inplannen in Agenda</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400">Kies een slim vrij blok of stel zelf een tijdstip in</p>
</div>
</div>
<button type="button" onclick="closeBulletScheduleModal()" 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="schedBulletId" value="">
<!-- Titel / Notitie Inhoud -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Omschrijving / Titel van afspraak</label>
<input
type="text"
id="schedBulletTitle"
placeholder="Bijv. Bespreken met collega..."
class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs font-semibold focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs"
>
</div>
<!-- Datum & Snelle knoppen -->
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300">Datum</label>
<div class="flex items-center space-x-1.5">
<button type="button" onclick="setSchedBulletDate('today')" class="text-[10px] px-2 py-0.5 rounded-md bg-slate-100 dark:bg-slate-800 hover:bg-indigo-50 dark:hover:bg-indigo-500/20 text-slate-600 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 font-semibold transition cursor-pointer">Vandaag</button>
<button type="button" onclick="setSchedBulletDate('tomorrow')" class="text-[10px] px-2 py-0.5 rounded-md bg-slate-100 dark:bg-slate-800 hover:bg-indigo-50 dark:hover:bg-indigo-500/20 text-slate-600 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 font-semibold transition cursor-pointer">Morgen</button>
</div>
</div>
<input
type="date"
id="schedBulletDate"
onchange="refreshBulletFreeSlots()"
class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 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>
<!-- Tijdsduur Kwartier/Halfuur Selector -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1.5">Geschatte Tijdsduur</label>
<input type="hidden" id="schedBulletDuration" value="30">
<div class="grid grid-cols-4 gap-2">
<button type="button" onclick="setSchedBulletDuration(15, this)" class="btn-sched-dur py-1.5 rounded-xl border border-slate-200 dark:border-slate-800 text-xs font-semibold text-slate-600 dark:text-slate-400 hover:border-indigo-500 transition cursor-pointer text-center">15m</button>
<button type="button" onclick="setSchedBulletDuration(30, this)" class="btn-sched-dur py-1.5 rounded-xl border border-indigo-500 bg-indigo-50 dark:bg-indigo-500/20 text-xs font-bold text-indigo-700 dark:text-indigo-300 transition cursor-pointer text-center">30m</button>
<button type="button" onclick="setSchedBulletDuration(45, this)" class="btn-sched-dur py-1.5 rounded-xl border border-slate-200 dark:border-slate-800 text-xs font-semibold text-slate-600 dark:text-slate-400 hover:border-indigo-500 transition cursor-pointer text-center">45m</button>
<button type="button" onclick="setSchedBulletDuration(60, this)" class="btn-sched-dur py-1.5 rounded-xl border border-slate-200 dark:border-slate-800 text-xs font-semibold text-slate-600 dark:text-slate-400 hover:border-indigo-500 transition cursor-pointer text-center">60m</button>
</div>
</div>
<!-- Vrije Agendablokken Container (Gedetecteerd op basis van meetings en taken) -->
<div>
<div class="flex items-center justify-between mb-1.5">
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 flex items-center space-x-1.5">
<i data-lucide="sparkles" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Kies een vrij agendablok:</span>
</label>
<button type="button" onclick="refreshBulletFreeSlots()" class="text-[10px] text-indigo-600 dark:text-indigo-400 hover:underline flex items-center space-x-1 cursor-pointer">
<i data-lucide="refresh-cw" class="w-2.5 h-2.5"></i>
<span>Vernieuwen</span>
</button>
</div>
<div id="schedBulletFreeSlotsArea" class="bg-slate-50 dark:bg-[#0c0f17]/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
<div id="schedBulletSlotsList" class="flex flex-wrap gap-2 max-h-36 overflow-y-auto">
<!-- Dynamisch geladen vrije blokken -->
<span class="text-xs text-slate-400 italic">Vrije blokken laden...</span>
</div>
</div>
</div>
<!-- Tijdblok Handmatig Instellen -->
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd</label>
<input
type="text"
id="schedBulletStart"
placeholder="Bijv. 14:00"
oninput="onSchedBulletTimeChange()"
class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 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>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Eindtijd</label>
<input
type="text"
id="schedBulletEnd"
placeholder="Bijv. 14:30"
class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 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>
<!-- Prioriteit & Outlook Synchronisatie -->
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800">
<div class="flex items-center justify-between">
<label class="text-xs font-semibold text-slate-700 dark:text-slate-300">Prioriteit op planning</label>
<select id="schedBulletPriority" class="bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="P1">P1 - Hoge prioriteit</option>
<option value="P2" selected>P2 - Normaal</option>
<option value="P3">P3 - Lage prioriteit</option>
</select>
</div>
<label class="flex items-center space-x-2 text-xs text-slate-700 dark:text-slate-300 cursor-pointer pt-1">
<input type="checkbox" id="schedBulletSyncOutlook" <?= $isOutlookConnected ? 'checked' : '' ?> class="rounded text-indigo-600 focus:ring-indigo-500 w-4 h-4">
<span>Direct synchroniseren naar Microsoft Outlook agenda</span>
</label>
</div>
<!-- Footer Actieknoppen -->
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800">
<button
type="button"
id="btnSchedBulletUnschedule"
onclick="unscheduleCurrentBullet()"
class="hidden text-rose-600 hover:text-rose-700 dark:text-rose-400 hover:underline text-xs font-semibold cursor-pointer"
>
Uit agenda verwijderen
</button>
<div class="flex space-x-2 ml-auto">
<button type="button" onclick="closeBulletScheduleModal()" 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 font-semibold text-xs transition cursor-pointer">
Annuleren
</button>
<button type="button" id="btnSubmitSchedBullet" onclick="submitBulletSchedule()" class="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition shadow-xs flex items-center space-x-1.5 cursor-pointer">
<i data-lucide="calendar-check" class="w-4 h-4"></i>
<span>In agenda plannen</span>
</button>
</div>
</div>
</div>
</div>
<!-- Modal: Jira Ticket Inplannen (met datum, tijdsuggestie & aanpasbare titel) -->
<div id="jiraPlanModal" onclick="if(event.target === this) closeJiraPlanModal()" 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 max-h-[90vh] overflow-y-auto 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="calendar-plus" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white" id="jiraPlanModalTitle">Jira Ticket Inplannen</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400" id="jiraPlanModalKey">—</p>
</div>
</div>
<button type="button" onclick="closeJiraPlanModal()" 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>
<!-- Hidden veld voor bestaande gekoppelde taak (bij herplannen) -->
<input type="hidden" id="jiraPlanTaskId" value="">
<!-- Banner: Al Ingepland (zichtbaar wanneer ticket reeds gepland is) -->
<div id="jiraPlanAlreadyScheduledBanner" class="hidden p-3 rounded-xl bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-300 dark:border-emerald-700 text-emerald-900 dark:text-emerald-200 text-xs">
<div class="flex items-start space-x-2">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0 mt-0.5"></i>
<div class="flex-1 min-w-0">
<p class="font-bold">Reeds ingepland in je dagplanning</p>
<p class="text-[11px] text-emerald-800 dark:text-emerald-300 mt-0.5" id="jiraPlanAlreadyScheduledDetails">
Ingepland op ...
</p>
</div>
</div>
</div>
<!-- Ticket samenvatting & Uitklapbare beschrijving -->
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 p-3.5 space-y-2.5">
<div>
<p class="text-xs font-semibold text-slate-800 dark:text-slate-200 leading-snug" id="jiraPlanModalSummary">—</p>
<div class="flex items-center space-x-2 mt-1.5">
<span class="text-[10px] text-slate-500" id="jiraPlanModalStatus">—</span>
<span class="text-slate-300 dark:text-slate-600">•</span>
<span class="text-[10px] text-slate-500" id="jiraPlanModalPriority">—</span>
<span id="jiraPlanModalSpBadge" class="hidden px-1.5 py-0.5 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"></span>
</div>
</div>
<!-- Uitklapknop voor inhoud ticket (10 regels verticaal scrollbaar) -->
<div class="pt-2 border-t border-slate-200/70 dark:border-slate-800/70">
<button type="button" onclick="toggleJiraPlanDescription()" class="w-full flex items-center justify-between text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition cursor-pointer group">
<span class="flex items-center space-x-1.5">
<i data-lucide="file-text" class="w-3.5 h-3.5"></i>
<span id="jiraPlanDescToggleLabel">Inhoud van het item bekijken</span>
</span>
<i data-lucide="chevron-down" id="jiraPlanDescChevron" class="w-3.5 h-3.5 transition-transform duration-200 group-hover:translate-y-0.5"></i>
</button>
<div id="jiraPlanDescWrapper" class="hidden mt-2.5">
<div id="jiraPlanModalDescription" class="max-h-48 overflow-y-auto p-3 bg-white dark:bg-[#0c0f17] rounded-xl border border-slate-200 dark:border-slate-800 text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans leading-relaxed"></div>
</div>
</div>
</div>
<!-- Formulier velden -->
<div class="space-y-3.5">
<!-- Titel voor de agenda / dagtaak -->
<div>
<label for="jiraPlanCustomTitle" id="jiraPlanCustomTitleLabel" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
Hoe het in de agenda komt (Titel)
</label>
<input type="text" id="jiraPlanCustomTitle" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 placeholder-slate-400" placeholder="Oppakken Jira item ...">
</div>
<!-- Datum & Tijdblok met slimme "Plan in open blok" actieknop -->
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center justify-between">
<span class="text-[11px] font-bold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
<button type="button" onclick="planJiraInNextOpenBlock()" id="btnJiraPlanOpenBlock" class="inline-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-bold transition shadow-xs cursor-pointer active:scale-95" title="Vind automatisch het eerstvolgende vrije kwartierblok binnen werktijden">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
<span>Plan in open blok</span>
</button>
</div>
<!-- Succes / statusmelding wanneer een open blok gevonden is -->
<div id="jiraPlanOpenBlockNotice" class="hidden p-2.5 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-800 dark:text-emerald-300 text-[11px] flex items-center justify-between">
<div class="flex items-center space-x-1.5 min-w-0 flex-1 mr-2">
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i>
<span id="jiraPlanOpenBlockNoticeText" class="truncate font-semibold"></span>
</div>
<button type="button" onclick="document.getElementById('jiraPlanOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
</div>
</div>
<!-- Datum & Starttijd -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label for="jiraPlanDate" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
Datum
</label>
<input type="date" id="jiraPlanDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" onchange="updateJiraPlanEndTimeDisplay()">
</div>
<div>
<div class="flex items-center justify-between mb-1.5">
<label for="jiraPlanStartTime" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300">
Starttijd
</label>
<span id="jiraPlanEndTimeBadge" class="text-[10px] font-mono text-indigo-600 dark:text-indigo-400 font-semibold"></span>
</div>
<div class="relative flex items-center">
<input type="time" id="jiraPlanStartTime" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="updateJiraPlanEndTimeDisplay()">
<button type="button" onclick="clearJiraPlanStartTime()" title="Starttijd wissen" class="absolute right-2 p-1 text-slate-400 hover:text-rose-500 dark:hover:text-rose-400 rounded transition cursor-pointer">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</div>
<p class="text-[10px] text-slate-400 mt-1 flex items-center justify-between">
<span>Laat leeg voor handmatig inplannen</span>
<button type="button" onclick="clearJiraPlanStartTime()" class="text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer flex items-center space-x-0.5" title="Starttijd wissen">
<i data-lucide="x" class="w-2.5 h-2.5"></i>
<span>Wissen</span>
</button>
</p>
</div>
</div>
<!-- Duur -->
<div>
<label class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Duur</label>
<div class="grid grid-cols-4 gap-2">
<button type="button" onclick="setJiraPlanDuration(15)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="15">15m</button>
<button type="button" onclick="setJiraPlanDuration(30)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="30">30m</button>
<button type="button" onclick="setJiraPlanDuration(60)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="60">1u</button>
<button type="button" onclick="setJiraPlanDuration(90)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="90">1,5u</button>
</div>
<input type="hidden" id="jiraPlanDuration" value="15">
</div>
<!-- Prioriteit -->
<div>
<label for="jiraPlanPriority" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Prioriteit</label>
<select id="jiraPlanPriority" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="P1">P1 — Urgent</option>
<option value="P2">P2 — Hoog</option>
<option value="P3" selected>P3 — Normaal</option>
</select>
</div>
<!-- Optie: Tevens toevoegen aan Bord & Tijdlijn -->
<div class="p-3 rounded-xl bg-slate-50 dark:bg-slate-900/40 border border-slate-200 dark:border-slate-800 space-y-2.5">
<label class="flex items-center space-x-2.5 cursor-pointer select-none">
<input type="checkbox" id="jiraPlanAddToBoard" onchange="toggleJiraBoardOptions(this.checked)" class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 dark:bg-slate-900 cursor-pointer">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5">
<i data-lucide="layout-grid" class="w-3.5 h-3.5 text-indigo-600 dark:text-indigo-400"></i>
<span>Tevens toevoegen aan Bord &amp; Tijdlijn</span>
</span>
</label>
<div id="jiraBoardOptionsWrapper" class="hidden space-y-2.5 pt-2.5 border-t border-slate-200/70 dark:border-slate-800">
<?php if (empty($userBoards)): ?>
<p class="text-[11px] text-slate-500 dark:text-slate-400">Er zijn geen actieve borden gevonden.</p>
<?php else: ?>
<div class="grid grid-cols-2 gap-2">
<div>
<label for="jiraPlanBoardSelect" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Bord</label>
<select id="jiraPlanBoardSelect" onchange="onJiraBoardSelectChange(this.value)" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<?php foreach ($userBoards as $b): ?>
<option value="<?= (int)$b['id'] ?>"><?= htmlspecialchars($b['title']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label for="jiraPlanLaneSelect" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Kolom</label>
<select id="jiraPlanLaneSelect" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<!-- Wordt gevuld via JS -->
</select>
</div>
</div>
<div class="pt-1">
<label class="flex items-center space-x-2 cursor-pointer select-none">
<input type="checkbox" id="jiraPlanScheduleOnTimeline" checked onchange="toggleJiraTimelineDates(this.checked)" class="w-3.5 h-3.5 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 dark:bg-slate-900 cursor-pointer">
<span class="text-xs font-medium text-slate-700 dark:text-slate-300">Inplannen op de Project Tijdlijn</span>
</label>
<div id="jiraTimelineDatesWrapper" class="grid grid-cols-2 gap-2 mt-2">
<div>
<label for="jiraTimelineStartDate" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Startdatum</label>
<input type="date" id="jiraTimelineStartDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label for="jiraTimelineEndDate" class="block text-[10px] font-bold text-slate-600 dark:text-slate-400 mb-1 uppercase tracking-wider">Einddatum</label>
<input type="date" id="jiraTimelineEndDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="flex space-x-3 pt-2 border-t border-slate-100 dark:border-slate-800/80">
<button type="button" onclick="closeJiraPlanModal()" class="flex-1 px-4 py-2.5 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 font-semibold text-sm transition cursor-pointer">
Annuleren
</button>
<button type="button" id="btnSubmitJiraPlan" onclick="submitJiraPlanModal()" class="flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer">
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
<span>Inplannen</span>
</button>
</div>
</div>
</div>
<!-- Modal: Jira Ticket Detail (popup bij klikken op ticket) -->
<div id="jiraTicketDetailModal" 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-0 w-full max-w-lg shadow-2xl overflow-hidden flex flex-col max-h-[85vh]">
<!-- Header -->
<div class="flex items-start justify-between p-5 border-b border-slate-100 dark:border-slate-800/80 flex-shrink-0">
<div class="space-y-1 min-w-0 flex-1 mr-3">
<div class="flex items-center space-x-2">
<span class="font-mono text-[11px] 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-2 py-0.5 rounded flex-shrink-0" id="jiraDetailKey">—</span>
<span class="text-[10px] px-2 py-0.5 rounded-full font-bold" id="jiraDetailStatusBadge">—</span>
<span class="text-[10px] px-2 py-0.5 rounded-full font-bold" id="jiraDetailPriorityBadge">—</span>
</div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white leading-snug" id="jiraDetailSummary">—</h3>
</div>
<button onclick="closeJiraTicketDetailModal()" 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 flex-shrink-0">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<!-- Body: scrollable -->
<div class="overflow-y-auto flex-1 p-5 space-y-4">
<!-- Meta info -->
<div class="grid grid-cols-2 gap-3">
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Aangemeld bij</p>
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailAssignee">—</p>
</div>
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Story Points</p>
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailSp">—</p>
</div>
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Project</p>
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailProject">—</p>
</div>
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Type</p>
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailType">—</p>
</div>
</div>
<!-- Beschrijving -->
<div id="jiraDetailDescSection" class="hidden">
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-1.5">Beschrijving</p>
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800 text-xs text-slate-700 dark:text-slate-300 leading-relaxed whitespace-pre-wrap max-h-40 overflow-y-auto" id="jiraDetailDesc"></div>
</div>
<!-- Labels -->
<div id="jiraDetailLabelsSection" class="hidden">
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-1.5">Labels</p>
<div class="flex flex-wrap gap-1.5" id="jiraDetailLabels"></div>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-between p-4 border-t border-slate-100 dark:border-slate-800/80 flex-shrink-0 gap-3">
<a href="#" target="_blank" id="jiraDetailOpenLink" class="flex items-center space-x-1.5 text-xs font-semibold text-sky-600 dark:text-sky-400 hover:underline cursor-pointer">
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
<span>Openen in Jira</span>
</a>
<div class="flex items-center space-x-2">
<button type="button" onclick="closeJiraTicketDetailModal()" class="px-3 py-1.5 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 font-semibold text-xs transition cursor-pointer">
Sluiten
</button>
<button type="button" onclick="openJiraPlanFromDetail()" id="btnJiraDetailPlan" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition cursor-pointer">
<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i>
<span>Inplannen</span>
</button>
</div>
</div>
</div>
</div>
<!-- Modal: Taak & Tijdblok Bewerken (Vrij invullen van tijden & duur) -->
<div id="editTaskModal" onclick="if(event.target === this) closeEditTaskModal()" 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 id="editTaskModalHeaderTitle" class="text-sm font-bold text-slate-900 dark:text-white">Taak & Tijdblok Bewerken</h3>
<p id="editTaskModalHeaderSubtitle" 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">
<input type="hidden" id="editTaskIsOutlook" value="0">
<div>
<label id="editTaskTitleLabel" 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>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Beschrijving / Notities</label>
<textarea id="editTaskDescription" rows="2" placeholder="Optionele toelichting..." 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"></textarea>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div id="editTaskPriorityContainer">
<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 id="editTaskDurationContainer">
<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>
<!-- Datum & Tijdblok met slimme "Plan in open blok" actieknop -->
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center justify-between">
<span class="text-xs font-semibold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
<button type="button" onclick="planInNextOpenBlock()" id="btnPlanOpenBlock" class="inline-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-bold transition shadow-xs cursor-pointer active:scale-95" title="Vind automatisch het eerstvolgende vrije kwartierblok binnen werktijden">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
<span>Plan in open blok</span>
</button>
</div>
<!-- Succes / statusmelding wanneer een open blok gevonden is -->
<div id="editTaskOpenBlockNotice" class="hidden p-2.5 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-800 dark:text-emerald-300 text-[11px] flex items-center justify-between">
<div class="flex items-center space-x-1.5 min-w-0 flex-1 mr-2">
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i>
<span id="editTaskOpenBlockNoticeText" class="truncate font-semibold"></span>
</div>
<button type="button" onclick="document.getElementById('editTaskOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label for="editTaskDate" class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Datum</label>
<input type="date" id="editTaskDate" 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">
</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">Starttijd</label>
<button type="button" onclick="clearEditTaskTimes()" title="Tijd wissen" class="text-[10px] text-slate-400 hover:text-rose-500 cursor-pointer">Wissen</button>
</div>
<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">Auto/Vrij</span>
</div>
<div class="relative">
<input type="text" id="editTaskEndTime" placeholder="Bijv. 10:00" 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: Waarschuwing Buiten Werktijden -->
<div id="outsideWorkhoursModal" onclick="if(event.target === this) closeOutsideWorkhoursModal('CANCEL')" 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-md shadow-2xl space-y-4 border border-amber-500/30">
<div class="flex items-start space-x-3">
<div class="w-10 h-10 rounded-2xl bg-amber-500/10 border border-amber-500/20 text-amber-500 flex items-center justify-center flex-shrink-0">
<i data-lucide="alert-triangle" class="w-5 h-5"></i>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base font-bold text-slate-900 dark:text-white leading-tight">
Let op! Je plant buiten werktijden
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">
Het ingeplande tijdblok valt buiten de gezette werktijden.
</p>
</div>
<button type="button" onclick="closeOutsideWorkhoursModal('CANCEL')" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg transition cursor-pointer flex-shrink-0">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800/40 rounded-xl p-3.5 text-xs text-amber-800 dark:text-amber-300 space-y-1.5">
<div class="flex items-center justify-between text-[11px] font-semibold text-amber-900 dark:text-amber-200">
<span>Gekozen tijdblok:</span>
<span id="outsideWorkhoursSelectedSlot" class="font-mono font-bold"></span>
</div>
<div class="flex items-center justify-between text-[11px] text-amber-700 dark:text-amber-400">
<span>Gezette werktijden:</span>
<span id="outsideWorkhoursBounds" class="font-mono font-semibold"></span>
</div>
<p class="text-[11px] text-amber-700/80 dark:text-amber-400/80 pt-1 border-t border-amber-200/60 dark:border-amber-800/30">
Kies <strong>Nee</strong> om het item automatisch in te plannen in het eerstvolgende open blok op de volgende werkdag, of kies <strong>Ja</strong> om toch nu buiten werktijd te plannen.
</p>
</div>
<div class="flex items-center space-x-2.5 pt-2">
<button type="button" id="btnOutsideWorkhoursNo" onclick="closeOutsideWorkhoursModal('NEE')" class="flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition shadow-xs flex items-center justify-center space-x-1.5 cursor-pointer">
<i data-lucide="calendar-arrow-up" class="w-4 h-4"></i>
<span>Nee (Volgende werkdag)</span>
</button>
<button type="button" id="btnOutsideWorkhoursYes" onclick="closeOutsideWorkhoursModal('JA')" class="px-4 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 font-semibold text-xs transition cursor-pointer">
<span>Ja, toch inplannen</span>
</button>
</div>
</div>
</div>
<!-- Modal: Direct E-mail Inplannen als Dagtaak -->
<div id="planEmailDirectModal" onclick="if(event.target === this) closePlanEmailDirectModal()" 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 max-h-[92vh] overflow-y-auto">
<!-- Header -->
<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</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400" id="planEmailModalSenderBadge">—</p>
</div>
</div>
<button type="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">
<input type="hidden" id="planEmailSender">
<!-- E-mail samenvatting & Uitklapbare inhoud -->
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 p-3.5 space-y-2.5">
<div>
<p class="text-xs font-semibold text-slate-800 dark:text-slate-200 leading-snug" id="planEmailModalSubject">—</p>
<div class="flex items-center space-x-2 mt-1.5">
<span class="text-[10px] text-slate-500" id="planEmailModalSenderName">—</span>
<span class="text-slate-300 dark:text-slate-600">•</span>
<span class="text-[10px] text-sky-600 dark:text-sky-400 font-medium">Outlook Mail</span>
</div>
</div>
<!-- Uitklapknop voor inhoud e-mail -->
<div class="pt-2 border-t border-slate-200/70 dark:border-slate-800/70">
<button type="button" onclick="toggleEmailPlanPreview()" class="w-full flex items-center justify-between text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition cursor-pointer group">
<span class="flex items-center space-x-1.5">
<i data-lucide="file-text" class="w-3.5 h-3.5"></i>
<span id="emailPlanDescToggleLabel">Inhoud van het bericht bekijken</span>
</span>
<i data-lucide="chevron-down" id="emailPlanDescChevron" class="w-3.5 h-3.5 transition-transform duration-200 group-hover:translate-y-0.5"></i>
</button>
<div id="emailPlanDescWrapper" class="hidden mt-2.5">
<div id="emailPlanModalDescription" class="max-h-48 overflow-y-auto p-3 bg-white dark:bg-[#0c0f17] rounded-xl border border-slate-200 dark:border-slate-800 text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans leading-relaxed"></div>
</div>
</div>
</div>
<!-- Formulier velden -->
<div class="space-y-3.5">
<!-- Titel voor de agenda / dagtaak -->
<div>
<label for="planEmailTitle" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
Hoe het in de agenda komt (Titel)
</label>
<input type="text" id="planEmailTitle" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 placeholder-slate-400" placeholder="E-mail opvolgen...">
</div>
<!-- Datum & Tijdblok met slimme "Plan in open blok" actieknop -->
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center justify-between">
<span class="text-[11px] font-bold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
<button type="button" onclick="planEmailInNextOpenBlock()" id="btnEmailPlanOpenBlock" class="inline-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-bold transition shadow-xs cursor-pointer active:scale-95" title="Vind automatisch het eerstvolgende vrije kwartierblok binnen werktijden">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
<span>Plan in open blok</span>
</button>
</div>
<!-- Succes / statusmelding wanneer een open blok gevonden is -->
<div id="emailPlanOpenBlockNotice" class="hidden p-2.5 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-800 dark:text-emerald-300 text-[11px] flex items-center justify-between">
<div class="flex items-center space-x-1.5 min-w-0 flex-1 mr-2">
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i>
<span id="emailPlanOpenBlockNoticeText" class="truncate font-semibold"></span>
</div>
<button type="button" onclick="document.getElementById('emailPlanOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
</div>
</div>
<!-- Datum & Starttijd -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label for="planEmailDate" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
Datum
</label>
<input type="date" id="planEmailDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" onchange="updatePlanEmailEndTimeDisplay()">
</div>
<div>
<div class="flex items-center justify-between mb-1.5">
<label for="planEmailStartTime" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300">
Starttijd
</label>
<span id="planEmailEndTimeBadge" class="text-[10px] font-mono text-indigo-600 dark:text-indigo-400 font-semibold"></span>
</div>
<div class="relative flex items-center">
<input type="time" id="planEmailStartTime" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="updatePlanEmailEndTimeDisplay()">
<button type="button" onclick="clearPlanEmailStartTime()" title="Starttijd wissen" class="absolute right-2 p-1 text-slate-400 hover:text-rose-500 dark:hover:text-rose-400 rounded transition cursor-pointer">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</div>
<p class="text-[10px] text-slate-400 mt-1 flex items-center justify-between">
<span>Laat leeg voor handmatig inplannen</span>
<button type="button" onclick="clearPlanEmailStartTime()" class="text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer flex items-center space-x-0.5" title="Starttijd wissen">
<i data-lucide="x" class="w-2.5 h-2.5"></i>
<span>Wissen</span>
</button>
</p>
</div>
</div>
<!-- Duur -->
<div>
<label class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Duur</label>
<div class="grid grid-cols-4 gap-2">
<button type="button" onclick="setEmailPlanDuration(15)" class="email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="15">15m</button>
<button type="button" onclick="setEmailPlanDuration(30)" class="email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="30">30m</button>
<button type="button" onclick="setEmailPlanDuration(60)" class="email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="60">1u</button>
<button type="button" onclick="setEmailPlanDuration(90)" class="email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="90">1,5u</button>
</div>
<input type="hidden" id="planEmailDuration" value="30">
</div>
<!-- Prioriteit & Outlook Synchronisatie -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 items-center">
<div>
<label for="planEmailPriority" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Prioriteit</label>
<select id="planEmailPriority" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="P1">P1 — Urgent</option>
<option value="P2" selected>P2 — Belangrijk</option>
<option value="P3">P3 — Normaal</option>
</select>
</div>
<div class="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-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700" <?= $isOutlookConnected ? 'checked' : 'disabled' ?>>
<span>Blokkeer in Outlook Agenda</span>
</label>
</div>
</div>
</div>
<div class="flex space-x-3 pt-2 border-t border-slate-100 dark:border-slate-800/80">
<button type="button" onclick="closePlanEmailDirectModal()" class="flex-1 px-4 py-2.5 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 font-semibold text-sm transition cursor-pointer">
Annuleren
</button>
<button type="button" id="btnSubmitPlanEmailDirect" onclick="submitPlanEmailDirect()" class="flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer">
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
<span>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>
<!-- Contextmenu voor Rechtermuisklik op een dag (geplaatst op body-niveau om absolute viewport containment te waarborgen) -->
<div id="dayContextMenu" class="fixed z-[99999] hidden min-w-[220px] max-w-[280px] bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-2xl shadow-2xl py-1 text-xs text-slate-800 dark:text-slate-200 select-none animate-in fade-in zoom-in-95 duration-75">
<div class="px-3 py-1.5 text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider border-b border-slate-100 dark:border-slate-800/80 flex items-center justify-between">
<span id="dayContextMenuDateLabel">Dag</span>
<span id="dayContextMenuStatusBadge"></span>
</div>
<div id="dayContextMenuActions" class="p-1 space-y-0.5">
<!-- Acties worden via JS ingevuld -->
</div>
</div>
<!-- Contextmenu voor Rechtermuisklik op een Taak (geplaatst op body-niveau) -->
<div id="taskContextMenu" class="fixed z-[99999] hidden min-w-[240px] max-w-[300px] bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-2xl shadow-2xl py-1.5 text-xs text-slate-800 dark:text-slate-200 select-none animate-in fade-in zoom-in-95 duration-75">
<div class="px-3 py-1.5 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center justify-between gap-1.5">
<span id="taskContextMenuTitle" class="font-bold text-slate-900 dark:text-white truncate max-w-[170px] text-xs">Taak</span>
<span id="taskContextMenuPriorityBadge" class="text-[9px] font-extrabold px-1.5 py-0.5 rounded-md"></span>
</div>
<div id="taskContextMenuMeta" class="text-[10px] text-slate-400 dark:text-slate-500 font-mono mt-0.5 truncate"></div>
</div>
<div id="taskContextMenuActions" class="p-1 space-y-0.5">
<button type="button" id="taskCtxBtnOpen" class="w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-indigo-50 dark:hover:bg-indigo-950/40 text-slate-700 dark:text-slate-200 hover:text-indigo-600 dark:hover:text-indigo-400 text-xs font-semibold transition cursor-pointer text-left">
<i data-lucide="eye" class="w-3.5 h-3.5 text-indigo-500 flex-shrink-0"></i>
<span>Volledige inhoud openen</span>
</button>
<button type="button" id="taskCtxBtnEdit" class="w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition cursor-pointer text-left">
<i data-lucide="edit-3" class="w-3.5 h-3.5 text-slate-500 flex-shrink-0"></i>
<span id="taskCtxBtnEditLabel">Taak bewerken</span>
</button>
<button type="button" id="taskCtxBtnToggle" class="w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition cursor-pointer text-left">
<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-emerald-500 flex-shrink-0"></i>
<span id="taskCtxBtnToggleLabel">Voltooien</span>
</button>
<button type="button" id="taskCtxBtnSubtasks" class="w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 hover:text-slate-900 dark:hover:text-white text-xs font-semibold transition cursor-pointer text-left">
<i data-lucide="list-checks" class="w-3.5 h-3.5 text-sky-500 flex-shrink-0"></i>
<span>Subtaken / Checklist</span>
</button>
<button type="button" id="taskCtxBtnOutlook" class="hidden w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-sky-50 dark:hover:bg-sky-950/40 text-slate-700 dark:text-slate-200 hover:text-sky-600 dark:hover:text-sky-400 text-xs font-semibold transition cursor-pointer text-left">
<i data-lucide="calendar-plus" class="w-3.5 h-3.5 text-sky-500 flex-shrink-0"></i>
<span>Naar Outlook synchroniseren</span>
</button>
<div id="taskCtxDivider" class="border-t border-slate-100 dark:border-slate-800/80 my-1"></div>
<button type="button" id="taskCtxBtnDelete" class="w-full flex items-center space-x-2.5 px-2.5 py-1.5 rounded-xl hover:bg-rose-50 dark:hover:bg-rose-950/40 text-rose-600 dark:text-rose-400 text-xs font-semibold transition cursor-pointer text-left">
<i data-lucide="trash-2" class="w-3.5 h-3.5 text-rose-500 flex-shrink-0"></i>
<span id="taskCtxBtnDeleteLabel">Taak verwijderen</span>
</button>
</div>
</div>
<!-- Modal: Volledige Taak Inhoud & Details Bekijken (Fociz Release v2.2) -->
<div id="taskDetailModal" onclick="if(event.target === this) closeTaskDetailModal()" 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-xl shadow-2xl space-y-4 max-h-[90vh] flex flex-col">
<!-- Header -->
<div class="flex items-start justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 flex-shrink-0">
<div class="flex items-start space-x-3 min-w-0 flex-1 mr-3">
<div id="taskDetailPriorityIcon" class="w-10 h-10 rounded-2xl flex items-center justify-center flex-shrink-0 mt-0.5">
<i data-lucide="check-square" class="w-5 h-5"></i>
</div>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-1.5 mb-1">
<span id="taskDetailPriorityBadge" class="px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wide"></span>
<span id="taskDetailSourceBadge" class="px-2 py-0.5 rounded-full text-[10px] font-mono font-bold"></span>
<span id="taskDetailStatusBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold"></span>
</div>
<h3 id="taskDetailTitle" class="text-base font-bold text-slate-900 dark:text-white leading-snug break-words"></h3>
</div>
</div>
<button type="button" onclick="closeTaskDetailModal()" 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 flex-shrink-0">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<!-- Scrollable Content Body -->
<div class="flex-1 overflow-y-auto pr-1 space-y-4 text-xs">
<!-- Tijd & Planning blok -->
<div class="p-3.5 rounded-2xl bg-slate-50/80 dark:bg-[#0c0f17]/70 border border-slate-200/80 dark:border-slate-800/80 grid grid-cols-2 sm:grid-cols-3 gap-3">
<div>
<span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 block mb-0.5">Datum</span>
<span id="taskDetailDate" class="font-semibold text-slate-800 dark:text-slate-200"></span>
</div>
<div>
<span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 block mb-0.5">Tijdblok</span>
<span id="taskDetailTimeSlot" class="font-mono font-semibold text-slate-800 dark:text-slate-200"></span>
</div>
<div>
<span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 block mb-0.5">Geschatte Duur</span>
<span id="taskDetailDuration" class="font-mono font-bold text-indigo-600 dark:text-indigo-400"></span>
</div>
</div>
<!-- Volledige Beschrijving / Inhoud -->
<div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300">
Volledige Beschrijving & Inhoud
</label>
<div id="taskDetailDescription" class="p-3.5 rounded-2xl bg-slate-50/50 dark:bg-[#0c0f17]/50 border border-slate-200/60 dark:border-slate-800/60 text-slate-700 dark:text-slate-300 whitespace-pre-wrap leading-relaxed min-h-[60px]">
</div>
</div>
<!-- Subtaken & Checklist -->
<div id="taskDetailSubtasksSection" class="space-y-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-1.5">
<i data-lucide="list-checks" class="w-4 h-4 text-indigo-500"></i>
<span class="text-xs font-bold text-slate-800 dark:text-slate-200">Subtaken / Checklist</span>
</div>
<span id="taskDetailSubtasksCount" class="text-[10px] font-mono text-slate-400"></span>
</div>
<div id="taskDetailSubtasksList" class="space-y-1.5">
<!-- Dynamisch geladen -->
</div>
<!-- Snel subtaak toevoegen binnen detail modal -->
<div class="flex items-center space-x-2 pt-1">
<input
type="text"
id="taskDetailNewSubtaskInput"
placeholder="Nieuwe checklist item 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'){event.preventDefault(); addSubtaskFromDetailModal();}"
>
<button type="button" onclick="addSubtaskFromDetailModal()" class="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold rounded-xl transition cursor-pointer flex items-center space-x-1">
<i data-lucide="plus" class="w-3 h-3"></i>
<span>Toevoegen</span>
</button>
</div>
</div>
<!-- Externe Koppeling / Broninfo (indien Jira, Outlook of Mail) -->
<div id="taskDetailExternalBlock" class="hidden p-3 rounded-xl bg-indigo-50/50 dark:bg-indigo-950/20 border border-indigo-200/50 dark:border-indigo-800/40 flex items-center justify-between">
<div class="flex items-center space-x-2 text-xs text-indigo-900 dark:text-indigo-200">
<i data-lucide="link" class="w-3.5 h-3.5 text-indigo-500"></i>
<span id="taskDetailExternalLabel">Gekoppeld item</span>
</div>
<a id="taskDetailExternalLink" href="#" target="_blank" class="px-2.5 py-1 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-[11px] font-bold transition flex items-center space-x-1 shadow-2xs">
<span>Openen</span>
<i data-lucide="external-link" class="w-3 h-3"></i>
</a>
</div>
</div>
<!-- Footer Actions -->
<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" id="taskDetailBtnDelete" class="px-3.5 py-2 rounded-xl text-xs font-bold text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer flex items-center space-x-1.5">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span id="taskDetailBtnDeleteLabel">Verwijderen</span>
</button>
<div class="flex items-center space-x-2">
<button type="button" onclick="closeTaskDetailModal()" 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">
Sluiten
</button>
<button type="button" id="taskDetailBtnEdit" 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="edit-3" class="w-3.5 h-3.5"></i>
<span id="taskDetailBtnEditLabel">Taak Bewerken</span>
</button>
</div>
</div>
</div>
</div>
<!-- Modal: Agenda Dagen Blokkeren (Vakantie / Verlof / Focus) -->
<div id="calendarBlockModal" 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 max-h-[90vh] flex flex-col">
<!-- Header -->
<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-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center">
<i data-lucide="palmtree" class="w-4 h-4"></i>
</div>
<div>
<h3 id="calendarBlockModalTitle" class="text-sm font-bold text-slate-900 dark:text-white">Dagen Blokkeren / Vakantie</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400">Blokkeer een hele dag of periode in je agenda & planner</p>
</div>
</div>
<button type="button" onclick="closeCalendarBlockModal()" 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>
<!-- Formulier / Content -->
<form id="calendarBlockForm" onsubmit="submitCalendarBlock(event)" class="flex-1 overflow-y-auto pr-1 space-y-3.5">
<input type="hidden" id="calBlockId" value="0">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Type Blokkade</label>
<select id="calBlockType" onchange="updateCalBlockDefaultTitle(this.value)" class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-amber-500">
<?php foreach ($calendarBlockReasons as $reason): ?>
<?php if (!empty($reason['is_active'])): ?>
<option value="<?= htmlspecialchars($reason['code']) ?>"><?= htmlspecialchars((!empty($reason['icon']) ? $reason['icon'] . ' ' : '') . $reason['title']) ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Titel / Omschrijving <span class="text-rose-500">*</span></label>
<input type="text" id="calBlockTitle" required placeholder="Bijv. Vakantie, Vrije dag, Verlof..." class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white placeholder-slate-400 focus:outline-hidden focus:ring-2 focus:ring-amber-500">
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Begindatum <span class="text-rose-500">*</span></label>
<input type="date" id="calBlockStartDate" required class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-amber-500">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Einddatum (t/m) <span class="text-rose-500">*</span></label>
<input type="date" id="calBlockEndDate" required class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-amber-500">
</div>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Notities (optioneel)</label>
<textarea id="calBlockNotes" rows="2" placeholder="Eventuele overdracht of bijzonderheden..." class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white placeholder-slate-400 focus:outline-hidden focus:ring-2 focus:ring-amber-500"></textarea>
</div>
<?php if ($isOutlookConnected): ?>
<div class="p-3 rounded-xl bg-sky-50/60 dark:bg-sky-950/20 border border-sky-200/60 dark:border-sky-800/40 flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<i data-lucide="refresh-cw" class="w-4 h-4 text-sky-600 dark:text-sky-400 flex-shrink-0"></i>
<div>
<p class="text-xs font-bold text-sky-900 dark:text-sky-200">Synchroniseren naar Outlook 365</p>
<p class="text-[10px] text-sky-700/80 dark:text-sky-400/80">Maakt een 'Afwezig' (OOF) dagblokkade aan in je Microsoft agenda</p>
</div>
</div>
<label class="relative inline-flex items-center cursor-pointer flex-shrink-0 ml-2">
<input type="checkbox" id="calBlockSyncOutlook" checked class="sr-only peer">
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-slate-600 peer-checked:bg-sky-600"></div>
</label>
</div>
<?php endif; ?>
<!-- Knoppen balk -->
<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" id="btnDeleteCurrentCalBlock" onclick="deleteCurrentCalendarBlock()" class="hidden px-3 py-2 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-950/50 dark:hover:bg-rose-900/60 text-rose-700 dark:text-rose-300 text-xs font-bold border border-rose-200 dark:border-rose-800/60 transition cursor-pointer items-center space-x-1">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>Opheffen</span>
</button>
<div class="flex items-center space-x-2 ml-auto">
<button type="button" onclick="closeCalendarBlockModal()" 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="submit" id="btnSaveCalBlock" class="px-4 py-2 rounded-xl bg-amber-600 hover:bg-amber-500 text-white text-xs font-bold transition shadow-xs cursor-pointer flex items-center space-x-1.5">
<i data-lucide="check" class="w-3.5 h-3.5"></i>
<span>Opslaan</span>
</button>
</div>
</div>
</form>
</div>
</div>
<script>
// --- Dashboard Banner Instellingen Dropdown Menu ---
function togglePlannerSettingsMenu(e) {
if (e) e.stopPropagation();
const menu = document.getElementById('plannerSettingsDropdownMenu');
if (menu) {
menu.classList.toggle('hidden');
}
}
function closePlannerSettingsMenu() {
const menu = document.getElementById('plannerSettingsDropdownMenu');
if (menu) {
menu.classList.add('hidden');
}
}
document.addEventListener('click', function(e) {
const menu = document.getElementById('plannerSettingsDropdownMenu');
const btn = document.getElementById('plannerSettingsDropdownBtn');
if (menu && !menu.classList.contains('hidden')) {
if (!menu.contains(e.target) && !btn.contains(e.target)) {
closePlannerSettingsMenu();
}
}
});
// --- Globale Planner Status & Naadloos AJAX Datumbeheer ---
let activePlannerDate = '<?= $selectedDate ?>';
let weekStartDate = '<?= $weekStart ?>';
let weekEndDate = '<?= $weekEnd ?>';
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
let isDateSwitching = false;
const FOCIZ_WORKDAY_START = '<?= $workdayStartStr ?>';
const FOCIZ_WORKDAY_END = '<?= $workdayEndStr ?>';
// --- Fociz Widget Beheer & Configuratielogica (v2.0) ---
let focizWidgets = <?= json_encode(array_values($userWidgetLayout), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
const defaultWidgetLayout = <?= json_encode(array_values($availableWidgets), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
// --- 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 {
focizWidgets.forEach((item, idx) => {
item.order = idx + 1;
});
const payload = focizWidgets.map((w) => {
const obj = {
id: w.id,
enabled: !!w.enabled,
col_span: parseInt(w.col_span, 10) || 6,
order: w.order
};
if (w.custom_title) {
obj.custom_title = w.custom_title;
}
return obj;
});
try {
localStorage.setItem('fociz_widget_layout_cache', JSON.stringify(payload));
} catch(e) {}
const res = await fetch('/api/planner.php?action=save_widget_layout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ layout: payload }),
keepalive: true
});
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);
}
// --- Agenda Dagen Blokkeren & Rechtermuisklik Context Menu ---
const CAL_BLOCK_REASONS = <?= json_encode(array_values(array_filter($calendarBlockReasons, fn($r) => !empty($r['is_active']))), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
const CAL_BLOCK_DEFAULT_TITLES = {};
if (Array.isArray(CAL_BLOCK_REASONS)) {
CAL_BLOCK_REASONS.forEach(r => {
if (r && r.code) CAL_BLOCK_DEFAULT_TITLES[r.code] = r.title;
});
}
function selectCalBlockType(type) {
const input = document.getElementById('calBlockType');
if (input) input.value = type;
}
function updateCalBlockDefaultTitle(type) {
const titleInput = document.getElementById('calBlockTitle');
const currentVal = titleInput ? titleInput.value.trim() : '';
selectCalBlockType(type);
if (titleInput && (!currentVal || Object.values(CAL_BLOCK_DEFAULT_TITLES).includes(currentVal))) {
titleInput.value = CAL_BLOCK_DEFAULT_TITLES[type] || '';
}
}
// --- Rechtermuisklik Context Menu voor Dagen ---
let activeContextMenuDay = null;
function openDayContextMenu(e, dateStr, blockId = 0, blockTitle = '') {
e.preventDefault();
e.stopPropagation();
activeContextMenuDay = {
date: dateStr,
blockId: blockId,
title: blockTitle
};
const menu = document.getElementById('dayContextMenu');
const label = document.getElementById('dayContextMenuDateLabel');
const statusBadge = document.getElementById('dayContextMenuStatusBadge');
const actions = document.getElementById('dayContextMenuActions');
if (!menu || !label || !actions) return;
// Nederlandse dag en maand formatteren
const dParts = dateStr.split('-');
const dObj = new Date(parseInt(dParts[0], 10), parseInt(dParts[1], 10) - 1, parseInt(dParts[2], 10));
const dayNames = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag'];
const monthNames = ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
const formatted = `${dayNames[dObj.getDay()]} ${dObj.getDate()} ${monthNames[dObj.getMonth()]}`;
label.textContent = formatted;
if (blockId > 0) {
statusBadge.innerHTML = `<span class="px-1.5 py-0.5 rounded text-[9px] font-bold bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-300 border border-amber-300/80 dark:border-amber-700/80">${escapeHtml(blockTitle || 'Geblokkeerd')}</span>`;
actions.innerHTML = `
<button type="button" onclick="quickUnblockDay(${blockId})" class="w-full flex items-center space-x-2 px-3 py-2 rounded-xl text-left text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer font-medium">
<i data-lucide="trash-2" class="w-3.5 h-3.5 flex-shrink-0 text-rose-500"></i>
<span>Blokkade opheffen</span>
</button>
<button type="button" onclick="closeDayContextMenu(); openCalendarBlockModal({id: ${blockId}, start_date: '${dateStr}', end_date: '${dateStr}', title: '${escapeHtml(blockTitle)}'}, '${dateStr}')" class="w-full flex items-center space-x-2 px-3 py-2 rounded-xl text-left hover:bg-slate-100 dark:hover:bg-slate-800/80 text-slate-700 dark:text-slate-300 transition cursor-pointer font-medium">
<i data-lucide="calendar" class="w-3.5 h-3.5 flex-shrink-0 text-slate-400"></i>
<span>Periode aanpassen...</span>
</button>
`;
} else {
statusBadge.innerHTML = `<span class="text-[9px] font-normal text-slate-400">Beschikbaar</span>`;
let reasonsHtml = '';
if (Array.isArray(CAL_BLOCK_REASONS) && CAL_BLOCK_REASONS.length > 0) {
reasonsHtml = CAL_BLOCK_REASONS.map(r => `
<button type="button" onclick="quickBlockDay('${dateStr}', '${escapeHtml(r.title)}', '${escapeHtml(r.code)}')" class="w-full flex items-center space-x-2 px-3 py-2 rounded-xl text-left hover:bg-amber-50 dark:hover:bg-amber-950/50 text-slate-800 dark:text-slate-200 transition cursor-pointer font-medium">
<span class="text-sm leading-none">${escapeHtml(r.icon || '📅')}</span>
<span>Hele dag: ${escapeHtml(r.title)}</span>
</button>
`).join('');
} else {
reasonsHtml = `
<button type="button" onclick="quickBlockDay('${dateStr}', 'Vakantie', 'VACATION')" class="w-full flex items-center space-x-2 px-3 py-2 rounded-xl text-left hover:bg-amber-50 dark:hover:bg-amber-950/50 text-slate-800 dark:text-slate-200 transition cursor-pointer font-medium">
<span class="text-sm leading-none">🏖️</span>
<span>Hele dag: Vakantie</span>
</button>
`;
}
actions.innerHTML = `
${reasonsHtml}
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
<button type="button" onclick="closeDayContextMenu(); openCalendarBlockModal(null, '${dateStr}')" class="w-full flex items-center space-x-2 px-3 py-2 rounded-xl text-left hover:bg-slate-100 dark:hover:bg-slate-800/80 text-slate-600 dark:text-slate-400 transition cursor-pointer font-medium">
<i data-lucide="calendar-plus" class="w-3.5 h-3.5 flex-shrink-0 text-slate-400"></i>
<span>Meerdere dagen blokkeren...</span>
</button>
`;
}
if (window.lucide && lucide.createIcons) {
lucide.createIcons();
}
menu.classList.remove('hidden');
const menuWidth = 220;
const menuHeight = menu.offsetHeight || 180;
let posX = Math.max(12, Math.min(window.innerWidth - menuWidth - 16, e.clientX));
let posY = Math.max(12, Math.min(window.innerHeight - menuHeight - 16, e.clientY));
menu.style.left = `${posX}px`;
menu.style.top = `${posY}px`;
}
function closeDayContextMenu() {
const menu = document.getElementById('dayContextMenu');
if (menu) {
menu.classList.add('hidden');
}
activeContextMenuDay = null;
}
// --- Rechtermuisklik Context Menu voor Taken & Outlook Events (Release v2.2) ---
let activeContextMenuTask = null;
function handleTaskContextMenu(e, taskData) {
e.preventDefault();
e.stopPropagation();
closeDayContextMenu();
activeContextMenuTask = taskData;
const menu = document.getElementById('taskContextMenu');
const titleEl = document.getElementById('taskContextMenuTitle');
const priorityBadge = document.getElementById('taskContextMenuPriorityBadge');
const metaEl = document.getElementById('taskContextMenuMeta');
const btnOpen = document.getElementById('taskCtxBtnOpen');
const btnEdit = document.getElementById('taskCtxBtnEdit');
const btnEditLabel = document.getElementById('taskCtxBtnEditLabel');
const btnToggle = document.getElementById('taskCtxBtnToggle');
const toggleLabel = document.getElementById('taskCtxBtnToggleLabel');
const btnSubtasks = document.getElementById('taskCtxBtnSubtasks');
const btnOutlook = document.getElementById('taskCtxBtnOutlook');
const btnDelete = document.getElementById('taskCtxBtnDelete');
const btnDeleteLabel = document.getElementById('taskCtxBtnDeleteLabel');
const divider = document.getElementById('taskCtxDivider');
if (!menu) return;
const isOutlookEvent = Boolean(taskData && (taskData.is_outlook_event || taskData.source === 'OUTLOOK_LIVE'));
if (titleEl) titleEl.textContent = (isOutlookEvent ? (taskData.subject || taskData.title) : taskData.title) || 'Item';
if (priorityBadge) {
if (isOutlookEvent) {
priorityBadge.textContent = 'AFSPRAAK';
priorityBadge.className = 'text-[9px] font-extrabold px-1.5 py-0.5 rounded-md bg-sky-100 text-sky-700 dark:bg-sky-950 dark:text-sky-400 border border-sky-300 dark:border-sky-800';
} else {
const p = taskData.priority || 'P2';
priorityBadge.textContent = p;
if (p === 'P1') {
priorityBadge.className = 'text-[9px] font-extrabold px-1.5 py-0.5 rounded-md bg-rose-100 text-rose-700 dark:bg-rose-950 dark:text-rose-400 border border-rose-300 dark:border-rose-800';
} else if (p === 'P2') {
priorityBadge.className = 'text-[9px] font-extrabold px-1.5 py-0.5 rounded-md bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-400 border border-amber-300 dark:border-amber-800';
} else {
priorityBadge.className = 'text-[9px] font-extrabold px-1.5 py-0.5 rounded-md bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 border border-slate-300 dark:border-slate-700';
}
}
}
if (metaEl) {
let meta = '';
if (taskData.start) {
meta += `${taskData.start}${taskData.end ? ' - ' + taskData.end : ''}`;
if (taskData.duration) meta += ` (${taskData.duration}m)`;
} else if (taskData.duration) {
meta += `${taskData.duration}m`;
}
if (isOutlookEvent) {
meta += (meta ? ' · ' : '') + 'Outlook';
if (taskData.location && taskData.location !== 'Geen locatie') {
meta += ` · ${taskData.location}`;
}
} else if (taskData.source && taskData.source !== 'FOCIZ') {
meta += (meta ? ' · ' : '') + taskData.source;
}
metaEl.textContent = meta || (taskData.date || 'Vandaag');
}
// Pas labels en zichtbaarheid aan op basis van type
if (isOutlookEvent) {
if (btnEditLabel) btnEditLabel.textContent = 'Afspraak bewerken';
if (btnDeleteLabel) btnDeleteLabel.textContent = 'Afspraak verwijderen';
if (btnToggle) btnToggle.classList.add('hidden');
if (btnSubtasks) btnSubtasks.classList.add('hidden');
if (btnOutlook) btnOutlook.classList.add('hidden');
} else {
if (btnEditLabel) btnEditLabel.textContent = 'Taak bewerken';
if (btnDeleteLabel) btnDeleteLabel.textContent = 'Taak verwijderen';
if (btnToggle) {
btnToggle.classList.remove('hidden');
if (toggleLabel) {
toggleLabel.textContent = taskData.is_completed ? 'Markeren als openstaand' : 'Markeren als voltooid';
}
}
if (btnSubtasks) btnSubtasks.classList.remove('hidden');
if (btnOutlook) {
if (window.isOutlookConnected && !taskData.external_id && !taskData.is_completed) {
btnOutlook.classList.remove('hidden');
} else {
btnOutlook.classList.add('hidden');
}
}
}
// Koppel actieknoppen
if (btnOpen) {
btnOpen.onclick = () => {
closeTaskContextMenu();
openTaskDetailModal(taskData);
};
}
if (btnEdit) {
btnEdit.onclick = () => {
closeTaskContextMenu();
openEditTaskModal(taskData);
};
}
if (btnToggle) {
btnToggle.onclick = () => {
closeTaskContextMenu();
toggleTask(taskData.id, taskData.is_completed ? 0 : 1);
};
}
if (btnSubtasks) {
btnSubtasks.onclick = () => {
closeTaskContextMenu();
if (typeof toggleSubtaskPanel === 'function') {
toggleSubtaskPanel(taskData.id);
} else {
openTaskDetailModal(taskData);
}
};
}
if (btnOutlook) {
btnOutlook.onclick = () => {
closeTaskContextMenu();
if (typeof syncTaskToOutlook === 'function') {
syncTaskToOutlook(taskData.id);
}
};
}
if (btnDelete) {
btnDelete.onclick = () => {
closeTaskContextMenu();
if (isOutlookEvent) {
deleteOutlookEvent(taskData.id, taskData.subject || taskData.title);
} else {
deleteTask(taskData.id);
}
};
}
if (window.lucide && lucide.createIcons) {
lucide.createIcons();
}
menu.classList.remove('hidden');
const menuWidth = 240;
const menuHeight = menu.offsetHeight || 220;
let posX = Math.max(12, Math.min(window.innerWidth - menuWidth - 16, e.clientX));
let posY = Math.max(12, Math.min(window.innerHeight - menuHeight - 16, e.clientY));
menu.style.left = `${posX}px`;
menu.style.top = `${posY}px`;
}
function closeTaskContextMenu() {
const menu = document.getElementById('taskContextMenu');
if (menu) {
menu.classList.add('hidden');
}
activeContextMenuTask = null;
}
// --- Task Detail Modal (Volledige inhoud bekijken) ---
let currentDetailTaskId = null;
let currentDetailTaskData = null;
async function openTaskDetailModal(taskDataOrId) {
let task = null;
let taskId = 0;
if (typeof taskDataOrId === 'object' && taskDataOrId !== null) {
task = taskDataOrId;
taskId = task.id;
} else {
taskId = parseInt(taskDataOrId, 10);
}
currentDetailTaskId = taskId;
currentDetailTaskData = task;
const modal = document.getElementById('taskDetailModal');
if (!modal) return;
// Toon direct modal met basisgegevens
if (task) {
populateTaskDetailModal(task, _subtaskCache[taskId] || []);
}
modal.classList.remove('hidden');
modal.classList.add('flex');
const isOutlookEvent = Boolean(task && (task.is_outlook_event || task.source === 'OUTLOOK_LIVE'));
// Als het een Outlook event is, hoeven we geen Fociz task ID API aan te roepen
if (!isOutlookEvent && taskId) {
try {
const resp = await fetch(`/api/planner.php?action=get_task_details&task_id=${taskId}`);
const data = await resp.json();
if (data.success && data.task) {
currentDetailTaskData = {
id: data.task.id,
title: data.task.title,
description: data.task.description || '',
priority: data.task.priority || 'P2',
start: data.task.time_block_start || '',
end: data.task.time_block_end || '',
duration: parseInt(data.task.duration_minutes, 10) || 30,
is_completed: parseInt(data.task.is_completed, 10) === 1,
source: data.task.source || 'FOCIZ',
external_id: data.task.external_id || '',
external_url: data.task.external_url || '',
date: data.task.plan_date || (task ? task.date : '')
};
_subtaskCache[taskId] = data.subtasks || [];
populateTaskDetailModal(currentDetailTaskData, data.subtasks || []);
}
} catch (err) {
console.error('Fout bij ophalen taakdetails:', err);
}
}
if (window.lucide && lucide.createIcons) {
lucide.createIcons();
}
}
function populateTaskDetailModal(task, subtasks) {
const titleEl = document.getElementById('taskDetailTitle');
const priorityBadge = document.getElementById('taskDetailPriorityBadge');
const priorityIcon = document.getElementById('taskDetailPriorityIcon');
const sourceBadge = document.getElementById('taskDetailSourceBadge');
const statusBadge = document.getElementById('taskDetailStatusBadge');
const dateEl = document.getElementById('taskDetailDate');
const timeSlotEl = document.getElementById('taskDetailTimeSlot');
const durationEl = document.getElementById('taskDetailDuration');
const descEl = document.getElementById('taskDetailDescription');
const subtasksSection = document.getElementById('taskDetailSubtasksSection');
const subtasksList = document.getElementById('taskDetailSubtasksList');
const extBlock = document.getElementById('taskDetailExternalBlock');
const extLabel = document.getElementById('taskDetailExternalLabel');
const extLink = document.getElementById('taskDetailExternalLink');
const btnEdit = document.getElementById('taskDetailBtnEdit');
const btnEditLabel = document.getElementById('taskDetailBtnEditLabel');
const btnDelete = document.getElementById('taskDetailBtnDelete');
const btnDeleteLabel = document.getElementById('taskDetailBtnDeleteLabel');
const isOutlookEvent = Boolean(task && (task.is_outlook_event || task.source === 'OUTLOOK_LIVE'));
if (titleEl) titleEl.textContent = (isOutlookEvent ? (task.subject || task.title) : task.title) || 'Geen titel';
// Prioriteit & icoon
if (isOutlookEvent) {
if (priorityBadge) {
priorityBadge.textContent = 'Outlook Afspraak';
priorityBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wide bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30';
}
if (priorityIcon) {
priorityIcon.className = 'w-10 h-10 rounded-2xl 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 flex-shrink-0 mt-0.5';
priorityIcon.innerHTML = '<i data-lucide="calendar" class="w-5 h-5"></i>';
}
} else {
const p = task.priority || 'P2';
if (priorityBadge) {
priorityBadge.textContent = p === 'P1' ? 'P1 · Hoge Prioriteit' : (p === 'P2' ? 'P2 · Belangrijk' : 'P3 · Regulier');
if (p === 'P1') {
priorityBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wide bg-rose-50 dark:bg-rose-500/15 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/30';
if (priorityIcon) priorityIcon.className = 'w-10 h-10 rounded-2xl bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/20 text-rose-600 dark:text-rose-400 flex items-center justify-center flex-shrink-0 mt-0.5';
} else if (p === 'P2') {
priorityBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wide bg-amber-50 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/30';
if (priorityIcon) priorityIcon.className = 'w-10 h-10 rounded-2xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center flex-shrink-0 mt-0.5';
} else {
priorityBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wide bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700';
if (priorityIcon) priorityIcon.className = 'w-10 h-10 rounded-2xl bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 flex items-center justify-center flex-shrink-0 mt-0.5';
}
}
if (priorityIcon) {
priorityIcon.innerHTML = '<i data-lucide="check-square" class="w-5 h-5"></i>';
}
}
// Source
if (sourceBadge) {
if (isOutlookEvent) {
sourceBadge.textContent = 'OUTLOOK 365';
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30';
} else {
const src = (task.source || 'FOCIZ').toUpperCase();
sourceBadge.textContent = src;
if (src === 'JIRA') {
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-indigo-50 dark:bg-indigo-500/15 text-indigo-700 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-500/30';
} else if (src === 'OUTLOOK' || src === 'MICROSOFT') {
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30';
} else if (src === 'OUTLOOK_MAIL' || src === 'MAIL') {
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-violet-50 dark:bg-violet-500/15 text-violet-700 dark:text-violet-400 border border-violet-200 dark:border-violet-500/30';
} else if (src === 'BRAINSTORM') {
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono 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';
} else {
sourceBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700';
}
}
}
// Status
if (statusBadge) {
if (isOutlookEvent) {
statusBadge.textContent = 'Agenda Afspraak';
statusBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-bold bg-indigo-50 dark:bg-indigo-500/15 text-indigo-700 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-500/30';
} else {
if (task.is_completed) {
statusBadge.textContent = 'Voltooid';
statusBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-50 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/30';
} else {
statusBadge.textContent = 'Openstaand';
statusBadge.className = 'px-2 py-0.5 rounded-full text-[10px] font-bold bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30';
}
}
}
// Datum & Tijd
if (dateEl) {
dateEl.textContent = task.date || (typeof activePlannerDate !== 'undefined' ? activePlannerDate : 'Vandaag');
}
if (timeSlotEl) {
timeSlotEl.textContent = (task.start) ? `${task.start} - ${task.end || '?'}` : 'Geen tijdblok';
}
if (durationEl) {
let dur = task.duration;
if (!dur && task.start && task.end) {
const sm = parseTimeToMinutes(task.start);
const em = parseTimeToMinutes(task.end);
if (sm !== null && em !== null && em > sm) dur = em - sm;
}
durationEl.textContent = `${dur || 30} minuten`;
}
// Beschrijving / Locatie
if (descEl) {
if (isOutlookEvent) {
let descParts = [];
if (task.location && task.location !== 'Geen locatie') {
descParts.push(`Locatie: ${task.location}`);
}
if (task.description && task.description.trim()) {
descParts.push(task.description.trim());
}
if (descParts.length > 0) {
descEl.textContent = descParts.join('\n\n');
descEl.classList.remove('italic', 'text-slate-400');
} else {
descEl.textContent = 'Geen extra notities of locatie voor deze Outlook-afspraak.';
descEl.classList.add('italic', 'text-slate-400');
}
} else {
if (task.description && task.description.trim()) {
descEl.textContent = task.description;
descEl.classList.remove('italic', 'text-slate-400');
} else {
descEl.textContent = 'Geen beschrijving of notities opgegeven voor deze taak.';
descEl.classList.add('italic', 'text-slate-400');
}
}
}
// Subtaken: verbergen voor Outlook afspraken
if (subtasksSection) {
if (isOutlookEvent) {
subtasksSection.classList.add('hidden');
} else {
subtasksSection.classList.remove('hidden');
if (subtasksList) {
renderSubtasksInDetailModal(task.id, subtasks || []);
}
}
}
// Externe link
if (extBlock && extLink && extLabel) {
const extUrl = isOutlookEvent ? (task.meeting_url || task.web_link || task.external_url) : task.external_url;
if (extUrl) {
extBlock.classList.remove('hidden');
extLink.href = extUrl;
if (isOutlookEvent) {
if (task.meeting_url && task.meeting_url.includes('teams.microsoft.com')) {
extLabel.textContent = 'Deelnemen via Microsoft Teams';
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'Deelnemen');
} else {
extLabel.textContent = 'Gekoppeld in Microsoft Outlook Agenda';
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'In Outlook');
}
} else if (task.source === 'JIRA') {
extLabel.textContent = `Gekoppeld Jira Ticket: ${task.external_id || 'Jira'}`;
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'Openen');
} else if (task.source === 'OUTLOOK' || task.source === 'MICROSOFT') {
extLabel.textContent = 'Gekoppeld in Microsoft Outlook Agenda';
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'Openen');
} else if (task.source === 'OUTLOOK_MAIL') {
extLabel.textContent = 'Afkomstig uit Outlook E-mail';
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'Openen');
} else {
extLabel.textContent = 'Externe koppeling';
extLink.querySelector('span') && (extLink.querySelector('span').textContent = 'Openen');
}
} else {
extBlock.classList.add('hidden');
}
}
// Buttons
if (btnEditLabel) {
btnEditLabel.textContent = isOutlookEvent ? 'Afspraak Bewerken' : 'Taak Bewerken';
}
if (btnDeleteLabel) {
btnDeleteLabel.textContent = isOutlookEvent ? 'Afspraak Verwijderen' : 'Verwijderen';
}
if (btnEdit) {
btnEdit.onclick = () => {
closeTaskDetailModal();
openEditTaskModal(task);
};
}
if (btnDelete) {
btnDelete.onclick = () => {
closeTaskDetailModal();
if (isOutlookEvent) {
deleteOutlookEvent(task.id, task.subject || task.title);
} else {
deleteTask(task.id);
}
};
}
}
function renderSubtasksInDetailModal(taskId, subtasks) {
const list = document.getElementById('taskDetailSubtasksList');
const countEl = document.getElementById('taskDetailSubtasksCount');
if (!list) return;
if (!subtasks || subtasks.length === 0) {
list.innerHTML = '<p class="text-[11px] text-slate-400 italic py-1">Nog geen subtaken of checklist items.</p>';
if (countEl) countEl.textContent = '0 items';
return;
}
const completedCount = subtasks.filter(s => parseInt(s.is_completed, 10) === 1).length;
if (countEl) countEl.textContent = `${completedCount} van ${subtasks.length} gereed`;
list.innerHTML = subtasks.map(s => {
const isDone = parseInt(s.is_completed, 10) === 1;
return `
<div class="flex items-center justify-between p-2 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200/80 dark:border-slate-800/80 text-xs">
<label class="flex items-center space-x-2.5 flex-1 min-w-0 cursor-pointer">
<input type="checkbox" ${isDone ? 'checked' : ''} onchange="toggleSubtaskFromDetailModal(${taskId}, ${s.id}, this.checked)" class="rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 w-4 h-4 cursor-pointer">
<span class="${isDone ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-800 dark:text-slate-200'} truncate">${escapeHtml(s.title)}</span>
</label>
<button type="button" onclick="deleteSubtaskFromDetailModal(${taskId}, ${s.id})" class="p-1 text-slate-400 hover:text-rose-500 rounded transition cursor-pointer">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
`;
}).join('');
if (window.lucide && lucide.createIcons) {
lucide.createIcons();
}
}
async function toggleSubtaskFromDetailModal(taskId, subtaskId, isChecked) {
try {
await fetch('/api/planner.php?action=toggle_subtask', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ subtask_id: subtaskId, is_completed: isChecked ? 1 : 0 })
});
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 || [];
renderSubtasksInDetailModal(taskId, data.subtasks || []);
if (typeof renderSubtasks === 'function') renderSubtasks(taskId);
}
} catch (e) {
console.error('Fout bij afvinken subtaak:', e);
}
}
async function deleteSubtaskFromDetailModal(taskId, subtaskId) {
try {
await fetch('/api/planner.php?action=delete_subtask', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ subtask_id: subtaskId })
});
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 || [];
renderSubtasksInDetailModal(taskId, data.subtasks || []);
if (typeof renderSubtasks === 'function') renderSubtasks(taskId);
}
} catch (e) {
console.error('Fout bij verwijderen subtaak:', e);
}
}
async function addSubtaskFromDetailModal() {
const input = document.getElementById('taskDetailNewSubtaskInput');
if (!input || !currentDetailTaskId) return;
const title = input.value.trim();
if (!title) return;
try {
const resp = await fetch('/api/planner.php?action=add_subtask', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ task_id: currentDetailTaskId, title: title })
});
const data = await resp.json();
if (data.success) {
input.value = '';
const subResp = await fetch(`/api/planner.php?action=get_subtasks&task_id=${currentDetailTaskId}`);
const subData = await subResp.json();
if (subData.success) {
_subtaskCache[currentDetailTaskId] = subData.subtasks || [];
renderSubtasksInDetailModal(currentDetailTaskId, subData.subtasks || []);
if (typeof renderSubtasks === 'function') renderSubtasks(currentDetailTaskId);
}
}
} catch (e) {
console.error('Fout bij toevoegen subtaak:', e);
}
}
function closeTaskDetailModal() {
const modal = document.getElementById('taskDetailModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
currentDetailTaskId = null;
currentDetailTaskData = null;
}
document.addEventListener('click', function(e) {
const dayMenu = document.getElementById('dayContextMenu');
if (dayMenu && !dayMenu.contains(e.target)) {
closeDayContextMenu();
}
const taskMenu = document.getElementById('taskContextMenu');
if (taskMenu && !taskMenu.contains(e.target)) {
closeTaskContextMenu();
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeDayContextMenu();
closeTaskContextMenu();
closeTaskDetailModal();
}
});
async function quickBlockDay(dateStr, title, blockType) {
closeDayContextMenu();
try {
const res = await fetch('/api/planner.php?action=save_calendar_block', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: title,
block_type: blockType,
start_date: dateStr,
end_date: dateStr,
sync_to_outlook: true
})
});
const data = await res.json();
if (data && data.success) {
showWidgetToast(`${title} ingepland voor ${dateStr}`, 'success');
setTimeout(() => {
window.location.reload();
}, 300);
} else {
alert(data.error || 'Fout bij inplannen van blokkade');
}
} catch (err) {
alert('Netwerkfout bij inplannen van blokkade');
}
}
async function quickUnblockDay(blockId) {
closeDayContextMenu();
if (!blockId) return;
try {
const res = await fetch('/api/planner.php?action=delete_calendar_block', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: blockId })
});
const data = await res.json();
if (data && data.success) {
showWidgetToast('Blokkade opgeheven', 'success');
setTimeout(() => {
window.location.reload();
}, 300);
} else {
alert(data.error || 'Fout bij opheffen van blokkade');
}
} catch (err) {
alert('Netwerkfout bij opheffen van blokkade');
}
}
function openCalendarBlockModal(existingBlock = null, defaultDate = null) {
const modal = document.getElementById('calendarBlockModal');
if (!modal) return;
const idInput = document.getElementById('calBlockId');
const titleInput = document.getElementById('calBlockTitle');
const startInput = document.getElementById('calBlockStartDate');
const endInput = document.getElementById('calBlockEndDate');
const notesInput = document.getElementById('calBlockNotes');
const deleteBtn = document.getElementById('btnDeleteCurrentCalBlock');
const modalTitle = document.getElementById('calendarBlockModalTitle');
const selDate = defaultDate || '<?= $selectedDate ?>';
if (existingBlock && existingBlock.id) {
if (idInput) idInput.value = existingBlock.id;
if (titleInput) titleInput.value = existingBlock.title || '';
if (startInput) startInput.value = existingBlock.start_date || selDate;
if (endInput) endInput.value = existingBlock.end_date || existingBlock.start_date || selDate;
if (notesInput) notesInput.value = existingBlock.notes || '';
selectCalBlockType(existingBlock.block_type || 'VACATION');
if (deleteBtn) {
deleteBtn.classList.remove('hidden');
deleteBtn.classList.add('flex');
}
if (modalTitle) modalTitle.textContent = 'Blokkade Aanpassen';
} else {
if (idInput) idInput.value = 0;
selectCalBlockType('VACATION');
if (titleInput) titleInput.value = 'Vakantie';
if (startInput) startInput.value = selDate;
if (endInput) endInput.value = selDate;
if (notesInput) notesInput.value = '';
if (deleteBtn) {
deleteBtn.classList.add('hidden');
deleteBtn.classList.remove('flex');
}
if (modalTitle) modalTitle.textContent = 'Meerdere Dagen Blokkeren';
}
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
}
function closeCalendarBlockModal() {
const modal = document.getElementById('calendarBlockModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
}
async function submitCalendarBlock(event) {
event.preventDefault();
const btn = document.getElementById('btnSaveCalBlock');
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.innerText = 'Opslaan...';
btn.disabled = true;
}
const id = parseInt(document.getElementById('calBlockId')?.value, 10) || 0;
const blockType = document.getElementById('calBlockType')?.value || 'VACATION';
const title = document.getElementById('calBlockTitle')?.value.trim();
const startDate = document.getElementById('calBlockStartDate')?.value;
const endDate = document.getElementById('calBlockEndDate')?.value;
const notes = document.getElementById('calBlockNotes')?.value.trim();
const syncOutlook = document.getElementById('calBlockSyncOutlook')?.checked || false;
if (!title) {
alert('Vul een titel in voor de blokkade.');
if (btn) { btn.innerHTML = origHtml; btn.disabled = false; }
return;
}
try {
const res = await fetch('/api/planner.php?action=save_calendar_block', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: id,
title: title,
block_type: blockType,
start_date: startDate,
end_date: endDate,
notes: notes,
sync_to_outlook: syncOutlook
})
});
const data = await res.json();
if (data.success) {
showWidgetToast(id > 0 ? 'Blokkade succesvol bijgewerkt' : 'Dagen succesvol geblokkeerd', 'success');
setTimeout(() => {
window.location.reload();
}, 600);
} else {
alert(data.error || 'Er is een fout opgetreden bij het opslaan.');
if (btn) { btn.innerHTML = origHtml; btn.disabled = false; }
}
} catch (err) {
alert('Netwerkfout bij opslaan van blokkade.');
if (btn) { btn.innerHTML = origHtml; btn.disabled = false; }
}
}
async function deleteCalendarBlock(blockId) {
if (!blockId) return;
if (!confirm('Weet je zeker dat je deze agenda-blokkade wilt opheffen?')) return;
try {
const res = await fetch('/api/planner.php?action=delete_calendar_block', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: blockId })
});
const data = await res.json();
if (data.success) {
showWidgetToast('Blokkade opgeheven', 'success');
setTimeout(() => {
window.location.reload();
}, 600);
} else {
alert(data.error || 'Kon blokkade niet opheffen.');
}
} catch (err) {
alert('Netwerkfout bij opheffen van blokkade.');
}
}
function deleteCurrentCalendarBlock() {
const id = parseInt(document.getElementById('calBlockId')?.value, 10) || 0;
if (id > 0) {
deleteCalendarBlock(id);
}
}
// --- Bepaal de actuele widget layout en exacte volgorde uit het scherm (DOM) ---
function getActiveWidgetLayoutPayload() {
const grid = document.getElementById('focizWidgetGrid');
const seenIds = new Set();
const payload = [];
let currentOrder = 1;
// 1. Zichtbare widgets in het raster in hun exacte visuele volgorde op het scherm
if (grid) {
const wrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
wrappers.forEach(wrap => {
const wId = wrap.getAttribute('data-widget-id');
if (wId && !seenIds.has(wId)) {
seenIds.add(wId);
const item = focizWidgets.find(x => x.id === wId);
const colSpan = parseInt(wrap.getAttribute('data-col-span'), 10) || (item ? parseInt(item.col_span, 10) : 6);
const widgetObj = {
id: wId,
enabled: true,
col_span: colSpan,
order: currentOrder++
};
if (item && item.custom_title) {
widgetObj.custom_title = item.custom_title;
}
payload.push(widgetObj);
if (item) {
item.order = widgetObj.order;
item.col_span = colSpan;
item.enabled = true;
}
}
});
}
// 2. Uitgeschakelde of verborgen widgets achteraan toevoegen
focizWidgets.forEach(w => {
if (!seenIds.has(w.id)) {
seenIds.add(w.id);
const hiddenObj = {
id: w.id,
enabled: !!w.enabled,
col_span: parseInt(w.col_span, 10) || 6,
order: currentOrder++
};
if (w.custom_title) {
hiddenObj.custom_title = w.custom_title;
}
payload.push(hiddenObj);
}
});
// 3. Sorteer focizWidgets array in het geheugen synchroon met de nieuwe volgorde
focizWidgets.sort((a, b) => (a.order || 0) - (b.order || 0));
return payload;
}
// --- Achtergrond Auto-Save voor Widget Layout met keepalive ---
async function persistWidgetLayoutAuto(customPayload = null) {
try {
const payload = customPayload || getActiveWidgetLayoutPayload();
try {
localStorage.setItem('fociz_widget_layout_cache', JSON.stringify(payload));
} catch(e) {}
const res = await fetch('/api/planner.php?action=save_widget_layout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ layout: payload }),
keepalive: true
});
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 Naam / Titel Aanpassen ---
function saveWidgetTitle(wId) {
const input = document.getElementById(`widgetTitleInput_${wId}`);
if (!input) return;
const defaultTitle = input.getAttribute('data-default-title') || '';
let newTitle = input.value.trim();
if (!newTitle) {
newTitle = defaultTitle;
input.value = defaultTitle;
}
// Directe DOM-update op de titelbalk van de widget
const titleEl = document.getElementById(`widgetTitleText_${wId}`);
if (titleEl) {
titleEl.textContent = newTitle;
}
// Werk de in-memory widget catalogus bij
const item = focizWidgets.find(x => x.id === wId);
if (item) {
if (newTitle !== defaultTitle) {
item.custom_title = newTitle;
item.title = newTitle;
} else {
delete item.custom_title;
item.title = defaultTitle;
}
}
// Sluit het instellingen menu
document.getElementById(`widgetSettingsMenu_${wId}`)?.classList.add('hidden');
// Automatisch opslaan naar database
persistWidgetLayoutAuto();
showWidgetToast(`Widgetnaam aangepast naar "${escapeHtml(newTitle)}"`, 'success');
}
function resetWidgetTitle(wId, defaultTitle) {
const input = document.getElementById(`widgetTitleInput_${wId}`);
if (input) {
input.value = defaultTitle;
}
const titleEl = document.getElementById(`widgetTitleText_${wId}`);
if (titleEl) {
titleEl.textContent = defaultTitle;
}
const item = focizWidgets.find(x => x.id === wId);
if (item) {
delete item.custom_title;
item.title = defaultTitle;
}
document.getElementById(`widgetSettingsMenu_${wId}`)?.classList.add('hidden');
persistWidgetLayoutAuto();
showWidgetToast('Standaard widgetnaam hersteld', 'info');
}
// --- 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) {}
}
// --- Dagenselectie (Weekstrook) Inklappen / Uitklappen ---
function toggleDaySelectionCollapse() {
const strip = document.getElementById('weekDaysStripContainer');
const icon = document.getElementById('iconToggleDaySelection');
const btn = document.getElementById('btnToggleDaySelection');
if (!strip) return;
const isCurrentlyHidden = strip.classList.contains('hidden');
if (isCurrentlyHidden) {
strip.classList.remove('hidden');
if (icon) icon.classList.remove('rotate-180');
if (btn) btn.title = 'Dagen inklappen';
try { localStorage.setItem('fociz_days_selection_collapsed', '0'); } catch(e) {}
} else {
strip.classList.add('hidden');
if (icon) icon.classList.add('rotate-180');
if (btn) btn.title = 'Dagen tonen';
try { localStorage.setItem('fociz_days_selection_collapsed', '1'); } catch(e) {}
}
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
}
function initPlannerDaySelectionCollapse() {
try {
if (localStorage.getItem('fociz_days_selection_collapsed') === '1') {
const strip = document.getElementById('weekDaysStripContainer');
const icon = document.getElementById('iconToggleDaySelection');
const btn = document.getElementById('btnToggleDaySelection');
if (strip) strip.classList.add('hidden');
if (icon) icon.classList.add('rotate-180');
if (btn) btn.title = 'Dagen tonen';
}
} 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 dagenselectie inklapstatus
initPlannerDaySelectionCollapse();
// 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 payload = getActiveWidgetLayoutPayload();
persistWidgetLayoutAuto(payload);
showWidgetToast('Widget 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 | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
const plannerJiraAll = <?= json_encode($jiraAllIssues, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
const plannerTicketStore = new Map();
(Array.isArray(plannerJiraAll) ? plannerJiraAll : []).forEach(iss => {
if (iss && iss.key) plannerTicketStore.set(String(iss.key).trim().toUpperCase(), iss);
});
(Array.isArray(plannerJiraAssigned) ? plannerJiraAssigned : []).forEach(iss => {
if (iss && iss.key) {
const normKey = String(iss.key).trim().toUpperCase();
if (!plannerTicketStore.has(normKey)) {
plannerTicketStore.set(normKey, iss);
}
}
});
const plannedExternalTasksMap = <?= json_encode($plannedExternalIds, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?> || {};
const plannedExternalIdsSet = new Set(Object.keys(plannedExternalTasksMap));
let activePlannerJiraQueue = ''; // '': Alle, 'my': Mijn, 'DX': DX Queue, 'SD': SD Queue
function isIssueInQueue(iss, qCode) {
if (!qCode) return true;
const code = qCode.toUpperCase().trim();
let pKey = (iss.project_key || '').toUpperCase().trim();
if (!pKey && iss.key) {
const parts = iss.key.split('-');
if (parts.length > 0) pKey = parts[0].toUpperCase().trim();
}
const pName = (iss.project || '').toLowerCase().trim();
const isSd = pKey ? (pKey === 'SD') : pName.includes('servicedesk');
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
const sum = (iss.summary || '').toLowerCase();
const dxTerms = ['koppeling', 'sso', 'endpoint', 'rest', 'datadump', 'uwv', 'migratie'];
const matchesDx = isSd && dxTerms.some(t => ol.includes(t) || sum.includes(t));
if (code === 'DX') return matchesDx;
if (code === 'SD') return isSd && !matchesDx;
if (code === 'IN') return pKey ? (pKey === 'IN') : pName.includes('interface');
if (code === 'AAN') return pKey ? (pKey === 'AAN') : pName.includes('aanvraag');
if (code === 'GEN') return pKey ? (pKey === 'GEN') : (pName.includes('general') || pName.includes('algemeen'));
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>';
const isPlanned = plannedExternalIdsSet.has(issue.key);
const planBtn = isPlanned
? `<button
onclick="quickImportJiraByKey(this, '${keyEsc}', event)"
data-jira-key="${keyEsc}"
data-jira-summary="${summaryEsc}"
data-jira-status="${statusEsc}"
data-jira-priority="${priorityEsc}"
class="plan-btn-jira flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
title="Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)"
>
<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i>
<span>Ingepland</span>
</button>`
: `<button
onclick="quickImportJiraByKey(this, '${keyEsc}', event)"
data-jira-key="${keyEsc}"
data-jira-summary="${summaryEsc}"
data-jira-status="${statusEsc}"
data-jira-priority="${priorityEsc}"
class="plan-btn-jira 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="calendar-plus" class="w-3 h-3"></i>
<span>Plan</span>
</button>`;
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 cursor-pointer" onclick="openJiraTicketDetail('${keyEsc}')" title="Klik voor ticketdetails">
<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>
${planBtn}
</div>
`;
}).join('');
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
}
// --- Jira Ticket Detail Modal ---
let _jiraDetailCurrentIssue = null;
function openJiraTicketDetail(issueKey) {
const rawKey = String(issueKey || '').trim();
const normKey = rawKey.toUpperCase();
let issue = plannerTicketStore.get(normKey) || plannerTicketStore.get(rawKey);
if (!issue) {
// Fallback: probeer element op te halen uit de lijst
const cardBtn = document.querySelector(`[data-jira-key="${rawKey}"]`);
const summary = cardBtn?.getAttribute('data-jira-summary') || '';
const status = cardBtn?.getAttribute('data-jira-status') || 'To Do';
const priority = cardBtn?.getAttribute('data-jira-priority') || 'Medium';
issue = {
key: rawKey,
summary: summary || `Jira item ${rawKey}`,
status: status,
priority: priority,
description: '',
url: ''
};
plannerTicketStore.set(normKey, issue);
}
_jiraDetailCurrentIssue = issue;
// Vul header
document.getElementById('jiraDetailKey').textContent = issue.key || '—';
document.getElementById('jiraDetailSummary').textContent = issue.summary || '—';
// Status badge
const statusEl = document.getElementById('jiraDetailStatusBadge');
const status = issue.status || '—';
statusEl.textContent = status;
statusEl.className = 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400';
// Priority badge
const priorityEl = document.getElementById('jiraDetailPriorityBadge');
const prio = issue.priority || '—';
priorityEl.textContent = prio;
const prioClass = prio === 'Highest' || prio === 'High'
? 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400'
: prio === 'Medium'
? 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400'
: 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400';
priorityEl.className = prioClass;
// Meta velden
document.getElementById('jiraDetailAssignee').textContent = issue.assignee_name || issue.assignee || 'Niet toegewezen';
document.getElementById('jiraDetailSp').textContent = issue.story_points ? `${issue.story_points} SP` : '—';
document.getElementById('jiraDetailProject').textContent = issue.project_key || issue.project || (issue.key ? issue.key.split('-')[0] : '—');
document.getElementById('jiraDetailType').textContent = issue.issue_type || issue.issuetype || 'Story';
// Beschrijving
const descSection = document.getElementById('jiraDetailDescSection');
const descEl = document.getElementById('jiraDetailDesc');
if (issue.description) {
descEl.textContent = issue.description;
descSection.classList.remove('hidden');
} else {
descSection.classList.add('hidden');
}
// Labels
const labelsSection = document.getElementById('jiraDetailLabelsSection');
const labelsEl = document.getElementById('jiraDetailLabels');
if (issue.labels && issue.labels.length > 0) {
labelsEl.innerHTML = issue.labels.map(l =>
`<span class="px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-[10px] font-semibold border border-slate-200 dark:border-slate-700">${escapeHtml(l)}</span>`
).join('');
labelsSection.classList.remove('hidden');
} else {
labelsSection.classList.add('hidden');
}
// Link naar Jira (veilig protocol controleren)
const linkEl = document.getElementById('jiraDetailOpenLink');
if (issue.url && /^https?:\/\//i.test(String(issue.url).trim())) {
linkEl.href = issue.url;
linkEl.classList.remove('hidden');
} else {
linkEl.classList.add('hidden');
}
// Knop inplannen / ingepland status
const detailPlanBtn = document.getElementById('btnJiraDetailPlan');
if (detailPlanBtn) {
if (plannedExternalIdsSet.has(issue.key)) {
detailPlanBtn.className = "flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs transition cursor-pointer shadow-xs";
detailPlanBtn.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5 stroke-[2.5]"></i><span>Ingepland</span>';
} else {
detailPlanBtn.className = "flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition cursor-pointer";
detailPlanBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i><span>Inplannen</span>';
}
}
const modal = document.getElementById('jiraTicketDetailModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closeJiraTicketDetailModal() {
const modal = document.getElementById('jiraTicketDetailModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
function openJiraPlanFromDetail() {
closeJiraTicketDetailModal();
if (_jiraDetailCurrentIssue) {
openJiraPlanModal(_jiraDetailCurrentIssue.key);
}
}
// --- Jira Plan Modal (met datum, tijdsuggestie & aanpasbare titel) ---
let _jiraPlanCurrentIssue = null;
function clearJiraPlanStartTime() {
const input = document.getElementById('jiraPlanStartTime');
if (input) {
input.value = '';
input.focus();
}
const notice = document.getElementById('jiraPlanOpenBlockNotice');
if (notice) notice.classList.add('hidden');
if (typeof updateJiraPlanEndTimeDisplay === 'function') {
updateJiraPlanEndTimeDisplay();
}
}
function toggleJiraPlanDescription() {
const wrapper = document.getElementById('jiraPlanDescWrapper');
const chevron = document.getElementById('jiraPlanDescChevron');
const label = document.getElementById('jiraPlanDescToggleLabel');
if (!wrapper) return;
const isHidden = wrapper.classList.contains('hidden');
if (isHidden) {
wrapper.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
if (label) label.textContent = 'Inhoud inklappen';
} else {
wrapper.classList.add('hidden');
if (chevron) chevron.classList.remove('rotate-180');
if (label) label.textContent = 'Inhoud van het item bekijken';
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
const availableUserBoards = <?= json_encode($userBoards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?> || [];
function toggleJiraBoardOptions(show) {
const wrapper = document.getElementById('jiraBoardOptionsWrapper');
if (wrapper) {
if (show) {
wrapper.classList.remove('hidden');
const boardSelect = document.getElementById('jiraPlanBoardSelect');
if (boardSelect && boardSelect.value) {
onJiraBoardSelectChange(boardSelect.value);
}
// Sync start en einddatum met de gekozen plandatum
const planDate = document.getElementById('jiraPlanDate')?.value || '';
const startInput = document.getElementById('jiraTimelineStartDate');
const endInput = document.getElementById('jiraTimelineEndDate');
if (startInput && !startInput.value) startInput.value = planDate;
if (endInput && !endInput.value) endInput.value = planDate;
} else {
wrapper.classList.add('hidden');
}
}
}
function onJiraBoardSelectChange(boardId) {
const laneSelect = document.getElementById('jiraPlanLaneSelect');
if (!laneSelect) return;
laneSelect.innerHTML = '';
const board = availableUserBoards.find(b => String(b.id) === String(boardId));
if (board && board.lanes && board.lanes.length > 0) {
board.lanes.forEach((lane, idx) => {
const opt = document.createElement('option');
opt.value = lane.id;
opt.textContent = lane.title;
if (idx === 0) opt.selected = true;
laneSelect.appendChild(opt);
});
} else {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Geen kolommen beschikbaar';
laneSelect.appendChild(opt);
}
}
function toggleJiraTimelineDates(show) {
const wrapper = document.getElementById('jiraTimelineDatesWrapper');
if (wrapper) {
if (show) wrapper.classList.remove('hidden');
else wrapper.classList.add('hidden');
}
}
function openJiraPlanModal(issueKey, fallbackData = null) {
const rawKey = String(issueKey || '').trim();
const normKey = rawKey.toUpperCase();
let issue = plannerTicketStore.get(normKey) || plannerTicketStore.get(rawKey);
if (!issue) {
// Zoek fallback gegevens op de plan-knop in de DOM
const btn = document.querySelector(`[data-jira-key="${rawKey}"]`) || document.querySelector(`[data-jira-key="${normKey}"]`);
const summary = fallbackData?.summary || btn?.getAttribute('data-jira-summary') || '';
const status = fallbackData?.status || btn?.getAttribute('data-jira-status') || 'To Do';
const priority = fallbackData?.priority || btn?.getAttribute('data-jira-priority') || 'Medium';
issue = {
key: rawKey || 'JIRA-ITEM',
summary: summary || (rawKey ? `Oppakken Jira item ${rawKey}` : 'Jira taak'),
status: status,
priority: priority,
description: fallbackData?.description || '',
url: fallbackData?.url || ''
};
if (rawKey) {
plannerTicketStore.set(normKey, issue);
}
}
_jiraPlanCurrentIssue = issue;
// Vul header
document.getElementById('jiraPlanModalKey').textContent = issue.key || '—';
document.getElementById('jiraPlanModalSummary').textContent = issue.summary || '—';
document.getElementById('jiraPlanModalStatus').textContent = issue.status || '—';
document.getElementById('jiraPlanModalPriority').textContent = issue.priority || '—';
// SP badge
const spBadge = document.getElementById('jiraPlanModalSpBadge');
if (issue.story_points) {
spBadge.textContent = `${issue.story_points} SP`;
spBadge.classList.remove('hidden');
} else {
spBadge.classList.add('hidden');
}
// Vul beschrijving (10 regels scrollbaar venstertje) en reset naar ingeklapt
const descEl = document.getElementById('jiraPlanModalDescription');
if (descEl) {
const descText = (issue.description && issue.description.trim()) ? issue.description.trim() : 'Geen beschrijving beschikbaar voor dit ticket.';
descEl.textContent = descText;
}
const descWrapper = document.getElementById('jiraPlanDescWrapper');
const descChevron = document.getElementById('jiraPlanDescChevron');
const descLabel = document.getElementById('jiraPlanDescToggleLabel');
if (descWrapper) descWrapper.classList.add('hidden');
if (descChevron) descChevron.classList.remove('rotate-180');
if (descLabel) descLabel.textContent = 'Inhoud van het item bekijken';
// Target datum: actieve planner datum of vandaag
const targetDate = (typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10);
// Controleer of dit ticket al is ingepland
const existingTask = (typeof plannedExternalTasksMap !== 'undefined' && plannedExternalTasksMap[issue.key]) ? plannedExternalTasksMap[issue.key] : null;
const isAlreadyPlanned = Boolean(existingTask);
const taskIdInput = document.getElementById('jiraPlanTaskId');
if (taskIdInput) {
taskIdInput.value = (existingTask && existingTask.task_id) ? existingTask.task_id : '';
}
const modalTitle = document.getElementById('jiraPlanModalTitle');
const scheduledBanner = document.getElementById('jiraPlanAlreadyScheduledBanner');
const scheduledDetails = document.getElementById('jiraPlanAlreadyScheduledDetails');
const titleInput = document.getElementById('jiraPlanCustomTitle');
const dateInput = document.getElementById('jiraPlanDate');
const startTimeInput = document.getElementById('jiraPlanStartTime');
if (isAlreadyPlanned && existingTask) {
if (modalTitle) modalTitle.textContent = 'Jira Ticket Herplannen';
if (scheduledBanner) scheduledBanner.classList.remove('hidden');
if (scheduledDetails) {
const timeInfo = existingTask.time_block_start
? `om ${escapeHtml(existingTask.time_block_start)}${existingTask.time_block_end ? ' - ' + escapeHtml(existingTask.time_block_end) : ''} (${existingTask.duration_minutes}m)`
: `(zonder specifiek tijdblok, ${existingTask.duration_minutes}m)`;
scheduledDetails.innerHTML = `Momenteel ingepland op <strong>${escapeHtml(existingTask.date)}</strong> ${timeInfo}. Pas hieronder de datum, tijd of duur aan om te herplannen.`;
}
if (titleInput) {
titleInput.value = existingTask.title || `Oppakken Jira item ${issue.key}`;
}
if (dateInput) {
dateInput.value = existingTask.date || targetDate;
}
setJiraPlanDuration(existingTask.duration_minutes || 30);
if (existingTask.priority) {
document.getElementById('jiraPlanPriority').value = existingTask.priority;
}
if (startTimeInput) {
startTimeInput.value = existingTask.time_block_start || '';
}
} else {
if (modalTitle) modalTitle.textContent = 'Jira Ticket Inplannen';
if (scheduledBanner) scheduledBanner.classList.add('hidden');
if (titleInput) {
titleInput.value = `Oppakken Jira item ${issue.key}`;
}
if (dateInput) {
dateInput.value = targetDate;
}
// Standaard duur: 15 min (of op basis van SP: 1 SP = 30 min)
let defaultDuration = 15;
if (issue.story_points && issue.story_points > 0) {
const spMin = Math.round(issue.story_points * 30);
defaultDuration = [15, 30, 60, 90].reduce((prev, curr) =>
Math.abs(curr - spMin) < Math.abs(prev - spMin) ? curr : prev
);
}
setJiraPlanDuration(defaultDuration);
// Prioriteit: vertaal Jira priority naar Fociz priority
const jiraPrio = (issue.priority || '').toLowerCase();
const focizPrio = (jiraPrio === 'highest' || jiraPrio === 'critical') ? 'P1'
: (jiraPrio === 'high') ? 'P2'
: 'P3';
document.getElementById('jiraPlanPriority').value = focizPrio;
// Slimme starttijd: stel het volgende vrije kwartier voor
const now = new Date();
const todayStr = now.toISOString().slice(0, 10);
const suggestTime = targetDate === todayStr ? suggestNextFreeSlot() : '09:00';
if (startTimeInput) {
startTimeInput.value = suggestTime;
}
}
const notice = document.getElementById('jiraPlanOpenBlockNotice');
if (notice) notice.classList.add('hidden');
updateJiraPlanEndTimeDisplay();
// Reset board & timeline velden
const addToBoardCb = document.getElementById('jiraPlanAddToBoard');
if (addToBoardCb) {
addToBoardCb.checked = false;
toggleJiraBoardOptions(false);
}
const schedTimelineCb = document.getElementById('jiraPlanScheduleOnTimeline');
if (schedTimelineCb) {
schedTimelineCb.checked = true;
toggleJiraTimelineDates(true);
}
const tStart = document.getElementById('jiraTimelineStartDate');
const tEnd = document.getElementById('jiraTimelineEndDate');
if (tStart) tStart.value = targetDate;
if (tEnd) tEnd.value = targetDate;
// Reset submit button state
const submitBtn = document.getElementById('btnSubmitJiraPlan');
if (submitBtn) {
submitBtn.disabled = false;
if (isAlreadyPlanned) {
submitBtn.className = "flex-1 px-4 py-2.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer";
submitBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i><span>Herplannen</span>';
} else {
submitBtn.className = "flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer";
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
}
}
const modal = document.getElementById('jiraPlanModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closeJiraPlanModal() {
const modal = document.getElementById('jiraPlanModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
const submitBtn = document.getElementById('btnSubmitJiraPlan');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
}
_jiraPlanCurrentIssue = null;
}
function updateJiraPlanEndTimeDisplay() {
const startTimeInput = document.getElementById('jiraPlanStartTime');
const badge = document.getElementById('jiraPlanEndTimeBadge');
const durationInput = document.getElementById('jiraPlanDuration');
if (!badge) return;
const startTime = startTimeInput ? startTimeInput.value.trim() : '';
const duration = durationInput ? (parseInt(durationInput.value, 10) || 15) : 15;
if (!startTime) {
badge.textContent = '';
return;
}
const parts = startTime.split(':').map(Number);
if (parts.length < 2 || isNaN(parts[0]) || isNaN(parts[1])) {
badge.textContent = '';
return;
}
const endTotalMin = parts[0] * 60 + parts[1] + duration;
const endH = Math.floor(endTotalMin / 60);
const endM = endTotalMin % 60;
const endTimeStr = `${String(endH).padStart(2,'0')}:${String(endM).padStart(2,'0')}`;
badge.textContent = `(tot ${endTimeStr})`;
}
function clearJiraPlanStartTime() {
const startInput = document.getElementById('jiraPlanStartTime');
if (startInput) {
startInput.value = '';
updateJiraPlanEndTimeDisplay();
}
}
async function planJiraInNextOpenBlock() {
const durationInput = document.getElementById('jiraPlanDuration');
const duration = durationInput ? (parseInt(durationInput.value, 10) || 15) : 15;
const btn = document.getElementById('btnJiraPlanOpenBlock');
const notice = document.getElementById('jiraPlanOpenBlockNotice');
const noticeText = document.getElementById('jiraPlanOpenBlockNoticeText');
let origBtnHtml = '';
if (btn) {
origBtnHtml = btn.innerHTML;
btn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin text-white"></i><span>Zoeken...</span>';
btn.disabled = true;
}
try {
const res = await fetch('/api/planner.php?action=find_next_free_slot', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
duration_minutes: duration
})
});
const data = await res.json();
if (data.success && data.slot) {
const slot = data.slot;
const dateInput = document.getElementById('jiraPlanDate');
if (dateInput) dateInput.value = slot.date;
const startInput = document.getElementById('jiraPlanStartTime');
if (startInput) startInput.value = slot.start;
updateJiraPlanEndTimeDisplay();
if (notice && noticeText) {
noticeText.textContent = `Open blok gevonden: ${slot.formatted_date} van ${slot.start} tot ${slot.end} (${slot.duration_minutes}m)`;
notice.classList.remove('hidden');
}
} else {
alert(data.error || 'Geen vrij blok gevonden binnen de gezette werktijden.');
}
} catch (err) {
alert('Netwerkfout bij zoeken naar een open blok.');
} finally {
if (btn) {
btn.innerHTML = origBtnHtml;
btn.disabled = false;
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
function setJiraPlanDuration(minutes) {
document.getElementById('jiraPlanDuration').value = minutes;
document.querySelectorAll('.jira-plan-dur-btn').forEach(btn => {
const dur = parseInt(btn.getAttribute('data-dur'), 10);
if (dur === minutes) {
btn.className = 'jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-indigo-600 border-indigo-600 text-white shadow-xs';
} else {
btn.className = 'jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-300 hover:border-indigo-400';
}
});
updateJiraPlanEndTimeDisplay();
}
function suggestNextFreeSlot() {
const now = new Date();
let h = now.getHours();
let m = now.getMinutes();
m = Math.ceil((m + 1) / 15) * 15;
if (m >= 60) { h += 1; m = 0; }
const wStart = (typeof FOCIZ_WORKDAY_START !== 'undefined' && FOCIZ_WORKDAY_START) ? FOCIZ_WORKDAY_START : '08:30';
const wEnd = (typeof FOCIZ_WORKDAY_END !== 'undefined' && FOCIZ_WORKDAY_END) ? FOCIZ_WORKDAY_END : '17:00';
const [startH, startM] = wStart.split(':').map(Number);
const [endH, endM] = wEnd.split(':').map(Number);
const curMin = h * 60 + m;
const startMin = startH * 60 + startM;
const endMin = endH * 60 + endM;
if (curMin < startMin) {
return `${String(startH).padStart(2,'0')}:${String(startM).padStart(2,'0')}`;
}
if (curMin >= endMin) {
return `${String(startH).padStart(2,'0')}:${String(startM).padStart(2,'0')}`;
}
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;
}
async function submitJiraPlanModal() {
if (!_jiraPlanCurrentIssue) return;
const issue = _jiraPlanCurrentIssue;
const titleInput = document.getElementById('jiraPlanCustomTitle');
const customTitle = titleInput ? titleInput.value.trim() : '';
const finalTitle = customTitle || `Oppakken Jira item ${issue.key}`;
const dateInput = document.getElementById('jiraPlanDate');
let planDate = (dateInput && dateInput.value) ? dateInput.value : ((typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10));
const duration = parseInt(document.getElementById('jiraPlanDuration').value, 10) || 15;
let startTime = document.getElementById('jiraPlanStartTime').value.trim();
const priority = document.getElementById('jiraPlanPriority').value;
// Controleer of de ingeplande tijd buiten de gezette werktijden valt
if (startTime) {
const [hh, mm] = startTime.split(':').map(Number);
const endMin = hh * 60 + mm + duration;
const endTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
const check = checkWorkdayBounds(planDate, startTime, endTime, duration);
if (check.isOutside) {
const answer = await promptOutsideWorkhoursModal(check);
if (answer === 'CANCEL') {
return;
}
if (answer === 'NEE') {
// Automatisch naar volgende werkdag met eerder gezette voorwaarden
try {
const nextSlot = await findSlotOnNextWorkday(duration, 0);
planDate = nextSlot.date;
startTime = nextSlot.start;
} catch (err) {
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
return;
}
}
}
}
const btn = document.getElementById('btnSubmitJiraPlan');
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.innerHTML = '<span class="text-xs font-bold animate-pulse">Inplannen...</span>';
btn.disabled = true;
}
const existingTaskId = parseInt(document.getElementById('jiraPlanTaskId')?.value || '0', 10);
const isUpdate = existingTaskId > 0;
try {
let res, data;
let finalStartTime = startTime;
let finalEndTime = '';
if (startTime) {
const [hh, mm] = startTime.split(':').map(Number);
const endMin = hh * 60 + mm + duration;
finalEndTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
}
const addToBoard = document.getElementById('jiraPlanAddToBoard')?.checked || false;
const boardId = parseInt(document.getElementById('jiraPlanBoardSelect')?.value || '0', 10);
const laneId = parseInt(document.getElementById('jiraPlanLaneSelect')?.value || '0', 10);
const scheduleOnTimeline = document.getElementById('jiraPlanScheduleOnTimeline')?.checked || false;
const timelineStartDate = document.getElementById('jiraTimelineStartDate')?.value || planDate;
const timelineEndDate = document.getElementById('jiraTimelineEndDate')?.value || timelineStartDate;
const boardPayload = {
add_to_board: addToBoard,
board_id: boardId,
lane_id: laneId,
schedule_on_timeline: scheduleOnTimeline,
timeline_start_date: timelineStartDate,
timeline_end_date: timelineEndDate,
description: issue.summary ? `${issue.key}: ${issue.summary}` : ''
};
if (isUpdate) {
// Bestaande taak bijwerken / verplaatsen naar eventueel andere datum
res = await fetch('/api/planner.php?action=update_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
task_id: existingTaskId,
title: finalTitle,
priority: priority,
duration_minutes: duration,
time_block_start: finalStartTime,
time_block_end: finalEndTime,
date: planDate,
...boardPayload
})
});
data = await res.json();
} else {
// Nieuwe taak toevoegen
const payload = {
title: finalTitle,
priority: priority,
source: 'JIRA',
external_id: issue.key,
external_url: issue.url || '',
duration_minutes: duration,
date: planDate,
...boardPayload
};
if (finalStartTime) {
payload.time_block_start = finalStartTime;
payload.time_block_end = finalEndTime;
}
res = await fetch('/api/planner.php?action=add_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
data = await res.json();
}
if (data.success) {
closeJiraPlanModal();
const actionVerb = isUpdate ? 'geherpland' : 'ingepland';
showWidgetToast(`<strong>${escapeHtml(issue.key)}</strong> succesvol ${actionVerb}`, 'success');
// Live update van cache en sets
const savedTaskId = isUpdate ? existingTaskId : (data.task ? data.task.id : existingTaskId);
if (typeof plannedExternalTasksMap !== 'undefined') {
plannedExternalTasksMap[issue.key] = {
task_id: savedTaskId,
title: finalTitle,
priority: priority,
time_block_start: finalStartTime || '',
time_block_end: finalEndTime || '',
duration_minutes: duration,
date: planDate
};
}
if (typeof plannedExternalIdsSet !== 'undefined') {
plannedExternalIdsSet.add(issue.key);
}
// Knoppen bijwerken
document.querySelectorAll(`.plan-btn-jira[data-jira-key="${issue.key}"]`).forEach(btn => {
btn.className = "plan-btn-jira flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer";
btn.title = "Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)";
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i><span>Ingepland</span>';
});
const detailPlanBtn = document.getElementById('btnJiraDetailPlan');
if (detailPlanBtn && typeof _jiraPlanCurrentIssue !== 'undefined' && _jiraPlanCurrentIssue && _jiraPlanCurrentIssue.key === issue.key) {
detailPlanBtn.className = "flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs transition cursor-pointer shadow-xs";
detailPlanBtn.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5 stroke-[2.5]"></i><span>Ingepland</span>';
}
if (typeof lucide !== 'undefined' && lucide.createIcons) lucide.createIcons();
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
} else {
alert(data.error || 'Kon ticket niet inplannen');
}
} catch(e) {
alert('Netwerkfout bij inplannen');
} finally {
if (btn) {
btn.innerHTML = origHtml || '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
btn.disabled = false;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
}
async function quickImportJiraByKey(btn, issueKey, event = null) {
if (event) {
try {
event.stopPropagation();
event.preventDefault();
} catch (_) {}
}
const rawKey = String(issueKey || '').trim();
let fallbackData = null;
if (btn) {
fallbackData = {
summary: btn.getAttribute('data-jira-summary') || '',
status: btn.getAttribute('data-jira-status') || 'To Do',
priority: btn.getAttribute('data-jira-priority') || 'Medium'
};
}
openJiraPlanModal(rawKey, fallbackData);
}
// --- Direct E-mail Inplannen als Dagtaak ---
let _emailPlanCurrentData = null;
function toggleEmailPlanPreview() {
const wrapper = document.getElementById('emailPlanDescWrapper');
const chevron = document.getElementById('emailPlanDescChevron');
const label = document.getElementById('emailPlanDescToggleLabel');
if (!wrapper) return;
const isHidden = wrapper.classList.contains('hidden');
if (isHidden) {
wrapper.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
if (label) label.textContent = 'Inhoud verbergen';
} else {
wrapper.classList.add('hidden');
if (chevron) chevron.classList.remove('rotate-180');
if (label) label.textContent = 'Inhoud van het bericht bekijken';
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function handleEmailPlanClick(btn, event = null) {
if (event) {
try {
event.stopPropagation();
event.preventDefault();
} catch (_) {}
}
if (!btn) return;
const mailId = btn.getAttribute('data-mail-id') || '';
const subject = btn.getAttribute('data-mail-subject') || '';
const sender = btn.getAttribute('data-mail-sender') || 'Outlook Mail';
const link = btn.getAttribute('data-mail-link') || '';
const preview = btn.getAttribute('data-mail-preview') || '';
openPlanEmailDirect(mailId, subject, sender, link, preview);
}
function openPlanEmailDirect(mailId, subject, sender, webLink, bodyPreview = '') {
_emailPlanCurrentData = { mailId, subject, sender, webLink, bodyPreview };
document.getElementById('planEmailId').value = mailId || '';
document.getElementById('planEmailWebLink').value = webLink || '';
document.getElementById('planEmailSender').value = sender || 'Outlook Mail';
document.getElementById('planEmailModalSenderBadge').textContent = sender || 'Outlook Mail';
document.getElementById('planEmailModalSenderName').textContent = sender || 'Outlook Mail';
document.getElementById('planEmailModalSubject').textContent = subject || '(Geen onderwerp)';
// Titel input
const titleInput = document.getElementById('planEmailTitle');
if (titleInput) {
titleInput.value = subject ? `[Mail] ${subject}` : '[Mail] E-mail opvolgen';
}
// Body preview
const descEl = document.getElementById('emailPlanModalDescription');
if (descEl) {
descEl.textContent = (bodyPreview && bodyPreview.trim()) ? bodyPreview.trim() : 'Geen berichtvoorbeeld beschikbaar.';
}
const descWrapper = document.getElementById('emailPlanDescWrapper');
const descChevron = document.getElementById('emailPlanDescChevron');
const descLabel = document.getElementById('emailPlanDescToggleLabel');
if (descWrapper) descWrapper.classList.add('hidden');
if (descChevron) descChevron.classList.remove('rotate-180');
if (descLabel) descLabel.textContent = 'Inhoud van het bericht bekijken';
// Datum: actieve planner datum of vandaag
const dateInput = document.getElementById('planEmailDate');
const targetDate = (typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10);
if (dateInput) {
dateInput.value = targetDate;
}
// Duur: standaard 30 minuten
setEmailPlanDuration(30);
// Prioriteit: standaard P2
const prioEl = document.getElementById('planEmailPriority');
if (prioEl) prioEl.value = 'P2';
// Starttijd: suggestie
const now = new Date();
const todayStr = now.toISOString().slice(0, 10);
const suggestTime = targetDate === todayStr ? suggestNextFreeSlot() : '09:00';
document.getElementById('planEmailStartTime').value = suggestTime;
const notice = document.getElementById('emailPlanOpenBlockNotice');
if (notice) notice.classList.add('hidden');
updatePlanEmailEndTimeDisplay();
// Reset submit button state
const submitBtn = document.getElementById('btnSubmitPlanEmailDirect');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
}
const modal = document.getElementById('planEmailDirectModal');
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closePlanEmailDirectModal() {
const modal = document.getElementById('planEmailDirectModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
const submitBtn = document.getElementById('btnSubmitPlanEmailDirect');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
}
_emailPlanCurrentData = null;
}
function setEmailPlanDuration(minutes) {
const durInput = document.getElementById('planEmailDuration');
if (durInput) durInput.value = minutes;
document.querySelectorAll('.email-plan-dur-btn').forEach(btn => {
const dur = parseInt(btn.getAttribute('data-dur'), 10);
if (dur === minutes) {
btn.className = 'email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-indigo-600 border-indigo-600 text-white shadow-xs';
} else {
btn.className = 'email-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-300 hover:border-indigo-400';
}
});
updatePlanEmailEndTimeDisplay();
}
function clearPlanEmailStartTime() {
const startInput = document.getElementById('planEmailStartTime');
if (startInput) {
startInput.value = '';
updatePlanEmailEndTimeDisplay();
}
}
function updatePlanEmailEndTimeDisplay() {
const startTimeInput = document.getElementById('planEmailStartTime');
const badge = document.getElementById('planEmailEndTimeBadge');
const durationInput = document.getElementById('planEmailDuration');
if (!badge) return;
const startTime = startTimeInput ? startTimeInput.value.trim() : '';
const duration = durationInput ? (parseInt(durationInput.value, 10) || 30) : 30;
if (!startTime) {
badge.textContent = '';
return;
}
const parts = startTime.split(':').map(Number);
if (parts.length < 2 || isNaN(parts[0]) || isNaN(parts[1])) {
badge.textContent = '';
return;
}
const endTotalMin = parts[0] * 60 + parts[1] + duration;
const endH = Math.floor(endTotalMin / 60);
const endM = endTotalMin % 60;
const endTimeStr = `${String(endH).padStart(2,'0')}:${String(endM).padStart(2,'0')}`;
badge.textContent = `(tot ${endTimeStr})`;
}
async function planEmailInNextOpenBlock() {
const durationInput = document.getElementById('planEmailDuration');
const duration = durationInput ? (parseInt(durationInput.value, 10) || 30) : 30;
const btn = document.getElementById('btnEmailPlanOpenBlock');
const notice = document.getElementById('emailPlanOpenBlockNotice');
const noticeText = document.getElementById('emailPlanOpenBlockNoticeText');
let origBtnHtml = '';
if (btn) {
origBtnHtml = btn.innerHTML;
btn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin text-white"></i><span>Zoeken...</span>';
btn.disabled = true;
}
try {
const res = await fetch('/api/planner.php?action=find_next_free_slot', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
duration_minutes: duration
})
});
const data = await res.json();
if (data.success && data.slot) {
const slot = data.slot;
const dateInput = document.getElementById('planEmailDate');
if (dateInput) dateInput.value = slot.date;
const startInput = document.getElementById('planEmailStartTime');
if (startInput) startInput.value = slot.start;
updatePlanEmailEndTimeDisplay();
if (notice && noticeText) {
noticeText.textContent = `Open blok gevonden: ${slot.formatted_date} van ${slot.start} tot ${slot.end} (${slot.duration_minutes}m)`;
notice.classList.remove('hidden');
}
} else {
alert(data.error || 'Geen vrij blok gevonden binnen de gezette werktijden.');
}
} catch (err) {
alert('Netwerkfout bij zoeken naar een open blok.');
} finally {
if (btn) {
btn.innerHTML = origBtnHtml;
btn.disabled = false;
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
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;
let startTime = document.getElementById('planEmailStartTime').value.trim();
const syncOutlook = document.getElementById('planEmailSyncOutlook') ? document.getElementById('planEmailSyncOutlook').checked : false;
const webLink = document.getElementById('planEmailWebLink').value;
const sender = document.getElementById('planEmailSender').value;
const dateInput = document.getElementById('planEmailDate');
let planDate = (dateInput && dateInput.value) ? dateInput.value : ((typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10));
if (!title) {
alert('Vul een taaktitel in');
return;
}
if (startTime) {
const [hh, mm] = startTime.split(':').map(Number);
const endMin = hh * 60 + mm + duration;
const endTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
const check = checkWorkdayBounds(planDate, startTime, endTime, duration);
if (check.isOutside) {
const answer = await promptOutsideWorkhoursModal(check);
if (answer === 'CANCEL') {
return;
}
if (answer === 'NEE') {
try {
const nextSlot = await findSlotOnNextWorkday(duration, 0);
planDate = nextSlot.date;
startTime = nextSlot.start;
} catch (err) {
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
return;
}
}
}
}
const btn = document.getElementById('btnSubmitPlanEmailDirect');
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.innerHTML = '<span class="text-xs font-bold animate-pulse">Inplannen...</span>';
btn.disabled = true;
}
try {
let timeBlockEnd = null;
if (startTime) {
const [hh, mm] = startTime.split(':').map(Number);
const endMin = hh * 60 + mm + duration;
timeBlockEnd = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
}
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,
sender: sender,
web_link: webLink,
priority: priority,
duration_minutes: duration,
time_block_start: startTime,
time_block_end: timeBlockEnd,
sync_outlook: syncOutlook,
date: planDate
})
});
const data = await res.json();
if (data.success) {
closePlanEmailDirectModal();
showWidgetToast(`<strong>E-mail</strong> succesvol ingepland als dagtaak`, 'success');
// Live update: voeg toe aan plannedExternalIdsSet en transformeer knoppen naar groen 'Ingepland'
if (mailId) {
if (typeof plannedExternalIdsSet !== 'undefined') {
plannedExternalIdsSet.add(mailId);
}
document.querySelectorAll(`.plan-btn-mail[data-mail-id="${mailId}"]`).forEach(btn => {
btn.className = "plan-btn-mail flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition cursor-pointer";
btn.title = "Deze e-mail is al ingepland als dagtaak (klik om opnieuw/anders in te plannen)";
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i><span>Ingepland</span>';
});
if (typeof lucide !== 'undefined' && lucide.createIcons) lucide.createIcons();
}
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
} else {
alert(data.error || 'Kon e-mail niet inplannen');
}
} catch(err) {
alert('Netwerkfout bij inplannen van e-mail');
} finally {
if (btn) {
btn.innerHTML = origHtml || '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
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);
}
// --- Bullet Notes & Quick Todo's Logica ---
function switchBulletView(view) {
const bulletsArea = document.getElementById('bulletListViewArea');
const notesArea = document.getElementById('bulletNotesViewArea');
const btnBullets = document.getElementById('btnTabBullets');
const btnNotes = document.getElementById('btnTabNotes');
if (!bulletsArea || !notesArea) return;
if (view === 'bullets') {
bulletsArea.classList.remove('hidden');
notesArea.classList.add('hidden');
if (btnBullets) {
btnBullets.classList.add('bg-white', 'dark:bg-[#111624]', 'text-indigo-600', 'dark:text-indigo-400', 'shadow-2xs', 'font-bold');
btnBullets.classList.remove('text-slate-500', 'dark:text-slate-400');
}
if (btnNotes) {
btnNotes.classList.remove('bg-white', 'dark:bg-[#111624]', 'text-indigo-600', 'dark:text-indigo-400', 'shadow-2xs', 'font-bold');
btnNotes.classList.add('text-slate-500', 'dark:text-slate-400');
}
localStorage.setItem('fociz_bullet_tab', 'bullets');
} else {
bulletsArea.classList.add('hidden');
notesArea.classList.remove('hidden');
if (btnNotes) {
btnNotes.classList.add('bg-white', 'dark:bg-[#111624]', 'text-indigo-600', 'dark:text-indigo-400', 'shadow-2xs', 'font-bold');
btnNotes.classList.remove('text-slate-500', 'dark:text-slate-400');
}
if (btnBullets) {
btnBullets.classList.remove('bg-white', 'dark:bg-[#111624]', 'text-indigo-600', 'dark:text-indigo-400', 'shadow-2xs', 'font-bold');
btnBullets.classList.add('text-slate-500', 'dark:text-slate-400');
}
localStorage.setItem('fociz_bullet_tab', 'notes');
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
async function addBulletItem() {
const input = document.getElementById('newBulletInput');
if (!input) return;
const content = input.value.trim();
if (!content) {
input.focus();
return;
}
const btn = document.getElementById('btnAddBullet');
const origHtml = btn ? btn.innerHTML : '';
if (btn) btn.disabled = true;
try {
const res = await fetch('/api/planner.php?action=add_bullet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
date: typeof activePlannerDate !== 'undefined' ? activePlannerDate : '',
content: content
})
});
const data = await res.json();
if (data.success && data.bullet) {
input.value = '';
await reloadBulletList();
} else {
alert(data.error || 'Kon notitie niet toevoegen');
}
} catch (e) {
alert('Netwerkfout bij toevoegen van notitie');
} finally {
if (btn) {
btn.innerHTML = origHtml;
btn.disabled = false;
}
}
}
async function toggleBulletItem(bulletId, newStatus) {
const textEl = document.getElementById(`bullet-text-${bulletId}`);
if (textEl) {
if (newStatus === 1) {
textEl.classList.add('line-through', 'text-slate-400', 'dark:text-slate-500');
textEl.classList.remove('text-slate-900', 'dark:text-slate-100');
} else {
textEl.classList.remove('line-through', 'text-slate-400', 'dark:text-slate-500');
textEl.classList.add('text-slate-900', 'dark:text-slate-100');
}
}
try {
const res = await fetch('/api/planner.php?action=toggle_bullet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bullet_id: bulletId, is_completed: newStatus })
});
const data = await res.json();
if (!data.success) {
await reloadBulletList();
} else {
await reloadBulletList();
}
} catch (e) {
await reloadBulletList();
}
}
function startEditBullet(bulletId) {
const textEl = document.getElementById(`bullet-text-${bulletId}`);
if (!textEl) return;
const currentVal = textEl.innerText.trim();
const parent = textEl.parentElement;
const input = document.createElement('input');
input.type = 'text';
input.value = currentVal;
input.className = 'w-full bg-slate-100 dark:bg-slate-800 border border-indigo-500 rounded-lg px-2 py-1 text-xs text-slate-900 dark:text-white focus:outline-none';
let isSaving = false;
const save = async () => {
if (isSaving) return;
isSaving = true;
const newVal = input.value.trim();
if (!newVal || newVal === currentVal) {
textEl.style.display = '';
input.remove();
return;
}
try {
await fetch('/api/planner.php?action=update_bullet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bullet_id: bulletId, content: newVal })
});
textEl.innerText = newVal;
} catch(e) {}
textEl.style.display = '';
input.remove();
};
input.onkeydown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
save();
} else if (e.key === 'Escape') {
textEl.style.display = '';
input.remove();
}
};
input.onblur = save;
textEl.style.display = 'none';
parent.appendChild(input);
input.focus();
input.select();
}
async function deleteBulletItem(bulletId) {
if (!confirm('Weet je zeker dat je deze notitie wilt verwijderen? (Indien ingepland in de agenda wordt ook het agendablok verwijderd)')) {
return;
}
const row = document.getElementById(`bullet-row-${bulletId}`);
if (row) row.style.opacity = '0.4';
try {
const res = await fetch('/api/planner.php?action=delete_bullet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bullet_id: bulletId, delete_task: true })
});
const data = await res.json();
if (data.success) {
await reloadBulletList();
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(activePlannerDate, null, false);
}
} else {
alert(data.error || 'Kon notitie niet verwijderen');
if (row) row.style.opacity = '1';
}
} catch (e) {
alert('Netwerkfout bij verwijderen');
if (row) row.style.opacity = '1';
}
}
async function carryOverOpenBullets() {
try {
const res = await fetch('/api/planner.php?action=carryover_bullets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_date: activePlannerDate })
});
const data = await res.json();
if (data.success) {
if (typeof showWidgetToast === 'function') {
showWidgetToast(`${data.moved_count} openstaande memo's overgezet naar vandaag!`, 'success');
}
await reloadBulletList();
}
} catch(e) {
alert('Netwerkfout bij overzetten van openstaande memo\'s');
}
}
async function reloadBulletList() {
try {
const res = await fetch(`/api/planner.php?action=get_bullets&date=${encodeURIComponent(activePlannerDate)}`);
const data = await res.json();
if (data.success) {
const container = document.getElementById('bulletItemsContainer');
const badge = document.getElementById('bulletCountBadge');
const carryBanner = document.getElementById('bulletCarryoverBanner');
const pastCountEl = document.getElementById('pastOpenBulletCountText');
if (badge) {
const count = data.bullets.length;
badge.innerText = count;
badge.className = `ml-1 px-1.5 py-0.2 rounded-full text-[9px] ${count > 0 ? 'bg-indigo-100 dark:bg-indigo-500/20 text-indigo-700 dark:text-indigo-300' : 'hidden'}`;
}
if (carryBanner && pastCountEl) {
if (data.past_open_count > 0) {
pastCountEl.innerText = data.past_open_count;
carryBanner.classList.remove('hidden');
} else {
carryBanner.classList.add('hidden');
}
}
if (container) {
if (!data.bullets || data.bullets.length === 0) {
container.innerHTML = `
<div id="bulletEmptyState" class="py-8 text-center text-slate-400 dark:text-slate-500">
<div class="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800/60 flex items-center justify-center mx-auto mb-2 text-slate-400">
<i data-lucide="check-circle" class="w-5 h-5 opacity-60"></i>
</div>
<p class="text-xs font-medium text-slate-600 dark:text-slate-400">Geen notities of actiepunten voor deze dag.</p>
<p class="text-[11px] opacity-75 mt-0.5">Typ hierboven een punt, bijv. "Bespreken met collega".</p>
</div>
`;
} else {
let html = '';
data.bullets.forEach(b => {
const bId = parseInt(b.id, 10);
const isComp = parseInt(b.is_completed, 10) === 1;
const isSched = !!b.scheduled_task_id || !!b.scheduled_start_time;
const schedTime = b.scheduled_start_time || b.live_start || '';
const schedEndTime = b.scheduled_end_time || b.live_end || '';
const schedDate = b.scheduled_date || activePlannerDate;
const contentEsc = typeof escapeHtml === 'function' ? escapeHtml(b.content) : b.content;
const contentAttr = (typeof escapeHtml === 'function' ? escapeHtml(b.content) : b.content).replace(/"/g, '&quot;');
html += `
<div id="bullet-row-${bId}" class="group/bullet flex items-center justify-between p-2.5 rounded-xl border ${isComp ? 'bg-slate-50/60 dark:bg-slate-900/30 border-slate-200/60 dark:border-slate-800/50' : 'bg-white/80 dark:bg-[#0c0f17]/70 border-slate-200/90 dark:border-slate-800'} transition shadow-2xs hover:border-indigo-300 dark:hover:border-indigo-500/40">
<div class="flex items-center space-x-2.5 min-w-0 flex-1 mr-2">
<button
type="button"
onclick="toggleBulletItem(${bId}, ${isComp ? 0 : 1})"
title="${isComp ? 'Markeren als niet afgerond' : 'Afvinken'}"
class="w-5 h-5 rounded-lg flex items-center justify-center flex-shrink-0 transition cursor-pointer ${isComp ? 'bg-emerald-500 text-white' : 'border border-slate-300 dark:border-slate-700 hover:border-indigo-500 text-transparent hover:text-indigo-400'}"
>
<i data-lucide="check" class="w-3.5 h-3.5 stroke-[3]"></i>
</button>
<div class="min-w-0 flex-1">
<div class="flex items-center flex-wrap gap-1.5">
<span
id="bullet-text-${bId}"
ondblclick="startEditBullet(${bId})"
class="text-xs font-medium select-text break-words ${isComp ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-900 dark:text-slate-100'}"
>${contentEsc}</span>
${isSched ? `
<button
type="button"
onclick="openBulletScheduleModal(${bId}, '${contentAttr}', '${schedDate}', '${schedTime}', '${schedEndTime}')"
title="Ingepland op agenda: ${schedDate} ${schedTime}${schedEndTime ? ' - ' + schedEndTime : ''} (klik om te wijzigen)"
class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md bg-indigo-50 dark:bg-indigo-500/15 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-500/30 text-[10px] font-mono font-bold hover:bg-indigo-100 transition cursor-pointer"
>
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
<span>${schedDate === activePlannerDate ? 'Vandaag' : schedDate} ${schedTime}${schedEndTime ? '-' + schedEndTime : ''}</span>
</button>
` : ''}
</div>
</div>
</div>
<div class="flex items-center space-x-1 opacity-80 sm:opacity-0 sm:group-hover/bullet:opacity-100 transition-opacity flex-shrink-0">
<button
type="button"
onclick="openBulletScheduleModal(${bId}, '${contentAttr}', '${schedDate}', '${schedTime}', '${schedEndTime}')"
title="In agenda inplannen (kies een vrij blok)"
class="p-1.5 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-500/20 transition cursor-pointer"
>
<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i>
</button>
<button
type="button"
onclick="startEditBullet(${bId})"
title="Tekst bewerken"
class="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer"
>
<i data-lucide="pencil" class="w-3.5 h-3.5"></i>
</button>
<button
type="button"
onclick="deleteBulletItem(${bId})"
title="Verwijderen"
class="p-1.5 rounded-lg text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-500/20 transition cursor-pointer"
>
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
`;
});
container.innerHTML = html;
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
} catch(e) {
console.error('Fout bij herladen bullet list:', e);
}
}
// --- Bullet Agenda Modal Logica ---
function openBulletScheduleModal(bulletId, content, scheduledDate, startTime, endTime) {
const modal = document.getElementById('bulletScheduleModal');
if (!modal) return;
document.getElementById('schedBulletId').value = bulletId;
document.getElementById('schedBulletTitle').value = content || '';
const dateInput = document.getElementById('schedBulletDate');
const targetDate = scheduledDate || activePlannerDate;
if (dateInput) dateInput.value = targetDate;
const startInput = document.getElementById('schedBulletStart');
const endInput = document.getElementById('schedBulletEnd');
if (startInput) startInput.value = startTime || '';
if (endInput) endInput.value = endTime || '';
const unscheduleBtn = document.getElementById('btnSchedBulletUnschedule');
if (unscheduleBtn) {
if (startTime) {
unscheduleBtn.classList.remove('hidden');
} else {
unscheduleBtn.classList.add('hidden');
}
}
modal.classList.remove('hidden');
modal.classList.add('flex');
refreshBulletFreeSlots();
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closeBulletScheduleModal() {
const modal = document.getElementById('bulletScheduleModal');
if (!modal) return;
modal.classList.add('hidden');
modal.classList.remove('flex');
}
function setSchedBulletDate(preset) {
const dateInput = document.getElementById('schedBulletDate');
if (!dateInput) return;
const now = new Date();
if (preset === 'tomorrow') {
now.setDate(now.getDate() + 1);
}
dateInput.value = now.toISOString().slice(0, 10);
refreshBulletFreeSlots();
}
function setSchedBulletDuration(minutes, btn) {
document.getElementById('schedBulletDuration').value = minutes;
document.querySelectorAll('.btn-sched-dur').forEach(b => {
b.className = 'btn-sched-dur py-1.5 rounded-xl border border-slate-200 dark:border-slate-800 text-xs font-semibold text-slate-600 dark:text-slate-400 hover:border-indigo-500 transition cursor-pointer text-center';
});
if (btn) {
btn.className = 'btn-sched-dur py-1.5 rounded-xl border border-indigo-500 bg-indigo-50 dark:bg-indigo-500/20 text-xs font-bold text-indigo-700 dark:text-indigo-300 transition cursor-pointer text-center';
}
onSchedBulletTimeChange();
refreshBulletFreeSlots();
}
function onSchedBulletTimeChange() {
const startVal = document.getElementById('schedBulletStart')?.value.trim();
const duration = parseInt(document.getElementById('schedBulletDuration')?.value || 30, 10);
const endInput = document.getElementById('schedBulletEnd');
if (!startVal || !endInput) return;
const parts = startVal.split(':');
if (parts.length === 2) {
const h = parseInt(parts[0], 10);
const m = parseInt(parts[1], 10);
if (!isNaN(h) && !isNaN(m)) {
const totalM = h * 60 + m + duration;
const endH = Math.floor(totalM / 60) % 24;
const endM = totalM % 60;
endInput.value = `${String(endH).padStart(2, '0')}:${String(endM).padStart(2, '0')}`;
}
}
}
async function refreshBulletFreeSlots() {
const container = document.getElementById('schedBulletSlotsList');
if (!container) return;
const date = document.getElementById('schedBulletDate')?.value || activePlannerDate;
const duration = parseInt(document.getElementById('schedBulletDuration')?.value || 30, 10);
container.innerHTML = '<span class="text-xs text-slate-400 italic">Vrije blokken zoeken...</span>';
try {
const res = await fetch(`/api/planner.php?action=get_day_free_slots&date=${encodeURIComponent(date)}&duration_minutes=${duration}`);
const data = await res.json();
if (data.success && data.free_slots && data.free_slots.length > 0) {
let html = '';
data.free_slots.forEach((slot, idx) => {
const isFirst = (idx === 0);
html += `
<button
type="button"
onclick="selectSchedBulletSlot('${slot.start}', '${slot.end}', this)"
class="slot-btn px-2.5 py-1.5 rounded-xl border border-slate-200 dark:border-slate-700 hover:border-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-500/20 text-xs font-semibold text-slate-700 dark:text-slate-300 transition cursor-pointer flex items-center space-x-1.5"
>
${isFirst ? '<i data-lucide="zap" class="w-3 h-3 text-amber-500"></i>' : '<i data-lucide="clock" class="w-3 h-3 text-indigo-400"></i>'}
<span>${slot.start} - ${slot.end}</span>
</button>
`;
});
container.innerHTML = html;
if (typeof lucide !== 'undefined') lucide.createIcons();
} else {
container.innerHTML = '<span class="text-xs text-slate-400 italic">Geen volledig vrije blokken binnen werktijden op deze dag. Kies hieronder handmatig een tijdstip.</span>';
}
} catch(e) {
container.innerHTML = '<span class="text-xs text-slate-400 italic">Kon vrije blokken niet laden.</span>';
}
}
function selectSchedBulletSlot(start, end, btn) {
const startInput = document.getElementById('schedBulletStart');
const endInput = document.getElementById('schedBulletEnd');
if (startInput) startInput.value = start;
if (endInput) endInput.value = end;
document.querySelectorAll('.slot-btn').forEach(b => {
b.classList.remove('bg-indigo-600', 'text-white', 'border-indigo-600');
b.classList.add('text-slate-700', 'dark:text-slate-300');
});
if (btn) {
btn.classList.remove('text-slate-700', 'dark:text-slate-300');
btn.classList.add('bg-indigo-600', 'text-white', 'border-indigo-600');
}
}
async function submitBulletSchedule() {
const bulletId = parseInt(document.getElementById('schedBulletId')?.value || 0, 10);
const title = document.getElementById('schedBulletTitle')?.value.trim();
const date = document.getElementById('schedBulletDate')?.value;
const startTime = document.getElementById('schedBulletStart')?.value.trim();
const endTime = document.getElementById('schedBulletEnd')?.value.trim();
const duration = parseInt(document.getElementById('schedBulletDuration')?.value || 30, 10);
const priority = document.getElementById('schedBulletPriority')?.value || 'P2';
const syncOutlook = document.getElementById('schedBulletSyncOutlook')?.checked || false;
if (!bulletId) {
alert('Geen notitie geselecteerd');
return;
}
if (!startTime) {
alert('Kies of vul een starttijd in voor het agendablok');
document.getElementById('schedBulletStart')?.focus();
return;
}
const btn = document.getElementById('btnSubmitSchedBullet');
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.innerHTML = '<span>Bezig met inplannen...</span>';
btn.disabled = true;
}
try {
const res = await fetch('/api/planner.php?action=schedule_bullet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bullet_id: bulletId,
title: title,
date: date,
start_time: startTime,
end_time: endTime,
duration_minutes: duration,
priority: priority,
sync_to_outlook: syncOutlook
})
});
const data = await res.json();
if (data.success) {
closeBulletScheduleModal();
if (typeof showWidgetToast === 'function') {
showWidgetToast(`Notitie succesvol ingepland op ${startTime} - ${endTime}!`, 'success');
}
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(activePlannerDate, null, false);
}
} else {
alert(data.error || 'Kon notitie niet inplannen in agenda');
}
} catch(e) {
alert('Netwerkfout bij inplannen van agendablok');
} finally {
if (btn) {
btn.innerHTML = origHtml;
btn.disabled = false;
}
}
}
async function unscheduleCurrentBullet() {
const bulletId = parseInt(document.getElementById('schedBulletId')?.value || 0, 10);
if (!bulletId) return;
if (!confirm('Weet je zeker dat je dit agendablok wilt verwijderen uit je agenda? De notitie blijft bewaard.')) {
return;
}
try {
const res = await fetch('/api/planner.php?action=unschedule_bullet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bullet_id: bulletId })
});
const data = await res.json();
if (data.success) {
closeBulletScheduleModal();
if (typeof showWidgetToast === 'function') {
showWidgetToast('Agendablok verwijderd uit planning', 'info');
}
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(activePlannerDate, null, false);
}
} else {
alert(data.error || 'Kon agendablok niet verwijderen');
}
} catch(e) {
alert('Netwerkfout bij verwijderen uit agenda');
}
}
// Initialiseer tab weergave
document.addEventListener('DOMContentLoaded', () => {
try {
const savedTab = localStorage.getItem('fociz_bullet_tab');
if (savedTab === 'notes') {
switchBulletView('notes');
}
} catch(e) {}
});
// --- 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() {}
/**
* Controleert of een gepland tijdstip buiten de gezette werktijden of in het weekend valt
*/
function checkWorkdayBounds(planDate, startTime, endTime, duration) {
if (!startTime || !startTime.trim()) {
return { isOutside: false };
}
const start = startTime.trim();
let [sH, sM] = start.split(':').map(Number);
if (isNaN(sH) || isNaN(sM)) return { isOutside: false };
let dur = parseInt(duration, 10);
if (isNaN(dur) || dur <= 0) dur = 30;
let end = (endTime && endTime.trim()) ? endTime.trim() : '';
let eH, eM;
if (end) {
[eH, eM] = end.split(':').map(Number);
}
if (isNaN(eH) || isNaN(eM)) {
const totalEndMin = sH * 60 + sM + dur;
eH = Math.floor(totalEndMin / 60);
eM = totalEndMin % 60;
end = `${String(eH).padStart(2,'0')}:${String(eM).padStart(2,'0')}`;
}
const startMin = sH * 60 + sM;
const endMin = eH * 60 + eM;
const wStart = (typeof FOCIZ_WORKDAY_START !== 'undefined' && FOCIZ_WORKDAY_START) ? FOCIZ_WORKDAY_START : '08:30';
const wEnd = (typeof FOCIZ_WORKDAY_END !== 'undefined' && FOCIZ_WORKDAY_END) ? FOCIZ_WORKDAY_END : '17:00';
const [wsH, wsM] = wStart.split(':').map(Number);
const [weH, weM] = wEnd.split(':').map(Number);
const wStartMin = wsH * 60 + wsM;
const wEndMin = weH * 60 + weM;
let isOutside = false;
let reason = '';
if (planDate) {
const d = new Date(planDate + 'T12:00:00');
const dayOfWeek = d.getDay(); // 0 = Zo, 6 = Za
if (dayOfWeek === 0 || dayOfWeek === 6) {
isOutside = true;
reason = 'Het gekozen tijdstip valt in het weekend.';
}
}
if (startMin < wStartMin || endMin > wEndMin || startMin >= wEndMin) {
isOutside = true;
reason = `Het gekozen tijdstip (${start} - ${end}) valt buiten de gezette werktijden (${wStart} - ${wEnd}).`;
}
return {
isOutside,
reason,
start,
end,
duration: dur,
workdayStart: wStart,
workdayEnd: wEnd,
date: planDate
};
}
let _outsideWorkhoursResolver = null;
function promptOutsideWorkhoursModal(checkResult) {
return new Promise((resolve) => {
_outsideWorkhoursResolver = resolve;
const modal = document.getElementById('outsideWorkhoursModal');
const selectedEl = document.getElementById('outsideWorkhoursSelectedSlot');
const boundsEl = document.getElementById('outsideWorkhoursBounds');
if (selectedEl) {
selectedEl.textContent = `${checkResult.start} - ${checkResult.end} (${checkResult.duration}m)`;
}
if (boundsEl) {
boundsEl.textContent = `${checkResult.workdayStart} tot ${checkResult.workdayEnd}`;
}
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined') lucide.createIcons();
setTimeout(() => {
const noBtn = document.getElementById('btnOutsideWorkhoursNo');
if (noBtn) noBtn.focus();
}, 50);
} else {
const ok = confirm(`Let op! Je plant buiten werktijden (${checkResult.workdayStart} - ${checkResult.workdayEnd}).\n\nKlik OK (Ja) om toch zo in te plannen, of Annuleren (Nee) om automatisch naar de volgende werkdag te gaan.`);
resolve(ok ? 'JA' : 'NEE');
}
});
}
function closeOutsideWorkhoursModal(choice) {
const modal = document.getElementById('outsideWorkhoursModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
if (typeof _outsideWorkhoursResolver === 'function') {
const res = _outsideWorkhoursResolver;
_outsideWorkhoursResolver = null;
res(choice);
}
}
/**
* Zoekt automatisch het eerstvolgende open kwartierblok op de volgende werkdag
*/
async function findSlotOnNextWorkday(duration, taskId = 0) {
const res = await fetch('/api/planner.php?action=find_next_free_slot', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
duration_minutes: duration,
task_id: taskId,
start_from_next_workday: true
})
});
const data = await res.json();
if (data.success && data.slot) {
return data.slot;
}
throw new Error(data.error || 'Geen vrij open blok gevonden op de volgende werkdag.');
}
// Globale sneltoetsafhandeling voor modals (Escape om te sluiten, Enter voor actie)
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const outsideModal = document.getElementById('outsideWorkhoursModal');
if (outsideModal && !outsideModal.classList.contains('hidden')) {
closeOutsideWorkhoursModal('CANCEL');
return;
}
const jiraModal = document.getElementById('jiraPlanModal');
if (jiraModal && !jiraModal.classList.contains('hidden')) {
closeJiraPlanModal();
return;
}
const editModal = document.getElementById('editTaskModal');
if (editModal && !editModal.classList.contains('hidden')) {
closeEditTaskModal();
return;
}
const emailModal = document.getElementById('planEmailDirectModal');
if (emailModal && !emailModal.classList.contains('hidden')) {
closePlanEmailDirectModal();
return;
}
const detailModal = document.getElementById('jiraTicketDetailModal');
if (detailModal && !detailModal.classList.contains('hidden')) {
closeJiraTicketDetailModal();
return;
}
} else if (e.key === 'Enter') {
const outsideModal = document.getElementById('outsideWorkhoursModal');
if (outsideModal && !outsideModal.classList.contains('hidden')) {
e.preventDefault();
closeOutsideWorkhoursModal('NEE');
return;
}
}
});
/**
* 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 datum & badges in top header bij
const headerDateEl = document.getElementById('headerCurrentDateDisplay');
if (headerDateEl && data.formattedDate) {
headerDateEl.textContent = data.formattedDate;
}
const headerBadgesEl = document.getElementById('headerDateBadgesContainer');
if (headerBadgesEl) {
let badgesHtml = '';
if (data.isWorkday) {
badgesHtml += `<span class="hidden lg:inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[9px] font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20" title="Reguliere werkdag">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>Werkdag</span>
</span>`;
} else {
badgesHtml += `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[9px] font-bold bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20" title="Weekend / Vrije dag">
<i data-lucide="coffee" class="w-2.5 h-2.5 text-amber-500"></i>
<span>Vrij</span>
</span>`;
}
if (data.isBlocked && data.selectedDayBlock) {
const blockJson = JSON.stringify(data.selectedDayBlock).replace(/"/g, '&quot;');
const blockTitle = (data.selectedDayBlock.title || '').replace(/</g, '&lt;').replace(/>/g, '&gt;');
badgesHtml += `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[9px] font-bold bg-amber-500/15 text-amber-700 dark:text-amber-300 border border-amber-500/30 cursor-pointer hover:bg-amber-500/25 transition" onclick='openCalendarBlockModal(${blockJson})' title="${blockTitle} (klik om te bewerken)">
<i data-lucide="palmtree" class="w-2.5 h-2.5 text-amber-600 dark:text-amber-400"></i>
<span>${blockTitle}</span>
</span>`;
}
headerBadgesEl.innerHTML = badgesHtml;
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
// 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 (localStorage.getItem('fociz_bullet_tab') === 'notes') {
switchBulletView('notes');
}
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;
}
let planDate = activePlannerDate;
if (time) {
const check = checkWorkdayBounds(planDate, time, endTime, duration);
if (check.isOutside) {
const answer = await promptOutsideWorkhoursModal(check);
if (answer === 'CANCEL') {
return;
}
if (answer === 'NEE') {
try {
const nextSlot = await findSlotOnNextWorkday(duration, 0);
planDate = nextSlot.date;
time = nextSlot.start;
endTime = nextSlot.end;
} catch (err) {
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
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: planDate
})
});
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(planDate, 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 || '';
const descEl = document.getElementById('editTaskDescription');
if (descEl) descEl.value = task.description || '';
document.getElementById('editTaskPriority').value = task.priority || 'P2';
document.getElementById('editTaskStartTime').value = task.start || '';
document.getElementById('editTaskEndTime').value = task.end || '';
// Datum
const dateInput = document.getElementById('editTaskDate');
if (dateInput) {
dateInput.value = task.date || (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
}
// Reset statusbericht van open blok
const notice = document.getElementById('editTaskOpenBlockNotice');
if (notice) notice.classList.add('hidden');
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 clearEditTaskTimes() {
const s = document.getElementById('editTaskStartTime');
const e = document.getElementById('editTaskEndTime');
if (s) s.value = '';
if (e) e.value = '';
const notice = document.getElementById('editTaskOpenBlockNotice');
if (notice) notice.classList.add('hidden');
}
async function planInNextOpenBlock() {
const duration = getSelectedEditTaskDurationMinutes();
const taskId = parseInt(document.getElementById('editTaskId')?.value, 10) || 0;
const btn = document.getElementById('btnPlanOpenBlock');
const notice = document.getElementById('editTaskOpenBlockNotice');
const noticeText = document.getElementById('editTaskOpenBlockNoticeText');
let origBtnHtml = '';
if (btn) {
origBtnHtml = btn.innerHTML;
btn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin text-white"></i><span>Zoeken...</span>';
btn.disabled = true;
}
try {
const res = await fetch('/api/planner.php?action=find_next_free_slot', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
duration_minutes: duration,
task_id: taskId
})
});
const data = await res.json();
if (data.success && data.slot) {
const slot = data.slot;
// Stel datum en tijden in
const dateInput = document.getElementById('editTaskDate');
if (dateInput) {
dateInput.value = slot.date;
}
const startInput = document.getElementById('editTaskStartTime');
if (startInput) {
startInput.value = slot.start;
}
const endInput = document.getElementById('editTaskEndTime');
if (endInput) {
endInput.value = slot.end;
}
// Toon statusmelding in dialoog
if (notice && noticeText) {
noticeText.textContent = `Open blok gevonden: ${slot.formatted_date} van ${slot.start} tot ${slot.end} (${slot.duration_minutes}m)`;
notice.classList.remove('hidden');
}
} else {
alert(data.error || 'Geen vrij blok gevonden binnen de gezette werktijden.');
}
} catch (err) {
alert('Netwerkfout bij zoeken naar een open blok.');
} finally {
if (btn) {
btn.innerHTML = origBtnHtml;
btn.disabled = false;
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
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`;
}
// --- Taak & Tijdblok Bewerken Modal Logica ---
function openEditTaskModal(task) {
if (!task) return;
const isOutlook = Boolean(task.is_outlook_event || task.source === 'OUTLOOK_LIVE');
document.getElementById('editTaskId').value = task.id;
const isOutlookInput = document.getElementById('editTaskIsOutlook');
if (isOutlookInput) isOutlookInput.value = isOutlook ? '1' : '0';
const headerTitle = document.getElementById('editTaskModalHeaderTitle');
const headerSubtitle = document.getElementById('editTaskModalHeaderSubtitle');
const titleLabel = document.getElementById('editTaskTitleLabel');
const priorityContainer = document.getElementById('editTaskPriorityContainer');
const saveBtn = document.getElementById('btnSaveEditTask');
if (isOutlook) {
if (headerTitle) headerTitle.textContent = 'Outlook Afspraak Bewerken';
if (headerSubtitle) headerSubtitle.textContent = 'Pas onderwerp, tijdblok en datum van je agenda-afspraak aan';
if (titleLabel) titleLabel.textContent = 'Onderwerp';
if (priorityContainer) priorityContainer.classList.add('hidden');
if (saveBtn) saveBtn.textContent = 'Afspraak Opslaan';
} else {
if (headerTitle) headerTitle.textContent = 'Taak & Tijdblok Bewerken';
if (headerSubtitle) headerSubtitle.textContent = 'Pas titel, start-/eindtijd en tijdsduur vrij aan';
if (titleLabel) titleLabel.textContent = 'Taaktitel';
if (priorityContainer) priorityContainer.classList.remove('hidden');
if (saveBtn) saveBtn.textContent = 'Wijzigingen Opslaan';
}
document.getElementById('editTaskTitle').value = (isOutlook ? (task.subject || task.title) : task.title) || '';
const descEl = document.getElementById('editTaskDescription');
if (descEl) descEl.value = task.description || '';
document.getElementById('editTaskPriority').value = task.priority || 'P2';
document.getElementById('editTaskStartTime').value = task.start || '';
document.getElementById('editTaskEndTime').value = task.end || '';
// Datum
const dateInput = document.getElementById('editTaskDate');
if (dateInput) {
dateInput.value = task.date || (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
}
// Reset statusbericht van open blok
const notice = document.getElementById('editTaskOpenBlockNotice');
if (notice) notice.classList.add('hidden');
let dur = task.duration;
if (!dur && task.start && task.end) {
const sm = parseTimeToMinutes(task.start);
const em = parseTimeToMinutes(task.end);
if (sm !== null && em !== null && em > sm) dur = em - sm;
}
dur = dur || 30;
setSelectedEditTaskDuration(dur);
const outlookNotice = document.getElementById('editTaskOutlookNotice');
if (outlookNotice) {
if (isOutlook) {
outlookNotice.classList.remove('hidden');
outlookNotice.innerHTML = '<i data-lucide="refresh-cw" class="w-3.5 h-3.5 flex-shrink-0"></i><span>Wijzigingen worden direct gesynchroniseerd naar Microsoft Outlook 365.</span>';
} else if (task.source === 'OUTLOOK' || task.external_id) {
outlookNotice.classList.remove('hidden');
outlookNotice.innerHTML = '<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>';
} 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');
}
}
async function submitEditTask() {
const isOutlook = document.getElementById('editTaskIsOutlook')?.value === '1';
const taskId = document.getElementById('editTaskId').value;
const title = document.getElementById('editTaskTitle').value.trim();
const priority = document.getElementById('editTaskPriority').value;
const dateInput = document.getElementById('editTaskDate');
let taskDate = (dateInput && dateInput.value) ? dateInput.value.trim() : (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
let startTime = document.getElementById('editTaskStartTime').value.trim();
let endTime = document.getElementById('editTaskEndTime').value.trim();
const duration = getSelectedEditTaskDurationMinutes();
const saveBtn = document.getElementById('btnSaveEditTask');
if (!title) {
alert(isOutlook ? 'Vul een onderwerp in voor de afspraak.' : 'Vul een taaktitel in.');
return;
}
// Controleer of de geplande tijd buiten de gezette werktijden valt
if (startTime) {
const check = checkWorkdayBounds(taskDate, startTime, endTime, duration);
if (check.isOutside) {
const answer = await promptOutsideWorkhoursModal(check);
if (answer === 'CANCEL') {
return;
}
if (answer === 'NEE') {
// Automatisch naar volgende werkdag met eerder gezette voorwaarden
try {
const nextSlot = await findSlotOnNextWorkday(duration, isOutlook ? 0 : parseInt(taskId, 10));
taskDate = nextSlot.date;
startTime = nextSlot.start;
endTime = nextSlot.end;
} catch (err) {
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
return;
}
}
// Bij 'JA': ga gewoon door met het buiten werktijd ingevulde tijdstip
}
}
if (saveBtn) {
saveBtn.innerText = isOutlook ? 'Afspraak Opslaan...' : 'Opslaan...';
saveBtn.disabled = true;
}
try {
const description = document.getElementById('editTaskDescription')?.value.trim() || '';
if (isOutlook) {
const res = await fetch('/api/planner.php?action=update_outlook_event', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
event_id: taskId,
title: title,
description: description,
time_block_start: startTime,
time_block_end: endTime,
date: taskDate
})
});
const data = await res.json();
if (data.success) {
closeEditTaskModal();
const targetDate = data.date || taskDate;
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(targetDate, null, false);
}
if (typeof showWidgetToast === 'function') {
showWidgetToast(`Outlook afspraak <strong>${escapeHtml(title)}</strong> succesvol bijgewerkt`, 'success');
}
} else {
alert(data.error || 'Kon afspraak niet bijwerken in Microsoft Outlook');
if (saveBtn) {
saveBtn.innerText = 'Afspraak Opslaan';
saveBtn.disabled = false;
}
}
} else {
const res = await fetch('/api/planner.php?action=update_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
task_id: parseInt(taskId, 10),
title: title,
description: description,
priority: priority,
time_block_start: startTime,
time_block_end: endTime,
duration_minutes: duration,
date: taskDate
})
});
const data = await res.json();
if (data.success) {
closeEditTaskModal();
const targetDate = data.date || taskDate;
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(targetDate, null, false);
}
if (typeof showWidgetToast === 'function') {
showWidgetToast(`Taak <strong>${escapeHtml(title)}</strong> succesvol bijgewerkt`, 'success');
}
} else {
alert(data.error || 'Kon taak niet bijwerken');
if (saveBtn) {
saveBtn.innerText = 'Wijzigingen Opslaan';
saveBtn.disabled = false;
}
}
}
} catch (e) {
alert('Netwerkfout bij bijwerken');
if (saveBtn) {
saveBtn.innerText = isOutlook ? 'Afspraak Opslaan' : 'Wijzigingen Opslaan';
saveBtn.disabled = false;
}
}
}
async function deleteOutlookEvent(eventId, eventSubject) {
const displayTitle = eventSubject ? ` "${eventSubject}"` : '';
if (!confirm(`Weet je zeker dat je de Outlook afspraak${displayTitle} wilt verwijderen uit je Microsoft Outlook agenda?`)) return;
try {
const res = await fetch('/api/planner.php?action=delete_outlook_event', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ event_id: eventId })
});
const data = await res.json();
if (data.success) {
if (typeof showWidgetToast === 'function') {
showWidgetToast(`Afspraak <strong>${escapeHtml(eventSubject || 'Outlook')}</strong> verwijderd`, 'info');
}
switchPlannerDate(activePlannerDate, null, false);
} else {
alert(data.error || 'Kon afspraak niet verwijderen uit Outlook');
}
} catch (err) {
alert('Netwerkfout bij verwijderen van Outlook afspraak');
}
}
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();
}
}
}
// --- Drag & Drop van Taken & Afspraken op Tijdlijn ---
let draggedTimelineItem = null;
function handleTimelineItemDragStart(e, type, item) {
draggedTimelineItem = { type, item };
try {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', JSON.stringify({ type, id: item.id }));
} catch (err) {}
// Voeg visuele feedback toe aan het versleepte element
setTimeout(() => {
if (e.target) {
e.target.classList.add('opacity-40', 'scale-[0.98]');
}
}, 0);
}
function handleTimelineItemDragEnd(e) {
if (e.target) {
e.target.classList.remove('opacity-40', 'scale-[0.98]');
}
// Verwijder eventuele achtergebleven drag-over styles van slots
document.querySelectorAll('.timeline-hour-slot').forEach(slot => {
slot.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
});
}
function handleTimelineSlotDragOver(e) {
if (!draggedTimelineItem) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const slot = e.currentTarget;
if (slot && !slot.classList.contains('ring-2')) {
slot.classList.add('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
}
}
function handleTimelineSlotDragLeave(e) {
const slot = e.currentTarget;
if (slot && !slot.contains(e.relatedTarget)) {
slot.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
}
}
async function handleTimelineSlotDrop(e, targetHour) {
e.preventDefault();
const slot = e.currentTarget;
if (slot) {
slot.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
}
if (!draggedTimelineItem) return;
const { type, item } = draggedTimelineItem;
draggedTimelineItem = null;
// Bepaal de oorspronkelijke tijdsduur van het item in minuten
let durationMinutes = 30;
if (item.duration && parseInt(item.duration, 10) > 0) {
durationMinutes = parseInt(item.duration, 10);
} else if (item.start && item.end) {
const sMins = parseTimeToMinutes(item.start);
const eMins = parseTimeToMinutes(item.end);
if (sMins !== null && eMins !== null && eMins > sMins) {
durationMinutes = eMins - sMins;
}
}
// Nieuwe starttijd is het drop-uurslot (bijv. "14:00")
const newStart = targetHour;
const startMins = parseTimeToMinutes(newStart);
let newEnd = '';
if (startMins !== null) {
newEnd = formatMinutesToTime(startMins + durationMinutes);
}
const itemTitle = (type === 'outlook' ? (item.subject || item.title) : item.title) || 'Item';
const planDate = item.date || (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
if (typeof showWidgetToast === 'function') {
showWidgetToast(`Verplaatsen naar <strong>${newStart} - ${newEnd}</strong>...`, 'info');
}
try {
if (type === 'outlook') {
const res = await fetch('/api/planner.php?action=update_outlook_event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event_id: item.id,
title: itemTitle,
description: item.description || '',
time_block_start: newStart,
time_block_end: newEnd,
date: planDate
})
});
const data = await res.json();
if (data.success) {
if (typeof showWidgetToast === 'function') {
showWidgetToast(`Outlook afspraak <strong>${escapeHtml(itemTitle)}</strong> verplaatst naar ${newStart} - ${newEnd}`, 'success');
}
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
} else {
alert(data.error || 'Kon afspraak niet verplaatsen in Outlook');
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
}
} else {
// Reguliere Fociz taak
const res = await fetch('/api/planner.php?action=update_task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
task_id: parseInt(item.id, 10),
title: itemTitle,
description: item.description || '',
priority: item.priority || 'P2',
time_block_start: newStart,
time_block_end: newEnd,
duration_minutes: durationMinutes,
date: planDate
})
});
const data = await res.json();
if (data.success) {
if (typeof showWidgetToast === 'function') {
showWidgetToast(`Taak <strong>${escapeHtml(itemTitle)}</strong> verplaatst naar ${newStart} - ${newEnd}`, 'success');
}
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
} else {
alert(data.error || 'Kon taak niet verplaatsen');
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
}
}
} catch (err) {
console.error('Drag and drop fout:', err);
alert('Netwerkfout bij verplaatsen van item');
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
}
}
// --- 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)}`, {
headers: isManual ? {} : { 'X-Background-Poll': '1' }
});
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'; ?>