Files
Fociz/jira.php
T

2433 lines
131 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'])) {
$sName = trim($iss['status']);
$sKey = strtolower($sName);
$allStatuses[$sKey] = [
'name' => $sName,
'status_category' => $iss['status_category'] ?? 'indeterminate',
'category_name' => '',
];
}
}
// Statussen uitbreiden met ALLE officiële Jira Cloud instantie statussen
$availableStatuses = $jiraData['available_statuses'] ?? [];
if (!empty($availableStatuses)) {
foreach ($availableStatuses as $st) {
if (!empty($st['name'])) {
$sName = trim($st['name']);
$sKey = strtolower($sName);
$cat = $st['status_category'] ?? ($allStatuses[$sKey]['status_category'] ?? 'indeterminate');
$allStatuses[$sKey] = [
'name' => $sName,
'status_category' => $cat,
'category_name' => $st['category_name'] ?? '',
];
}
}
}
// Fallback statussen als Jira nog geen verbinding heeft
if (empty($allStatuses)) {
$fallbackList = [
['name' => 'To Do', 'status_category' => 'new'],
['name' => 'In behandeling', 'status_category' => 'indeterminate'],
['name' => 'In Progress', 'status_category' => 'indeterminate'],
['name' => 'Wachten op klant', 'status_category' => 'indeterminate'],
['name' => 'In Review', 'status_category' => 'indeterminate'],
['name' => 'Gesloten', 'status_category' => 'done'],
['name' => 'Done', 'status_category' => 'done'],
];
foreach ($fallbackList as $fb) {
$allStatuses[strtolower($fb['name'])] = $fb;
}
}
// Sorteren: To Do / Nieuw (1), In Uitvoering / Behandeling (2), Gesloten / Gereed (3), daarbinnen alfabetisch
uasort($allStatuses, function($a, $b) {
$order = ['new' => 1, 'indeterminate' => 2, 'done' => 3];
$oa = $order[$a['status_category'] ?? 'indeterminate'] ?? 2;
$ob = $order[$b['status_category'] ?? 'indeterminate'] ?? 2;
if ($oa !== $ob) return $oa - $ob;
return strcasecmp($a['name'], $b['name']);
});
$allStatusesList = array_values($allStatuses);
ksort($allProjects);
ksort($allTypes);
$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);
$statusesJson = json_encode($allStatusesList, 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="filterByMyTickets()" 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="filterByAllTickets()" 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="selectActiveStatusesOnly()" 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#projects" 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: Status Multi-select Dropdown & Zoekveld -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<!-- Linkerkant: Statussen multi-select dropdown -->
<div class="flex flex-wrap items-center gap-2">
<div class="relative" id="statusDropdownWrapper">
<button
type="button"
onclick="toggleStatusDropdown()"
id="statusDropdownBtn"
class="flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-semibold bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 text-slate-800 dark:text-slate-200 transition cursor-pointer shadow-2xs"
aria-expanded="false"
>
<i data-lucide="check-square" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Status:</span>
<span id="statusDropdownLabel" class="font-bold text-indigo-600 dark:text-indigo-400">Alle statussen</span>
<span id="statusDropdownBadge" class="hidden px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300"></span>
<i data-lucide="chevron-down" id="statusDropdownChevron" class="w-3.5 h-3.5 text-slate-400 transition-transform duration-200"></i>
</button>
<!-- Dropdown Menu met Checkboxes -->
<div
id="statusDropdownMenu"
class="hidden absolute left-0 top-full mt-2 w-72 max-w-[90vw] bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 py-3 px-3.5 z-50 text-xs space-y-2.5"
>
<!-- Quick actions header -->
<div class="flex items-center justify-between pb-2 border-b border-slate-100 dark:border-slate-800">
<span class="font-bold text-slate-900 dark:text-white text-[11px] uppercase tracking-wider">Jira Statussen</span>
<div class="flex items-center space-x-1.5">
<button type="button" onclick="selectAllStatuses(true)" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer">
Alles
</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" onclick="selectActiveStatusesOnly()" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-950/60 transition cursor-pointer" title="Alleen actieve taken (geen gesloten)">
Actief
</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" onclick="selectAllStatuses(false)" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
Geen
</button>
</div>
</div>
<!-- Status Checkboxes List (scrollable) -->
<div class="max-h-64 overflow-y-auto space-y-1 pr-1 custom-scrollbar" id="statusCheckboxList">
<!-- Dynamisch gevuld via renderStatusCheckboxes() -->
</div>
</div>
</div>
<!-- Actieve filters reset knop als er iets gefilterd is -->
<button type="button" onclick="resetAllFilters()" id="btnResetFiltersInline" class="hidden flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-semibold text-slate-500 hover:text-slate-800 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="rotate-ccw" class="w-3 h-3"></i>
<span>Filters wissen</span>
</button>
</div>
<!-- Rechterkant: Zoekveld -->
<div class="relative w-full sm:w-64">
<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 in alle tickets..."
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-2 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>
<!-- 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 currentProject = '';
let currentQueue = (jiraData.user_queue_filters && jiraData.user_queue_filters.length > 0) ? 'MY_QUEUES' : (jiraData.default_queue_filter || '');
let selectedStatuses = new Set();
let activeUrgentOnly = false;
let expandedKeys = new Set();
const ticketStore = new Map();
const allJiraStatuses = <?= $statusesJson ?>;
// Hulpfunctie voor deduplicatie van tickets (assigned + team)
function getDeduplicatedIssues() {
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const seen = new Set();
const list = [];
[...assigned, ...team].forEach(iss => {
if (iss && iss.key && !seen.has(iss.key)) {
seen.add(iss.key);
list.push(iss);
}
});
return list;
}
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 voor de drawer
function calculateKpis() {
const all = getDeduplicatedIssues();
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
let inProgressCount = 0;
let highPriorityCount = 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 (p === 'highest' || p === 'high' || p === 'p1') {
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;
updateProjectCounts();
updateQueueCounts();
}
// Drawer open/dicht
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 badge
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 (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) activeCount++;
if (activeCount > 0) {
badge.innerText = activeCount;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
// ==========================================
// STATUS MULTI-SELECT CHECKBOX DROPDOWN LOGICA
// ==========================================
function renderStatusCheckboxes() {
const container = document.getElementById('statusCheckboxList');
if (!container) return;
if (!allJiraStatuses || allJiraStatuses.length === 0) {
container.innerHTML = '<div class="py-2 text-center text-slate-400 italic text-[11px]">Geen Jira statussen gevonden</div>';
return;
}
let html = '';
allJiraStatuses.forEach((st, idx) => {
const sNorm = (st.name || '').toLowerCase().trim();
const isChecked = selectedStatuses.has(sNorm);
const cat = (st.status_category || '').toLowerCase().trim();
let dotColor = 'bg-blue-500';
if (cat === 'new' || sNorm.includes('to do') || sNorm.includes('open') || sNorm.includes('nieuw')) {
dotColor = 'bg-slate-400';
} else if (cat === 'done' || isDoneStatus(sNorm)) {
dotColor = 'bg-emerald-500';
} else if (sNorm.includes('wacht') || sNorm.includes('klant') || sNorm.includes('pending')) {
dotColor = 'bg-amber-500';
}
const safeId = 'chkStatus_' + idx;
html += `
<label for="${safeId}" class="flex items-center justify-between px-2.5 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/80 cursor-pointer transition select-none group">
<div class="flex items-center space-x-2.5 min-w-0 flex-1">
<input
type="checkbox"
id="${safeId}"
value="${escapeHtml(sNorm)}"
${isChecked ? 'checked' : ''}
onchange="onStatusCheckboxChange()"
class="status-checkbox w-4 h-4 rounded text-indigo-600 border-slate-300 dark:border-slate-700 focus:ring-indigo-500 cursor-pointer flex-shrink-0"
>
<span class="w-2 h-2 rounded-full ${dotColor} flex-shrink-0"></span>
<span class="font-semibold text-slate-800 dark:text-slate-200 truncate text-xs group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition">${escapeHtml(st.name)}</span>
</div>
<span id="statusCount_${idx}" class="text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 ml-2 flex-shrink-0">0</span>
</label>
`;
});
container.innerHTML = html;
updateStatusCounts();
}
function updateStatusCounts() {
const all = getDeduplicatedIssues();
allJiraStatuses.forEach((st, idx) => {
const sNorm = (st.name || '').toLowerCase().trim();
const el = document.getElementById(`statusCount_${idx}`);
if (el) {
let currentList = all;
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());
});
}
if (currentQueue) {
currentList = currentList.filter(iss => isIssueInQueue(iss, currentQueue));
}
const cnt = currentList.filter(iss => (iss.status || '').toLowerCase().trim() === sNorm).length;
el.innerText = cnt;
}
});
}
function toggleStatusDropdown(force) {
const menu = document.getElementById('statusDropdownMenu');
const chevron = document.getElementById('statusDropdownChevron');
const btn = document.getElementById('statusDropdownBtn');
if (!menu) return;
const willOpen = (typeof force === 'boolean') ? force : menu.classList.contains('hidden');
if (willOpen) {
menu.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
if (btn) btn.setAttribute('aria-expanded', 'true');
updateStatusCounts();
} else {
menu.classList.add('hidden');
if (chevron) chevron.classList.remove('rotate-180');
if (btn) btn.setAttribute('aria-expanded', 'false');
}
}
document.addEventListener('click', (e) => {
const wrapper = document.getElementById('statusDropdownWrapper');
if (wrapper && !wrapper.contains(e.target)) {
toggleStatusDropdown(false);
}
});
function onStatusCheckboxChange() {
selectedStatuses.clear();
const checked = document.querySelectorAll('.status-checkbox:checked');
checked.forEach(chk => {
if (chk.value) selectedStatuses.add(chk.value.toLowerCase().trim());
});
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
applyFilters();
}
function selectAllStatuses(selectAll) {
selectedStatuses.clear();
const chks = document.querySelectorAll('.status-checkbox');
chks.forEach(chk => {
chk.checked = !!selectAll;
if (selectAll && chk.value) {
selectedStatuses.add(chk.value.toLowerCase().trim());
}
});
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
applyFilters();
}
function selectActiveStatusesOnly() {
selectedStatuses.clear();
const chks = document.querySelectorAll('.status-checkbox');
chks.forEach(chk => {
const val = (chk.value || '').toLowerCase().trim();
const isDone = isDoneStatus(val);
chk.checked = !isDone;
if (!isDone) {
selectedStatuses.add(val);
}
});
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
applyFilters();
}
function updateStatusDropdownLabel() {
const labelEl = document.getElementById('statusDropdownLabel');
const badgeEl = document.getElementById('statusDropdownBadge');
if (!labelEl) return;
const total = allJiraStatuses.length;
const selected = selectedStatuses.size;
if (selected === total || (total === 0 && selected === 0)) {
labelEl.innerText = "Alle statussen";
if (badgeEl) badgeEl.classList.add('hidden');
} else if (selected === 0) {
labelEl.innerText = "Geen geselecteerd";
if (badgeEl) {
badgeEl.innerText = "0";
badgeEl.classList.remove('hidden');
badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-rose-100 dark:bg-rose-950/60 text-rose-700 dark:text-rose-300";
}
} else if (selected === 1) {
const singleNorm = [...selectedStatuses][0];
const match = allJiraStatuses.find(st => (st.name || '').toLowerCase().trim() === singleNorm);
labelEl.innerText = match ? match.name : singleNorm;
if (badgeEl) {
badgeEl.innerText = "1";
badgeEl.classList.remove('hidden');
badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300";
}
} else {
labelEl.innerText = `${selected} statussen`;
if (badgeEl) {
badgeEl.innerText = `${selected}/${total}`;
badgeEl.classList.remove('hidden');
badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300";
}
}
const btnReset = document.getElementById('btnResetFiltersInline');
if (btnReset) {
const isCustom = selected < total || currentProject !== '' || (currentQueue !== '' && currentQueue !== 'MY_QUEUES') || document.getElementById('jiraSearchInput')?.value;
if (isCustom) btnReset.classList.remove('hidden');
else btnReset.classList.add('hidden');
}
}
function initStatusSelection() {
selectedStatuses = new Set();
let saved = null;
try {
const raw = localStorage.getItem('fociz_jira_selected_statuses');
if (raw) saved = JSON.parse(raw);
} catch (e) {}
if (Array.isArray(saved) && saved.length > 0) {
saved.forEach(s => {
if (s) selectedStatuses.add(String(s).toLowerCase().trim());
});
} else {
// Standaard alle actieve statussen selecteren, gesloten uit conform gebruikerswens
allJiraStatuses.forEach(st => {
const sNorm = (st.name || '').toLowerCase().trim();
const cat = (st.status_category || '').toLowerCase().trim();
if (cat !== 'done' && !isDoneStatus(sNorm)) {
selectedStatuses.add(sNorm);
}
});
if (selectedStatuses.size === 0) {
allJiraStatuses.forEach(st => {
selectedStatuses.add((st.name || '').toLowerCase().trim());
});
}
}
renderStatusCheckboxes();
updateStatusDropdownLabel();
}
// 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
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;
}
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";
}
});
}
function updateProjectCounts() {
const all = getDeduplicatedIssues();
let currentList = all;
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
currentList = currentList.filter(iss => selectedStatuses.has((iss.status || '').toLowerCase().trim()));
} else if (selectedStatuses.size === 0) {
currentList = [];
}
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;
}
});
}
function updateQueueCounts() {
const all = getDeduplicatedIssues();
let currentList = all;
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
currentList = currentList.filter(iss => selectedStatuses.has((iss.status || '').toLowerCase().trim()));
} else if (selectedStatuses.size === 0) {
currentList = [];
}
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;
}
}
});
}
function filterByMyTickets() {
const sInput = document.getElementById('jiraSearchInput');
if (sInput) {
sInput.value = 'Mijzelf';
}
applyFilters();
}
function filterByAllTickets() {
const sInput = document.getElementById('jiraSearchInput');
if (sInput) {
sInput.value = '';
}
applyFilters();
}
function filterByUrgent() {
const pSelect = document.getElementById('filterPriority');
if (pSelect) {
pSelect.value = 'High';
}
applyFilters();
}
function clearSearch() {
const input = document.getElementById('jiraSearchInput');
if (input) {
input.value = '';
}
document.getElementById('clearSearchBtn')?.classList.add('hidden');
applyFilters();
}
function resetAllFilters() {
currentProject = '';
currentQueue = '';
selectAllStatuses(true);
const searchInput = document.getElementById('jiraSearchInput');
const typeSelect = document.getElementById('filterType');
const prioritySelect = document.getElementById('filterPriority');
if (searchInput) searchInput.value = '';
if (typeSelect) typeSelect.value = '';
if (prioritySelect) prioritySelect.value = '';
document.getElementById('clearSearchBtn')?.classList.add('hidden');
updateProjectPills();
updateQueuePills();
applyFilters();
}
// Filter en render logica
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 clearBtn = document.getElementById('clearSearchBtn');
if (clearBtn) {
if (searchVal.length > 0) clearBtn.classList.remove('hidden');
else clearBtn.classList.add('hidden');
}
const all = getDeduplicatedIssues();
// Filter tickets
const filtered = all.filter(iss => {
const issPKey = (iss.project_key || '').toUpperCase().trim();
const issPName = (iss.project || '').toLowerCase().trim();
const issStatus = (iss.status || '').toLowerCase().trim();
// 1. Status Filter via Checkboxes
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
if (!selectedStatuses.has(issStatus)) {
return false;
}
} else if (selectedStatuses.size === 0) {
return false;
}
// 2. 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;
}
// 3. Wachtrijfilter (MY_QUEUES, SD, IN, AAN, GEN, DX)
if (currentQueue) {
if (!isIssueInQueue(iss, currentQueue)) {
return false;
}
}
// 4. Type filter (uit drawer)
if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) {
return false;
}
// 5. Prioriteit filter (uit drawer)
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;
}
}
// 6. 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();
updateStatusCounts();
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();
initStatusSelection();
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'; ?>