Jira en admin aanpassingen

This commit is contained in:
2026-09-15 12:30:58 +02:00
parent f992ca1ec9
commit e3af7c7aa5
2 changed files with 652 additions and 512 deletions
+391 -245
View File
@@ -77,23 +77,62 @@ foreach ($allIssues as $iss) {
$allTypes[$iss['issue_type']] = $iss['issue_type'];
}
if (!empty($iss['status'])) {
$allStatuses[$iss['status']] = $iss['status'];
$sName = trim($iss['status']);
$sKey = strtolower($sName);
$allStatuses[$sKey] = [
'name' => $sName,
'status_category' => $iss['status_category'] ?? 'indeterminate',
'category_name' => '',
];
}
}
// Statussen uitbreiden met officiële Jira instantie statussen
// Statussen uitbreiden met ALLE officiële Jira Cloud instantie statussen
$availableStatuses = $jiraData['available_statuses'] ?? [];
if (!empty($availableStatuses)) {
foreach ($availableStatuses as $st) {
if (!empty($st['name'])) {
$allStatuses[$st['name']] = $st['name'];
$sName = trim($st['name']);
$sKey = strtolower($sName);
$cat = $st['status_category'] ?? ($allStatuses[$sKey]['status_category'] ?? 'indeterminate');
$allStatuses[$sKey] = [
'name' => $sName,
'status_category' => $cat,
'category_name' => $st['category_name'] ?? '',
];
}
}
}
// Fallback statussen als Jira nog geen verbinding heeft
if (empty($allStatuses)) {
$fallbackList = [
['name' => 'To Do', 'status_category' => 'new'],
['name' => 'In behandeling', 'status_category' => 'indeterminate'],
['name' => 'In Progress', 'status_category' => 'indeterminate'],
['name' => 'Wachten op klant', 'status_category' => 'indeterminate'],
['name' => 'In Review', 'status_category' => 'indeterminate'],
['name' => 'Gesloten', 'status_category' => 'done'],
['name' => 'Done', 'status_category' => 'done'],
];
foreach ($fallbackList as $fb) {
$allStatuses[strtolower($fb['name'])] = $fb;
}
}
// Sorteren: To Do / Nieuw (1), In Uitvoering / Behandeling (2), Gesloten / Gereed (3), daarbinnen alfabetisch
uasort($allStatuses, function($a, $b) {
$order = ['new' => 1, 'indeterminate' => 2, 'done' => 3];
$oa = $order[$a['status_category'] ?? 'indeterminate'] ?? 2;
$ob = $order[$b['status_category'] ?? 'indeterminate'] ?? 2;
if ($oa !== $ob) return $oa - $ob;
return strcasecmp($a['name'], $b['name']);
});
$allStatusesList = array_values($allStatuses);
ksort($allProjects);
ksort($allTypes);
ksort($allStatuses);
$queueFilters = $jiraData['queue_filters'] ?? jira_get_queue_filters(true);
$defaultQueueFilter = $jiraData['default_queue_filter'] ?? jira_get_user_queue_preference($userId);
@@ -101,6 +140,7 @@ $userQueueFilters = $jiraData['user_queue_filters'] ?? jira_get_user_queue_prefe
$jiraDataJson = json_encode($jiraData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
?>
<div class="space-y-6 w-full">
@@ -211,7 +251,7 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
<!-- 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 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">
@@ -224,7 +264,7 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
</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 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">
@@ -237,7 +277,7 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
</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 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">
@@ -423,101 +463,83 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
</button>
<?php if ($isAdmin): ?>
<a href="/admin.php#jira" title="Wachtrijfilters beheren in Beheer" class="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0">
<a href="/admin.php#projects" title="Wachtrijfilters beheren in Beheer" class="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0">
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
</a>
<?php endif; ?>
</div>
</div>
<!-- Rij 3: Tabs, Verberg Gesloten, In behandeling & Zoekveld -->
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-3">
<!-- Tabs Navigatie -->
<div class="flex items-center space-x-1 sm:space-x-1.5 overflow-x-auto pb-1 lg:pb-0">
<button onclick="switchTab('assigned')" id="tabBtn_assigned" class="flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800/60 flex-shrink-0">
<i data-lucide="user-check" class="w-3.5 h-3.5"></i>
<span>Aan mij</span>
<span id="tabCount_assigned" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-200/70 dark:bg-indigo-800/60 text-indigo-800 dark:text-indigo-200">
<?= count($assignedIssues) ?>
</span>
</button>
<!-- Rij 3: Status Multi-select Dropdown & Zoekveld -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<!-- Linkerkant: Statussen multi-select dropdown -->
<div class="flex flex-wrap items-center gap-2">
<div class="relative" id="statusDropdownWrapper">
<button
type="button"
onclick="toggleStatusDropdown()"
id="statusDropdownBtn"
class="flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-semibold bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 text-slate-800 dark:text-slate-200 transition cursor-pointer shadow-2xs"
aria-expanded="false"
>
<i data-lucide="check-square" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Status:</span>
<span id="statusDropdownLabel" class="font-bold text-indigo-600 dark:text-indigo-400">Alle statussen</span>
<span id="statusDropdownBadge" class="hidden px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300"></span>
<i data-lucide="chevron-down" id="statusDropdownChevron" class="w-3.5 h-3.5 text-slate-400 transition-transform duration-200"></i>
</button>
<button onclick="switchTab('team')" id="tabBtn_team" class="flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
<i data-lucide="users" class="w-3.5 h-3.5"></i>
<span>Team & Projecten</span>
<span id="tabCount_team" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
<?= count($teamIssues) ?>
</span>
</button>
<!-- Dropdown Menu met Checkboxes -->
<div
id="statusDropdownMenu"
class="hidden absolute left-0 top-full mt-2 w-72 max-w-[90vw] bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 py-3 px-3.5 z-50 text-xs space-y-2.5"
>
<!-- Quick actions header -->
<div class="flex items-center justify-between pb-2 border-b border-slate-100 dark:border-slate-800">
<span class="font-bold text-slate-900 dark:text-white text-[11px] uppercase tracking-wider">Jira Statussen</span>
<div class="flex items-center space-x-1.5">
<button type="button" onclick="selectAllStatuses(true)" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer">
Alles
</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" onclick="selectActiveStatusesOnly()" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-950/60 transition cursor-pointer" title="Alleen actieve taken (geen gesloten)">
Actief
</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" onclick="selectAllStatuses(false)" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
Geen
</button>
</div>
</div>
<button onclick="switchTab('in_progress')" id="tabBtn_in_progress" class="flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
<span>In Uitvoering</span>
<span id="tabCount_in_progress" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
0
</span>
</button>
<!-- Status Checkboxes List (scrollable) -->
<div class="max-h-64 overflow-y-auto space-y-1 pr-1 custom-scrollbar" id="statusCheckboxList">
<!-- Dynamisch gevuld via renderStatusCheckboxes() -->
</div>
</div>
</div>
<button onclick="switchTab('done')" id="tabBtn_done" class="flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
<span>Afgerond</span>
<span id="tabCount_done" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
0
</span>
<!-- Actieve filters reset knop als er iets gefilterd is -->
<button type="button" onclick="resetAllFilters()" id="btnResetFiltersInline" class="hidden flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-semibold text-slate-500 hover:text-slate-800 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="rotate-ccw" class="w-3 h-3"></i>
<span>Filters wissen</span>
</button>
</div>
<!-- Status Acties: Verberg Gesloten + Dropdown + Zoeken -->
<div class="flex flex-wrap items-center gap-2">
<!-- Toggle: Verberg Gesloten (Standaard Actief!) -->
<button type="button" onclick="toggleHideClosed()" id="btnHideClosed" class="flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80 shadow-2xs" title="Verberg afgeronde en gesloten tickets">
<i data-lucide="eye-off" id="iconHideClosed" class="w-3.5 h-3.5 text-emerald-600"></i>
<span>Verberg Gesloten</span>
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<!-- Rechterkant: Zoekveld -->
<div class="relative w-full sm:w-64">
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2 pointer-events-none"></i>
<input
type="text"
id="jiraSearchInput"
placeholder="Zoek in alle tickets..."
oninput="applyFilters()"
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl pl-7 pr-6 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
<button onclick="clearSearch()" id="clearSearchBtn" class="hidden absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
<!-- Snelfilter: In behandeling -->
<button type="button" onclick="toggleOnlyInProgress()" id="btnOnlyInProgress" class="flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700" title="Toon alleen tickets die op In Behandeling staan">
<i data-lucide="play" class="w-3 h-3 text-blue-500"></i>
<span>In behandeling</span>
</button>
<!-- Status Dropdown met Jira statussen -->
<div class="relative">
<select id="filterStatus" onchange="onStatusSelectChange(this.value)" class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl pl-2.5 pr-7 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer">
<option value="">Alle statussen</option>
<optgroup label="Nieuw / To Do">
<option value="To Do">To Do</option>
<option value="Nieuw">Nieuw</option>
</optgroup>
<optgroup label="In Uitvoering / Behandeling">
<option value="In behandeling">In behandeling</option>
<option value="In Progress">In Progress</option>
<option value="Wachten op klant">Wachten op klant</option>
<option value="In Review">In Review</option>
</optgroup>
<optgroup label="Afgerond / Gesloten">
<option value="Gereed">Gereed</option>
<option value="Gesloten">Gesloten</option>
<option value="Done">Done</option>
</optgroup>
</select>
</div>
<!-- Zoekveld -->
<div class="relative w-full sm:w-44">
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2 pointer-events-none"></i>
<input
type="text"
id="jiraSearchInput"
placeholder="Zoek ticket..."
oninput="applyFilters()"
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl pl-7 pr-6 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
<button onclick="clearSearch()" id="clearSearchBtn" class="hidden absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
</div>
</div>
</div>
@@ -873,14 +895,28 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
const jiraData = <?= $jiraDataJson ?>;
const availableBoards = <?= $boardsJson ?>;
let currentTab = 'assigned'; // 'assigned', 'team', 'in_progress', 'done'
let currentProject = '';
let currentQueue = (jiraData.user_queue_filters && jiraData.user_queue_filters.length > 0) ? 'MY_QUEUES' : (jiraData.default_queue_filter || '');
let hideClosed = true; // Standaard gesloten tickets verbergen conform gebruikerswens!
let onlyInProgress = false;
let selectedStatuses = new Set();
let activeUrgentOnly = false;
let expandedKeys = new Set();
const ticketStore = new Map();
const allJiraStatuses = <?= $statusesJson ?>;
// Hulpfunctie voor deduplicatie van tickets (assigned + team)
function getDeduplicatedIssues() {
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const seen = new Set();
const list = [];
[...assigned, ...team].forEach(iss => {
if (iss && iss.key && !seen.has(iss.key)) {
seen.add(iss.key);
list.push(iss);
}
});
return list;
}
function isDoneStatus(st) {
const s = (st || '').toLowerCase().trim();
@@ -893,15 +929,14 @@ function isInProgressStatus(st, sc) {
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
// Bereken statussen en KPI statistieken voor de drawer
function calculateKpis() {
const all = getDeduplicatedIssues();
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();
@@ -911,10 +946,7 @@ function calculateKpis() {
if (isInProgressStatus(s, sc)) {
inProgressCount++;
}
if (isDoneStatus(s) || sc === 'done') {
doneCount++;
}
if (p === 'highest' || p === 'high') {
if (p === 'highest' || p === 'high' || p === 'p1') {
highPriorityCount++;
}
});
@@ -929,21 +961,11 @@ function calculateKpis() {
if (elStatInProg) elStatInProg.innerText = inProgressCount;
if (elStatHigh) elStatHigh.innerText = highPriorityCount;
const elTabAssigned = document.getElementById('tabCount_assigned');
const elTabTeam = document.getElementById('tabCount_team');
const elTabInProg = document.getElementById('tabCount_in_progress');
const elTabDone = document.getElementById('tabCount_done');
if (elTabAssigned) elTabAssigned.innerText = assigned.length;
if (elTabTeam) elTabTeam.innerText = team.length;
if (elTabInProg) elTabInProg.innerText = inProgressCount;
if (elTabDone) elTabDone.innerText = doneCount;
updateProjectCounts();
updateQueueCounts();
}
// Uitklappen en inklappen van de filters & statistieken drawer
// Drawer open/dicht
function toggleJiraFiltersDrawer(forceOpen) {
const drawer = document.getElementById('jiraFiltersDrawer');
const chevron = document.getElementById('filtersDrawerChevron');
@@ -961,7 +983,7 @@ function toggleJiraFiltersDrawer(forceOpen) {
}
}
// Bepaal aantal actieve filters voor de badge in de rechter onderhoek
// Bepaal aantal actieve filters voor badge
function updateActiveFilterBadge() {
const badge = document.getElementById('activeFilterBadge');
if (!badge) return;
@@ -971,8 +993,7 @@ function updateActiveFilterBadge() {
if (currentQueue && currentQueue !== 'MY_QUEUES') activeCount++;
if (document.getElementById('filterType')?.value) activeCount++;
if (document.getElementById('filterPriority')?.value) activeCount++;
if (document.getElementById('filterStatus')?.value) activeCount++;
if (onlyInProgress) activeCount++;
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) activeCount++;
if (activeCount > 0) {
badge.innerText = activeCount;
@@ -982,73 +1003,236 @@ function updateActiveFilterBadge() {
}
}
// Wissel tussen tabs
function switchTab(tab) {
currentTab = tab;
activeUrgentOnly = false;
// ==========================================
// STATUS MULTI-SELECT CHECKBOX DROPDOWN LOGICA
// ==========================================
// Update tab knoppen styling
const tabs = ['assigned', 'team', 'in_progress', 'done'];
tabs.forEach(t => {
const btn = document.getElementById(`tabBtn_${t}`);
if (!btn) return;
if (t === tab) {
btn.className = "flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800/60 flex-shrink-0";
} else {
btn.className = "flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0";
function renderStatusCheckboxes() {
const container = document.getElementById('statusCheckboxList');
if (!container) return;
if (!allJiraStatuses || allJiraStatuses.length === 0) {
container.innerHTML = '<div class="py-2 text-center text-slate-400 italic text-[11px]">Geen Jira statussen gevonden</div>';
return;
}
let html = '';
allJiraStatuses.forEach((st, idx) => {
const sNorm = (st.name || '').toLowerCase().trim();
const isChecked = selectedStatuses.has(sNorm);
const cat = (st.status_category || '').toLowerCase().trim();
let dotColor = 'bg-blue-500';
if (cat === 'new' || sNorm.includes('to do') || sNorm.includes('open') || sNorm.includes('nieuw')) {
dotColor = 'bg-slate-400';
} else if (cat === 'done' || isDoneStatus(sNorm)) {
dotColor = 'bg-emerald-500';
} else if (sNorm.includes('wacht') || sNorm.includes('klant') || sNorm.includes('pending')) {
dotColor = 'bg-amber-500';
}
const safeId = 'chkStatus_' + idx;
html += `
<label for="${safeId}" class="flex items-center justify-between px-2.5 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/80 cursor-pointer transition select-none group">
<div class="flex items-center space-x-2.5 min-w-0 flex-1">
<input
type="checkbox"
id="${safeId}"
value="${escapeHtml(sNorm)}"
${isChecked ? 'checked' : ''}
onchange="onStatusCheckboxChange()"
class="status-checkbox w-4 h-4 rounded text-indigo-600 border-slate-300 dark:border-slate-700 focus:ring-indigo-500 cursor-pointer flex-shrink-0"
>
<span class="w-2 h-2 rounded-full ${dotColor} flex-shrink-0"></span>
<span class="font-semibold text-slate-800 dark:text-slate-200 truncate text-xs group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition">${escapeHtml(st.name)}</span>
</div>
<span id="statusCount_${idx}" class="text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 ml-2 flex-shrink-0">0</span>
</label>
`;
});
container.innerHTML = html;
updateStatusCounts();
}
function updateStatusCounts() {
const all = getDeduplicatedIssues();
allJiraStatuses.forEach((st, idx) => {
const sNorm = (st.name || '').toLowerCase().trim();
const el = document.getElementById(`statusCount_${idx}`);
if (el) {
let currentList = all;
if (currentProject) {
const pNorm = currentProject.trim().toUpperCase();
currentList = currentList.filter(iss => {
const pk = (iss.project_key || '').trim().toUpperCase();
const pn = (iss.project || '').trim().toLowerCase();
return pk === pNorm || pn.includes(pNorm.toLowerCase());
});
}
if (currentQueue) {
currentList = currentList.filter(iss => isIssueInQueue(iss, currentQueue));
}
const cnt = currentList.filter(iss => (iss.status || '').toLowerCase().trim() === sNorm).length;
el.innerText = cnt;
}
});
}
function toggleStatusDropdown(force) {
const menu = document.getElementById('statusDropdownMenu');
const chevron = document.getElementById('statusDropdownChevron');
const btn = document.getElementById('statusDropdownBtn');
if (!menu) return;
const willOpen = (typeof force === 'boolean') ? force : menu.classList.contains('hidden');
if (willOpen) {
menu.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
if (btn) btn.setAttribute('aria-expanded', 'true');
updateStatusCounts();
} else {
menu.classList.add('hidden');
if (chevron) chevron.classList.remove('rotate-180');
if (btn) btn.setAttribute('aria-expanded', 'false');
}
}
document.addEventListener('click', (e) => {
const wrapper = document.getElementById('statusDropdownWrapper');
if (wrapper && !wrapper.contains(e.target)) {
toggleStatusDropdown(false);
}
});
function onStatusCheckboxChange() {
selectedStatuses.clear();
const checked = document.querySelectorAll('.status-checkbox:checked');
checked.forEach(chk => {
if (chk.value) selectedStatuses.add(chk.value.toLowerCase().trim());
});
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
applyFilters();
}
function selectAllStatuses(selectAll) {
selectedStatuses.clear();
const chks = document.querySelectorAll('.status-checkbox');
chks.forEach(chk => {
chk.checked = !!selectAll;
if (selectAll && chk.value) {
selectedStatuses.add(chk.value.toLowerCase().trim());
}
});
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
applyFilters();
}
// Toggle: Verberg Gesloten
function toggleHideClosed() {
hideClosed = !hideClosed;
updateHideClosedButton();
function selectActiveStatusesOnly() {
selectedStatuses.clear();
const chks = document.querySelectorAll('.status-checkbox');
chks.forEach(chk => {
const val = (chk.value || '').toLowerCase().trim();
const isDone = isDoneStatus(val);
chk.checked = !isDone;
if (!isDone) {
selectedStatuses.add(val);
}
});
try {
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
} catch (e) {}
updateStatusDropdownLabel();
applyFilters();
}
function updateHideClosedButton() {
const btn = document.getElementById('btnHideClosed');
if (!btn) return;
if (hideClosed) {
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80 shadow-2xs";
btn.innerHTML = `<i data-lucide="eye-off" class="w-3.5 h-3.5 text-emerald-600"></i><span>Verberg Gesloten</span><span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>`;
function updateStatusDropdownLabel() {
const labelEl = document.getElementById('statusDropdownLabel');
const badgeEl = document.getElementById('statusDropdownBadge');
if (!labelEl) return;
const total = allJiraStatuses.length;
const selected = selectedStatuses.size;
if (selected === total || (total === 0 && selected === 0)) {
labelEl.innerText = "Alle statussen";
if (badgeEl) badgeEl.classList.add('hidden');
} else if (selected === 0) {
labelEl.innerText = "Geen geselecteerd";
if (badgeEl) {
badgeEl.innerText = "0";
badgeEl.classList.remove('hidden');
badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-rose-100 dark:bg-rose-950/60 text-rose-700 dark:text-rose-300";
}
} else if (selected === 1) {
const singleNorm = [...selectedStatuses][0];
const match = allJiraStatuses.find(st => (st.name || '').toLowerCase().trim() === singleNorm);
labelEl.innerText = match ? match.name : singleNorm;
if (badgeEl) {
badgeEl.innerText = "1";
badgeEl.classList.remove('hidden');
badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300";
}
} else {
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700";
btn.innerHTML = `<i data-lucide="eye" class="w-3.5 h-3.5 text-slate-400"></i><span>Toon Ook Gesloten</span>`;
labelEl.innerText = `${selected} statussen`;
if (badgeEl) {
badgeEl.innerText = `${selected}/${total}`;
badgeEl.classList.remove('hidden');
badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300";
}
}
const btnReset = document.getElementById('btnResetFiltersInline');
if (btnReset) {
const isCustom = selected < total || currentProject !== '' || (currentQueue !== '' && currentQueue !== 'MY_QUEUES') || document.getElementById('jiraSearchInput')?.value;
if (isCustom) btnReset.classList.remove('hidden');
else btnReset.classList.add('hidden');
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
// Toggle: Snelfilter Alleen In Behandeling
function toggleOnlyInProgress() {
onlyInProgress = !onlyInProgress;
updateOnlyInProgressButton();
applyFilters();
}
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) {}
function updateOnlyInProgressButton() {
const btn = document.getElementById('btnOnlyInProgress');
if (!btn) return;
if (onlyInProgress) {
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-blue-600 text-white shadow-xs";
btn.innerHTML = `<i data-lucide="play" class="w-3 h-3 text-white fill-white"></i><span>In behandeling (Actief)</span>`;
if (Array.isArray(saved) && saved.length > 0) {
saved.forEach(s => {
if (s) selectedStatuses.add(String(s).toLowerCase().trim());
});
} else {
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700";
btn.innerHTML = `<i data-lucide="play" class="w-3 h-3 text-blue-500"></i><span>In behandeling</span>`;
// Standaard alle actieve statussen selecteren, gesloten uit conform gebruikerswens
allJiraStatuses.forEach(st => {
const sNorm = (st.name || '').toLowerCase().trim();
const cat = (st.status_category || '').toLowerCase().trim();
if (cat !== 'done' && !isDoneStatus(sNorm)) {
selectedStatuses.add(sNorm);
}
});
if (selectedStatuses.size === 0) {
allJiraStatuses.forEach(st => {
selectedStatuses.add((st.name || '').toLowerCase().trim());
});
}
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
// Status dropdown verandering
function onStatusSelectChange(val) {
if (val && isDoneStatus(val)) {
hideClosed = false;
updateHideClosedButton();
}
applyFilters();
renderStatusCheckboxes();
updateStatusDropdownLabel();
}
// Project selectie via pill tabs
@@ -1093,7 +1277,7 @@ function updateProjectPills() {
});
}
// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort (SD, IN, AAN, GEN, DX of MY_QUEUES)
// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort
function isIssueInQueue(iss, qCode) {
if (!qCode) return true;
const code = qCode.toUpperCase().trim();
@@ -1144,7 +1328,6 @@ function isIssueInQueue(iss, qCode) {
return true;
}
// Wachtrijfilter selectie
function selectQueueFilter(code) {
currentQueue = code;
updateQueuePills();
@@ -1164,23 +1347,14 @@ function updateQueuePills() {
});
}
// Update de tellers op de project buttons
function updateProjectCounts() {
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const all = [...assigned, ...team];
const all = getDeduplicatedIssues();
let currentList = all;
if (currentTab === 'assigned') currentList = assigned;
else if (currentTab === 'team') currentList = team;
else if (currentTab === 'in_progress') {
currentList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category));
} else if (currentTab === 'done') {
currentList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done');
}
if (hideClosed && currentTab !== 'done') {
currentList = currentList.filter(iss => !isDoneStatus(iss.status) && (iss.status_category || '').toLowerCase() !== 'done');
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
currentList = currentList.filter(iss => selectedStatuses.has((iss.status || '').toLowerCase().trim()));
} else if (selectedStatuses.size === 0) {
currentList = [];
}
const elAll = document.getElementById('projCount_all');
@@ -1204,23 +1378,14 @@ function updateProjectCounts() {
});
}
// Update de tellers op de wachtrijfilter knoppen
function updateQueueCounts() {
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const all = [...assigned, ...team];
const all = getDeduplicatedIssues();
let currentList = all;
if (currentTab === 'assigned') currentList = assigned;
else if (currentTab === 'team') currentList = team;
else if (currentTab === 'in_progress') {
currentList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category));
} else if (currentTab === 'done') {
currentList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done');
}
if (hideClosed && currentTab !== 'done') {
currentList = currentList.filter(iss => !isDoneStatus(iss.status) && (iss.status_category || '').toLowerCase() !== 'done');
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
currentList = currentList.filter(iss => selectedStatuses.has((iss.status || '').toLowerCase().trim()));
} else if (selectedStatuses.size === 0) {
currentList = [];
}
if (currentProject) {
@@ -1257,7 +1422,22 @@ function updateQueueCounts() {
});
}
// Snelkoppeling KPI-kaart Urgent
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) {
@@ -1266,48 +1446,40 @@ function filterByUrgent() {
applyFilters();
}
// Zoekopdracht wissen
function clearSearch() {
const input = document.getElementById('jiraSearchInput');
if (input) {
input.value = '';
}
document.getElementById('clearSearchBtn').classList.add('hidden');
document.getElementById('clearSearchBtn')?.classList.add('hidden');
applyFilters();
}
// Alle filters herstellen
function resetAllFilters() {
currentProject = '';
currentQueue = '';
hideClosed = true;
onlyInProgress = false;
selectAllStatuses(true);
const searchInput = document.getElementById('jiraSearchInput');
const typeSelect = document.getElementById('filterType');
const prioritySelect = document.getElementById('filterPriority');
const statusSelect = document.getElementById('filterStatus');
if (searchInput) searchInput.value = '';
if (typeSelect) typeSelect.value = '';
if (prioritySelect) prioritySelect.value = '';
if (statusSelect) statusSelect.value = '';
document.getElementById('clearSearchBtn')?.classList.add('hidden');
updateHideClosedButton();
updateOnlyInProgressButton();
updateProjectPills();
updateQueuePills();
applyFilters();
}
// Filter en render logic
// 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 statusVal = (document.getElementById('filterStatus')?.value || '').trim().toLowerCase();
const clearBtn = document.getElementById('clearSearchBtn');
if (clearBtn) {
@@ -1315,43 +1487,24 @@ function applyFilters() {
else clearBtn.classList.add('hidden');
}
const assigned = jiraData.assigned_issues || [];
const team = jiraData.team_issues || [];
const all = [...assigned, ...team];
const all = getDeduplicatedIssues();
let sourceList = [];
if (currentTab === 'assigned') {
sourceList = assigned;
} else if (currentTab === 'team') {
sourceList = team;
} else if (currentTab === 'in_progress') {
sourceList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category));
} else if (currentTab === 'done') {
sourceList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done');
}
// Pas filters toe
const filtered = sourceList.filter(iss => {
// Filter tickets
const filtered = all.filter(iss => {
const issPKey = (iss.project_key || '').toUpperCase().trim();
const issPName = (iss.project || '').toLowerCase().trim();
const issStatus = (iss.status || '').toLowerCase().trim();
const issCat = (iss.status_category || '').toLowerCase().trim();
// 1. Verberg Gesloten filter (als tab niet 'done' is en er niet expliciet een gesloten status gekozen is)
if (currentTab !== 'done' && hideClosed && !isDoneStatus(statusVal)) {
if (issCat === 'done' || isDoneStatus(issStatus)) {
// 1. Status Filter via Checkboxes
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
if (!selectedStatuses.has(issStatus)) {
return false;
}
} else if (selectedStatuses.size === 0) {
return false;
}
// 2. Snelfilter "Alleen in behandeling"
if (onlyInProgress) {
if (!isInProgressStatus(issStatus, issCat)) {
return false;
}
}
// 3. Project Filter (SD, IN, AAN, GEN of ander)
// 2. Project Filter (SD, IN, AAN, GEN of ander)
if (projectVal) {
let matchesProj = (issPKey === projectVal);
if (!matchesProj) {
@@ -1364,26 +1517,19 @@ function applyFilters() {
if (!matchesProj) return false;
}
// 4. Wachtrijfilter
// 3. Wachtrijfilter (MY_QUEUES, SD, IN, AAN, GEN, DX)
if (currentQueue) {
if (!isIssueInQueue(iss, currentQueue)) {
return false;
}
}
// 5. Specifieke status selectie
if (statusVal) {
if (issStatus !== statusVal) {
return false;
}
}
// 6. Type filter
// 4. Type filter (uit drawer)
if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) {
return false;
}
// 7. Prioriteit filter
// 5. Prioriteit filter (uit drawer)
if (priorityVal) {
const pVal = priorityVal.toLowerCase();
const ip = (iss.priority || '').toLowerCase();
@@ -1398,7 +1544,7 @@ function applyFilters() {
}
}
// 8. Zoekterm
// 6. Zoekterm
if (searchVal) {
const keyMatch = (iss.key || '').toLowerCase().includes(searchVal);
const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal);
@@ -1417,6 +1563,7 @@ function applyFilters() {
updateActiveFilterBadge();
updateProjectCounts();
updateQueueCounts();
updateStatusCounts();
renderTickets(filtered);
}
@@ -2266,8 +2413,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
calculateKpis();
updateHideClosedButton();
updateOnlyInProgressButton();
initStatusSelection();
updateProjectPills();
updateQueueCounts();
if (currentQueue) {