Ai aanpassingen en slepen in planner blokken #69

Merged
NiekRabelink merged 1 commits from dev into main 2026-09-12 12:24:36 +00:00
7 changed files with 545 additions and 31 deletions
Showing only changes of commit 5e35f5152d - Show all commits
+33
View File
@@ -113,6 +113,39 @@ try {
exit; exit;
} }
// 3b. Kaart verplaatsen naar andere categorie/lane (Drag & Drop)
if ($action === 'move_card') {
$data = json_decode(file_get_contents('php://input'), true);
$cardId = (int)($data['card_id'] ?? 0);
$category = trim($data['category'] ?? '');
$validCategories = ['Ideeën', 'Knelpunten', 'Oplossingen', 'Quick Wins'];
if (!$cardId || !in_array($category, $validCategories, true)) {
echo json_encode(['error' => 'Ongeldige kaart of categorie']);
exit;
}
// Bepaal kleur behorend bij de nieuwe categorie
$colorMap = [
'Ideeën' => 'amber',
'Knelpunten' => 'rose',
'Oplossingen' => 'teal',
'Quick Wins' => 'indigo'
];
$newColor = $colorMap[$category] ?? 'amber';
$stmt = $db->prepare("UPDATE brainstorm_cards SET category = ?, color = ? WHERE id = ?");
$stmt->execute([$category, $newColor, $cardId]);
echo json_encode([
'success' => true,
'card_id' => $cardId,
'category' => $category,
'color' => $newColor
]);
exit;
}
// 4. Omzetten naar Roadmap! // 4. Omzetten naar Roadmap!
if ($action === 'convert_to_roadmap') { if ($action === 'convert_to_roadmap') {
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
+4
View File
@@ -53,6 +53,10 @@ try {
$fields[] = "status = ?"; $fields[] = "status = ?";
$params[] = $status; $params[] = $status;
} }
if (isset($data['quarter']) && in_array($data['quarter'], ['Q1', 'Q2', 'Q3', 'Q4'], true)) {
$fields[] = "quarter = ?";
$params[] = $data['quarter'];
}
if ($progress !== null) { if ($progress !== null) {
$fields[] = "progress = ?"; $fields[] = "progress = ?";
$params[] = max(0, min(100, $progress)); $params[] = max(0, min(100, $progress));
+1 -1
View File
@@ -34,7 +34,7 @@ if ($currentUser) {
$error = null; $error = null;
$infoMessage = null; $infoMessage = null;
if (isset($_GET['expired'])) { if (isset($_GET['expired'])) {
$infoMessage = "Uw sessie is beëindigd wegens inactiviteit of een dagwissel. Log opnieuw in om verder te gaan."; $infoMessage = "Uw sessie is beeïndigd. Log opnieuw in om verder te gaan.";
} }
if (isset($_GET['revoked'])) { if (isset($_GET['revoked'])) {
$infoMessage = "Uw sessie is op afstand beëindigd of ingetrokken. Log opnieuw in om verder te gaan."; $infoMessage = "Uw sessie is op afstand beëindigd of ingetrokken. Log opnieuw in om verder te gaan.";
+144 -18
View File
@@ -121,33 +121,45 @@ $categories = [
<?php foreach ($categories as $catKey => $catConfig): <?php foreach ($categories as $catKey => $catConfig):
$catCards = array_filter($cards, fn($c) => $c['category'] === $catKey); $catCards = array_filter($cards, fn($c) => $c['category'] === $catKey);
?> ?>
<div class="fociz-card p-4 flex flex-col min-h-[500px]"> <div class="fociz-card p-4 flex flex-col min-h-[500px] transition-all duration-200">
<!-- Column Header --> <!-- Column Header -->
<div class="flex items-center justify-between pb-3 mb-3 border-b border-slate-100 dark:border-slate-800"> <div class="flex items-center justify-between pb-3 mb-3 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<i data-lucide="<?= $catConfig['icon'] ?>" class="w-4 h-4 text-<?= $catConfig['color'] ?>-500"></i> <i data-lucide="<?= $catConfig['icon'] ?>" class="w-4 h-4 text-<?= $catConfig['color'] ?>-500"></i>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider"><?= $catConfig['title'] ?></h3> <h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider"><?= $catConfig['title'] ?></h3>
</div> </div>
<span class="text-xs font-mono font-bold px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700"> <span id="badge-count-<?= htmlspecialchars(md5($catKey)) ?>" class="text-xs font-mono font-bold px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700">
<?= count($catCards) ?> <?= count($catCards) ?>
</span> </span>
</div> </div>
<!-- Cards Container --> <!-- Cards Container as Drop Zone -->
<div class="flex-1 space-y-3"> <div
class="brainstorm-lane flex-1 space-y-3 min-h-[140px] rounded-xl p-1.5 transition-all duration-150"
data-category="<?= htmlspecialchars($catKey, ENT_QUOTES) ?>"
ondragover="handleCanvasLaneDragOver(event)"
ondragleave="handleCanvasLaneDragLeave(event)"
ondrop="handleCanvasLaneDrop(event, '<?= htmlspecialchars($catKey, ENT_QUOTES) ?>')"
>
<?php if (empty($catCards)): ?> <?php if (empty($catCards)): ?>
<div class="py-8 text-center text-slate-400 dark:text-slate-600 text-xs font-medium"> <div class="lane-empty-placeholder py-8 text-center text-slate-400 dark:text-slate-600 text-xs font-medium pointer-events-none">
Nog geen kaarten in deze categorie. Nog geen kaarten in deze categorie.
</div> </div>
<?php else: ?> <?php else: ?>
<?php foreach ($catCards as $card): ?> <?php foreach ($catCards as $card): ?>
<div class="group relative rounded-xl p-4 border <?= $catConfig['border'] ?> <?= $catConfig['bg'] ?> backdrop-blur-sm shadow-2xs transition hover:-translate-y-0.5" id="card-<?= $card['id'] ?>"> <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"
>
<!-- Top: Checkbox voor selectie + Roadmap badge --> <!-- Top: Checkbox voor selectie + Roadmap badge -->
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between gap-2 mb-2.5">
<label class="flex items-center space-x-2 cursor-pointer"> <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"> <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-[10px] text-slate-500 dark:text-slate-400 font-medium">Selecteer</span> <span class="text-[11px] text-slate-500 dark:text-slate-400 font-medium">Selecteer</span>
</label> </label>
<?php if (!empty($card['converted_to_roadmap_item_id'])): ?> <?php if (!empty($card['converted_to_roadmap_item_id'])): ?>
@@ -158,24 +170,26 @@ $categories = [
<?php endif; ?> <?php endif; ?>
</div> </div>
<!-- Inhoud --> <!-- Inhoud Post-it met verfijnde typografie en uitlijning -->
<p class="text-xs text-slate-800 dark:text-slate-200 leading-relaxed whitespace-pre-wrap font-medium"> <div class="text-left py-1">
<?= htmlspecialchars($card['content']) ?> <p class="text-xs text-slate-800 dark:text-slate-100 font-medium leading-relaxed whitespace-pre-wrap break-words text-left">
</p> <?= htmlspecialchars($card['content']) ?>
</p>
</div>
<!-- Footer van kaart --> <!-- 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-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="flex items-center space-x-1.5"> <div class="flex items-center space-x-1.5 min-w-0">
<span><?= htmlspecialchars($card['author_name'] ?? 'Collega') ?></span> <span class="truncate font-semibold text-slate-700 dark:text-slate-300"><?= htmlspecialchars($card['author_name'] ?? 'Collega') ?></span>
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?> <?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" title="Splits dit idee uit naar behapbare taken via Google Gemini Pro"> <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> <i data-lucide="sparkles" class="w-3 h-3 text-violet-500"></i>
<span>Werk uit met AI</span> <span>AI</span>
</button> </button>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-1.5 flex-shrink-0">
<!-- Upvote button --> <!-- 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-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">
<i data-lucide="thumbs-up" class="w-3 h-3"></i> <i data-lucide="thumbs-up" class="w-3 h-3"></i>
@@ -550,6 +564,118 @@ async function deleteCard(cardId) {
document.getElementById(`card-${cardId}`)?.remove(); document.getElementById(`card-${cardId}`)?.remove();
} }
// --- Drag & Drop tussen Brainstorm Categorie Lanes ---
let draggedCanvasCardId = null;
function handleCanvasCardDragStart(e, cardId) {
draggedCanvasCardId = cardId;
try {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(cardId));
} catch (err) {}
setTimeout(() => {
const el = document.getElementById(`card-${cardId}`);
if (el) {
el.classList.add('opacity-40', 'scale-[0.98]');
}
}, 0);
}
function handleCanvasCardDragEnd(e) {
if (draggedCanvasCardId) {
const el = document.getElementById(`card-${draggedCanvasCardId}`);
if (el) {
el.classList.remove('opacity-40', 'scale-[0.98]');
}
draggedCanvasCardId = null;
}
document.querySelectorAll('.brainstorm-lane').forEach(lane => {
lane.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
});
}
function handleCanvasLaneDragOver(e) {
if (!draggedCanvasCardId) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const lane = e.currentTarget;
if (lane && !lane.classList.contains('ring-2')) {
lane.classList.add('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
}
}
function handleCanvasLaneDragLeave(e) {
const lane = e.currentTarget;
if (lane && !lane.contains(e.relatedTarget)) {
lane.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
}
}
async function handleCanvasLaneDrop(e, targetCategory) {
e.preventDefault();
const lane = e.currentTarget;
if (lane) {
lane.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
}
const cardId = draggedCanvasCardId || parseInt(e.dataTransfer.getData('text/plain'), 10);
draggedCanvasCardId = null;
if (!cardId || !targetCategory) return;
const cardEl = document.getElementById(`card-${cardId}`);
if (!cardEl) return;
const sourceLane = cardEl.closest('.brainstorm-lane');
const sourceCategory = sourceLane ? sourceLane.getAttribute('data-category') : null;
// Als de kaart al in deze categorie zit, doe niets
if (sourceCategory === targetCategory) return;
// Verplaats DOM element direct voor een directe, vloeiende UX
lane.appendChild(cardEl);
const placeholder = lane.querySelector('.lane-empty-placeholder');
if (placeholder) placeholder.remove();
// Kleurstijlen per categorie bijwerken
const categoryStyles = {
'Ideeën': { border: 'border-amber-200/80', darkBorder: 'dark:border-amber-800/40', bg: 'bg-amber-50/60', darkBg: 'dark:bg-amber-950/20' },
'Knelpunten': { border: 'border-rose-200/80', darkBorder: 'dark:border-rose-800/40', bg: 'bg-rose-50/60', darkBg: 'dark:bg-rose-950/20' },
'Oplossingen': { border: 'border-teal-200/80', darkBorder: 'dark:border-teal-800/40', bg: 'bg-teal-50/60', darkBg: 'dark:bg-teal-950/20' },
'Quick Wins': { border: 'border-indigo-200/80', darkBorder: 'dark:border-indigo-800/40', bg: 'bg-indigo-50/60', darkBg: 'dark:bg-indigo-950/20' }
};
// Verwijder oude kleurklassen en voeg nieuwe toe
Object.values(categoryStyles).forEach(st => {
cardEl.classList.remove(st.border, st.darkBorder, st.bg, st.darkBg);
});
const targetStyle = categoryStyles[targetCategory] || categoryStyles['Ideeën'];
cardEl.classList.add(targetStyle.border, targetStyle.darkBorder, targetStyle.bg, targetStyle.darkBg);
// Stuur POST naar backend om categorie en kleur te bewaren
try {
const res = await fetch('/api/brainstorm.php?action=move_card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
card_id: cardId,
category: targetCategory
})
});
const data = await res.json();
if (!data.success) {
alert(data.error || 'Kon kaart niet verplaatsen');
location.reload();
}
} catch (err) {
console.error('Fout bij verplaatsen brainstormkaart:', err);
alert('Netwerkfout bij verplaatsen');
location.reload();
}
}
// --- Roadmap Conversie Logica --- // --- Roadmap Conversie Logica ---
function getSelectedCardIds() { function getSelectedCardIds() {
const checkboxes = document.querySelectorAll('.card-checkbox:checked'); const checkboxes = document.querySelectorAll('.card-checkbox:checked');
+172 -4
View File
@@ -1240,8 +1240,12 @@ for ($h = $startHour; $h <= $endHour; $h++) {
return !empty($t['time_block_start']) && (int)explode(':', $t['time_block_start'])[0] === $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="timeline-hour-slot flex items-start group min-h-[48px] border-b border-slate-100 dark:border-slate-800/40 pb-2.5 last:border-0 min-w-0 transition-all duration-150 rounded-xl p-1"
<div class="w-12 sm:w-14 text-xs font-mono font-semibold text-slate-500 dark:text-slate-400 pt-1.5 flex-shrink-0"> data-hour="<?= $hour ?>"
ondragover="handleTimelineSlotDragOver(event)"
ondragleave="handleTimelineSlotDragLeave(event)"
ondrop="handleTimelineSlotDrop(event, '<?= $hour ?>')">
<div class="w-12 sm:w-14 text-xs font-mono font-semibold text-slate-500 dark:text-slate-400 pt-1.5 flex-shrink-0 select-none">
<?= $hour ?> <?= $hour ?>
</div> </div>
@@ -1278,7 +1282,13 @@ for ($h = $startHour; $h <= $endHour; $h++) {
'is_outlook_event' => true, 'is_outlook_event' => true,
]; ];
?> ?>
<div oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)" title="Rechtermuisklik voor opties (openen, bewerken, verwijderen)" class="p-3 rounded-xl bg-sky-500/[0.07] dark:bg-sky-500/[0.12] border-l-4 border-l-sky-500 border border-sky-500/20 text-sky-950 dark:text-sky-100 text-xs shadow-2xs min-w-0 overflow-hidden transition-all hover:bg-sky-500/[0.10] dark:hover:bg-sky-500/[0.16] cursor-pointer group/evt"> <div id="timeline-evt-<?= htmlspecialchars(preg_replace('/[^a-zA-Z0-9_-]/', '', $evt['id'] ?? '')) ?>"
draggable="true"
ondragstart="handleTimelineItemDragStart(event, 'outlook', <?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)"
ondragend="handleTimelineItemDragEnd(event)"
oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)"
title="Sleep naar ander tijdblok, of rechtermuisklik voor opties"
class="p-3 rounded-xl bg-sky-500/[0.07] dark:bg-sky-500/[0.12] border-l-4 border-l-sky-500 border border-sky-500/20 text-sky-950 dark:text-sky-100 text-xs shadow-2xs min-w-0 overflow-hidden transition-all hover:bg-sky-500/[0.10] dark:hover:bg-sky-500/[0.16] cursor-grab active:cursor-grabbing group/evt">
<div class="flex items-start justify-between gap-2.5 min-w-0"> <div class="flex items-start justify-between gap-2.5 min-w-0">
<div class="flex items-start space-x-2 min-w-0 flex-1" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)"> <div class="flex items-start space-x-2 min-w-0 flex-1" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($evtJson), ENT_QUOTES, 'UTF-8') ?>)">
<span class="px-2 py-0.5 rounded-md bg-sky-500/15 text-sky-800 dark:text-sky-300 font-mono text-[10px] font-bold flex-shrink-0 whitespace-nowrap mt-0.5"> <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">
@@ -1322,7 +1332,13 @@ for ($h = $startHour; $h <= $endHour; $h++) {
'date' => $selectedDate 'date' => $selectedDate
]; ];
?> ?>
<div id="timeline-task-<?= $t['id'] ?>" oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)" title="Rechtermuisklik voor opties (openen, bewerken, verwijderen)" class="p-3 rounded-xl border-l-4 <?= $t['priority'] === 'P1' ? 'border-l-rose-500' : ($t['priority'] === 'P2' ? 'border-l-amber-500' : 'border-l-indigo-400') ?> <?= $t['is_completed'] ? 'bg-slate-100/60 dark:bg-slate-900/40 border border-slate-200 dark:border-slate-800 text-slate-400 dark:text-slate-500 line-through' : 'bg-white/80 dark:bg-[#0c0f17]/70 border border-slate-200/90 dark:border-slate-800 text-slate-900 dark:text-slate-100 shadow-2xs hover:shadow-xs' ?> text-xs min-w-0 overflow-hidden transition-all group/task"> <div id="timeline-task-<?= $t['id'] ?>"
draggable="true"
ondragstart="handleTimelineItemDragStart(event, 'task', <?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)"
ondragend="handleTimelineItemDragEnd(event)"
oncontextmenu="handleTaskContextMenu(event, <?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)"
title="Sleep naar ander tijdblok, of rechtermuisklik voor opties"
class="p-3 rounded-xl border-l-4 <?= $t['priority'] === 'P1' ? 'border-l-rose-500' : ($t['priority'] === 'P2' ? 'border-l-amber-500' : 'border-l-indigo-400') ?> <?= $t['is_completed'] ? 'bg-slate-100/60 dark:bg-slate-900/40 border border-slate-200 dark:border-slate-800 text-slate-400 dark:text-slate-500 line-through' : 'bg-white/80 dark:bg-[#0c0f17]/70 border border-slate-200/90 dark:border-slate-800 text-slate-900 dark:text-slate-100 shadow-2xs hover:shadow-xs' ?> text-xs min-w-0 overflow-hidden transition-all cursor-grab active:cursor-grabbing group/task">
<div class="flex items-start justify-between gap-2.5 min-w-0"> <div class="flex items-start justify-between gap-2.5 min-w-0">
<div class="flex items-start space-x-2 min-w-0 flex-1 cursor-pointer" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)"> <div class="flex items-start space-x-2 min-w-0 flex-1 cursor-pointer" onclick="openTaskDetailModal(<?= htmlspecialchars(json_encode($slotTaskJson), ENT_QUOTES, 'UTF-8') ?>)">
<span class="px-2 py-0.5 rounded-md bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-500/20 font-mono text-[10px] font-bold flex-shrink-0 whitespace-nowrap mt-0.5"> <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">
@@ -6873,6 +6889,158 @@ async function aiSmartSchedule(btn) {
} }
} }
} }
// --- Drag & Drop van Taken & Afspraken op Tijdlijn ---
let draggedTimelineItem = null;
function handleTimelineItemDragStart(e, type, item) {
draggedTimelineItem = { type, item };
try {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', JSON.stringify({ type, id: item.id }));
} catch (err) {}
// Voeg visuele feedback toe aan het versleepte element
setTimeout(() => {
if (e.target) {
e.target.classList.add('opacity-40', 'scale-[0.98]');
}
}, 0);
}
function handleTimelineItemDragEnd(e) {
if (e.target) {
e.target.classList.remove('opacity-40', 'scale-[0.98]');
}
// Verwijder eventuele achtergebleven drag-over styles van slots
document.querySelectorAll('.timeline-hour-slot').forEach(slot => {
slot.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
});
}
function handleTimelineSlotDragOver(e) {
if (!draggedTimelineItem) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const slot = e.currentTarget;
if (slot && !slot.classList.contains('ring-2')) {
slot.classList.add('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
}
}
function handleTimelineSlotDragLeave(e) {
const slot = e.currentTarget;
if (slot && !slot.contains(e.relatedTarget)) {
slot.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
}
}
async function handleTimelineSlotDrop(e, targetHour) {
e.preventDefault();
const slot = e.currentTarget;
if (slot) {
slot.classList.remove('bg-indigo-50/80', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400', 'dark:ring-indigo-500', 'ring-inset');
}
if (!draggedTimelineItem) return;
const { type, item } = draggedTimelineItem;
draggedTimelineItem = null;
// Bepaal de oorspronkelijke tijdsduur van het item in minuten
let durationMinutes = 30;
if (item.duration && parseInt(item.duration, 10) > 0) {
durationMinutes = parseInt(item.duration, 10);
} else if (item.start && item.end) {
const sMins = parseTimeToMinutes(item.start);
const eMins = parseTimeToMinutes(item.end);
if (sMins !== null && eMins !== null && eMins > sMins) {
durationMinutes = eMins - sMins;
}
}
// Nieuwe starttijd is het drop-uurslot (bijv. "14:00")
const newStart = targetHour;
const startMins = parseTimeToMinutes(newStart);
let newEnd = '';
if (startMins !== null) {
newEnd = formatMinutesToTime(startMins + durationMinutes);
}
const itemTitle = (type === 'outlook' ? (item.subject || item.title) : item.title) || 'Item';
const planDate = item.date || (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
if (typeof showWidgetToast === 'function') {
showWidgetToast(`Verplaatsen naar <strong>${newStart} - ${newEnd}</strong>...`, 'info');
}
try {
if (type === 'outlook') {
const res = await fetch('/api/planner.php?action=update_outlook_event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event_id: item.id,
title: itemTitle,
description: item.description || '',
time_block_start: newStart,
time_block_end: newEnd,
date: planDate
})
});
const data = await res.json();
if (data.success) {
if (typeof showWidgetToast === 'function') {
showWidgetToast(`Outlook afspraak <strong>${escapeHtml(itemTitle)}</strong> verplaatst naar ${newStart} - ${newEnd}`, 'success');
}
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
} else {
alert(data.error || 'Kon afspraak niet verplaatsen in Outlook');
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
}
} else {
// Reguliere Fociz taak
const res = await fetch('/api/planner.php?action=update_task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
task_id: parseInt(item.id, 10),
title: itemTitle,
description: item.description || '',
priority: item.priority || 'P2',
time_block_start: newStart,
time_block_end: newEnd,
duration_minutes: durationMinutes,
date: planDate
})
});
const data = await res.json();
if (data.success) {
if (typeof showWidgetToast === 'function') {
showWidgetToast(`Taak <strong>${escapeHtml(itemTitle)}</strong> verplaatst naar ${newStart} - ${newEnd}`, 'success');
}
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
} else {
alert(data.error || 'Kon taak niet verplaatsen');
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
}
}
} catch (err) {
console.error('Drag and drop fout:', err);
alert('Netwerkfout bij verplaatsen van item');
if (typeof switchPlannerDate === 'function') {
switchPlannerDate(planDate, null, false);
}
}
}
// --- Outlook 365 Auto-Sync & Achtergrond Controle --- // --- Outlook 365 Auto-Sync & Achtergrond Controle ---
const isOutlookConnected = <?= $isOutlookConnected ? 'true' : 'false' ?>; const isOutlookConnected = <?= $isOutlookConnected ? 'true' : 'false' ?>;
const syncIntervalSeconds = <?= (int)($syncIntervalSeconds ?? 30) ?>; const syncIntervalSeconds = <?= (int)($syncIntervalSeconds ?? 30) ?>;
+95 -3
View File
@@ -64,13 +64,21 @@ $quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
<!-- Filters: Quarters & Status --> <!-- Filters: Quarters & Status -->
<div class="fociz-card p-4 flex flex-wrap items-center justify-between gap-3"> <div class="fociz-card p-4 flex flex-wrap items-center justify-between gap-3">
<!-- Quarter Tabs --> <!-- Quarter Tabs (tevens drop targets) -->
<div class="flex items-center space-x-1 text-xs"> <div class="flex items-center space-x-1 text-xs">
<a href="/roadmap.php?quarter=ALL&status=<?= htmlspecialchars($selectedStatus) ?>" class="px-3 py-1.5 rounded-lg transition font-semibold <?= $selectedQuarter === 'ALL' ? 'bg-indigo-600 text-white shadow-xs' : 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800/80' ?>"> <a href="/roadmap.php?quarter=ALL&status=<?= htmlspecialchars($selectedStatus) ?>" class="px-3 py-1.5 rounded-lg transition font-semibold <?= $selectedQuarter === 'ALL' ? 'bg-indigo-600 text-white shadow-xs' : 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800/80' ?>">
Alle Quarters Alle Quarters
</a> </a>
<?php foreach ($quarters as $q): ?> <?php foreach ($quarters as $q): ?>
<a href="/roadmap.php?quarter=<?= $q ?>&status=<?= htmlspecialchars($selectedStatus) ?>" class="px-3 py-1.5 rounded-lg transition font-semibold <?= $selectedQuarter === $q ? 'bg-indigo-600 text-white shadow-xs' : 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800/80' ?>"> <a
href="/roadmap.php?quarter=<?= $q ?>&status=<?= htmlspecialchars($selectedStatus) ?>"
class="roadmap-quarter-tab px-3.5 py-1.5 rounded-lg transition-all font-semibold <?= $selectedQuarter === $q ? 'bg-indigo-600 text-white shadow-xs' : 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800/80' ?>"
data-quarter="<?= $q ?>"
ondragover="handleRoadmapQuarterDragOver(event)"
ondragleave="handleRoadmapQuarterDragLeave(event)"
ondrop="handleRoadmapQuarterDrop(event, '<?= $q ?>')"
title="Sleep een roadmap item hierheen om het kwartaal direct te wijzigen"
>
<?= $q ?> <?= $q ?>
</a> </a>
<?php endforeach; ?> <?php endforeach; ?>
@@ -108,7 +116,14 @@ $quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
default => 'border-l-4 border-l-slate-300 dark:border-l-slate-700', default => 'border-l-4 border-l-slate-300 dark:border-l-slate-700',
}; };
?> ?>
<div class="fociz-card-interactive p-5 space-y-3.5 <?= $statusBorder ?>" id="roadmap-item-<?= $item['id'] ?>"> <div
id="roadmap-item-<?= $item['id'] ?>"
draggable="true"
ondragstart="handleRoadmapItemDragStart(event, <?= $item['id'] ?>)"
ondragend="handleRoadmapItemDragEnd(event)"
class="fociz-card-interactive p-5 space-y-3.5 <?= $statusBorder ?> cursor-grab active:cursor-grabbing transition-all duration-150"
title="Sleep naar een ander kwartaal (Q1, Q2, Q3, Q4) bovenaan"
>
<!-- Top header van item --> <!-- Top header van item -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2"> <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
@@ -333,6 +348,83 @@ async function submitManualItem() {
if (data.success) { if (data.success) {
location.reload(); location.reload();
} }
}
// --- Drag & Drop van Roadmap Items naar ander Kwartaal (Q1, Q2, Q3, Q4) ---
let draggedRoadmapItemId = null;
function handleRoadmapItemDragStart(e, itemId) {
draggedRoadmapItemId = itemId;
try {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(itemId));
} catch (err) {}
setTimeout(() => {
const el = document.getElementById(`roadmap-item-${itemId}`);
if (el) el.classList.add('opacity-40', 'scale-[0.99]');
}, 0);
}
function handleRoadmapItemDragEnd(e) {
if (draggedRoadmapItemId) {
const el = document.getElementById(`roadmap-item-${draggedRoadmapItemId}`);
if (el) el.classList.remove('opacity-40', 'scale-[0.99]');
draggedRoadmapItemId = null;
}
document.querySelectorAll('.roadmap-quarter-tab').forEach(tab => {
tab.classList.remove('ring-2', 'ring-indigo-400', 'scale-105', 'bg-indigo-100', 'dark:bg-indigo-900/60');
});
}
function handleRoadmapQuarterDragOver(e) {
if (!draggedRoadmapItemId) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const tab = e.currentTarget;
if (tab && !tab.classList.contains('ring-2')) {
tab.classList.add('ring-2', 'ring-indigo-400', 'scale-105', 'bg-indigo-100', 'dark:bg-indigo-900/60');
}
}
function handleRoadmapQuarterDragLeave(e) {
const tab = e.currentTarget;
if (tab && !tab.contains(e.relatedTarget)) {
tab.classList.remove('ring-2', 'ring-indigo-400', 'scale-105', 'bg-indigo-100', 'dark:bg-indigo-900/60');
}
}
async function handleRoadmapQuarterDrop(e, targetQuarter) {
e.preventDefault();
const tab = e.currentTarget;
if (tab) {
tab.classList.remove('ring-2', 'ring-indigo-400', 'scale-105', 'bg-indigo-100', 'dark:bg-indigo-900/60');
}
const itemId = draggedRoadmapItemId || parseInt(e.dataTransfer.getData('text/plain'), 10);
draggedRoadmapItemId = null;
if (!itemId || !targetQuarter) return;
try {
const res = await fetch('/api/roadmap.php?action=update_item', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id: itemId, quarter: targetQuarter })
});
const data = await res.json();
if (data.success) {
location.reload();
} else {
alert(data.error || 'Kon item niet verplaatsen');
}
} catch (err) {
console.error('Fout bij verplaatsen roadmap item:', err);
alert('Netwerkfout bij verplaatsen');
}
}
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
const modal = document.getElementById('newItemModal'); const modal = document.getElementById('newItemModal');
+96 -5
View File
@@ -164,13 +164,18 @@ html.dark #timelineGridWrapper::-webkit-scrollbar-thumb {
<!-- LINKERPANEEL: 'Werk' Takenlijst & Snelle Invoer --> <!-- LINKERPANEEL: 'Werk' Takenlijst & Snelle Invoer -->
<div class="w-80 sm:w-96 border-r border-slate-200 dark:border-slate-800/80 flex flex-col flex-shrink-0 bg-white dark:bg-[#0c0f17]/90 z-20"> <div class="w-80 sm:w-96 border-r border-slate-200 dark:border-slate-800/80 flex flex-col flex-shrink-0 bg-white dark:bg-[#0c0f17]/90 z-20">
<!-- Werk Paneel Header --> <!-- Werk Paneel Header met Lane Drop Targets -->
<div class="h-12 px-4 border-b border-slate-200/80 dark:border-slate-800/80 flex items-center justify-between bg-slate-50/70 dark:bg-slate-900/40"> <div class="h-12 px-4 border-b border-slate-200/80 dark:border-slate-800/80 flex items-center justify-between bg-slate-50/70 dark:bg-slate-900/40">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">Werk</span> <span class="text-xs font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">Werk</span>
<span id="workItemsCountBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700">0</span> <span id="workItemsCountBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700">0</span>
</div> </div>
<div class="text-[11px] text-slate-400">Status & Datum</div> <div class="text-[11px] text-slate-400">Sleep taak naar lane / datum</div>
</div>
<!-- Snelle Lane Drop Zone Balk (verschijnt subtiel of is altijd zichtbaar) -->
<div id="timelineLaneDropBar" class="px-2.5 py-1.5 bg-slate-100/70 dark:bg-slate-900/60 border-b border-slate-200/60 dark:border-slate-800/60 flex items-center gap-1.5 overflow-x-auto text-[11px]">
<!-- Dynamisch gevuld via renderTimelineLaneDropBar() -->
</div> </div>
<!-- Inline Snelle Taakinvoer: ⚡ Wat moet er gedaan worden? (Exact zoals Jira) --> <!-- Inline Snelle Taakinvoer: ⚡ Wat moet er gedaan worden? (Exact zoals Jira) -->
@@ -528,6 +533,53 @@ function populateLaneFilter() {
sel.appendChild(opt); sel.appendChild(opt);
}); });
} }
renderTimelineLaneDropBar();
}
// Render drop targets voor snelle statuswijziging in het werkpaneel
function renderTimelineLaneDropBar() {
const bar = document.getElementById('timelineLaneDropBar');
if (!bar) return;
bar.innerHTML = '';
const lanes = (currentBoardData && currentBoardData.lanes) ? currentBoardData.lanes : [];
if (lanes.length === 0) {
bar.classList.add('hidden');
return;
}
bar.classList.remove('hidden');
lanes.forEach(lane => {
const dotColor = LANE_COLORS[lane.color] || 'bg-slate-500';
const dropBadge = document.createElement('div');
dropBadge.className = 'timeline-lane-target px-2.5 py-1 rounded-lg border border-slate-200/80 dark:border-slate-800 bg-white/80 dark:bg-slate-800/70 hover:bg-indigo-50 dark:hover:bg-indigo-950/40 text-slate-700 dark:text-slate-200 font-semibold text-[11px] flex items-center space-x-1.5 cursor-pointer transition flex-shrink-0 select-none shadow-2xs';
dropBadge.setAttribute('data-lane-id', lane.id);
dropBadge.title = `Sleep een taak hierheen om te verplaatsen naar: ${lane.title}`;
dropBadge.innerHTML = `
<span class="w-2 h-2 rounded-full ${dotColor}"></span>
<span class="truncate max-w-[90px]">${escapeHtml(lane.title)}</span>
`;
dropBadge.ondragover = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
dropBadge.classList.add('ring-2', 'ring-indigo-500', 'bg-indigo-100', 'dark:bg-indigo-900/60');
};
dropBadge.ondragleave = () => {
dropBadge.classList.remove('ring-2', 'ring-indigo-500', 'bg-indigo-100', 'dark:bg-indigo-900/60');
};
dropBadge.ondrop = async (e) => {
e.preventDefault();
dropBadge.classList.remove('ring-2', 'ring-indigo-500', 'bg-indigo-100', 'dark:bg-indigo-900/60');
const cardIdStr = e.dataTransfer.getData('text/plain');
const cardId = parseInt(cardIdStr, 10);
if (cardId && lane.id) {
await moveTimelineCardToLane(cardId, lane.id);
}
};
bar.appendChild(dropBadge);
});
} }
function populateModalDropdowns() { function populateModalDropdowns() {
@@ -668,10 +720,26 @@ function renderTimeline() {
const laneDotColor = LANE_COLORS[card.lane_color] || 'bg-slate-500'; const laneDotColor = LANE_COLORS[card.lane_color] || 'bg-slate-500';
const hasSchedule = (card.start_date && card.end_date); const hasSchedule = (card.start_date && card.end_date);
// 1. Linkerpaneel Item // 1. Linkerpaneel Item (draggable naar lane drop zones)
const leftItem = document.createElement('div'); const leftItem = document.createElement('div');
leftItem.className = 'timeline-row h-12 px-3.5 flex items-center justify-between transition cursor-pointer hover:bg-slate-100/70 dark:hover:bg-slate-800/50'; leftItem.className = 'timeline-row h-12 px-3.5 flex items-center justify-between transition cursor-grab active:cursor-grabbing hover:bg-slate-100/70 dark:hover:bg-slate-800/50 group select-none';
leftItem.onclick = () => openCardModal(card.id); leftItem.setAttribute('draggable', 'true');
leftItem.setAttribute('data-card-id', card.id);
leftItem.ondragstart = (e) => {
e.dataTransfer.setData('text/plain', String(card.id));
e.dataTransfer.effectAllowed = 'move';
leftItem.classList.add('opacity-50', 'bg-indigo-50/50', 'dark:bg-indigo-950/30');
};
leftItem.ondragend = () => {
leftItem.classList.remove('opacity-50', 'bg-indigo-50/50', 'dark:bg-indigo-950/30');
};
leftItem.onclick = (e) => {
// Voorkom trigger als er net gedragged werd
if (e.defaultPrevented) return;
openCardModal(card.id);
};
// Date badge // Date badge
let dateBadge = '<span class="text-[10px] text-slate-400 italic">Niet gepland</span>'; let dateBadge = '<span class="text-[10px] text-slate-400 italic">Niet gepland</span>';
@@ -858,6 +926,29 @@ async function updateCardDates(cardId, startDate, endDate) {
} }
} }
// API call om een taak direct naar een andere lane / statuscategorie te verplaatsen
async function moveTimelineCardToLane(cardId, targetLaneId) {
try {
const res = await fetch('/api/board.php?action=move_card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
card_id: cardId,
target_lane_id: targetLaneId,
target_position: 999
})
});
const data = await res.json();
if (data.success) {
await loadBoardData(currentBoardId);
} else {
alert(data.error || 'Kon taak niet verplaatsen');
}
} catch (e) {
console.error('Fout bij verplaatsen naar lane:', e);
}
}
// Verslepen van tijdlijnbalken // Verslepen van tijdlijnbalken
function enableBarDragging(barEl, cardId, originalStart, originalEnd) { function enableBarDragging(barEl, cardId, originalStart, originalEnd) {
let isDragging = false; let isDragging = false;