Files
Fociz/board.php
T

1545 lines
73 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 Trello / Kanban Bord & Time-blocking
* Aanpasbare lanes, post-its, toewijzingen en directe Outlook 365 agenda time-blocking
*/
$pageTitle = 'Fociz — Projectbord';
require_once __DIR__ . '/includes/header.php';
$userId = (int)($currentUser['id'] ?? 0);
$userRole = $currentUser['role'] ?? 'MEMBER';
$isAdmin = ($userRole === 'ADMIN');
?>
<style>
/* Soepele overgang voor de lanes container bij maximaliseren */
#lanesSectionWrapper {
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
#lanesSectionWrapper.is-maximized {
max-width: 100% !important;
width: 100% !important;
}
/* --- Realistische Post-it Styling met Opkrullende Rand Links Onder --- */
.postit-note {
position: relative;
border-radius: 4px 18px 4px 18px;
box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.12), 0 2px 6px -1px rgba(0, 0, 0, 0.08);
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.04));
}
.postit-note:hover {
transform: translateY(-3px) scale(1.01);
box-shadow: 0 12px 20px -4px rgba(0, 0, 0, 0.16), 0 6px 10px -2px rgba(0, 0, 0, 0.08);
}
.postit-note::after {
content: '';
position: absolute;
bottom: -1px;
left: -1px;
width: 20px;
height: 20px;
background: linear-gradient(135deg, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0.06) 45%, transparent 50%);
border-bottom-left-radius: 4px;
box-shadow: -2px 2px 4px rgba(0,0,0,0.12);
pointer-events: none;
z-index: 5;
}
.postit-note::before {
content: '';
position: absolute;
top: 0;
left: 15%;
right: 15%;
height: 4px;
background: rgba(255, 255, 255, 0.4);
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
pointer-events: none;
}
.dark .postit-note::before {
background: rgba(255, 255, 255, 0.1);
}
</style>
<div id="boardPageContainer" class="space-y-4 w-full">
<!-- Bovenbalk: Bord Titel, Filters & Actieknoppen (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: Bordkiezer & Titel -->
<div class="flex items-center space-x-3.5">
<div class="w-10 h-10 rounded-2xl bg-gradient-to-tr from-indigo-600 to-sky-500 text-white flex items-center justify-center shadow-sm shadow-indigo-500/20 flex-shrink-0">
<i data-lucide="kanban" 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">
Teamtaken, opdrachten & Outlook agendablokkering
</p>
</div>
</div>
<!-- Weergave-schakelaar: Bord (Kanban) / 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">
<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="kanban" class="w-3.5 h-3.5"></i>
<span>Bord</span>
</span>
<a id="linkToTimeline" href="/timeline.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="clock" class="w-3.5 h-3.5"></i>
<span>Tijdlijn</span>
</a>
</div>
<!-- Midden & Rechts: Filters & Actieknoppen -->
<div class="flex flex-wrap items-center gap-2">
<!-- Filter: Assignee / Collega -->
<div class="relative">
<select id="filterAssignee" onchange="renderCards()" 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 opdrachten</option>
<option value="UNASSIGNED">Niet toegewezen</option>
</select>
</div>
<!-- Filter: Prioriteit -->
<div class="relative">
<select id="filterPriority" onchange="renderCards()" 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 prioriteiten</option>
<option value="P1">P1 — Urgent</option>
<option value="P2">P2 — Hoog</option>
<option value="P3">P3 — Normaal</option>
</select>
</div>
<!-- Zoekveld -->
<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="filterSearch"
placeholder="Zoeken..."
oninput="renderCards()"
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-32 sm:w-40 shadow-2xs"
>
</div>
<!-- Beheer knoppen -->
<button
id="btnManageBoard"
onclick="openManageBoardModal()"
title="Bordinstellingen, kolommen en toegangsrechten"
class="hidden p-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer shadow-2xs"
>
<i data-lucide="settings" class="w-4 h-4"></i>
</button>
<?php if ($isAdmin): ?>
<button
onclick="openCreateBoardModal()"
title="Nieuw bord toevoegen"
class="p-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer shadow-2xs"
>
<i data-lucide="folder-plus" class="w-4 h-4"></i>
</button>
<?php endif; ?>
<!-- Hoofdactie: Nieuwe Taak / Post-it 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>
<!-- Lanes Sectie: Wisselt tussen standaard gecentreerde breedte (max-w-7xl) en volledig scherm (max-w-none w-full) -->
<div id="lanesSectionWrapper" class="max-w-7xl mx-auto w-full transition-all duration-300">
<!-- Strook direct boven de lane blokken: klein maximaliseer-icoon rechts -->
<div class="flex items-center justify-between px-1 mb-2">
<div class="text-[11px] font-semibold text-slate-500 dark:text-slate-400" id="boardLanesSummary"></div>
<button
id="btnToggleMaximize"
onclick="toggleBoardMaximize()"
title="Kolommen maximaliseren naar volledige vensterbreedte"
class="w-7 h-7 rounded-lg bg-white dark:bg-[#111624] hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-300 flex items-center justify-center transition cursor-pointer shadow-2xs hover:scale-105 active:scale-95 ml-auto"
>
<i id="maximizeIcon" data-lucide="maximize-2" class="w-3.5 h-3.5"></i>
</button>
</div>
<!-- Kanban Board Container (past zich in standaardmodus soepel aan zonder scrollbalk) -->
<div id="boardLanesContainer" class="flex gap-3 overflow-x-auto pb-2 pt-1 min-h-[440px] items-start select-none w-full transition-all duration-300">
<div class="flex items-center justify-center w-full py-20 text-slate-400 text-sm">
<i data-lucide="loader-2" class="w-5 h-5 animate-spin mr-2"></i>
<span>Bord laden...</span>
</div>
</div>
</div>
</div>
<!-- MODAL 1: Taak (Post-it / Opdracht) Toevoegen of Bewerken -->
<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">Nieuwe Taak / Opdracht</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="submitCardForm(event)" class="space-y-4">
<input type="hidden" id="cardModalId" value="">
<!-- Titel -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1.5">
Titel van de opdracht *
</label>
<input
type="text"
id="cardModalTitleInput"
required
placeholder="Bijv. Functioneel ontwerp afronden"
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-sm text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
>
</div>
<!-- Kolom, Prioriteit & Kleur -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1.5">Kolom</label>
<select id="cardModalLaneSelect" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1.5">Prioriteit</label>
<select id="cardModalPriority" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
<option value="P1">P1 — Urgent</option>
<option value="P2" selected>P2 — Hoog</option>
<option value="P3">P3 — Normaal</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1.5">Kleur Post-it</label>
<select id="cardModalColor" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
<option value="yellow">Geel (Klassiek)</option>
<option value="blue">Blauw (Focus)</option>
<option value="green">Groen (Gereed)</option>
<option value="purple">Paars (Strategie)</option>
<option value="pink">Roze (Belangrijk)</option>
<option value="orange">Oranje (Actie)</option>
<option value="slate">Neutraal Grijs</option>
</select>
</div>
</div>
<!-- Toegewezen collega (Assignee) -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1.5">
Toewijzen aan collega
</label>
<select id="cardModalAssignee" onchange="onAssigneeChange()" 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 cursor-pointer">
<option value="">Niet toegewezen</option>
</select>
<div id="assigneeOutlookBadge" class="hidden mt-1.5 flex items-center space-x-1.5 text-[11px] text-sky-600 dark:text-sky-400">
<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
<span>Deze collega heeft Microsoft Outlook 365 gekoppeld (agenda-sync beschikbaar).</span>
</div>
</div>
<!-- Omschrijving / Briefing -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1.5">
Omschrijving & Briefing
</label>
<textarea
id="cardModalDescription"
rows="3"
placeholder="Voeg specifieke instructies of vereisten toe voor deze opdracht..."
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 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
></textarea>
</div>
<!-- Tijdspad & Outlook Agenda Sync Box -->
<div class="p-4 rounded-2xl bg-indigo-50/70 dark:bg-indigo-500/5 border border-indigo-200/80 dark:border-indigo-500/20 space-y-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<i data-lucide="calendar-clock" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<span class="text-xs font-bold text-indigo-950 dark:text-indigo-200 uppercase tracking-wider">
Tijdspad & Outlook Agenda
</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="cardModalScheduleEnabled" onchange="toggleScheduleFields()" class="sr-only peer">
<div class="w-9 h-5 bg-slate-300 peer-focus:outline-none rounded-full peer dark:bg-slate-700 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 dark:border-slate-600 peer-checked:bg-indigo-600"></div>
</label>
</div>
<div id="scheduleFieldsWrapper" class="hidden space-y-3 pt-2 border-t border-indigo-200/50 dark:border-indigo-500/20">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">Type tijdspad</label>
<select id="cardModalScheduleType" onchange="onScheduleTypeChange()" 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-800 dark:text-slate-200 focus:outline-none">
<option value="SINGLE">Eenmalig tijdblok</option>
<option value="DAILY_RANGE">Herhalend bereik (ma t/m vr)</option>
</select>
</div>
<div>
<label class="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">Datum</label>
<input type="date" id="cardModalStartDate" value="<?= date('Y-m-d') ?>" 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-800 dark:text-slate-200 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="btnBoardFindSlot" onclick="findFreeSlotForBoardAssignee()" 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="boardFoundSlotNotice" 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="boardFoundSlotText"></span>
</div>
<div id="scheduleEndDateRow" class="hidden">
<label class="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">Einddatum herhaling</label>
<input type="date" id="cardModalEndDate" value="<?= date('Y-m-d', strtotime('+7 days')) ?>" 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-800 dark:text-slate-200 focus:outline-none">
</div>
<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">Starttijd</label>
<input type="time" id="cardModalStartTime" value="14:00" 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-800 dark:text-slate-200 focus:outline-none">
</div>
<div>
<label class="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">Eindtijd</label>
<input type="time" id="cardModalEndTime" value="15:00" 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-800 dark:text-slate-200 focus:outline-none">
</div>
</div>
<!-- Outlook Vinkje -->
<div class="pt-1">
<label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" id="cardModalSyncOutlook" checked class="rounded text-indigo-600 focus:ring-indigo-500">
<span class="text-xs font-semibold text-slate-800 dark:text-slate-200">
Direct inplannen in Outlook 365 agenda van toegewezen collega
</span>
</label>
<p class="text-[10px] text-slate-500 dark:text-slate-400 mt-0.5 ml-5">
Plaatst automatisch afspraken met meldingen in de agenda van de assignee.
</p>
</div>
</div>
</div>
<!-- Footer Buttons -->
<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-xs text-red-600 hover:text-red-700 font-semibold px-2 py-1 transition cursor-pointer"
>
Taak Verwijderen
</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 rounded-xl text-xs font-bold text-white bg-indigo-600 hover:bg-indigo-500 transition shadow-sm cursor-pointer"
>
Opslaan
</button>
</div>
</div>
</form>
</div>
</div>
<!-- MODAL 2: Bord Instellingen, Kolommen & Leden Autorisatie (Beheerder) -->
<div id="manageBoardModal" onclick="if(event.target === this) closeManageBoardModal()" 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="settings" class="w-4 h-4"></i>
</div>
<h3 class="text-base font-bold text-slate-900 dark:text-white">Bord Instellingen & Autorisatie</h3>
</div>
<button onclick="closeManageBoardModal()" 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>
<div class="space-y-6">
<!-- Kolommen / Lanes Beheren -->
<div class="space-y-3">
<div class="flex items-center justify-between">
<h4 class="text-xs font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
Kolommen / Lanes
</h4>
<button onclick="promptAddLane()" class="text-xs font-bold text-indigo-600 dark:text-indigo-400 hover:underline flex items-center space-x-1 cursor-pointer">
<i data-lucide="plus" class="w-3.5 h-3.5"></i>
<span>Kolom toevoegen</span>
</button>
</div>
<div id="manageLanesList" class="space-y-2">
<!-- Dynamisch gevuld via JS -->
</div>
</div>
<!-- Leden Autorisatie -->
<div class="space-y-3 pt-4 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center justify-between">
<div>
<h4 class="text-xs font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
Gebruikers Autorisatie
</h4>
<p class="text-[11px] text-slate-500 dark:text-slate-400">
Vink de collega's aan die toegang mogen hebben tot dit bord.
</p>
</div>
</div>
<div id="manageMembersList" class="max-h-52 overflow-y-auto space-y-1.5 p-2 rounded-xl bg-slate-50 dark:bg-slate-950/70 border border-slate-200 dark:border-slate-800">
<!-- Dynamisch gevuld via JS -->
</div>
</div>
<!-- Sluitknop -->
<div class="flex justify-end pt-3 border-t border-slate-100 dark:border-slate-800/80">
<button onclick="closeManageBoardModal()" class="px-5 py-2 bg-slate-900 text-white dark:bg-white dark:text-slate-900 font-bold text-xs rounded-xl transition cursor-pointer">
Klaar
</button>
</div>
</div>
</div>
</div>
<!-- MODAL 3: Nieuw Bord Aanmaken (Beheerder) -->
<div id="createBoardModal" onclick="if(event.target === this) closeCreateBoardModal()" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
<div class="fociz-card p-6 w-full max-w-md shadow-2xl space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
<h3 class="text-base font-bold text-slate-900 dark:text-white">Nieuw Bord Toevoegen</h3>
<button onclick="closeCreateBoardModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1.5 rounded-lg cursor-pointer">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="space-y-3">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1">Titel *</label>
<input type="text" id="newBoardTitle" placeholder="Bijv. Klantprojecten, Sprint 15" 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>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-1">Omschrijving</label>
<input type="text" id="newBoardDescription" placeholder="Korte toelichting..." 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>
<div class="flex justify-end space-x-2 pt-2">
<button onclick="closeCreateBoardModal()" 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">Annuleren</button>
<button onclick="submitCreateBoard()" class="px-5 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs rounded-xl transition">Bord Aanmaken</button>
</div>
</div>
<!-- Floating Rechtermuisknop Contextmenu voor Bord Kaarten -->
<div id="boardCardContextMenu" class="hidden fixed z-[999] bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-2xl shadow-2xl py-1.5 w-64 text-xs font-medium text-slate-700 dark:text-slate-200 select-none animate-in fade-in zoom-in-95 duration-100">
<div class="px-3.5 py-1.5 border-b border-slate-100 dark:border-slate-800/80 mb-1">
<span id="boardCtxTitle" class="text-[11px] font-bold text-slate-900 dark:text-white truncate block"></span>
<span id="boardCtxMeta" class="text-[10px] text-slate-400 truncate block"></span>
</div>
<button onclick="triggerBoardCtxEdit()" 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="triggerBoardCtxQuickSlot()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-amber-50 dark:hover:bg-amber-950/40 hover:text-amber-600 dark:hover:text-amber-400 transition cursor-pointer">
<i data-lucide="zap" class="w-4 h-4 text-amber-500"></i>
<span>⚡ Zoek direct vrij blok</span>
</button>
<button onclick="triggerBoardCtxQuickMove(-1)" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="arrow-left" class="w-4 h-4 text-slate-500"></i>
<span>Verplaats naar vorige kolom</span>
</button>
<button onclick="triggerBoardCtxQuickMove(1)" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="arrow-right" class="w-4 h-4 text-slate-500"></i>
<span>Verplaats naar volgende kolom</span>
</button>
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
<button onclick="triggerBoardCtxDelete()" 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 Bord ---
let currentBoardId = null;
let currentBoardData = null;
let allBoards = [];
const currentUserId = <?= (int)$userId ?>;
const isUserAdmin = <?= $isAdmin ? 'true' : 'false' ?>;
// Kleurthema's voor post-its
const POSTIT_COLORS = {
yellow: 'bg-amber-50/90 dark:bg-amber-950/20 border-amber-300/80 dark:border-amber-500/30 text-amber-900 dark:text-amber-100',
blue: 'bg-sky-50/90 dark:bg-sky-950/20 border-sky-300/80 dark:border-sky-500/30 text-sky-900 dark:text-sky-100',
green: 'bg-emerald-50/90 dark:bg-emerald-950/20 border-emerald-300/80 dark:border-emerald-500/30 text-emerald-900 dark:text-emerald-100',
purple: 'bg-purple-50/90 dark:bg-purple-950/20 border-purple-300/80 dark:border-purple-500/30 text-purple-900 dark:text-purple-100',
pink: 'bg-pink-50/90 dark:bg-pink-950/20 border-pink-300/80 dark:border-pink-500/30 text-pink-900 dark:text-pink-100',
orange: 'bg-orange-50/90 dark:bg-orange-950/20 border-orange-300/80 dark:border-orange-500/30 text-orange-900 dark:text-orange-100',
slate: 'bg-slate-50/90 dark:bg-slate-900/40 border-slate-200/80 dark:border-slate-800/80 text-slate-800 dark:text-slate-200'
};
const LANE_ACCENT_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'
};
// 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);
}
await loadBoardsList();
});
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);
} else {
document.getElementById('boardLanesContainer').innerHTML = `
<div class="text-center py-20 w-full text-slate-500">
<p class="font-bold text-base">Nog geen borden aanwezig.</p>
<p class="text-xs mt-1">Maak een nieuw bord aan om te beginnen.</p>
</div>
`;
}
}
} 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 van specifiek bord inladen
async function loadBoardData(boardId, showLoader = true) {
currentBoardId = boardId;
document.getElementById('linkToTimeline')?.setAttribute('href', `/timeline.php?board_id=${boardId}`);
const container = document.getElementById('boardLanesContainer');
if (showLoader) {
container.innerHTML = `
<div class="flex items-center justify-center w-full py-20 text-slate-400 text-sm">
<i data-lucide="loader-2" class="w-5 h-5 animate-spin mr-2"></i>
<span>Bord laden...</span>
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
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;
// Titels en beheerknop
document.getElementById('boardDescriptionText').innerText = data.board.description || 'Geen toelichting';
const btnManage = document.getElementById('btnManageBoard');
const roleBadge = document.getElementById('boardRoleBadge');
if (data.is_board_admin) {
btnManage.classList.remove('hidden');
roleBadge.classList.remove('hidden');
} else {
btnManage.classList.add('hidden');
roleBadge.classList.add('hidden');
}
renderCards();
} else {
container.innerHTML = `<div class="p-8 text-center text-red-500 text-sm">${data.error || 'Kon bord niet laden'}</div>`;
}
} catch (e) {
console.error('Fout bij ophalen borddata:', e);
container.innerHTML = `<div class="p-8 text-center text-red-500 text-sm">Netwerkfout bij laden van het bord.</div>`;
}
}
// Drag & Drop Status & Handlers
let isDraggingCard = false;
let draggedCardId = null;
function onCardDragStart(e, cardId) {
isDraggingCard = true;
draggedCardId = cardId;
e.dataTransfer.setData('text/plain', String(cardId));
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => {
const el = document.getElementById(`card-${cardId}`);
if (el) el.classList.add('opacity-40');
}, 0);
}
function onCardDragEnd(e) {
if (draggedCardId) {
const el = document.getElementById(`card-${draggedCardId}`);
if (el) el.classList.remove('opacity-40', 'pointer-events-none');
draggedCardId = null;
}
// Verwijder eventuele achtergebleven opacity op alle kaarten
document.querySelectorAll('[id^="card-"].opacity-40').forEach(el => {
el.classList.remove('opacity-40', 'pointer-events-none');
});
setTimeout(() => {
isDraggingCard = false;
}, 150);
}
function onCardClick(e, cardId) {
if (isDraggingCard) return;
openCardModal(cardId);
}
function onLaneDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const dropZone = e.currentTarget;
dropZone.classList.add('bg-indigo-50/70', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400/50');
}
function onLaneDragLeave(e) {
const dropZone = e.currentTarget;
dropZone.classList.remove('bg-indigo-50/70', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400/50');
}
async function onLaneDrop(e, targetLaneId) {
e.preventDefault();
const dropZone = e.currentTarget;
dropZone.classList.remove('bg-indigo-50/70', 'dark:bg-indigo-950/40', 'ring-2', 'ring-indigo-400/50');
const rawId = draggedCardId || e.dataTransfer.getData('text/plain');
const cardId = parseInt(rawId, 10);
// Direct re-activeren van het gesleepte element
const activeEl = cardId ? document.getElementById(`card-${cardId}`) : null;
if (activeEl) {
activeEl.classList.remove('opacity-40', 'pointer-events-none');
// Optimistic DOM append als dropZone en element bestaan
const emptyNotice = dropZone.querySelector('.pointer-events-none');
if (emptyNotice && emptyNotice.innerText.includes('Geen taken')) {
emptyNotice.remove();
}
dropZone.appendChild(activeEl);
}
draggedCardId = null;
setTimeout(() => {
isDraggingCard = false;
}, 150);
if (!cardId) return;
try {
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
})
});
await loadBoardData(currentBoardId, false);
} catch (err) {
console.error('Fout bij verplaatsen taak:', err);
}
}
// 3. Kolommen en kaarten renderen met actieve filters
function renderCards() {
if (!currentBoardData) return;
const container = document.getElementById('boardLanesContainer');
container.innerHTML = '';
const filterAssignee = document.getElementById('filterAssignee').value;
const filterPriority = document.getElementById('filterPriority').value;
const filterSearch = document.getElementById('filterSearch').value.toLowerCase().trim();
const lanes = currentBoardData.lanes || [];
const cardsByLane = currentBoardData.cards_by_lane || {};
lanes.forEach(lane => {
let cards = cardsByLane[lane.id] || [];
// Filters toepassen
if (filterAssignee === 'ME') {
cards = cards.filter(c => c.assignee_id && parseInt(c.assignee_id, 10) === currentUserId);
} else if (filterAssignee === 'UNASSIGNED') {
cards = cards.filter(c => !c.assignee_id);
}
if (filterPriority !== 'ALL') {
cards = cards.filter(c => c.priority === filterPriority);
}
if (filterSearch) {
cards = cards.filter(c =>
(c.title && c.title.toLowerCase().includes(filterSearch)) ||
(c.description && c.description.toLowerCase().includes(filterSearch))
);
}
const dotColorClass = LANE_ACCENT_COLORS[lane.color] || 'bg-slate-500';
// Lane kolom HTML: flex-1 en min-w-[160px] zodat 5+ lanes in standaardmodus binnen 1280px passen zonder scrollbalk
const laneEl = document.createElement('div');
laneEl.className = 'flex-1 min-w-0 sm:min-w-[160px] rounded-2xl bg-slate-100/70 dark:bg-[#0c0f17]/70 border border-slate-200/80 dark:border-slate-800/80 p-3 flex flex-col shadow-xs max-h-[calc(100vh-310px)] min-h-[440px] transition-all duration-200';
laneEl.setAttribute('data-lane-id', lane.id);
let cardsHtml = '';
cards.forEach(card => {
const colorClass = POSTIT_COLORS[card.color] || POSTIT_COLORS.yellow;
// Prioriteitsbadge
let priorityBadge = '';
if (card.priority === 'P1') {
priorityBadge = '<span class="px-2 py-0.5 rounded text-[9px] font-extrabold bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-300">P1 Urgent</span>';
} else if (card.priority === 'P2') {
priorityBadge = '<span class="px-2 py-0.5 rounded text-[9px] font-bold bg-amber-100 text-amber-800 dark:bg-amber-500/20 dark:text-amber-300">P2 Hoog</span>';
} else {
priorityBadge = '<span class="px-2 py-0.5 rounded text-[9px] font-semibold bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300">P3 Normaal</span>';
}
// Assignee weergave
let assigneeHtml = '<span class="text-[10px] text-slate-400 italic">Niet toegewezen</span>';
if (card.assignee_name) {
const initial = card.assignee_name.charAt(0).toUpperCase();
const avatarMarkup = 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" onerror="this.classList.add('hidden'); if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');"><div class="hidden 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">${initial}</div>`
: `<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">${initial}</div>`;
assigneeHtml = `
<div class="flex items-center space-x-1.5" title="Toegewezen aan: ${escapeHtml(card.assignee_name)}">
${avatarMarkup}
<span class="text-[11px] font-medium text-slate-700 dark:text-slate-300 truncate max-w-[110px]">
${escapeHtml(card.assignee_name)}
</span>
</div>
`;
}
// Outlook agenda badge
let scheduleBadge = '';
if (card.start_time && card.end_time) {
const hasOutlookSync = card.outlook_event_id ? true : false;
scheduleBadge = `
<div class="mt-2.5 pt-2 border-t border-slate-200/50 dark:border-slate-800/60 flex items-center justify-between text-[10px]">
<span class="flex items-center space-x-1 text-indigo-700 dark:text-indigo-300 font-semibold">
<i data-lucide="clock" class="w-3 h-3"></i>
<span>${escapeHtml(card.start_time)} - ${escapeHtml(card.end_time)}</span>
</span>
${hasOutlookSync
? '<span class="flex items-center space-x-1 text-[9px] font-bold text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 px-1.5 py-0.5 rounded border border-sky-200 dark:border-sky-500/30" title="Gesynchroniseerd met Outlook 365 agenda"><i data-lucide="calendar-check" class="w-3 h-3"></i><span>Outlook</span></span>'
: '<span class="text-[9px] text-slate-400">Tijdblok</span>'
}
</div>
`;
}
cardsHtml += `
<div
id="card-${card.id}"
draggable="true"
ondragstart="onCardDragStart(event, ${card.id})"
ondragend="onCardDragEnd(event)"
onclick="onCardClick(event, ${card.id})"
oncontextmenu="openBoardCardContextMenu(event, ${card.id}); return false;"
class="group/card relative p-3 rounded-xl border ${colorClass} shadow-2xs hover:shadow-md hover:-translate-y-0.5 transition cursor-grab active:cursor-grabbing mb-2.5 postit-note"
>
<div class="flex items-start justify-between gap-1.5 mb-1.5">
<h4 class="text-xs font-bold leading-snug line-clamp-2 break-words">
${escapeHtml(card.title)}
</h4>
<div class="flex-shrink-0">${priorityBadge}</div>
</div>
${card.description ? `<p class="text-[11px] opacity-80 line-clamp-2 leading-relaxed mb-2.5 break-words">${escapeHtml(card.description)}</p>` : ''}
<div class="flex items-center justify-between mt-auto">
${assigneeHtml}
<div class="opacity-0 group-hover/card:opacity-100 transition-opacity flex items-center space-x-1">
<button onclick="event.stopPropagation(); quickMoveCard(${card.id}, -1)" title="Verplaats naar vorige kolom" class="p-1 hover:bg-black/10 dark:hover:bg-white/10 rounded cursor-pointer">
<i data-lucide="chevron-left" class="w-3 h-3"></i>
</button>
<button onclick="event.stopPropagation(); quickMoveCard(${card.id}, 1)" title="Verplaats naar volgende kolom" class="p-1 hover:bg-black/10 dark:hover:bg-white/10 rounded cursor-pointer">
<i data-lucide="chevron-right" class="w-3 h-3"></i>
</button>
</div>
</div>
${scheduleBadge}
</div>
`;
});
laneEl.innerHTML = `
<!-- Lane Header -->
<div class="flex items-center justify-between pb-2.5 mb-2.5 border-b border-slate-200/80 dark:border-slate-800/80">
<div class="flex items-center space-x-2 min-w-0">
<span class="w-2.5 h-2.5 rounded-full ${dotColorClass} flex-shrink-0"></span>
<h3 class="text-xs font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider truncate">
${escapeHtml(lane.title)}
</h3>
</div>
<span 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 shadow-2xs flex-shrink-0 ml-1">
${cards.length}
</span>
</div>
<!-- Cards Scroll Container met Drag & Drop target -->
<div
class="flex-1 overflow-y-auto pr-1 min-h-[100px] rounded-xl transition-all duration-150 p-1"
ondragover="onLaneDragOver(event)"
ondragleave="onLaneDragLeave(event)"
ondrop="onLaneDrop(event, ${lane.id})"
>
${cardsHtml || '<div class="py-8 text-center text-[11px] text-slate-400 italic pointer-events-none">Geen taken in deze kolom</div>'}
</div>
<!-- Quick Add Card Button -->
<button
onclick="openCardModal(null, ${lane.id})"
class="mt-2 w-full py-2 rounded-xl border border-dashed border-slate-300 dark:border-slate-700/80 hover:border-indigo-400 dark:hover:border-indigo-500/60 hover:bg-white dark:hover:bg-slate-800/50 text-[11px] font-semibold text-slate-600 dark:text-slate-400 transition flex items-center justify-center space-x-1 cursor-pointer"
>
<i data-lucide="plus" class="w-3.5 h-3.5"></i>
<span>Taak toevoegen</span>
</button>
`;
container.appendChild(laneEl);
});
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
// 4. Kaart Modal: Openen voor Nieuw of Bewerken
function openCardModal(cardId = null, defaultLaneId = null) {
const modal = document.getElementById('cardModal');
const form = document.getElementById('cardForm');
form.reset();
// Populate Lanes dropdown
const laneSelect = document.getElementById('cardModalLaneSelect');
laneSelect.innerHTML = '';
(currentBoardData.lanes || []).forEach(l => {
const opt = document.createElement('option');
opt.value = l.id;
opt.innerText = l.title;
laneSelect.appendChild(opt);
});
// Populate Assignees dropdown
const assigneeSelect = document.getElementById('cardModalAssignee');
assigneeSelect.innerHTML = '<option value="">Niet toegewezen</option>';
(currentBoardData.team_members || []).forEach(u => {
const opt = document.createElement('option');
opt.value = u.id;
opt.innerText = `${u.name} (${u.role === 'ADMIN' ? 'Beheerder' : 'Collega'})`;
opt.setAttribute('data-has-outlook', u.has_outlook ? '1' : '0');
assigneeSelect.appendChild(opt);
});
const btnDelete = document.getElementById('btnDeleteCard');
const modalTitle = document.getElementById('cardModalTitle');
if (cardId) {
// Bestaande kaart bewerken
modalTitle.innerText = 'Taak & Opdracht Bewerken';
btnDelete.classList.remove('hidden');
// Zoek kaart op
let targetCard = null;
Object.values(currentBoardData.cards_by_lane || {}).forEach(arr => {
const f = arr.find(c => c.id === cardId);
if (f) targetCard = f;
});
if (targetCard) {
document.getElementById('cardModalId').value = targetCard.id;
document.getElementById('cardModalTitleInput').value = targetCard.title || '';
document.getElementById('cardModalLaneSelect').value = targetCard.lane_id;
document.getElementById('cardModalPriority').value = targetCard.priority || 'P2';
document.getElementById('cardModalColor').value = targetCard.color || 'yellow';
document.getElementById('cardModalAssignee').value = targetCard.assignee_id || '';
document.getElementById('cardModalDescription').value = targetCard.description || '';
// Tijdspad invullen indien aanwezig
const schedCheckbox = document.getElementById('cardModalScheduleEnabled');
if (targetCard.start_time && targetCard.end_time) {
schedCheckbox.checked = true;
document.getElementById('cardModalScheduleType').value = targetCard.schedule_type || 'SINGLE';
document.getElementById('cardModalStartDate').value = targetCard.start_date || '<?= date('Y-m-d') ?>';
document.getElementById('cardModalEndDate').value = targetCard.end_date || '<?= date('Y-m-d') ?>';
document.getElementById('cardModalStartTime').value = targetCard.start_time || '14:00';
document.getElementById('cardModalEndTime').value = targetCard.end_time || '15:00';
} else {
schedCheckbox.checked = false;
}
}
} else {
// Nieuwe kaart
modalTitle.innerText = 'Nieuwe Taak / Opdracht';
btnDelete.classList.add('hidden');
document.getElementById('cardModalId').value = '';
if (defaultLaneId) {
document.getElementById('cardModalLaneSelect').value = defaultLaneId;
}
document.getElementById('cardModalScheduleEnabled').checked = false;
}
toggleScheduleFields();
onAssigneeChange();
onScheduleTypeChange();
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined') lucide.createIcons();
document.getElementById('cardModalTitleInput')?.focus();
}
function closeCardModal() {
const modal = document.getElementById('cardModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
function onAssigneeChange() {
const sel = document.getElementById('cardModalAssignee');
const selectedOpt = sel.options[sel.selectedIndex];
const badge = document.getElementById('assigneeOutlookBadge');
if (selectedOpt && selectedOpt.getAttribute('data-has-outlook') === '1') {
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
function toggleScheduleFields() {
const enabled = document.getElementById('cardModalScheduleEnabled').checked;
const wrapper = document.getElementById('scheduleFieldsWrapper');
if (enabled) {
wrapper.classList.remove('hidden');
} else {
wrapper.classList.add('hidden');
}
}
function onScheduleTypeChange() {
const type = document.getElementById('cardModalScheduleType').value;
const row = document.getElementById('scheduleEndDateRow');
if (type === 'DAILY_RANGE') {
row.classList.remove('hidden');
} else {
row.classList.add('hidden');
}
}
// 5. Kaart Opslaan
async function submitCardForm(e) {
e.preventDefault();
const btn = document.getElementById('btnSaveCard');
btn.disabled = true;
btn.innerText = 'Opslaan...';
const cardId = parseInt(document.getElementById('cardModalId').value, 10) || 0;
const title = document.getElementById('cardModalTitleInput').value.trim();
const laneId = parseInt(document.getElementById('cardModalLaneSelect').value, 10);
const priority = document.getElementById('cardModalPriority').value;
const color = document.getElementById('cardModalColor').value;
const assigneeId = document.getElementById('cardModalAssignee').value ? parseInt(document.getElementById('cardModalAssignee').value, 10) : null;
const description = document.getElementById('cardModalDescription').value.trim();
const scheduleEnabled = document.getElementById('cardModalScheduleEnabled').checked;
let schedule = null;
if (scheduleEnabled) {
schedule = {
enabled: true,
type: document.getElementById('cardModalScheduleType').value,
start_date: document.getElementById('cardModalStartDate').value,
end_date: document.getElementById('cardModalEndDate').value,
start_time: document.getElementById('cardModalStartTime').value,
end_time: document.getElementById('cardModalEndTime').value,
sync_outlook: document.getElementById('cardModalSyncOutlook').checked
};
} else {
schedule = { enabled: false };
}
try {
const res = await fetch('/api/board.php?action=save_card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
board_id: currentBoardId,
card_id: cardId,
lane_id: laneId,
title,
priority,
color,
assignee_id: assigneeId,
description,
schedule
})
});
const data = await res.json();
if (data.success) {
closeCardModal();
if (data.outlook_message) {
alert(data.outlook_message);
}
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';
}
}
// 6. Kaart Verwijderen
async function deleteCurrentCard() {
const cardId = parseInt(document.getElementById('cardModalId').value, 10);
if (!cardId || !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');
}
}
// 7. Snelle verplaatsing tussen lanes (links / rechts pijl)
async function quickMoveCard(cardId, direction) {
const lanes = currentBoardData.lanes || [];
let currentLaneIdx = -1;
let card = null;
lanes.forEach((l, idx) => {
const f = (currentBoardData.cards_by_lane[l.id] || []).find(c => c.id === cardId);
if (f) {
currentLaneIdx = idx;
card = f;
}
});
if (currentLaneIdx === -1) return;
const targetLaneIdx = currentLaneIdx + direction;
if (targetLaneIdx < 0 || targetLaneIdx >= lanes.length) return;
const targetLane = lanes[targetLaneIdx];
try {
await fetch('/api/board.php?action=move_card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
card_id: cardId,
target_lane_id: targetLane.id,
target_position: 999
})
});
await loadBoardData(currentBoardId);
} catch (e) {
console.error('Kon kaart niet verplaatsen:', e);
}
}
// Rechtermuisknop Context Menu Logica voor Bord
let activeBoardCtxCardId = null;
function openBoardCardContextMenu(e, cardId) {
e.preventDefault();
e.stopPropagation();
activeBoardCtxCardId = cardId;
const menu = document.getElementById('boardCardContextMenu');
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 closeBoardCardContextMenu() {
const menu = document.getElementById('boardCardContextMenu');
if (menu) menu.classList.add('hidden');
activeBoardCtxCardId = null;
}
document.addEventListener('click', (e) => {
if (!e.target.closest('#boardCardContextMenu')) {
closeBoardCardContextMenu();
}
});
function triggerBoardCtxEdit() {
if (!activeBoardCtxCardId) return;
const cid = activeBoardCtxCardId;
closeBoardCardContextMenu();
openCardModal(cid);
}
async function triggerBoardCtxQuickSlot() {
if (!activeBoardCtxCardId) return;
const cid = activeBoardCtxCardId;
closeBoardCardContextMenu();
openCardModal(cid);
setTimeout(() => {
findFreeSlotForBoardAssignee();
}, 150);
}
function triggerBoardCtxQuickMove(dir) {
if (!activeBoardCtxCardId) return;
const cid = activeBoardCtxCardId;
closeBoardCardContextMenu();
quickMoveCard(cid, dir);
}
async function triggerBoardCtxDelete() {
if (!activeBoardCtxCardId) return;
const cid = activeBoardCtxCardId;
closeBoardCardContextMenu();
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 taak');
}
}
// Zoek vrij agendablok voor de in de modal geselecteerde assignee
async function findFreeSlotForBoardAssignee() {
const sel = document.getElementById('cardModalAssignee');
const targetUserId = sel.value ? parseInt(sel.value, 10) : currentUserId;
const schedCheckbox = document.getElementById('cardModalScheduleEnabled');
if (!schedCheckbox.checked) {
schedCheckbox.checked = true;
toggleScheduleFields();
}
const btn = document.getElementById('btnBoardFindSlot');
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('cardModalStartDate').value = slot.date;
document.getElementById('cardModalEndDate').value = slot.date;
document.getElementById('cardModalStartTime').value = slot.start;
document.getElementById('cardModalEndTime').value = slot.end;
const notice = document.getElementById('boardFoundSlotNotice');
const noticeText = document.getElementById('boardFoundSlotText');
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();
}
}
// 8. Beheer Modal: Lanes & Leden Autorisatie
function openManageBoardModal() {
if (!currentBoardData) return;
const modal = document.getElementById('manageBoardModal');
// Render Lanes list
const lanesList = document.getElementById('manageLanesList');
lanesList.innerHTML = '';
(currentBoardData.lanes || []).forEach(l => {
const div = document.createElement('div');
div.className = 'flex items-center justify-between p-2.5 rounded-xl bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-xs';
div.innerHTML = `
<div class="flex items-center space-x-2">
<span class="w-2.5 h-2.5 rounded-full ${LANE_ACCENT_COLORS[l.color] || 'bg-slate-500'}"></span>
<span class="font-bold text-slate-800 dark:text-slate-200">${escapeHtml(l.title)}</span>
</div>
<div class="flex items-center space-x-1">
<button onclick="promptEditLane(${l.id}, '${escapeHtml(l.title)}', '${l.color}')" title="Hernoemen" class="p-1 hover:bg-slate-200 dark:hover:bg-slate-800 rounded text-slate-500 cursor-pointer">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
</button>
<button onclick="deleteLane(${l.id})" title="Verwijderen" class="p-1 hover:bg-red-50 dark:hover:bg-red-500/10 rounded text-red-500 cursor-pointer">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
`;
lanesList.appendChild(div);
});
// Render Members Autorisatie list
const membersList = document.getElementById('manageMembersList');
membersList.innerHTML = '';
const authMap = currentBoardData.authorized_members || {};
(currentBoardData.team_members || []).forEach(u => {
const isAuthorized = authMap[u.id] !== undefined;
const label = document.createElement('label');
label.className = 'flex items-center justify-between p-2 rounded-lg hover:bg-white dark:hover:bg-slate-900 cursor-pointer transition text-xs';
label.innerHTML = `
<div class="flex items-center space-x-2">
<input type="checkbox" value="${u.id}" ${isAuthorized ? 'checked' : ''} onchange="toggleMemberAccess(${u.id}, this.checked)" class="rounded text-indigo-600 focus:ring-indigo-500">
<span class="font-semibold text-slate-800 dark:text-slate-200">${escapeHtml(u.name)}</span>
<span class="text-[10px] text-slate-400">(${escapeHtml(u.email)})</span>
</div>
<span class="text-[10px] uppercase font-bold text-slate-400">${u.role}</span>
`;
membersList.appendChild(label);
});
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closeManageBoardModal() {
const modal = document.getElementById('manageBoardModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
async function promptAddLane() {
const title = prompt('Naam voor de nieuwe kolom:');
if (!title || !title.trim()) return;
try {
const res = await fetch('/api/board.php?action=save_lane', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ board_id: currentBoardId, title: title.trim(), color: 'slate' })
});
const data = await res.json();
if (data.success) {
await loadBoardData(currentBoardId);
openManageBoardModal();
} else {
alert(data.error || 'Kon kolom niet toevoegen');
}
} catch(e) {
alert('Netwerkfout');
}
}
async function promptEditLane(laneId, oldTitle, oldColor) {
const newTitle = prompt('Nieuwe kolomnaam:', oldTitle);
if (!newTitle || !newTitle.trim()) return;
try {
const res = await fetch('/api/board.php?action=save_lane', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ board_id: currentBoardId, lane_id: laneId, title: newTitle.trim(), color: oldColor })
});
const data = await res.json();
if (data.success) {
await loadBoardData(currentBoardId);
openManageBoardModal();
} else {
alert(data.error || 'Kon kolom niet bijwerken');
}
} catch(e) {
alert('Netwerkfout');
}
}
async function deleteLane(laneId) {
if (!confirm('Weet je zeker dat je deze kolom wilt verwijderen? (Kolom moet leeg zijn)')) return;
try {
const res = await fetch('/api/board.php?action=delete_lane', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ board_id: currentBoardId, lane_id: laneId })
});
const data = await res.json();
if (data.success) {
await loadBoardData(currentBoardId);
openManageBoardModal();
} else {
alert(data.error || 'Kon kolom niet verwijderen');
}
} catch(e) {
alert('Netwerkfout');
}
}
async function toggleMemberAccess(targetUserId, grantAccess) {
if (!currentBoardData) return;
const authMap = currentBoardData.authorized_members || {};
if (grantAccess) {
authMap[targetUserId] = 'MEMBER';
} else {
delete authMap[targetUserId];
}
const memberIds = Object.keys(authMap).map(id => parseInt(id, 10));
try {
await fetch('/api/board.php?action=manage_members', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ board_id: currentBoardId, member_ids: memberIds })
});
} catch (e) {
console.error('Fout bij autorisatie bijwerken:', e);
}
}
// 9. Nieuw Bord Aanmaken
function openCreateBoardModal() {
const modal = document.getElementById('createBoardModal');
document.getElementById('newBoardTitle').value = '';
document.getElementById('newBoardDescription').value = '';
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeCreateBoardModal() {
const modal = document.getElementById('createBoardModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
async function submitCreateBoard() {
const title = document.getElementById('newBoardTitle').value.trim();
const description = document.getElementById('newBoardDescription').value.trim();
if (!title) {
alert('Vul een bordtitel in.');
return;
}
try {
const res = await fetch('/api/board.php?action=create_board', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ title, description })
});
const data = await res.json();
if (data.success) {
closeCreateBoardModal();
currentBoardId = data.board_id;
await loadBoardsList();
} else {
alert(data.error || 'Kon bord niet aanmaken');
}
} catch(e) {
alert('Netwerkfout bij aanmaken bord');
}
}
// Helper voor HTML escaping
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// 10. Kolommen Maximaliseren & Verkleinen (Soepele breedte-overgang)
// De site-header blijft ALTIJD staan.
// De bovenbalk (Hoofdbord, filters, actieknoppen) behoudt altijd de vaste standaard breedte.
// ALLEEN de aparte lanes wisselen tussen standaard gecentreerde breedte en de volledige schermbreedte.
let isBoardMaximized = localStorage.getItem('fociz_board_maximized') === 'true';
function applyMaximizeState(isMax) {
const lanesWrapper = document.getElementById('lanesSectionWrapper');
const mainContainer = document.getElementById('focizMainContainer');
const icon = document.getElementById('maximizeIcon');
const btn = document.getElementById('btnToggleMaximize');
if (!lanesWrapper || !btn) return;
// Zorg dat header altijd zichtbaar blijft
document.querySelector('header')?.classList.remove('hidden');
if (mainContainer) {
mainContainer.style.maxWidth = '';
mainContainer.style.paddingLeft = '';
mainContainer.style.paddingRight = '';
}
if (isMax) {
lanesWrapper.classList.remove('max-w-7xl', 'mx-auto');
lanesWrapper.classList.add('is-maximized', 'max-w-none', 'w-full', 'px-1', 'sm:px-3');
btn.setAttribute('title', 'Kolommen verkleinen naar standaard breedte (Escape)');
if (icon) {
icon.setAttribute('data-lucide', 'minimize-2');
}
} else {
lanesWrapper.classList.remove('is-maximized', 'max-w-none', 'px-1', 'sm:px-3');
lanesWrapper.classList.add('max-w-7xl', 'mx-auto', 'w-full');
btn.setAttribute('title', 'Kolommen maximaliseren naar volledige vensterbreedte');
if (icon) {
icon.setAttribute('data-lucide', 'maximize-2');
}
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function toggleBoardMaximize() {
isBoardMaximized = !isBoardMaximized;
localStorage.setItem('fociz_board_maximized', isBoardMaximized ? 'true' : 'false');
applyMaximizeState(isBoardMaximized);
}
// Pas bewaarde breedte-status direct toe bij het laden
applyMaximizeState(isBoardMaximized);
// Sneltoets Escape om geopende modals te sluiten of gemaximaliseerd bord te verkleinen
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const cardModal = document.getElementById('cardModal');
if (cardModal && !cardModal.classList.contains('hidden')) {
closeCardModal();
return;
}
const manageModal = document.getElementById('manageBoardModal');
if (manageModal && !manageModal.classList.contains('hidden')) {
closeManageBoardModal();
return;
}
const createModal = document.getElementById('createBoardModal');
if (createModal && !createModal.classList.contains('hidden')) {
closeCreateBoardModal();
return;
}
if (isBoardMaximized) {
toggleBoardMaximize();
}
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>