1327 lines
64 KiB
PHP
1327 lines
64 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>
|
|
/* 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;
|
|
}
|
|
</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>
|
|
|
|
<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>
|
|
</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 () => {
|
|
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})"
|
|
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"
|
|
>
|
|
<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);
|
|
}
|
|
}
|
|
|
|
// 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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
// 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'; ?>
|