Files
Fociz/planner.php
T
NiekRabelink 9262b09c4d Add roadmap management, service worker, and user invitation features
- Implemented roadmap.php for managing roadmap items with filtering options by quarter and status.
- Added a service worker (sw.js) to handle push notifications and click events for calendar appointments.
- Created users.php for admin to manage team members and invitations, including user creation and deletion functionalities.
2026-09-09 15:54:24 +02:00

1718 lines
98 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)
*/
require_once __DIR__ . '/includes/header.php';
require_once __DIR__ . '/includes/settings.php';
require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/integrations/jira.php';
$db = get_db_connection();
$userId = $currentUser['id'];
$today = date('Y-m-d');
$selectedDate = (!empty($_GET['date']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date'])) ? $_GET['date'] : $today;
$isTodaySelected = ($selectedDate === $today);
$selectedTs = strtotime($selectedDate);
// Haal organisatie werkdag- en AI-standaarden op (Release v1.4 & v1.5)
$workdaySettings = get_workday_settings();
$aiSettings = get_ai_settings();
// Bereken weekgrenzen (Maandag t/m Zondag) rondom de geselecteerde datum
$dayOfWeek = (int)date('N', $selectedTs); // 1 = Maandag, 7 = Zondag
$mondayTs = strtotime('-' . ($dayOfWeek - 1) . ' days', $selectedTs);
$sundayTs = strtotime('+' . (7 - $dayOfWeek) . ' days', $selectedTs);
$weekStart = date('Y-m-d', $mondayTs);
$weekEnd = date('Y-m-d', $sundayTs);
$weekNumber = (int)date('W', $selectedTs);
$yearNumber = (int)date('o', $selectedTs);
// URLs voor weeknavigatie
$prevWeekDate = date('Y-m-d', strtotime('-7 days', $mondayTs));
$nextWeekDate = date('Y-m-d', strtotime('+7 days', $mondayTs));
// Nederlandse vertalingen voor datums
$dutchShortDays = [1 => 'Ma', 2 => 'Di', 3 => 'Wo', 4 => 'Do', 5 => 'Vr', 6 => 'Za', 7 => 'Zo'];
$dutchFullDays = [1 => 'Maandag', 2 => 'Dinsdag', 3 => 'Woensdag', 4 => 'Donderdag', 5 => 'Vrijdag', 6 => 'Zaterdag', 7 => 'Zondag'];
$dutchMonths = ['', 'januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'];
$dutchShortMonths = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
$formattedSelectedDate = $dutchFullDays[(int)date('N', $selectedTs)] . ' ' . date('j', $selectedTs) . ' ' . $dutchMonths[(int)date('n', $selectedTs)] . ' ' . date('Y', $selectedTs);
$formattedWeekRange = date('j', $mondayTs) . ' ' . $dutchShortMonths[(int)date('n', $mondayTs)] . ' – ' . date('j', $sundayTs) . ' ' . $dutchShortMonths[(int)date('n', $sundayTs)] . ' ' . date('Y', $sundayTs);
// 1. Haal integratiestatussen op
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ?");
$stmtInt->execute([$userId]);
$userIntegrations = [];
foreach ($stmtInt->fetchAll() as $row) {
$userIntegrations[$row['provider']] = $row;
}
$isOutlookConnected = !empty($userIntegrations['OUTLOOK']['access_token']) && empty($userIntegrations['OUTLOOK']['is_demo_mode']);
$outlookIntegrationSettings = json_decode($userIntegrations['OUTLOOK']['settings'] ?? '{}', true) ?: [];
$syncIntervalSeconds = isset($outlookIntegrationSettings['sync_interval_seconds']) ? (int)$outlookIntegrationSettings['sync_interval_seconds'] : 30;
$weekOutlookEvents = [];
$isLiveOutlook = false;
if ($isOutlookConnected) {
// Haal afspraken op voor het volledige datumbereik van de week
$weekOutlookEvents = microsoft_fetch_events_range($userId, $weekStart, $weekEnd);
$isLiveOutlook = true;
// Synchroniseer / reconcilieer Fociz taken direct met actuele Outlook data
microsoft_reconcile_tasks_with_outlook($userId, $weekStart, $weekEnd, $weekOutlookEvents);
} else {
// Demo data indien Microsoft Outlook nog niet gekoppeld is
$weekOutlookEvents = [
[
'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
$stmtPlan = $db->prepare("SELECT * FROM daily_plans WHERE user_id = ? AND plan_date = ?");
$stmtPlan->execute([$userId, $selectedDate]);
$dailyPlan = $stmtPlan->fetch();
if (!$dailyPlan) {
$defaultGoal = ($selectedDate === $today) ? 'Mijn belangrijkste doel van vandaag behalen' : 'Focusdoel voor deze dag bepalen';
$stmtCreatePlan = $db->prepare("
INSERT INTO daily_plans (user_id, plan_date, focus_goal)
VALUES (?, ?, ?)
");
$stmtCreatePlan->execute([$userId, $selectedDate, $defaultGoal]);
$planId = (int)$db->lastInsertId();
$dailyPlan = ['id' => $planId, 'plan_date' => $selectedDate, 'focus_goal' => $defaultGoal, 'notes' => ''];
}
// 3. Haal taken op voor de geselecteerde dag (vers bijgewerkt na eventuele reconciliatie)
$stmtTasks = $db->prepare("
SELECT * FROM tasks
WHERE user_id = ? AND (daily_plan_id = ? " . ($selectedDate === $today ? "OR daily_plan_id IS NULL" : "") . ")
ORDER BY is_completed ASC, priority ASC, time_block_start ASC, created_at DESC
");
$stmtTasks->execute([$userId, $dailyPlan['id']]);
$tasks = $stmtTasks->fetchAll();
// 4. Haal weekstatistieken voor taken op (aantal per dag)
$stmtWeekTasks = $db->prepare("
SELECT dp.plan_date, COUNT(t.id) as total_tasks, SUM(CASE WHEN t.is_completed = 1 THEN 1 ELSE 0 END) as completed_tasks
FROM daily_plans dp
LEFT JOIN tasks t ON t.daily_plan_id = dp.id AND t.user_id = ?
WHERE dp.user_id = ? AND dp.plan_date BETWEEN ? AND ?
GROUP BY dp.plan_date
");
$stmtWeekTasks->execute([$userId, $userId, $weekStart, $weekEnd]);
$weekTaskStats = [];
foreach ($stmtWeekTasks->fetchAll() as $row) {
$weekTaskStats[$row['plan_date']] = [
'total' => (int)$row['total_tasks'],
'completed' => (int)$row['completed_tasks'],
];
}
// Groepeer Outlook afspraken per datum
$outlookEventsByDate = [];
foreach ($weekOutlookEvents as $evt) {
$evtDate = $evt['date'] ?? $selectedDate;
$outlookEventsByDate[$evtDate][] = $evt;
}
// 7 Dagen van de week samenstellen voor de weekselector
$weekDays = [];
for ($i = 0; $i < 7; $i++) {
$dayTs = strtotime("+{$i} days", $mondayTs);
$dayDate = date('Y-m-d', $dayTs);
$dayN = (int)date('N', $dayTs);
$dayM = (int)date('n', $dayTs);
$tStat = $weekTaskStats[$dayDate] ?? ['total' => 0, 'completed' => 0];
$mCount = count($outlookEventsByDate[$dayDate] ?? []);
$weekDays[] = [
'date' => $dayDate,
'day_num' => date('j', $dayTs),
'short_name' => $dutchShortDays[$dayN],
'full_name' => $dutchFullDays[$dayN],
'month_short' => $dutchShortMonths[$dayM],
'is_today' => ($dayDate === $today),
'is_selected' => ($dayDate === $selectedDate),
'task_count' => $tStat['total'],
'completed_count' => $tStat['completed'],
'meeting_count' => $mCount,
];
}
// 5. Haal Jira issues op voor de snelle import widget
$jiraData = jira_get_user_issues($userId);
$jiraIssues = $jiraData['issues'] ?? [];
$isJiraLive = !empty($jiraData['success']) && empty($jiraData['is_demo']);
// Filter Outlook events die afkomstig zijn van Fociz taken voor de geselecteerde dag om duplicaten op de tijdlijn te voorkomen
$dayOutlookEvents = $outlookEventsByDate[$selectedDate] ?? [];
$syncedTaskExternalIds = [];
foreach ($tasks as $t) {
if (!empty($t['external_id'])) {
$syncedTaskExternalIds[$t['external_id']] = true;
}
}
$displayedOutlookEvents = [];
foreach ($dayOutlookEvents as $evt) {
if (!empty($evt['id']) && isset($syncedTaskExternalIds[$evt['id']])) {
continue;
}
$displayedOutlookEvents[] = $evt;
}
// Bereken initiële status-hash voor achtergrondsynchronisatie
$dayEventsForHash = [];
foreach ($displayedOutlookEvents as $e) {
$dayEventsForHash[] = [
'id' => $e['id'] ?? '',
'start' => $e['start'] ?? '',
'end' => $e['end'] ?? '',
'subject' => $e['subject'] ?? '',
];
}
$tasksForHash = array_map(function($t) {
return [
'id' => $t['id'],
'title' => $t['title'],
'time_block_start' => $t['time_block_start'],
'time_block_end' => $t['time_block_end'],
'is_completed' => $t['is_completed'],
];
}, $tasks);
$initialSyncHash = md5(json_encode([
'events' => $dayEventsForHash,
'tasks' => $tasksForHash
]));
// 6. Tijdlijn uren & Werkdag Standaarden (Release v1.4)
$workdayStartStr = $workdaySettings['workday_start'] ?? '08:30';
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:30';
$lunchStart = $workdaySettings['lunch_start'] ?? '12:30';
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
$lunchHour = (int)explode(':', $lunchStart)[0];
$startHour = (int)explode(':', $workdayStartStr)[0];
$endHourParts = explode(':', $workdayEndStr);
$endHour = (int)$endHourParts[0];
if (!empty($endHourParts[1]) && (int)$endHourParts[1] > 0) {
$endHour++;
}
if ($startHour < 6) $startHour = 6;
if ($endHour > 23) $endHour = 23;
foreach ($displayedOutlookEvents as $e) {
if (!empty($e['start'])) {
$h = (int)explode(':', $e['start'])[0];
if ($h < $startHour && $h >= 6) $startHour = $h;
if ($h > $endHour && $h <= 23) $endHour = $h + 1;
}
}
foreach ($tasks as $t) {
if (!empty($t['time_block_start'])) {
$h = (int)explode(':', $t['time_block_start'])[0];
if ($h < $startHour && $h >= 6) $startHour = $h;
if ($h > $endHour && $h <= 23) $endHour = $h + 1;
}
}
$hours = [];
for ($h = $startHour; $h <= $endHour; $h++) {
$hours[] = sprintf('%02d:00', $h);
}
?>
<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 class="space-y-6 max-w-7xl mx-auto">
<!-- Top Banner: Datum, Hoofddoel & Integraties Status -->
<div class="bg-white dark:bg-slate-900/90 border border-slate-200/80 dark:border-slate-800/60 rounded-2xl p-6 sm:p-7 shadow-xs backdrop-blur-sm transition">
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div>
<div class="flex items-center space-x-2 text-indigo-600 dark:text-indigo-400 text-xs font-semibold uppercase tracking-wider mb-1">
<i data-lucide="calendar" class="w-4 h-4"></i>
<span><?= htmlspecialchars($formattedSelectedDate) ?></span>
<?php if ($isTodaySelected): ?>
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300">VANDAAG</span>
<?php else: ?>
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400">GEKOZEN DAG</span>
<?php endif; ?>
</div>
<h1 class="text-2xl sm:text-3xl font-black text-slate-900 dark:text-white tracking-tight">
Dagplanning & Structuur
</h1>
</div>
<!-- Integratie Status Badges -->
<div class="flex flex-wrap items-center gap-2">
<?php if ($isOutlookConnected): ?>
<div id="outlookSyncBadge" class="flex items-center space-x-2 px-3 py-1.5 rounded-xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200/80 dark:border-sky-500/20 text-sky-800 dark:text-sky-300 text-xs font-medium shadow-2xs transition">
<span id="outlookSyncDot" class="w-2 h-2 rounded-full bg-sky-500"></span>
<span id="outlookSyncStatusText">Outlook 365 Live</span>
<span id="outlookLastSyncText" class="text-[10px] text-sky-600/70 dark:text-sky-400/60 font-mono hidden sm:inline">(synced <span id="syncTimeDisplay"><?= date('H:i') ?></span>)</span>
<button id="manualSyncBtn" onclick="triggerManualSync()" title="Nu handmatig synchroniseren met Outlook" class="p-0.5 hover:bg-sky-100 dark:hover:bg-sky-500/20 text-sky-600 hover:text-sky-900 dark:text-sky-400 dark:hover:text-white rounded transition cursor-pointer">
<i id="manualSyncIcon" data-lucide="refresh-cw" class="w-3 h-3"></i>
</button>
</div>
<?php else: ?>
<a href="/profile.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white text-xs font-medium transition shadow-2xs">
<i data-lucide="link-2" class="w-3.5 h-3.5 text-sky-500"></i>
<span>Koppel Outlook</span>
</a>
<?php endif; ?>
<?php if ($isJiraLive): ?>
<a href="#jiraPanel" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-100 dark:bg-slate-800/70 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white text-xs font-medium transition shadow-2xs">
<span class="w-2 h-2 rounded-full bg-indigo-500 animate-pulse"></span>
<span>Jira: <?= count($jiraIssues) ?> tickets</span>
</a>
<?php else: ?>
<a href="/profile.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white text-xs font-medium transition shadow-2xs">
<i data-lucide="trello" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Koppel Jira</span>
</a>
<?php endif; ?>
</div>
</div>
<!-- Dagdoel invoerveld -->
<div class="mt-5 pt-4 border-t border-slate-100 dark:border-slate-800/60">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1.5 flex items-center space-x-1.5">
<i data-lucide="target" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<span>Mijn Belangrijkste Dagdoel voor <?= htmlspecialchars($dutchFullDays[(int)date('N', $selectedTs)]) ?></span>
</label>
<div class="flex items-center space-x-2">
<input
type="text"
id="focusGoalInput"
value="<?= htmlspecialchars($dailyPlan['focus_goal'] ?? '') ?>"
placeholder="Wat moet er deze dag absoluut gebeuren voor succes?"
class="flex-1 bg-slate-50 dark:bg-slate-950/60 border border-slate-200 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/30 focus:border-indigo-500 font-medium transition"
>
<button
onclick="saveGoal()"
id="saveGoalBtn"
class="px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold text-xs rounded-xl transition flex items-center space-x-1 shadow-sm shadow-indigo-600/20 flex-shrink-0 cursor-pointer"
>
<i data-lucide="check-circle-2" class="w-4 h-4"></i>
<span>Opslaan</span>
</button>
</div>
</div>
</div>
<!-- Week Navigatie & Dagselector -->
<div class="bg-white dark:bg-slate-900/90 border border-slate-200/80 dark:border-slate-800/60 rounded-2xl p-5 sm:p-6 shadow-xs backdrop-blur-sm transition">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-4 mb-4 border-b border-slate-100 dark:border-slate-800/60">
<!-- Navigatie controls -->
<div class="flex items-center space-x-2">
<a href="planner.php?date=<?= $prevWeekDate ?>" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition shadow-2xs" title="Vorige week (Maandag <?= date('j M', strtotime($prevWeekDate)) ?>)">
<i data-lucide="chevron-left" class="w-4 h-4"></i>
<span class="hidden sm:inline">Vorige week</span>
</a>
<a href="planner.php?date=<?= $today ?>" class="px-3 py-1.5 rounded-xl <?= $isTodaySelected ? 'bg-slate-900 text-white dark:bg-white dark:text-slate-900 font-bold' : '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 shadow-2xs" title="Spring direct naar vandaag">
Vandaag
</a>
<a href="planner.php?date=<?= $nextWeekDate ?>" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition shadow-2xs" title="Volgende week (Maandag <?= date('j M', strtotime($nextWeekDate)) ?>)">
<span class="hidden sm:inline">Volgende week</span>
<i data-lucide="chevron-right" class="w-4 h-4"></i>
</a>
</div>
<!-- Week Titel & Datumrange -->
<div class="text-center md:text-left">
<div class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400">
Week <?= $weekNumber ?> &bull; <?= $yearNumber ?>
</div>
<div class="text-base font-black text-slate-900 dark:text-white">
<?= htmlspecialchars($formattedWeekRange) ?>
</div>
</div>
<!-- Directe datumselector -->
<div class="flex items-center space-x-2 justify-end">
<label for="jumpDateInput" class="text-xs text-slate-500 dark:text-slate-400 font-medium hidden sm:inline">Datum:</label>
<input
type="date"
id="jumpDateInput"
value="<?= htmlspecialchars($selectedDate) ?>"
onchange="if(this.value) window.location.href='planner.php?date=' + this.value;"
class="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 font-medium cursor-pointer"
>
</div>
</div>
<!-- 7 Dagen Weekstrook -->
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-7 gap-2.5">
<?php foreach ($weekDays as $wd): ?>
<a
href="planner.php?date=<?= $wd['date'] ?>"
class="group relative flex flex-col p-3 rounded-xl border transition text-left <?= $wd['is_selected']
? 'bg-indigo-50/80 dark:bg-indigo-500/10 border-indigo-500/40 ring-1 ring-indigo-500/30 shadow-xs'
: 'bg-slate-50/70 dark:bg-slate-950/40 border-slate-200/80 dark:border-slate-800/60 hover:border-slate-300 dark:hover:border-slate-700 hover:bg-white dark:hover:bg-slate-900/60 shadow-2xs' ?>"
>
<div class="flex items-center justify-between mb-1.5">
<span class="text-[11px] font-semibold uppercase tracking-wider <?= $wd['is_selected'] ? 'text-indigo-700 dark:text-indigo-400 font-bold' : 'text-slate-500 dark:text-slate-400' ?>">
<?= $wd['short_name'] ?>
</span>
<?php if ($wd['is_today']): ?>
<span class="px-1.5 py-0.5 rounded-full text-[9px] font-bold uppercase bg-indigo-600 text-white">
Nu
</span>
<?php endif; ?>
</div>
<div class="flex items-baseline space-x-1 mb-2">
<span class="text-xl font-black <?= $wd['is_selected'] ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-900 dark:text-white' ?>">
<?= $wd['day_num'] ?>
</span>
<span class="text-[10px] font-medium text-slate-400 dark:text-slate-500">
<?= $wd['month_short'] ?>
</span>
</div>
<!-- Indicatoren 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-[10px]">
<div class="flex items-center justify-between text-slate-600 dark:text-slate-400">
<span class="flex items-center space-x-1">
<i data-lucide="check-square" class="w-3 h-3 text-slate-400 dark:text-slate-500"></i>
<span>Taken</span>
</span>
<span class="font-semibold <?= $wd['task_count'] > 0 ? ($wd['completed_count'] === $wd['task_count'] ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-700 dark:text-slate-300') : 'text-slate-400' ?>">
<?= $wd['completed_count'] ?>/<?= $wd['task_count'] ?>
</span>
</div>
<div class="flex items-center justify-between text-slate-600 dark:text-slate-400">
<span class="flex items-center space-x-1">
<i data-lucide="calendar" class="w-3 h-3 text-sky-500"></i>
<span>Agenda</span>
</span>
<span class="font-semibold <?= $wd['meeting_count'] > 0 ? 'text-sky-600 dark:text-sky-400' : 'text-slate-400' ?>">
<?= $wd['meeting_count'] ?>
</span>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Main Grid: Timer + Takenlijst + Jira (Links) & Timeblocking (Rechts) -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Linker kolom: Focus Timer & Taken -->
<div class="lg:col-span-7 space-y-6 min-w-0">
<!-- Focus Modus Timer -->
<div class="bg-white dark:bg-slate-900/90 border border-slate-200/80 dark:border-slate-800/60 rounded-2xl p-6 shadow-xs relative overflow-hidden backdrop-blur-sm transition">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-2">
<div class="w-8 h-8 rounded-xl bg-amber-50 dark:bg-amber-500/10 text-amber-500 flex items-center justify-center font-bold">
<i data-lucide="flame" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white uppercase tracking-wider">Focus Modus</h3>
<p class="text-[11px] text-slate-500 dark:text-slate-400">Pomodoro & geconcentreerd werken</p>
</div>
</div>
<div class="flex bg-slate-100 dark:bg-slate-950/80 p-1 rounded-xl border border-slate-200/80 dark:border-slate-800 text-xs">
<button onclick="setTimerPreset(<?= (int)$workdaySettings['pomodoro_focus'] ?>)" id="btnPresetFocus" class="px-2.5 py-1 rounded-lg font-semibold bg-indigo-600 text-white shadow-xs transition"><?= (int)$workdaySettings['pomodoro_focus'] ?>m Focus</button>
<button onclick="setTimerPreset(<?= (int)$workdaySettings['pomodoro_deep'] ?>)" id="btnPresetDeep" class="px-2.5 py-1 rounded-lg font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 transition"><?= (int)$workdaySettings['pomodoro_deep'] ?>m Deep</button>
<button onclick="setTimerPreset(<?= (int)$workdaySettings['pomodoro_break'] ?>)" id="btnPresetBreak" class="px-2.5 py-1 rounded-lg font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 transition"><?= (int)$workdaySettings['pomodoro_break'] ?>m Pauze</button>
</div>
</div>
<div class="text-center my-6">
<div id="timerDisplay" class="text-5xl sm:text-6xl font-black font-mono tracking-tight text-slate-900 dark:text-white mb-2"><?= sprintf('%02d:00', (int)$workdaySettings['pomodoro_focus']) ?></div>
<div class="min-h-[22px] text-xs text-slate-500 dark:text-slate-400 truncate max-w-sm mx-auto flex items-center justify-center space-x-1">
<span class="text-indigo-600 dark:text-indigo-400 font-medium">Actieve focus:</span>
<span id="activeTaskDisplay" class="text-slate-800 dark:text-slate-200 font-semibold">Kies een taak hieronder</span>
</div>
<div class="w-full bg-slate-100 dark:bg-slate-950/80 h-1.5 rounded-full overflow-hidden mt-4 border border-slate-200/80 dark:border-slate-800/80">
<div id="timerProgress" class="h-full bg-gradient-to-r from-indigo-600 to-sky-400 transition-all duration-300 rounded-full" style="width: 0%"></div>
</div>
</div>
<div class="flex items-center justify-center space-x-3">
<button id="timerToggleBtn" onclick="toggleTimer()" class="flex items-center space-x-2 px-6 py-2.5 rounded-xl font-semibold text-sm bg-indigo-600 hover:bg-indigo-500 text-white transition shadow-sm shadow-indigo-600/20 cursor-pointer">
<i data-lucide="play" class="w-4 h-4 fill-current"></i>
<span id="timerToggleText">Start Focus</span>
</button>
<button onclick="resetTimer()" class="p-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 transition border border-slate-200 dark:border-slate-700/60 cursor-pointer" title="Reset">
<i data-lucide="rotate-ccw" class="w-4 h-4"></i>
</button>
</div>
</div>
<!-- Takenlijst -->
<div class="bg-white dark:bg-slate-900/90 border border-slate-200/80 dark:border-slate-800/60 rounded-2xl p-6 shadow-xs backdrop-blur-sm transition">
<div class="flex items-center justify-between mb-4 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">
<span>Dagtaken & Prioriteiten</span>
<span class="text-xs font-normal text-slate-500 dark:text-slate-400">(<?= count($tasks) ?>)</span>
</h3>
</div>
<button onclick="toggleAddTaskForm()" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-semibold transition shadow-sm shadow-indigo-600/20 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-4 p-4 rounded-xl bg-slate-50 dark:bg-slate-950/80 border border-indigo-500/30 space-y-3 transition">
<div>
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Taaktitel</label>
<input type="text" id="newTaskTitle" placeholder="Wat moet er vandaag gedaan worden?" class="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-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">
</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-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-1.5 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-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl pl-2.5 pr-7 py-1.5 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 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-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl pl-2.5 pr-7 py-1.5 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 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-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-1.5 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-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-2 py-1.5 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-2.5 rounded-xl bg-sky-50/70 dark:bg-sky-950/30 border border-sky-200/70 dark:border-sky-800/40">
<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-medium">
<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-4 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg text-xs transition cursor-pointer">Opslaan</button>
<button onclick="toggleAddTaskForm()" class="px-3 py-1.5 bg-slate-200 hover:bg-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-400 rounded-lg text-xs transition cursor-pointer">Annuleren</button>
</div>
</div>
<!-- Taken weergave -->
<div class="space-y-2" id="tasksContainer">
<?php if (empty($tasks)): ?>
<div class="text-center py-8 text-slate-400 dark:text-slate-500 text-xs">
Nog geen taken gepland voor <?= htmlspecialchars($formattedSelectedDate) ?>. Klik op 'Nieuwe Taak' of importeer een Jira ticket hieronder.
</div>
<?php else: ?>
<?php foreach ($tasks as $task): ?>
<div class="group flex items-center justify-between p-3 rounded-xl border <?= $task['is_completed'] ? 'bg-slate-50/50 dark:bg-slate-950/30 border-slate-200 dark:border-slate-900 opacity-60' : 'bg-white dark:bg-slate-950/40 border-slate-200/80 dark:border-slate-800/70 hover:border-slate-300 dark:hover:border-slate-700' ?> transition shadow-2xs" id="task-row-<?= $task['id'] ?>">
<div class="flex items-center space-x-3 truncate mr-2">
<button onclick="toggleTask(<?= $task['id'] ?>, <?= $task['is_completed'] ? 0 : 1 ?>)" class="w-5 h-5 rounded-lg border flex items-center justify-center transition <?= $task['is_completed'] ? 'bg-indigo-600 border-indigo-600 text-white font-bold' : 'border-slate-300 dark:border-slate-700 hover:border-indigo-500 bg-white dark:bg-slate-900' ?>">
<?php if ($task['is_completed']): ?>
<i data-lucide="check" class="w-3.5 h-3.5 stroke-[3]"></i>
<?php endif; ?>
</button>
<div class="truncate">
<div class="flex items-center space-x-1.5 truncate">
<?php if ($task['source'] === 'JIRA'): ?>
<span class="px-1.5 py-0.2 rounded bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-700 dark:text-indigo-400 font-mono text-[9px] font-bold">
JIRA
</span>
<?php elseif ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])): ?>
<span class="px-1.5 py-0.2 rounded bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-700 dark:text-sky-400 font-mono text-[9px] font-bold flex items-center space-x-0.5">
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
<span>OUTLOOK</span>
</span>
<?php endif; ?>
<span class="text-xs font-medium truncate <?= $task['is_completed'] ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-800 dark:text-slate-200' ?>">
<?= htmlspecialchars($task['title']) ?>
</span>
</div>
<div class="flex items-center space-x-2 mt-0.5">
<?php if (!empty($task['time_block_start'])): ?>
<span class="text-[10px] text-slate-500 dark:text-slate-400 font-mono flex items-center space-x-1">
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
<span><?= htmlspecialchars($task['time_block_start']) ?><?= !empty($task['time_block_end']) ? ' - ' . htmlspecialchars($task['time_block_end']) : '' ?><?= !empty($task['duration_minutes']) ? ' (' . (int)$task['duration_minutes'] . 'm)' : '' ?></span>
</span>
<?php endif; ?>
<?php if (!empty($task['external_url'])): ?>
<a href="<?= htmlspecialchars($task['external_url']) ?>" target="_blank" class="text-[10px] <?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'text-sky-600 dark:text-sky-400' : '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' : 'Open in Jira' ?></span>
<i data-lucide="external-link" class="w-2.5 h-2.5"></i>
</a>
<?php endif; ?>
</div>
</div>
</div>
<div class="flex items-center space-x-2 flex-shrink-0">
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full <?= $task['priority'] === 'P1' ? 'bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20' : ($task['priority'] === 'P2' ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700') ?>">
<?= htmlspecialchars($task['priority']) ?>
</span>
<?php if ($isOutlookConnected && empty($task['external_id']) && !$task['is_completed']): ?>
<button onclick="syncTaskToOutlook(<?= $task['id'] ?>, this)" title="Synchroniseer deze taak direct naar Outlook Agenda" class="flex items-center space-x-1 text-[10px] font-semibold px-2 py-0.5 rounded-lg bg-sky-50 hover:bg-sky-100 dark:bg-sky-500/10 dark:hover:bg-sky-500/20 text-sky-800 dark:text-sky-300 border border-sky-200 dark:border-sky-500/20 transition cursor-pointer">
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
<span class="hidden sm:inline">Naar Outlook</span>
</button>
<?php endif; ?>
<?php if (!$task['is_completed']): ?>
<button onclick="selectFocusTask('<?= htmlspecialchars(addslashes($task['title'])) ?>')" title="Koppel aan Focus Timer" class="p-1.5 rounded-lg text-slate-400 hover:text-amber-500 hover:bg-amber-500/10 transition cursor-pointer">
<i data-lucide="flame" class="w-3.5 h-3.5"></i>
</button>
<?php endif; ?>
<button onclick="openEditTaskModal(<?= htmlspecialchars(json_encode([
'id' => (int)$task['id'],
'title' => $task['title'],
'priority' => $task['priority'],
'start' => $task['time_block_start'] ?? '',
'end' => $task['time_block_end'] ?? '',
'duration' => (int)($task['duration_minutes'] ?: 30),
'source' => $task['source'],
'external_id' => $task['external_id'] ?? ''
]), ENT_QUOTES, 'UTF-8') ?>)" title="Taak & Tijd aanpassen" class="p-1.5 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-500/10 opacity-0 group-hover:opacity-100 transition cursor-pointer">
<i data-lucide="edit-3" class="w-3.5 h-3.5"></i>
</button>
<button onclick="deleteTask(<?= $task['id'] ?>)" title="Verwijderen" class="p-1.5 rounded-lg text-slate-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-500/10 opacity-0 group-hover:opacity-100 transition cursor-pointer">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<!-- Jira Snelle Import Widget -->
<div id="jiraPanel" class="bg-white dark:bg-[#111622] border border-slate-200/80 dark:border-slate-800 rounded-2xl p-6 shadow-xs backdrop-blur-sm transition">
<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">
<i data-lucide="trello" class="w-4 h-4 text-sky-500"></i>
<h3 class="text-xs font-semibold text-slate-900 dark:text-white uppercase tracking-wider">
Mijn Jira Tickets
</h3>
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold <?= $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($jiraIssues) ?>
</span>
</div>
<a href="/profile.php" class="text-xs text-slate-500 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition flex items-center space-x-1">
<span>Beheren</span>
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
</a>
</div>
<?php if (empty($jiraIssues)): ?>
<div class="text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
Geen openstaande tickets gevonden in Jira.
</div>
<?php else: ?>
<div class="space-y-2 max-h-64 overflow-y-auto pr-1">
<?php foreach (array_slice($jiraIssues, 0, 6) as $issue): ?>
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-slate-300 dark:hover:border-slate-700 transition shadow-xs">
<div class="truncate mr-2">
<div class="flex items-center space-x-2">
<span class="font-mono text-[10px] font-bold text-sky-700 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-1.5 py-0.5 rounded">
<?= htmlspecialchars($issue['key']) ?>
</span>
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
<?= htmlspecialchars($issue['summary']) ?>
</span>
</div>
<span class="text-[10px] text-slate-500 mt-0.5 block">
<?= htmlspecialchars($issue['status']) ?> &bull; <?= htmlspecialchars($issue['priority']) ?>
</span>
</div>
<button
onclick="quickImportJira(this, '<?= htmlspecialchars(addslashes($issue['summary'])) ?>', '<?= htmlspecialchars($issue['key']) ?>', '<?= htmlspecialchars(addslashes($issue['url'] ?? '')) ?>')"
class="flex items-center space-x-1 text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20 px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
title="Plan in als dagtaak"
>
<i data-lucide="plus" class="w-3 h-3"></i>
<span>Plan</span>
</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Dagnotities & Reflectie -->
<div class="bg-white dark:bg-[#111622] border border-slate-200/80 dark:border-slate-800 rounded-2xl p-6 shadow-xs backdrop-blur-sm transition">
<div class="flex items-center space-x-2 mb-3">
<i data-lucide="book-open" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<h3 class="text-xs font-semibold text-slate-900 dark:text-white uppercase tracking-wider">Dagnotities & Kladblok</h3>
</div>
<textarea
id="dailyNotesInput"
rows="3"
placeholder="Belangrijke aantekeningen, reflecties of snelle memo's voor vandaag..."
onblur="saveGoal()"
class="w-full bg-slate-50/70 dark:bg-slate-900/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"
><?= htmlspecialchars($dailyPlan['notes'] ?? '') ?></textarea>
</div>
</div>
<!-- Rechter kolom: Timeblocking & Outlook Tijdlijn -->
<div class="lg:col-span-5 space-y-6 min-w-0">
<div class="bg-white dark:bg-[#111622] border border-slate-200/80 dark:border-slate-800 rounded-2xl p-5 sm:p-6 shadow-xs backdrop-blur-sm transition overflow-hidden">
<div class="flex items-center justify-between mb-4 pb-3 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2">
<i data-lucide="clock" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<h3 class="text-xs font-semibold text-slate-900 dark:text-white uppercase tracking-wider">Tijdlijn & Time-blocking</h3>
</div>
<div class="flex items-center space-x-2">
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
<button onclick="aiSmartSchedule(this)" id="aiScheduleBtn" title="Laat AI Co-Pilot openstaande taken optimaal inplannen om meetings en lunch heen" class="flex items-center space-x-1.5 px-3 py-1 rounded-lg bg-gradient-to-r from-indigo-600 to-sky-600 hover:from-indigo-500 hover:to-sky-500 text-white text-xs font-semibold shadow-xs transition cursor-pointer">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
<span class="hidden sm:inline">AI Slim Indelen</span>
</button>
<?php endif; ?>
<?php if ($isOutlookConnected): ?>
<span class="px-2 py-0.5 rounded bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-700 dark:text-sky-400 text-[10px] font-semibold">Outlook 365</span>
<?php endif; ?>
<span class="text-xs text-slate-500 dark:text-slate-400 font-mono"><?= $hours[0] ?> – <?= end($hours) ?></span>
</div>
</div>
<div class="space-y-2.5">
<?php foreach ($hours as $hour):
$hourInt = (int)explode(':', $hour)[0];
$isLunchSlot = ($hourInt === $lunchHour && !empty($lunchStart) && !empty($lunchEnd));
// Zoek Outlook meetings die binnen dit uur vallen (gefilterd op Fociz duplicaten)
$slotEvents = array_filter($displayedOutlookEvents, function($e) use ($hourInt) {
if (empty($e['start'])) return false;
return (int)explode(':', $e['start'])[0] === $hourInt;
});
// Zoek taken gepland in dit uur
$slotTasks = array_filter($tasks, function($t) use ($hourInt) {
return !empty($t['time_block_start']) && (int)explode(':', $t['time_block_start'])[0] === $hourInt;
});
?>
<div class="flex items-start group min-h-[48px] border-b border-slate-100 dark:border-slate-800/40 pb-2.5 last:border-0 min-w-0">
<div class="w-12 sm:w-14 text-xs font-mono font-medium text-slate-500 dark:text-slate-400 pt-1 flex-shrink-0">
<?= $hour ?>
</div>
<div class="flex-1 space-y-1.5 min-w-0">
<?php if ($isLunchSlot): ?>
<!-- Automatische Lunchpauze Reservering (Release v1.4) -->
<div class="p-2.5 rounded-xl bg-amber-50/80 dark:bg-amber-950/25 border border-amber-200/80 dark:border-amber-800/40 text-amber-950 dark:text-amber-200 text-xs shadow-xs flex items-center justify-between min-w-0">
<div class="flex items-center space-x-2.5 min-w-0">
<span class="px-2 py-0.5 rounded-md bg-amber-500/15 text-amber-800 dark:text-amber-300 font-mono text-[10px] font-bold flex-shrink-0">
<?= htmlspecialchars($lunchStart) ?> - <?= htmlspecialchars($lunchEnd) ?>
</span>
<span class="font-semibold text-xs flex items-center space-x-1.5 truncate">
<i data-lucide="coffee" class="w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0"></i>
<span class="truncate">Lunchpauze (Gereserveerd)</span>
</span>
</div>
<span class="text-[10px] font-semibold text-amber-700 dark:text-amber-400 flex-shrink-0 ml-2">Pauze</span>
</div>
<?php endif; ?>
<!-- Outlook meetings -->
<?php foreach ($slotEvents as $evt): ?>
<div class="p-2.5 rounded-xl bg-sky-50/80 dark:bg-sky-950/30 border border-sky-200/80 dark:border-sky-800/40 text-sky-950 dark:text-sky-100 text-xs shadow-xs min-w-0 overflow-hidden">
<div class="flex items-start justify-between gap-2.5 min-w-0">
<div class="flex items-start space-x-2 min-w-0 flex-1">
<span class="px-2 py-0.5 rounded-md bg-sky-500/15 text-sky-800 dark:text-sky-300 font-mono text-[10px] font-bold flex-shrink-0 whitespace-nowrap mt-0.5">
<?= htmlspecialchars($evt['start']) ?> - <?= htmlspecialchars($evt['end']) ?>
</span>
<span class="font-semibold text-xs leading-snug break-words line-clamp-2 min-w-0 flex-1" title="<?= htmlspecialchars($evt['subject']) ?>">
<?= htmlspecialchars($evt['subject']) ?>
</span>
</div>
<div class="flex items-center space-x-1 flex-shrink-0">
<?php if (!empty($evt['meeting_url'])): ?>
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-white bg-indigo-600 hover:bg-indigo-700 px-2.5 py-1 rounded-lg flex-shrink-0 transition font-semibold shadow-xs">
<i data-lucide="video" class="w-3 h-3"></i>
<span class="hidden sm:inline">Deelnemen</span>
</a>
<?php elseif (!empty($evt['web_link'])): ?>
<a href="<?= htmlspecialchars($evt['web_link']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-sky-700 dark:text-sky-300 hover:underline px-1 py-0.5 transition font-medium">
<i data-lucide="external-link" class="w-3 h-3"></i>
<span class="hidden sm:inline">Outlook</span>
</a>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<!-- Geplande taken -->
<?php foreach ($slotTasks as $t): ?>
<div class="p-2.5 rounded-xl border text-xs min-w-0 overflow-hidden transition <?= $t['is_completed'] ? 'bg-slate-50 dark:bg-slate-900/40 border-slate-200 dark:border-slate-800 text-slate-400 dark:text-slate-500 line-through' : 'bg-white dark:bg-[#111622] border-slate-200/80 dark:border-slate-800 text-slate-900 dark:text-slate-100 shadow-xs' ?>">
<div class="flex items-start justify-between gap-2.5 min-w-0">
<div class="flex items-start space-x-2 min-w-0 flex-1">
<span class="px-2 py-0.5 rounded-md bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-500/20 font-mono text-[10px] font-bold flex-shrink-0 whitespace-nowrap mt-0.5">
<?= htmlspecialchars($t['time_block_start']) ?><?= !empty($t['time_block_end']) ? ' - ' . htmlspecialchars($t['time_block_end']) : '' ?>
</span>
<span class="font-semibold text-xs leading-snug break-words line-clamp-2 min-w-0 flex-1" title="<?= htmlspecialchars($t['title']) ?>">
<?= htmlspecialchars($t['title']) ?>
</span>
</div>
<div class="flex items-center space-x-1.5 flex-shrink-0">
<?php if (!empty($t['external_id']) && $t['source'] === 'OUTLOOK'): ?>
<span class="text-[9px] font-semibold px-2 py-0.5 rounded bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-500/25 flex items-center space-x-1 whitespace-nowrap" title="In Outlook agenda">
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
<span class="hidden sm:inline">In Outlook</span>
</span>
<?php elseif ($isOutlookConnected && !$t['is_completed']): ?>
<button onclick="syncTaskToOutlook(<?= $t['id'] ?>, this)" title="Synchroniseer naar Outlook Agenda" class="text-[9px] font-semibold px-2 py-0.5 rounded-lg bg-sky-50 hover:bg-sky-100 dark:bg-sky-500/10 dark:hover:bg-sky-500/20 text-sky-800 dark:text-sky-300 border border-sky-200 dark:border-sky-500/20 flex items-center space-x-1 transition cursor-pointer whitespace-nowrap">
<i data-lucide="calendar-plus" class="w-2.5 h-2.5"></i>
<span class="hidden sm:inline">Naar Outlook</span>
</button>
<?php endif; ?>
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full <?= $t['priority'] === 'P1' ? 'bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20' : ($t['priority'] === 'P2' ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700') ?>">
<?= $t['priority'] ?>
</span>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($slotEvents) && empty($slotTasks) && !$isLunchSlot): ?>
<button onclick="quickSchedule('<?= $hour ?>')" class="w-full text-left py-1.5 px-2.5 rounded-xl border border-dashed border-slate-200 dark:border-slate-800 hover:border-indigo-300 dark:hover:border-indigo-700/60 hover:bg-indigo-50/40 dark:hover:bg-indigo-950/20 text-slate-400 dark:text-slate-500 hover:text-indigo-600 dark:hover:text-indigo-400 text-xs transition cursor-pointer">
+ Plan taak om <?= $hour ?>
</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<!-- Modal: Taak & Tijdblok Bewerken (Vrij invullen van tijden & duur) -->
<div id="editTaskModal" class="fixed inset-0 z-50 bg-slate-900/60 backdrop-blur-xs hidden items-center justify-center p-4">
<div class="bg-white dark:bg-[#111622] border border-slate-200/90 dark:border-slate-800 rounded-2xl p-6 w-full max-w-lg shadow-2xl space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
<i data-lucide="edit-3" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Taak & Tijdblok Bewerken</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400">Pas titel, start-/eindtijd en tijdsduur vrij aan</p>
</div>
</div>
<button onclick="closeEditTaskModal()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<input type="hidden" id="editTaskId">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Taaktitel</label>
<input type="text" id="editTaskTitle" class="w-full bg-white dark:bg-slate-950 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">
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
<select id="editTaskPriority" class="w-full bg-white dark:bg-slate-950 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">P2 - Belangrijk (Standaard)</option>
<option value="P3">P3 - Regulier / Lage druk</option>
</select>
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Tijdsduur</label>
<span id="editTaskDurationDisplay" class="text-[11px] font-mono font-bold text-indigo-600 dark:text-indigo-400">30m</span>
</div>
<div class="flex items-center space-x-1.5">
<select id="editTaskDuration" class="w-full bg-white dark:bg-slate-950 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="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-white dark:bg-slate-950 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="onEditTaskTimeOrDurationChange('custom_duration')">
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd</label>
<div class="relative">
<input type="text" id="editTaskStartTime" placeholder="Bijv. 09:30" class="w-full bg-white dark:bg-slate-950 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" oninput="onEditTaskTimeOrDurationChange('start')">
<i data-lucide="clock" class="w-4 h-4 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
</div>
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Eindtijd</label>
<span class="text-[9px] text-indigo-600 dark:text-indigo-400 font-medium">Vrij invulbaar</span>
</div>
<div class="relative">
<input type="text" id="editTaskEndTime" placeholder="Bijv. 09:40" class="w-full bg-white dark:bg-slate-950 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" 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-2.5 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>
<script>
// --- Focus Timer Logica (Release v1.4) ---
const pomodoroFocusTime = <?= (int)$workdaySettings['pomodoro_focus'] ?>;
const pomodoroDeepTime = <?= (int)$workdaySettings['pomodoro_deep'] ?>;
const pomodoroBreakTime = <?= (int)$workdaySettings['pomodoro_break'] ?>;
let timerInterval = null;
let timerSeconds = pomodoroFocusTime * 60;
let totalDuration = pomodoroFocusTime * 60;
let isRunning = false;
function updateTimerDisplay() {
const min = Math.floor(timerSeconds / 60);
const sec = timerSeconds % 60;
document.getElementById('timerDisplay').innerText =
`${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
const progress = ((totalDuration - timerSeconds) / totalDuration) * 100;
document.getElementById('timerProgress').style.width = `${progress}%`;
}
function setTimerPreset(minutes) {
clearInterval(timerInterval);
isRunning = false;
timerSeconds = minutes * 60;
totalDuration = minutes * 60;
updateTimerDisplay();
document.getElementById('timerToggleText').innerText = 'Start Focus';
const inactiveClass = 'px-2.5 py-1 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 transition';
const activeClass = 'px-2.5 py-1 rounded-lg font-semibold bg-indigo-600 text-white shadow-xs transition';
if (document.getElementById('btnPresetFocus')) document.getElementById('btnPresetFocus').className = minutes === pomodoroFocusTime ? activeClass : inactiveClass;
if (document.getElementById('btnPresetDeep')) document.getElementById('btnPresetDeep').className = minutes === pomodoroDeepTime ? activeClass : inactiveClass;
if (document.getElementById('btnPresetBreak')) document.getElementById('btnPresetBreak').className = minutes === pomodoroBreakTime ? activeClass : inactiveClass;
}
function toggleTimer() {
if (isRunning) {
clearInterval(timerInterval);
isRunning = false;
document.getElementById('timerToggleText').innerText = 'Hervatten';
saveTimerStateBeforeReload();
} else {
isRunning = true;
document.getElementById('timerToggleText').innerText = 'Pauzeren';
timerInterval = setInterval(() => {
if (timerSeconds > 0) {
timerSeconds--;
updateTimerDisplay();
} else {
clearInterval(timerInterval);
isRunning = false;
sessionStorage.removeItem('fociz_timer_state');
document.getElementById('timerToggleText').innerText = 'Start Focus';
alert('Focus sessie voltooid! Neem even een korte pauze.');
}
}, 1000);
}
}
function resetTimer() {
clearInterval(timerInterval);
isRunning = false;
timerSeconds = totalDuration;
sessionStorage.removeItem('fociz_timer_state');
document.getElementById('timerToggleText').innerText = 'Start Focus';
updateTimerDisplay();
}
function saveTimerStateBeforeReload() {
try {
if (isRunning || timerSeconds < totalDuration) {
const state = {
isRunning: isRunning,
timerSeconds: timerSeconds,
totalDuration: totalDuration,
timestamp: Date.now()
};
sessionStorage.setItem('fociz_timer_state', JSON.stringify(state));
}
} catch (e) {
console.error('Kon timer status niet opslaan:', e);
}
}
function restoreTimerStateIfAny() {
try {
const raw = sessionStorage.getItem('fociz_timer_state');
if (!raw) return;
sessionStorage.removeItem('fociz_timer_state');
const state = JSON.parse(raw);
if (state && typeof state.timerSeconds === 'number') {
totalDuration = state.totalDuration || (pomodoroFocusTime * 60);
let remaining = state.timerSeconds;
if (state.isRunning && state.timestamp) {
const elapsed = Math.floor((Date.now() - state.timestamp) / 1000);
remaining = Math.max(0, remaining - elapsed);
}
timerSeconds = remaining;
updateTimerDisplay();
if (state.isRunning && timerSeconds > 0) {
toggleTimer(); // Hervat timer
} else if (!state.isRunning && timerSeconds < totalDuration) {
const toggleBtnText = document.getElementById('timerToggleText');
if (toggleBtnText) toggleBtnText.innerText = 'Hervatten';
}
}
} catch (e) {
console.error('Kon timer status niet herstellen:', e);
}
}
window.addEventListener('beforeunload', () => {
saveTimerStateBeforeReload();
});
function selectFocusTask(title) {
document.getElementById('activeTaskDisplay').innerText = title;
}
// --- Dagdoel & Taken AJAX ---
const activePlannerDate = '<?= $selectedDate ?>';
async function saveGoal() {
const focusGoal = document.getElementById('focusGoalInput').value;
const notes = document.getElementById('dailyNotesInput').value;
const btn = document.getElementById('saveGoalBtn');
btn.innerText = 'Opslaan...';
try {
await fetch('/api/planner.php?action=save_goal', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ focus_goal: focusGoal, notes: notes, date: activePlannerDate })
});
btn.innerHTML = '<i data-lucide="check" class="w-4 h-4"></i><span>Opgeslagen</span>';
if (typeof lucide !== 'undefined') lucide.createIcons();
setTimeout(() => {
btn.innerHTML = '<i data-lucide="check-circle-2" class="w-4 h-4"></i><span>Opslaan</span>';
if (typeof lucide !== 'undefined') lucide.createIcons();
}, 1500);
} catch(err) {
btn.innerText = 'Fout';
}
}
// --- Tijdblok Hulpfuncties & Vrije Duur Berekening ---
function parseTimeToMinutes(str) {
if (!str || typeof str !== 'string') return null;
str = str.trim();
if (!str) return null;
let m = str.match(/^(\d{1,2})[:.](\d{2})$/);
if (m) {
const hours = parseInt(m[1], 10);
const mins = parseInt(m[2], 10);
if (hours >= 0 && hours < 24 && mins >= 0 && mins < 60) {
return (hours * 60) + mins;
}
}
m = str.match(/^(\d{1,2})$/);
if (m) {
const hours = parseInt(m[1], 10);
if (hours >= 0 && hours < 24) {
return hours * 60;
}
}
return null;
}
function formatMinutesToTime(totalMins) {
totalMins = (totalMins + (24 * 60)) % (24 * 60);
const h = Math.floor(totalMins / 60);
const m = totalMins % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}
// Tweeweg synchronisatie bij Nieuwe Taak (Starttijd, Eindtijd & Tijdsduur)
function onNewTaskTimeOrDurationChange(source) {
const startInput = document.getElementById('newTaskTime');
const endInput = document.getElementById('newTaskEndTime');
const durationSelect = document.getElementById('newTaskDuration');
const customDurationInput = document.getElementById('newTaskCustomDuration');
const durationDisplay = document.getElementById('newTaskDurationDisplay');
if (!startInput || !endInput || !durationSelect) return;
const startMins = parseTimeToMinutes(startInput.value);
if (source === 'end') {
const endMins = parseTimeToMinutes(endInput.value);
if (startMins !== null && endMins !== null && endMins > startMins) {
const diffMins = endMins - startMins;
setSelectedNewTaskDuration(diffMins);
}
} else if (source === 'duration') {
const val = durationSelect.value;
if (val === 'custom') {
customDurationInput.classList.remove('hidden');
customDurationInput.focus();
const customVal = parseInt(customDurationInput.value, 10);
if (!isNaN(customVal) && customVal > 0 && startMins !== null) {
endInput.value = formatMinutesToTime(startMins + customVal);
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
}
} else {
customDurationInput.classList.add('hidden');
const dur = parseInt(val, 10);
if (!isNaN(dur) && dur > 0) {
if (durationDisplay) durationDisplay.innerText = `${dur}m`;
if (startMins !== null) {
endInput.value = formatMinutesToTime(startMins + dur);
}
}
}
} else if (source === 'custom_duration') {
const customVal = parseInt(customDurationInput.value, 10);
if (!isNaN(customVal) && customVal > 0) {
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
if (startMins !== null) {
endInput.value = formatMinutesToTime(startMins + customVal);
}
}
} else if (source === 'start') {
let dur = getSelectedNewTaskDurationMinutes();
if (startMins !== null) {
if (endInput.value && parseTimeToMinutes(endInput.value) !== null) {
const currentEndMins = parseTimeToMinutes(endInput.value);
if (currentEndMins > startMins) {
setSelectedNewTaskDuration(currentEndMins - startMins);
} else if (dur > 0) {
endInput.value = formatMinutesToTime(startMins + dur);
}
} else if (dur > 0) {
endInput.value = formatMinutesToTime(startMins + dur);
}
}
}
}
function getSelectedNewTaskDurationMinutes() {
const durationSelect = document.getElementById('newTaskDuration');
const customDurationInput = document.getElementById('newTaskCustomDuration');
if (!durationSelect) return 30;
if (durationSelect.value === 'custom') {
const val = parseInt(customDurationInput?.value, 10);
return (!isNaN(val) && val > 0) ? val : 30;
}
const val = parseInt(durationSelect.value, 10);
return (!isNaN(val) && val > 0) ? val : 30;
}
function setSelectedNewTaskDuration(mins) {
const durationSelect = document.getElementById('newTaskDuration');
const customDurationInput = document.getElementById('newTaskCustomDuration');
const durationDisplay = document.getElementById('newTaskDurationDisplay');
if (!durationSelect) return;
let found = false;
for (let i = 0; i < durationSelect.options.length; i++) {
if (durationSelect.options[i].value === String(mins)) {
durationSelect.selectedIndex = i;
found = true;
break;
}
}
if (!found) {
durationSelect.value = 'custom';
if (customDurationInput) {
customDurationInput.value = mins;
customDurationInput.classList.remove('hidden');
}
} else {
if (customDurationInput) customDurationInput.classList.add('hidden');
}
if (durationDisplay) durationDisplay.innerText = `${mins}m`;
}
function toggleAddTaskForm() {
const form = document.getElementById('addTaskForm');
form.classList.toggle('hidden');
if (!form.classList.contains('hidden')) {
document.getElementById('newTaskTitle').focus();
}
}
function quickSchedule(hour) {
const form = document.getElementById('addTaskForm');
if (form && form.classList.contains('hidden')) {
form.classList.remove('hidden');
}
const timeInput = document.getElementById('newTaskTime');
if (timeInput) {
timeInput.value = hour;
onNewTaskTimeOrDurationChange('start');
}
const outlookCheckbox = document.getElementById('newTaskSyncOutlook');
if (outlookCheckbox) outlookCheckbox.checked = true;
document.getElementById('newTaskTitle')?.focus();
}
async function submitNewTask() {
const title = document.getElementById('newTaskTitle').value.trim();
const priority = document.getElementById('newTaskPriority').value;
const time = document.getElementById('newTaskTime').value.trim();
const endTime = document.getElementById('newTaskEndTime') ? document.getElementById('newTaskEndTime').value.trim() : '';
const duration = getSelectedNewTaskDurationMinutes();
const syncOutlook = document.getElementById('newTaskSyncOutlook') ? document.getElementById('newTaskSyncOutlook').checked : false;
if (!title) {
alert('Vul een taaktitel in.');
return;
}
try {
const res = await fetch('/api/planner.php?action=add_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title,
priority,
time_block_start: time,
time_block_end: endTime,
duration_minutes: duration,
sync_to_outlook: syncOutlook,
date: activePlannerDate
})
});
const data = await res.json();
if (data.success) {
if (syncOutlook && !data.outlook_synced) {
alert('Let op: De taak is opgeslagen in Fociz, maar kon niet direct naar Microsoft Outlook worden verzonden:\n\n' + (data.outlook_error || 'Controleer uw verbinding in Mijn Profiel.'));
}
location.reload();
} else {
alert(data.error || 'Fout bij opslaan van de taak');
}
} catch(err) {
alert('Netwerkfout bij opslaan van de taak');
}
}
// --- Taak & Tijdblok Bewerken Modal Logica ---
function openEditTaskModal(task) {
if (!task) return;
document.getElementById('editTaskId').value = task.id;
document.getElementById('editTaskTitle').value = task.title || '';
document.getElementById('editTaskPriority').value = task.priority || 'P2';
document.getElementById('editTaskStartTime').value = task.start || '';
document.getElementById('editTaskEndTime').value = task.end || '';
const dur = task.duration || 30;
setSelectedEditTaskDuration(dur);
const outlookNotice = document.getElementById('editTaskOutlookNotice');
if (outlookNotice) {
if (task.source === 'OUTLOOK' || task.external_id) {
outlookNotice.classList.remove('hidden');
} else {
outlookNotice.classList.add('hidden');
}
}
const modal = document.getElementById('editTaskModal');
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined') lucide.createIcons();
document.getElementById('editTaskTitle')?.focus();
}
}
function closeEditTaskModal() {
const modal = document.getElementById('editTaskModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
}
function onEditTaskTimeOrDurationChange(source) {
const startInput = document.getElementById('editTaskStartTime');
const endInput = document.getElementById('editTaskEndTime');
const durationSelect = document.getElementById('editTaskDuration');
const customDurationInput = document.getElementById('editTaskCustomDuration');
const durationDisplay = document.getElementById('editTaskDurationDisplay');
if (!startInput || !endInput || !durationSelect) return;
const startMins = parseTimeToMinutes(startInput.value);
if (source === 'end') {
const endMins = parseTimeToMinutes(endInput.value);
if (startMins !== null && endMins !== null && endMins > startMins) {
const diffMins = endMins - startMins;
setSelectedEditTaskDuration(diffMins);
}
} else if (source === 'duration') {
const val = durationSelect.value;
if (val === 'custom') {
customDurationInput.classList.remove('hidden');
customDurationInput.focus();
const customVal = parseInt(customDurationInput.value, 10);
if (!isNaN(customVal) && customVal > 0 && startMins !== null) {
endInput.value = formatMinutesToTime(startMins + customVal);
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
}
} else {
customDurationInput.classList.add('hidden');
const dur = parseInt(val, 10);
if (!isNaN(dur) && dur > 0) {
if (durationDisplay) durationDisplay.innerText = `${dur}m`;
if (startMins !== null) {
endInput.value = formatMinutesToTime(startMins + dur);
}
}
}
} else if (source === 'custom_duration') {
const customVal = parseInt(customDurationInput.value, 10);
if (!isNaN(customVal) && customVal > 0) {
if (durationDisplay) durationDisplay.innerText = `${customVal}m`;
if (startMins !== null) {
endInput.value = formatMinutesToTime(startMins + customVal);
}
}
} else if (source === 'start') {
let dur = getSelectedEditTaskDurationMinutes();
if (startMins !== null) {
if (endInput.value && parseTimeToMinutes(endInput.value) !== null) {
const currentEndMins = parseTimeToMinutes(endInput.value);
if (currentEndMins > startMins) {
setSelectedEditTaskDuration(currentEndMins - startMins);
} else if (dur > 0) {
endInput.value = formatMinutesToTime(startMins + dur);
}
} else if (dur > 0) {
endInput.value = formatMinutesToTime(startMins + dur);
}
}
}
}
function getSelectedEditTaskDurationMinutes() {
const durationSelect = document.getElementById('editTaskDuration');
const customDurationInput = document.getElementById('editTaskCustomDuration');
if (!durationSelect) return 30;
if (durationSelect.value === 'custom') {
const val = parseInt(customDurationInput?.value, 10);
return (!isNaN(val) && val > 0) ? val : 30;
}
const val = parseInt(durationSelect.value, 10);
return (!isNaN(val) && val > 0) ? val : 30;
}
function setSelectedEditTaskDuration(mins) {
const durationSelect = document.getElementById('editTaskDuration');
const customDurationInput = document.getElementById('editTaskCustomDuration');
const durationDisplay = document.getElementById('editTaskDurationDisplay');
if (!durationSelect) return;
let found = false;
for (let i = 0; i < durationSelect.options.length; i++) {
if (durationSelect.options[i].value === String(mins)) {
durationSelect.selectedIndex = i;
found = true;
break;
}
}
if (!found) {
durationSelect.value = 'custom';
if (customDurationInput) {
customDurationInput.value = mins;
customDurationInput.classList.remove('hidden');
}
} else {
if (customDurationInput) customDurationInput.classList.add('hidden');
}
if (durationDisplay) durationDisplay.innerText = `${mins}m`;
}
async function submitEditTask() {
const taskId = parseInt(document.getElementById('editTaskId').value, 10);
const title = document.getElementById('editTaskTitle').value.trim();
const priority = document.getElementById('editTaskPriority').value;
const startTime = document.getElementById('editTaskStartTime').value.trim();
const endTime = document.getElementById('editTaskEndTime').value.trim();
const duration = getSelectedEditTaskDurationMinutes();
const saveBtn = document.getElementById('btnSaveEditTask');
if (!title) {
alert('Vul een taaktitel in.');
return;
}
if (saveBtn) {
saveBtn.innerText = 'Opslaan...';
saveBtn.disabled = true;
}
try {
const res = await fetch('/api/planner.php?action=update_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
task_id: taskId,
title: title,
priority: priority,
time_block_start: startTime,
time_block_end: endTime,
duration_minutes: duration
})
});
const data = await res.json();
if (data.success) {
location.reload();
} else {
alert(data.error || 'Kon taak niet bijwerken');
if (saveBtn) {
saveBtn.innerText = 'Wijzigingen Opslaan';
saveBtn.disabled = false;
}
}
} catch (e) {
alert('Netwerkfout bij bijwerken van de taak');
if (saveBtn) {
saveBtn.innerText = 'Wijzigingen Opslaan';
saveBtn.disabled = false;
}
}
}
async function syncTaskToOutlook(taskId, btn) {
if (!btn) return;
const originalHtml = btn.innerHTML;
btn.innerHTML = '<span class="text-[9px] animate-pulse font-medium">Syncen...</span>';
btn.disabled = true;
try {
const res = await fetch('/api/planner.php?action=sync_task_to_outlook', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ task_id: taskId })
});
const data = await res.json();
if (data.success) {
btn.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5 text-teal-500"></i>';
if (typeof lucide !== 'undefined') lucide.createIcons();
setTimeout(() => {
location.reload();
}, 600);
} 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>';
location.reload();
} 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 })
});
location.reload();
}
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 })
});
document.getElementById(`task-row-${taskId}`)?.remove();
}
// --- 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.`);
location.reload();
} else {
alert(data.error || 'Kon dagplanning niet genereren via AI');
if (btn) {
btn.innerHTML = origHtml;
btn.disabled = false;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
} catch(err) {
alert('Netwerkfout bij aanroepen van AI Co-Pilot');
if (btn) {
btn.innerHTML = origHtml;
btn.disabled = false;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
}
// --- Outlook 365 Auto-Sync & Achtergrond Controle ---
const isOutlookConnected = <?= $isOutlookConnected ? 'true' : 'false' ?>;
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
const syncIntervalSeconds = <?= (int)($syncIntervalSeconds ?? 30) ?>;
const weekStartDate = '<?= $weekStart ?>';
const weekEndDate = '<?= $weekEnd ?>';
let isSyncCheckActive = false;
async function checkOutlookChanges(isManual = false) {
if (!isOutlookConnected || isSyncCheckActive) return;
isSyncCheckActive = true;
const icon = document.getElementById('manualSyncIcon');
const dot = document.getElementById('outlookSyncDot');
const statusText = document.getElementById('outlookSyncStatusText');
if (icon) icon.classList.add('animate-spin');
if (dot) {
dot.classList.remove('bg-sky-500');
dot.classList.add('bg-amber-500', 'animate-ping');
}
if (isManual && statusText) {
statusText.innerText = 'Synchroniseren...';
}
try {
const res = await fetch(`/api/planner.php?action=check_outlook_sync&plan_date=${encodeURIComponent(activePlannerDate)}&week_start=${encodeURIComponent(weekStartDate)}&week_end=${encodeURIComponent(weekEndDate)}&last_hash=${encodeURIComponent(currentSyncHash)}`);
if (!res.ok) throw new Error('Netwerkfout bij synchronisatie controle');
const data = await res.json();
if (data.success && data.connected) {
if (data.last_sync) {
const timeEl = document.getElementById('syncTimeDisplay');
if (timeEl) timeEl.innerText = data.last_sync.substring(0, 5);
}
if (data.has_changes) {
if (statusText) statusText.innerText = 'Wijzigingen bijwerken...';
saveTimerStateBeforeReload();
setTimeout(() => {
location.reload();
}, 400);
return;
} else {
currentSyncHash = data.hash;
if (statusText) statusText.innerText = 'Outlook 365 Live';
}
}
} catch (err) {
console.warn('Outlook achtergrond controle:', err);
if (statusText) statusText.innerText = 'Outlook 365 Live';
} finally {
isSyncCheckActive = false;
if (icon) icon.classList.remove('animate-spin');
if (dot) {
dot.classList.remove('bg-amber-500', 'animate-ping');
dot.classList.add('bg-sky-500');
}
}
}
function triggerManualSync() {
checkOutlookChanges(true);
}
// Initialisatie bij pagina-inladen
document.addEventListener('DOMContentLoaded', () => {
restoreTimerStateIfAny();
if (isOutlookConnected) {
if (syncIntervalSeconds > 0) {
setInterval(() => {
checkOutlookChanges(false);
}, syncIntervalSeconds * 1000);
}
// Direct controleren wanneer gebruiker terugkeert naar het tabblad / venster
window.addEventListener('focus', () => {
checkOutlookChanges(false);
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
checkOutlookChanges(false);
}
});
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>