feat: Add story points support and AI daily briefing feature

- Enhanced JIRA integration to fetch story points from multiple custom fields.
- Updated task display to show story points and their estimated duration.
- Introduced AI daily briefing banner with dynamic content loading.
- Implemented subtasks functionality with add, toggle, and delete capabilities.
- Improved task management UI with better accessibility and interaction.
This commit is contained in:
2026-09-10 22:38:06 +02:00
parent c0e0dd5947
commit 5ce08443cc
8 changed files with 780 additions and 69 deletions
+428 -69
View File
@@ -464,6 +464,51 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</div>
</div>
<!-- AI Dagbriefing Banner (async geladen, alleen vandaag) -->
<?php if ($isTodaySelected): ?>
<div id="aiBriefingBanner" class="fociz-card p-5 relative overflow-hidden border-l-4 border-l-indigo-500 hidden">
<!-- Skeleton laadindicator -->
<div id="aiBriefingLoading" class="flex items-center space-x-3">
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center flex-shrink-0 animate-pulse">
<i data-lucide="sparkles" class="w-4 h-4"></i>
</div>
<div class="flex-1 space-y-1.5">
<div class="h-3 bg-slate-200 dark:bg-slate-800 rounded-lg animate-pulse w-3/4"></div>
<div class="h-2.5 bg-slate-200 dark:bg-slate-800 rounded-lg animate-pulse w-1/2"></div>
</div>
</div>
<!-- Inhoud (gevuld via JS) -->
<div id="aiBriefingContent" class="hidden">
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center flex-shrink-0 mt-0.5">
<i data-lucide="sparkles" class="w-4 h-4"></i>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2 mb-1">
<span class="text-[10px] font-black uppercase tracking-wider text-indigo-600 dark:text-indigo-400">AI Co-Pilot · Dagbriefing</span>
<span class="text-[10px] text-slate-400 font-mono"><?= date('H:i') ?></span>
</div>
<p id="aiBriefingGreeting" class="text-sm font-semibold text-slate-900 dark:text-white"></p>
<div id="aiBriefingDetails" class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-600 dark:text-slate-400">
<span id="aiBriefingFocusTask" class="hidden flex items-center space-x-1">
<i data-lucide="zap" class="w-3 h-3 text-amber-500 flex-shrink-0"></i>
<span id="aiBriefingFocusText"></span>
</span>
<span id="aiBriefingTip" class="hidden flex items-center space-x-1">
<i data-lucide="lightbulb" class="w-3 h-3 text-emerald-500 flex-shrink-0"></i>
<span id="aiBriefingTipText"></span>
</span>
</div>
<div id="aiBriefingWarnings" class="mt-2 space-y-1 hidden"></div>
</div>
<button onclick="document.getElementById('aiBriefingBanner').classList.add('hidden')" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0" title="Sluiten">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
</div>
<?php endif; ?>
<!-- Modular Customizable Widget Grid Container (Fociz v2.0) -->
<div class="relative w-full" id="focizWidgetGridContainer">
<!-- Blueprint Achtergrond Raster (zichtbaar zodra een widget wordt opgepakt) -->
@@ -698,81 +743,109 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</div>
<?php else: ?>
<?php foreach ($tasks as $task): ?>
<div class="fociz-card-interactive group flex items-center justify-between p-3.5 rounded-2xl border <?= $task['is_completed'] ? 'bg-slate-50/50 dark:bg-slate-950/25 border-slate-200/60 dark:border-slate-800/40 opacity-60' : 'bg-white/95 dark:bg-[#111624]/90 border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-400 dark:hover:border-indigo-500/50 shadow-2xs hover:shadow-xs' ?> <?= $task['priority'] === 'P1' ? 'border-l-4 border-l-rose-500' : ($task['priority'] === 'P2' ? 'border-l-4 border-l-amber-500' : 'border-l-4 border-l-indigo-400 dark:border-l-indigo-500') ?> transition min-w-0" id="task-row-<?= $task['id'] ?>">
<div class="flex items-center space-x-3 truncate mr-2">
<button onclick="toggleTask(<?= $task['id'] ?>, <?= $task['is_completed'] ? 0 : 1 ?>)" class="w-5 h-5 rounded-lg border flex items-center justify-center transition <?= $task['is_completed'] ? 'bg-indigo-600 border-indigo-600 text-white font-bold' : 'border-slate-300 dark:border-slate-700 hover:border-indigo-500 bg-white dark:bg-slate-900' ?>">
<?php if ($task['is_completed']): ?>
<i data-lucide="check" class="w-3.5 h-3.5 stroke-[3]"></i>
<div class="rounded-2xl border <?= $task['is_completed'] ? 'bg-slate-50/50 dark:bg-slate-950/25 border-slate-200/60 dark:border-slate-800/40 opacity-60' : 'bg-white/95 dark:bg-[#111624]/90 border-slate-200/80 dark:border-slate-800/80 shadow-2xs' ?> <?= $task['priority'] === 'P1' ? 'border-l-4 border-l-rose-500' : ($task['priority'] === 'P2' ? 'border-l-4 border-l-amber-500' : 'border-l-4 border-l-indigo-400 dark:border-l-indigo-500') ?> transition min-w-0" id="task-row-<?= $task['id'] ?>">
<!-- Hoofd taakrij -->
<div class="fociz-card-interactive group flex items-center justify-between p-3.5 <?= $task['is_completed'] ? '' : 'hover:border-indigo-400 dark:hover:border-indigo-500/50' ?>">
<div class="flex items-center space-x-3 truncate mr-2">
<button onclick="toggleTask(<?= $task['id'] ?>, <?= $task['is_completed'] ? 0 : 1 ?>)" class="w-5 h-5 rounded-lg border flex items-center justify-center transition <?= $task['is_completed'] ? 'bg-indigo-600 border-indigo-600 text-white font-bold' : 'border-slate-300 dark:border-slate-700 hover:border-indigo-500 bg-white dark:bg-slate-900' ?>">
<?php if ($task['is_completed']): ?>
<i data-lucide="check" class="w-3.5 h-3.5 stroke-[3]"></i>
<?php endif; ?>
</button>
<div class="truncate">
<div class="flex items-center space-x-1.5 truncate">
<?php if ($task['source'] === 'JIRA'): ?>
<span class="px-1.5 py-0.2 rounded bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-700 dark:text-indigo-400 font-mono text-[9px] font-bold">
JIRA
</span>
<?php elseif ($task['source'] === 'OUTLOOK_MAIL'): ?>
<span class="px-1.5 py-0.2 rounded bg-violet-50 dark:bg-violet-500/10 border border-violet-200 dark:border-violet-500/20 text-violet-700 dark:text-violet-400 font-mono text-[9px] font-bold flex items-center space-x-0.5">
<i data-lucide="mail" class="w-2.5 h-2.5"></i>
<span>MAIL</span>
</span>
<?php elseif ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])): ?>
<span class="px-1.5 py-0.2 rounded bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-700 dark:text-sky-400 font-mono text-[9px] font-bold flex items-center space-x-0.5">
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
<span>OUTLOOK</span>
</span>
<?php endif; ?>
<span class="text-xs font-semibold truncate <?= $task['is_completed'] ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-800 dark:text-slate-200' ?>">
<?= htmlspecialchars($task['title']) ?>
</span>
</div>
<div class="flex items-center space-x-2 mt-0.5">
<?php if (!empty($task['time_block_start'])): ?>
<span class="text-[10px] text-slate-500 dark:text-slate-400 font-mono flex items-center space-x-1">
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
<span><?= htmlspecialchars($task['time_block_start']) ?><?= !empty($task['time_block_end']) ? ' - ' . htmlspecialchars($task['time_block_end']) : '' ?><?= !empty($task['duration_minutes']) ? ' (' . (int)$task['duration_minutes'] . 'm)' : '' ?></span>
</span>
<?php endif; ?>
<?php if (!empty($task['external_url'])): ?>
<a href="<?= htmlspecialchars($task['external_url']) ?>" target="_blank" class="text-[10px] <?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'text-sky-600 dark:text-sky-400' : (($task['source'] === 'OUTLOOK_MAIL') ? 'text-violet-600 dark:text-violet-400' : 'text-indigo-600 dark:text-indigo-400') ?> hover:underline flex items-center space-x-0.5">
<span><?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'Open in Outlook' : (($task['source'] === 'OUTLOOK_MAIL') ? 'Open Mail' : 'Open in Jira') ?></span>
<i data-lucide="external-link" class="w-2.5 h-2.5"></i>
</a>
<?php endif; ?>
</div>
</div>
</div>
<div class="flex items-center space-x-2 flex-shrink-0">
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full <?= $task['priority'] === 'P1' ? 'bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20' : ($task['priority'] === 'P2' ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700') ?>">
<?= htmlspecialchars($task['priority']) ?>
</span>
<?php if ($isOutlookConnected && empty($task['external_id']) && !$task['is_completed']): ?>
<button onclick="syncTaskToOutlook(<?= $task['id'] ?>, this)" title="Synchroniseer deze taak direct naar Outlook Agenda" class="flex items-center space-x-1 text-[10px] font-semibold px-2 py-0.5 rounded-lg bg-sky-50 hover:bg-sky-100 dark:bg-sky-500/10 dark:hover:bg-sky-500/20 text-sky-800 dark:text-sky-300 border border-sky-200 dark:border-sky-500/20 transition cursor-pointer">
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
<span class="hidden sm:inline">Naar Outlook</span>
</button>
<?php endif; ?>
</button>
<div class="truncate">
<div class="flex items-center space-x-1.5 truncate">
<?php if ($task['source'] === 'JIRA'): ?>
<span class="px-1.5 py-0.2 rounded bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-700 dark:text-indigo-400 font-mono text-[9px] font-bold">
JIRA
</span>
<?php elseif ($task['source'] === 'OUTLOOK_MAIL'): ?>
<span class="px-1.5 py-0.2 rounded bg-violet-50 dark:bg-violet-500/10 border border-violet-200 dark:border-violet-500/20 text-violet-700 dark:text-violet-400 font-mono text-[9px] font-bold flex items-center space-x-0.5">
<i data-lucide="mail" class="w-2.5 h-2.5"></i>
<span>MAIL</span>
</span>
<?php elseif ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])): ?>
<span class="px-1.5 py-0.2 rounded bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-700 dark:text-sky-400 font-mono text-[9px] font-bold flex items-center space-x-0.5">
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
<span>OUTLOOK</span>
</span>
<?php endif; ?>
<span class="text-xs font-semibold truncate <?= $task['is_completed'] ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-800 dark:text-slate-200' ?>">
<?= htmlspecialchars($task['title']) ?>
</span>
</div>
<!-- Subtaak toggle knop -->
<button onclick="toggleSubtaskPanel(<?= $task['id'] ?>)" title="Subtaken / Checklist" class="p-1.5 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-500/10 opacity-0 group-hover:opacity-100 transition cursor-pointer" id="subtask-btn-<?= $task['id'] ?>">
<i data-lucide="list-checks" class="w-3.5 h-3.5"></i>
</button>
<div class="flex items-center space-x-2 mt-0.5">
<?php if (!empty($task['time_block_start'])): ?>
<span class="text-[10px] text-slate-500 dark:text-slate-400 font-mono flex items-center space-x-1">
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
<span><?= htmlspecialchars($task['time_block_start']) ?><?= !empty($task['time_block_end']) ? ' - ' . htmlspecialchars($task['time_block_end']) : '' ?><?= !empty($task['duration_minutes']) ? ' (' . (int)$task['duration_minutes'] . 'm)' : '' ?></span>
</span>
<?php endif; ?>
<?php if (!empty($task['external_url'])): ?>
<a href="<?= htmlspecialchars($task['external_url']) ?>" target="_blank" class="text-[10px] <?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'text-sky-600 dark:text-sky-400' : (($task['source'] === 'OUTLOOK_MAIL') ? 'text-violet-600 dark:text-violet-400' : 'text-indigo-600 dark:text-indigo-400') ?> hover:underline flex items-center space-x-0.5">
<span><?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'Open in Outlook' : (($task['source'] === 'OUTLOOK_MAIL') ? 'Open Mail' : 'Open in Jira') ?></span>
<i data-lucide="external-link" class="w-2.5 h-2.5"></i>
</a>
<?php endif; ?>
</div>
<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>
<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>
<!-- 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>
<?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>
</div>
<?php endforeach; ?>
@@ -910,7 +983,67 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</span>
</div>
<!-- 4. De urentijdlijn rijen -->
<!-- 4. Capaciteitsbar: Geplande minuten vs. werkdagcapaciteit -->
<?php
$workdayCapacityMinutes = max(1, ($endHour - $startHour) * 60);
$lunchMinutes = 0;
if (!empty($lunchStart) && !empty($lunchEnd)) {
$lunchMinutes = (int)((strtotime("2000-01-01 {$lunchEnd}") - strtotime("2000-01-01 {$lunchStart}")) / 60);
}
$effectiveCapacity = max(1, $workdayCapacityMinutes - $lunchMinutes);
$meetingMinutes = 0;
foreach ($displayedOutlookEvents as $ev) {
if (!empty($ev['start']) && !empty($ev['end'])) {
$meetingMinutes += max(0, (int)((strtotime("2000-01-01 {$ev['end']}") - strtotime("2000-01-01 {$ev['start']}")) / 60));
}
}
$usedMinutes = $totalPlannedMinutes + $meetingMinutes;
$capPercent = min(100, round(($usedMinutes / $effectiveCapacity) * 100));
$taskOnlyPercent = min(100, round(($totalPlannedMinutes / $effectiveCapacity) * 100));
$meetingPercent = min(100 - $taskOnlyPercent, round(($meetingMinutes / $effectiveCapacity) * 100));
$capColor = $capPercent > 100 ? 'bg-rose-500' : ($capPercent >= 80 ? 'bg-amber-500' : 'bg-indigo-500');
$capLabel = $capPercent > 100 ? 'Overbelast' : ($capPercent >= 80 ? 'Bijna vol' : 'Goed ingepland');
$capLabelColor = $capPercent > 100 ? 'text-rose-600 dark:text-rose-400' : ($capPercent >= 80 ? 'text-amber-600 dark:text-amber-400' : 'text-indigo-600 dark:text-indigo-400');
$freeMinutes = max(0, $effectiveCapacity - $usedMinutes);
$freeH = intdiv($freeMinutes, 60);
$freeM = $freeMinutes % 60;
$freeLabel = $freeH > 0 ? "{$freeH}u " : '';
$freeLabel .= $freeM > 0 ? "{$freeM}m" : '';
$freeLabel = $freeLabel ?: '0m';
?>
<div class="space-y-1.5">
<div class="flex items-center justify-between text-[10px]">
<div class="flex items-center space-x-2">
<span class="font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">Dagcapaciteit</span>
<span class="font-mono font-black <?= $capLabelColor ?>"><?= $capPercent ?>% · <?= $capLabel ?></span>
</div>
<div class="flex items-center space-x-3 font-mono text-slate-500 dark:text-slate-400">
<?php if ($meetingMinutes > 0): ?>
<span class="flex items-center space-x-1">
<span class="w-2 h-2 rounded-sm bg-sky-400 dark:bg-sky-500 inline-block"></span>
<span><?= $meetingMinutes ?>m meetings</span>
</span>
<?php endif; ?>
<?php if ($totalPlannedMinutes > 0): ?>
<span class="flex items-center space-x-1">
<span class="w-2 h-2 rounded-sm bg-indigo-500 inline-block"></span>
<span><?= $totalPlannedMinutes ?>m taken</span>
</span>
<?php endif; ?>
<span class="text-emerald-600 dark:text-emerald-400 font-semibold"><?= $freeLabel ?> vrij</span>
</div>
</div>
<div class="h-2.5 w-full bg-slate-100 dark:bg-slate-900/60 rounded-full overflow-hidden flex">
<?php if ($meetingPercent > 0): ?>
<div class="h-full bg-sky-400 dark:bg-sky-500 transition-all duration-500 rounded-l-full" style="width: <?= $meetingPercent ?>%"></div>
<?php endif; ?>
<?php if ($taskOnlyPercent > 0): ?>
<div class="h-full <?= $capColor ?> transition-all duration-500 <?= $meetingPercent === 0 ? 'rounded-l-full' : '' ?> <?= ($meetingPercent + $taskOnlyPercent) >= 100 ? 'rounded-r-full' : '' ?>" style="width: <?= $taskOnlyPercent ?>%"></div>
<?php endif; ?>
</div>
</div>
<!-- 5. De urentijdlijn rijen -->
<div class="space-y-2.5">
<?php foreach ($hours as $hour):
$hourInt = (int)explode(':', $hour)[0];
@@ -1794,6 +1927,83 @@ if ($isAjaxRequest) {
let focizWidgets = <?= json_encode(array_values($userWidgetLayout), JSON_UNESCAPED_UNICODE) ?>;
const defaultWidgetLayout = <?= json_encode(array_values($availableWidgets), JSON_UNESCAPED_UNICODE) ?>;
// --- AI Dagbriefing (laden bij pagina-open, alleen voor vandaag) ---
<?php if ($isTodaySelected): ?>
const _aiBriefingDate = '<?= $selectedDate ?>';
let _aiBriefingLoaded = false;
async function loadDailyBriefing() {
if (_aiBriefingLoaded) return;
const banner = document.getElementById('aiBriefingBanner');
if (!banner) return;
// Toon banner met skeleton
banner.classList.remove('hidden');
try {
const resp = await fetch('/api/ai.php?action=daily_briefing', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date: _aiBriefingDate })
});
const data = await resp.json();
if (!data.success || data.error) {
// Geen AI key of fout: banner verbergen, stil falen
banner.classList.add('hidden');
return;
}
// Vul content
document.getElementById('aiBriefingGreeting').textContent = data.greeting || '';
const focusEl = document.getElementById('aiBriefingFocusTask');
const focusTextEl = document.getElementById('aiBriefingFocusText');
if (data.focus_task) {
focusTextEl.textContent = data.focus_task;
focusEl.classList.remove('hidden');
focusEl.style.display = 'flex';
}
const tipEl = document.getElementById('aiBriefingTip');
const tipTextEl = document.getElementById('aiBriefingTipText');
if (data.tip) {
tipTextEl.textContent = data.tip;
tipEl.classList.remove('hidden');
tipEl.style.display = 'flex';
}
if (data.warnings && data.warnings.length > 0) {
const warnContainer = document.getElementById('aiBriefingWarnings');
warnContainer.innerHTML = data.warnings.map(w =>
`<div class="flex items-center space-x-1.5 text-[11px] text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800/50 rounded-lg px-2.5 py-1">
<i data-lucide="alert-triangle" class="w-3 h-3 flex-shrink-0"></i>
<span>${w}</span>
</div>`
).join('');
warnContainer.classList.remove('hidden');
}
// Wissel skeleton ↔ content
document.getElementById('aiBriefingLoading').classList.add('hidden');
document.getElementById('aiBriefingContent').classList.remove('hidden');
_aiBriefingLoaded = true;
if (typeof lucide !== 'undefined') lucide.createIcons();
} catch (err) {
// Netwerk- of serverfout: banner verbergen
banner.classList.add('hidden');
}
}
// Laad de briefing 1,5 seconden na paginalading (niet blokkeren)
window.addEventListener('DOMContentLoaded', () => {
setTimeout(loadDailyBriefing, 1500);
});
<?php endif; ?>
function openWidgetManagerModal() {
renderWidgetManagerList();
const modal = document.getElementById('widgetManagerModal');
@@ -3381,6 +3591,155 @@ async function deleteTask(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;