Files
Fociz/jira.php
T

1685 lines
92 KiB
PHP

<?php
/**
* Fociz — Atlassian Jira Hub
* Toont tickets toegewezen aan de gebruiker en project-/teamtickets.
* Biedt filters, zoekfunctie, KPI-statistieken en 1-klik import naar de Dagplanner en het Kanban bord.
*/
$pageTitle = 'Fociz — Jira Hub';
require_once __DIR__ . '/includes/header.php';
require_once __DIR__ . '/includes/integrations/jira.php';
$userId = (int)($currentUser['id'] ?? 0);
$isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$forceRefresh = isset($_GET['refresh']) && $_GET['refresh'] === '1';
// Haal Jira data op
$jiraData = jira_get_dashboard_data($userId, $forceRefresh);
$isDemo = !empty($jiraData['is_demo']);
$isLive = !empty($jiraData['success']) && !$isDemo;
$domain = $jiraData['domain'] ?? '';
$email = $jiraData['email'] ?? '';
$accountName = $jiraData['account_name'] ?? '';
$lastSync = $jiraData['last_sync_at'] ?? date('Y-m-d H:i:s');
$syncError = $jiraData['error'] ?? null;
$assignedIssues = $jiraData['assigned_issues'] ?? [];
$teamIssues = $jiraData['team_issues'] ?? [];
// Haal borden en kolommen op voor het "Naar Bord" modal
$db = get_db_connection();
if ($isAdmin) {
$boards = $db->query("SELECT id, title FROM boards WHERE is_archived = 0 ORDER BY id ASC")->fetchAll();
} else {
$stmt = $db->prepare("
SELECT b.id, b.title
FROM boards b
JOIN board_members bm ON b.id = bm.board_id
WHERE bm.user_id = ? AND b.is_archived = 0
ORDER BY b.id ASC
");
$stmt->execute([$userId]);
$boards = $stmt->fetchAll();
}
$boardIds = array_column($boards, 'id');
$lanesByBoard = [];
if (!empty($boardIds)) {
$inClause = implode(',', array_map('intval', $boardIds));
$lanes = $db->query("SELECT id, board_id, title, color FROM board_lanes WHERE board_id IN ({$inClause}) ORDER BY position ASC")->fetchAll();
foreach ($lanes as $lane) {
$lanesByBoard[$lane['board_id']][] = $lane;
}
}
foreach ($boards as &$b) {
$b['lanes'] = $lanesByBoard[$b['id']] ?? [];
}
unset($b);
// Verzamel unieke projecten en types voor filter dropdowns en project switcher
$allIssues = array_merge($assignedIssues, $teamIssues);
$allProjects = [];
$allTypes = [];
$allPriorities = ['Highest', 'High', 'Medium', 'Low'];
$allStatuses = [];
$projectCounts = [];
foreach ($allIssues as $iss) {
if (!empty($iss['project'])) {
$p = $iss['project'];
$allProjects[$p] = [
'name' => $p,
'key' => $iss['project_key'] ?? '',
];
$projectCounts[$p] = ($projectCounts[$p] ?? 0) + 1;
}
if (!empty($iss['issue_type'])) {
$allTypes[$iss['issue_type']] = $iss['issue_type'];
}
if (!empty($iss['status'])) {
$allStatuses[$iss['status']] = $iss['status'];
}
}
ksort($allProjects);
ksort($allTypes);
ksort($allStatuses);
$queueFilters = $jiraData['queue_filters'] ?? jira_get_queue_filters(true);
$defaultQueueFilter = $jiraData['default_queue_filter'] ?? jira_get_user_queue_preference($userId);
$jiraDataJson = json_encode($jiraData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
?>
<div class="space-y-6 w-full">
<!-- Bovenbalk: Titel, Jira status & Actieknoppen -->
<div class="fociz-card p-5 sm:p-6">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<!-- Linkerkant: Titel & Atlassian Jira branding -->
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-tr from-blue-600 via-indigo-600 to-sky-500 text-white flex items-center justify-center shadow-md shadow-indigo-500/20 flex-shrink-0">
<i data-lucide="layers" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2.5">
<h1 class="text-2xl font-black text-slate-900 dark:text-white tracking-tight">Atlassian Jira Hub</h1>
<?php if ($isLive): ?>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5 animate-pulse"></span>
Live Verbonden
</span>
<?php else: ?>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-300 dark:border-amber-800/80">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 mr-1.5"></span>
Demomodus
</span>
<?php endif; ?>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">
Beheer tickets toegewezen aan jou en je projectteams. Plan ze direct in je dagplanning of zet ze op een Kanban bord.
</p>
</div>
</div>
<!-- Rechterkant: Knoppen Vernieuwen & Instellingen -->
<div class="flex items-center space-x-2.5 self-start md:self-center">
<button onclick="refreshJiraData()" id="refreshBtn" class="flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-semibold bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-700 transition cursor-pointer shadow-2xs">
<i data-lucide="refresh-cw" id="refreshIcon" class="w-3.5 h-3.5"></i>
<span>Vernieuwen</span>
</button>
<a href="/profile.php#integraties" class="flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-semibold bg-indigo-600 hover:bg-indigo-700 text-white transition cursor-pointer shadow-sm shadow-indigo-600/20">
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
<span>Jira Instellingen</span>
</a>
</div>
</div>
<?php if ($isDemo): ?>
<!-- Demomodus Informatiebanner -->
<div class="mt-5 p-4 rounded-xl bg-gradient-to-r from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/20 border border-amber-200/90 dark:border-amber-800/50 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-lg bg-amber-500/15 text-amber-600 dark:text-amber-400 flex items-center justify-center flex-shrink-0 mt-0.5">
<i data-lucide="info" class="w-4 h-4"></i>
</div>
<div>
<p class="text-xs font-bold text-amber-900 dark:text-amber-200">
Interactieve Voorbeeldweergave (Sandbox)
</p>
<p class="text-xs text-amber-700 dark:text-amber-300/80 mt-0.5">
Je bekijkt momenteel demotickets. Koppel je Atlassian Jira Cloud instantie in je profiel om direct de live tickets van jouw organisatie in te laden.
</p>
</div>
</div>
<a href="/profile.php#integraties" class="px-3.5 py-1.5 rounded-lg bg-amber-600 hover:bg-amber-700 text-white font-bold text-xs shadow-xs transition flex-shrink-0 cursor-pointer">
Koppel Jira Cloud
</a>
</div>
<?php endif; ?>
<?php if (!empty($syncError)): ?>
<!-- Synchronisatiefout Banner -->
<div class="mt-4 p-3 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900/50 flex items-center space-x-3 text-xs text-red-700 dark:text-red-300">
<i data-lucide="alert-triangle" class="w-4 h-4 text-red-500 flex-shrink-0"></i>
<div class="flex-1">
<strong>Synchronisatiefout:</strong> <?= htmlspecialchars($syncError) ?>
</div>
<a href="/profile.php#integraties" class="underline hover:no-underline font-semibold">Controleer instellingen</a>
</div>
<?php endif; ?>
<!-- Hub Voettekst: Live Status & Filters Uitklapknop (Rechtsonder) -->
<div class="mt-4 pt-3 border-t border-slate-100 dark:border-slate-800/60 flex flex-wrap items-center justify-between text-xs text-slate-500 dark:text-slate-400 gap-2">
<div class="flex items-center space-x-2">
<?php if ($isLive): ?>
<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-emerald-500"></i>
<span>Verbonden met <strong class="text-slate-700 dark:text-slate-200"><?= htmlspecialchars($domain) ?></strong> als <strong class="text-slate-700 dark:text-slate-200"><?= htmlspecialchars($accountName ?: $email) ?></strong></span>
<span class="text-slate-300 dark:text-slate-700 hidden sm:inline">•</span>
<span class="text-[11px] text-slate-400 hidden sm:inline">Sync: <?= date('d-m-Y H:i', strtotime($lastSync)) ?></span>
<?php elseif ($isDemo): ?>
<i data-lucide="database" class="w-3.5 h-3.5 text-amber-500"></i>
<span>Demodataset actief met voorbeeldtickets</span>
<?php endif; ?>
</div>
<!-- Uitklapknop in de rechter onderhoek -->
<div class="flex items-center space-x-2 ml-auto">
<button type="button"
onclick="toggleJiraFiltersDrawer()"
id="btnToggleFiltersDrawer"
class="flex items-center space-x-2 px-3 py-1.5 rounded-xl text-xs font-semibold bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-700 transition cursor-pointer shadow-2xs group">
<i data-lucide="sliders-horizontal" class="w-3.5 h-3.5 text-indigo-500 group-hover:rotate-12 transition-transform"></i>
<span>Filters & Statistieken</span>
<span id="activeFilterBadge" class="hidden px-1.5 py-0.2 rounded-full text-[10px] font-black bg-indigo-600 text-white leading-tight">0</span>
<i data-lucide="chevron-down" id="filtersDrawerChevron" class="w-3.5 h-3.5 transition-transform duration-200"></i>
</button>
</div>
</div>
<!-- Uitklapbare Filters & Statistieken Drawer -->
<div id="jiraFiltersDrawer" class="hidden mt-4 pt-4 border-t border-slate-100 dark:border-slate-800/80 space-y-4">
<!-- 1. 4 KPI Statistieken Kaarten -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div onclick="switchTab('assigned')" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Aan mij toegewezen</span>
<div class="w-7 h-7 rounded-lg bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 flex items-center justify-center transition group-hover:scale-110">
<i data-lucide="user-check" class="w-3.5 h-3.5"></i>
</div>
</div>
<div class="mt-2 flex items-baseline space-x-2">
<span id="statAssignedCount" class="text-xl font-black text-slate-900 dark:text-white"><?= count($assignedIssues) ?></span>
<span class="text-[11px] text-slate-400 font-medium">persoonlijke taken</span>
</div>
</div>
<div onclick="switchTab('team')" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Team & Projecten</span>
<div class="w-7 h-7 rounded-lg bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 flex items-center justify-center transition group-hover:scale-110">
<i data-lucide="users" class="w-3.5 h-3.5"></i>
</div>
</div>
<div class="mt-2 flex items-baseline space-x-2">
<span id="statTeamCount" class="text-xl font-black text-slate-900 dark:text-white"><?= count($teamIssues) ?></span>
<span class="text-[11px] text-slate-400 font-medium">in actieve projecten</span>
</div>
</div>
<div onclick="switchTab('in_progress')" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">In Uitvoering</span>
<div class="w-7 h-7 rounded-lg bg-amber-50 dark:bg-amber-950/40 text-amber-600 dark:text-amber-400 flex items-center justify-center transition group-hover:scale-110">
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
</div>
</div>
<div class="mt-2 flex items-baseline space-x-2">
<span id="statInProgressCount" class="text-xl font-black text-slate-900 dark:text-white">...</span>
<span class="text-[11px] text-slate-400 font-medium">actief mee bezig</span>
</div>
</div>
<div onclick="filterByUrgent()" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Hoge Prioriteit / Urgent</span>
<div class="w-7 h-7 rounded-lg bg-rose-50 dark:bg-rose-950/40 text-rose-600 dark:text-rose-400 flex items-center justify-center transition group-hover:scale-110">
<i data-lucide="alert-circle" class="w-3.5 h-3.5"></i>
</div>
</div>
<div class="mt-2 flex items-baseline space-x-2">
<span id="statHighPriorityCount" class="text-xl font-black text-slate-900 dark:text-white">...</span>
<span class="text-[11px] text-slate-400 font-medium">Highest / High</span>
</div>
</div>
</div>
<!-- 2. Snelle Project Switcher (Pill Tabs) -->
<div class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/30 border border-slate-200/70 dark:border-slate-800">
<div class="flex items-center justify-between pb-2 mb-2 border-b border-slate-200/60 dark:border-slate-800">
<div class="flex items-center space-x-2">
<i data-lucide="folder-kanban" class="w-3.5 h-3.5 text-indigo-500"></i>
<span class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">Jira Projecten</span>
</div>
<span class="text-[11px] text-slate-400"><?= count($allProjects) ?> projecten</span>
</div>
<div class="flex items-center gap-2 overflow-x-auto pb-1" id="projectPillsContainer">
<button type="button" onclick="selectProject('')" data-project="" class="project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs flex-shrink-0">
<i data-lucide="layers" class="w-3 h-3"></i>
<span>Alle Projecten</span>
<span class="px-1.5 py-0.5 rounded-full text-[10px] bg-white/20 text-white font-black"><?= count($allIssues) ?></span>
</button>
<?php foreach ($allProjects as $pName => $pData): ?>
<button type="button" onclick="selectProject('<?= htmlspecialchars(addslashes($pName)) ?>')" data-project="<?= htmlspecialchars($pName) ?>" class="project-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200/70 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0">
<?php if (!empty($pData['key'])): ?>
<span class="px-1 py-0.5 rounded text-[10px] font-mono font-black bg-indigo-100 dark:bg-indigo-950/80 text-indigo-700 dark:text-indigo-300"><?= htmlspecialchars($pData['key']) ?></span>
<?php else: ?>
<i data-lucide="folder" class="w-3 h-3 text-slate-400"></i>
<?php endif; ?>
<span><?= htmlspecialchars($pName) ?></span>
<span class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300"><?= $projectCounts[$pName] ?? 0 ?></span>
</button>
<?php endforeach; ?>
</div>
</div>
<!-- 3. Servicedesk Wachtrijfilter Sub-balk (DX / SD) -->
<div id="queueFiltersBar" class="p-3.5 rounded-xl bg-gradient-to-r from-slate-50 via-indigo-50/20 to-sky-50/10 dark:from-slate-900 dark:via-indigo-950/20 dark:to-sky-950/10 border border-indigo-100 dark:border-indigo-900/40">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-2.5">
<div class="flex items-center space-x-2">
<i data-lucide="filter" class="w-3.5 h-3.5 text-indigo-500"></i>
<span class="text-xs font-bold text-slate-900 dark:text-white">Servicedesk Wachtrijen</span>
<?php if (!empty($defaultQueueFilter)): ?>
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-800 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/80">
Jouw voorkeur: <?= htmlspecialchars($defaultQueueFilter) ?>
</span>
<?php endif; ?>
</div>
<div class="flex items-center gap-1.5 overflow-x-auto" id="queueButtonsContainer">
<button type="button" onclick="selectQueueFilter('')" data-queue="" class="queue-pill flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300 flex-shrink-0">
<i data-lucide="list" class="w-3 h-3"></i>
<span>Alle Wachtrijen</span>
<span id="queueCount_all" class="px-1.5 py-0.5 rounded-full text-[10px] bg-slate-300 dark:bg-slate-700 font-bold">0</span>
</button>
<button type="button" onclick="selectQueueFilter('DX')" data-queue="DX" class="queue-pill flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0">
<i data-lucide="zap" class="w-3 h-3 text-amber-500"></i>
<span>DX (Koppelingen)</span>
<span id="queueCount_DX" class="px-1.5 py-0.5 rounded-full text-[10px] bg-amber-100 dark:bg-amber-950/60 text-amber-800 dark:text-amber-300 font-bold">0</span>
</button>
<button type="button" onclick="selectQueueFilter('SD')" data-queue="SD" class="queue-pill flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0">
<i data-lucide="wrench" class="w-3 h-3 text-sky-500"></i>
<span>SD (Regulier)</span>
<span id="queueCount_SD" class="px-1.5 py-0.5 rounded-full text-[10px] bg-sky-100 dark:bg-sky-950/60 text-sky-800 dark:text-sky-300 font-bold">0</span>
</button>
<?php foreach ($queueFilters as $qf):
$code = $qf['filter_code'] ?? '';
if ($code === 'DX' || $code === 'SD' || empty($code)) continue;
?>
<button type="button" onclick="selectQueueFilter('<?= htmlspecialchars($code) ?>')" data-queue="<?= htmlspecialchars($code) ?>" class="queue-pill flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-white dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 flex-shrink-0">
<i data-lucide="filter" class="w-3 h-3 text-indigo-500"></i>
<span><?= htmlspecialchars($qf['filter_name'] ?? $code) ?></span>
<span id="queueCount_<?= htmlspecialchars($code) ?>" class="px-1.5 py-0.5 rounded-full text-[10px] bg-indigo-100 dark:bg-indigo-950/60 text-indigo-800 dark:text-indigo-300 font-bold">0</span>
</button>
<?php endforeach; ?>
<?php if ($isAdmin): ?>
<a href="/admin.php#jira-wachtrijen" title="Wachtrijfilters beheren in Admin" class="p-1 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-200/60 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0">
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
</a>
<?php endif; ?>
</div>
</div>
</div>
<!-- 4. Gedetailleerde 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-2 lg:grid-cols-5 gap-2.5">
<div>
<label class="block text-[11px] font-semibold text-slate-500 dark:text-slate-400 mb-1">Project</label>
<select id="filterProject" onchange="onProjectSelectChange(this.value)" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
<option value="">Alle projecten</option>
<?php foreach ($allProjects as $pName => $pData): ?>
<option value="<?= htmlspecialchars($pName) ?>"><?= htmlspecialchars($pName) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="block text-[11px] font-semibold text-slate-500 dark:text-slate-400 mb-1">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>
<label class="block text-[11px] font-semibold text-slate-500 dark:text-slate-400 mb-1">Status</label>
<select id="filterStatus" onchange="applyFilters()" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
<option value="">Alle statussen</option>
<?php foreach ($allStatuses as $s): ?>
<option value="<?= htmlspecialchars($s) ?>"><?= htmlspecialchars($s) ?></option>
<?php endforeach; ?>
</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 Filters</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Compacte Werkbalk: Tabs & Zoekveld direct boven de tickets -->
<div class="fociz-card p-3 sm:p-4 flex flex-col md:flex-row md:items-center justify-between gap-3 shadow-xs">
<!-- Tabs Navigatie -->
<div class="flex items-center space-x-1 sm:space-x-1.5 overflow-x-auto pb-1 md:pb-0">
<button onclick="switchTab('assigned')" id="tabBtn_assigned" class="flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800/60 flex-shrink-0">
<i data-lucide="user-check" class="w-3.5 h-3.5"></i>
<span>Aan mij toegewezen</span>
<span id="tabCount_assigned" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-200/70 dark:bg-indigo-800/60 text-indigo-800 dark:text-indigo-200">
<?= count($assignedIssues) ?>
</span>
</button>
<button onclick="switchTab('team')" id="tabBtn_team" class="flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
<i data-lucide="users" class="w-3.5 h-3.5"></i>
<span>Teams & Projecten</span>
<span id="tabCount_team" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
<?= count($teamIssues) ?>
</span>
</button>
<button onclick="switchTab('in_progress')" id="tabBtn_in_progress" class="flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
<span>In Uitvoering</span>
<span id="tabCount_in_progress" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
0
</span>
</button>
<button onclick="switchTab('done')" id="tabBtn_done" class="flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
<span>Recent Afgerond</span>
<span id="tabCount_done" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
0
</span>
</button>
</div>
<!-- Rechterkant: Zoekveld & Tickets Tellen -->
<div class="flex items-center space-x-2.5 w-full md:w-auto">
<div class="relative flex-1 md:w-72">
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none"></i>
<input
type="text"
id="jiraSearchInput"
placeholder="Zoek ticket (FOC-101) of titel..."
oninput="applyFilters()"
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl pl-8 pr-7 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 transition placeholder-slate-400"
>
<button onclick="clearSearch()" id="clearSearchBtn" class="hidden absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</div>
<div class="text-[11px] text-slate-400 whitespace-nowrap hidden sm:block">
<span id="renderedCount" class="font-bold text-slate-700 dark:text-slate-200">0</span> tickets
</div>
</div>
</div>
<!-- Tickets Container -->
<div id="ticketsContainer" class="space-y-3">
<!-- Wordt dynamisch gerenderd via JavaScript -->
</div>
<!-- Lege Toestand (No Results) -->
<div id="emptyStateContainer" class="hidden fociz-card p-12 text-center">
<div class="w-12 h-12 mx-auto rounded-2xl bg-slate-100 dark:bg-slate-800 text-slate-400 flex items-center justify-center mb-3">
<i data-lucide="inbox" class="w-6 h-6"></i>
</div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Geen tickets gevonden</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1 max-w-sm mx-auto">
Er zijn geen Jira tickets die overeenkomen met de geselecteerde tab of zoekfilters.
</p>
<button onclick="resetAllFilters()" class="mt-4 px-4 py-2 rounded-xl text-xs font-semibold bg-indigo-600 hover:bg-indigo-700 text-white transition cursor-pointer">
Filters Wissen
</button>
</div>
</div>
<!-- MODAL 1: Inplannen in Dagplanner (planner.php) -->
<div id="planTaskModal" 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-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</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 -->
<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="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>
</div>
<div id="planTicketSummaryText" class="text-xs font-semibold text-slate-800 dark:text-slate-200 mt-1 line-clamp-2">
Tweestapsverificatie implementeren
</div>
</div>
<!-- Datum Kiezer -->
<div>
<label class="block text-xs font-bold 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-2 gap-3">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Starttijd (optioneel)</label>
<input
type="time"
id="planStartTimeInput"
value="09:00"
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>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Verwachte Duur</label>
<select id="planDurationInput" 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="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 2: Toevoegen aan Kanban Bord (board.php) -->
<div id="boardTaskModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl w-full max-w-md shadow-2xl p-6 transition transform">
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-lg bg-emerald-50 dark:bg-emerald-950/50 text-emerald-600 dark:text-emerald-400 flex items-center justify-center">
<i data-lucide="kanban" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Plaats op Kanban Bord</h3>
<p class="text-[11px] text-slate-400">Maak een post-it kaart aan</p>
</div>
</div>
<button onclick="closeBoardModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1.5 rounded-lg transition cursor-pointer">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<form id="boardTaskForm" onsubmit="submitBoardTask(event)" class="mt-4 space-y-4">
<input type="hidden" id="boardIssueKey" value="">
<input type="hidden" id="boardIssueSummary" value="">
<input type="hidden" id="boardIssueUrl" value="">
<input type="hidden" id="boardIssueProject" value="">
<!-- Ticket Info Preview -->
<div class="p-3 rounded-xl bg-slate-50 dark:bg-slate-950 border border-slate-200/80 dark:border-slate-800/80">
<div class="flex items-center space-x-2">
<span id="boardTicketKeyBadge" class="font-mono text-xs font-bold text-emerald-600 dark:text-emerald-400">FOC-101</span>
<span id="boardTicketProject" class="text-[11px] text-slate-400">• Fociz Platform</span>
</div>
<div id="boardTicketSummaryText" class="text-xs font-semibold text-slate-800 dark:text-slate-200 mt-1 line-clamp-2">
Tweestapsverificatie implementeren
</div>
</div>
<!-- Bord Selectie -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Kies Kanban Bord</label>
<select id="boardSelectInput" onchange="updateLaneOptions()" required class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500/40 cursor-pointer">
<?php if (empty($boards)): ?>
<option value="">Geen borden beschikbaar</option>
<?php else: ?>
<?php foreach ($boards as $b): ?>
<option value="<?= (int)$b['id'] ?>"><?= htmlspecialchars($b['title']) ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<!-- Kolom / Lane Selectie -->
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Kies Kolom / Fase</label>
<select id="laneSelectInput" required class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500/40 cursor-pointer">
<!-- Wordt dynamisch gevuld -->
</select>
</div>
<!-- Kaartkleur & Prioriteit -->
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Kaartkleur</label>
<select id="cardColorInput" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500/40 cursor-pointer">
<option value="blue" selected>Blauw (Standaard)</option>
<option value="yellow">Geel</option>
<option value="green">Groen</option>
<option value="purple">Paars</option>
<option value="red">Rood</option>
<option value="pink">Roze</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
<select id="cardPriorityInput" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-emerald-500/40 cursor-pointer">
<option value="P1">P1 • Hoog</option>
<option value="P2" selected>P2 • Normaal</option>
<option value="P3">P3 • Laag</option>
</select>
</div>
</div>
<!-- Knoppen -->
<div class="flex items-center justify-end space-x-2 pt-3 border-t border-slate-100 dark:border-slate-800">
<button type="button" onclick="closeBoardModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
Annuleren
</button>
<button type="submit" id="boardSubmitBtn" class="flex items-center space-x-1.5 px-4 py-2 rounded-xl text-xs font-bold bg-emerald-600 hover:bg-emerald-700 text-white transition cursor-pointer shadow-sm shadow-emerald-600/20">
<i data-lucide="plus-circle" class="w-3.5 h-3.5"></i>
<span>Plaats op Bord</span>
</button>
</div>
</form>
</div>
</div>
<!-- Toast Notificatie Element -->
<div id="jiraToast" class="fixed bottom-6 right-6 z-50 transform translate-y-24 opacity-0 pointer-events-none transition-all duration-300 flex items-center space-x-3 p-4 rounded-2xl bg-slate-900/95 dark:bg-white text-white dark:text-slate-900 shadow-2xl border border-slate-800 dark:border-slate-200 max-w-md">
<div id="toastIconWrapper" class="w-8 h-8 rounded-xl bg-emerald-500/20 text-emerald-400 dark:text-emerald-600 flex items-center justify-center flex-shrink-0">
<i data-lucide="check" class="w-4 h-4"></i>
</div>
<div class="flex-1 text-xs">
<p id="toastMessage" class="font-bold">Ticket succesvol verwerkt</p>
<p id="toastSubMessage" class="text-slate-400 dark:text-slate-600 text-[11px] mt-0.5"></p>
</div>
<a id="toastActionLink" href="#" class="hidden px-2.5 py-1 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-[11px] font-bold transition flex-shrink-0">
Bekijken
</a>
</div>
<script>
// Data vanuit PHP
const jiraData = <?= $jiraDataJson ?>;
const availableBoards = <?= $boardsJson ?>;
let currentTab = 'assigned'; // 'assigned', 'team', 'in_progress', 'done'
let currentProject = '';
let currentQueue = jiraData.default_queue_filter || '';
let activeUrgentOnly = false;
let expandedKeys = new Set();
const ticketStore = new Map();
// Bereken statussen en KPI statistieken
function calculateKpis() {
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const all = [...assigned, ...team];
let inProgressCount = 0;
let highPriorityCount = 0;
let doneCount = 0;
all.forEach(iss => {
const s = (iss.status || '').toLowerCase();
const sc = (iss.status_category || '').toLowerCase();
const p = (iss.priority || '').toLowerCase();
if (s.includes('progress') || s.includes('bezig') || s.includes('review') || sc === 'indeterminate') {
inProgressCount++;
}
if (s.includes('done') || s.includes('afgerond') || s.includes('gesloten') || sc === 'done') {
doneCount++;
}
if (p === 'highest' || p === 'high') {
highPriorityCount++;
}
});
document.getElementById('statAssignedCount').innerText = assigned.length;
document.getElementById('statTeamCount').innerText = team.length;
document.getElementById('statInProgressCount').innerText = inProgressCount;
document.getElementById('statHighPriorityCount').innerText = highPriorityCount;
document.getElementById('tabCount_assigned').innerText = assigned.length;
document.getElementById('tabCount_team').innerText = team.length;
document.getElementById('tabCount_in_progress').innerText = inProgressCount;
document.getElementById('tabCount_done').innerText = doneCount;
}
// Uitklappen en inklappen van de filters & statistieken drawer
function toggleJiraFiltersDrawer(forceOpen) {
const drawer = document.getElementById('jiraFiltersDrawer');
const chevron = document.getElementById('filtersDrawerChevron');
if (!drawer) return;
const willOpen = (typeof forceOpen === 'boolean') ? forceOpen : drawer.classList.contains('hidden');
if (willOpen) {
drawer.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
localStorage.setItem('fociz_jira_filters_open', '1');
} else {
drawer.classList.add('hidden');
if (chevron) chevron.classList.remove('rotate-180');
localStorage.setItem('fociz_jira_filters_open', '0');
}
}
// Bepaal aantal actieve filters voor de badge in de rechter onderhoek
function updateActiveFilterBadge() {
const badge = document.getElementById('activeFilterBadge');
if (!badge) return;
let activeCount = 0;
if (currentProject) activeCount++;
if (currentQueue) activeCount++;
if (document.getElementById('filterType')?.value) activeCount++;
if (document.getElementById('filterPriority')?.value) activeCount++;
if (document.getElementById('filterStatus')?.value) activeCount++;
if (activeCount > 0) {
badge.innerText = activeCount;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
// Wissel tussen tabs
function switchTab(tab) {
currentTab = tab;
activeUrgentOnly = false;
// Update tab knoppen styling
const tabs = ['assigned', 'team', 'in_progress', 'done'];
tabs.forEach(t => {
const btn = document.getElementById(`tabBtn_${t}`);
if (!btn) return;
if (t === tab) {
btn.className = "flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800/60 flex-shrink-0";
} else {
btn.className = "flex items-center space-x-1.5 px-3.5 py-2 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0";
}
});
updateQueueCounts();
applyFilters();
}
// Project selectie via pill tabs
function selectProject(proj) {
currentProject = proj;
const pSelect = document.getElementById('filterProject');
if (pSelect) {
pSelect.value = proj;
}
updateProjectPills();
updateQueueCounts();
applyFilters();
}
// Project selectie via dropdown
function onProjectSelectChange(proj) {
currentProject = proj;
updateProjectPills();
updateQueueCounts();
applyFilters();
}
function updateProjectPills() {
const pills = document.querySelectorAll('.project-pill');
const curNorm = (currentProject || '').trim().toLowerCase();
pills.forEach(p => {
const pKey = (p.getAttribute('data-project') || '').trim().toLowerCase();
if (pKey === curNorm) {
p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0";
} else {
p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0";
}
});
}
// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort (DX, SD of aangepast)
function isIssueInQueue(iss, qCode) {
if (!qCode) return true;
const code = qCode.toUpperCase();
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
const sum = (iss.summary || '').toLowerCase();
const pKey = (iss.project_key || '').toUpperCase().trim();
const dxTerms = ['koppeling', 'sso', 'endpoint', 'rest', 'datadump', 'uwv', 'migratie', 'api', 'webhook', 'integrat'];
const matchesDx = dxTerms.some(t => ol.includes(t) || sum.includes(t));
if (code === 'DX') {
return matchesDx;
}
if (code === 'SD') {
// SD is regulier Servicedesk (alle tickets die niet tot DX routering behoren)
return !matchesDx;
}
// Aangepaste wachtrijfilter uit de database
const fObj = (jiraData.queue_filters || []).find(f => f.filter_code.toUpperCase() === code);
if (fObj) {
if (fObj.project_key && pKey && fObj.project_key.toUpperCase() !== pKey) {
return false;
}
const clause = (fObj.jql_clause || '').toLowerCase();
if (ol && (clause.includes(ol) || ol.includes(code.toLowerCase()))) {
return true;
}
if (clause.includes('koppeling') && matchesDx) return true;
}
return true;
}
// Wachtrijfilter selectie
function selectQueueFilter(code) {
currentQueue = code;
updateQueuePills();
applyFilters();
}
function updateQueuePills() {
const pills = document.querySelectorAll('.queue-pill');
pills.forEach(p => {
const qCode = p.getAttribute('data-queue') || '';
if (qCode === currentQueue) {
p.className = "queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0";
} else {
p.className = "queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0";
}
});
}
// Update de tellers op de DX / SD wachtrijfilter knoppen
function updateQueueCounts() {
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const all = [...assigned, ...team];
let currentList = all;
if (currentTab === 'assigned') currentList = assigned;
else if (currentTab === 'team') currentList = team;
else if (currentTab === 'in_progress') {
currentList = all.filter(iss => {
const s = (iss.status || '').toLowerCase();
const sc = (iss.status_category || '').toLowerCase();
return s.includes('progress') || s.includes('bezig') || s.includes('review') || sc === 'indeterminate';
});
} else if (currentTab === 'done') {
currentList = all.filter(iss => {
const s = (iss.status || '').toLowerCase();
const sc = (iss.status_category || '').toLowerCase();
return s.includes('done') || s.includes('afgerond') || s.includes('gesloten') || sc === 'done';
});
}
if (currentProject) {
const pNorm = currentProject.trim().toLowerCase();
currentList = currentList.filter(iss => {
const pName = (iss.project || '').trim().toLowerCase();
const pKey = (iss.project_key || '').trim().toLowerCase();
return pName === pNorm || pKey === pNorm;
});
}
let dxCount = 0;
let sdCount = 0;
currentList.forEach(iss => {
if (isIssueInQueue(iss, 'DX')) {
dxCount++;
} else {
sdCount++;
}
});
const elAll = document.getElementById('queueCount_all');
const elDx = document.getElementById('queueCount_DX');
const elSd = document.getElementById('queueCount_SD');
if (elAll) elAll.innerText = currentList.length;
if (elDx) elDx.innerText = dxCount;
if (elSd) elSd.innerText = sdCount;
(jiraData.queue_filters || []).forEach(f => {
const el = document.getElementById(`queueCount_${f.filter_code}`);
if (el && f.filter_code !== 'DX' && f.filter_code !== 'SD') {
const cnt = currentList.filter(iss => isIssueInQueue(iss, f.filter_code)).length;
el.innerText = cnt;
}
});
}
// Snelkoppeling KPI-kaart Urgent
function filterByUrgent() {
const pSelect = document.getElementById('filterPriority');
if (pSelect) {
pSelect.value = 'High';
}
applyFilters();
}
// Zoekopdracht wissen
function clearSearch() {
const input = document.getElementById('jiraSearchInput');
if (input) {
input.value = '';
}
document.getElementById('clearSearchBtn').classList.add('hidden');
applyFilters();
}
// Alle filters herstellen
function resetAllFilters() {
currentProject = '';
currentQueue = '';
const searchInput = document.getElementById('jiraSearchInput');
const projectSelect = document.getElementById('filterProject');
const typeSelect = document.getElementById('filterType');
const prioritySelect = document.getElementById('filterPriority');
const statusSelect = document.getElementById('filterStatus');
if (searchInput) searchInput.value = '';
if (projectSelect) projectSelect.value = '';
if (typeSelect) typeSelect.value = '';
if (prioritySelect) prioritySelect.value = '';
if (statusSelect) statusSelect.value = '';
document.getElementById('clearSearchBtn')?.classList.add('hidden');
updateProjectPills();
updateQueuePills();
updateQueueCounts();
applyFilters();
}
// Filter en render logic
function applyFilters() {
const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase();
const projectVal = currentProject || (document.getElementById('filterProject')?.value || '');
const typeVal = (document.getElementById('filterType')?.value || '').trim().toLowerCase();
const priorityVal = (document.getElementById('filterPriority')?.value || '').trim();
const statusVal = (document.getElementById('filterStatus')?.value || '').trim().toLowerCase();
const clearBtn = document.getElementById('clearSearchBtn');
if (clearBtn) {
if (searchVal.length > 0) clearBtn.classList.remove('hidden');
else clearBtn.classList.add('hidden');
}
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const all = [...assigned, ...team];
let sourceList = [];
if (currentTab === 'assigned') {
sourceList = assigned;
} else if (currentTab === 'team') {
sourceList = team;
} else if (currentTab === 'in_progress') {
sourceList = all.filter(iss => {
const s = (iss.status || '').toLowerCase();
const sc = (iss.status_category || '').toLowerCase();
return s.includes('progress') || s.includes('bezig') || s.includes('review') || sc === 'indeterminate';
});
} else if (currentTab === 'done') {
sourceList = all.filter(iss => {
const s = (iss.status || '').toLowerCase();
const sc = (iss.status_category || '').toLowerCase();
return s.includes('done') || s.includes('afgerond') || s.includes('gesloten') || sc === 'done';
});
}
// Pas zoek-, project-, wachtrij-, type-, prioriteit- en statusfilters toe
const filtered = sourceList.filter(iss => {
if (searchVal) {
const keyMatch = (iss.key || '').toLowerCase().includes(searchVal);
const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal);
const prjMatch = (iss.project || '').toLowerCase().includes(searchVal);
const prjKeyMatch = (iss.project_key || '').toLowerCase().includes(searchVal);
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;
}
if (projectVal) {
const pNorm = projectVal.trim().toLowerCase();
const pName = (iss.project || '').trim().toLowerCase();
const pKey = (iss.project_key || '').trim().toLowerCase();
if (pName !== pNorm && pKey !== pNorm) {
return false;
}
}
if (currentQueue) {
if (!isIssueInQueue(iss, currentQueue)) {
return false;
}
}
if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) {
return false;
}
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;
}
}
if (statusVal && (iss.status || '').toLowerCase() !== statusVal) {
return false;
}
return true;
});
updateActiveFilterBadge();
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';
}
// 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') || 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('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') || 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>';
}
// Due date badge
let dueDateHtml = '';
if (ticket.due_date) {
const today = new Date().toISOString().split('T')[0];
const isOverdue = ticket.due_date < today;
const isToday = ticket.due_date === today;
let dueClass = 'text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-slate-800/60';
if (isOverdue) dueClass = 'text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-900/50';
else if (isToday) dueClass = 'text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-900/50';
dueDateHtml = `
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-semibold ${dueClass}">
<i data-lucide="calendar" class="w-3 h-3"></i>
<span>${ticket.due_date}</span>
</span>
`;
}
// Assignee badge
let assigneeHtml = '';
if (ticket.assignee_name) {
const initials = ticket.assignee_name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
assigneeHtml = `
<div class="flex items-center space-x-1.5 text-xs text-slate-600 dark:text-slate-400">
<div class="w-5 h-5 rounded-full bg-indigo-100 dark:bg-indigo-900/60 text-indigo-700 dark:text-indigo-300 flex items-center justify-center font-bold text-[10px]">
${initials}
</div>
<span class="truncate max-w-[120px] sm:max-w-[160px]">${escapeHtml(ticket.assignee_name)}</span>
</div>
`;
}
// Onderwerp logging badge
let onderwerpBadge = '<span class="text-slate-400 italic font-normal">Niet ingevuld</span>';
if (ticket.onderwerp_logging) {
const olLower = ticket.onderwerp_logging.toLowerCase();
const dxList = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
const isDx = dxList.includes(olLower);
if (isDx) {
onderwerpBadge = `
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-bold bg-amber-50 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-300 dark:border-amber-800/80">
<i data-lucide="zap" class="w-3 h-3 text-amber-500"></i>
<span>DX: ${escapeHtml(ticket.onderwerp_logging)}</span>
</span>
`;
} else {
onderwerpBadge = `
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-bold bg-sky-50 text-sky-800 dark:bg-sky-950/60 dark:text-sky-300 border border-sky-300 dark:border-sky-800/80">
<i data-lucide="wrench" class="w-3 h-3 text-sky-500"></i>
<span>SD: ${escapeHtml(ticket.onderwerp_logging)}</span>
</span>
`;
}
}
const isExpanded = expandedKeys.has(ticket.key);
const detailsClass = isExpanded ? '' : 'hidden';
const chevronRotateClass = isExpanded ? 'rotate-180' : '';
html += `
<div id="ticketCard_${escapeHtml(ticket.key)}" class="fociz-card p-4 sm:p-5 transition hover:border-indigo-500/40 dark:hover:border-indigo-500/40">
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-3">
<!-- Linkerkant: Metadata & Titel -->
<div class="flex-1 min-w-0 space-y-2">
<!-- Header regel met Type, Key, Project, Prioriteit & Status -->
<div class="flex flex-wrap items-center gap-2">
<!-- Type Badge -->
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-wider border ${typeBadgeClass}">
<i data-lucide="${typeIcon}" class="w-3 h-3"></i>
<span>${escapeHtml(ticket.issue_type || 'Taak')}</span>
</span>
<!-- Key Link -->
<a href="${escapeHtml(ticket.url)}" target="_blank" rel="noopener noreferrer" class="font-mono text-xs font-black text-indigo-600 dark:text-indigo-400 hover:underline flex items-center space-x-1">
<span>${escapeHtml(ticket.key)}</span>
<i data-lucide="external-link" class="w-3 h-3 text-slate-400"></i>
</a>
<!-- Project Tag -->
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-medium bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300">
<i data-lucide="folder" class="w-3 h-3 text-slate-400"></i>
<span>${escapeHtml(ticket.project || 'Project')}</span>
</span>
<!-- Status Badge -->
<span class="inline-flex items-center px-2 py-0.5 rounded-md text-[11px] font-bold border ${statusBadgeClass}">
${statusDot}
<span>${escapeHtml(ticket.status || 'To Do')}</span>
</span>
<!-- Priority Badge -->
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md text-[11px] font-bold border ${prioBadgeClass}">
<i data-lucide="${prioIcon}" class="w-3 h-3"></i>
<span>${escapeHtml(ticket.priority || 'Medium')}</span>
</span>
<!-- Due date -->
${dueDateHtml}
</div>
<!-- Ticket Titel / Summary (Klikbaar voor accordion) -->
<h3 class="text-sm sm:text-base font-bold text-slate-900 dark:text-white leading-snug cursor-pointer group" onclick="toggleTicketDetails('${escapeHtml(ticket.key)}', event)">
<span class="group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition">
${escapeHtml(ticket.summary || '(Geen titel)')}
</span>
</h3>
<!-- Sub-info: Assignee, Update timestamp & Inklap knop -->
<div class="flex flex-wrap items-center gap-4 pt-1">
${assigneeHtml}
${ticket.updated ? `<span class="text-[11px] text-slate-400">Gewijzigd: ${escapeHtml(ticket.updated)}</span>` : ''}
${ticket.onderwerp_logging ? onderwerpBadge : ''}
</div>
</div>
<!-- Rechterkant: Snelle Actieknoppen + Uitklapknop -->
<div class="flex items-center space-x-2 pt-2 lg:pt-0 flex-shrink-0 border-t lg:border-t-0 border-slate-100 dark:border-slate-800">
<button type="button" onclick="toggleTicketDetails('${escapeHtml(ticket.key)}', event)" class="flex items-center space-x-1 px-2.5 py-2 rounded-xl text-xs font-bold text-slate-600 dark:text-slate-300 hover:text-indigo-600 dark:hover:text-indigo-400 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700/80 transition cursor-pointer shadow-2xs">
<span>Details</span>
<i data-lucide="chevron-down" id="chevron_${escapeHtml(ticket.key)}" class="w-3.5 h-3.5 transition-transform duration-200 ${chevronRotateClass}"></i>
</button>
<button onclick="openPlanModal('${escapeHtml(ticket.key)}')" class="flex items-center space-x-1.5 px-3 py-2 rounded-xl text-xs font-bold bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-950/50 dark:hover:bg-indigo-900/60 dark:text-indigo-300 border border-indigo-200/80 dark:border-indigo-800/80 transition cursor-pointer shadow-2xs">
<i data-lucide="calendar" class="w-3.5 h-3.5"></i>
<span>Plan in Dagplanner</span>
</button>
<button onclick="openBoardModal('${escapeHtml(ticket.key)}')" class="flex items-center space-x-1.5 px-3 py-2 rounded-xl text-xs font-bold bg-emerald-50 hover:bg-emerald-100 text-emerald-700 dark:bg-emerald-950/50 dark:hover:bg-indigo-900/60 dark:text-emerald-300 border border-emerald-200/80 dark:border-emerald-800/80 transition cursor-pointer shadow-2xs">
<i data-lucide="kanban" class="w-3.5 h-3.5"></i>
<span>Naar Bord</span>
</button>
<a href="${escapeHtml(ticket.url)}" target="_blank" rel="noopener noreferrer" title="Bekijk in Jira" class="p-2 rounded-xl text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="external-link" class="w-4 h-4"></i>
</a>
</div>
</div>
<!-- UITKLAPBAAR PANEEL (Accordion Details) -->
<div id="details_${escapeHtml(ticket.key)}" class="${detailsClass} mt-4 pt-4 border-t border-slate-100 dark:border-slate-800 space-y-4 text-xs">
<!-- Beschrijving -->
<div class="space-y-1.5">
<div class="flex items-center space-x-1.5 font-bold text-slate-700 dark:text-slate-300">
<i data-lucide="align-left" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Volledige Ticket Omschrijving</span>
</div>
<div class="p-4 rounded-xl bg-slate-50 dark:bg-slate-900/90 border border-slate-200/80 dark:border-slate-800 text-slate-800 dark:text-slate-200 leading-relaxed whitespace-pre-wrap select-text font-normal">
${escapeHtml(ticket.description || 'Geen uitgebreide omschrijving beschikbaar in Jira.')}
</div>
</div>
<!-- Metadata Grid (Reporter, Aanmaakdatum, Wijziging, Onderwerp) -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 p-3.5 rounded-xl bg-slate-50/60 dark:bg-slate-950/40 border border-slate-200/60 dark:border-slate-800/60 text-[11px]">
<div>
<span class="text-slate-400 block mb-1 font-medium">Aangemeld door (Reporter):</span>
<div class="flex items-center space-x-1.5 font-semibold text-slate-800 dark:text-slate-200">
<div class="w-4 h-4 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center font-bold text-[9px] flex-shrink-0">
${escapeHtml((ticket.reporter_name || 'J').charAt(0).toUpperCase())}
</div>
<span class="truncate">${escapeHtml(ticket.reporter_name || 'Onbekend')}</span>
</div>
</div>
<div>
<span class="text-slate-400 block mb-1 font-medium">Aangemaakt op:</span>
<span class="font-medium text-slate-700 dark:text-slate-300">
${ticket.created ? escapeHtml(ticket.created) : 'Niet opgegeven'}
</span>
</div>
<div>
<span class="text-slate-400 block mb-1 font-medium">Laatst gewijzigd:</span>
<span class="font-medium text-slate-700 dark:text-slate-300">
${ticket.updated ? escapeHtml(ticket.updated) : 'Recent'}
</span>
</div>
<div>
<span class="text-slate-400 block mb-1 font-medium">Onderwerp logging:</span>
${onderwerpBadge}
</div>
</div>
<!-- Snelle acties onderaan detailvak -->
<div class="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2">
<button type="button" onclick='copyTicketKey("${escapeHtml(ticket.key)}")' class="flex items-center space-x-1 px-2.5 py-1.5 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold transition cursor-pointer">
<i data-lucide="copy" class="w-3 h-3"></i>
<span>Sleutel Kopiëren</span>
</button>
<a href="${escapeHtml(ticket.url)}" target="_blank" rel="noopener noreferrer" class="flex items-center space-x-1 px-2.5 py-1.5 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-indigo-600 dark:text-indigo-400 text-xs font-semibold transition cursor-pointer">
<i data-lucide="external-link" class="w-3 h-3"></i>
<span>Open in Jira Cloud</span>
</a>
</div>
<div class="flex items-center space-x-2">
<button type="button" onclick='openPlanModal("${escapeHtml(ticket.key)}")' class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-700 text-white transition cursor-pointer shadow-xs">
<i data-lucide="calendar" class="w-3 h-3"></i>
<span>Inplannen in Dagplanner</span>
</button>
<button type="button" onclick='openBoardModal("${escapeHtml(ticket.key)}")' class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-emerald-600 hover:bg-emerald-700 text-white transition cursor-pointer shadow-xs">
<i data-lucide="kanban" class="w-3 h-3"></i>
<span>Naar Kanban Bord</span>
</button>
</div>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
// Herinitialiseer Lucide iconen
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
// Modal 1: Plan in Dagplanner
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;
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;
// 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;
}
const modal = document.getElementById('planTaskModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closePlanModal() {
const modal = document.getElementById('planTaskModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
async function submitPlanTask(e) {
e.preventDefault();
const btn = document.getElementById('planSubmitBtn');
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>Inplannen...</span>`;
if (typeof lucide !== 'undefined') lucide.createIcons();
const key = document.getElementById('planIssueKey').value;
const summary = document.getElementById('planIssueSummary').value;
const url = document.getElementById('planIssueUrl').value;
const planDate = document.getElementById('planDateInput').value;
const timeBlockStart = document.getElementById('planStartTimeInput').value;
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;
}
try {
const response = await fetch('/api/integrations.php?action=plan_jira_task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
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 {
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;');
}
// Sluit modals bij Escape toets
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closePlanModal();
closeBoardModal();
}
});
// Initiële opstart
document.addEventListener('DOMContentLoaded', () => {
// Herstel filters drawer open/dicht status
if (localStorage.getItem('fociz_jira_filters_open') === '1') {
toggleJiraFiltersDrawer(true);
}
calculateKpis();
updateProjectPills();
updateQueueCounts();
if (currentQueue) {
updateQueuePills();
}
applyFilters();
// Luister naar live events voor nieuwe/gewijzigde Jira tickets
window.addEventListener('fociz:jira-updated', () => {
window.location.reload();
});
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>