Files
Fociz/jira.php
T
2026-09-15 22:46:21 +02:00

3369 lines
180 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'] ?? [];
// Markeer expliciet welke tickets zijn toegewezen aan de ingelogde gebruiker
foreach ($assignedIssues as &$ai) {
$ai['is_assigned_to_me'] = true;
}
unset($ai);
$jiraData['assigned_issues'] = $assignedIssues;
$currentUserName = $currentUser['name'] ?? ($accountName ?: '');
$currentUserEmail = $currentUser['email'] ?? ($email ?: '');
// 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);
// Ophalen van actieve wachtrijfilters en toewijzingen van de ingelogde gebruiker
$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);
$userQueueFilters = array_values(array_filter(array_map('strtoupper', array_map('trim', $userQueueFilters ?? []))));
$jiraData['user_queue_filters'] = $userQueueFilters;
// Hulpfunctie om te controleren of een ticket behoort tot een wachtrij / project
$issueMatchesQueue = function(array $iss, string $code) use ($queueFilters): bool {
$code = strtoupper(trim($code));
if (empty($code)) return true;
$pKey = strtoupper(trim($iss['project_key'] ?? ''));
if (empty($pKey) && !empty($iss['key']) && preg_match('/^([A-Z0-9]+)-/i', $iss['key'], $m)) {
$pKey = strtoupper($m[1]);
}
$pName = strtolower(trim($iss['project'] ?? ''));
$ol = strtolower(trim($iss['onderwerp_logging'] ?? ''));
$sum = strtolower(trim($iss['summary'] ?? ''));
// 1. Zoek geconfigureerd filter in database
foreach ($queueFilters as $qf) {
if (strtoupper(trim($qf['filter_code'] ?? '')) === $code) {
if (!empty($qf['project_key'])) {
$filterPKey = strtoupper(trim($qf['project_key']));
if ($pKey) {
if ($pKey !== $filterPKey) return false;
} elseif (strpos($pName, strtolower($filterPKey)) === false) {
return false;
}
}
if (!empty($qf['jql_clause'])) {
$jql = $qf['jql_clause'];
if (preg_match('/onderwerp\s+logging.*?in\s*\(([^)]+)\)/i', $jql, $jm)) {
$terms = array_map(function($t) { return trim($t, " '\"\t\n\r\0\x0B"); }, explode(',', strtolower($jm[1])));
$found = false;
foreach ($terms as $t) {
if ($t !== '' && (strpos($ol, $t) !== false || strpos($sum, $t) !== false)) {
$found = true;
break;
}
}
if (!$found) return false;
}
}
return true;
}
}
// 2. Fallbacks voor standaard wachtrijen/projecten
if ($code === 'DX') {
$isSd = ($pKey === 'SD') || strpos($pName, 'servicedesk') !== false;
if (!$isSd) return false;
$dxTerms = ['koppeling', 'sso', 'endpoint', 'rest', 'datadump', 'uwv', 'migratie'];
foreach ($dxTerms as $t) {
if (strpos($ol, $t) !== false || strpos($sum, $t) !== false) return true;
}
return false;
}
if ($code === 'SD') {
return ($pKey === 'SD') || strpos($pName, 'servicedesk') !== false;
}
if ($code === 'IN') {
return ($pKey === 'IN') || strpos($pName, 'interface') !== false;
}
if ($code === 'AAN') {
return ($pKey === 'AAN') || strpos($pName, 'aanvraag') !== false;
}
if ($code === 'GEN') {
return ($pKey === 'GEN') || strpos($pName, 'general') !== false || strpos($pName, 'algemeen') !== false;
}
return ($pKey === $code);
};
// Als er specifieke projecten aan de gebruiker zijn toegewezen: verberg tickets van niet-toegewezen projecten
if (!empty($userQueueFilters)) {
$filterByAssigned = function(array $issues) use ($userQueueFilters, $issueMatchesQueue): array {
return array_values(array_filter($issues, function($iss) use ($userQueueFilters, $issueMatchesQueue) {
foreach ($userQueueFilters as $qCode) {
if ($issueMatchesQueue($iss, $qCode)) return true;
}
return false;
}));
};
$assignedIssues = $filterByAssigned($assignedIssues);
$teamIssues = $filterByAssigned($teamIssues);
$jiraData['assigned_issues'] = $assignedIssues;
$jiraData['team_issues'] = $teamIssues;
}
// Verzamel unieke projecten en types voor filter dropdowns en project switcher
$allIssues = array_merge($assignedIssues, $teamIssues);
$allProjects = [];
$allTypes = [];
$allPriorities = ['Highest', 'High', 'Medium', 'Low'];
$projectCounts = [];
// Core projecten met styling en namen
$coreProjects = [
'SD' => ['key' => 'SD', 'name' => 'Servicedesk', 'color' => 'sky'],
'IN' => ['key' => 'IN', 'name' => 'Interfaces', 'color' => 'amber'],
'AAN' => ['key' => 'AAN', 'name' => 'Aanvragen', 'color' => 'emerald'],
'GEN' => ['key' => 'GEN', 'name' => 'General', 'color' => 'indigo'],
];
$usedProjectKeys = [];
foreach ($allIssues as $iss) {
$pKey = strtoupper(trim($iss['project_key'] ?? ''));
if (empty($pKey) && !empty($iss['key']) && preg_match('/^([A-Z0-9]+)-/i', $iss['key'], $m)) {
$pKey = strtoupper($m[1]);
}
if (!empty($pKey)) {
$usedProjectKeys[$pKey] = true;
}
if (!empty($iss['project'])) {
$p = $iss['project'];
$allProjects[$p] = [
'name' => $p,
'key' => $pKey ?: ($iss['project_key'] ?? ''),
];
$projectCounts[$p] = ($projectCounts[$p] ?? 0) + 1;
}
if (!empty($iss['issue_type'])) {
$allTypes[$iss['issue_type']] = $iss['issue_type'];
}
if (!empty($iss['status'])) {
$allStatuses[$iss['status']] = $iss['status'];
}
}
// Bepaal welke projecten getoond worden: alleen projecten die tickets hebben, plus SD/IN/AAN/GEN
// Geen statussen van projecten die niet gebruikt worden!
$activeProjectKeys = [];
foreach ($coreProjects as $cKey => $cMeta) {
if (isset($usedProjectKeys[$cKey]) || !empty($projectCounts[$cMeta['name']]) || !empty($projectCounts[$cKey])) {
$activeProjectKeys[$cKey] = true;
}
}
foreach ($usedProjectKeys as $uKey => $val) {
$activeProjectKeys[$uKey] = true;
}
if (empty($activeProjectKeys)) {
$activeProjectKeys = array_fill_keys(['SD', 'IN', 'AAN', 'GEN'], true);
}
if (!empty($userQueueFilters)) {
$filteredActiveKeys = [];
foreach ($userQueueFilters as $code) {
$clean = strtoupper(trim($code));
if ($clean === 'DX') {
$filteredActiveKeys['SD'] = true;
} elseif ($clean !== 'MY_QUEUES') {
$filteredActiveKeys[$clean] = true;
}
}
if (!empty($filteredActiveKeys)) {
$activeProjectKeys = $filteredActiveKeys;
}
}
// Haal projectstatussen op uit jiraData
$rawProjectStatuses = $jiraData['project_statuses'] ?? [];
// Bouw gestructureerde lijst van statussen gegroepeerd per project
$projectStatusesData = [];
$allStatusesMap = [];
$allTargetKeys = array_keys($activeProjectKeys);
if ($isCodeVisible('DX') && !in_array('DX', $allTargetKeys, true)) {
$allTargetKeys[] = 'DX';
}
if (!empty($queueFilters)) {
foreach ($queueFilters as $qf) {
$qCode = strtoupper(trim($qf['filter_code'] ?? ''));
if (!empty($qCode) && $isCodeVisible($qCode) && !in_array($qCode, $allTargetKeys, true)) {
$allTargetKeys[] = $qCode;
}
}
}
foreach ($allTargetKeys as $pKey) {
if ($pKey === 'DX') {
$pName = 'DX Koppelingen';
$pColor = 'amber';
} else {
$pName = $coreProjects[$pKey]['name'] ?? ($allProjects[$pKey]['name'] ?? $pKey);
$pColor = $coreProjects[$pKey]['color'] ?? 'slate';
if (!isset($coreProjects[$pKey]) && !empty($queueFilters)) {
foreach ($queueFilters as $qf) {
if (strtoupper(trim($qf['filter_code'] ?? '')) === $pKey && !empty($qf['filter_name'])) {
$pName = $qf['filter_name'];
break;
}
}
}
}
$pStatuses = [];
// 1. Statussen uit Jira API voor dit project
$apiPKey = ($pKey === 'DX') ? 'SD' : $pKey;
if (!empty($rawProjectStatuses[$apiPKey])) {
foreach ($rawProjectStatuses[$apiPKey] as $st) {
$sName = trim($st['name'] ?? '');
if (empty($sName)) continue;
$sKey = strtolower($sName);
$pStatuses[$sKey] = [
'name' => $sName,
'status_category' => $st['status_category'] ?? 'indeterminate',
'category_name' => $st['category_name'] ?? '',
];
}
}
// 2. Statussen die op de tickets van dit project voorkomen toevoegen/mergen
foreach ($allIssues as $iss) {
$matches = false;
if ($pKey === 'DX') {
$matches = $issueMatchesQueue($iss, 'DX');
} else {
$issPKey = strtoupper(trim($iss['project_key'] ?? ''));
if (empty($issPKey) && !empty($iss['key']) && preg_match('/^([A-Z0-9]+)-/i', $iss['key'], $m)) {
$issPKey = strtoupper($m[1]);
}
$matches = ($issPKey === $pKey);
if (!$matches && !empty($queueFilters)) {
foreach ($queueFilters as $qf) {
if (strtoupper(trim($qf['filter_code'] ?? '')) === $pKey) {
$matches = $issueMatchesQueue($iss, $pKey);
break;
}
}
}
}
if ($matches && !empty($iss['status'])) {
$sName = trim($iss['status']);
$sKey = strtolower($sName);
if (!isset($pStatuses[$sKey])) {
$pStatuses[$sKey] = [
'name' => $sName,
'status_category' => $iss['status_category'] ?? 'indeterminate',
'category_name' => '',
];
}
}
}
// Fallback als een project nog geen statussen heeft
if (empty($pStatuses)) {
$fallback = [
['name' => 'To Do', 'status_category' => 'new'],
['name' => 'In behandeling', 'status_category' => 'indeterminate'],
['name' => 'Gesloten', 'status_category' => 'done'],
];
foreach ($fallback as $fb) {
$pStatuses[strtolower($fb['name'])] = $fb;
}
}
// Sorteren: To Do / Nieuw (1), In behandeling / Uitvoering (2), Gesloten / Gereed (3)
uasort($pStatuses, 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']);
});
$statusList = array_values($pStatuses);
foreach ($statusList as $st) {
$allStatusesMap[strtolower($st['name'])] = $st;
}
$projectStatusesData[] = [
'key' => $pKey,
'name' => $pName,
'color' => $pColor,
'statuses' => $statusList,
];
}
$allStatusesList = array_values($allStatusesMap);
ksort($allProjects);
ksort($allTypes);
$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);
$projectStatusesJson = json_encode($projectStatusesData, 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);
$currentUserNameJson = json_encode($currentUserName, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$currentUserEmailJson = json_encode($currentUserEmail, 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 flex-wrap items-center gap-2 self-start md:self-center">
<button onclick="refreshJiraData()" id="refreshBtn" class="flex items-center space-x-2 px-3 sm: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 sm: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><span class="hidden sm:inline">Jira </span>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 min-w-0 max-w-full">
<?php if ($isLive): ?>
<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-emerald-500 flex-shrink-0"></i>
<span class="truncate">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 flex-shrink-0">•</span>
<span class="text-[11px] text-slate-400 hidden sm:inline flex-shrink-0">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 flex-shrink-0"></i>
<span class="truncate">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 met Dropdown Pijltjes voor Status Settings & Uitvoerder -->
<div class="fociz-card relative z-30 p-4 sm:p-5 space-y-4 shadow-sm border border-slate-200/80 dark:border-slate-800 overflow-visible">
<!-- Bovenste balk: Titel links, Zoekveld + Reset rechts -->
<div class="flex flex-col sm:flex-row sm: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 flex-shrink-0">
<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 schakelen tussen projecten • Klik op het pijltje voor status instellingen &amp; uitvoerder</p>
</div>
</div>
<div class="flex items-center space-x-2 w-full sm:w-auto">
<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 flex-shrink-0">
<i data-lucide="rotate-ccw" class="w-3 h-3"></i>
<span>Filters wissen</span>
</button>
<div class="relative w-full sm:w-64 flex-shrink-0">
<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>
<!-- Project Knoppen met direct ingebouwde dropdowns voor statusinstellingen & uitvoerder -->
<div class="flex flex-wrap items-center gap-2 relative z-50" id="projectPillsContainer">
<?php
$hasUserAssignments = !empty($userQueueFilters);
$isCodeVisible = function(string $code) use ($hasUserAssignments, $userQueueFilters): bool {
if (!$hasUserAssignments) return true;
return in_array(strtoupper(trim($code)), $userQueueFilters, true);
};
?>
<!-- Alle Projecten -->
<div class="relative inline-flex items-stretch rounded-xl shadow-xs group flex-shrink-0 project-pill-container" data-project="" id="projGroup_all">
<button type="button" onclick="selectProject('')" data-project="" class="project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-slate-900 dark:bg-white text-white dark:text-slate-900">
<i data-lucide="grid" class="w-3.5 h-3.5"></i>
<span>Alle<span class="hidden sm:inline"> Projecten</span></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>
<button type="button" onclick="toggleProjectDropdown('all', event)" id="btnProjectArrow_all" class="project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-white/20 dark:border-slate-800 text-white dark:text-slate-900 hover:bg-white/10 dark:hover:bg-black/10 transition cursor-pointer relative" title="Status instellingen voor Alle Projecten" aria-expanded="false">
<i data-lucide="chevron-down" id="projChevron_all" class="w-3.5 h-3.5 transition-transform duration-200"></i>
<span id="projFilterActiveDot_all" class="hidden absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-amber-400 ring-1 ring-white dark:ring-slate-900"></span>
</button>
<div id="projectDropdownMenu_all" class="hidden project-dropdown-menu absolute left-0 top-full mt-2 w-72 sm:w-80 max-w-[calc(100vw-2rem)] 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"></div>
</div>
<!-- SD: Servicedesk -->
<?php if ($isCodeVisible('SD')): ?>
<div class="relative inline-flex items-stretch rounded-xl shadow-xs group flex-shrink-0 project-pill-container" data-project="SD" id="projGroup_SD">
<button type="button" onclick="selectProject('SD')" data-project="SD" class="project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-sky-50 dark:bg-sky-950/40 text-sky-800 dark:text-sky-300 border border-sky-300 dark:border-sky-800 border-r-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 class="hidden sm:inline">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>
<button type="button" onclick="toggleProjectDropdown('SD', event)" id="btnProjectArrow_SD" class="project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-sky-300 dark:border-sky-800 border-l-sky-200 dark:border-l-sky-800 text-sky-700 dark:text-sky-300 hover:bg-sky-100 dark:hover:bg-sky-900/60 transition cursor-pointer relative" title="Status instellingen voor Servicedesk" aria-expanded="false">
<i data-lucide="chevron-down" id="projChevron_SD" class="w-3.5 h-3.5 transition-transform duration-200"></i>
<span id="projFilterActiveDot_SD" class="hidden absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-amber-400 ring-1 ring-white dark:ring-slate-900"></span>
</button>
<div id="projectDropdownMenu_SD" class="hidden project-dropdown-menu absolute left-0 top-full mt-2 w-72 sm:w-80 max-w-[calc(100vw-2rem)] 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"></div>
</div>
<?php endif; ?>
<!-- GEN: General -->
<?php if ($isCodeVisible('GEN')): ?>
<div class="relative inline-flex items-stretch rounded-xl shadow-xs group flex-shrink-0 project-pill-container" data-project="GEN" id="projGroup_GEN">
<button type="button" onclick="selectProject('GEN')" data-project="GEN" class="project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/40 text-indigo-800 dark:text-indigo-300 border border-indigo-300 dark:border-indigo-800 border-r-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 class="hidden sm:inline">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>
<button type="button" onclick="toggleProjectDropdown('GEN', event)" id="btnProjectArrow_GEN" class="project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-indigo-300 dark:border-indigo-800 border-l-indigo-200 dark:border-l-indigo-800 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/60 transition cursor-pointer relative" title="Status instellingen voor General" aria-expanded="false">
<i data-lucide="chevron-down" id="projChevron_GEN" class="w-3.5 h-3.5 transition-transform duration-200"></i>
<span id="projFilterActiveDot_GEN" class="hidden absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-amber-400 ring-1 ring-white dark:ring-slate-900"></span>
</button>
<div id="projectDropdownMenu_GEN" class="hidden project-dropdown-menu absolute left-0 top-full mt-2 w-72 sm:w-80 max-w-[calc(100vw-2rem)] 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"></div>
</div>
<?php endif; ?>
<!-- IN: Interfaces -->
<?php if ($isCodeVisible('IN')): ?>
<div class="relative inline-flex items-stretch rounded-xl shadow-xs group flex-shrink-0 project-pill-container" data-project="IN" id="projGroup_IN">
<button type="button" onclick="selectProject('IN')" data-project="IN" class="project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 border border-amber-300 dark:border-amber-800 border-r-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 class="hidden sm:inline">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>
<button type="button" onclick="toggleProjectDropdown('IN', event)" id="btnProjectArrow_IN" class="project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-amber-300 dark:border-amber-800 border-l-amber-200 dark:border-l-amber-800 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 transition cursor-pointer relative" title="Status instellingen voor Interfaces" aria-expanded="false">
<i data-lucide="chevron-down" id="projChevron_IN" class="w-3.5 h-3.5 transition-transform duration-200"></i>
<span id="projFilterActiveDot_IN" class="hidden absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-amber-400 ring-1 ring-white dark:ring-slate-900"></span>
</button>
<div id="projectDropdownMenu_IN" class="hidden project-dropdown-menu absolute left-0 top-full mt-2 w-72 sm:w-80 max-w-[calc(100vw-2rem)] 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"></div>
</div>
<?php endif; ?>
<!-- AAN: Aanvragen -->
<?php if ($isCodeVisible('AAN')): ?>
<div class="relative inline-flex items-stretch rounded-xl shadow-xs group flex-shrink-0 project-pill-container" data-project="AAN" id="projGroup_AAN">
<button type="button" onclick="selectProject('AAN')" data-project="AAN" class="project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800 border-r-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 class="hidden sm:inline">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>
<button type="button" onclick="toggleProjectDropdown('AAN', event)" id="btnProjectArrow_AAN" class="project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-emerald-300 dark:border-emerald-800 border-l-emerald-200 dark:border-l-emerald-800 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/60 transition cursor-pointer relative" title="Status instellingen voor Aanvragen" aria-expanded="false">
<i data-lucide="chevron-down" id="projChevron_AAN" class="w-3.5 h-3.5 transition-transform duration-200"></i>
<span id="projFilterActiveDot_AAN" class="hidden absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-amber-400 ring-1 ring-white dark:ring-slate-900"></span>
</button>
<div id="projectDropdownMenu_AAN" class="hidden project-dropdown-menu absolute left-0 top-full mt-2 w-72 sm:w-80 max-w-[calc(100vw-2rem)] 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"></div>
</div>
<?php endif; ?>
<!-- DX: DX Koppelingen -->
<?php if ($isCodeVisible('DX')): ?>
<div class="relative inline-flex items-stretch rounded-xl shadow-xs group flex-shrink-0 project-pill-container" data-project="DX" id="projGroup_DX">
<button type="button" onclick="selectProject('DX')" data-project="DX" class="project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 border border-amber-300 dark:border-amber-800 border-r-0">
<i data-lucide="zap" class="w-3.5 h-3.5 text-amber-500"></i>
<span>DX</span>
<span id="projCount_DX" 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>
<button type="button" onclick="toggleProjectDropdown('DX', event)" id="btnProjectArrow_DX" class="project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-amber-300 dark:border-amber-800 border-l-amber-200 dark:border-l-amber-800 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 transition cursor-pointer relative" title="Status instellingen voor DX Koppelingen" aria-expanded="false">
<i data-lucide="chevron-down" id="projChevron_DX" class="w-3.5 h-3.5 transition-transform duration-200"></i>
<span id="projFilterActiveDot_DX" class="hidden absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-amber-400 ring-1 ring-white dark:ring-slate-900"></span>
</button>
<div id="projectDropdownMenu_DX" class="hidden project-dropdown-menu absolute left-0 top-full mt-2 w-72 sm:w-80 max-w-[calc(100vw-2rem)] 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"></div>
</div>
<?php endif; ?>
<!-- Overige geconfigureerde projectfilters -->
<?php foreach ($queueFilters as $qf):
$qCode = strtoupper(trim($qf['filter_code'] ?? ''));
if (in_array($qCode, ['SD', 'IN', 'AAN', 'GEN', 'DX'], true)) continue;
if (!$isCodeVisible($qCode)) continue;
?>
<div class="relative inline-flex items-stretch rounded-xl shadow-xs group flex-shrink-0 project-pill-container" data-project="<?= htmlspecialchars($qCode) ?>" id="projGroup_<?= htmlspecialchars($qCode) ?>">
<button type="button" onclick="selectProject('<?= htmlspecialchars(addslashes($qCode)) ?>')" data-project="<?= htmlspecialchars($qCode) ?>" class="project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700 border-r-0">
<span class="px-1.5 py-0.5 rounded font-mono text-[10px] font-black bg-slate-200 dark:bg-slate-700"><?= htmlspecialchars($qCode) ?></span>
<span class="hidden sm:inline"><?= htmlspecialchars($qf['filter_name']) ?></span>
<span id="projCount_<?= htmlspecialchars($qCode) ?>" 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">0</span>
</button>
<button type="button" onclick="toggleProjectDropdown('<?= htmlspecialchars(addslashes($qCode)) ?>', event)" id="btnProjectArrow_<?= htmlspecialchars($qCode) ?>" class="project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition cursor-pointer relative" title="Status instellingen voor <?= htmlspecialchars($qf['filter_name']) ?>" aria-expanded="false">
<i data-lucide="chevron-down" id="projChevron_<?= htmlspecialchars($qCode) ?>" class="w-3.5 h-3.5 transition-transform duration-200"></i>
<span id="projFilterActiveDot_<?= htmlspecialchars($qCode) ?>" class="hidden absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-amber-400 ring-1 ring-white dark:ring-slate-900"></span>
</button>
<div id="projectDropdownMenu_<?= htmlspecialchars($qCode) ?>" class="hidden project-dropdown-menu absolute left-0 top-full mt-2 w-72 sm:w-80 max-w-[calc(100vw-2rem)] 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"></div>
</div>
<?php endforeach; ?>
<?php if ($isAdmin): ?>
<a href="/admin.php#projects" title="Wachtrijen & Projecten beheren in Beheer" 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 flex-shrink-0">
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
</a>
<?php endif; ?>
</div>
</div>
<!-- Tickets Container -->
<div id="ticketsContainer" class="relative z-10 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 flex-col sm:flex-row items-stretch sm:items-center gap-2 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 text-center">
<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 ?>;
const projectStatusesData = <?= $projectStatusesJson ?>;
const allJiraStatuses = <?= $statusesJson ?>;
const currentUserName = <?= $currentUserNameJson ?>;
const currentUserEmail = <?= $currentUserEmailJson ?>;
let currentProject = '';
let currentQueue = '';
let selectedStatuses = new Set();
let activeUrgentOnly = false;
let expandedKeys = new Set();
const ticketStore = new Map();
// Filter per project: 'Toegewezen aan mij (uitvoerder)'
let projectOnlyAssigned = new Set();
try {
const savedAssigned = localStorage.getItem('fociz_jira_project_only_assigned');
if (savedAssigned) {
const parsed = JSON.parse(savedAssigned);
if (Array.isArray(parsed)) {
parsed.forEach(p => projectOnlyAssigned.add(String(p).toUpperCase().trim()));
}
}
} catch (e) {}
// Set van alle aan mij toegewezen issue keys
const assignedIssueKeys = new Set((jiraData.assigned_issues || []).map(i => i.key));
function isIssueAssignedToMe(iss) {
if (!iss) return false;
if (iss.is_assigned_to_me === true) return true;
if (iss.key && assignedIssueKeys.has(iss.key)) return true;
const ass = (iss.assignee_name || '').toLowerCase().trim();
if (!ass || ass === 'niet toegewezen' || ass === 'unassigned') return false;
if (ass === 'mijzelf' || ass === 'myself' || ass === 'me') return true;
const myName = (currentUserName || jiraData.account_name || '').toLowerCase().trim();
const myEmail = (currentUserEmail || jiraData.email || '').toLowerCase().trim();
if (myName && (ass === myName || ass.includes(myName) || myName.includes(ass))) return true;
if (myEmail && (ass.includes(myEmail) || (iss.assignee_email && String(iss.assignee_email).toLowerCase().includes(myEmail)))) return true;
return false;
}
function isProjectOnlyAssignedActive(projectVal, issPKey) {
if (projectOnlyAssigned.has('ALL')) return true;
const pNorm = (projectVal || '').toUpperCase().trim();
const issPNorm = (issPKey || '').toUpperCase().trim();
if (pNorm && projectOnlyAssigned.has(pNorm)) return true;
if (!pNorm && issPNorm && projectOnlyAssigned.has(issPNorm)) return true;
return false;
}
function toggleProjectOnlyAssigned(pKey, forceVal) {
const key = (pKey || 'ALL').toUpperCase().trim();
const isCurrently = projectOnlyAssigned.has(key);
const willSet = (typeof forceVal === 'boolean') ? forceVal : !isCurrently;
if (willSet) {
projectOnlyAssigned.add(key);
} else {
projectOnlyAssigned.delete(key);
}
try {
localStorage.setItem('fociz_jira_project_only_assigned', JSON.stringify([...projectOnlyAssigned]));
} catch (e) {}
updateAssignedPillState();
updateStatusDropdownLabel();
updateStatusCounts();
applyFilters();
}
function toggleCurrentProjectOnlyAssigned() {
const targetKey = (currentProject || 'ALL').toUpperCase().trim();
toggleProjectOnlyAssigned(targetKey);
}
function updateAssignedPillState() {
const all = getDeduplicatedIssues();
const myCountAll = all.filter(iss => isIssueAssignedToMe(iss)).length;
// Update 'Alle Projecten' dropdown
const chkAll = document.getElementById('chkOnlyAssigned_all');
if (chkAll) {
chkAll.checked = projectOnlyAssigned.has('ALL');
}
const cntAllEl = document.getElementById('projAssignedCount_all');
if (cntAllEl) {
cntAllEl.innerText = myCountAll;
}
const btnMijnAll = document.getElementById('btnDropdownMijn_all');
if (btnMijnAll) {
if (projectOnlyAssigned.has('ALL')) {
btnMijnAll.className = "px-1.5 py-0.5 rounded font-bold bg-indigo-600 text-white transition cursor-pointer";
} else {
btnMijnAll.className = "px-1.5 py-0.5 rounded font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer";
}
}
// Update per-project dropdowns
(projectStatusesData || []).forEach(proj => {
const pKey = (proj.key || '').toUpperCase().trim();
const chk = document.getElementById(`chkOnlyAssigned_${pKey}`);
if (chk) {
chk.checked = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
}
let projIssues = all.filter(iss => {
const k = getIssueProjectKey(iss);
return (k === pKey || (!k && (iss.project || '').toUpperCase().includes(pKey)));
});
if (pKey === 'DX' || !['SD', 'IN', 'AAN', 'GEN'].includes(pKey)) {
projIssues = all.filter(iss => isIssueInQueue(iss, pKey));
}
const cCnt = projIssues.filter(iss => isIssueAssignedToMe(iss)).length;
const cntEl = document.getElementById(`projAssignedCount_${pKey}`);
if (cntEl) cntEl.innerText = cCnt;
const btnMijn = document.getElementById(`btnDropdownMijn_${pKey}`);
if (btnMijn) {
const isPActive = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
if (isPActive) {
btnMijn.className = "px-1.5 py-0.5 rounded font-bold bg-indigo-600 text-white transition cursor-pointer";
} else {
btnMijn.className = "px-1.5 py-0.5 rounded font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer";
}
}
});
if (typeof updateProjectPills === 'function') {
updateProjectPills();
}
}
// 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 = [];
const myQueues = (jiraData.user_queue_filters || []).map(q => String(q).toUpperCase().trim());
const hasAssignments = myQueues.length > 0;
[...assigned, ...team].forEach(iss => {
if (iss && iss.key && !seen.has(iss.key)) {
seen.add(iss.key);
// Als er specifieke projecten aan de gebruiker zijn toegewezen:
// verberg tickets die niet tot minstens één van de toegewezen projecten behoren
if (hasAssignments) {
const isAllowed = myQueues.some(q => isIssueInQueue(iss, q));
if (!isAllowed) return;
}
list.push(iss);
}
});
return list;
}
// Hulpfunctie voor het betrouwbaar achterhalen van de projectcode van een issue
function getIssueProjectKey(iss) {
if (!iss) return '';
let k = (iss.project_key || '').toUpperCase().trim();
if (!k && iss.key) {
const m = iss.key.match(/^([A-Z0-9]+)-/i);
if (m) k = m[1].toUpperCase().trim();
}
return k;
}
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 myQueues = (jiraData.user_queue_filters || []).map(q => String(q).toUpperCase().trim());
const hasAssignments = myQueues.length > 0;
let assigned = jiraData.assigned_issues || [];
let team = jiraData.team_issues || [];
if (hasAssignments) {
assigned = assigned.filter(iss => myQueues.some(q => isIssueInQueue(iss, q)));
team = team.filter(iss => myQueues.some(q => isIssueInQueue(iss, q)));
}
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();
}
// 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 de badge in de rechter onderhoek
function updateActiveFilterBadge() {
const badge = document.getElementById('activeFilterBadge');
if (!badge) return;
let activeCount = 0;
if (currentProject) activeCount++;
if (document.getElementById('filterType')?.value) activeCount++;
if (document.getElementById('filterPriority')?.value) activeCount++;
const total = getTotalStatusCount();
if (selectedStatuses.size > 0 && selectedStatuses.size < total) activeCount++;
if (activeCount > 0) {
badge.innerText = activeCount;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
// =========================================================================
// STATUS MULTI-SELECT CHECKBOX DROPDOWN LOGICA (PER PROJECT DROPDOWNS)
// =========================================================================
function getTotalStatusCount() {
let total = 0;
(projectStatusesData || []).forEach(p => {
total += (p.statuses || []).length;
});
return total;
}
// Hulpfunctie: Bepaal of de status van een ticket geselecteerd is (ondersteunt compound keys en statusnamen)
function isIssueStatusSelected(iss) {
const total = getTotalStatusCount();
if (total === 0) return true;
if (selectedStatuses.size === 0) return true;
if (selectedStatuses.size >= total) return true;
const issPKey = getIssueProjectKey(iss).toLowerCase();
const issStatus = (iss.status || '').toLowerCase().trim();
const compound = issPKey ? `${issPKey}::${issStatus}` : '';
if (compound && selectedStatuses.has(compound)) return true;
if (selectedStatuses.has(issStatus)) return true;
if (typeof isIssueInQueue === 'function' && isIssueInQueue(iss, 'DX') && selectedStatuses.has(`dx::${issStatus}`)) return true;
return false;
}
// Renders each project's status dropdown directly inside its anchored container (#projectDropdownMenu_${pKey})
function buildStatusCheckboxRowHtml(pKey, st, sIdx, prefix = '') {
const sNorm = (st.name || '').toLowerCase().trim();
const pKeyUpper = (pKey || '').toUpperCase().trim();
const pKeyLower = (pKey || '').toLowerCase().trim();
const compoundVal = `${pKeyLower}::${sNorm}`;
const cat = (st.status_category || '').toLowerCase().trim();
const isDone = (cat === 'done' || isDoneStatus(sNorm));
const isChecked = selectedStatuses.has(compoundVal) || selectedStatuses.has(sNorm);
if (isChecked) {
selectedStatuses.add(compoundVal);
}
let dotColor = 'bg-blue-500';
if (cat === 'new' || sNorm.includes('to do') || sNorm.includes('open') || sNorm.includes('nieuw')) {
dotColor = 'bg-slate-400';
} else if (isDone) {
dotColor = 'bg-emerald-500';
} else if (sNorm.includes('wacht') || sNorm.includes('klant') || sNorm.includes('pending')) {
dotColor = 'bg-amber-500';
}
const safeKey = `${pKeyUpper}_${sNorm.replace(/[^a-z0-9]/gi, '_')}`;
const safeId = `chkStatus_${prefix}${pKeyUpper}_${sIdx}`;
return `
<label for="${safeId}" class="flex items-center justify-between px-2.5 py-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800/70 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(compoundVal)}"
data-project="${escapeHtml(pKeyUpper)}"
data-status="${escapeHtml(sNorm)}"
data-is-done="${isDone ? 1 : 0}"
${isChecked ? 'checked' : ''}
onchange="onStatusCheckboxChange('${escapeHtml(pKeyUpper)}', '${escapeHtml(compoundVal)}', this.checked)"
class="status-checkbox status-checkbox-${escapeHtml(pKeyUpper)} 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-700 dark:text-slate-300 truncate text-xs group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition">${escapeHtml(st.name)}</span>
</div>
<span class="status-count-${safeKey} 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>
`;
}
function renderStatusCheckboxes() {
const colorClasses = {
sky: {
badge: 'bg-sky-100 text-sky-800 dark:bg-sky-950/70 dark:text-sky-300 border border-sky-300 dark:border-sky-800',
hdrBg: 'bg-sky-50/80 dark:bg-sky-950/40'
},
amber: {
badge: 'bg-amber-100 text-amber-800 dark:bg-amber-950/70 dark:text-amber-300 border border-amber-300 dark:border-amber-800',
hdrBg: 'bg-amber-50/80 dark:bg-amber-950/40'
},
emerald: {
badge: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/70 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800',
hdrBg: 'bg-emerald-50/80 dark:bg-emerald-950/40'
},
indigo: {
badge: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-300 dark:border-indigo-800',
hdrBg: 'bg-indigo-50/80 dark:bg-indigo-950/40'
},
slate: {
badge: 'bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-200 border border-slate-300 dark:border-slate-700',
hdrBg: 'bg-slate-50/80 dark:bg-slate-800/40'
}
};
// 1. Dropdown voor "Alle Projecten" (#projectDropdownMenu_all)
const menuAll = document.getElementById('projectDropdownMenu_all');
if (menuAll) {
let allStatusesGroupedHtml = '';
(projectStatusesData || []).forEach(proj => {
const pKey = (proj.key || '').toUpperCase().trim();
const pColor = proj.color || 'slate';
const styles = colorClasses[pColor] || colorClasses.slate;
const statuses = proj.statuses || [];
if (statuses.length === 0) return;
let rows = '';
statuses.forEach((st, sIdx) => {
rows += buildStatusCheckboxRowHtml(pKey, st, sIdx, 'all_');
});
allStatusesGroupedHtml += `
<div class="mb-2.5 pb-2 border-b border-slate-100 dark:border-slate-800 last:border-b-0 last:mb-0 last:pb-0">
<div class="flex items-center justify-between px-2 py-1 bg-slate-50 dark:bg-slate-800/40 rounded-lg mb-1">
<div class="flex items-center space-x-1.5 min-w-0">
<span class="px-1.5 py-0.5 rounded font-mono text-[9px] font-black ${styles.badge}">${escapeHtml(pKey)}</span>
<span class="font-bold text-[11px] text-slate-700 dark:text-slate-300 truncate">${escapeHtml(proj.name || pKey)}</span>
</div>
<div class="flex items-center space-x-1 text-[10px] flex-shrink-0">
<button type="button" onclick="selectProjectStatuses('${escapeHtml(pKey)}', true)" class="font-bold text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer">Alles</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" onclick="selectProjectStatuses('${escapeHtml(pKey)}', false)" class="font-bold text-slate-500 hover:underline cursor-pointer">Geen</button>
</div>
</div>
<div class="space-y-0.5">
${rows}
</div>
</div>
`;
});
menuAll.innerHTML = `
<div class="flex items-center justify-between pb-2 border-b border-slate-100 dark:border-slate-800 bg-slate-50/80 dark:bg-slate-800/50 -mx-3.5 -mt-3 p-3 rounded-t-2xl">
<div class="flex items-center space-x-1.5 min-w-0">
<i data-lucide="layers" class="w-3.5 h-3.5 text-slate-600 dark:text-slate-400"></i>
<span class="font-bold text-slate-900 dark:text-white text-xs truncate">Alle Projecten</span>
<span id="projDropdownTotal_all" class="text-[10px] text-slate-400 font-bold">(0)</span>
</div>
<div class="flex items-center space-x-1 text-[10px] flex-shrink-0">
<button type="button" onclick="selectAllStatuses(true)" class="px-1.5 py-0.5 rounded 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="selectAllActiveStatuses()" class="px-1.5 py-0.5 rounded 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">
Actief
</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" id="btnDropdownMijn_all" onclick="toggleProjectOnlyAssigned('ALL')" class="px-1.5 py-0.5 rounded font-bold ${projectOnlyAssigned.has('ALL') ? 'bg-indigo-600 text-white' : 'text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60'} transition cursor-pointer" title="Toegewezen aan mij (uitvoerder)">
Mijn
</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" onclick="selectAllStatuses(false)" class="px-1.5 py-0.5 rounded font-bold text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition cursor-pointer">
Geen
</button>
</div>
</div>
<!-- Toegewezen aan mij toggle row -->
<div class="px-3 py-2 -mx-3.5 bg-slate-50/80 dark:bg-slate-800/50 border-b border-slate-100 dark:border-slate-800/80 flex items-center justify-between">
<label for="chkOnlyAssigned_all" class="flex items-center space-x-2 cursor-pointer select-none min-w-0 flex-1 group">
<div class="w-6 h-6 rounded-lg ${projectOnlyAssigned.has('ALL') ? 'bg-indigo-600 text-white' : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300'} flex items-center justify-center flex-shrink-0 transition">
<i data-lucide="user-check" class="w-3.5 h-3.5"></i>
</div>
<div class="min-w-0 flex-1">
<div class="text-xs font-bold text-slate-800 dark:text-slate-200 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition truncate">Toegewezen aan mij (uitvoerder)</div>
<div class="text-[10px] text-slate-400">Alleen mijn taken in alle projecten</div>
</div>
</label>
<div class="flex items-center space-x-2 flex-shrink-0 ml-2">
<span id="projAssignedCount_all" class="px-1.5 py-0.5 rounded-full text-[10px] font-black bg-indigo-100 dark:bg-indigo-950/70 text-indigo-700 dark:text-indigo-300">0</span>
<input
type="checkbox"
id="chkOnlyAssigned_all"
${projectOnlyAssigned.has('ALL') ? 'checked' : ''}
onchange="toggleProjectOnlyAssigned('ALL', this.checked)"
class="w-4 h-4 rounded text-indigo-600 border-slate-300 dark:border-slate-700 focus:ring-indigo-500 cursor-pointer"
>
</div>
</div>
<div class="max-h-64 overflow-y-auto space-y-1 pr-1 custom-scrollbar">
${allStatusesGroupedHtml || '<div class="text-slate-400 italic text-[11px] py-2 text-center">Geen project statussen gevonden</div>'}
</div>
`;
}
// 2. Dropdown per afzonderlijk project (#projectDropdownMenu_${pKey})
(projectStatusesData || []).forEach(proj => {
const pKey = (proj.key || '').toUpperCase().trim();
const menuEl = document.getElementById(`projectDropdownMenu_${pKey}`);
if (!menuEl) return;
const pColor = proj.color || 'slate';
const styles = colorClasses[pColor] || colorClasses.slate;
const statuses = proj.statuses || [];
let statusRowsHtml = '';
statuses.forEach((st, sIdx) => {
statusRowsHtml += buildStatusCheckboxRowHtml(pKey, st, sIdx, '');
});
const isAssigned = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
menuEl.innerHTML = `
<div class="flex items-center justify-between pb-2 border-b border-slate-100 dark:border-slate-800 ${styles.hdrBg} -mx-3.5 -mt-3 p-3 rounded-t-2xl">
<div class="flex items-center space-x-1.5 min-w-0">
<span class="px-1.5 py-0.5 rounded font-mono text-[10px] font-black ${styles.badge}">${escapeHtml(pKey)}</span>
<span class="font-bold text-slate-900 dark:text-white text-xs truncate">${escapeHtml(proj.name || pKey)}</span>
<span id="projDropdownTotal_${escapeHtml(pKey)}" class="text-[10px] text-slate-400 font-bold">(0)</span>
</div>
<div class="flex items-center space-x-1 text-[10px] flex-shrink-0">
<button type="button" onclick="selectProjectStatuses('${escapeHtml(pKey)}', true)" class="px-1.5 py-0.5 rounded 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="selectProjectActiveStatuses('${escapeHtml(pKey)}')" class="px-1.5 py-0.5 rounded 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">
Actief
</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" id="btnDropdownMijn_${escapeHtml(pKey)}" onclick="toggleProjectOnlyAssigned('${escapeHtml(pKey)}')" class="px-1.5 py-0.5 rounded font-bold ${isAssigned ? 'bg-indigo-600 text-white' : 'text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60'} transition cursor-pointer" title="Toegewezen aan mij (uitvoerder)">
Mijn
</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" onclick="selectProjectStatuses('${escapeHtml(pKey)}', false)" class="px-1.5 py-0.5 rounded font-bold text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition cursor-pointer">
Geen
</button>
</div>
</div>
<!-- Toegewezen aan mij toggle card -->
<div class="px-3 py-2 -mx-3.5 bg-slate-50/80 dark:bg-slate-800/50 border-b border-slate-100 dark:border-slate-800/80 flex items-center justify-between">
<label for="chkOnlyAssigned_${escapeHtml(pKey)}" class="flex items-center space-x-2 cursor-pointer select-none min-w-0 flex-1 group">
<div class="w-6 h-6 rounded-lg ${isAssigned ? 'bg-indigo-600 text-white' : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300'} flex items-center justify-center flex-shrink-0 transition">
<i data-lucide="user-check" class="w-3.5 h-3.5"></i>
</div>
<div class="min-w-0 flex-1">
<div class="text-xs font-bold text-slate-800 dark:text-slate-200 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition truncate">Toegewezen aan mij (uitvoerder)</div>
<div class="text-[10px] text-slate-400">Alleen mijn taken in ${escapeHtml(pKey)}</div>
</div>
</label>
<div class="flex items-center space-x-2 flex-shrink-0 ml-2">
<span id="projAssignedCount_${escapeHtml(pKey)}" class="px-1.5 py-0.5 rounded-full text-[10px] font-black bg-indigo-100 dark:bg-indigo-950/70 text-indigo-700 dark:text-indigo-300">0</span>
<input
type="checkbox"
id="chkOnlyAssigned_${escapeHtml(pKey)}"
${isAssigned ? 'checked' : ''}
onchange="toggleProjectOnlyAssigned('${escapeHtml(pKey)}', this.checked)"
class="w-4 h-4 rounded text-indigo-600 border-slate-300 dark:border-slate-700 focus:ring-indigo-500 cursor-pointer"
>
</div>
</div>
<div class="max-h-60 overflow-y-auto space-y-1 pr-1 custom-scrollbar" id="statusCheckboxList_${escapeHtml(pKey)}">
${statusRowsHtml || '<div class="text-slate-400 italic text-[11px] py-2 text-center">Geen statussen gevonden</div>'}
</div>
`;
});
if (typeof lucide !== 'undefined') lucide.createIcons();
updateStatusCounts();
updateStatusDropdownLabel();
updateProjectPills();
}
function toggleProjectDropdown(projectKey, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
const targetKey = (projectKey || 'all').trim();
const menu = document.getElementById(`projectDropdownMenu_${targetKey}`);
const chevron = document.getElementById(`projChevron_${targetKey}`);
const btnArrow = document.getElementById(`btnProjectArrow_${targetKey}`);
if (!menu) return;
const isOpen = !menu.classList.contains('hidden');
closeAllProjectDropdowns();
if (!isOpen) {
menu.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
if (btnArrow) btnArrow.setAttribute('aria-expanded', 'true');
// Check of menu over de rechter rand van het scherm loopt
const rect = menu.getBoundingClientRect();
if (rect.right > (window.innerWidth || document.documentElement.clientWidth) - 16) {
menu.classList.remove('left-0');
menu.classList.add('right-0');
} else {
menu.classList.remove('right-0');
menu.classList.add('left-0');
}
updateStatusCounts();
}
}
function closeAllProjectDropdowns(exceptKey = '') {
const exceptNorm = (exceptKey || '').trim();
document.querySelectorAll('.project-dropdown-menu').forEach(menu => {
const mKey = menu.id.replace('projectDropdownMenu_', '');
if (mKey === exceptNorm) return;
if (!menu.classList.contains('hidden')) {
menu.classList.add('hidden');
const chevron = document.getElementById(`projChevron_${mKey}`);
if (chevron) chevron.classList.remove('rotate-180');
const btnArrow = document.getElementById(`btnProjectArrow_${mKey}`);
if (btnArrow) btnArrow.setAttribute('aria-expanded', 'false');
}
});
}
// Backwards compatibility aliases
function toggleProjectStatusDropdown(projectKey, force) { toggleProjectDropdown(projectKey); }
function closeAllProjectStatusDropdowns(exceptKey) { closeAllProjectDropdowns(exceptKey); }
function toggleStatusDropdown(force) { toggleProjectDropdown(currentProject || 'all'); }
function updateProjectStatusDropdownVisibility() {}
// Sluit dropdowns bij klikken buiten dropdowns
document.addEventListener('click', (e) => {
if (!e.target.closest('.project-pill-container')) {
closeAllProjectDropdowns();
}
});
function updateStatusCounts() {
const all = getDeduplicatedIssues();
// 1. Totaal en Toegewezen aan mij per project updaten in de dropdowns
const allAssignedCount = all.filter(iss => isIssueAssignedToMe(iss)).length;
const elAllAssigned = document.getElementById('projAssignedCount_all');
if (elAllAssigned) elAllAssigned.innerText = allAssignedCount;
const elAllTotal = document.getElementById('projDropdownTotal_all');
if (elAllTotal) elAllTotal.innerText = `(${all.length})`;
const chkAll = document.getElementById('chkOnlyAssigned_all');
if (chkAll) chkAll.checked = projectOnlyAssigned.has('ALL');
const btnMijnAll = document.getElementById('btnDropdownMijn_all');
if (btnMijnAll) {
if (projectOnlyAssigned.has('ALL')) {
btnMijnAll.className = "px-1.5 py-0.5 rounded font-bold bg-indigo-600 text-white transition cursor-pointer";
} else {
btnMijnAll.className = "px-1.5 py-0.5 rounded font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer";
}
}
(projectStatusesData || []).forEach(proj => {
const pKey = (proj.key || '').toUpperCase().trim();
let projIssues = all.filter(iss => {
const k = getIssueProjectKey(iss);
const pn = (iss.project || '').toUpperCase().trim();
if (k) return k === pKey;
return pn.includes(pKey);
});
if (pKey === 'DX' || !['SD', 'IN', 'AAN', 'GEN'].includes(pKey)) {
projIssues = all.filter(iss => isIssueInQueue(iss, pKey));
}
const projTotalCount = projIssues.length;
const projAssignedCount = projIssues.filter(iss => isIssueAssignedToMe(iss)).length;
const elDropdownTotal = document.getElementById(`projDropdownTotal_${pKey}`);
if (elDropdownTotal) elDropdownTotal.innerText = `(${projTotalCount})`;
const elAssignedCount = document.getElementById(`projAssignedCount_${pKey}`);
if (elAssignedCount) elAssignedCount.innerText = projAssignedCount;
const chk = document.getElementById(`chkOnlyAssigned_${pKey}`);
if (chk) chk.checked = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
const btnMijn = document.getElementById(`btnDropdownMijn_${pKey}`);
if (btnMijn) {
if (projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL')) {
btnMijn.className = "px-1.5 py-0.5 rounded font-bold bg-indigo-600 text-white transition cursor-pointer";
} else {
btnMijn.className = "px-1.5 py-0.5 rounded font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer";
}
}
// Tellingen per status
let statusBaseList = projIssues;
if (projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL')) {
statusBaseList = statusBaseList.filter(iss => isIssueAssignedToMe(iss));
}
(proj.statuses || []).forEach(st => {
const sNorm = (st.name || '').toLowerCase().trim();
const safeKey = `${pKey}_${sNorm.replace(/[^a-z0-9]/gi, '_')}`;
const cnt = statusBaseList.filter(iss => (iss.status || '').toLowerCase().trim() === sNorm).length;
const countEls = document.querySelectorAll(`.status-count-${safeKey}`);
countEls.forEach(el => el.innerText = cnt);
const singleEl = document.getElementById(`statusCount_${safeKey}`);
if (singleEl) singleEl.innerText = cnt;
});
});
updateProjectCounts();
updateProjectPills();
}
function onStatusCheckboxChange(projectKey, changedVal, isChecked) {
if (changedVal) {
const normVal = changedVal.toLowerCase().trim();
if (isChecked) {
selectedStatuses.add(normVal);
} else {
selectedStatuses.delete(normVal);
}
// Sync checkboxes that have this same value across dropdowns
const chks = document.querySelectorAll('.status-checkbox');
chks.forEach(chk => {
if (chk.value && chk.value.toLowerCase().trim() === normVal) {
chk.checked = isChecked;
}
});
}
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
updateStatusCounts();
applyFilters();
}
function selectProjectStatuses(projectKey, selectAll) {
const targetKey = (projectKey || '').toUpperCase().trim();
const chks = document.querySelectorAll(`.status-checkbox[data-project="${targetKey}"]`);
chks.forEach(chk => {
chk.checked = !!selectAll;
const val = chk.value.toLowerCase().trim();
if (selectAll) {
selectedStatuses.add(val);
} else {
selectedStatuses.delete(val);
}
});
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
updateStatusCounts();
applyFilters();
}
function selectProjectActiveStatuses(projectKey) {
const targetKey = (projectKey || '').toUpperCase().trim();
const chks = document.querySelectorAll(`.status-checkbox[data-project="${targetKey}"]`);
chks.forEach(chk => {
const isDone = chk.getAttribute('data-is-done') === '1';
chk.checked = !isDone;
const val = chk.value.toLowerCase().trim();
if (!isDone) {
selectedStatuses.add(val);
} else {
selectedStatuses.delete(val);
}
});
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
updateStatusCounts();
applyFilters();
}
function selectAllActiveStatuses() {
const chks = document.querySelectorAll('.status-checkbox');
chks.forEach(chk => {
const isDone = chk.getAttribute('data-is-done') === '1';
chk.checked = !isDone;
const val = chk.value.toLowerCase().trim();
if (!isDone) {
selectedStatuses.add(val);
} else {
selectedStatuses.delete(val);
}
});
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
updateStatusCounts();
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();
updateStatusCounts();
applyFilters();
}
function updateStatusDropdownLabel() {
let hasAnyCustomFilter = false;
if (currentProject !== '' || projectOnlyAssigned.size > 0 || (document.getElementById('jiraSearchInput')?.value || '').trim().length > 0) {
hasAnyCustomFilter = true;
}
const total = getTotalStatusCount();
if (selectedStatuses.size > 0 && selectedStatuses.size < total) {
hasAnyCustomFilter = true;
}
const btnReset = document.getElementById('btnResetFiltersInline');
if (btnReset) {
if (hasAnyCustomFilter) btnReset.classList.remove('hidden');
else btnReset.classList.add('hidden');
}
updateActiveFilterBadge();
}
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) {}
let validSaved = false;
if (Array.isArray(saved) && saved.length > 0) {
const hasCompound = saved.some(s => String(s).includes('::'));
if (hasCompound) {
saved.forEach(s => {
if (s) selectedStatuses.add(String(s).toLowerCase().trim());
});
validSaved = true;
}
}
if (!validSaved) {
// Standaard alle actieve statussen selecteren, gesloten uit conform gebruikerswens
(projectStatusesData || []).forEach(proj => {
const pKeyLower = (proj.key || '').toLowerCase().trim();
(proj.statuses || []).forEach(st => {
const sNorm = (st.name || '').toLowerCase().trim();
const cat = (st.status_category || '').toLowerCase().trim();
const isDone = (cat === 'done' || isDoneStatus(sNorm));
if (!isDone) {
selectedStatuses.add(`${pKeyLower}::${sNorm}`);
}
});
});
if (selectedStatuses.size === 0) {
(projectStatusesData || []).forEach(proj => {
const pKeyLower = (proj.key || '').toLowerCase().trim();
(proj.statuses || []).forEach(st => {
const sNorm = (st.name || '').toLowerCase().trim();
selectedStatuses.add(`${pKeyLower}::${sNorm}`);
});
});
}
if (selectedStatuses.size === 0) {
const all = getDeduplicatedIssues();
all.forEach(iss => {
const pk = getIssueProjectKey(iss).toLowerCase();
const st = (iss.status || '').toLowerCase().trim();
if (st && !isDoneStatus(st)) {
selectedStatuses.add(pk ? `${pk}::${st}` : st);
}
});
}
}
renderStatusCheckboxes();
}
// Project selectie via pill tabs
function selectProject(proj) {
currentProject = (proj || '').trim();
closeAllProjectDropdowns();
updateProjectPills();
updateStatusCounts();
applyFilters();
}
function updateProjectPills() {
const containers = document.querySelectorAll('.project-pill-container');
const curNorm = (currentProject || '').toUpperCase().trim();
containers.forEach(container => {
const pKey = (container.getAttribute('data-project') || '').toUpperCase().trim();
const isSelected = (pKey === curNorm);
const btn = container.querySelector('.project-pill-btn');
const arrow = container.querySelector('.project-pill-arrow');
const dot = container.querySelector(`[id^="projFilterActiveDot_"]`);
// Check of filters actief zijn voor dit project
let hasFilter = false;
if (pKey === '') {
const isAssigned = projectOnlyAssigned.has('ALL');
const total = getTotalStatusCount();
const isCustomStatus = (selectedStatuses.size > 0 && selectedStatuses.size < total);
hasFilter = isAssigned || isCustomStatus;
} else {
const isAssigned = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
const projObj = (projectStatusesData || []).find(p => (p.key || '').toUpperCase().trim() === pKey);
let isCustomStatus = false;
if (projObj && (projObj.statuses || []).length > 0) {
const pKeyLower = pKey.toLowerCase();
const totalProjStatuses = projObj.statuses.length;
let checkedCount = 0;
projObj.statuses.forEach(st => {
const sNorm = (st.name || '').toLowerCase().trim();
const compound = `${pKeyLower}::${sNorm}`;
if (selectedStatuses.has(compound) || selectedStatuses.has(sNorm)) {
checkedCount++;
}
});
if (checkedCount < totalProjStatuses) {
isCustomStatus = true;
}
}
hasFilter = isAssigned || isCustomStatus;
}
if (dot) {
if (hasFilter) {
dot.classList.remove('hidden');
} else {
dot.classList.add('hidden');
}
}
if (!btn || !arrow) return;
// Pas styling toe op de linker knop en rechter pijltjesknop
if (pKey === '') {
if (isSelected) {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-slate-900 dark:bg-white text-white dark:text-slate-900";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-white/20 dark:border-slate-800 bg-slate-900 dark:bg-white text-white dark:text-slate-900 hover:bg-slate-800 dark:hover:bg-slate-100 transition cursor-pointer relative";
} else {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-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 border-r-0";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-slate-200 dark:border-slate-700 border-l-slate-300 dark:border-l-slate-600 bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition cursor-pointer relative";
}
} else if (pKey === 'SD') {
if (isSelected) {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-sky-600 text-white shadow-xs";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-sky-500 bg-sky-600 text-white hover:bg-sky-700 transition cursor-pointer relative";
} else {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-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 border-r-0";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-sky-300 dark:border-sky-800 border-l-sky-200 dark:border-l-sky-800 bg-sky-50 dark:bg-sky-950/40 text-sky-700 dark:text-sky-300 hover:bg-sky-100 dark:hover:bg-sky-900/60 transition cursor-pointer relative";
}
} else if (pKey === 'GEN') {
if (isSelected) {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-indigo-500 bg-indigo-600 text-white hover:bg-indigo-700 transition cursor-pointer relative";
} else {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-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 border-r-0";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-indigo-300 dark:border-indigo-800 border-l-indigo-200 dark:border-l-indigo-800 bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/60 transition cursor-pointer relative";
}
} else if (pKey === 'IN') {
if (isSelected) {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-amber-600 text-white shadow-xs";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-amber-500 bg-amber-600 text-white hover:bg-amber-700 transition cursor-pointer relative";
} else {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-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 border-r-0";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-amber-300 dark:border-amber-800 border-l-amber-200 dark:border-l-amber-800 bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 transition cursor-pointer relative";
}
} else if (pKey === 'AAN') {
if (isSelected) {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-emerald-600 text-white shadow-xs";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-emerald-500 bg-emerald-600 text-white hover:bg-emerald-700 transition cursor-pointer relative";
} else {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-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 border-r-0";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-emerald-300 dark:border-emerald-800 border-l-emerald-200 dark:border-l-emerald-800 bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/60 transition cursor-pointer relative";
}
} else if (pKey === 'DX') {
if (isSelected) {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-amber-600 text-white shadow-xs";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-amber-500 bg-amber-600 text-white hover:bg-amber-700 transition cursor-pointer relative";
} else {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-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 border-r-0";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-amber-300 dark:border-amber-800 border-l-amber-200 dark:border-l-amber-800 bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/60 transition cursor-pointer relative";
}
} else {
if (isSelected) {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border-l border-indigo-500 bg-indigo-600 text-white hover:bg-indigo-700 transition cursor-pointer relative";
} else {
btn.className = "project-pill-btn flex items-center space-x-1.5 pl-2.5 sm:pl-3 pr-2 py-1.5 rounded-l-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 border-r-0";
arrow.className = "project-pill-arrow px-2 py-1.5 flex items-center justify-center rounded-r-xl border border-slate-200 dark:border-slate-700 border-l-slate-200 dark:border-l-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition cursor-pointer relative";
}
}
});
}
// Hulpfunctie: Match een issue tegen een JQL clausule
function matchJqlClause(iss, jql) {
if (!jql || typeof jql !== 'string') return true;
const trimmed = jql.trim();
if (!trimmed) return true;
const issPKey = getIssueProjectKey(iss);
const issPName = (iss.project || '').toLowerCase().trim();
const issOl = (iss.onderwerp_logging || '').toLowerCase().trim();
const issSum = (iss.summary || '').toLowerCase();
const issPriority = (iss.priority || '').toLowerCase().trim();
const issType = (iss.issue_type || '').toLowerCase().trim();
const issStatus = (iss.status || '').toLowerCase().trim();
// Normalisatie helper voor onderwerpteksten (verwijder leestekens, spaties, streepjes voor flexibele matching)
function normalizeText(str) {
if (!str) return '';
return str.toLowerCase().replace(/["']/g, '').replace(/[-_\s]/g, '').trim();
}
// Splits condities op AND (hoofdletterongevoelig)
const conditions = trimmed.split(/\s+AND\s+/i);
for (let rawCond of conditions) {
const cond = rawCond.trim();
if (!cond) continue;
// --- 1. Project condities ---
// project = "SD" of project = SD
const projEqMatch = cond.match(/^project\s*=\s*["']?([A-Z0-9_-]+)["']?$/i);
if (projEqMatch) {
const req = projEqMatch[1].toUpperCase().trim();
if (issPKey) {
if (issPKey !== req) return false;
} else if (!issPName.includes(req.toLowerCase()) && !(req === 'SD' && issPName.includes('servicedesk'))) {
return false;
}
continue;
}
// project != "SD" of project != SD
const projNeqMatch = cond.match(/^project\s*!=\s*["']?([A-Z0-9_-]+)["']?$/i);
if (projNeqMatch) {
const notReq = projNeqMatch[1].toUpperCase().trim();
if (issPKey) {
if (issPKey === notReq) return false;
} else if (issPName.includes(notReq.toLowerCase()) || (notReq === 'SD' && issPName.includes('servicedesk'))) {
return false;
}
continue;
}
// project in ("SD", "IN")
const projInMatch = cond.match(/^project\s+in\s*\(([^)]+)\)$/i);
if (projInMatch) {
const allowed = projInMatch[1].split(',').map(s => s.replace(/["']/g, '').toUpperCase().trim());
if (issPKey) {
if (!allowed.includes(issPKey)) return false;
} else if (!allowed.some(p => issPName.includes(p.toLowerCase()) || (p === 'SD' && issPName.includes('servicedesk')))) {
return false;
}
continue;
}
// project not in ("SD", "IN")
const projNotInMatch = cond.match(/^project\s+not\s+in\s*\(([^)]+)\)$/i);
if (projNotInMatch) {
const disallowed = projNotInMatch[1].split(',').map(s => s.replace(/["']/g, '').toUpperCase().trim());
if (issPKey) {
if (disallowed.includes(issPKey)) return false;
} else if (disallowed.some(p => issPName.includes(p.toLowerCase()) || (p === 'SD' && issPName.includes('servicedesk')))) {
return false;
}
continue;
}
// --- 2. Onderwerp logging condities ---
// bv: "Onderwerp logging[dropdown]" in (Koppelingen, SSO-koppeling, Rest-endpoints, Datadump, UWV-koppeling, Migratie)
const olInMatch = cond.match(/["']?(?:onderwerp(?:[\w\s\[\]_-]*)?|customfield_\d+)["']?\s+in\s*\(([^)]+)\)/i);
if (olInMatch) {
const allowedTopics = olInMatch[1].split(',').map(s => normalizeText(s)).filter(Boolean);
const normOl = normalizeText(issOl);
let matched = false;
if (normOl) {
matched = allowedTopics.some(t => normOl === t || normOl.includes(t) || t.includes(normOl));
}
if (!matched && !normOl) {
// Als onderwerp_logging op het ticket leeg is, kijk in de summary
const normSum = normalizeText(issSum);
matched = allowedTopics.some(t => normSum.includes(t));
}
if (!matched) return false;
continue;
}
// "Onderwerp logging[dropdown]" not in (...)
const olNotInMatch = cond.match(/["']?(?:onderwerp(?:[\w\s\[\]_-]*)?|customfield_\d+)["']?\s+not\s+in\s*\(([^)]+)\)/i);
if (olNotInMatch) {
const forbiddenTopics = olNotInMatch[1].split(',').map(s => normalizeText(s)).filter(Boolean);
const normOl = normalizeText(issOl);
if (normOl && forbiddenTopics.some(t => normOl === t || normOl.includes(t) || t.includes(normOl))) {
return false;
}
continue;
}
// "Onderwerp logging[dropdown]" = "..."
const olEqMatch = cond.match(/["']?(?:onderwerp(?:[\w\s\[\]_-]*)?|customfield_\d+)["']?\s*=\s*["']?([^"'\)]+)["']?/i);
if (olEqMatch) {
const reqTopic = normalizeText(olEqMatch[1]);
const normOl = normalizeText(issOl);
const matched = (normOl === reqTopic || normOl.includes(reqTopic) || reqTopic.includes(normOl) || (!normOl && normalizeText(issSum).includes(reqTopic)));
if (!matched) return false;
continue;
}
// "Onderwerp logging[dropdown]" != "..."
const olNeqMatch = cond.match(/["']?(?:onderwerp(?:[\w\s\[\]_-]*)?|customfield_\d+)["']?\s*!=\s*["']?([^"'\)]+)["']?/i);
if (olNeqMatch) {
const notTopic = normalizeText(olNeqMatch[1]);
const normOl = normalizeText(issOl);
if (normOl && (normOl === notTopic || normOl.includes(notTopic))) {
return false;
}
continue;
}
// --- 3. Prioriteit condities ---
const prioInMatch = cond.match(/^priority\s+in\s*\(([^)]+)\)$/i);
if (prioInMatch) {
const allowed = prioInMatch[1].split(',').map(s => s.replace(/["']/g, '').toLowerCase().trim());
if (!allowed.includes(issPriority)) return false;
continue;
}
const prioEqMatch = cond.match(/^priority\s*=\s*["']?([^"']+)["']?$/i);
if (prioEqMatch) {
const req = prioEqMatch[1].toLowerCase().trim();
if (issPriority !== req) return false;
continue;
}
// --- 4. Type condities ---
const typeInMatch = cond.match(/^(?:issuetype|type)\s+in\s*\(([^)]+)\)$/i);
if (typeInMatch) {
const allowed = typeInMatch[1].split(',').map(s => s.replace(/["']/g, '').toLowerCase().trim());
if (!allowed.includes(issType)) return false;
continue;
}
const typeEqMatch = cond.match(/^(?:issuetype|type)\s*=\s*["']?([^"']+)["']?$/i);
if (typeEqMatch) {
const req = typeEqMatch[1].toLowerCase().trim();
if (issType !== req) return false;
continue;
}
// --- 5. StatusCategory conditie ---
if (/statusCategory\s*!=\s*Done/i.test(cond)) {
if (isDoneStatus(issStatus) || (iss.status_category || '').toLowerCase() === 'done') {
return false;
}
continue;
}
if (/statusCategory\s*=\s*Done/i.test(cond)) {
if (!isDoneStatus(issStatus) && (iss.status_category || '').toLowerCase() !== 'done') {
return false;
}
continue;
}
// --- 6. Status condities ---
const statusInMatch = cond.match(/^status\s+in\s*\(([^)]+)\)$/i);
if (statusInMatch) {
const allowed = statusInMatch[1].split(',').map(s => s.replace(/["']/g, '').toLowerCase().trim());
if (!allowed.includes(issStatus)) return false;
continue;
}
const statusEqMatch = cond.match(/^status\s*=\s*["']?([^"']+)["']?$/i);
if (statusEqMatch) {
const req = statusEqMatch[1].toLowerCase().trim();
if (issStatus !== req) return false;
continue;
}
}
return true;
}
// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort
function isIssueInQueue(iss, qCode) {
if (!qCode) return true;
const code = qCode.toUpperCase().trim();
const pKey = getIssueProjectKey(iss);
const pName = (iss.project || '').toLowerCase().trim();
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
const sum = (iss.summary || '').toLowerCase();
// 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));
}
// 1. Zoek geconfigureerd wachtrijfilter in jiraData.queue_filters (database)
const fObj = (jiraData.queue_filters || []).find(f => f.filter_code && f.filter_code.toUpperCase() === code);
if (fObj) {
// Project key validatie indien expliciet in filter ingesteld
if (fObj.project_key) {
const filterPKey = fObj.project_key.toUpperCase().trim();
if (pKey) {
if (pKey !== filterPKey) return false;
} else if (!pName.includes(filterPKey.toLowerCase())) {
return false;
}
}
// JQL clausule evaluatie
if (fObj.jql_clause && fObj.jql_clause.trim()) {
return matchJqlClause(iss, fObj.jql_clause);
}
// Als er alleen een project_key was zonder nadere jql_clause:
if (fObj.project_key) {
const filterPKey = fObj.project_key.toUpperCase().trim();
return pKey ? (pKey === filterPKey) : pName.includes(filterPKey.toLowerCase());
}
}
// 2. Fallbacks voor standaard wachtrijen (als er geen filter in de database staat)
if (code === 'DX') {
const isSd = pKey ? (pKey === 'SD') : pName.includes('servicedesk');
if (!isSd) return false;
const dxTerms = ['koppeling', 'sso', 'endpoint', 'rest', 'datadump', 'uwv', 'migratie'];
return dxTerms.some(t => ol.includes(t) || sum.includes(t));
}
if (code === 'SD') {
return pKey ? (pKey === 'SD') : pName.includes('servicedesk');
}
if (code === 'IN') {
return pKey ? (pKey === 'IN') : pName.includes('interface');
}
if (code === 'AAN') {
return pKey ? (pKey === 'AAN') : pName.includes('aanvraag');
}
if (code === 'GEN') {
return pKey ? (pKey === 'GEN') : (pName.includes('general') || pName.includes('algemeen'));
}
return true;
}
function selectQueueFilter(code) {
selectProject(code);
}
function updateQueuePills() {
updateProjectPills();
}
function updateProjectCounts() {
const all = getDeduplicatedIssues();
const total = getTotalStatusCount();
let currentList = all;
if (selectedStatuses.size > 0 && selectedStatuses.size < total) {
currentList = currentList.filter(iss => isIssueStatusSelected(iss));
}
const elAll = document.getElementById('projCount_all');
if (elAll) {
let allFiltered = currentList;
if (projectOnlyAssigned.has('ALL')) {
allFiltered = allFiltered.filter(iss => isIssueAssignedToMe(iss));
}
elAll.innerText = allFiltered.length;
}
const containers = document.querySelectorAll('.project-pill-container, .project-pill');
containers.forEach(p => {
const pKey = p.getAttribute('data-project');
if (!pKey) return;
const el = document.getElementById(`projCount_${pKey}`);
if (el) {
let pList = currentList.filter(iss => isIssueInQueue(iss, pKey));
if (projectOnlyAssigned.has(pKey.toUpperCase()) || projectOnlyAssigned.has('ALL')) {
pList = pList.filter(iss => isIssueAssignedToMe(iss));
}
el.innerText = pList.length;
}
});
}
function updateQueueCounts() {
updateProjectCounts();
}
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 = '';
projectOnlyAssigned.clear();
try {
localStorage.removeItem('fociz_jira_project_only_assigned');
} catch (e) {}
closeAllProjectStatusDropdowns();
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();
updateProjectStatusDropdownVisibility();
updateAssignedPillState();
updateStatusCounts();
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 = getIssueProjectKey(iss);
const issPName = (iss.project || '').toLowerCase().trim();
// 1. Status Filter via Checkboxes
const totalStatuses = getTotalStatusCount();
if (selectedStatuses.size > 0 && selectedStatuses.size < totalStatuses) {
if (!isIssueStatusSelected(iss)) {
return false;
}
}
// 2. Project Filter (SD, GEN, IN, AAN, DX of ander)
if (projectVal) {
if (!isIssueInQueue(iss, projectVal)) {
return false;
}
}
// 3. Toegewezen aan mij (uitvoerder) per project filter
const isAssignedFilterActive = isProjectOnlyAssignedActive(projectVal, issPKey);
if (isAssignedFilterActive && !isIssueAssignedToMe(iss)) {
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 = getIssueProjectKey(ticket);
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' || (!pKey && 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<span class="hidden sm:inline"> • Servicedesk</span>';
} else if (pKey === 'IN' || (!pKey && 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<span class="hidden sm:inline"> • Interfaces</span>';
} else if (pKey === 'AAN' || (!pKey && 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<span class="hidden sm:inline"> • Aanvragen</span>';
} else if (pKey === 'GEN' || (!pKey && (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<span class="hidden sm:inline"> • General</span>';
}
// 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-1.5 sm: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="${sanitizeUrl(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 break-words" 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-3 sm: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 flex-wrap sm:flex-nowrap items-center gap-1.5 sm:gap-2 pt-2 lg:pt-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-1.5 sm: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 sm:space-x-1.5 px-2.5 sm:px-3 py-1.5 sm: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<span class="hidden sm:inline"> in Dagplanner</span></span>
</button>
<button onclick="openBoardModal('${escapeHtml(ticket.key)}')" class="flex items-center space-x-1 sm:space-x-1.5 px-2.5 sm:px-3 py-1.5 sm: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><span class="hidden sm:inline">Naar </span>Bord</span>
</button>
<a href="${sanitizeUrl(ticket.url)}" target="_blank" rel="noopener noreferrer" title="Bekijk in Jira" class="p-1.5 sm: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-3.5 sm: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 break-words overflow-x-hidden 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 flex-wrap items-center gap-1.5 sm:gap-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>Kopieer<span class="hidden sm:inline"> Sleutel</span></span>
</button>
<a href="${sanitizeUrl(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>Jira<span class="hidden sm:inline"> Cloud</span></span>
</a>
</div>
<div class="flex flex-wrap items-center gap-1.5 sm:gap-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<span class="hidden sm:inline"> in Dagplanner</span></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><span class="hidden sm:inline">Naar </span>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;');
}
// Helper voor veilige URL sanitization (voorkomt javascript: of onveilige protocols)
function sanitizeUrl(url) {
if (!url) return '#';
const s = String(url).trim();
if (/^https?:\/\//i.test(s)) return escapeHtml(s);
return '#';
}
// 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);
}
initStatusSelection();
calculateKpis();
updateProjectPills();
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'; ?>