Ai aanpassingen en slepen in planner blokken #69
@@ -113,6 +113,39 @@ try {
|
||||
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!
|
||||
if ($action === 'convert_to_roadmap') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
@@ -53,6 +53,10 @@ try {
|
||||
$fields[] = "status = ?";
|
||||
$params[] = $status;
|
||||
}
|
||||
if (isset($data['quarter']) && in_array($data['quarter'], ['Q1', 'Q2', 'Q3', 'Q4'], true)) {
|
||||
$fields[] = "quarter = ?";
|
||||
$params[] = $data['quarter'];
|
||||
}
|
||||
if ($progress !== null) {
|
||||
$fields[] = "progress = ?";
|
||||
$params[] = max(0, min(100, $progress));
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ if ($currentUser) {
|
||||
$error = null;
|
||||
$infoMessage = null;
|
||||
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'])) {
|
||||
$infoMessage = "Uw sessie is op afstand beëindigd of ingetrokken. Log opnieuw in om verder te gaan.";
|
||||
|
||||
+142
-16
@@ -121,33 +121,45 @@ $categories = [
|
||||
<?php foreach ($categories as $catKey => $catConfig):
|
||||
$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 -->
|
||||
<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">
|
||||
<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>
|
||||
</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) ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Cards Container -->
|
||||
<div class="flex-1 space-y-3">
|
||||
<!-- Cards Container as Drop Zone -->
|
||||
<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)): ?>
|
||||
<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.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?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 -->
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<div class="flex items-center justify-between gap-2 mb-2.5">
|
||||
<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-[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>
|
||||
|
||||
<?php if (!empty($card['converted_to_roadmap_item_id'])): ?>
|
||||
@@ -158,24 +170,26 @@ $categories = [
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Inhoud -->
|
||||
<p class="text-xs text-slate-800 dark:text-slate-200 leading-relaxed whitespace-pre-wrap font-medium">
|
||||
<!-- 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">
|
||||
<?= htmlspecialchars($card['content']) ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 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="flex items-center space-x-1.5">
|
||||
<span><?= htmlspecialchars($card['author_name'] ?? 'Collega') ?></span>
|
||||
<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>
|
||||
<?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>
|
||||
<span>Werk uit met AI</span>
|
||||
<span>AI</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<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">
|
||||
<i data-lucide="thumbs-up" class="w-3 h-3"></i>
|
||||
@@ -550,6 +564,118 @@ async function deleteCard(cardId) {
|
||||
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 ---
|
||||
function getSelectedCardIds() {
|
||||
const checkboxes = document.querySelectorAll('.card-checkbox:checked');
|
||||
|
||||
+172
-4
@@ -1240,8 +1240,12 @@ for ($h = $startHour; $h <= $endHour; $h++) {
|
||||
return !empty($t['time_block_start']) && (int)explode(':', $t['time_block_start'])[0] === $hourInt;
|
||||
});
|
||||
?>
|
||||
<div class="flex items-start group min-h-[48px] border-b border-slate-100 dark:border-slate-800/40 pb-2.5 last:border-0 min-w-0">
|
||||
<div class="w-12 sm:w-14 text-xs font-mono font-semibold text-slate-500 dark:text-slate-400 pt-1.5 flex-shrink-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"
|
||||
data-hour="<?= $hour ?>"
|
||||
ondragover="handleTimelineSlotDragOver(event)"
|
||||
ondragleave="handleTimelineSlotDragLeave(event)"
|
||||
ondrop="handleTimelineSlotDrop(event, '<?= $hour ?>')">
|
||||
<div class="w-12 sm:w-14 text-xs font-mono font-semibold text-slate-500 dark:text-slate-400 pt-1.5 flex-shrink-0 select-none">
|
||||
<?= $hour ?>
|
||||
</div>
|
||||
|
||||
@@ -1278,7 +1282,13 @@ for ($h = $startHour; $h <= $endHour; $h++) {
|
||||
'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 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">
|
||||
@@ -1322,7 +1332,13 @@ for ($h = $startHour; $h <= $endHour; $h++) {
|
||||
'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 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">
|
||||
@@ -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 ---
|
||||
const isOutlookConnected = <?= $isOutlookConnected ? 'true' : 'false' ?>;
|
||||
const syncIntervalSeconds = <?= (int)($syncIntervalSeconds ?? 30) ?>;
|
||||
|
||||
+95
-3
@@ -64,13 +64,21 @@ $quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
|
||||
|
||||
<!-- Filters: Quarters & Status -->
|
||||
<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">
|
||||
<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
|
||||
</a>
|
||||
<?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 ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
@@ -108,7 +116,14 @@ $quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
|
||||
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 -->
|
||||
<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) {
|
||||
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) => {
|
||||
if (e.key === 'Escape') {
|
||||
const modal = document.getElementById('newItemModal');
|
||||
|
||||
+96
-5
@@ -164,13 +164,18 @@ html.dark #timelineGridWrapper::-webkit-scrollbar-thumb {
|
||||
<!-- 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">
|
||||
|
||||
<!-- 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="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 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 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>
|
||||
|
||||
<!-- Inline Snelle Taakinvoer: ⚡ Wat moet er gedaan worden? (Exact zoals Jira) -->
|
||||
@@ -528,6 +533,53 @@ function populateLaneFilter() {
|
||||
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() {
|
||||
@@ -668,10 +720,26 @@ function renderTimeline() {
|
||||
const laneDotColor = LANE_COLORS[card.lane_color] || 'bg-slate-500';
|
||||
const hasSchedule = (card.start_date && card.end_date);
|
||||
|
||||
// 1. Linkerpaneel Item
|
||||
// 1. Linkerpaneel Item (draggable naar lane drop zones)
|
||||
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.onclick = () => openCardModal(card.id);
|
||||
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.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
|
||||
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
|
||||
function enableBarDragging(barEl, cardId, originalStart, originalEnd) {
|
||||
let isDragging = false;
|
||||
|
||||
Reference in New Issue
Block a user