1177 lines
53 KiB
PHP
1177 lines
53 KiB
PHP
<?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="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
|
Gantt-planning, mijlpalen & datumbereiken
|
|
</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>
|
|
|
|
<!-- 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>
|
|
|
|
<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;
|
|
|
|
document.getElementById('boardDescriptionText').innerText = data.board.description || 'Geen toelichting';
|
|
|
|
// 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 && 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.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);
|
|
|
|
// 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.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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeCardModal();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
|
|