Postits en editable items.

This commit is contained in:
2026-09-12 17:14:47 +02:00
parent 7c27949cc2
commit 94d6325693
9 changed files with 1680 additions and 56 deletions
+559 -46
View File
@@ -25,51 +25,112 @@ if (!$session) {
exit;
}
// Haal alle kaarten op van deze sessie
// Haal alle kaarten op van deze sessie inclusief toewijzing
$stmtCards = $db->prepare("
SELECT * FROM brainstorm_cards
WHERE session_id = ?
ORDER BY votes DESC, created_at ASC
SELECT c.*, u.name as assignee_name, u.avatar_url as assignee_avatar
FROM brainstorm_cards c
LEFT JOIN users u ON c.assignee_id = u.id
WHERE c.session_id = ?
ORDER BY c.votes DESC, c.created_at ASC
");
$stmtCards->execute([$sessionId]);
$cards = $stmtCards->fetchAll();
// Haal alle actieve teamleden op voor toewijzing & agendablokkades
$teamMembersStmt = $db->query("SELECT id, name, email, role, avatar_url FROM users ORDER BY name ASC");
$teamMembers = $teamMembersStmt->fetchAll();
$outlookUsers = [];
$intStmt = $db->query("SELECT user_id FROM integrations WHERE provider = 'OUTLOOK' AND is_enabled = 1 AND access_token IS NOT NULL");
foreach ($intStmt->fetchAll() as $row) {
$outlookUsers[(int)$row['user_id']] = true;
}
foreach ($teamMembers as &$tm) {
$tm['has_outlook'] = !empty($outlookUsers[(int)$tm['id']]);
}
unset($tm);
$categories = [
'Ideeën' => [
'title' => 'Ideeën & Concepten',
'icon' => 'lightbulb',
'color' => 'amber',
'border' => 'border-amber-200/80 dark:border-amber-800/40',
'bg' => 'bg-amber-50/60 dark:bg-amber-950/20',
'badge' => 'bg-amber-500/15 text-amber-800 dark:text-amber-300'
'border' => 'border-amber-300 dark:border-amber-600/40',
'bg' => 'bg-[#fef9c3] dark:bg-[#713f12]/30 text-amber-950 dark:text-amber-100',
'badge' => 'bg-amber-500/20 text-amber-900 dark:text-amber-200'
],
'Knelpunten' => [
'title' => 'Knelpunten & Uitdagingen',
'icon' => 'alert-triangle',
'color' => 'rose',
'border' => 'border-rose-200/80 dark:border-rose-800/40',
'bg' => 'bg-rose-50/60 dark:bg-rose-950/20',
'badge' => 'bg-rose-500/15 text-rose-800 dark:text-rose-300'
'border' => 'border-rose-300 dark:border-rose-600/40',
'bg' => 'bg-[#ffe4e6] dark:bg-[#881337]/30 text-rose-950 dark:text-rose-100',
'badge' => 'bg-rose-500/20 text-rose-900 dark:text-rose-200'
],
'Oplossingen' => [
'title' => 'Oplossingen & Features',
'icon' => 'rocket',
'color' => 'teal',
'border' => 'border-teal-200/80 dark:border-teal-800/40',
'bg' => 'bg-teal-50/60 dark:bg-teal-950/20',
'badge' => 'bg-teal-500/15 text-teal-800 dark:text-teal-300'
'border' => 'border-teal-300 dark:border-teal-600/40',
'bg' => 'bg-[#ccfbf1] dark:bg-[#134e4a]/30 text-teal-950 dark:text-teal-100',
'badge' => 'bg-teal-500/20 text-teal-900 dark:text-teal-200'
],
'Quick Wins' => [
'title' => 'Quick Wins (Direct)',
'icon' => 'zap',
'color' => 'indigo',
'border' => 'border-indigo-200/80 dark:border-indigo-800/40',
'bg' => 'bg-indigo-50/60 dark:bg-indigo-950/20',
'badge' => 'bg-indigo-500/15 text-indigo-800 dark:text-indigo-300'
'border' => 'border-indigo-300 dark:border-indigo-600/40',
'bg' => 'bg-[#e0e7ff] dark:bg-[#312e81]/30 text-indigo-950 dark:text-indigo-100',
'badge' => 'bg-indigo-500/20 text-indigo-900 dark:text-indigo-200'
],
];
?>
<style>
/* --- Realistische Post-it Styling met Opkrullende Rand Links Onder --- */
.postit-note {
position: relative;
border-radius: 4px 18px 4px 18px;
box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.12), 0 2px 6px -1px rgba(0, 0, 0, 0.08);
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.04));
}
.postit-note:hover {
transform: translateY(-3px) scale(1.01);
box-shadow: 0 12px 20px -4px rgba(0, 0, 0, 0.16), 0 6px 10px -2px rgba(0, 0, 0, 0.08);
}
/* Opkrullend hoekje links onder */
.postit-note::after {
content: '';
position: absolute;
bottom: -1px;
left: -1px;
width: 22px;
height: 22px;
background: linear-gradient(135deg, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0.06) 45%, transparent 50%);
border-bottom-left-radius: 4px;
box-shadow: -2px 2px 4px rgba(0,0,0,0.12);
pointer-events: none;
z-index: 5;
}
/* Subtiele tape / plakrand bovenaan */
.postit-note::before {
content: '';
position: absolute;
top: 0;
left: 15%;
right: 15%;
height: 4px;
background: rgba(255, 255, 255, 0.4);
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
pointer-events: none;
}
.dark .postit-note::before {
background: rgba(255, 255, 255, 0.1);
}
</style>
<div class="space-y-6 max-w-7xl mx-auto">
<!-- Top Bar -->
<div class="fociz-card p-5 flex flex-col lg:flex-row lg:items-center justify-between gap-4">
@@ -146,44 +207,89 @@ $categories = [
Nog geen kaarten in deze categorie.
</div>
<?php else: ?>
<?php foreach ($catCards as $card): ?>
<?php foreach ($catCards as $card):
$hasAssignee = !empty($card['assignee_id']) && !empty($card['assignee_name']);
$hasSchedule = !empty($card['scheduled_date']) && !empty($card['scheduled_start_time']);
$cardJson = htmlspecialchars(json_encode([
'id' => (int)$card['id'],
'content' => $card['content'],
'category' => $card['category'],
'color' => $card['color'],
'assignee_id' => $card['assignee_id'] ? (int)$card['assignee_id'] : null,
'assignee_name' => $card['assignee_name'] ?? null,
'scheduled_date' => $card['scheduled_date'] ?? null,
'scheduled_start_time' => $card['scheduled_start_time'] ?? null,
'scheduled_end_time' => $card['scheduled_end_time'] ?? null,
]), ENT_QUOTES, 'UTF-8');
?>
<div
id="card-<?= $card['id'] ?>"
draggable="true"
ondragstart="handleCanvasCardDragStart(event, <?= $card['id'] ?>)"
ondragend="handleCanvasCardDragEnd(event)"
class="brainstorm-card group relative rounded-2xl p-4 border <?= $catConfig['border'] ?> <?= $catConfig['bg'] ?> backdrop-blur-sm shadow-sm hover:shadow-md transition-all duration-150 cursor-grab active:cursor-grabbing hover:-translate-y-0.5 flex flex-col justify-between"
oncontextmenu="openCardContextMenu(event, <?= $cardJson ?>); return false;"
class="brainstorm-card postit-note group relative p-4 border <?= $catConfig['border'] ?> <?= $catConfig['bg'] ?> transition-all duration-150 cursor-grab active:cursor-grabbing hover:-translate-y-1 flex flex-col justify-between"
>
<!-- Top: Checkbox voor selectie + Roadmap badge -->
<div class="flex items-center justify-between gap-2 mb-2.5">
<div class="flex items-center justify-between gap-2 mb-2">
<label class="inline-flex items-center space-x-2 cursor-pointer select-none">
<input type="checkbox" value="<?= $card['id'] ?>" data-content="<?= htmlspecialchars($card['content'], ENT_QUOTES) ?>" class="card-checkbox w-4 h-4 rounded bg-white dark:bg-slate-900 border-slate-300 dark:border-slate-700 text-indigo-600 focus:ring-0">
<span class="text-[11px] text-slate-500 dark:text-slate-400 font-medium">Selecteer</span>
<input type="checkbox" value="<?= $card['id'] ?>" data-content="<?= htmlspecialchars($card['content'], ENT_QUOTES) ?>" class="card-checkbox w-4 h-4 rounded bg-white/80 dark:bg-slate-900/80 border-slate-400/50 text-indigo-600 focus:ring-0">
<span class="text-[11px] opacity-70 font-medium">Selecteer</span>
</label>
<?php if (!empty($card['converted_to_roadmap_item_id'])): ?>
<span class="px-2 py-0.5 rounded-md bg-teal-50 dark:bg-teal-500/15 text-teal-700 dark:text-teal-400 font-bold text-[10px] flex items-center space-x-1 border border-teal-200 dark:border-teal-500/20">
<i data-lucide="check" class="w-3 h-3"></i>
<span>Op Roadmap</span>
</span>
<?php endif; ?>
<div class="flex items-center space-x-1">
<?php if (!empty($card['converted_to_roadmap_item_id'])): ?>
<span class="px-2 py-0.5 rounded-md bg-teal-600/15 text-teal-800 dark:text-teal-300 font-bold text-[10px] flex items-center space-x-1 border border-teal-500/30">
<i data-lucide="check" class="w-3 h-3"></i>
<span>Roadmap</span>
</span>
<?php endif; ?>
<button type="button" onclick="openCardContextMenu(event, <?= $cardJson ?>)" class="p-1 rounded text-slate-500 hover:text-slate-800 dark:hover:text-white transition cursor-pointer" title="Opties & Inplannen (Rechtermuisknop)">
<i data-lucide="more-horizontal" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
<!-- Inhoud Post-it met verfijnde typografie en uitlijning -->
<div class="text-left py-1">
<p class="text-xs text-slate-800 dark:text-slate-100 font-medium leading-relaxed whitespace-pre-wrap break-words text-left">
<div class="text-left py-1" onclick="openEditCardModal(<?= $cardJson ?>)">
<p class="text-xs font-semibold leading-relaxed whitespace-pre-wrap break-words text-left cursor-pointer" title="Klik om te bewerken">
<?= htmlspecialchars($card['content']) ?>
</p>
</div>
<!-- Assignee & Agenda Tijdblok Badge (indien gekoppeld) -->
<?php if ($hasAssignee || $hasSchedule): ?>
<div class="mt-2.5 pt-2 border-t border-black/10 dark:border-white/10 flex items-center justify-between gap-1 text-[10px]">
<?php if ($hasAssignee): ?>
<div class="flex items-center space-x-1.5 truncate">
<?php if (!empty($card['assignee_avatar'])): ?>
<img src="<?= htmlspecialchars($card['assignee_avatar']) ?>" class="w-4 h-4 rounded-full object-cover border border-black/10">
<?php else: ?>
<div class="w-4 h-4 rounded-full bg-indigo-600 text-white font-bold text-[8px] flex items-center justify-center">
<?= strtoupper(substr($card['assignee_name'], 0, 1)) ?>
</div>
<?php endif; ?>
<span class="font-bold truncate max-w-[100px]"><?= htmlspecialchars($card['assignee_name']) ?></span>
</div>
<?php endif; ?>
<?php if ($hasSchedule): ?>
<span class="inline-flex items-center space-x-1 font-bold px-1.5 py-0.5 rounded bg-black/10 dark:bg-white/10 text-[9px]" title="Ingepland op <?= htmlspecialchars($card['scheduled_date']) ?>">
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
<span><?= htmlspecialchars(substr($card['scheduled_start_time'], 0, 5)) ?></span>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Footer van kaart -->
<div class="mt-3 pt-2.5 border-t border-slate-200/60 dark:border-slate-800/60 flex items-center justify-between text-[10px] text-slate-500 dark:text-slate-400">
<div class="mt-2.5 pt-2 border-t border-black/10 dark:border-white/10 flex items-center justify-between text-[10px] opacity-80">
<div class="flex items-center space-x-1.5 min-w-0">
<span class="truncate font-semibold text-slate-700 dark:text-slate-300"><?= htmlspecialchars($card['author_name'] ?? 'Collega') ?></span>
<span class="truncate font-medium"><?= htmlspecialchars($card['author_name'] ?? 'Collega') ?></span>
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
<button onclick="openAiBreakdownModal(<?= $card['id'] ?>, <?= htmlspecialchars(json_encode($card['content']), ENT_QUOTES) ?>)" class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-lg bg-violet-100/90 hover:bg-violet-200 dark:bg-violet-950/60 dark:hover:bg-violet-900/60 text-violet-700 dark:text-violet-300 font-bold text-[10px] transition cursor-pointer border border-violet-200/80 dark:border-violet-800/60 shadow-2xs flex-shrink-0" title="Splits dit idee uit naar behapbare taken via Google Gemini Pro">
<i data-lucide="sparkles" class="w-3 h-3 text-violet-500"></i>
<button onclick="openAiBreakdownModal(<?= $card['id'] ?>, <?= htmlspecialchars(json_encode($card['content']), ENT_QUOTES) ?>)" class="inline-flex items-center space-x-1 px-1.5 py-0.5 rounded bg-black/10 hover:bg-black/15 dark:bg-white/10 dark:hover:bg-white/15 font-bold text-[9px] transition cursor-pointer" title="Splits dit idee uit naar taken via AI">
<i data-lucide="sparkles" class="w-2.5 h-2.5 text-violet-600 dark:text-violet-400"></i>
<span>AI</span>
</button>
<?php endif; ?>
@@ -191,13 +297,13 @@ $categories = [
<div class="flex items-center space-x-1.5 flex-shrink-0">
<!-- Upvote button -->
<button onclick="voteCard(<?= $card['id'] ?>)" class="flex items-center space-x-1 px-2 py-1 rounded-lg bg-white/80 hover:bg-white dark:bg-slate-800/80 dark:hover:bg-slate-700 text-slate-600 dark:text-slate-300 hover:text-amber-500 dark:hover:text-amber-400 border border-slate-200/80 dark:border-transparent transition shadow-xs cursor-pointer">
<button onclick="voteCard(<?= $card['id'] ?>)" class="flex items-center space-x-1 px-2 py-0.5 rounded bg-black/10 hover:bg-black/15 dark:bg-white/10 dark:hover:bg-white/15 transition shadow-2xs cursor-pointer">
<i data-lucide="thumbs-up" class="w-3 h-3"></i>
<span id="votes-<?= $card['id'] ?>" class="font-bold"><?= $card['votes'] ?></span>
</button>
<!-- Delete button -->
<button onclick="deleteCard(<?= $card['id'] ?>)" class="p-1 rounded text-slate-400 hover:text-rose-600 dark:text-slate-500 dark:hover:text-rose-400 transition opacity-0 group-hover:opacity-100 cursor-pointer">
<button onclick="deleteCard(<?= $card['id'] ?>)" class="p-1 rounded text-rose-600 hover:text-rose-700 opacity-0 group-hover:opacity-100 transition cursor-pointer" title="Verwijderen">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
@@ -217,23 +323,66 @@ $categories = [
</div>
</div>
<!-- Modal: Nieuwe Kaart Toevoegen -->
<div id="addCardModal" onclick="if(event.target === this) closeAddCardModal()" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
<!-- Floating Rechtermuisknop Contextmenu -->
<div id="cardContextMenu" class="hidden fixed z-[999] bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-2xl shadow-2xl py-1.5 w-64 text-xs font-medium text-slate-700 dark:text-slate-200 select-none animate-in fade-in zoom-in-95 duration-100">
<div class="px-3.5 py-1.5 border-b border-slate-100 dark:border-slate-800/80 mb-1">
<span id="ctxMenuCardTitle" class="text-[11px] font-bold text-slate-900 dark:text-white truncate block"></span>
<span id="ctxMenuCardMeta" class="text-[10px] text-slate-400 truncate block"></span>
</div>
<button onclick="triggerCtxEdit()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-indigo-50 dark:hover:bg-indigo-950/50 hover:text-indigo-600 dark:hover:text-indigo-400 transition cursor-pointer">
<i data-lucide="edit-3" class="w-4 h-4 text-indigo-500"></i>
<span>Kaart bewerken</span>
</button>
<button onclick="triggerCtxAssign()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-indigo-50 dark:hover:bg-indigo-950/50 hover:text-indigo-600 dark:hover:text-indigo-400 transition cursor-pointer">
<i data-lucide="user-check" class="w-4 h-4 text-emerald-500"></i>
<span>Toewijzen & Agenda inplannen</span>
</button>
<button onclick="triggerCtxQuickSlot()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-amber-50 dark:hover:bg-amber-950/40 hover:text-amber-600 dark:hover:text-amber-400 transition cursor-pointer">
<i data-lucide="zap" class="w-4 h-4 text-amber-500"></i>
<span>⚡ Zoek direct vrij blok</span>
</button>
<button onclick="triggerCtxRoadmap()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-indigo-50 dark:hover:bg-indigo-950/50 hover:text-indigo-600 dark:hover:text-indigo-400 transition cursor-pointer">
<i data-lucide="map" class="w-4 h-4 text-sky-500"></i>
<span>Zet om naar Roadmap</span>
</button>
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
<button onclick="triggerCtxBreakdown()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-violet-50 dark:hover:bg-violet-950/50 hover:text-violet-600 dark:hover:text-violet-400 transition cursor-pointer">
<i data-lucide="sparkles" class="w-4 h-4 text-violet-500"></i>
<span>AI Splits uit in taken</span>
</button>
<?php endif; ?>
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
<button onclick="triggerCtxDelete()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer">
<i data-lucide="trash-2" class="w-4 h-4"></i>
<span>Kaart verwijderen</span>
</button>
</div>
<!-- Modal: Kaart Bewerken -->
<div id="editCardModal" onclick="if(event.target === this) closeEditCardModal()" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
<div class="fociz-card p-6 shadow-2xl max-w-md w-full space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
<h3 class="text-sm font-bold text-slate-900 dark:text-white flex items-center space-x-2">
<i data-lucide="sticky-note" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<span>Brainstormkaart Toevoegen</span>
<i data-lucide="edit-3" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<span>Brainstormkaart Bewerken</span>
</h3>
<button onclick="closeAddCardModal()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-white rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<button onclick="closeEditCardModal()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-white rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="space-y-4">
<input type="hidden" id="editCardId" value="">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Categorie</label>
<select id="modalCardCategory" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<select id="editCardCategory" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
<option value="Ideeën">💡 Ideeën & Concepten</option>
<option value="Knelpunten">⚠️ Knelpunten & Uitdagingen</option>
<option value="Oplossingen">🚀 Oplossingen & Features</option>
@@ -242,15 +391,125 @@ $categories = [
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Idee / Beschrijving</label>
<textarea id="modalCardContent" rows="4" placeholder="Schrijf hier uw inzicht, idee of knelpunt..." class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 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 shadow-2xs"></textarea>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Inhoud Post-it</label>
<textarea id="editCardContent" rows="4" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl p-3 text-slate-900 dark:text-white text-xs placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500 resize-none shadow-2xs"></textarea>
</div>
<div class="flex space-x-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
<button onclick="submitNewCard()" class="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2.5 px-4 rounded-xl text-xs transition cursor-pointer shadow-xs">
Plaats op Canvas
<button onclick="submitEditCard()" class="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2.5 px-4 rounded-xl text-xs transition cursor-pointer shadow-xs">
Wijzigingen Opslaan
</button>
<button onclick="closeAddCardModal()" class="px-4 py-2.5 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl text-xs font-semibold transition cursor-pointer">
<button onclick="closeEditCardModal()" class="px-4 py-2.5 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl text-xs font-semibold transition cursor-pointer">
Annuleren
</button>
</div>
</div>
</div>
</div>
<!-- Modal: Toewijzen aan Collega & Inplannen in Agenda (Vrij Blok) -->
<div id="assignScheduleModal" onclick="if(event.target === this) closeAssignScheduleModal()" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
<div class="fociz-card p-6 shadow-2xl max-w-lg w-full space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2">
<div class="w-8 h-8 rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Toewijzen & Agenda Inplanning</h3>
<p class="text-[11px] text-slate-400">Plan deze brainstormkaart in als taak in het vrije blok van een collega</p>
</div>
</div>
<button onclick="closeAssignScheduleModal()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-white rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="space-y-4">
<input type="hidden" id="assignCardId" value="">
<div class="p-3 bg-slate-50 dark:bg-slate-900/60 rounded-xl border border-slate-200 dark:border-slate-800 text-xs">
<span class="text-slate-400 text-[10px] uppercase font-bold tracking-wider block mb-1">Kaart</span>
<p id="assignCardContentPreview" class="text-slate-800 dark:text-slate-200 font-medium line-clamp-2"></p>
</div>
<!-- Collega selecteren -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1.5">
Toewijzen aan collega *
</label>
<select id="assignColleagueSelect" onchange="onAssignColleagueChange()" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 cursor-pointer">
<option value="">-- Kies een collega --</option>
<?php foreach ($teamMembers as $tm): ?>
<option value="<?= $tm['id'] ?>" data-has-outlook="<?= $tm['has_outlook'] ? '1' : '0' ?>" data-name="<?= htmlspecialchars($tm['name']) ?>">
<?= htmlspecialchars($tm['name']) ?> <?= $tm['has_outlook'] ? '📅 (Outlook 365)' : '' ?>
</option>
<?php endforeach; ?>
</select>
<p id="assignOutlookNotice" class="hidden mt-1 text-[11px] text-sky-600 dark:text-sky-400 font-medium flex items-center space-x-1">
<i data-lucide="calendar-check" class="w-3 h-3"></i>
<span>Deze collega heeft Microsoft Outlook 365 actief. De afspraak kan direct in Outlook gezet worden!</span>
</p>
</div>
<!-- Slim Zoeken: Knop Eerstvolgend Vrij Blok -->
<div class="p-3 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-between gap-3">
<div class="text-xs">
<span class="font-bold text-amber-900 dark:text-amber-200 flex items-center space-x-1">
<i data-lucide="sparkles" class="w-3.5 h-3.5 text-amber-500"></i>
<span>Automatisch vrij blok vinden</span>
</span>
<span class="text-[11px] text-amber-700 dark:text-amber-400">Zoekt binnen de gezette werktijden en Outlook agenda</span>
</div>
<button type="button" id="btnFindSlot" onclick="findFreeSlotForColleague()" class="px-3 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-600 text-white font-bold text-xs shadow-xs transition flex items-center space-x-1.5 flex-shrink-0 cursor-pointer">
<i data-lucide="search" class="w-3.5 h-3.5"></i>
<span>Zoek vrij blok</span>
</button>
</div>
<div id="foundSlotNotice" class="hidden p-2.5 rounded-lg bg-emerald-50 dark:bg-emerald-950/40 border border-emerald-300 dark:border-emerald-800 text-xs text-emerald-800 dark:text-emerald-300 font-medium flex items-center space-x-1.5">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-500 flex-shrink-0"></i>
<span id="foundSlotText"></span>
</div>
<!-- Datum & Tijdblok -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Datum</label>
<input type="date" id="assignPlanDate" value="<?= date('Y-m-d') ?>" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd</label>
<input type="time" id="assignStartTime" value="09:00" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Duur</label>
<select id="assignDurationMinutes" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none">
<option value="15">15 minuten</option>
<option value="30" selected>30 minuten</option>
<option value="45">45 minuten</option>
<option value="60">1 uur (60m)</option>
<option value="90">1,5 uur (90m)</option>
<option value="120">2 uur (120m)</option>
</select>
</div>
</div>
<!-- Vinkje Outlook -->
<label class="flex items-center space-x-2 cursor-pointer pt-1">
<input type="checkbox" id="assignSyncOutlook" checked class="rounded text-indigo-600 focus:ring-indigo-500">
<span class="text-xs font-semibold text-slate-800 dark:text-slate-200">
Direct als agenda-afspraak plaatsen in Outlook 365 van deze collega
</span>
</label>
<!-- Buttons -->
<div class="flex space-x-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
<button type="button" onclick="submitAssignAndSchedule()" id="btnSubmitAssign" class="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2.5 px-4 rounded-xl text-xs transition cursor-pointer shadow-xs flex items-center justify-center space-x-1.5">
<i data-lucide="check" class="w-4 h-4"></i>
<span>Inplannen & Toewijzen</span>
</button>
<button type="button" onclick="closeAssignScheduleModal()" class="px-4 py-2.5 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl text-xs font-semibold transition cursor-pointer">
Annuleren
</button>
</div>
@@ -564,6 +823,260 @@ async function deleteCard(cardId) {
document.getElementById(`card-${cardId}`)?.remove();
}
// --- Rechtermuisknop Contextmenu Logica ---
let _activeCtxCard = null;
function openCardContextMenu(e, card) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
_activeCtxCard = card;
const menu = document.getElementById('cardContextMenu');
const titleEl = document.getElementById('ctxMenuCardTitle');
const metaEl = document.getElementById('ctxMenuCardMeta');
const firstLine = card.content.split('\n')[0] || 'Brainstormkaart';
titleEl.innerText = firstLine;
metaEl.innerText = `${card.category} ${card.assignee_name ? '• ' + card.assignee_name : ''}`;
// Positioneer het menu op de muiscursor
const mouseX = e ? e.clientX : window.innerWidth / 2;
const mouseY = e ? e.clientY : window.innerHeight / 2;
menu.classList.remove('hidden');
menu.style.left = `${Math.min(mouseX, window.innerWidth - 270)}px`;
menu.style.top = `${Math.min(mouseY, window.innerHeight - 300)}px`;
if (window.lucide) lucide.createIcons();
}
function closeCardContextMenu() {
const menu = document.getElementById('cardContextMenu');
if (menu) menu.classList.add('hidden');
}
// Contextmenu acties
function triggerCtxEdit() {
const card = _activeCtxCard;
closeCardContextMenu();
if (card) openEditCardModal(card);
}
function triggerCtxAssign() {
const card = _activeCtxCard;
closeCardContextMenu();
if (card) openAssignScheduleModal(card);
}
function triggerCtxQuickSlot() {
const card = _activeCtxCard;
closeCardContextMenu();
if (card) {
openAssignScheduleModal(card);
setTimeout(() => findFreeSlotForColleague(), 100);
}
}
function triggerCtxRoadmap() {
const card = _activeCtxCard;
closeCardContextMenu();
if (card) {
const chk = document.querySelector(`.card-checkbox[value="${card.id}"]`);
if (chk) chk.checked = true;
openConvertModal();
}
}
function triggerCtxBreakdown() {
const card = _activeCtxCard;
closeCardContextMenu();
if (card) openAiBreakdownModal(card.id, card.content);
}
function triggerCtxDelete() {
const card = _activeCtxCard;
closeCardContextMenu();
if (card) deleteCard(card.id);
}
// Sluit contextmenu bij klik erbuiten
document.addEventListener('click', (e) => {
if (!e.target.closest('#cardContextMenu')) {
closeCardContextMenu();
}
});
// --- Kaart Bewerken Modal Logica ---
function openEditCardModal(card) {
document.getElementById('editCardId').value = card.id;
document.getElementById('editCardCategory').value = card.category || 'Ideeën';
document.getElementById('editCardContent').value = card.content || '';
document.getElementById('editCardModal').classList.remove('hidden');
document.getElementById('editCardContent').focus();
}
function closeEditCardModal() {
document.getElementById('editCardModal').classList.add('hidden');
}
async function submitEditCard() {
const cardId = parseInt(document.getElementById('editCardId').value, 10);
const category = document.getElementById('editCardCategory').value;
const content = document.getElementById('editCardContent').value.trim();
if (!cardId || !content) return;
try {
const res = await fetch('/api/brainstorm.php?action=update_card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ card_id: cardId, category, content })
});
const data = await res.json();
if (data.success) {
closeEditCardModal();
location.reload();
} else {
alert(data.error || 'Kon kaart niet bijwerken');
}
} catch(err) {
alert('Netwerkfout bij bijwerken: ' + err.message);
}
}
// --- Toewijzen & Agenda Inplannen Modal Logica ---
function openAssignScheduleModal(card) {
document.getElementById('assignCardId').value = card.id;
document.getElementById('assignCardContentPreview').innerText = card.content;
const sel = document.getElementById('assignColleagueSelect');
if (card.assignee_id) {
sel.value = card.assignee_id;
} else {
sel.value = '';
}
onAssignColleagueChange();
if (card.scheduled_date) {
document.getElementById('assignPlanDate').value = card.scheduled_date;
} else {
document.getElementById('assignPlanDate').value = new Date().toISOString().slice(0, 10);
}
if (card.scheduled_start_time) {
document.getElementById('assignStartTime').value = card.scheduled_start_time.slice(0, 5);
} else {
document.getElementById('assignStartTime').value = '09:00';
}
document.getElementById('foundSlotNotice').classList.add('hidden');
document.getElementById('assignScheduleModal').classList.remove('hidden');
if (window.lucide) lucide.createIcons();
}
function closeAssignScheduleModal() {
document.getElementById('assignScheduleModal').classList.add('hidden');
}
function onAssignColleagueChange() {
const sel = document.getElementById('assignColleagueSelect');
const opt = sel.options[sel.selectedIndex];
const notice = document.getElementById('assignOutlookNotice');
if (opt && opt.getAttribute('data-has-outlook') === '1') {
notice.classList.remove('hidden');
} else {
notice.classList.add('hidden');
}
}
// Zoek eerstvolgend vrij blok van de geselecteerde collega
async function findFreeSlotForColleague() {
const sel = document.getElementById('assignColleagueSelect');
const targetUserId = sel.value ? parseInt(sel.value, 10) : <?= $userId ?>;
const duration = parseInt(document.getElementById('assignDurationMinutes').value, 10) || 30;
const btn = document.getElementById('btnFindSlot');
const origHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Zoeken...</span>';
if (window.lucide) lucide.createIcons();
try {
const res = await fetch('/api/planner.php?action=find_next_free_slot', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
duration_minutes: duration,
target_user_id: targetUserId
})
});
const data = await res.json();
if (data.success && data.slot) {
const slot = data.slot;
document.getElementById('assignPlanDate').value = slot.date;
document.getElementById('assignStartTime').value = slot.start;
const notice = document.getElementById('foundSlotNotice');
const noticeText = document.getElementById('foundSlotText');
noticeText.innerHTML = `Vrij blok gevonden: <strong>${slot.formatted_date}</strong> om <strong>${slot.start} – ${slot.end}</strong> (${duration} min)`;
notice.classList.remove('hidden');
} else {
alert(data.error || 'Geen beschikbaar tijdblok gevonden binnen werktijden.');
}
} catch(err) {
alert('Netwerkfout bij zoeken van vrij blok: ' + err.message);
} finally {
btn.disabled = false;
btn.innerHTML = origHtml;
if (window.lucide) lucide.createIcons();
}
}
async function submitAssignAndSchedule() {
const cardId = parseInt(document.getElementById('assignCardId').value, 10);
const assigneeId = document.getElementById('assignColleagueSelect').value ? parseInt(document.getElementById('assignColleagueSelect').value, 10) : null;
const planDate = document.getElementById('assignPlanDate').value;
const startTime = document.getElementById('assignStartTime').value;
const durationMinutes = parseInt(document.getElementById('assignDurationMinutes').value, 10) || 30;
const syncOutlook = document.getElementById('assignSyncOutlook').checked;
const btn = document.getElementById('btnSubmitAssign');
const origHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i><span>Opslaan...</span>';
if (window.lucide) lucide.createIcons();
try {
const res = await fetch('/api/brainstorm.php?action=assign_and_schedule', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
card_id: cardId,
assignee_id: assigneeId,
plan_date: planDate,
start_time: startTime,
duration_minutes: durationMinutes,
sync_outlook: syncOutlook
})
});
const data = await res.json();
if (data.success) {
closeAssignScheduleModal();
location.reload();
} else {
alert(data.error || 'Kon toewijzing niet opslaan');
}
} catch(err) {
alert('Netwerkfout bij opslaan: ' + err.message);
} finally {
btn.disabled = false;
btn.innerHTML = origHtml;
if (window.lucide) lucide.createIcons();
}
}
// --- Drag & Drop tussen Brainstorm Categorie Lanes ---
let draggedCanvasCardId = null;