Files
Fociz/jira.php
T
2026-09-14 16:24:23 +02:00

2287 lines
128 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Fociz — Atlassian Jira Hub
* Toont tickets toegewezen aan de gebruiker en project-/teamtickets.
* Biedt filters, zoekfunctie, KPI-statistieken en 1-klik import naar de Dagplanner en het Kanban bord.
*/
$pageTitle = 'Fociz — Jira Hub';
require_once __DIR__ . '/includes/header.php';
require_once __DIR__ . '/includes/integrations/jira.php';
$userId = (int)($currentUser['id'] ?? 0);
$isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$forceRefresh = isset($_GET['refresh']) && $_GET['refresh'] === '1';
// Haal Jira data op
$jiraData = jira_get_dashboard_data($userId, $forceRefresh);
$isDemo = !empty($jiraData['is_demo']);
$isLive = !empty($jiraData['success']) && !$isDemo;
$domain = $jiraData['domain'] ?? '';
$email = $jiraData['email'] ?? '';
$accountName = $jiraData['account_name'] ?? '';
$lastSync = $jiraData['last_sync_at'] ?? date('Y-m-d H:i:s');
$syncError = $jiraData['error'] ?? null;
$assignedIssues = $jiraData['assigned_issues'] ?? [];
$teamIssues = $jiraData['team_issues'] ?? [];
// Haal borden en kolommen op voor het "Naar Bord" modal
$db = get_db_connection();
if ($isAdmin) {
$boards = $db->query("SELECT id, title FROM boards WHERE is_archived = 0 ORDER BY id ASC")->fetchAll();
} else {
$stmt = $db->prepare("
SELECT b.id, b.title
FROM boards b
JOIN board_members bm ON b.id = bm.board_id
WHERE bm.user_id = ? AND b.is_archived = 0
ORDER BY b.id ASC
");
$stmt->execute([$userId]);
$boards = $stmt->fetchAll();
}
$boardIds = array_column($boards, 'id');
$lanesByBoard = [];
if (!empty($boardIds)) {
$inClause = implode(',', array_map('intval', $boardIds));
$lanes = $db->query("SELECT id, board_id, title, color FROM board_lanes WHERE board_id IN ({$inClause}) ORDER BY position ASC")->fetchAll();
foreach ($lanes as $lane) {
$lanesByBoard[$lane['board_id']][] = $lane;
}
}
foreach ($boards as &$b) {
$b['lanes'] = $lanesByBoard[$b['id']] ?? [];
}
unset($b);
// Verzamel unieke projecten en types voor filter dropdowns en project switcher
$allIssues = array_merge($assignedIssues, $teamIssues);
$allProjects = [];
$allTypes = [];
$allPriorities = ['Highest', 'High', 'Medium', 'Low'];
$allStatuses = [];
$projectCounts = [];
foreach ($allIssues as $iss) {
if (!empty($iss['project'])) {
$p = $iss['project'];
$allProjects[$p] = [
'name' => $p,
'key' => $iss['project_key'] ?? '',
];
$projectCounts[$p] = ($projectCounts[$p] ?? 0) + 1;
}
if (!empty($iss['issue_type'])) {
$allTypes[$iss['issue_type']] = $iss['issue_type'];
}
if (!empty($iss['status'])) {
$allStatuses[$iss['status']] = $iss['status'];
}
}
// Statussen uitbreiden met officiële Jira instantie statussen
$availableStatuses = $jiraData['available_statuses'] ?? [];
if (!empty($availableStatuses)) {
foreach ($availableStatuses as $st) {
if (!empty($st['name'])) {
$allStatuses[$st['name']] = $st['name'];
}
}
}
ksort($allProjects);
ksort($allTypes);
ksort($allStatuses);
$queueFilters = $jiraData['queue_filters'] ?? jira_get_queue_filters(true);
$defaultQueueFilter = $jiraData['default_queue_filter'] ?? jira_get_user_queue_preference($userId);
$userQueueFilters = $jiraData['user_queue_filters'] ?? jira_get_user_queue_preferences($userId);
$jiraDataJson = json_encode($jiraData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
?>
<div class="space-y-6 w-full">
<!-- Bovenbalk: Titel, Jira status & Actieknoppen -->
<div class="fociz-card p-5 sm:p-6">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<!-- Linkerkant: Titel & Atlassian Jira branding -->
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-tr from-blue-600 via-indigo-600 to-sky-500 text-white flex items-center justify-center shadow-md shadow-indigo-500/20 flex-shrink-0">
<i data-lucide="layers" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2.5">
<h1 class="text-2xl font-black text-slate-900 dark:text-white tracking-tight">Atlassian Jira Hub</h1>
<?php if ($isLive): ?>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5 animate-pulse"></span>
Live Verbonden
</span>
<?php else: ?>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-300 dark:border-amber-800/80">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 mr-1.5"></span>
Demomodus
</span>
<?php endif; ?>
</div>
</div>
</div>
<!-- Rechterkant: Knoppen Vernieuwen & Instellingen -->
<div class="flex items-center space-x-2.5 self-start md:self-center">
<button onclick="refreshJiraData()" id="refreshBtn" class="flex items-center space-x-2 px-3.5 py-2 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">
<i data-lucide="refresh-cw" id="refreshIcon" class="w-3.5 h-3.5"></i>
<span>Vernieuwen</span>
</button>
<a href="/profile.php#integraties" class="flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-semibold bg-indigo-600 hover:bg-indigo-700 text-white transition cursor-pointer shadow-sm shadow-indigo-600/20">
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
<span>Jira Instellingen</span>
</a>
</div>
</div>
<?php if ($isDemo): ?>
<!-- Demomodus Informatiebanner -->
<div class="mt-5 p-4 rounded-xl bg-gradient-to-r from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/20 border border-amber-200/90 dark:border-amber-800/50 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-lg bg-amber-500/15 text-amber-600 dark:text-amber-400 flex items-center justify-center flex-shrink-0 mt-0.5">
<i data-lucide="info" class="w-4 h-4"></i>
</div>
<div>
<p class="text-xs font-bold text-amber-900 dark:text-amber-200">
Interactieve Voorbeeldweergave (Sandbox)
</p>
<p class="text-xs text-amber-700 dark:text-amber-300/80 mt-0.5">
Je bekijkt momenteel demotickets. Koppel je Atlassian Jira Cloud instantie in je profiel om direct de live tickets van jouw organisatie in te laden.
</p>
</div>
</div>
<a href="/profile.php#integraties" class="px-3.5 py-1.5 rounded-lg bg-amber-600 hover:bg-amber-700 text-white font-bold text-xs shadow-xs transition flex-shrink-0 cursor-pointer">
Koppel Jira Cloud
</a>
</div>
<?php endif; ?>
<?php if (!empty($syncError)): ?>
<!-- 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">
<i data-lucide="alert-triangle" class="w-4 h-4 text-red-500 flex-shrink-0"></i>
<div class="flex-1">
<strong>Synchronisatiefout:</strong> <?= htmlspecialchars($syncError) ?>
</div>
<a href="/profile.php#integraties" class="underline hover:no-underline font-semibold">Controleer instellingen</a>
</div>
<?php endif; ?>
<!-- Hub Voettekst: Live Status & Filters Uitklapknop (Rechtsonder) -->
<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">
<?php if ($isLive): ?>
<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>
<span class="text-slate-300 dark:text-slate-700 hidden sm:inline">•</span>
<span class="text-[11px] text-slate-400 hidden sm:inline">Sync: <?= date('d-m-Y H:i', strtotime($lastSync)) ?></span>
<?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>
<!-- Uitklapknop in de rechter onderhoek -->
<div class="flex items-center space-x-2 ml-auto">
<button type="button"
onclick="toggleJiraFiltersDrawer()"
id="btnToggleFiltersDrawer"
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">
<i data-lucide="sliders-horizontal" class="w-3.5 h-3.5 text-indigo-500 group-hover:rotate-12 transition-transform"></i>
<span>Filters & Statistieken</span>
<span id="activeFilterBadge" class="hidden px-1.5 py-0.5 rounded-full text-[10px] font-black bg-indigo-600 text-white leading-tight">0</span>
<i data-lucide="chevron-down" id="filtersDrawerChevron" class="w-3.5 h-3.5 transition-transform duration-200"></i>
</button>
</div>
</div>
<!-- Uitklapbare Filters & Statistieken Drawer -->
<div id="jiraFiltersDrawer" class="hidden mt-4 pt-4 border-t border-slate-100 dark:border-slate-800/80 space-y-4">
<!-- 1. 4 KPI Statistieken Kaarten -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
<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 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. Geavanceerde Filter Dropdowns -->
<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="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
<div>
<label class="block text-[11px] font-semibold text-slate-500 dark:text-slate-400 mb-1">Issue Type</label>
<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">
<option value="">Alle typen</option>
<?php foreach ($allTypes as $t): ?>
<option value="<?= htmlspecialchars($t) ?>"><?= htmlspecialchars($t) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="block text-[11px] font-semibold text-slate-500 dark:text-slate-400 mb-1">Prioriteit</label>
<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">
<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>
<div class="flex items-end">
<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="rotate-ccw" class="w-3.5 h-3.5"></i>
<span>Herstel Alle Filters</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Hoofdbalk: Project Switcher, Wachtrijen, Tabs & Status Filtering -->
<div class="fociz-card p-4 sm:p-5 space-y-4 shadow-sm border border-slate-200/80 dark:border-slate-800">
<!-- Rij 1: Prominente Project Switcher (SD, IN, AAN, GEN, Alle) -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3 pb-3 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-lg bg-indigo-50 dark:bg-indigo-950/60 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="layers" class="w-4 h-4"></i>
</div>
<div>
<span class="text-xs font-black uppercase tracking-wider text-slate-900 dark:text-white">Projecten</span>
<p class="text-[10px] text-slate-400">Direct filteren op projectcode</p>
</div>
</div>
<div class="flex items-center gap-1.5 overflow-x-auto pb-1 md:pb-0" 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-slate-900 dark:bg-white text-white dark:text-slate-900 shadow-xs flex-shrink-0">
<i data-lucide="grid" class="w-3.5 h-3.5"></i>
<span>Alle Projecten</span>
<span id="projCount_all" class="px-1.5 py-0.5 rounded-full text-[10px] bg-white/20 dark:bg-slate-900/20 font-black"><?= count($allIssues) ?></span>
</button>
<!-- SD: Servicedesk -->
<button type="button" onclick="selectProject('SD')" data-project="SD" 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-sky-50 dark:bg-sky-950/40 text-sky-800 dark:text-sky-300 hover:bg-sky-100 dark:hover:bg-sky-900/60 border border-sky-300 dark:border-sky-800 flex-shrink-0">
<span class="px-1.5 py-0.5 rounded font-mono text-[10px] font-black bg-sky-200 dark:bg-sky-800 text-sky-900 dark:text-sky-100">SD</span>
<span>Servicedesk</span>
<span id="projCount_SD" class="px-1.5 py-0.5 rounded-full text-[10px] bg-sky-200/80 dark:bg-sky-800/80 font-bold">0</span>
</button>
<!-- IN: Interfaces -->
<button type="button" onclick="selectProject('IN')" data-project="IN" 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-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 border border-amber-300 dark:border-amber-800 flex-shrink-0">
<span class="px-1.5 py-0.5 rounded font-mono text-[10px] font-black bg-amber-200 dark:bg-amber-800 text-amber-900 dark:text-amber-100">IN</span>
<span>Interfaces</span>
<span id="projCount_IN" class="px-1.5 py-0.5 rounded-full text-[10px] bg-amber-200/80 dark:bg-amber-800/80 font-bold">0</span>
</button>
<!-- AAN: Aanvragen -->
<button type="button" onclick="selectProject('AAN')" data-project="AAN" 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-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/60 border border-emerald-300 dark:border-emerald-800 flex-shrink-0">
<span class="px-1.5 py-0.5 rounded font-mono text-[10px] font-black bg-emerald-200 dark:bg-emerald-800 text-emerald-900 dark:text-emerald-100">AAN</span>
<span>Aanvragen</span>
<span id="projCount_AAN" class="px-1.5 py-0.5 rounded-full text-[10px] bg-emerald-200/80 dark:bg-emerald-800/80 font-bold">0</span>
</button>
<!-- GEN: General -->
<button type="button" onclick="selectProject('GEN')" data-project="GEN" 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-50 dark:bg-indigo-950/40 text-indigo-800 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/60 border border-indigo-300 dark:border-indigo-800 flex-shrink-0">
<span class="px-1.5 py-0.5 rounded font-mono text-[10px] font-black bg-indigo-200 dark:bg-indigo-800 text-indigo-900 dark:text-indigo-100">GEN</span>
<span>General</span>
<span id="projCount_GEN" class="px-1.5 py-0.5 rounded-full text-[10px] bg-indigo-200/80 dark:bg-indigo-800/80 font-bold">0</span>
</button>
<?php foreach ($allProjects as $pKey => $pData):
$pKeyNorm = strtoupper($pKey);
$pDataKeyNorm = strtoupper($pData['key'] ?? '');
if (in_array($pKeyNorm, ['SD', 'IN', 'AAN', 'GEN']) || in_array($pDataKeyNorm, ['SD', 'IN', 'AAN', 'GEN'])) continue;
$displayKey = !empty($pData['key']) ? $pData['key'] : $pKey;
?>
<button type="button" onclick="selectProject('<?= htmlspecialchars(addslashes($displayKey)) ?>')" data-project="<?= htmlspecialchars($displayKey) ?>" 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 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0">
<span class="px-1.5 py-0.5 rounded font-mono text-[10px] font-black bg-slate-200 dark:bg-slate-700"><?= htmlspecialchars($displayKey) ?></span>
<span><?= htmlspecialchars($pData['name']) ?></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[$pKey] ?? 0 ?></span>
</button>
<?php endforeach; ?>
</div>
</div>
<!-- Rij 2: Wachtrijen & Multi-queue voorkeur -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3 pb-3 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-lg bg-indigo-50 dark:bg-indigo-950/60 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="filter" class="w-4 h-4"></i>
</div>
<div>
<span class="text-xs font-black uppercase tracking-wider text-slate-900 dark:text-white">Wachtrij</span>
<p class="text-[10px] text-slate-400">Filter op gekoppelde wachtrijen</p>
</div>
</div>
<div class="flex items-center gap-1.5 overflow-x-auto pb-1 md:pb-0" id="queueButtonsContainer">
<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">
<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>
<?php if (!empty($userQueueFilters)): ?>
<button type="button" onclick="selectQueueFilter('MY_QUEUES')" data-queue="MY_QUEUES" 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-amber-50 dark:bg-amber-950/50 text-amber-800 dark:text-amber-200 border border-amber-300 dark:border-amber-700/80 flex-shrink-0" title="Alleen tickets uit jouw gekoppelde wachtrijen (<?= htmlspecialchars(implode(', ', $userQueueFilters)) ?>)">
<i data-lucide="star" class="w-3.5 h-3.5 text-amber-500 fill-amber-500"></i>
<span>Mijn Wachtrijen (<?= htmlspecialchars(implode('+', $userQueueFilters)) ?>)</span>
<span id="queueCount_MY_QUEUES" class="px-1.5 py-0.5 rounded-full text-[10px] bg-amber-200 dark:bg-amber-800 font-bold">0</span>
</button>
<?php endif; ?>
<button type="button" onclick="selectQueueFilter('SD')" data-queue="SD" class="queue-pill flex items-center space-x-1.5 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">
<span class="w-2 h-2 rounded-full bg-sky-500"></span>
<span>SD (Servicedesk)</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>
<button type="button" onclick="selectQueueFilter('IN')" data-queue="IN" class="queue-pill flex items-center space-x-1.5 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">
<span class="w-2 h-2 rounded-full bg-amber-500"></span>
<span>IN (Interfaces)</span>
<span id="queueCount_IN" 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('AAN')" data-queue="AAN" class="queue-pill flex items-center space-x-1.5 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">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span>AAN (Aanvragen)</span>
<span id="queueCount_AAN" class="px-1.5 py-0.5 rounded-full text-[10px] bg-emerald-100 dark:bg-emerald-950/60 text-emerald-800 dark:text-emerald-300 font-bold">0</span>
</button>
<button type="button" onclick="selectQueueFilter('GEN')" data-queue="GEN" class="queue-pill flex items-center space-x-1.5 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">
<span class="w-2 h-2 rounded-full bg-indigo-500"></span>
<span>GEN (General)</span>
<span id="queueCount_GEN" 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>
<button type="button" onclick="selectQueueFilter('DX')" data-queue="DX" class="queue-pill flex items-center space-x-1.5 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>
<?php if ($isAdmin): ?>
<a href="/admin.php#jira" title="Wachtrijfilters beheren in Beheer" class="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 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>
<!-- Rij 3: Tabs, Verberg Gesloten, In behandeling & Zoekveld -->
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-3">
<!-- Tabs Navigatie -->
<div class="flex items-center space-x-1 sm:space-x-1.5 overflow-x-auto pb-1 lg:pb-0">
<button onclick="switchTab('assigned')" id="tabBtn_assigned" class="flex items-center space-x-1.5 px-3 py-1.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 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</span>
<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) ?>
</span>
</button>
<button onclick="switchTab('team')" id="tabBtn_team" class="flex items-center space-x-1.5 px-3 py-1.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 border border-transparent flex-shrink-0">
<i data-lucide="users" class="w-3.5 h-3.5"></i>
<span>Team & Projecten</span>
<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) ?>
</span>
</button>
<button onclick="switchTab('in_progress')" id="tabBtn_in_progress" class="flex items-center space-x-1.5 px-3 py-1.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 border border-transparent flex-shrink-0">
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
<span>In Uitvoering</span>
<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
</span>
</button>
<button onclick="switchTab('done')" id="tabBtn_done" class="flex items-center space-x-1.5 px-3 py-1.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 border border-transparent flex-shrink-0">
<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
<span>Afgerond</span>
<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
</span>
</button>
</div>
<!-- Status Acties: Verberg Gesloten + Dropdown + Zoeken -->
<div class="flex flex-wrap items-center gap-2">
<!-- Toggle: Verberg Gesloten (Standaard Actief!) -->
<button type="button" onclick="toggleHideClosed()" id="btnHideClosed" class="flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80 shadow-2xs" title="Verberg afgeronde en gesloten tickets">
<i data-lucide="eye-off" id="iconHideClosed" class="w-3.5 h-3.5 text-emerald-600"></i>
<span>Verberg Gesloten</span>
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
</button>
<!-- Snelfilter: In behandeling -->
<button type="button" onclick="toggleOnlyInProgress()" id="btnOnlyInProgress" class="flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700" title="Toon alleen tickets die op In Behandeling staan">
<i data-lucide="play" class="w-3 h-3 text-blue-500"></i>
<span>In behandeling</span>
</button>
<!-- Status Dropdown met Jira statussen -->
<div class="relative">
<select id="filterStatus" onchange="onStatusSelectChange(this.value)" class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl pl-2.5 pr-7 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer">
<option value="">Alle statussen</option>
<optgroup label="Nieuw / To Do">
<option value="To Do">To Do</option>
<option value="Nieuw">Nieuw</option>
</optgroup>
<optgroup label="In Uitvoering / Behandeling">
<option value="In behandeling">In behandeling</option>
<option value="In Progress">In Progress</option>
<option value="Wachten op klant">Wachten op klant</option>
<option value="In Review">In Review</option>
</optgroup>
<optgroup label="Afgerond / Gesloten">
<option value="Gereed">Gereed</option>
<option value="Gesloten">Gesloten</option>
<option value="Done">Done</option>
</optgroup>
</select>
</div>
<!-- Zoekveld -->
<div class="relative w-full sm:w-44">
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2 pointer-events-none"></i>
<input
type="text"
id="jiraSearchInput"
placeholder="Zoek ticket..."
oninput="applyFilters()"
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl pl-7 pr-6 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
<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 h-3"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Tickets Container -->
<div id="ticketsContainer" class="space-y-3">
<!-- Wordt dynamisch gerenderd via JavaScript -->
</div>
<!-- Lege Toestand (No Results) -->
<div id="emptyStateContainer" class="hidden fociz-card p-12 text-center">
<div class="w-12 h-12 mx-auto rounded-2xl bg-slate-100 dark:bg-slate-800 text-slate-400 flex items-center justify-center mb-3">
<i data-lucide="inbox" class="w-6 h-6"></i>
</div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Geen tickets gevonden</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1 max-w-sm mx-auto">
Er zijn geen Jira tickets die overeenkomen met de geselecteerde tab of zoekfilters.
</p>
<button onclick="resetAllFilters()" class="mt-4 px-4 py-2 rounded-xl text-xs font-semibold bg-indigo-600 hover:bg-indigo-700 text-white transition cursor-pointer">
Filters Wissen
</button>
</div>
</div>
<!-- MODAL 1: Inplannen in Dagplanner (planner.php) -->
<div id="planTaskModal" onclick="if(event.target === this) closePlanModal()" 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="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl p-6 transition transform">
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-lg bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
<i data-lucide="calendar" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Inplannen in Dagplanner</h3>
<p class="text-[11px] text-slate-400">Voeg toe aan je dagfocus op de tijdlijn</p>
</div>
</div>
<button onclick="closePlanModal()" 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-4 h-4"></i>
</button>
</div>
<form id="planTaskForm" onsubmit="submitPlanTask(event)" class="mt-4 space-y-4">
<input type="hidden" id="planIssueKey" value="">
<input type="hidden" id="planIssueSummary" value="">
<input type="hidden" id="planIssueUrl" value="">
<!-- Ticket Info Preview met uitklapbare inhoud -->
<div class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-950 border border-slate-200/80 dark:border-slate-800/80 space-y-2">
<div class="flex items-center space-x-2">
<span id="planTicketKeyBadge" class="font-mono text-xs font-bold text-indigo-600 dark:text-indigo-400">FOC-101</span>
<span id="planTicketProject" class="text-[11px] text-slate-400">• Fociz Platform</span>
<span id="planSpBadge" class="hidden ml-auto text-[10px] font-bold px-2 py-0.5 rounded-full bg-violet-50 dark:bg-violet-500/10 text-violet-700 dark:text-violet-300 border border-violet-200 dark:border-violet-500/20"></span>
</div>
<div id="planTicketSummaryText" class="text-xs font-semibold text-slate-800 dark:text-slate-200 line-clamp-2">
Tweestapsverificatie implementeren
</div>
<div id="planSpHint" class="hidden mt-1 text-[11px] text-violet-600 dark:text-violet-400 flex items-center space-x-1">
<i data-lucide="zap" class="w-3 h-3"></i>
<span id="planSpHintText"></span>
</div>
<!-- Uitklapbare beschrijving (10 regels verticaal scrollen) -->
<div class="pt-1.5 border-t border-slate-200/60 dark:border-slate-800/60">
<button type="button" onclick="togglePlanTicketDesc()" class="text-[11px] text-indigo-600 dark:text-indigo-400 hover:underline flex items-center space-x-1 font-medium cursor-pointer">
<i data-lucide="file-text" class="w-3 h-3"></i>
<span id="planTicketDescToggleText">Toon ticket inhoud</span>
<i data-lucide="chevron-down" id="planTicketDescChevron" class="w-3 h-3 transition-transform duration-200"></i>
</button>
<div id="planTicketDescContainer" class="hidden mt-2 p-3 rounded-lg bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-[11px] text-slate-700 dark:text-slate-300 max-h-40 overflow-y-auto leading-relaxed whitespace-pre-wrap select-text font-normal">
</div>
</div>
</div>
<!-- Aanpasbare Taaktitel in Agenda -->
<div>
<div class="flex items-center justify-between mb-1">
<label for="planTaskTitleInput" class="block text-xs font-bold text-slate-700 dark:text-slate-300">Naam op de tijdlijn</label>
<button type="button" onclick="clearPlanTaskTitle()" class="text-[10px] text-slate-400 hover:text-rose-500 cursor-pointer flex items-center space-x-0.5" title="Titel wissen">
<i data-lucide="x-circle" class="w-3 h-3"></i>
<span>Wissen</span>
</button>
</div>
<div class="relative">
<input
type="text"
id="planTaskTitleInput"
required
placeholder="Bijv. Oppakken Jira item FOC-101"
class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-xs text-slate-900 dark:text-white font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
>
<button type="button" onclick="clearPlanTaskTitle()" class="absolute right-2.5 top-2.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer" title="Wissen">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
<!-- Datum & Slimme "Plan in open blok" knop -->
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
<button type="button" onclick="planInNextOpenBlockJira()" id="btnJiraPlanOpenBlock" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-gradient-to-r from-indigo-600 to-sky-600 hover:from-indigo-500 hover:to-sky-500 text-white text-xs font-bold transition shadow-xs cursor-pointer active:scale-95" title="Vind automatisch het eerstvolgende vrije kwartierblok binnen werktijden">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
<span>Plan in open blok</span>
</button>
</div>
<!-- Open blok banner -->
<div id="jiraPlanOpenBlockNotice" class="hidden p-2.5 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-800 dark:text-emerald-300 text-[11px] flex items-center justify-between">
<div class="flex items-center space-x-1.5 min-w-0 flex-1 mr-2">
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i>
<span id="jiraPlanOpenBlockNoticeText" class="truncate font-semibold"></span>
</div>
<button type="button" onclick="document.getElementById('jiraPlanOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded cursor-pointer">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
</div>
</div>
<!-- Datum Kiezer -->
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Datum</label>
<input
type="date"
id="planDateInput"
value="<?= date('Y-m-d') ?>"
required
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"
>
</div>
<!-- Tijdsblok Starttijd & Duur -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Starttijd (optioneel)</label>
<span id="planEndTimeBadge" class="text-[10px] text-indigo-600 dark:text-indigo-400 font-mono font-semibold"></span>
</div>
<div class="relative">
<input
type="time"
id="planStartTimeInput"
value="09:00"
oninput="updatePlanEndTimeBadge()"
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 font-mono text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
>
</div>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Verwachte Duur</label>
<select id="planDurationInput" onchange="updatePlanEndTimeBadge()" 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="15">15 minuten</option>
<option value="30">30 minuten</option>
<option value="45" selected>45 minuten</option>
<option value="60">1 uur (60 min)</option>
<option value="90">1,5 uur (90 min)</option>
<option value="120">2 uur (120 min)</option>
</select>
</div>
</div>
<!-- Prioriteit -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Focus Prioriteit</label>
<div class="grid grid-cols-3 gap-2">
<label class="flex items-center justify-center p-2 rounded-xl border border-slate-200 dark:border-slate-800 text-xs font-bold cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/50 has-checked:border-red-500 has-checked:bg-red-50 dark:has-checked:bg-red-950/30 has-checked:text-red-600">
<input type="radio" name="planPriority" value="P1" class="sr-only">
<span>P1 • Hoog</span>
</label>
<label class="flex items-center justify-center p-2 rounded-xl border border-slate-200 dark:border-slate-800 text-xs font-bold cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/50 has-checked:border-amber-500 has-checked:bg-amber-50 dark:has-checked:bg-amber-950/30 has-checked:text-amber-600">
<input type="radio" name="planPriority" value="P2" checked class="sr-only">
<span>P2 • Normaal</span>
</label>
<label class="flex items-center justify-center p-2 rounded-xl border border-slate-200 dark:border-slate-800 text-xs font-bold cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/50 has-checked:border-slate-500 has-checked:bg-slate-100 dark:has-checked:bg-slate-800 has-checked:text-slate-800 dark:has-checked:text-slate-200">
<input type="radio" name="planPriority" value="P3" class="sr-only">
<span>P3 • Laag</span>
</label>
</div>
</div>
<!-- Knoppen -->
<div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800">
<button type="button" onclick="closePlanModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
Annuleren
</button>
<button type="submit" id="planSubmitBtn" class="flex items-center space-x-1.5 px-4 py-2 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-700 text-white transition cursor-pointer shadow-sm shadow-indigo-600/20">
<i data-lucide="check" class="w-3.5 h-3.5"></i>
<span>In Dagplanner Plakken</span>
</button>
</div>
</form>
</div>
</div>
<!-- Modal: Waarschuwing Buiten Werktijden (Jira) -->
<div id="outsideWorkhoursModalJira" onclick="if(event.target === this) closeOutsideWorkhoursModalJira('CANCEL')" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
<div class="bg-white dark:bg-slate-900 border border-amber-500/30 rounded-2xl w-full max-w-md shadow-2xl p-6 space-y-4">
<div class="flex items-start space-x-3">
<div class="w-10 h-10 rounded-2xl bg-amber-500/10 border border-amber-500/20 text-amber-500 flex items-center justify-center flex-shrink-0">
<i data-lucide="alert-triangle" class="w-5 h-5"></i>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base font-bold text-slate-900 dark:text-white leading-tight">
Let op! Je plant buiten werktijden
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">
Het ingeplande tijdblok valt buiten de gezette werktijden.
</p>
</div>
<button type="button" onclick="closeOutsideWorkhoursModalJira('CANCEL')" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg transition cursor-pointer flex-shrink-0">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800/40 rounded-xl p-3.5 text-xs text-amber-800 dark:text-amber-300 space-y-1.5">
<div class="flex items-center justify-between text-[11px] font-semibold text-amber-900 dark:text-amber-200">
<span>Gekozen tijdblok:</span>
<span id="outsideWorkhoursSelectedSlotJira" class="font-mono font-bold"></span>
</div>
<div class="flex items-center justify-between text-[11px] text-amber-700 dark:text-amber-400">
<span>Gezette werktijden:</span>
<span id="outsideWorkhoursBoundsJira" class="font-mono font-semibold">08:30 – 17:00</span>
</div>
<p class="text-[11px] text-amber-700/80 dark:text-amber-400/80 pt-1 border-t border-amber-200/60 dark:border-amber-800/30">
Kies <strong>Nee</strong> om het item automatisch in te plannen in het eerstvolgende open blok op de volgende werkdag, of kies <strong>Ja</strong> om toch nu buiten werktijd te plannen.
</p>
</div>
<div class="flex items-center space-x-2.5 pt-2">
<button type="button" id="btnOutsideWorkhoursNoJira" onclick="closeOutsideWorkhoursModalJira('NEE')" class="flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition shadow-xs flex items-center justify-center space-x-1.5 cursor-pointer">
<i data-lucide="calendar-arrow-up" class="w-4 h-4"></i>
<span>Nee (Volgende werkdag)</span>
</button>
<button type="button" id="btnOutsideWorkhoursYesJira" onclick="closeOutsideWorkhoursModalJira('JA')" class="px-4 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 font-semibold text-xs transition cursor-pointer">
<span>Ja, toch inplannen</span>
</button>
</div>
</div>
</div>
<!-- MODAL 2: Toevoegen aan Kanban Bord (board.php) -->
<div id="boardTaskModal" onclick="if(event.target === this) closeBoardModal()" 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="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl w-full max-w-md shadow-2xl p-6 transition transform">
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-lg bg-emerald-50 dark:bg-emerald-950/50 text-emerald-600 dark:text-emerald-400 flex items-center justify-center">
<i data-lucide="kanban" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Plaats op Kanban Bord</h3>
<p class="text-[11px] text-slate-400">Maak een post-it kaart aan</p>
</div>
</div>
<button onclick="closeBoardModal()" 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-4 h-4"></i>
</button>
</div>
<form id="boardTaskForm" onsubmit="submitBoardTask(event)" class="mt-4 space-y-4">
<input type="hidden" id="boardIssueKey" value="">
<input type="hidden" id="boardIssueSummary" value="">
<input type="hidden" id="boardIssueUrl" value="">
<input type="hidden" id="boardIssueProject" value="">
<!-- Ticket Info Preview -->
<div class="p-3 rounded-xl bg-slate-50 dark:bg-slate-950 border border-slate-200/80 dark:border-slate-800/80">
<div class="flex items-center space-x-2">
<span id="boardTicketKeyBadge" class="font-mono text-xs font-bold text-emerald-600 dark:text-emerald-400">FOC-101</span>
<span id="boardTicketProject" class="text-[11px] text-slate-400">• Fociz Platform</span>
</div>
<div id="boardTicketSummaryText" class="text-xs font-semibold text-slate-800 dark:text-slate-200 mt-1 line-clamp-2">
Tweestapsverificatie implementeren
</div>
</div>
<!-- Bord Selectie -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Kies Kanban Bord</label>
<select id="boardSelectInput" onchange="updateLaneOptions()" required 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-emerald-500/40 cursor-pointer">
<?php if (empty($boards)): ?>
<option value="">Geen borden beschikbaar</option>
<?php else: ?>
<?php foreach ($boards as $b): ?>
<option value="<?= (int)$b['id'] ?>"><?= htmlspecialchars($b['title']) ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<!-- Kolom / Lane Selectie -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Kies Kolom / Fase</label>
<select id="laneSelectInput" required 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-emerald-500/40 cursor-pointer">
<!-- Wordt dynamisch gevuld -->
</select>
</div>
<!-- Kaartkleur & Prioriteit -->
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Kaartkleur</label>
<select id="cardColorInput" 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-emerald-500/40 cursor-pointer">
<option value="blue" selected>Blauw (Standaard)</option>
<option value="yellow">Geel</option>
<option value="green">Groen</option>
<option value="purple">Paars</option>
<option value="red">Rood</option>
<option value="pink">Roze</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
<select id="cardPriorityInput" 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-emerald-500/40 cursor-pointer">
<option value="P1">P1 • Hoog</option>
<option value="P2" selected>P2 • Normaal</option>
<option value="P3">P3 • Laag</option>
</select>
</div>
</div>
<!-- Knoppen -->
<div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800">
<button type="button" onclick="closeBoardModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
Annuleren
</button>
<button type="submit" id="boardSubmitBtn" class="flex items-center space-x-1.5 px-4 py-2 rounded-xl text-xs font-bold bg-emerald-600 hover:bg-emerald-700 text-white transition cursor-pointer shadow-sm shadow-emerald-600/20">
<i data-lucide="plus-circle" class="w-3.5 h-3.5"></i>
<span>Plaats op Bord</span>
</button>
</div>
</form>
</div>
</div>
<!-- Toast Notificatie Element -->
<div id="jiraToast" class="fixed bottom-6 right-6 z-50 transform translate-y-24 opacity-0 pointer-events-none transition-all duration-300 flex items-center space-x-3 p-4 rounded-2xl bg-slate-900/95 dark:bg-white text-white dark:text-slate-900 shadow-2xl border border-slate-800 dark:border-slate-200 max-w-md">
<div id="toastIconWrapper" class="w-8 h-8 rounded-xl bg-emerald-500/20 text-emerald-400 dark:text-emerald-600 flex items-center justify-center flex-shrink-0">
<i data-lucide="check" class="w-4 h-4"></i>
</div>
<div class="flex-1 text-xs">
<p id="toastMessage" class="font-bold">Ticket succesvol verwerkt</p>
<p id="toastSubMessage" class="text-slate-400 dark:text-slate-600 text-[11px] mt-0.5"></p>
</div>
<a id="toastActionLink" href="#" class="hidden px-2.5 py-1 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-[11px] font-bold transition flex-shrink-0">
Bekijken
</a>
</div>
<script>
// Data vanuit PHP
const jiraData = <?= $jiraDataJson ?>;
const availableBoards = <?= $boardsJson ?>;
let currentTab = 'assigned'; // 'assigned', 'team', 'in_progress', 'done'
let currentProject = '';
let currentQueue = (jiraData.user_queue_filters && jiraData.user_queue_filters.length > 0) ? 'MY_QUEUES' : (jiraData.default_queue_filter || '');
let hideClosed = true; // Standaard gesloten tickets verbergen conform gebruikerswens!
let onlyInProgress = false;
let activeUrgentOnly = false;
let expandedKeys = new Set();
const ticketStore = new Map();
function isDoneStatus(st) {
const s = (st || '').toLowerCase().trim();
return s.includes('done') || s.includes('gesloten') || s.includes('afgerond') || s.includes('gereed') || s.includes('closed') || s.includes('resolved');
}
function isInProgressStatus(st, sc) {
const s = (st || '').toLowerCase().trim();
const c = (sc || '').toLowerCase().trim();
return c === 'indeterminate' || s.includes('progress') || s.includes('bezig') || s.includes('review') || s.includes('uitvoering') || s.includes('behandeling') || s.includes('wachten');
}
// Bereken statussen en KPI statistieken
function calculateKpis() {
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const all = [...assigned, ...team];
let inProgressCount = 0;
let highPriorityCount = 0;
let doneCount = 0;
all.forEach(iss => {
const s = (iss.status || '').toLowerCase();
const sc = (iss.status_category || '').toLowerCase();
const p = (iss.priority || '').toLowerCase();
if (isInProgressStatus(s, sc)) {
inProgressCount++;
}
if (isDoneStatus(s) || sc === 'done') {
doneCount++;
}
if (p === 'highest' || p === 'high') {
highPriorityCount++;
}
});
const elStatAssigned = document.getElementById('statAssignedCount');
const elStatTeam = document.getElementById('statTeamCount');
const elStatInProg = document.getElementById('statInProgressCount');
const elStatHigh = document.getElementById('statHighPriorityCount');
if (elStatAssigned) elStatAssigned.innerText = assigned.length;
if (elStatTeam) elStatTeam.innerText = team.length;
if (elStatInProg) elStatInProg.innerText = inProgressCount;
if (elStatHigh) elStatHigh.innerText = highPriorityCount;
const elTabAssigned = document.getElementById('tabCount_assigned');
const elTabTeam = document.getElementById('tabCount_team');
const elTabInProg = document.getElementById('tabCount_in_progress');
const elTabDone = document.getElementById('tabCount_done');
if (elTabAssigned) elTabAssigned.innerText = assigned.length;
if (elTabTeam) elTabTeam.innerText = team.length;
if (elTabInProg) elTabInProg.innerText = inProgressCount;
if (elTabDone) elTabDone.innerText = doneCount;
updateProjectCounts();
updateQueueCounts();
}
// 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 && currentQueue !== 'MY_QUEUES') activeCount++;
if (document.getElementById('filterType')?.value) activeCount++;
if (document.getElementById('filterPriority')?.value) activeCount++;
if (document.getElementById('filterStatus')?.value) activeCount++;
if (onlyInProgress) activeCount++;
if (activeCount > 0) {
badge.innerText = activeCount;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
// Wissel tussen tabs
function switchTab(tab) {
currentTab = tab;
activeUrgentOnly = false;
// Update tab knoppen styling
const tabs = ['assigned', 'team', 'in_progress', 'done'];
tabs.forEach(t => {
const btn = document.getElementById(`tabBtn_${t}`);
if (!btn) return;
if (t === tab) {
btn.className = "flex items-center space-x-1.5 px-3 py-1.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 border-indigo-200 dark:border-indigo-800/60 flex-shrink-0";
} else {
btn.className = "flex items-center space-x-1.5 px-3 py-1.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 border border-transparent flex-shrink-0";
}
});
applyFilters();
}
// Toggle: Verberg Gesloten
function toggleHideClosed() {
hideClosed = !hideClosed;
updateHideClosedButton();
applyFilters();
}
function updateHideClosedButton() {
const btn = document.getElementById('btnHideClosed');
if (!btn) return;
if (hideClosed) {
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80 shadow-2xs";
btn.innerHTML = `<i data-lucide="eye-off" class="w-3.5 h-3.5 text-emerald-600"></i><span>Verberg Gesloten</span><span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>`;
} else {
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700";
btn.innerHTML = `<i data-lucide="eye" class="w-3.5 h-3.5 text-slate-400"></i><span>Toon Ook Gesloten</span>`;
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
// Toggle: Snelfilter Alleen In Behandeling
function toggleOnlyInProgress() {
onlyInProgress = !onlyInProgress;
updateOnlyInProgressButton();
applyFilters();
}
function updateOnlyInProgressButton() {
const btn = document.getElementById('btnOnlyInProgress');
if (!btn) return;
if (onlyInProgress) {
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-blue-600 text-white shadow-xs";
btn.innerHTML = `<i data-lucide="play" class="w-3 h-3 text-white fill-white"></i><span>In behandeling (Actief)</span>`;
} else {
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700";
btn.innerHTML = `<i data-lucide="play" class="w-3 h-3 text-blue-500"></i><span>In behandeling</span>`;
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
// Status dropdown verandering
function onStatusSelectChange(val) {
if (val && isDoneStatus(val)) {
hideClosed = false;
updateHideClosedButton();
}
applyFilters();
}
// Project selectie via pill tabs
function selectProject(proj) {
currentProject = (proj || '').trim();
updateProjectPills();
applyFilters();
}
function updateProjectPills() {
const pills = document.querySelectorAll('.project-pill');
const curNorm = (currentProject || '').toUpperCase().trim();
pills.forEach(p => {
const pKey = (p.getAttribute('data-project') || '').toUpperCase().trim();
const isSelected = (pKey === curNorm);
if (pKey === '') {
p.className = isSelected
? "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-900 dark:bg-white text-white dark:text-slate-900 shadow-xs flex-shrink-0"
: "project-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 border border-slate-200 dark:border-slate-700 flex-shrink-0";
} else if (pKey === 'SD') {
p.className = isSelected
? "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-sky-600 text-white shadow-xs flex-shrink-0"
: "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-sky-50 dark:bg-sky-950/40 text-sky-800 dark:text-sky-300 hover:bg-sky-100 dark:hover:bg-sky-900/60 border border-sky-300 dark:border-sky-800 flex-shrink-0";
} else if (pKey === 'IN') {
p.className = isSelected
? "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-amber-600 text-white shadow-xs flex-shrink-0"
: "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 border border-amber-300 dark:border-amber-800 flex-shrink-0";
} else if (pKey === 'AAN') {
p.className = isSelected
? "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-600 text-white shadow-xs flex-shrink-0"
: "project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/60 border border-emerald-300 dark:border-emerald-800 flex-shrink-0";
} else if (pKey === 'GEN') {
p.className = isSelected
? "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"
: "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-50 dark:bg-indigo-950/40 text-indigo-800 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/60 border border-indigo-300 dark:border-indigo-800 flex-shrink-0";
} else {
p.className = isSelected
? "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"
: "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 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0";
}
});
}
// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort (SD, IN, AAN, GEN, DX of MY_QUEUES)
function isIssueInQueue(iss, qCode) {
if (!qCode) return true;
const code = qCode.toUpperCase().trim();
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
const sum = (iss.summary || '').toLowerCase();
const pKey = (iss.project_key || '').toUpperCase().trim();
const pName = (iss.project || '').toLowerCase().trim();
// Multi-queue matching voor de ingelogde gebruiker
if (code === 'MY_QUEUES') {
const myQueues = jiraData.user_queue_filters || [];
if (myQueues.length === 0) return true;
return myQueues.some(q => isIssueInQueue(iss, q));
}
const dxTerms = ['koppeling', 'sso', 'endpoint', 'rest', 'datadump', 'uwv', 'migratie', 'api', 'webhook', 'integrat'];
const matchesDx = dxTerms.some(t => ol.includes(t) || sum.includes(t));
if (code === 'DX') {
return matchesDx;
}
if (code === 'SD') {
return (pKey === 'SD' || pName.includes('servicedesk') || !matchesDx);
}
if (code === 'IN') {
return (pKey === 'IN' || pName.includes('interface') || matchesDx || ol.includes('interface') || sum.includes('interface'));
}
if (code === 'AAN') {
return (pKey === 'AAN' || pName.includes('aanvraag') || ol.includes('aanvraag') || sum.includes('aanvraag'));
}
if (code === 'GEN') {
return (pKey === 'GEN' || pName.includes('general') || pName.includes('algemeen') || ol.includes('general') || ol.includes('algemeen'));
}
// Aangepaste wachtrijfilter uit de database
const fObj = (jiraData.queue_filters || []).find(f => f.filter_code && f.filter_code.toUpperCase() === code);
if (fObj) {
if (fObj.project_key && pKey && fObj.project_key.toUpperCase() !== pKey) {
return false;
}
const clause = (fObj.jql_clause || '').toLowerCase();
if (ol && (clause.includes(ol) || ol.includes(code.toLowerCase()))) {
return true;
}
if (clause.includes('koppeling') && matchesDx) return true;
if (clause.includes(pKey.toLowerCase())) return true;
}
return true;
}
// Wachtrijfilter selectie
function selectQueueFilter(code) {
currentQueue = code;
updateQueuePills();
applyFilters();
}
function updateQueuePills() {
const pills = document.querySelectorAll('.queue-pill');
pills.forEach(p => {
const qCode = p.getAttribute('data-queue') || '';
const isSelected = (qCode === currentQueue);
if (isSelected) {
p.className = "queue-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-sm shadow-indigo-600/20 flex-shrink-0";
} else {
p.className = "queue-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/80 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0";
}
});
}
// Update de tellers op de project buttons
function updateProjectCounts() {
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const all = [...assigned, ...team];
let currentList = all;
if (currentTab === 'assigned') currentList = assigned;
else if (currentTab === 'team') currentList = team;
else if (currentTab === 'in_progress') {
currentList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category));
} else if (currentTab === 'done') {
currentList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done');
}
if (hideClosed && currentTab !== 'done') {
currentList = currentList.filter(iss => !isDoneStatus(iss.status) && (iss.status_category || '').toLowerCase() !== 'done');
}
const elAll = document.getElementById('projCount_all');
if (elAll) elAll.innerText = currentList.length;
['SD', 'IN', 'AAN', 'GEN'].forEach(key => {
const el = document.getElementById(`projCount_${key}`);
if (el) {
const cnt = currentList.filter(iss => {
const pk = (iss.project_key || '').toUpperCase().trim();
const pn = (iss.project || '').toLowerCase().trim();
if (pk === key) return true;
if (key === 'SD' && pn.includes('servicedesk')) return true;
if (key === 'IN' && pn.includes('interface')) return true;
if (key === 'AAN' && pn.includes('aanvraag')) return true;
if (key === 'GEN' && (pn.includes('general') || pn.includes('algemeen'))) return true;
return false;
}).length;
el.innerText = cnt;
}
});
}
// Update de tellers op de wachtrijfilter knoppen
function updateQueueCounts() {
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const all = [...assigned, ...team];
let currentList = all;
if (currentTab === 'assigned') currentList = assigned;
else if (currentTab === 'team') currentList = team;
else if (currentTab === 'in_progress') {
currentList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category));
} else if (currentTab === 'done') {
currentList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done');
}
if (hideClosed && currentTab !== 'done') {
currentList = currentList.filter(iss => !isDoneStatus(iss.status) && (iss.status_category || '').toLowerCase() !== 'done');
}
if (currentProject) {
const pNorm = currentProject.trim().toUpperCase();
currentList = currentList.filter(iss => {
const pk = (iss.project_key || '').trim().toUpperCase();
const pn = (iss.project || '').trim().toLowerCase();
return pk === pNorm || pn.includes(pNorm.toLowerCase());
});
}
const elAll = document.getElementById('queueCount_all');
if (elAll) elAll.innerText = currentList.length;
const elMy = document.getElementById('queueCount_MY_QUEUES');
if (elMy) {
elMy.innerText = currentList.filter(iss => isIssueInQueue(iss, 'MY_QUEUES')).length;
}
['SD', 'IN', 'AAN', 'GEN', 'DX'].forEach(code => {
const el = document.getElementById(`queueCount_${code}`);
if (el) {
el.innerText = currentList.filter(iss => isIssueInQueue(iss, code)).length;
}
});
(jiraData.queue_filters || []).forEach(f => {
if (!['SD', 'IN', 'AAN', 'GEN', 'DX'].includes(f.filter_code)) {
const el = document.getElementById(`queueCount_${f.filter_code}`);
if (el) {
el.innerText = currentList.filter(iss => isIssueInQueue(iss, f.filter_code)).length;
}
}
});
}
// Snelkoppeling KPI-kaart Urgent
function filterByUrgent() {
const pSelect = document.getElementById('filterPriority');
if (pSelect) {
pSelect.value = 'High';
}
applyFilters();
}
// Zoekopdracht wissen
function clearSearch() {
const input = document.getElementById('jiraSearchInput');
if (input) {
input.value = '';
}
document.getElementById('clearSearchBtn').classList.add('hidden');
applyFilters();
}
// Alle filters herstellen
function resetAllFilters() {
currentProject = '';
currentQueue = '';
hideClosed = true;
onlyInProgress = false;
const searchInput = document.getElementById('jiraSearchInput');
const typeSelect = document.getElementById('filterType');
const prioritySelect = document.getElementById('filterPriority');
const statusSelect = document.getElementById('filterStatus');
if (searchInput) searchInput.value = '';
if (typeSelect) typeSelect.value = '';
if (prioritySelect) prioritySelect.value = '';
if (statusSelect) statusSelect.value = '';
document.getElementById('clearSearchBtn')?.classList.add('hidden');
updateHideClosedButton();
updateOnlyInProgressButton();
updateProjectPills();
updateQueuePills();
applyFilters();
}
// Filter en render logic
function applyFilters() {
const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase();
const projectVal = (currentProject || '').toUpperCase().trim();
const typeVal = (document.getElementById('filterType')?.value || '').trim().toLowerCase();
const priorityVal = (document.getElementById('filterPriority')?.value || '').trim();
const statusVal = (document.getElementById('filterStatus')?.value || '').trim().toLowerCase();
const clearBtn = document.getElementById('clearSearchBtn');
if (clearBtn) {
if (searchVal.length > 0) clearBtn.classList.remove('hidden');
else clearBtn.classList.add('hidden');
}
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const all = [...assigned, ...team];
let sourceList = [];
if (currentTab === 'assigned') {
sourceList = assigned;
} else if (currentTab === 'team') {
sourceList = team;
} else if (currentTab === 'in_progress') {
sourceList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category));
} else if (currentTab === 'done') {
sourceList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done');
}
// Pas filters toe
const filtered = sourceList.filter(iss => {
const issPKey = (iss.project_key || '').toUpperCase().trim();
const issPName = (iss.project || '').toLowerCase().trim();
const issStatus = (iss.status || '').toLowerCase().trim();
const issCat = (iss.status_category || '').toLowerCase().trim();
// 1. Verberg Gesloten filter (als tab niet 'done' is en er niet expliciet een gesloten status gekozen is)
if (currentTab !== 'done' && hideClosed && !isDoneStatus(statusVal)) {
if (issCat === 'done' || isDoneStatus(issStatus)) {
return false;
}
}
// 2. Snelfilter "Alleen in behandeling"
if (onlyInProgress) {
if (!isInProgressStatus(issStatus, issCat)) {
return false;
}
}
// 3. Project Filter (SD, IN, AAN, GEN of ander)
if (projectVal) {
let matchesProj = (issPKey === projectVal);
if (!matchesProj) {
if (projectVal === 'SD' && issPName.includes('servicedesk')) matchesProj = true;
else if (projectVal === 'IN' && issPName.includes('interface')) matchesProj = true;
else if (projectVal === 'AAN' && issPName.includes('aanvraag')) matchesProj = true;
else if (projectVal === 'GEN' && (issPName.includes('general') || issPName.includes('algemeen'))) matchesProj = true;
else if (issPName === projectVal.toLowerCase()) matchesProj = true;
}
if (!matchesProj) return false;
}
// 4. Wachtrijfilter
if (currentQueue) {
if (!isIssueInQueue(iss, currentQueue)) {
return false;
}
}
// 5. Specifieke status selectie
if (statusVal) {
if (issStatus !== statusVal) {
return false;
}
}
// 6. Type filter
if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) {
return false;
}
// 7. Prioriteit filter
if (priorityVal) {
const pVal = priorityVal.toLowerCase();
const ip = (iss.priority || '').toLowerCase();
if (pVal === 'high' || pVal === 'highest' || pVal === 'p1') {
if (!['highest', 'high', 'hoogste', 'hoog', 'urgent', 'p1'].includes(ip)) return false;
} else if (pVal === 'medium' || pVal === 'p2') {
if (!['medium', 'gemiddeld', 'normaal', 'p2'].includes(ip)) return false;
} else if (pVal === 'low' || pVal === 'lowest' || pVal === 'p3') {
if (!['low', 'lowest', 'laag', 'laagste', 'p3'].includes(ip)) return false;
} else if (ip !== pVal) {
return false;
}
}
// 8. Zoekterm
if (searchVal) {
const keyMatch = (iss.key || '').toLowerCase().includes(searchVal);
const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal);
const prjMatch = issPName.includes(searchVal);
const prjKeyMatch = issPKey.includes(searchVal.toUpperCase());
const descMatch = (iss.description || '').toLowerCase().includes(searchVal);
const repMatch = (iss.reporter_name || '').toLowerCase().includes(searchVal);
const assMatch = (iss.assignee_name || '').toLowerCase().includes(searchVal);
const olMatch = (iss.onderwerp_logging || '').toLowerCase().includes(searchVal);
if (!keyMatch && !sumMatch && !prjMatch && !prjKeyMatch && !descMatch && !repMatch && !assMatch && !olMatch) return false;
}
return true;
});
updateActiveFilterBadge();
updateProjectCounts();
updateQueueCounts();
renderTickets(filtered);
}
// Uitklappen en inklappen van ticket details
function toggleTicketDetails(key, event) {
if (event) {
event.stopPropagation();
}
const details = document.getElementById(`details_${key}`);
const chevron = document.getElementById(`chevron_${key}`);
if (!details) return;
if (details.classList.contains('hidden')) {
details.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
expandedKeys.add(key);
} else {
details.classList.add('hidden');
if (chevron) chevron.classList.remove('rotate-180');
expandedKeys.delete(key);
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function copyTicketKey(key) {
navigator.clipboard.writeText(key).then(() => {
showToast({
message: `Ticket ${key} gekopieerd!`,
subMessage: 'De ticketsleutel is direct naar je klembord gekopieerd.'
});
});
}
// Renders the list of tickets
function renderTickets(tickets) {
const container = document.getElementById('ticketsContainer');
const emptyState = document.getElementById('emptyStateContainer');
const countSpan = document.getElementById('renderedCount');
if (countSpan) {
countSpan.innerText = tickets.length;
}
if (!tickets || tickets.length === 0) {
container.innerHTML = '';
emptyState.classList.remove('hidden');
return;
}
emptyState.classList.add('hidden');
let html = '';
tickets.forEach(ticket => {
ticketStore.set(ticket.key, ticket);
// Issue type styling & icon
let typeBadgeClass = 'bg-sky-50 text-sky-700 dark:bg-sky-950/40 dark:text-sky-400 border-sky-200 dark:border-sky-800/60';
let typeIcon = 'check-square';
const tLower = (ticket.issue_type || '').toLowerCase();
if (tLower.includes('bug')) {
typeBadgeClass = 'bg-red-50 text-red-700 dark:bg-red-950/40 dark:text-red-400 border-red-200 dark:border-red-800/60';
typeIcon = 'alert-circle';
} else if (tLower.includes('story')) {
typeBadgeClass = 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-400 border-emerald-200 dark:border-emerald-800/60';
typeIcon = 'bookmark';
} else if (tLower.includes('epic')) {
typeBadgeClass = 'bg-purple-50 text-purple-700 dark:bg-purple-950/40 dark:text-purple-400 border-purple-200 dark:border-purple-800/60';
typeIcon = 'zap';
} else if (tLower.includes('feature')) {
typeBadgeClass = 'bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-400 border-amber-200 dark:border-amber-800/60';
typeIcon = 'star';
}
// Priority badge styling
let prioBadgeClass = 'text-slate-600 dark:text-slate-400 bg-slate-100 dark:bg-slate-800 border-slate-200 dark:border-slate-700';
let prioIcon = 'minus';
const pLower = (ticket.priority || '').toLowerCase();
if (pLower === 'highest') {
prioBadgeClass = 'text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-950/40 border-red-200 dark:border-red-800/60';
prioIcon = 'chevrons-up';
} else if (pLower === 'high') {
prioBadgeClass = 'text-orange-700 dark:text-orange-400 bg-orange-50 dark:bg-orange-950/40 border-orange-200 dark:border-orange-800/60';
prioIcon = 'chevron-up';
} else if (pLower === 'low' || pLower === 'lowest') {
prioBadgeClass = 'text-slate-500 dark:text-slate-400 bg-slate-50 dark:bg-slate-900 border-slate-200 dark:border-slate-800';
prioIcon = 'chevron-down';
}
// Project badge styling (SD, IN, AAN, GEN)
const pKey = (ticket.project_key || '').toUpperCase().trim();
const pName = (ticket.project || '').toLowerCase().trim();
let projBadgeClass = 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 border border-slate-200/80 dark:border-slate-700/80';
let projLabel = escapeHtml(ticket.project || 'Project');
if (pKey === 'SD' || pName.includes('servicedesk')) {
projBadgeClass = 'bg-sky-50 dark:bg-sky-950/40 text-sky-700 dark:text-sky-300 border border-sky-300 dark:border-sky-800/80';
projLabel = 'SD • Servicedesk';
} else if (pKey === 'IN' || pName.includes('interface')) {
projBadgeClass = 'bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-300 border border-amber-300 dark:border-amber-800/80';
projLabel = 'IN • Interfaces';
} else if (pKey === 'AAN' || pName.includes('aanvraag')) {
projBadgeClass = 'bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80';
projLabel = 'AAN • Aanvragen';
} else if (pKey === 'GEN' || pName.includes('general') || pName.includes('algemeen')) {
projBadgeClass = 'bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border border-indigo-300 dark:border-indigo-800/80';
projLabel = 'GEN • General';
}
// Status badge styling
let statusBadgeClass = 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 border-slate-200 dark:border-slate-700';
let statusDot = '';
const sLower = (ticket.status || '').toLowerCase();
const scLower = (ticket.status_category || '').toLowerCase();
if (sLower.includes('progress') || sLower.includes('bezig') || sLower.includes('behandeling') || sLower.includes('uitvoering') || scLower === 'indeterminate') {
statusBadgeClass = 'bg-blue-50 text-blue-700 dark:bg-blue-950/40 dark:text-blue-300 border-blue-200 dark:border-blue-800/60';
statusDot = '<span class="w-1.5 h-1.5 rounded-full bg-blue-500 mr-1.5 animate-pulse"></span>';
} else if (sLower.includes('wachten') || sLower.includes('klant') || sLower.includes('waiting') || sLower.includes('pending')) {
statusBadgeClass = 'bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300 border-amber-200 dark:border-amber-800/60';
statusDot = '<span class="w-1.5 h-1.5 rounded-full bg-amber-500 mr-1.5"></span>';
} else if (sLower.includes('review') || sLower.includes('test')) {
statusBadgeClass = 'bg-purple-50 text-purple-700 dark:bg-purple-950/40 dark:text-purple-300 border-purple-200 dark:border-purple-800/60';
statusDot = '<span class="w-1.5 h-1.5 rounded-full bg-purple-500 mr-1.5"></span>';
} else if (sLower.includes('done') || sLower.includes('afgerond') || sLower.includes('gesloten') || sLower.includes('gereed') || sLower.includes('closed') || scLower === 'done') {
statusBadgeClass = 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300 border-emerald-200 dark:border-emerald-800/60';
statusDot = '<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5"></span>';
} else if (sLower.includes('to do') || sLower.includes('open') || sLower.includes('nieuw') || scLower === 'new') {
statusBadgeClass = 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 border-slate-200 dark:border-slate-700';
statusDot = '<span class="w-1.5 h-1.5 rounded-full bg-slate-400 mr-1.5"></span>';
}
// Due date badge
let dueDateHtml = '';
if (ticket.due_date) {
const today = new Date().toISOString().split('T')[0];
const isOverdue = ticket.due_date < today;
const isToday = ticket.due_date === today;
let dueClass = 'text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-slate-800/60';
if (isOverdue) dueClass = 'text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-900/50';
else if (isToday) dueClass = 'text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-900/50';
dueDateHtml = `
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-semibold ${dueClass}">
<i data-lucide="calendar" class="w-3 h-3"></i>
<span>${ticket.due_date}</span>
</span>
`;
}
// Assignee badge
let assigneeHtml = '';
if (ticket.assignee_name) {
const initials = ticket.assignee_name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
assigneeHtml = `
<div class="flex items-center space-x-1.5 text-xs text-slate-600 dark:text-slate-400">
<div class="w-5 h-5 rounded-full bg-indigo-100 dark:bg-indigo-900/60 text-indigo-700 dark:text-indigo-300 flex items-center justify-center font-bold text-[10px]">
${initials}
</div>
<span class="truncate max-w-[120px] sm:max-w-[160px]">${escapeHtml(ticket.assignee_name)}</span>
</div>
`;
}
// Onderwerp logging badge
let onderwerpBadge = '<span class="text-slate-400 italic font-normal">Niet ingevuld</span>';
if (ticket.onderwerp_logging) {
const olLower = ticket.onderwerp_logging.toLowerCase();
const dxList = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
const isDx = dxList.includes(olLower);
if (isDx) {
onderwerpBadge = `
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-bold bg-amber-50 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-300 dark:border-amber-800/80">
<i data-lucide="zap" class="w-3 h-3 text-amber-500"></i>
<span>DX: ${escapeHtml(ticket.onderwerp_logging)}</span>
</span>
`;
} else {
onderwerpBadge = `
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-bold bg-sky-50 text-sky-800 dark:bg-sky-950/60 dark:text-sky-300 border border-sky-300 dark:border-sky-800/80">
<i data-lucide="wrench" class="w-3 h-3 text-sky-500"></i>
<span>SD: ${escapeHtml(ticket.onderwerp_logging)}</span>
</span>
`;
}
}
const isExpanded = expandedKeys.has(ticket.key);
const detailsClass = isExpanded ? '' : 'hidden';
const chevronRotateClass = isExpanded ? 'rotate-180' : '';
html += `
<div id="ticketCard_${escapeHtml(ticket.key)}" class="fociz-card p-4 sm:p-5 transition hover:border-indigo-500/40 dark:hover:border-indigo-500/40">
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-3">
<!-- Linkerkant: Metadata & Titel -->
<div class="flex-1 min-w-0 space-y-2">
<!-- Header regel met Type, Key, Project, Prioriteit & Status -->
<div class="flex flex-wrap items-center gap-2">
<!-- Type Badge -->
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-wider border ${typeBadgeClass}">
<i data-lucide="${typeIcon}" class="w-3 h-3"></i>
<span>${escapeHtml(ticket.issue_type || 'Taak')}</span>
</span>
<!-- Key Link -->
<a href="${escapeHtml(ticket.url)}" target="_blank" rel="noopener noreferrer" class="font-mono text-xs font-black text-indigo-600 dark:text-indigo-400 hover:underline flex items-center space-x-1">
<span>${escapeHtml(ticket.key)}</span>
<i data-lucide="external-link" class="w-3 h-3 text-slate-400"></i>
</a>
<!-- Project Badge -->
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-bold ${projBadgeClass}">
<i data-lucide="layers" class="w-3 h-3 opacity-75"></i>
<span>${projLabel}</span>
</span>
<!-- Status Badge -->
<span class="inline-flex items-center px-2 py-0.5 rounded-md text-[11px] font-bold border ${statusBadgeClass}">
${statusDot}
<span>${escapeHtml(ticket.status || 'To Do')}</span>
</span>
<!-- Priority Badge -->
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-bold border ${prioBadgeClass}">
<i data-lucide="${prioIcon}" class="w-3 h-3"></i>
<span>${escapeHtml(ticket.priority || 'Medium')}</span>
</span>
<!-- Due date -->
${dueDateHtml}
</div>
<!-- Ticket Titel / Summary (Klikbaar voor accordion) -->
<h3 class="text-sm sm:text-base font-bold text-slate-900 dark:text-white leading-snug cursor-pointer group" onclick="toggleTicketDetails('${escapeHtml(ticket.key)}', event)">
<span class="group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition">
${escapeHtml(ticket.summary || '(Geen titel)')}
</span>
</h3>
<!-- Sub-info: Assignee, Update timestamp & Inklap knop -->
<div class="flex flex-wrap items-center gap-4 pt-1">
${assigneeHtml}
${ticket.updated ? `<span class="text-[11px] text-slate-400">Gewijzigd: ${escapeHtml(ticket.updated)}</span>` : ''}
${ticket.onderwerp_logging ? onderwerpBadge : ''}
</div>
</div>
<!-- Rechterkant: Snelle Actieknoppen + Uitklapknop -->
<div class="flex items-center space-x-2 pt-2 lg:pt-0 flex-shrink-0 border-t lg:border-t-0 border-slate-100 dark:border-slate-800">
<button type="button" onclick="toggleTicketDetails('${escapeHtml(ticket.key)}', event)" class="flex items-center space-x-1 px-2.5 py-2 rounded-xl text-xs font-bold text-slate-600 dark:text-slate-300 hover:text-indigo-600 dark:hover:text-indigo-400 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700/80 transition cursor-pointer shadow-2xs">
<span>Details</span>
<i data-lucide="chevron-down" id="chevron_${escapeHtml(ticket.key)}" class="w-3.5 h-3.5 transition-transform duration-200 ${chevronRotateClass}"></i>
</button>
<button onclick="openPlanModal('${escapeHtml(ticket.key)}')" class="flex items-center space-x-1.5 px-3 py-2 rounded-xl text-xs font-bold bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-950/50 dark:hover:bg-indigo-900/60 dark:text-indigo-300 border border-indigo-200/80 dark:border-indigo-800/80 transition cursor-pointer shadow-2xs">
<i data-lucide="calendar" class="w-3.5 h-3.5"></i>
<span>Plan in Dagplanner</span>
</button>
<button onclick="openBoardModal('${escapeHtml(ticket.key)}')" class="flex items-center space-x-1.5 px-3 py-2 rounded-xl text-xs font-bold bg-emerald-50 hover:bg-emerald-100 text-emerald-700 dark:bg-emerald-950/50 dark:hover:bg-indigo-900/60 dark:text-emerald-300 border border-emerald-200/80 dark:border-emerald-800/80 transition cursor-pointer shadow-2xs">
<i data-lucide="kanban" class="w-3.5 h-3.5"></i>
<span>Naar Bord</span>
</button>
<a href="${escapeHtml(ticket.url)}" target="_blank" rel="noopener noreferrer" title="Bekijk in Jira" class="p-2 rounded-xl text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="external-link" class="w-4 h-4"></i>
</a>
</div>
</div>
<!-- UITKLAPBAAR PANEEL (Accordion Details) -->
<div id="details_${escapeHtml(ticket.key)}" class="${detailsClass} mt-4 pt-4 border-t border-slate-100 dark:border-slate-800 space-y-4 text-xs">
<!-- Beschrijving -->
<div class="space-y-1.5">
<div class="flex items-center space-x-1.5 font-bold text-slate-700 dark:text-slate-300">
<i data-lucide="align-left" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Volledige Ticket Omschrijving</span>
</div>
<div class="p-4 rounded-xl bg-slate-50 dark:bg-slate-900/90 border border-slate-200/80 dark:border-slate-800 text-slate-800 dark:text-slate-200 leading-relaxed whitespace-pre-wrap select-text font-normal">
${escapeHtml(ticket.description || 'Geen uitgebreide omschrijving beschikbaar in Jira.')}
</div>
</div>
<!-- Metadata Grid (Reporter, Aanmaakdatum, Wijziging, Onderwerp) -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 p-3.5 rounded-xl bg-slate-50/60 dark:bg-slate-950/40 border border-slate-200/60 dark:border-slate-800/60 text-[11px]">
<div>
<span class="text-slate-400 block mb-1 font-medium">Aangemeld door (Reporter):</span>
<div class="flex items-center space-x-1.5 font-semibold text-slate-800 dark:text-slate-200">
<div class="w-4 h-4 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center font-bold text-[9px] flex-shrink-0">
${escapeHtml((ticket.reporter_name || 'J').charAt(0).toUpperCase())}
</div>
<span class="truncate">${escapeHtml(ticket.reporter_name || 'Onbekend')}</span>
</div>
</div>
<div>
<span class="text-slate-400 block mb-1 font-medium">Aangemaakt op:</span>
<span class="font-medium text-slate-700 dark:text-slate-300">
${ticket.created ? escapeHtml(ticket.created) : 'Niet opgegeven'}
</span>
</div>
<div>
<span class="text-slate-400 block mb-1 font-medium">Laatst gewijzigd:</span>
<span class="font-medium text-slate-700 dark:text-slate-300">
${ticket.updated ? escapeHtml(ticket.updated) : 'Recent'}
</span>
</div>
<div>
<span class="text-slate-400 block mb-1 font-medium">Onderwerp logging:</span>
${onderwerpBadge}
</div>
</div>
<!-- Snelle acties onderaan detailvak -->
<div class="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2">
<button type="button" onclick='copyTicketKey("${escapeHtml(ticket.key)}")' class="flex items-center space-x-1 px-2.5 py-1.5 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
<i data-lucide="copy" class="w-3 h-3"></i>
<span>Sleutel Kopiëren</span>
</button>
<a href="${escapeHtml(ticket.url)}" target="_blank" rel="noopener noreferrer" class="flex items-center space-x-1 px-2.5 py-1.5 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-indigo-600 dark:text-indigo-400 text-xs font-semibold transition cursor-pointer">
<i data-lucide="external-link" class="w-3 h-3"></i>
<span>Open in Jira Cloud</span>
</a>
</div>
<div class="flex items-center space-x-2">
<button type="button" onclick='openPlanModal("${escapeHtml(ticket.key)}")' class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-700 text-white transition cursor-pointer shadow-xs">
<i data-lucide="calendar" class="w-3 h-3"></i>
<span>Inplannen in Dagplanner</span>
</button>
<button type="button" onclick='openBoardModal("${escapeHtml(ticket.key)}")' class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-emerald-600 hover:bg-emerald-700 text-white transition cursor-pointer shadow-xs">
<i data-lucide="kanban" class="w-3 h-3"></i>
<span>Naar Kanban Bord</span>
</button>
</div>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
// Herinitialiseer Lucide iconen
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
// Modal 1: Plan in Dagplanner
let _activeJiraTicket = null;
let _outsideWorkhoursPendingJira = null;
function togglePlanTicketDesc() {
const container = document.getElementById('planTicketDescContainer');
const chevron = document.getElementById('planTicketDescChevron');
const text = document.getElementById('planTicketDescToggleText');
if (!container) return;
if (container.classList.contains('hidden')) {
container.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
if (text) text.textContent = 'Verberg ticket inhoud';
} else {
container.classList.add('hidden');
if (chevron) chevron.classList.remove('rotate-180');
if (text) text.textContent = 'Toon ticket inhoud';
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function clearPlanTaskTitle() {
const input = document.getElementById('planTaskTitleInput');
if (input) {
input.value = '';
input.focus();
}
}
function updatePlanEndTimeBadge() {
const startInput = document.getElementById('planStartTimeInput');
const durationInput = document.getElementById('planDurationInput');
const badge = document.getElementById('planEndTimeBadge');
if (!startInput || !durationInput || !badge) return;
const startVal = (startInput.value || '').trim();
const duration = parseInt(durationInput.value || '30', 10);
if (!startVal || !startVal.includes(':') || isNaN(duration)) {
badge.innerText = '';
return;
}
const [hStr, mStr] = startVal.split(':');
const totalMinutes = (parseInt(hStr, 10) * 60 + parseInt(mStr, 10)) + duration;
const endH = Math.floor((totalMinutes / 60) % 24);
const endM = totalMinutes % 60;
const endStr = `${String(endH).padStart(2, '0')}:${String(endM).padStart(2, '0')}`;
badge.innerText = `(tot ${endStr})`;
}
async function planInNextOpenBlockJira(startFromNextWorkday = false) {
const durationInput = document.getElementById('planDurationInput');
const duration = parseInt(durationInput?.value || '45', 10);
const btn = document.getElementById('btnJiraPlanOpenBlock');
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.disabled = true;
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Zoeken...</span>`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
try {
let url = `/api/planner.php?action=find_next_free_slot&duration=${duration}`;
if (startFromNextWorkday) {
url += '&start_from_next_workday=1';
}
const resp = await fetch(url);
const data = await resp.json();
if (data.success && data.slot) {
const slot = data.slot;
const dateInput = document.getElementById('planDateInput');
const startInput = document.getElementById('planStartTimeInput');
if (dateInput) dateInput.value = slot.date;
if (startInput) startInput.value = slot.start;
updatePlanEndTimeBadge();
const notice = document.getElementById('jiraPlanOpenBlockNotice');
const noticeText = document.getElementById('jiraPlanOpenBlockNoticeText');
if (notice && noticeText) {
const formattedDate = new Date(slot.date + 'T00:00:00').toLocaleDateString('nl-NL', { weekday: 'short', day: 'numeric', month: 'short' });
noticeText.innerText = `Vrij blok gevonden: ${formattedDate} om ${slot.start} – ${slot.end} (${duration} min)`;
notice.classList.remove('hidden');
}
} else {
alert(data.error || 'Geen beschikbaar tijdblok gevonden binnen de komende 14 dagen.');
}
} catch (err) {
alert('Netwerkfout bij zoeken van open blok: ' + err.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = origHtml;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
}
function closeOutsideWorkhoursModalJira(choice) {
const modal = document.getElementById('outsideWorkhoursModalJira');
if (modal) modal.classList.add('hidden');
if (choice === 'JA') {
if (_outsideWorkhoursPendingJira) {
_outsideWorkhoursPendingJira.bypassWorkhours = true;
submitPlanTask(null, _outsideWorkhoursPendingJira);
}
} else if (choice === 'NEE') {
_outsideWorkhoursPendingJira = null;
planInNextOpenBlockJira(true);
} else {
_outsideWorkhoursPendingJira = null;
}
}
function openPlanModal(item) {
const ticket = (typeof item === 'string')
? (ticketStore.get(item) || (jiraData.assigned_issues || []).concat(jiraData.team_issues || []).find(t => t.key === item))
: item;
if (!ticket) return;
_activeJiraTicket = ticket;
document.getElementById('planIssueKey').value = ticket.key;
document.getElementById('planIssueSummary').value = ticket.summary;
document.getElementById('planIssueUrl').value = ticket.url;
document.getElementById('planTicketKeyBadge').innerText = ticket.key;
document.getElementById('planTicketProject').innerText = '• ' + (ticket.project || 'Project');
document.getElementById('planTicketSummaryText').innerText = ticket.summary;
// Vul aanpasbare taaktitel in
const titleInput = document.getElementById('planTaskTitleInput');
if (titleInput) {
titleInput.value = `Oppakken Jira item ${ticket.key} - ${ticket.summary}`;
}
// Vul ticket omschrijving preview
const descContainer = document.getElementById('planTicketDescContainer');
if (descContainer) {
descContainer.textContent = ticket.description || 'Geen gedetailleerde omschrijving beschikbaar in Jira.';
descContainer.classList.add('hidden');
}
const chevron = document.getElementById('planTicketDescChevron');
if (chevron) chevron.classList.remove('rotate-180');
const descText = document.getElementById('planTicketDescToggleText');
if (descText) descText.textContent = 'Toon ticket inhoud';
// Verberg open blok notice
const notice = document.getElementById('jiraPlanOpenBlockNotice');
if (notice) notice.classList.add('hidden');
// Story Points → Duur conversie (1 SP = 30 minuten, configurabel)
const spBadge = document.getElementById('planSpBadge');
const spHint = document.getElementById('planSpHint');
const spHintText = document.getElementById('planSpHintText');
const sp = parseFloat(ticket.story_points);
if (!isNaN(sp) && sp > 0) {
const spMinutes = Math.round(sp * 30); // 1 SP = 30 minuten
const closestDuration = [15, 30, 45, 60, 90, 120].reduce((prev, curr) =>
Math.abs(curr - spMinutes) < Math.abs(prev - spMinutes) ? curr : prev
);
const durationSelect = document.getElementById('planDurationInput');
if (durationSelect) {
const opt = durationSelect.querySelector(`option[value="${closestDuration}"]`);
if (opt) opt.selected = true;
}
spBadge.textContent = `${sp} SP`;
spBadge.classList.remove('hidden');
spHintText.textContent = `${sp} story ${sp === 1 ? 'point' : 'points'} ≈ ${spMinutes} min → automatisch ${closestDuration} min ingesteld`;
spHint.classList.remove('hidden');
spHint.style.display = 'flex';
} else {
spBadge.classList.add('hidden');
spHint.classList.add('hidden');
}
// Suggesteer prioriteit op basis van Jira prio
let focizPrio = 'P2';
const jp = (ticket.priority || '').toLowerCase();
if (jp === 'highest' || jp === 'high') focizPrio = 'P1';
else if (jp === 'low' || jp === 'lowest') focizPrio = 'P3';
const prioRadios = document.getElementsByName('planPriority');
for (let r of prioRadios) {
if (r.value === focizPrio) r.checked = true;
}
updatePlanEndTimeBadge();
const modal = document.getElementById('planTaskModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closePlanModal() {
const modal = document.getElementById('planTaskModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
async function submitPlanTask(e, options = {}) {
if (e) e.preventDefault();
const btn = document.getElementById('planSubmitBtn');
const oldBtnText = btn ? btn.innerHTML : '';
const key = document.getElementById('planIssueKey').value;
const summary = document.getElementById('planIssueSummary').value;
const customTitle = (document.getElementById('planTaskTitleInput')?.value || '').trim();
const url = document.getElementById('planIssueUrl').value;
const planDate = document.getElementById('planDateInput').value;
const timeBlockStart = (document.getElementById('planStartTimeInput')?.value || '').trim();
const durationMinutes = parseInt(document.getElementById('planDurationInput').value, 10);
let priority = 'P2';
const prioRadios = document.getElementsByName('planPriority');
for (let r of prioRadios) {
if (r.checked) priority = r.value;
}
// Controleer of de geplande tijd buiten de gezette werktijden valt (standaard 08:30 - 17:00)
if (!options.bypassWorkhours && timeBlockStart && timeBlockStart.includes(':')) {
const [shStr, smStr] = timeBlockStart.split(':');
const startMin = parseInt(shStr, 10) * 60 + parseInt(smStr, 10);
const endMin = startMin + durationMinutes;
const workStartMin = 8 * 60 + 30; // 08:30
const workEndMin = 17 * 60; // 17:00
if (startMin < workStartMin || endMin > workEndMin) {
_outsideWorkhoursPendingJira = { key, summary, title: customTitle, url, planDate, timeBlockStart, durationMinutes, priority };
const endH = Math.floor((endMin / 60) % 24);
const endM = endMin % 60;
const endStr = `${String(endH).padStart(2, '0')}:${String(endM).padStart(2, '0')}`;
const slotEl = document.getElementById('outsideWorkhoursSelectedSlotJira');
if (slotEl) slotEl.innerText = `${timeBlockStart} – ${endStr} (${durationMinutes} min)`;
const outsideModal = document.getElementById('outsideWorkhoursModalJira');
if (outsideModal) {
outsideModal.classList.remove('hidden');
outsideModal.classList.add('flex');
setTimeout(() => {
const btnNo = document.getElementById('btnOutsideWorkhoursNoJira');
if (btnNo) btnNo.focus();
}, 50);
if (typeof lucide !== 'undefined') lucide.createIcons();
}
return;
}
}
if (btn) {
btn.disabled = true;
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Inplannen...</span>`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
try {
const response = await fetch('/api/integrations.php?action=plan_jira_task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key,
summary,
title: customTitle || `[${key}] ${summary}`,
url,
plan_date: planDate,
time_block_start: timeBlockStart,
duration_minutes: durationMinutes,
priority
})
});
const data = await response.json();
if (data.success) {
closePlanModal();
showToast({
message: `Ticket ${key} ingepland!`,
subMessage: `Ingepland voor ${planDate} om ${timeBlockStart} (${durationMinutes}m).`,
linkText: 'Naar Dagplanner',
linkUrl: `/planner.php?date=${planDate}`
});
} else {
alert(data.error || 'Er is een fout opgetreden bij het inplannen.');
}
} catch (err) {
alert('Netwerkfout: ' + err.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = oldBtnText;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
}
// Modal 2: Plaats op Kanban Bord
function openBoardModal(item) {
const ticket = (typeof item === 'string')
? (ticketStore.get(item) || (jiraData.assigned_issues || []).concat(jiraData.team_issues || []).find(t => t.key === item))
: item;
if (!ticket) return;
document.getElementById('boardIssueKey').value = ticket.key;
document.getElementById('boardIssueSummary').value = ticket.summary;
document.getElementById('boardIssueUrl').value = ticket.url;
document.getElementById('boardIssueProject').value = ticket.project || '';
document.getElementById('boardTicketKeyBadge').innerText = ticket.key;
document.getElementById('boardTicketProject').innerText = '• ' + (ticket.project || 'Project');
document.getElementById('boardTicketSummaryText').innerText = ticket.summary;
updateLaneOptions();
const modal = document.getElementById('boardTaskModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeBoardModal() {
const modal = document.getElementById('boardTaskModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
function updateLaneOptions() {
const boardSelect = document.getElementById('boardSelectInput');
const laneSelect = document.getElementById('laneSelectInput');
if (!boardSelect || !laneSelect) return;
const boardId = parseInt(boardSelect.value, 10);
const targetBoard = availableBoards.find(b => parseInt(b.id, 10) === boardId);
laneSelect.innerHTML = '';
if (!targetBoard || !targetBoard.lanes || targetBoard.lanes.length === 0) {
laneSelect.innerHTML = '<option value="">Geen kolommen beschikbaar</option>';
return;
}
targetBoard.lanes.forEach(lane => {
const opt = document.createElement('option');
opt.value = lane.id;
opt.innerText = lane.title;
laneSelect.appendChild(opt);
});
}
async function submitBoardTask(e) {
e.preventDefault();
const btn = document.getElementById('boardSubmitBtn');
const oldBtnText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Plaatsen...</span>`;
if (typeof lucide !== 'undefined') lucide.createIcons();
const key = document.getElementById('boardIssueKey').value;
const summary = document.getElementById('boardIssueSummary').value;
const url = document.getElementById('boardIssueUrl').value;
const project = document.getElementById('boardIssueProject').value;
const boardId = parseInt(document.getElementById('boardSelectInput').value, 10);
const laneId = parseInt(document.getElementById('laneSelectInput').value, 10);
const color = document.getElementById('cardColorInput').value;
const priority = document.getElementById('cardPriorityInput').value;
if (!boardId || !laneId) {
alert('Selecteer een geldig bord en een kolom.');
btn.disabled = false;
btn.innerHTML = oldBtnText;
return;
}
try {
const response = await fetch('/api/integrations.php?action=board_jira_card', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
board_id: boardId,
lane_id: laneId,
key,
summary,
url,
description: `Project: ${project}\nJira URL: ${url}`,
color,
priority
})
});
const data = await response.json();
if (data.success) {
closeBoardModal();
showToast({
message: `Ticket ${key} geplaatst op bord!`,
subMessage: 'De kaart is direct zichtbaar in de gekozen kolom.',
linkText: 'Naar Bord',
linkUrl: `/board.php?board_id=${boardId}`
});
} else {
alert(data.error || 'Er is een fout opgetreden bij het plaatsen van de kaart.');
}
} catch (err) {
alert('Netwerkfout: ' + err.message);
} finally {
btn.disabled = false;
btn.innerHTML = oldBtnText;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
// Vernieuwen van Jira data
async function refreshJiraData() {
const btn = document.getElementById('refreshBtn');
const icon = document.getElementById('refreshIcon');
if (icon) icon.classList.add('animate-spin');
window.location.href = '/jira.php?refresh=1';
}
// Toast Notificatiesysteem
let toastTimer = null;
function showToast({ message, subMessage, linkText, linkUrl }) {
const toast = document.getElementById('jiraToast');
const msgEl = document.getElementById('toastMessage');
const subMsgEl = document.getElementById('toastSubMessage');
const linkEl = document.getElementById('toastActionLink');
if (!toast || !msgEl) return;
msgEl.innerText = message;
if (subMsgEl) subMsgEl.innerText = subMessage || '';
if (linkText && linkUrl && linkEl) {
linkEl.innerText = linkText;
linkEl.href = linkUrl;
linkEl.classList.remove('hidden');
} else if (linkEl) {
linkEl.classList.add('hidden');
}
toast.classList.remove('translate-y-24', 'opacity-0', 'pointer-events-none');
toast.classList.add('translate-y-0', 'opacity-100', 'pointer-events-auto');
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toast.classList.add('translate-y-24', 'opacity-0', 'pointer-events-none');
toast.classList.remove('translate-y-0', 'opacity-100', 'pointer-events-auto');
}, 5000);
}
// Helper voor veilige HTML rendering
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// Toetsenbordnavigatie voor alle modals (Escape & Enter)
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const outsideModal = document.getElementById('outsideWorkhoursModalJira');
if (outsideModal && !outsideModal.classList.contains('hidden')) {
closeOutsideWorkhoursModalJira('CANCEL');
return;
}
closePlanModal();
closeBoardModal();
} else if (e.key === 'Enter') {
const outsideModal = document.getElementById('outsideWorkhoursModalJira');
if (outsideModal && !outsideModal.classList.contains('hidden')) {
e.preventDefault();
closeOutsideWorkhoursModalJira('NEE');
return;
}
}
});
// Initiële opstart
document.addEventListener('DOMContentLoaded', () => {
// Herstel filters drawer open/dicht status
if (localStorage.getItem('fociz_jira_filters_open') === '1') {
toggleJiraFiltersDrawer(true);
}
calculateKpis();
updateHideClosedButton();
updateOnlyInProgressButton();
updateProjectPills();
updateQueueCounts();
if (currentQueue) {
updateQueuePills();
}
applyFilters();
// Luister naar live events voor nieuwe/gewijzigde Jira tickets
window.addEventListener('fociz:jira-updated', () => {
window.location.reload();
});
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>