Files
Fociz/board.php
T

1271 lines
60 KiB
PHP

<?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>
/* Volledige breedte benutting en soepele schermmaximalisatie */
#boardMainWrapper {
width: 100% !important;
max-width: 100% !important;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
#boardMainWrapper.is-maximized {
padding-left: 0 !important;
padding-right: 0 !important;
}
#boardMainWrapper #boardLanesContainer {
width: 100% !important;
}
</style>
<div id="boardMainWrapper" class="space-y-4 w-full max-w-none">
<!-- Bovenbalk: Bord Titel, Filters & Actieknoppen -->
<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>
<!-- 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>
<!-- Strook direct boven de lane blokken: klein maximaliseer-icoon rechts -->
<div class="flex items-center justify-between px-1 -mb-1">
<div class="text-[11px] font-semibold text-slate-500 dark:text-slate-400" id="boardLanesSummary"></div>
<button
id="btnToggleMaximize"
onclick="toggleBoardMaximize()"
title="Bord 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 (Horizontaal scrollbaar indien te smal, past automatisch in de breedte) -->
<div id="boardLanesContainer" class="flex gap-4 overflow-x-auto pb-8 pt-1 min-h-[650px] 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>
<!-- MODAL 1: Taak (Post-it / Opdracht) Toevoegen of Bewerken -->
<div id="cardModal" 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>
<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" 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" 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>
</div>
</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 () => {
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) {
currentBoardId = boardId;
const container = document.getElementById('boardLanesContainer');
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');
draggedCardId = null;
}
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);
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);
} 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 (past zich automatisch aan de schermbreedte aan)
const laneEl = document.createElement('div');
laneEl.className = 'flex-1 min-w-[260px] rounded-2xl bg-slate-100/70 dark:bg-[#0c0f17]/70 border border-slate-200/80 dark:border-slate-800/80 p-3.5 flex flex-col shadow-xs max-h-[82vh] 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();
assigneeHtml = `
<div class="flex items-center space-x-1.5" title="Toegewezen aan: ${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">
${initial}
</div>
<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})"
class="group/card relative p-3.5 rounded-xl border ${colorClass} shadow-2xs hover:shadow-md hover:-translate-y-0.5 transition cursor-grab active:cursor-grabbing mb-2.5"
>
<div class="flex items-start justify-between gap-2 mb-1.5">
<h4 class="text-xs font-bold leading-snug line-clamp-2">
${escapeHtml(card.title)}
</h4>
${priorityBadge}
</div>
${card.description ? `<p class="text-[11px] opacity-80 line-clamp-2 leading-relaxed mb-2.5">${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-3 mb-3 border-b border-slate-200/80 dark:border-slate-800/80">
<div class="flex items-center space-x-2">
<span class="w-2.5 h-2.5 rounded-full ${dotColorClass}"></span>
<h3 class="text-xs font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
${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">
${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);
}
}
// 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. Bord Maximaliseren & Verkleinen (Soepele breedte-overgang)
let isBoardMaximized = localStorage.getItem('fociz_board_maximized') === 'true';
function applyMaximizeState(isMax) {
const wrapper = document.getElementById('boardMainWrapper');
const mainContainer = document.getElementById('focizMainContainer');
const icon = document.getElementById('maximizeIcon');
const btn = document.getElementById('btnToggleMaximize');
if (!wrapper || !btn) return;
if (isMax) {
wrapper.classList.add('is-maximized');
if (mainContainer) {
mainContainer.style.maxWidth = '100vw';
mainContainer.style.width = '100%';
mainContainer.style.paddingLeft = '0.5rem';
mainContainer.style.paddingRight = '0.5rem';
}
document.querySelector('header')?.classList.add('hidden');
btn.setAttribute('title', 'Bord verkleinen / navigatie tonen (Escape)');
if (icon) {
icon.setAttribute('data-lucide', 'minimize-2');
}
} else {
wrapper.classList.remove('is-maximized');
if (mainContainer) {
mainContainer.style.maxWidth = '';
mainContainer.style.width = '';
mainContainer.style.paddingLeft = '';
mainContainer.style.paddingRight = '';
}
document.querySelector('header')?.classList.remove('hidden');
btn.setAttribute('title', 'Bord maximaliseren (volledig venster)');
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 gemaximaliseerd bord te verkleinen (mits geen modal openstaat)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isBoardMaximized) {
const anyModalOpen = document.querySelector('#cardModal:not(.hidden), #manageBoardModal:not(.hidden), #createBoardModal:not(.hidden)');
if (!anyModalOpen) {
toggleBoardMaximize();
}
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>