Merge pull request 'Dev' (#42) from dev into main

Reviewed-on: #42
This commit was merged in pull request #42.
This commit is contained in:
2026-09-10 14:16:04 +00:00
2 changed files with 577 additions and 321 deletions
+275 -235
View File
@@ -156,18 +156,7 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
Koppel Jira Cloud Koppel Jira Cloud
</a> </a>
</div> </div>
<?php elseif ($isLive): ?> <?php if (!empty($syncError)): ?>
<!-- Live Status Subbalk -->
<div class="mt-4 pt-3 border-t border-slate-100 dark:border-slate-800/60 flex flex-wrap items-center justify-between text-xs text-slate-500 dark:text-slate-400 gap-2">
<div class="flex items-center space-x-2">
<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-emerald-500"></i>
<span>Verbonden met <strong class="text-slate-700 dark:text-slate-200"><?= htmlspecialchars($domain) ?></strong> als <strong class="text-slate-700 dark:text-slate-200"><?= htmlspecialchars($accountName ?: $email) ?></strong></span>
</div>
<div class="text-[11px] text-slate-400">
Laatste synchronisatie: <?= date('d-m-Y H:i', strtotime($lastSync)) ?>
</div>
</div>
<?php elseif (!empty($syncError)): ?>
<!-- Synchronisatiefout Banner --> <!-- Synchronisatiefout Banner -->
<div class="mt-4 p-3 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900/50 flex items-center space-x-3 text-xs text-red-700 dark:text-red-300"> <div class="mt-4 p-3 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900/50 flex items-center space-x-3 text-xs text-red-700 dark:text-red-300">
<i data-lucide="alert-triangle" class="w-4 h-4 text-red-500 flex-shrink-0"></i> <i data-lucide="alert-triangle" class="w-4 h-4 text-red-500 flex-shrink-0"></i>
@@ -177,273 +166,280 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
<a href="/profile.php#integraties" class="underline hover:no-underline font-semibold">Controleer instellingen</a> <a href="/profile.php#integraties" class="underline hover:no-underline font-semibold">Controleer instellingen</a>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div>
<!-- 4 KPI Statistieken Kaarten --> <!-- Hub Voettekst: Live Status & Filters Uitklapknop (Rechtsonder) -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4"> <div class="mt-4 pt-3 border-t border-slate-100 dark:border-slate-800/60 flex flex-wrap items-center justify-between text-xs text-slate-500 dark:text-slate-400 gap-2">
<!-- Kaart 1: Aan mij toegewezen --> <div class="flex items-center space-x-2">
<div onclick="switchTab('assigned')" class="fociz-card-interactive p-4 cursor-pointer group"> <?php if ($isLive): ?>
<div class="flex items-center justify-between"> <i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-emerald-500"></i>
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Aan mij toegewezen</span> <span>Verbonden met <strong class="text-slate-700 dark:text-slate-200"><?= htmlspecialchars($domain) ?></strong> als <strong class="text-slate-700 dark:text-slate-200"><?= htmlspecialchars($accountName ?: $email) ?></strong></span>
<div class="w-8 h-8 rounded-xl bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 flex items-center justify-center transition group-hover:scale-110"> <span class="text-slate-300 dark:text-slate-700 hidden sm:inline">•</span>
<i data-lucide="user-check" class="w-4 h-4"></i> <span class="text-[11px] text-slate-400 hidden sm:inline">Sync: <?= date('d-m-Y H:i', strtotime($lastSync)) ?></span>
</div> <?php elseif ($isDemo): ?>
<i data-lucide="database" class="w-3.5 h-3.5 text-amber-500"></i>
<span>Demodataset actief met voorbeeldtickets</span>
<?php endif; ?>
</div> </div>
<div class="mt-3 flex items-baseline space-x-2">
<span id="statAssignedCount" class="text-2xl font-black text-slate-900 dark:text-white"><?= count($assignedIssues) ?></span>
<span class="text-[11px] text-slate-400 font-medium">persoonlijke taken</span>
</div>
</div>
<!-- Kaart 2: Mijn Teams & Projecten --> <!-- Uitklapknop in de rechter onderhoek -->
<div onclick="switchTab('team')" class="fociz-card-interactive p-4 cursor-pointer group"> <div class="flex items-center space-x-2 ml-auto">
<div class="flex items-center justify-between"> <button type="button"
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Team & Projecten</span> onclick="toggleJiraFiltersDrawer()"
<div class="w-8 h-8 rounded-xl bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 flex items-center justify-center transition group-hover:scale-110"> id="btnToggleFiltersDrawer"
<i data-lucide="users" class="w-4 h-4"></i> class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-700 transition cursor-pointer shadow-2xs group">
</div> <i data-lucide="sliders-horizontal" class="w-3.5 h-3.5 text-indigo-500 group-hover:rotate-12 transition-transform"></i>
</div> <span>Filters & Statistieken</span>
<div class="mt-3 flex items-baseline space-x-2"> <span id="activeFilterBadge" class="hidden px-1.5 py-0.2 rounded-full text-[10px] font-black bg-indigo-600 text-white leading-tight">0</span>
<span id="statTeamCount" class="text-2xl font-black text-slate-900 dark:text-white"><?= count($teamIssues) ?></span> <i data-lucide="chevron-down" id="filtersDrawerChevron" class="w-3.5 h-3.5 transition-transform duration-200"></i>
<span class="text-[11px] text-slate-400 font-medium">in actieve projecten</span>
</div>
</div>
<!-- Kaart 3: In Uitvoering -->
<div onclick="switchTab('in_progress')" class="fociz-card-interactive p-4 cursor-pointer group">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">In Uitvoering</span>
<div class="w-8 h-8 rounded-xl bg-amber-50 dark:bg-amber-950/40 text-amber-600 dark:text-amber-400 flex items-center justify-center transition group-hover:scale-110">
<i data-lucide="clock" class="w-4 h-4"></i>
</div>
</div>
<div class="mt-3 flex items-baseline space-x-2">
<span id="statInProgressCount" class="text-2xl font-black text-slate-900 dark:text-white">...</span>
<span class="text-[11px] text-slate-400 font-medium">actief mee bezig</span>
</div>
</div>
<!-- Kaart 4: Urgent / P1 -->
<div onclick="filterByUrgent()" class="fociz-card-interactive p-4 cursor-pointer group">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Hoge Prioriteit / Urgent</span>
<div class="w-8 h-8 rounded-xl bg-rose-50 dark:bg-rose-950/40 text-rose-600 dark:text-rose-400 flex items-center justify-center transition group-hover:scale-110">
<i data-lucide="alert-circle" class="w-4 h-4"></i>
</div>
</div>
<div class="mt-3 flex items-baseline space-x-2">
<span id="statHighPriorityCount" class="text-2xl font-black text-slate-900 dark:text-white">...</span>
<span class="text-[11px] text-slate-400 font-medium">Highest / High</span>
</div>
</div>
</div>
<!-- 1. Snelle Project Switcher (Pill Tabs) -->
<div class="fociz-card p-4 sm:p-5">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 pb-3 mb-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-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="folder-kanban" class="w-4 h-4"></i>
</div>
<div>
<h2 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">Jira Projecten</h2>
<p class="text-[11px] text-slate-500 dark:text-slate-400">Klik op een project om direct te filteren</p>
</div>
</div>
<div class="text-[11px] text-slate-400 hidden sm:block">
<span><?= count($allProjects) ?> actieve projecten gevonden</span>
</div>
</div>
<div class="flex items-center gap-2 overflow-x-auto pb-1" id="projectPillsContainer">
<!-- Alle Projecten Pill -->
<button type="button" onclick="selectProject('')" data-project="" class="project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0">
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
<span>Alle Projecten</span>
<span class="px-2 py-0.5 rounded-full text-[10px] bg-white/20 text-white font-black"><?= count($allIssues) ?></span>
</button>
<!-- Per project pills -->
<?php foreach ($allProjects as $pName => $pData): ?>
<button type="button" onclick="selectProject('<?= htmlspecialchars(addslashes($pName)) ?>')" data-project="<?= htmlspecialchars($pName) ?>" class="project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0">
<?php if (!empty($pData['key'])): ?>
<span class="px-1.5 py-0.5 rounded text-[10px] font-mono font-black bg-indigo-100 dark:bg-indigo-950/80 text-indigo-700 dark:text-indigo-300"><?= htmlspecialchars($pData['key']) ?></span>
<?php else: ?>
<i data-lucide="folder" class="w-3.5 h-3.5 text-slate-400"></i>
<?php endif; ?>
<span><?= htmlspecialchars($pName) ?></span>
<span class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300"><?= $projectCounts[$pName] ?? 0 ?></span>
</button> </button>
<?php endforeach; ?> </div>
</div> </div>
</div>
<!-- 2. Servicedesk Wachtrijfilter Sub-balk (DX / SD) --> <!-- Uitklapbare Filters & Statistieken Drawer -->
<div id="queueFiltersBar" class="fociz-card p-4 bg-gradient-to-r from-slate-50 via-indigo-50/30 to-sky-50/20 dark:from-[#0c0f17] dark:via-indigo-950/20 dark:to-sky-950/20 border-indigo-200/70 dark:border-indigo-900/40"> <div id="jiraFiltersDrawer" class="hidden mt-4 pt-4 border-t border-slate-100 dark:border-slate-800/80 space-y-4">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3">
<div class="flex items-center space-x-3"> <!-- 1. 4 KPI Statistieken Kaarten -->
<div class="w-8 h-8 rounded-xl bg-indigo-500/15 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold flex-shrink-0"> <div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
<i data-lucide="filter" class="w-4 h-4"></i> <div onclick="switchTab('assigned')" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Aan mij toegewezen</span>
<div class="w-7 h-7 rounded-lg bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 flex items-center justify-center transition group-hover:scale-110">
<i data-lucide="user-check" class="w-3.5 h-3.5"></i>
</div>
</div>
<div class="mt-2 flex items-baseline space-x-2">
<span id="statAssignedCount" class="text-xl font-black text-slate-900 dark:text-white"><?= count($assignedIssues) ?></span>
<span class="text-[11px] text-slate-400 font-medium">persoonlijke taken</span>
</div>
</div> </div>
<div>
<div onclick="switchTab('team')" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Team & Projecten</span>
<div class="w-7 h-7 rounded-lg bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 flex items-center justify-center transition group-hover:scale-110">
<i data-lucide="users" class="w-3.5 h-3.5"></i>
</div>
</div>
<div class="mt-2 flex items-baseline space-x-2">
<span id="statTeamCount" class="text-xl font-black text-slate-900 dark:text-white"><?= count($teamIssues) ?></span>
<span class="text-[11px] text-slate-400 font-medium">in actieve projecten</span>
</div>
</div>
<div onclick="switchTab('in_progress')" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">In Uitvoering</span>
<div class="w-7 h-7 rounded-lg bg-amber-50 dark:bg-amber-950/40 text-amber-600 dark:text-amber-400 flex items-center justify-center transition group-hover:scale-110">
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
</div>
</div>
<div class="mt-2 flex items-baseline space-x-2">
<span id="statInProgressCount" class="text-xl font-black text-slate-900 dark:text-white">...</span>
<span class="text-[11px] text-slate-400 font-medium">actief mee bezig</span>
</div>
</div>
<div onclick="filterByUrgent()" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Hoge Prioriteit / Urgent</span>
<div class="w-7 h-7 rounded-lg bg-rose-50 dark:bg-rose-950/40 text-rose-600 dark:text-rose-400 flex items-center justify-center transition group-hover:scale-110">
<i data-lucide="alert-circle" class="w-3.5 h-3.5"></i>
</div>
</div>
<div class="mt-2 flex items-baseline space-x-2">
<span id="statHighPriorityCount" class="text-xl font-black text-slate-900 dark:text-white">...</span>
<span class="text-[11px] text-slate-400 font-medium">Highest / High</span>
</div>
</div>
</div>
<!-- 2. Snelle Project Switcher (Pill Tabs) -->
<div class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/30 border border-slate-200/70 dark:border-slate-800">
<div class="flex items-center justify-between pb-2 mb-2 border-b border-slate-200/60 dark:border-slate-800">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-xs font-bold text-slate-900 dark:text-white">Servicedesk Wachtrijfilters</span> <i data-lucide="folder-kanban" class="w-3.5 h-3.5 text-indigo-500"></i>
<span class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">Jira Projecten</span>
</div>
<span class="text-[11px] text-slate-400"><?= count($allProjects) ?> projecten</span>
</div>
<div class="flex items-center gap-2 overflow-x-auto pb-1" id="projectPillsContainer">
<button type="button" onclick="selectProject('')" data-project="" class="project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs flex-shrink-0">
<i data-lucide="layers" class="w-3 h-3"></i>
<span>Alle Projecten</span>
<span class="px-1.5 py-0.5 rounded-full text-[10px] bg-white/20 text-white font-black"><?= count($allIssues) ?></span>
</button>
<?php foreach ($allProjects as $pName => $pData): ?>
<button type="button" onclick="selectProject('<?= htmlspecialchars(addslashes($pName)) ?>')" data-project="<?= htmlspecialchars($pName) ?>" class="project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200/70 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0">
<?php if (!empty($pData['key'])): ?>
<span class="px-1 py-0.5 rounded text-[10px] font-mono font-black bg-indigo-100 dark:bg-indigo-950/80 text-indigo-700 dark:text-indigo-300"><?= htmlspecialchars($pData['key']) ?></span>
<?php else: ?>
<i data-lucide="folder" class="w-3 h-3 text-slate-400"></i>
<?php endif; ?>
<span><?= htmlspecialchars($pName) ?></span>
<span class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300"><?= $projectCounts[$pName] ?? 0 ?></span>
</button>
<?php endforeach; ?>
</div>
</div>
<!-- 3. Servicedesk Wachtrijfilter Sub-balk (DX / SD) -->
<div id="queueFiltersBar" class="p-3.5 rounded-xl bg-gradient-to-r from-slate-50 via-indigo-50/20 to-sky-50/10 dark:from-slate-900 dark:via-indigo-950/20 dark:to-sky-950/10 border border-indigo-100 dark:border-indigo-900/40">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-2.5">
<div class="flex items-center space-x-2">
<i data-lucide="filter" class="w-3.5 h-3.5 text-indigo-500"></i>
<span class="text-xs font-bold text-slate-900 dark:text-white">Servicedesk Wachtrijen</span>
<?php if (!empty($defaultQueueFilter)): ?> <?php if (!empty($defaultQueueFilter)): ?>
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-800 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/80"> <span class="text-[10px] font-bold px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-800 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/80">
Jouw voorkeur: <?= htmlspecialchars($defaultQueueFilter) ?> Jouw voorkeur: <?= htmlspecialchars($defaultQueueFilter) ?>
</span> </span>
<?php endif; ?> <?php endif; ?>
</div> </div>
<p class="text-[11px] text-slate-500 dark:text-slate-400"> <div class="flex items-center gap-1.5 overflow-x-auto" id="queueButtonsContainer">
Filtert op onderwerp logging routering (DX voor koppelingen & endpoints, SD voor reguliere support) <button type="button" onclick="selectQueueFilter('')" data-queue="" class="queue-pill flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300 flex-shrink-0">
</p> <i data-lucide="list" class="w-3 h-3"></i>
<span>Alle Wachtrijen</span>
<span id="queueCount_all" class="px-1.5 py-0.5 rounded-full text-[10px] bg-slate-300 dark:bg-slate-700 font-bold">0</span>
</button>
<button type="button" onclick="selectQueueFilter('DX')" data-queue="DX" class="queue-pill flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0">
<i data-lucide="zap" class="w-3 h-3 text-amber-500"></i>
<span>DX (Koppelingen)</span>
<span id="queueCount_DX" class="px-1.5 py-0.5 rounded-full text-[10px] bg-amber-100 dark:bg-amber-950/60 text-amber-800 dark:text-amber-300 font-bold">0</span>
</button>
<button type="button" onclick="selectQueueFilter('SD')" data-queue="SD" class="queue-pill flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0">
<i data-lucide="wrench" class="w-3 h-3 text-sky-500"></i>
<span>SD (Regulier)</span>
<span id="queueCount_SD" class="px-1.5 py-0.5 rounded-full text-[10px] bg-sky-100 dark:bg-sky-950/60 text-sky-800 dark:text-sky-300 font-bold">0</span>
</button>
<?php foreach ($queueFilters as $qf):
$code = $qf['filter_code'] ?? '';
if ($code === 'DX' || $code === 'SD' || empty($code)) continue;
?>
<button type="button" onclick="selectQueueFilter('<?= htmlspecialchars($code) ?>')" data-queue="<?= htmlspecialchars($code) ?>" class="queue-pill flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0">
<i data-lucide="filter" class="w-3 h-3 text-indigo-500"></i>
<span><?= htmlspecialchars($qf['filter_name'] ?? $code) ?></span>
<span id="queueCount_<?= htmlspecialchars($code) ?>" class="px-1.5 py-0.5 rounded-full text-[10px] bg-indigo-100 dark:bg-indigo-950/60 text-indigo-800 dark:text-indigo-300 font-bold">0</span>
</button>
<?php endforeach; ?>
<?php if ($isAdmin): ?>
<a href="/admin.php#jira-wachtrijen" title="Wachtrijfilters beheren in Admin" class="p-1 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-200/60 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0">
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
</a>
<?php endif; ?>
</div>
</div> </div>
</div> </div>
<!-- Knoppen rij voor wachtrijen --> <!-- 4. Gedetailleerde Filter Dropdowns -->
<div class="flex items-center gap-2 overflow-x-auto" id="queueButtonsContainer"> <div class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/30 border border-slate-200/70 dark:border-slate-800">
<button type="button" onclick="selectQueueFilter('')" data-queue="" class="queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300 flex-shrink-0"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-2.5">
<i data-lucide="list" class="w-3.5 h-3.5"></i> <div>
<span>Alle Wachtrijen</span> <label class="block text-[11px] font-semibold text-slate-500 dark:text-slate-400 mb-1">Project</label>
<span id="queueCount_all" class="px-1.5 py-0.5 rounded-full text-[10px] bg-slate-300 dark:bg-slate-700 font-bold">0</span> <select id="filterProject" onchange="onProjectSelectChange(this.value)" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
</button> <option value="">Alle projecten</option>
<?php foreach ($allProjects as $pName => $pData): ?>
<option value="<?= htmlspecialchars($pName) ?>"><?= htmlspecialchars($pName) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="button" onclick="selectQueueFilter('DX')" data-queue="DX" class="queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0"> <div>
<i data-lucide="zap" class="w-3.5 h-3.5 text-amber-500"></i> <label class="block text-[11px] font-semibold text-slate-500 dark:text-slate-400 mb-1">Type</label>
<span>DX Wachtrij (Koppelingen)</span> <select id="filterType" onchange="applyFilters()" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
<span id="queueCount_DX" class="px-1.5 py-0.5 rounded-full text-[10px] bg-amber-100 dark:bg-amber-950/60 text-amber-800 dark:text-amber-300 font-bold">0</span> <option value="">Alle typen</option>
</button> <?php foreach ($allTypes as $t): ?>
<option value="<?= htmlspecialchars($t) ?>"><?= htmlspecialchars($t) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="button" onclick="selectQueueFilter('SD')" data-queue="SD" class="queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0"> <div>
<i data-lucide="wrench" class="w-3.5 h-3.5 text-sky-500"></i> <label class="block text-[11px] font-semibold text-slate-500 dark:text-slate-400 mb-1">Prioriteit</label>
<span>SD Wachtrij (Regulier)</span> <select id="filterPriority" onchange="applyFilters()" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
<span id="queueCount_SD" class="px-1.5 py-0.5 rounded-full text-[10px] bg-sky-100 dark:bg-sky-950/60 text-sky-800 dark:text-sky-300 font-bold">0</span> <option value="">Alle prioriteiten</option>
</button> <option value="Highest">Highest</option>
<option value="High">High</option>
<option value="Medium">Medium</option>
<option value="Low">Low</option>
</select>
</div>
<?php foreach ($queueFilters as $qf): <div>
$code = $qf['filter_code'] ?? ''; <label class="block text-[11px] font-semibold text-slate-500 dark:text-slate-400 mb-1">Status</label>
if ($code === 'DX' || $code === 'SD' || empty($code)) continue; <select id="filterStatus" onchange="applyFilters()" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
?> <option value="">Alle statussen</option>
<button type="button" onclick="selectQueueFilter('<?= htmlspecialchars($code) ?>')" data-queue="<?= htmlspecialchars($code) ?>" class="queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0"> <?php foreach ($allStatuses as $s): ?>
<i data-lucide="filter" class="w-3.5 h-3.5 text-indigo-500"></i> <option value="<?= htmlspecialchars($s) ?>"><?= htmlspecialchars($s) ?></option>
<span><?= htmlspecialchars($qf['filter_name'] ?? $code) ?></span> <?php endforeach; ?>
<span id="queueCount_<?= htmlspecialchars($code) ?>" class="px-1.5 py-0.5 rounded-full text-[10px] bg-indigo-100 dark:bg-indigo-950/60 text-indigo-800 dark:text-indigo-300 font-bold">0</span> </select>
</button> </div>
<?php endforeach; ?>
<?php if ($isAdmin): ?> <div class="flex items-end">
<a href="/admin.php#jira-wachtrijen" title="Wachtrijfilters beheren in Admin" class="p-1.5 rounded-xl text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-200/60 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0"> <button type="button" onclick="resetAllFilters()" class="w-full flex items-center justify-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white bg-white hover:bg-slate-100 dark:bg-slate-800 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 transition cursor-pointer">
<i data-lucide="settings" class="w-3.5 h-3.5"></i> <i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
</a> <span>Herstel Filters</span>
<?php endif; ?> </button>
</div> </div>
</div> </div>
</div>
<!-- Filterbalk & Zoekveld -->
<div class="fociz-card p-4">
<div class="grid grid-cols-1 md:grid-cols-12 gap-3">
<!-- Zoekveld (key of titel) -->
<div class="md:col-span-4 relative">
<i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none"></i>
<input
type="text"
id="jiraSearchInput"
placeholder="Zoek op ticket key (bijv. FOC-101) of titel..."
oninput="applyFilters()"
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl pl-9 pr-8 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 transition placeholder-slate-400"
>
<button onclick="clearSearch()" id="clearSearchBtn" class="hidden absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</div>
<!-- Filter: Project -->
<div class="md:col-span-2">
<select id="filterProject" onchange="onProjectSelectChange(this.value)" class="w-full bg-slate-50 dark:bg-slate-900 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="">Alle projecten</option>
<?php foreach ($allProjects as $pName => $pData): ?>
<option value="<?= htmlspecialchars($pName) ?>"><?= htmlspecialchars($pName) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Filter: Type (Story, Bug, Task, etc.) -->
<div class="md:col-span-2">
<select id="filterType" onchange="applyFilters()" class="w-full bg-slate-50 dark:bg-slate-900 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="">Alle typen</option>
<?php foreach ($allTypes as $t): ?>
<option value="<?= htmlspecialchars($t) ?>"><?= htmlspecialchars($t) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Filter: Prioriteit -->
<div class="md:col-span-2">
<select id="filterPriority" onchange="applyFilters()" class="w-full bg-slate-50 dark:bg-slate-900 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="">Alle prioriteiten</option>
<option value="Highest">Highest</option>
<option value="High">High</option>
<option value="Medium">Medium</option>
<option value="Low">Low</option>
</select>
</div>
<!-- Filter: Status -->
<div class="md:col-span-2 flex items-center space-x-2">
<select id="filterStatus" onchange="applyFilters()" class="w-full bg-slate-50 dark:bg-slate-900 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="">Alle statussen</option>
<?php foreach ($allStatuses as $s): ?>
<option value="<?= htmlspecialchars($s) ?>"><?= htmlspecialchars($s) ?></option>
<?php endforeach; ?>
</select>
<button onclick="resetAllFilters()" title="Herstel alle filters" class="p-2 rounded-xl text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 bg-slate-50 hover:bg-slate-100 dark:bg-slate-900 dark:hover:bg-slate-800 border border-slate-200 dark:border-slate-800 transition cursor-pointer flex-shrink-0">
<i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Tabs Weergave --> <!-- Compacte Werkbalk: Tabs & Zoekveld direct boven de tickets -->
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 overflow-x-auto"> <div class="fociz-card p-3 sm:p-4 flex flex-col md:flex-row md:items-center justify-between gap-3 shadow-xs">
<div class="flex space-x-1 sm:space-x-2 py-1"> <!-- Tabs Navigatie -->
<button onclick="switchTab('assigned')" id="tabBtn_assigned" class="flex items-center space-x-2 px-4 py-2.5 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border-b-2 border-indigo-600 dark:border-indigo-400"> <div class="flex items-center space-x-1 sm:space-x-1.5 overflow-x-auto pb-1 md:pb-0">
<i data-lucide="user-check" class="w-4 h-4"></i> <button onclick="switchTab('assigned')" id="tabBtn_assigned" class="flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800/60 flex-shrink-0">
<i data-lucide="user-check" class="w-3.5 h-3.5"></i>
<span>Aan mij toegewezen</span> <span>Aan mij toegewezen</span>
<span id="tabCount_assigned" class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-indigo-200/70 dark:bg-indigo-800/60 text-indigo-800 dark:text-indigo-200"> <span id="tabCount_assigned" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-200/70 dark:bg-indigo-800/60 text-indigo-800 dark:text-indigo-200">
<?= count($assignedIssues) ?> <?= count($assignedIssues) ?>
</span> </span>
</button> </button>
<button onclick="switchTab('team')" id="tabBtn_team" class="flex items-center space-x-2 px-4 py-2.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60"> <button onclick="switchTab('team')" id="tabBtn_team" class="flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
<i data-lucide="users" class="w-4 h-4"></i> <i data-lucide="users" class="w-3.5 h-3.5"></i>
<span>Mijn Teams & Projecten</span> <span>Teams & Projecten</span>
<span id="tabCount_team" class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300"> <span id="tabCount_team" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
<?= count($teamIssues) ?> <?= count($teamIssues) ?>
</span> </span>
</button> </button>
<button onclick="switchTab('in_progress')" id="tabBtn_in_progress" class="flex items-center space-x-2 px-4 py-2.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60"> <button onclick="switchTab('in_progress')" id="tabBtn_in_progress" class="flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
<i data-lucide="clock" class="w-4 h-4"></i> <i data-lucide="clock" class="w-3.5 h-3.5"></i>
<span>In Uitvoering</span> <span>In Uitvoering</span>
<span id="tabCount_in_progress" class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300"> <span id="tabCount_in_progress" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
0 0
</span> </span>
</button> </button>
<button onclick="switchTab('done')" id="tabBtn_done" class="flex items-center space-x-2 px-4 py-2.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60"> <button onclick="switchTab('done')" id="tabBtn_done" class="flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
<i data-lucide="check-circle" class="w-4 h-4"></i> <i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
<span>Recent Afgerond</span> <span>Recent Afgerond</span>
<span id="tabCount_done" class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300"> <span id="tabCount_done" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
0 0
</span> </span>
</button> </button>
</div> </div>
<div class="text-[11px] text-slate-400 whitespace-nowrap pl-4 hidden sm:block"> <!-- Rechterkant: Zoekveld & Tickets Tellen -->
<span id="renderedCount">0</span> tickets getoond <div class="flex items-center space-x-2.5 w-full md:w-auto">
<div class="relative flex-1 md:w-72">
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none"></i>
<input
type="text"
id="jiraSearchInput"
placeholder="Zoek ticket (FOC-101) of titel..."
oninput="applyFilters()"
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl pl-8 pr-7 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 transition placeholder-slate-400"
>
<button onclick="clearSearch()" id="clearSearchBtn" class="hidden absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</div>
<div class="text-[11px] text-slate-400 whitespace-nowrap hidden sm:block">
<span id="renderedCount" class="font-bold text-slate-700 dark:text-slate-200">0</span> tickets
</div>
</div> </div>
</div> </div>
@@ -727,6 +723,44 @@ function calculateKpis() {
document.getElementById('tabCount_done').innerText = doneCount; document.getElementById('tabCount_done').innerText = doneCount;
} }
// Uitklappen en inklappen van de filters & statistieken drawer
function toggleJiraFiltersDrawer(forceOpen) {
const drawer = document.getElementById('jiraFiltersDrawer');
const chevron = document.getElementById('filtersDrawerChevron');
if (!drawer) return;
const willOpen = (typeof forceOpen === 'boolean') ? forceOpen : drawer.classList.contains('hidden');
if (willOpen) {
drawer.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
localStorage.setItem('fociz_jira_filters_open', '1');
} else {
drawer.classList.add('hidden');
if (chevron) chevron.classList.remove('rotate-180');
localStorage.setItem('fociz_jira_filters_open', '0');
}
}
// Bepaal aantal actieve filters voor de badge in de rechter onderhoek
function updateActiveFilterBadge() {
const badge = document.getElementById('activeFilterBadge');
if (!badge) return;
let activeCount = 0;
if (currentProject) activeCount++;
if (currentQueue) activeCount++;
if (document.getElementById('filterType')?.value) activeCount++;
if (document.getElementById('filterPriority')?.value) activeCount++;
if (document.getElementById('filterStatus')?.value) activeCount++;
if (activeCount > 0) {
badge.innerText = activeCount;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
// Wissel tussen tabs // Wissel tussen tabs
function switchTab(tab) { function switchTab(tab) {
currentTab = tab; currentTab = tab;
@@ -738,9 +772,9 @@ function switchTab(tab) {
const btn = document.getElementById(`tabBtn_${t}`); const btn = document.getElementById(`tabBtn_${t}`);
if (!btn) return; if (!btn) return;
if (t === tab) { if (t === tab) {
btn.className = "flex items-center space-x-2 px-4 py-2.5 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border-b-2 border-indigo-600 dark:border-indigo-400"; btn.className = "flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800/60 flex-shrink-0";
} else { } else {
btn.className = "flex items-center space-x-2 px-4 py-2.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60"; btn.className = "flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0";
} }
}); });
@@ -1027,6 +1061,7 @@ function applyFilters() {
return true; return true;
}); });
updateActiveFilterBadge();
renderTickets(filtered); renderTickets(filtered);
} }
@@ -1623,6 +1658,11 @@ document.addEventListener('keydown', function(e) {
// Initiële opstart // Initiële opstart
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Herstel filters drawer open/dicht status
if (localStorage.getItem('fociz_jira_filters_open') === '1') {
toggleJiraFiltersDrawer(true);
}
calculateKpis(); calculateKpis();
updateProjectPills(); updateProjectPills();
updateQueueCounts(); updateQueueCounts();
+302 -86
View File
@@ -564,8 +564,23 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</div> </div>
</div> </div>
<!-- Modular Customizable Widget Grid (Fociz v2.0) --> <!-- Modular Customizable Widget Grid Container (Fociz v2.0) -->
<div id="focizWidgetGrid" class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start"> <div class="relative w-full" id="focizWidgetGridContainer">
<!-- Blueprint Achtergrond Raster (zichtbaar zodra een widget wordt opgepakt) -->
<div id="widgetGridBlueprint" class="absolute inset-0 hidden pointer-events-none z-0 rounded-3xl border-2 border-dashed border-indigo-400/30 dark:border-indigo-500/25 bg-indigo-500/[0.02] p-3 transition-all duration-300">
<div class="grid grid-cols-1 lg:grid-cols-12 h-full gap-6">
<?php for ($col = 1; $col <= 12; $col++): ?>
<div class="h-full min-h-[360px] rounded-2xl border border-dashed border-indigo-400/25 dark:border-indigo-400/15 bg-indigo-50/20 dark:bg-indigo-950/20 flex flex-col items-center justify-between py-4 select-none">
<span class="text-[10px] font-mono font-black text-indigo-500/60 dark:text-indigo-400/50 tracking-wider">KOLOM <?= $col ?></span>
<div class="w-2 h-2 rounded-full bg-indigo-400/30 dark:bg-indigo-500/30"></div>
<span class="text-[10px] font-mono font-black text-indigo-500/40 dark:text-indigo-400/30">1/12</span>
</div>
<?php endfor; ?>
</div>
</div>
<!-- Widget Raster (Fociz v2.0) -->
<div id="focizWidgetGrid" class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start relative z-10 min-h-[300px]">
<?php foreach ($userWidgetLayout as $wId => $widget): <?php foreach ($userWidgetLayout as $wId => $widget):
if (empty($widget['enabled'])) continue; if (empty($widget['enabled'])) continue;
$span = (int)($widget['col_span'] ?? 6); $span = (int)($widget['col_span'] ?? 6);
@@ -583,7 +598,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
<!-- Bovenste rand van de widget: Prachtig gestyled en direct geïntegreerd in de widget --> <!-- Bovenste rand van de widget: Prachtig gestyled en direct geïntegreerd in de widget -->
<div class="widget-header-bar flex items-center justify-between px-3.5 py-1.5 bg-slate-50/90 dark:bg-[#0c0f17]/90 border border-b-0 border-slate-200/80 dark:border-slate-800/80 rounded-t-2xl select-none backdrop-blur-xs transition-colors group-hover/widget:border-indigo-400/50 dark:group-hover/widget:border-indigo-500/30"> <div class="widget-header-bar flex items-center justify-between px-3.5 py-1.5 bg-slate-50/90 dark:bg-[#0c0f17]/90 border border-b-0 border-slate-200/80 dark:border-slate-800/80 rounded-t-2xl select-none backdrop-blur-xs transition-colors group-hover/widget:border-indigo-400/50 dark:group-hover/widget:border-indigo-500/30">
<!-- Links: Drag Handle Grip + Widget Titel --> <!-- Links: Drag Handle Grip + Widget Titel -->
<div class="widget-drag-handle flex items-center space-x-1.5 cursor-grab active:cursor-grabbing text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 py-0.5 px-1 -ml-1 rounded-lg hover:bg-slate-200/50 dark:hover:bg-slate-800/50 transition group/drag" draggable="true" data-widget-id="<?= htmlspecialchars($wId) ?>" title="Sleep om te verplaatsen"> <div class="widget-drag-handle flex items-center space-x-1.5 cursor-grab active:cursor-grabbing text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 py-0.5 px-1 -ml-1 rounded-lg hover:bg-slate-200/50 dark:hover:bg-slate-800/50 transition group/drag touch-none" data-widget-id="<?= htmlspecialchars($wId) ?>" title="Sleep om te verplaatsen">
<i data-lucide="grip-vertical" class="w-3.5 h-3.5 text-slate-400 group-hover/drag:text-indigo-500 transition-colors flex-shrink-0"></i> <i data-lucide="grip-vertical" class="w-3.5 h-3.5 text-slate-400 group-hover/drag:text-indigo-500 transition-colors flex-shrink-0"></i>
<span class="text-xs font-bold text-slate-700 dark:text-slate-300 tracking-wide truncate max-w-[160px] sm:max-w-[240px]"><?= htmlspecialchars($widget['title']) ?></span> <span class="text-xs font-bold text-slate-700 dark:text-slate-300 tracking-wide truncate max-w-[160px] sm:max-w-[240px]"><?= htmlspecialchars($widget['title']) ?></span>
</div> </div>
@@ -1526,6 +1541,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</div><!-- /.fociz-widget-wrapper --> </div><!-- /.fociz-widget-wrapper -->
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</div><!-- /#focizWidgetGridContainer -->
<?php <?php
if ($isAjaxRequest) { if ($isAjaxRequest) {
$contentHtml = ob_get_clean(); $contentHtml = ob_get_clean();
@@ -2090,8 +2106,17 @@ function undoHideWidget(wId) {
}, 400); }, 400);
} }
// --- Direct On-Screen Drag & Drop Engine (Live Shift & Reflow) --- // --- Direct On-Screen Drag & Drop Engine (Hover Ghost, Blueprint Grid & Auto-Scroll) ---
let draggedWidgetEl = null; let activeDraggedWrapper = null;
let dragCandidate = null;
let isDraggingActive = false;
let floatingGhost = null;
let dropPlaceholder = null;
let grabOffsetX = 0;
let grabOffsetY = 0;
let currentPointerX = 0;
let currentPointerY = 0;
let autoScrollAnimId = null;
let dragInitialIndex = -1; let dragInitialIndex = -1;
function initPlannerWidgets() { function initPlannerWidgets() {
@@ -2111,100 +2136,290 @@ function initPlannerWidgets() {
} catch(e) {} } catch(e) {}
}); });
// Luister naar drag handles // Luister naar drag handles met Pointer Events voor soepele hovering en scroll
document.querySelectorAll('.widget-drag-handle').forEach(handle => { document.querySelectorAll('.widget-drag-handle').forEach(handle => {
handle.setAttribute('draggable', 'true'); handle.setAttribute('draggable', 'false'); // Uitschakelen van krakkemikkige native browser drag
handle.style.touchAction = 'none';
handle.onpointerdown = (e) => {
// Alleen primaire muisklik (links)
if (e.button !== 0) return;
// Negeer als er op knoppen in de buurt geklikt wordt
if (e.target.closest('.widget-settings-btn') || e.target.closest('.widget-settings-dropdown')) return;
handle.ondragstart = (e) => {
const wrapper = handle.closest('.fociz-widget-wrapper'); const wrapper = handle.closest('.fociz-widget-wrapper');
if (!wrapper) return; if (!wrapper) return;
draggedWidgetEl = wrapper;
// Bewaar beginpositie om te controleren op wijzigingen dragCandidate = {
const allWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper')); wrapper: wrapper,
dragInitialIndex = allWrappers.indexOf(draggedWidgetEl); handle: handle,
startX: e.clientX,
e.dataTransfer.effectAllowed = 'move'; startY: e.clientY
e.dataTransfer.setData('text/plain', wrapper.getAttribute('data-widget-id')); };
// Sluit eventuele open popover menu's
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
setTimeout(() => {
if (draggedWidgetEl) {
draggedWidgetEl.classList.add('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl');
}
}, 0);
};
handle.ondragend = (e) => {
if (draggedWidgetEl) {
draggedWidgetEl.classList.remove('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl');
}
saveCurrentGridOrderIfChanged();
draggedWidgetEl = null;
}; };
}); });
// Dragover op grid: verplaatst de widget live in het DOM zodat andere blokken direct opzij schuiven
grid.ondragover = (e) => {
if (!draggedWidgetEl) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
// Check of cursor onderaan het grid zweeft
const gridRect = grid.getBoundingClientRect();
if (e.clientY > gridRect.bottom - 40) {
if (grid.lastElementChild !== draggedWidgetEl) {
grid.appendChild(draggedWidgetEl);
}
return;
}
// Check of cursor bovenaan het grid zweeft
if (e.clientY < gridRect.top + 40) {
if (grid.firstElementChild !== draggedWidgetEl) {
grid.prepend(draggedWidgetEl);
}
return;
}
const target = e.target.closest('.fociz-widget-wrapper');
if (target && target !== draggedWidgetEl && target.parentElement === grid) {
const rect = target.getBoundingClientRect();
const midY = rect.top + rect.height / 2;
const midX = rect.left + rect.width / 2;
// Bepaal of de muis voorbij het midden is (verticaal of horizontaal in dezelfde rij)
const isAfter = (e.clientY > midY) || (Math.abs(e.clientY - midY) < rect.height * 0.35 && e.clientX > midX);
if (isAfter) {
if (target.nextElementSibling !== draggedWidgetEl) {
target.after(draggedWidgetEl);
}
} else {
if (target.previousElementSibling !== draggedWidgetEl) {
target.before(draggedWidgetEl);
}
}
}
};
grid.ondrop = (e) => {
e.preventDefault();
if (draggedWidgetEl) {
draggedWidgetEl.classList.remove('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl');
}
saveCurrentGridOrderIfChanged();
draggedWidgetEl = null;
};
} }
// Globale pointer listeners (blijven soepel actief zelfs als cursor snel beweegt)
window.addEventListener('pointermove', (e) => {
currentPointerX = e.clientX;
currentPointerY = e.clientY;
if (!isDraggingActive && dragCandidate) {
// Controleer of de gebruiker minimaal 5px heeft bewogen (voorkomt valse triggers bij simpele kliks)
const dist = Math.hypot(e.clientX - dragCandidate.startX, e.clientY - dragCandidate.startY);
if (dist > 5) {
startWidgetDragging(dragCandidate.wrapper, dragCandidate.startX, dragCandidate.startY);
}
}
if (isDraggingActive) {
updateFloatingGhostPosition();
updateDropTargetFromPointer();
}
});
window.addEventListener('pointerup', (e) => {
if (isDraggingActive) {
finishWidgetDragging();
}
dragCandidate = null;
});
window.addEventListener('pointercancel', () => {
if (isDraggingActive) {
cancelWidgetDragging();
}
dragCandidate = null;
});
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isDraggingActive) {
cancelWidgetDragging();
}
});
// Start het oppakken & slepen van de widget
function startWidgetDragging(wrapper, startX, startY) {
const grid = document.getElementById('focizWidgetGrid');
if (!grid || !wrapper) return;
isDraggingActive = true;
activeDraggedWrapper = wrapper;
// Sluit openstaande popovers
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
// Bepaal beginpositie
const allWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
dragInitialIndex = allWrappers.indexOf(wrapper);
// 1. Toon het 12-koloms Blueprint Achtergrond Raster
const blueprint = document.getElementById('widgetGridBlueprint');
if (blueprint) {
blueprint.classList.remove('hidden');
blueprint.classList.add('block');
}
// 2. Creëer de zwevende Hover Ghost kaart
const rect = wrapper.getBoundingClientRect();
grabOffsetX = startX - rect.left;
grabOffsetY = startY - rect.top;
floatingGhost = wrapper.cloneNode(true);
floatingGhost.id = 'widgetFloatingGhost';
floatingGhost.removeAttribute('data-widget-id');
floatingGhost.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
floatingGhost.querySelectorAll('.widget-settings-dropdown').forEach(d => d.remove());
floatingGhost.style.position = 'fixed';
floatingGhost.style.left = `${rect.left}px`;
floatingGhost.style.top = `${rect.top}px`;
floatingGhost.style.width = `${rect.width}px`;
floatingGhost.style.height = `${rect.height}px`;
floatingGhost.style.pointerEvents = 'none';
floatingGhost.style.zIndex = '999999';
floatingGhost.style.transform = 'scale(1.02) rotate(1.2deg)';
floatingGhost.style.opacity = '0.94';
floatingGhost.style.transition = 'transform 0.15s ease, box-shadow 0.15s ease';
floatingGhost.style.boxShadow = '0 25px 60px -10px rgba(79, 70, 229, 0.45), 0 0 0 2px rgba(99, 102, 241, 0.7)';
floatingGhost.style.cursor = 'grabbing';
document.body.appendChild(floatingGhost);
// 3. Plaats een duidelijke Drop Slot Placeholder in het raster
const span = wrapper.getAttribute('data-col-span') || '6';
dropPlaceholder = document.createElement('div');
dropPlaceholder.id = 'widgetDropPlaceholder';
dropPlaceholder.className = `lg:col-span-${span} col-span-1 min-w-0 rounded-2xl border-2 border-dashed border-indigo-500 dark:border-indigo-400 bg-indigo-50/70 dark:bg-indigo-950/50 flex flex-col items-center justify-center p-6 transition-all duration-200 shadow-inner`;
dropPlaceholder.style.minHeight = `${Math.max(130, Math.min(rect.height, 280))}px`;
dropPlaceholder.innerHTML = `
<div class="flex items-center space-x-2 text-indigo-600 dark:text-indigo-400 font-bold text-xs animate-pulse">
<svg class="w-5 h-5 animate-bounce" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path></svg>
<span>Laat los om hier te plaatsen</span>
</div>
`;
wrapper.after(dropPlaceholder);
wrapper.style.display = 'none';
document.body.classList.add('select-none', 'cursor-grabbing');
// 4. Start automatische soepele scroll bij nadering van schermranden
startAutoScroller();
}
function updateFloatingGhostPosition() {
if (!floatingGhost) return;
floatingGhost.style.left = `${currentPointerX - grabOffsetX}px`;
floatingGhost.style.top = `${currentPointerY - grabOffsetY}px`;
}
function updateDropTargetFromPointer() {
const grid = document.getElementById('focizWidgetGrid');
if (!grid || !dropPlaceholder || !activeDraggedWrapper) return;
const wrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper')).filter(w => w !== activeDraggedWrapper);
let targetFound = false;
for (const wrap of wrappers) {
const rect = wrap.getBoundingClientRect();
if (currentPointerY >= rect.top - 15 && currentPointerY <= rect.bottom + 15 &&
currentPointerX >= rect.left - 15 && currentPointerX <= rect.right + 15) {
const midY = rect.top + rect.height / 2;
const midX = rect.left + rect.width / 2;
const isAfter = (currentPointerY > midY) || (Math.abs(currentPointerY - midY) < rect.height * 0.35 && currentPointerX > midX);
if (isAfter) {
if (wrap.nextElementSibling !== dropPlaceholder) {
wrap.after(dropPlaceholder);
}
} else {
if (wrap.previousElementSibling !== dropPlaceholder) {
wrap.before(dropPlaceholder);
}
}
targetFound = true;
break;
}
}
if (!targetFound && wrappers.length > 0) {
const firstRect = wrappers[0].getBoundingClientRect();
const lastRect = wrappers[wrappers.length - 1].getBoundingClientRect();
if (currentPointerY < firstRect.top) {
if (grid.firstElementChild !== dropPlaceholder) {
grid.prepend(dropPlaceholder);
}
} else if (currentPointerY > lastRect.bottom) {
if (grid.lastElementChild !== dropPlaceholder) {
grid.appendChild(dropPlaceholder);
}
}
}
}
// Auto-scroller wanneer je met de widget naar de boven- of onderkant van het scherm beweegt
function startAutoScroller() {
stopAutoScroller();
function scrollStep() {
if (!isDraggingActive) return;
const viewportHeight = window.innerHeight;
const threshold = 110;
if (currentPointerY < threshold && window.scrollY > 0) {
const factor = (threshold - currentPointerY) / threshold;
const speed = Math.max(3, Math.round(factor * 26));
window.scrollBy(0, -speed);
updateDropTargetFromPointer();
} else if (currentPointerY > viewportHeight - threshold) {
const factor = (currentPointerY - (viewportHeight - threshold)) / threshold;
const speed = Math.max(3, Math.round(factor * 26));
window.scrollBy(0, speed);
updateDropTargetFromPointer();
}
autoScrollAnimId = requestAnimationFrame(scrollStep);
}
autoScrollAnimId = requestAnimationFrame(scrollStep);
}
function stopAutoScroller() {
if (autoScrollAnimId) {
cancelAnimationFrame(autoScrollAnimId);
autoScrollAnimId = null;
}
}
// Voltooi het slepen (loslaten & AJAX opslaan)
function finishWidgetDragging() {
isDraggingActive = false;
stopAutoScroller();
document.body.classList.remove('select-none', 'cursor-grabbing');
// Verberg blueprint raster
const blueprint = document.getElementById('widgetGridBlueprint');
if (blueprint) {
blueprint.classList.add('hidden');
blueprint.classList.remove('block');
}
// Verwijder zwevende ghost
if (floatingGhost) {
floatingGhost.remove();
floatingGhost = null;
}
// Plaats originele widget terug op de plek van de placeholder
if (dropPlaceholder && activeDraggedWrapper) {
dropPlaceholder.replaceWith(activeDraggedWrapper);
activeDraggedWrapper.style.display = '';
dropPlaceholder = null;
// Subtiel succes flitsje op de neergezette widget
activeDraggedWrapper.classList.add('ring-2', 'ring-indigo-500', 'shadow-xl');
setTimeout(() => {
activeDraggedWrapper?.classList.remove('ring-2', 'ring-indigo-500', 'shadow-xl');
}, 400);
}
// Controleer of de volgorde gewijzigd is en sla op via AJAX
saveCurrentGridOrderIfChanged();
activeDraggedWrapper = null;
}
// Annuleer slepen (bijv. via Escape)
function cancelWidgetDragging() {
isDraggingActive = false;
stopAutoScroller();
document.body.classList.remove('select-none', 'cursor-grabbing');
const blueprint = document.getElementById('widgetGridBlueprint');
if (blueprint) {
blueprint.classList.add('hidden');
blueprint.classList.remove('block');
}
if (floatingGhost) {
floatingGhost.remove();
floatingGhost = null;
}
if (dropPlaceholder && activeDraggedWrapper) {
dropPlaceholder.remove();
dropPlaceholder = null;
activeDraggedWrapper.style.display = '';
}
activeDraggedWrapper = null;
}
// Controleer gewijzigde volgorde en sla asynchroon op via AJAX
function saveCurrentGridOrderIfChanged() { function saveCurrentGridOrderIfChanged() {
const grid = document.getElementById('focizWidgetGrid'); const grid = document.getElementById('focizWidgetGrid');
if (!grid) return; if (!grid) return;
const currentWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper')); const currentWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
const currentIndex = currentWrappers.indexOf(draggedWidgetEl); const currentIndex = activeDraggedWrapper ? currentWrappers.indexOf(activeDraggedWrapper) : -1;
let hasOrderChanged = false; let hasOrderChanged = false;
currentWrappers.forEach((el, index) => { currentWrappers.forEach((el, index) => {
@@ -2219,6 +2434,7 @@ function saveCurrentGridOrderIfChanged() {
}); });
if (hasOrderChanged || (dragInitialIndex !== -1 && currentIndex !== dragInitialIndex)) { if (hasOrderChanged || (dragInitialIndex !== -1 && currentIndex !== dragInitialIndex)) {
// Geruisloos opslaan via AJAX
persistWidgetLayoutAuto(); persistWidgetLayoutAuto();
showWidgetToast('Widget verplaatst & indeling opgeslagen', 'success'); showWidgetToast('Widget verplaatst & indeling opgeslagen', 'success');
} }