Files
Fociz/timeline.php
T
2026-09-12 22:28:03 +02:00

1359 lines
61 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Fociz Project Tijdlijn & Gantt Planning
* Interactieve tijdlijn per maand/dag met blauwe Vandaag-indicator,
* inline taakcreatie en 100% gedeelde projectdata met het Kanban-bord.
*/
$pageTitle = 'Fociz — Project Tijdlijn';
require_once __DIR__ . '/includes/header.php';
$userId = (int)($currentUser['id'] ?? 0);
$userRole = $currentUser['role'] ?? 'MEMBER';
$isAdmin = ($userRole === 'ADMIN');
?>
<style>
/* Tijdlijn styling */
.timeline-row:hover {
background-color: rgba(99, 102, 241, 0.04);
}
html.dark .timeline-row:hover {
background-color: rgba(99, 102, 241, 0.08);
}
.timeline-bar {
transition: box-shadow 0.15s ease, transform 0.1s ease;
user-select: none;
}
.timeline-bar:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.25);
z-index: 20;
}
.today-line {
pointer-events: none;
z-index: 30;
}
/* Custom scrollbar voor timeline grid */
#timelineGridWrapper::-webkit-scrollbar {
height: 8px;
}
#timelineGridWrapper::-webkit-scrollbar-track {
background: transparent;
}
#timelineGridWrapper::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.35);
border-radius: 4px;
}
html.dark #timelineGridWrapper::-webkit-scrollbar-thumb {
background: rgba(51, 65, 85, 0.6);
}
</style>
<div id="timelineMainContainer" class="space-y-4 w-full">
<!-- 1. Bovenbalk: Projectkiezer, Weergave-schakelaar, Filters & Acties (ALTIJD vaste standaard breedte) -->
<div class="max-w-7xl mx-auto w-full">
<div class="fociz-card p-4 sm:p-5">
<div class="flex flex-col xl:flex-row xl:items-center justify-between gap-4">
<!-- Linkerkant: Projectkiezer & Titel -->
<div class="flex items-center space-x-3.5">
<div class="w-10 h-10 rounded-2xl bg-gradient-to-tr from-violet-600 to-indigo-600 text-white flex items-center justify-center shadow-sm shadow-indigo-500/20 flex-shrink-0">
<i data-lucide="clock" class="w-5 h-5"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<select id="boardSelector" onchange="onBoardSelectChange(this.value)" class="bg-transparent font-black text-xl sm:text-2xl text-slate-900 dark:text-white cursor-pointer focus:outline-none border-b border-transparent hover:border-indigo-500 transition py-0.5">
<option value="">Laden...</option>
</select>
<span id="boardRoleBadge" class="hidden px-2 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-wider bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-500/30">
Beheerder
</span>
</div>
<p id="boardDescriptionText" class="hidden text-xs text-slate-500 dark:text-slate-400 mt-0.5"></p>
</div>
</div>
<!-- Weergave-schakelaar: Bord (Kanban) vs Tijdlijn (Gantt) -->
<div class="inline-flex p-1 bg-slate-100/90 dark:bg-[#0c0f17] border border-slate-200/90 dark:border-slate-800/90 rounded-xl text-xs font-semibold shadow-2xs self-start xl:self-center">
<a id="linkToBoard" href="/board.php" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-200/60 dark:hover:bg-slate-800/60 transition">
<i data-lucide="kanban" class="w-3.5 h-3.5"></i>
<span>Bord</span>
</a>
<span class="flex items-center space-x-1.5 px-3 py-1.5 rounded-lg bg-white dark:bg-slate-800 text-indigo-600 dark:text-indigo-400 font-bold shadow-2xs cursor-default">
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
<span>Tijdlijn</span>
</span>
</div>
<!-- Midden & Rechts: Filters & Actieknoppen (zoals in Jira Timeline) -->
<div class="flex flex-wrap items-center gap-2">
<!-- Zoekbalk: Tijdlijn doorzoeken -->
<div class="relative">
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
<input
type="text"
id="timelineSearch"
placeholder="Tijdlijn doorzoeken..."
oninput="renderTimeline()"
class="pl-8 pr-3 py-2 text-xs bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 w-36 sm:w-44 shadow-2xs"
>
</div>
<!-- Filter: Assignee / Collega -->
<div class="relative">
<select id="filterAssignee" onchange="renderTimeline()" class="bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-700 dark:text-slate-300 font-medium focus:ring-2 focus:ring-indigo-500/30 cursor-pointer shadow-2xs">
<option value="ALL">Alle collega's</option>
<option value="ME">Mijn taken</option>
<option value="UNASSIGNED">Niet toegewezen</option>
</select>
</div>
<!-- Filter: Statuscategorie / Kolom -->
<div class="relative">
<select id="filterLane" onchange="renderTimeline()" class="bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-700 dark:text-slate-300 font-medium focus:ring-2 focus:ring-indigo-500/30 cursor-pointer shadow-2xs">
<option value="ALL">Statuscategorie (Alle)</option>
</select>
</div>
<!-- Filter: Snelle filters (Ingepland / Urgent) -->
<div class="relative">
<select id="filterQuick" onchange="renderTimeline()" class="bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-700 dark:text-slate-300 font-medium focus:ring-2 focus:ring-indigo-500/30 cursor-pointer shadow-2xs">
<option value="ALL">Snelle filters</option>
<option value="SCHEDULED">Alleen ingepland</option>
<option value="UNSCHEDULED">Nog niet ingepland</option>
<option value="URGENT">P1 Urgent</option>
</select>
</div>
<!-- Knop Vandaag: direct scrollen naar huidige datum -->
<button
type="button"
onclick="scrollToToday()"
title="Direct scrollen naar vandaag"
class="px-3 py-2 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-200 font-bold text-xs rounded-xl transition flex items-center space-x-1.5 shadow-2xs cursor-pointer"
>
<span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
<span>Vandaag</span>
</button>
<!-- Hoofdactie: Nieuwe Taak / Opdracht toevoegen -->
<button
onclick="openCardModal(null)"
class="px-4 py-2 bg-gradient-to-r from-indigo-600 to-indigo-500 hover:from-indigo-500 hover:to-indigo-400 text-white font-semibold text-xs rounded-xl transition flex items-center space-x-1.5 shadow-sm shadow-indigo-600/30 cursor-pointer"
>
<i data-lucide="plus" class="w-4 h-4 stroke-[2.5]"></i>
<span>Nieuwe Taak</span>
</button>
</div>
</div>
</div>
</div>
<!-- 2. Tijdlijn Split-View Container (Volledige breedte benutting zoals gewenst) -->
<div class="w-full transition-all duration-300">
<div class="fociz-card p-0 overflow-hidden shadow-md flex flex-col min-h-[600px]">
<div class="flex flex-1 overflow-hidden relative">
<!-- 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 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">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) -->
<div class="p-2.5 border-b border-slate-200/60 dark:border-slate-800/60 bg-slate-50/40 dark:bg-[#111624]/40">
<form onsubmit="handleQuickCreateTask(event)" class="relative flex items-center">
<i data-lucide="zap" class="w-3.5 h-3.5 text-indigo-500 absolute left-3 top-1/2 -translate-y-1/2"></i>
<input
type="text"
id="quickTaskInput"
placeholder="Wat moet er gedaan worden? (Druk op Enter)"
class="w-full pl-8 pr-8 py-2 text-xs bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/40 shadow-2xs"
>
<button type="submit" title="Taak direct toevoegen" class="absolute right-2 p-1 text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">
<i data-lucide="corner-down-left" class="w-3.5 h-3.5"></i>
</button>
</form>
</div>
<!-- Werk Items Rijen Lijst -->
<div id="workItemsListContainer" class="flex-1 overflow-y-auto divide-y divide-slate-100 dark:divide-slate-800/40 select-none">
<!-- Dynamisch gevuld via renderTimeline() -->
</div>
</div>
<!-- RECHTERPANEEL: Interactief Gantt Tijdlijn Rooster -->
<div id="timelineGridWrapper" class="flex-1 overflow-x-auto overflow-y-hidden flex flex-col relative bg-slate-50/30 dark:bg-[#080a10]">
<!-- Maanden / Weken Kalender Header -->
<div id="timelineCalendarHeader" class="h-12 border-b border-slate-200/80 dark:border-slate-800/80 flex items-center sticky top-0 bg-slate-50/90 dark:bg-slate-900/90 backdrop-blur-sm z-10 select-none font-semibold text-xs text-slate-600 dark:text-slate-300">
<!-- Dynamisch gevuld via generateCalendarHeader() -->
</div>
<!-- Rijen Grid voor Taken en Balken -->
<div id="timelineRowsGrid" class="flex-1 relative divide-y divide-slate-100 dark:divide-slate-800/40 min-h-[480px]">
<!-- Verticale "Vandaag" Blauwe Lijn (loopt van top naar bottom over alle rijen) -->
<div id="todayIndicatorLine" class="today-line absolute top-0 bottom-0 border-l-2 border-blue-500 dark:border-blue-400 hidden">
<div class="w-2.5 h-2.5 rounded-full bg-blue-500 -ml-[5px] -mt-1.5 shadow-xs"></div>
</div>
<!-- Rijen per taak dynamically rendered -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Gedeelde MODAL 1: Taak Toevoegen of Bewerken (100% gesynchroniseerd met Kanban bord) -->
<div id="cardModal" onclick="if(event.target === this) closeCardModal()" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
<div class="fociz-card p-6 w-full max-w-xl shadow-2xl space-y-5 my-8">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
<i data-lucide="edit-3" class="w-4 h-4"></i>
</div>
<h3 id="cardModalTitle" class="text-base font-bold text-slate-900 dark:text-white">Taak / Opdracht Bewerken</h3>
</div>
<button onclick="closeCardModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1.5 rounded-lg transition cursor-pointer">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<form id="cardForm" onsubmit="saveCardForm(event)" class="space-y-4 text-xs">
<input type="hidden" id="cardId" value="">
<!-- Titel van de opdracht -->
<div>
<label class="block font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1">
Titel van de Taak *
</label>
<input
type="text"
id="cardTitle"
required
placeholder="Bijv. API koppeling testen, Klantvoorstel schrijven"
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/40"
>
</div>
<!-- Beschrijving & Details -->
<div>
<label class="block font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1">
Omschrijving & Briefing
</label>
<textarea
id="cardDescription"
rows="3"
placeholder="Voeg toelichting, links of instructies toe..."
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 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
></textarea>
</div>
<!-- Grid: Kolom / Status, Toewijzing (Assignee), Prioriteit & Kleur -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<!-- Kolom / Lane -->
<div>
<label class="block font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1">
Status / Kolom *
</label>
<select id="cardLaneSelect" 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-slate-900 dark:text-white focus:ring-2 focus:ring-indigo-500/40">
<!-- Dynamisch gevuld -->
</select>
</div>
<!-- Toewijzen aan Collega -->
<div>
<label class="block font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1">
Toewijzen aan Collega
</label>
<select id="cardAssigneeSelect" 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-slate-900 dark:text-white focus:ring-2 focus:ring-indigo-500/40">
<option value="">-- Niet toegewezen --</option>
</select>
</div>
<!-- Prioriteit -->
<div>
<label class="block font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1">
Prioriteit
</label>
<select id="cardPrioritySelect" 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-slate-900 dark:text-white focus:ring-2 focus:ring-indigo-500/40">
<option value="P1">P1 — Urgent</option>
<option value="P2" selected>P2 — Hoog</option>
<option value="P3">P3 — Normaal</option>
</select>
</div>
<!-- Post-it Kaartkleur -->
<div>
<label class="block font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1">
Kleurthema
</label>
<select id="cardColorSelect" 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-slate-900 dark:text-white focus:ring-2 focus:ring-indigo-500/40">
<option value="yellow">Geel (Klassiek Post-it)</option>
<option value="blue">Blauw</option>
<option value="green">Groen</option>
<option value="purple">Paars</option>
<option value="pink">Roze</option>
<option value="orange">Oranje</option>
<option value="slate">Neutraal Slate</option>
</select>
</div>
</div>
<!-- Tijdlijn & Agenda Planning Sectie -->
<div class="p-4 rounded-xl bg-slate-50 dark:bg-slate-950/70 border border-slate-200 dark:border-slate-800 space-y-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<i data-lucide="calendar" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<span class="font-bold text-slate-900 dark:text-white">Tijdlijn & Agenda Planning</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="cardScheduleToggle" onchange="toggleScheduleFields(this.checked)" class="sr-only peer">
<div class="w-9 h-5 bg-slate-200 dark:bg-slate-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div id="cardScheduleFields" class="space-y-3 hidden pt-2 border-t border-slate-200/60 dark:border-slate-800/60">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">Startdatum *</label>
<input type="date" id="cardStartDate" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none">
</div>
<div>
<label class="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">Einddatum *</label>
<input type="date" id="cardEndDate" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none">
</div>
</div>
<!-- Knop Vrij Blok Zoeken voor Assignee -->
<div class="p-2.5 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-between gap-2">
<div class="text-[11px]">
<span class="font-bold text-amber-900 dark:text-amber-200 flex items-center space-x-1">
<i data-lucide="sparkles" class="w-3 h-3 text-amber-500"></i>
<span>Vrij agendablok zoeken</span>
</span>
<span class="text-[10px] text-amber-700 dark:text-amber-400">Zoekt binnen werktijden van de toegewezen collega</span>
</div>
<button type="button" id="btnTimelineFindSlot" onclick="findFreeSlotForTimelineAssignee()" class="px-2.5 py-1 rounded-lg bg-amber-500 hover:bg-amber-600 text-white font-bold text-[10px] shadow-xs transition flex items-center space-x-1 flex-shrink-0 cursor-pointer">
<i data-lucide="search" class="w-3 h-3"></i>
<span>Zoek vrij blok</span>
</button>
</div>
<div id="timelineFoundSlotNotice" class="hidden p-2 rounded-lg bg-emerald-50 dark:bg-emerald-950/40 border border-emerald-300 dark:border-emerald-800 text-[11px] text-emerald-800 dark:text-emerald-300 font-medium flex items-center space-x-1.5">
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-500 flex-shrink-0"></i>
<span id="timelineFoundSlotText"></span>
</div>
<!-- Outlook Agenda Synchronisatie -->
<div class="flex items-center justify-between pt-2 border-t border-slate-200/50 dark:border-slate-800/50">
<div class="flex items-center space-x-2">
<i data-lucide="calendar-check" class="w-3.5 h-3.5 text-sky-600 dark:text-sky-400"></i>
<div>
<span class="text-[11px] font-semibold text-slate-800 dark:text-slate-200">Outlook 365 Agenda Blokkering</span>
<span class="text-[10px] text-slate-500 dark:text-slate-400 block">Afspraak aanmaken in de agenda van de collega</span>
</div>
</div>
<input type="checkbox" id="cardSyncOutlook" class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 cursor-pointer">
</div>
</div>
</div>
<!-- Knoppen & Acties -->
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80">
<button
type="button"
id="btnDeleteCard"
onclick="deleteCurrentCard()"
class="hidden text-rose-600 hover:text-rose-700 dark:text-rose-400 text-xs font-semibold flex items-center space-x-1 cursor-pointer"
>
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>Verwijderen</span>
</button>
<div class="flex items-center space-x-2 ml-auto">
<button type="button" onclick="closeCardModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
Annuleren
</button>
<button type="submit" id="btnSaveCard" class="px-5 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs rounded-xl transition shadow-xs cursor-pointer">
Opslaan
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Rechtermuisknop Contextmenu voor Tijdlijn Items -->
<div id="timelineContextMenu" class="hidden fixed z-50 w-56 bg-white dark:bg-slate-900 rounded-xl shadow-2xl border border-slate-200 dark:border-slate-800 py-1.5 text-xs text-slate-700 dark:text-slate-200">
<button onclick="triggerTimelineCtxEdit()" 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>Taak bewerken</span>
</button>
<button onclick="triggerTimelineCtxQuickSlot()" 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>
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
<button onclick="triggerTimelineCtxDelete()" 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>Taak verwijderen</span>
</button>
</div>
<script>
// --- Globale Status voor Tijdlijn ---
let currentBoardId = null;
let currentBoardData = null;
let allBoards = [];
const currentUserId = <?= (int)$userId ?>;
const isUserAdmin = <?= $isAdmin ? 'true' : 'false' ?>;
// Kleuren voor de balken per post-it kleur
const TIMELINE_BAR_COLORS = {
yellow: 'bg-amber-500/90 text-amber-950 border-amber-400 dark:bg-amber-500/80 dark:text-amber-100 dark:border-amber-400/30',
blue: 'bg-sky-500/90 text-sky-950 border-sky-400 dark:bg-sky-500/80 dark:text-sky-100 dark:border-sky-400/30',
green: 'bg-emerald-500/90 text-emerald-950 border-emerald-400 dark:bg-emerald-500/80 dark:text-emerald-100 dark:border-emerald-400/30',
purple: 'bg-purple-500/90 text-purple-950 border-purple-400 dark:bg-purple-500/80 dark:text-purple-100 dark:border-purple-400/30',
pink: 'bg-pink-500/90 text-pink-950 border-pink-400 dark:bg-pink-500/80 dark:text-pink-100 dark:border-pink-400/30',
orange: 'bg-orange-500/90 text-orange-950 border-orange-400 dark:bg-orange-500/80 dark:text-orange-100 dark:border-orange-400/30',
slate: 'bg-slate-600/90 text-white border-slate-500 dark:bg-slate-700/80 dark:text-slate-100 dark:border-slate-600'
};
const LANE_COLORS = {
slate: 'bg-slate-500',
amber: 'bg-amber-500',
purple: 'bg-purple-500',
emerald: 'bg-emerald-500',
sky: 'bg-sky-500',
rose: 'bg-rose-500'
};
// Kalender instellingen: dagbreedte in pixels & datumbereik
const PX_PER_DAY = 14; // pixels per dag (voldoende compact en overzichtelijk)
let timelineStartDate = null;
let timelineEndDate = null;
let totalTimelineDays = 0;
// 1. Initialisatie
document.addEventListener('DOMContentLoaded', async () => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('board_id')) {
currentBoardId = parseInt(urlParams.get('board_id'), 10);
}
setupTimelineCalendarRange();
await loadBoardsList();
});
// Configureert kalenderbereik: 2 maanden terug t/m 8 maanden vooruit
function setupTimelineCalendarRange() {
const today = new Date();
// 2 maanden terug op de 1e van de maand
timelineStartDate = new Date(today.getFullYear(), today.getMonth() - 2, 1);
// 8 maanden vooruit op de laatste van de maand
timelineEndDate = new Date(today.getFullYear(), today.getMonth() + 9, 0);
totalTimelineDays = Math.round((timelineEndDate - timelineStartDate) / (1000 * 60 * 60 * 24)) + 1;
}
// Haalt alle borden op
async function loadBoardsList() {
try {
const res = await fetch('/api/board.php?action=get_boards');
const data = await res.json();
if (data.success && data.boards) {
allBoards = data.boards;
populateBoardSelector();
if (allBoards.length > 0) {
const activeId = currentBoardId || allBoards[0].id;
await loadBoardData(activeId);
}
}
} catch (e) {
console.error('Fout bij laden borden:', e);
}
}
function populateBoardSelector() {
const sel = document.getElementById('boardSelector');
sel.innerHTML = '';
allBoards.forEach(b => {
const opt = document.createElement('option');
opt.value = b.id;
opt.innerText = b.title;
if (String(b.id) === String(currentBoardId)) opt.selected = true;
sel.appendChild(opt);
});
}
function onBoardSelectChange(bId) {
if (bId) {
loadBoardData(parseInt(bId, 10));
}
}
// 2. Data inladen voor het geselecteerde project
async function loadBoardData(boardId) {
currentBoardId = boardId;
document.getElementById('linkToBoard')?.setAttribute('href', `/board.php?board_id=${boardId}`);
try {
const res = await fetch(`/api/board.php?action=get_board_data&board_id=${boardId}`);
const data = await res.json();
if (data.success && data.has_board) {
currentBoardData = data;
const descEl = document.getElementById('boardDescriptionText');
if (data.board.description && data.board.description.trim() !== '') {
descEl.innerText = data.board.description;
descEl.classList.remove('hidden');
} else {
descEl.innerText = '';
descEl.classList.add('hidden');
}
// Beheerdersbadge
const badge = document.getElementById('boardRoleBadge');
if (data.is_board_admin) {
badge.classList.remove('hidden');
badge.innerText = 'Beheerder';
} else {
badge.classList.add('hidden');
}
// Vul filter dropdowns
populateLaneFilter();
populateModalDropdowns();
// Render tijdlijn
generateCalendarHeader();
renderTimeline();
// Automatisch scrollen naar vandaag na eerste render
setTimeout(scrollToToday, 200);
}
} catch (err) {
console.error('Fout bij ophalen tijdlijndata:', err);
}
}
function populateLaneFilter() {
const sel = document.getElementById('filterLane');
sel.innerHTML = '<option value="ALL">Statuscategorie (Alle)</option>';
if (currentBoardData && currentBoardData.lanes) {
currentBoardData.lanes.forEach(lane => {
const opt = document.createElement('option');
opt.value = lane.id;
opt.innerText = lane.title;
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) {
const el = document.querySelector(`.timeline-row[data-card-id="${cardId}"]`);
if (el) el.classList.remove('opacity-50', 'bg-indigo-50/50', 'dark:bg-indigo-950/30', 'pointer-events-none');
}
document.querySelectorAll('.timeline-row.opacity-50').forEach(el => {
el.classList.remove('opacity-50', 'bg-indigo-50/50', 'dark:bg-indigo-950/30', 'pointer-events-none');
});
if (cardId && lane.id) {
await moveTimelineCardToLane(cardId, lane.id);
}
};
bar.appendChild(dropBadge);
});
}
function populateModalDropdowns() {
if (!currentBoardData) return;
// Status / Lane select in modal
const laneSel = document.getElementById('cardLaneSelect');
laneSel.innerHTML = '';
(currentBoardData.lanes || []).forEach(l => {
const opt = document.createElement('option');
opt.value = l.id;
opt.innerText = l.title;
laneSel.appendChild(opt);
});
// Assignee select in modal
const assSel = document.getElementById('cardAssigneeSelect');
assSel.innerHTML = '<option value="">-- Niet toegewezen --</option>';
(currentBoardData.team_members || []).forEach(tm => {
const opt = document.createElement('option');
opt.value = tm.id;
opt.innerText = `${tm.name} ${tm.has_outlook ? '(Outlook 365)' : ''}`;
assSel.appendChild(opt);
});
}
// 3. Kalender Header Genereren (Maanden & Weken zoals in Jira)
const MONTH_NAMES_NL = ['Januari', 'Februari', 'Maart', 'April', 'Mei', 'Juni', 'Juli', 'Augustus', 'September', 'Oktober', 'November', 'December'];
function generateCalendarHeader() {
const header = document.getElementById('timelineCalendarHeader');
header.innerHTML = '';
const totalWidthPx = totalTimelineDays * PX_PER_DAY;
header.style.width = `${totalWidthPx}px`;
let cur = new Date(timelineStartDate);
while (cur <= timelineEndDate) {
const year = cur.getFullYear();
const month = cur.getMonth();
const monthName = MONTH_NAMES_NL[month];
// Bepaal aantal dagen van deze maand binnen ons bereik
const daysInMonth = new Date(year, month + 1, 0).getDate();
const monthWidthPx = daysInMonth * PX_PER_DAY;
const monthEl = document.createElement('div');
monthEl.className = 'border-r border-slate-200 dark:border-slate-800/80 px-3 flex items-center justify-between text-xs font-bold tracking-tight text-slate-700 dark:text-slate-300 flex-shrink-0';
monthEl.style.width = `${monthWidthPx}px`;
monthEl.innerHTML = `
<span>${monthName} ${year}</span>
<span class="text-[10px] text-slate-400 font-normal hidden sm:inline">${daysInMonth}d</span>
`;
header.appendChild(monthEl);
// Ga naar 1e van volgende maand
cur = new Date(year, month + 1, 1);
}
}
// 4. Render Tijdlijn: Takenlijst links & Gantt rijen rechts
function renderTimeline() {
if (!currentBoardData) return;
const workContainer = document.getElementById('workItemsListContainer');
const rowsGrid = document.getElementById('timelineRowsGrid');
workContainer.innerHTML = '';
// Verwijder oude rijen uit rowsGrid (behalve de today line)
const existingRows = rowsGrid.querySelectorAll('.timeline-task-row');
existingRows.forEach(r => r.remove());
const totalWidthPx = totalTimelineDays * PX_PER_DAY;
rowsGrid.style.width = `${totalWidthPx}px`;
const filterAssignee = document.getElementById('filterAssignee').value;
const filterLane = document.getElementById('filterLane').value;
const filterQuick = document.getElementById('filterQuick').value;
const filterSearch = document.getElementById('timelineSearch').value.toLowerCase().trim();
const lanes = currentBoardData.lanes || [];
const cardsByLane = currentBoardData.cards_by_lane || {};
// Verzamel alle kaarten
let allCards = [];
lanes.forEach(lane => {
const cList = cardsByLane[lane.id] || [];
cList.forEach(c => {
allCards.push({ ...c, lane_title: lane.title, lane_color: lane.color });
});
});
// Filters toepassen
if (filterAssignee === 'ME') {
allCards = allCards.filter(c => c.assignee_id && parseInt(c.assignee_id, 10) === currentUserId);
} else if (filterAssignee === 'UNASSIGNED') {
allCards = allCards.filter(c => !c.assignee_id);
}
if (filterLane !== 'ALL') {
allCards = allCards.filter(c => String(c.lane_id) === String(filterLane));
}
if (filterQuick === 'SCHEDULED') {
allCards = allCards.filter(c => c.start_date && c.end_date);
} else if (filterQuick === 'UNSCHEDULED') {
allCards = allCards.filter(c => !c.start_date || !c.end_date);
} else if (filterQuick === 'URGENT') {
allCards = allCards.filter(c => c.priority === 'P1');
}
if (filterSearch) {
allCards = allCards.filter(c =>
(c.title && c.title.toLowerCase().includes(filterSearch)) ||
(c.description && c.description.toLowerCase().includes(filterSearch))
);
}
// Update teller
document.getElementById('workItemsCountBadge').innerText = allCards.length;
// Vandaag Indicator Lijn positioneren
updateTodayIndicator();
if (allCards.length === 0) {
workContainer.innerHTML = `
<div class="py-16 text-center text-xs text-slate-400 italic">
Geen taken gevonden die voldoen aan de filters.
</div>
`;
return;
}
// Render elke taak: linker werkitem + rechter tijdlijn rij
allCards.forEach(card => {
const laneDotColor = LANE_COLORS[card.lane_color] || 'bg-slate-500';
const hasSchedule = (card.start_date && card.end_date);
// 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-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.oncontextmenu = (e) => {
openTimelineContextMenu(e, card.id);
return false;
};
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>';
if (hasSchedule) {
const sFormatted = formatDateShort(card.start_date);
const eFormatted = formatDateShort(card.end_date);
dateBadge = `<span class="text-[10px] font-semibold text-slate-600 dark:text-slate-300 bg-slate-100 dark:bg-slate-800 px-2 py-0.5 rounded">${sFormatted} - ${eFormatted}</span>`;
}
leftItem.innerHTML = `
<div class="flex items-center space-x-2.5 min-w-0 pr-2">
<span class="w-2.5 h-2.5 rounded-full ${laneDotColor} flex-shrink-0" title="Status: ${escapeHtml(card.lane_title)}"></span>
<span class="text-xs font-semibold text-slate-800 dark:text-slate-200 truncate" title="${escapeHtml(card.title)}">
${escapeHtml(card.title)}
</span>
</div>
<div class="flex items-center space-x-2 flex-shrink-0">
${dateBadge}
${card.assignee_name ? (
card.assignee_avatar
? `<img src="${escapeHtml(card.assignee_avatar)}" class="w-5 h-5 rounded-full object-cover flex-shrink-0 bg-white dark:bg-slate-900 border border-indigo-200 dark:border-indigo-800/60" title="${escapeHtml(card.assignee_name)}">`
: `<div class="w-5 h-5 rounded-full bg-gradient-to-tr from-indigo-600 to-sky-500 text-white font-bold text-[9px] flex items-center justify-center flex-shrink-0" title="${escapeHtml(card.assignee_name)}">${card.assignee_name.charAt(0).toUpperCase()}</div>`
) : ''}
</div>
`;
workContainer.appendChild(leftItem);
// 2. Rechterpaneel Tijdlijn Rij
const rightRow = document.createElement('div');
rightRow.className = 'timeline-task-row timeline-row h-12 relative flex items-center';
rightRow.setAttribute('data-card-id', card.id);
rightRow.oncontextmenu = (e) => {
openTimelineContextMenu(e, card.id);
return false;
};
// Klikken op een lege plek in de rij om direct in te plannen
rightRow.onclick = (e) => {
if (e.target.closest('.timeline-bar')) return;
handleGridRowClick(e, card.id);
};
// Render tijdlijnbalk indien ingepland
if (hasSchedule) {
const sDate = new Date(card.start_date + 'T00:00:00');
const eDate = new Date(card.end_date + 'T23:59:59');
const startOffsetDays = (sDate - timelineStartDate) / (1000 * 60 * 60 * 24);
const durationDays = Math.max(1, Math.round((eDate - sDate) / (1000 * 60 * 60 * 24)));
const leftPx = Math.max(0, startOffsetDays * PX_PER_DAY);
const widthPx = Math.max(PX_PER_DAY * 2, durationDays * PX_PER_DAY);
const barColorClass = TIMELINE_BAR_COLORS[card.color] || TIMELINE_BAR_COLORS.yellow;
const barEl = document.createElement('div');
barEl.className = `timeline-bar absolute h-7 rounded-lg border px-2.5 flex items-center text-xs font-bold truncate shadow-xs cursor-pointer ${barColorClass}`;
barEl.style.left = `${leftPx}px`;
barEl.style.width = `${widthPx}px`;
barEl.title = `${escapeHtml(card.title)} (${card.start_date} t/m ${card.end_date})`;
barEl.oncontextmenu = (ev) => {
openTimelineContextMenu(ev, card.id);
return false;
};
barEl.onclick = (ev) => {
ev.stopPropagation();
openCardModal(card.id);
};
barEl.innerHTML = `
<span class="truncate leading-none">${escapeHtml(card.title)}</span>
`;
// Maak de balk versleepbaar om datums direct aan te passen
enableBarDragging(barEl, card.id, card.start_date, card.end_date);
rightRow.appendChild(barEl);
}
rowsGrid.appendChild(rightRow);
});
if (typeof lucide !== 'undefined') lucide.createIcons();
}
// Positioneert de blauwe "Vandaag" lijn exact over de datum van vandaag
function updateTodayIndicator() {
const line = document.getElementById('todayIndicatorLine');
if (!line || !timelineStartDate) return;
const today = new Date();
today.setHours(12, 0, 0, 0);
if (today >= timelineStartDate && today <= timelineEndDate) {
const offsetDays = (today - timelineStartDate) / (1000 * 60 * 60 * 24);
const leftPx = offsetDays * PX_PER_DAY;
line.style.left = `${leftPx}px`;
line.classList.remove('hidden');
} else {
line.classList.add('hidden');
}
}
// Scrollt het tijdlijnrooster direct naar vandaag
function scrollToToday() {
const line = document.getElementById('todayIndicatorLine');
const wrapper = document.getElementById('timelineGridWrapper');
if (line && wrapper) {
const leftPx = parseFloat(line.style.left || '0');
const viewWidth = wrapper.clientWidth;
wrapper.scrollTo({
left: Math.max(0, leftPx - (viewWidth / 3)),
behavior: 'smooth'
});
}
}
// 5. Snelle Inline Taak Aanmaken: ⚡ Wat moet er gedaan worden?
async function handleQuickCreateTask(e) {
e.preventDefault();
const input = document.getElementById('quickTaskInput');
const title = input.value.trim();
if (!title || !currentBoardId) return;
// Bepaal default start- en einddatum (vandaag t/m over 7 dagen)
const today = new Date();
const nextWeek = new Date();
nextWeek.setDate(today.getDate() + 7);
const sStr = today.toISOString().split('T')[0];
const eStr = nextWeek.toISOString().split('T')[0];
try {
const res = await fetch('/api/board.php?action=quick_create_card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
board_id: currentBoardId,
title: title,
start_date: sStr,
end_date: eStr
})
});
const data = await res.json();
if (data.success) {
input.value = '';
await loadBoardData(currentBoardId);
} else {
alert(data.error || 'Kon taak niet toevoegen');
}
} catch (err) {
console.error('Fout bij snelle taakinvoer:', err);
}
}
// 6. Klikken op een leeg gedeelte van een rij -> direct datums toewijzen
async function handleGridRowClick(e, cardId) {
const rowRect = e.currentTarget.getBoundingClientRect();
const clickX = e.clientX - rowRect.left;
const clickOffsetDays = Math.floor(clickX / PX_PER_DAY);
const clickedDate = new Date(timelineStartDate);
clickedDate.setDate(clickedDate.getDate() + clickOffsetDays);
const endDate = new Date(clickedDate);
endDate.setDate(endDate.getDate() + 5); // standaard 5 dagen
const sStr = clickedDate.toISOString().split('T')[0];
const eStr = endDate.toISOString().split('T')[0];
await updateCardDates(cardId, sStr, eStr);
}
// API call om start en einddatum direct bij te werken
async function updateCardDates(cardId, startDate, endDate) {
try {
const res = await fetch('/api/board.php?action=update_card_dates', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
card_id: cardId,
start_date: startDate,
end_date: endDate
})
});
const data = await res.json();
if (data.success) {
await loadBoardData(currentBoardId);
}
} catch (e) {
console.error('Fout bij bijwerken datums:', e);
}
}
// 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;
let startMouseX = 0;
let initialLeftPx = 0;
barEl.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
isDragging = true;
startMouseX = e.clientX;
initialLeftPx = parseFloat(barEl.style.left);
barEl.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
const onMouseMove = (moveEvent) => {
if (!isDragging) return;
const deltaX = moveEvent.clientX - startMouseX;
barEl.style.left = `${Math.max(0, initialLeftPx + deltaX)}px`;
};
const onMouseUp = async (upEvent) => {
if (!isDragging) return;
isDragging = false;
barEl.style.cursor = 'pointer';
document.body.style.userSelect = '';
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
const finalDeltaX = upEvent.clientX - startMouseX;
const deltaDays = Math.round(finalDeltaX / PX_PER_DAY);
if (deltaDays !== 0) {
const s = new Date(originalStart);
const e = new Date(originalEnd);
s.setDate(s.getDate() + deltaDays);
e.setDate(e.getDate() + deltaDays);
await updateCardDates(cardId, s.toISOString().split('T')[0], e.toISOString().split('T')[0]);
}
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
});
}
// 7. Taak Modal Handlers (100% gedeeld met Kanban)
function openCardModal(cardId = null) {
const modal = document.getElementById('cardModal');
const form = document.getElementById('cardForm');
form.reset();
const titleEl = document.getElementById('cardModalTitle');
const deleteBtn = document.getElementById('btnDeleteCard');
document.getElementById('cardId').value = cardId || '';
if (cardId) {
titleEl.innerText = 'Taak Bewerken';
deleteBtn.classList.remove('hidden');
// Zoek kaart op in data
let foundCard = null;
if (currentBoardData && currentBoardData.cards_by_lane) {
for (const lId in currentBoardData.cards_by_lane) {
const match = currentBoardData.cards_by_lane[lId].find(c => c.id === cardId);
if (match) { foundCard = match; break; }
}
}
if (foundCard) {
document.getElementById('cardTitle').value = foundCard.title || '';
document.getElementById('cardDescription').value = foundCard.description || '';
document.getElementById('cardLaneSelect').value = foundCard.lane_id || '';
document.getElementById('cardAssigneeSelect').value = foundCard.assignee_id || '';
document.getElementById('cardPrioritySelect').value = foundCard.priority || 'P2';
document.getElementById('cardColorSelect').value = foundCard.color || 'yellow';
// Tijdlijn/agenda
const hasSched = !!(foundCard.start_date && foundCard.end_date);
document.getElementById('cardScheduleToggle').checked = hasSched;
toggleScheduleFields(hasSched);
if (hasSched) {
document.getElementById('cardStartDate').value = foundCard.start_date || '';
document.getElementById('cardEndDate').value = foundCard.end_date || '';
document.getElementById('cardSyncOutlook').checked = !!foundCard.outlook_event_id;
}
}
} else {
titleEl.innerText = 'Nieuwe Taak Toevoegen';
deleteBtn.classList.add('hidden');
document.getElementById('cardScheduleToggle').checked = false;
toggleScheduleFields(false);
}
modal.classList.remove('hidden');
modal.classList.add('flex');
document.getElementById('cardTitle').focus();
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closeCardModal() {
const modal = document.getElementById('cardModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
function toggleScheduleFields(show) {
const fields = document.getElementById('cardScheduleFields');
if (show) {
fields.classList.remove('hidden');
if (!document.getElementById('cardStartDate').value) {
const today = new Date().toISOString().split('T')[0];
document.getElementById('cardStartDate').value = today;
document.getElementById('cardEndDate').value = today;
}
} else {
fields.classList.add('hidden');
}
}
async function saveCardForm(e) {
e.preventDefault();
const btn = document.getElementById('btnSaveCard');
btn.disabled = true;
btn.innerText = 'Opslaan...';
const cardId = parseInt(document.getElementById('cardId').value, 10) || 0;
const isScheduleEnabled = document.getElementById('cardScheduleToggle').checked;
const payload = {
board_id: currentBoardId,
card_id: cardId,
title: document.getElementById('cardTitle').value.trim(),
description: document.getElementById('cardDescription').value.trim(),
lane_id: parseInt(document.getElementById('cardLaneSelect').value, 10),
assignee_id: document.getElementById('cardAssigneeSelect').value || null,
priority: document.getElementById('cardPrioritySelect').value,
color: document.getElementById('cardColorSelect').value,
schedule: {
enabled: isScheduleEnabled,
type: 'DAILY_RANGE',
start_date: document.getElementById('cardStartDate').value,
end_date: document.getElementById('cardEndDate').value,
start_time: '09:00',
end_time: '17:00',
sync_outlook: document.getElementById('cardSyncOutlook').checked
}
};
try {
const res = await fetch('/api/board.php?action=save_card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.success) {
closeCardModal();
await loadBoardData(currentBoardId);
} else {
alert(data.error || 'Kon taak niet opslaan');
}
} catch (err) {
alert('Netwerkfout bij opslaan taak');
} finally {
btn.disabled = false;
btn.innerText = 'Opslaan';
}
}
async function deleteCurrentCard() {
const cardId = parseInt(document.getElementById('cardId').value, 10);
if (!cardId) return;
if (!confirm('Weet je zeker dat je deze taak wilt verwijderen?')) return;
try {
const res = await fetch('/api/board.php?action=delete_card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ card_id: cardId })
});
const data = await res.json();
if (data.success) {
closeCardModal();
await loadBoardData(currentBoardId);
} else {
alert(data.error || 'Kon taak niet verwijderen');
}
} catch (e) {
alert('Netwerkfout bij verwijderen taak');
}
}
// Datum formaat helper
function formatDateShort(dateStr) {
if (!dateStr) return '';
const parts = dateStr.split('-');
if (parts.length < 3) return dateStr;
const d = parseInt(parts[2], 10);
const mIdx = parseInt(parts[1], 10) - 1;
const months = ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
return `${d} ${months[mIdx] || ''}`;
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// --- Rechtermuisknop Context Menu voor Tijdlijn ---
let activeTimelineCtxCardId = null;
function openTimelineContextMenu(e, cardId) {
e.preventDefault();
e.stopPropagation();
activeTimelineCtxCardId = cardId;
const menu = document.getElementById('timelineContextMenu');
if (!menu) return;
menu.classList.remove('hidden');
const x = Math.min(e.clientX, window.innerWidth - 240);
const y = Math.min(e.clientY, window.innerHeight - 240);
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closeTimelineContextMenu() {
const menu = document.getElementById('timelineContextMenu');
if (menu) menu.classList.add('hidden');
activeTimelineCtxCardId = null;
}
document.addEventListener('click', (e) => {
if (!e.target.closest('#timelineContextMenu')) {
closeTimelineContextMenu();
}
});
function triggerTimelineCtxEdit() {
if (!activeTimelineCtxCardId) return;
const cid = activeTimelineCtxCardId;
closeTimelineContextMenu();
openCardModal(cid);
}
function triggerTimelineCtxQuickSlot() {
if (!activeTimelineCtxCardId) return;
const cid = activeTimelineCtxCardId;
closeTimelineContextMenu();
openCardModal(cid);
setTimeout(() => {
findFreeSlotForTimelineAssignee();
}, 150);
}
async function triggerTimelineCtxDelete() {
if (!activeTimelineCtxCardId) return;
const cid = activeTimelineCtxCardId;
closeTimelineContextMenu();
if (!confirm('Weet je zeker dat je deze taak wilt verwijderen?')) return;
try {
const res = await fetch('/api/board.php?action=delete_card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ card_id: cid })
});
const data = await res.json();
if (data.success) {
await loadBoardData(currentBoardId);
} else {
alert(data.error || 'Kon taak niet verwijderen');
}
} catch(e) {
alert('Netwerkfout bij verwijderen');
}
}
// Zoek vrij agendablok voor de in de tijdlijn-modal geselecteerde assignee
async function findFreeSlotForTimelineAssignee() {
const sel = document.getElementById('cardAssigneeSelect');
const targetUserId = sel.value ? parseInt(sel.value, 10) : currentUserId;
const schedToggle = document.getElementById('cardScheduleToggle');
if (!schedToggle.checked) {
schedToggle.checked = true;
toggleScheduleFields(true);
}
const btn = document.getElementById('btnTimelineFindSlot');
const origHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i data-lucide="loader-2" class="w-3 h-3 animate-spin"></i><span>Zoeken...</span>';
if (typeof lucide !== 'undefined') 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: 60,
target_user_id: targetUserId
})
});
const data = await res.json();
if (data.success && data.slot) {
const slot = data.slot;
document.getElementById('cardStartDate').value = slot.date;
document.getElementById('cardEndDate').value = slot.date;
const notice = document.getElementById('timelineFoundSlotNotice');
const noticeText = document.getElementById('timelineFoundSlotText');
noticeText.innerHTML = `Vrij agendablok gevonden: <strong>${slot.formatted_date}</strong> om <strong>${slot.start} – ${slot.end}</strong>`;
notice.classList.remove('hidden');
} else {
alert(data.error || 'Geen beschikbaar tijdblok gevonden binnen werktijden voor deze collega.');
}
} catch (err) {
alert('Netwerkfout bij zoeken van vrij agendablok: ' + err.message);
} finally {
btn.disabled = false;
btn.innerHTML = origHtml;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeCardModal();
closeTimelineContextMenu();
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>