From 8cff08f253d2d82d3634a7ffc5c69d998db531a2 Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Tue, 15 Sep 2026 15:42:23 +0200 Subject: [PATCH] Jira repair --- jira.php | 665 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 526 insertions(+), 139 deletions(-) diff --git a/jira.php b/jira.php index fa4bbc1..ed02880 100644 --- a/jira.php +++ b/jira.php @@ -98,15 +98,102 @@ foreach ($allIssues as $iss) { } } -// Statussen uitbreiden met officiële Jira instantie statussen -$availableStatuses = $jiraData['available_statuses'] ?? []; -if (!empty($availableStatuses)) { - foreach ($availableStatuses as $st) { - if (!empty($st['name'])) { - $allStatuses[$st['name']] = $st['name']; - } +// Bepaal welke projecten getoond worden: alleen projecten die tickets hebben, plus SD/IN/AAN/GEN +// Geen statussen van projecten die niet gebruikt worden! +$activeProjectKeys = []; +foreach ($coreProjects as $cKey => $cMeta) { + if (isset($usedProjectKeys[$cKey]) || !empty($projectCounts[$cMeta['name']]) || !empty($projectCounts[$cKey])) { + $activeProjectKeys[$cKey] = true; } } +foreach ($usedProjectKeys as $uKey => $val) { + $activeProjectKeys[$uKey] = true; +} +if (empty($activeProjectKeys)) { + $activeProjectKeys = array_fill_keys(['SD', 'IN', 'AAN', 'GEN'], true); +} + +// Haal projectstatussen op uit jiraData +$rawProjectStatuses = $jiraData['project_statuses'] ?? []; + +// Bouw gestructureerde lijst van statussen gegroepeerd per project +$projectStatusesData = []; +$allStatusesMap = []; + +foreach (array_keys($activeProjectKeys) as $pKey) { + $pName = $coreProjects[$pKey]['name'] ?? ($allProjects[$pKey]['name'] ?? $pKey); + $pColor = $coreProjects[$pKey]['color'] ?? 'slate'; + + $pStatuses = []; + + // 1. Statussen uit Jira API voor dit project + if (!empty($rawProjectStatuses[$pKey])) { + foreach ($rawProjectStatuses[$pKey] as $st) { + $sName = trim($st['name'] ?? ''); + if (empty($sName)) continue; + $sKey = strtolower($sName); + $pStatuses[$sKey] = [ + 'name' => $sName, + 'status_category' => $st['status_category'] ?? 'indeterminate', + 'category_name' => $st['category_name'] ?? '', + ]; + } + } + + // 2. Statussen die op de tickets van dit project voorkomen toevoegen/mergen + foreach ($allIssues as $iss) { + $issPKey = strtoupper(trim($iss['project_key'] ?? '')); + if (empty($issPKey) && !empty($iss['key']) && preg_match('/^([A-Z0-9]+)-/i', $iss['key'], $m)) { + $issPKey = strtoupper($m[1]); + } + if ($issPKey === $pKey && !empty($iss['status'])) { + $sName = trim($iss['status']); + $sKey = strtolower($sName); + if (!isset($pStatuses[$sKey])) { + $pStatuses[$sKey] = [ + 'name' => $sName, + 'status_category' => $iss['status_category'] ?? 'indeterminate', + 'category_name' => '', + ]; + } + } + } + + // Fallback als een project nog geen statussen heeft + if (empty($pStatuses)) { + $fallback = [ + ['name' => 'To Do', 'status_category' => 'new'], + ['name' => 'In behandeling', 'status_category' => 'indeterminate'], + ['name' => 'Gesloten', 'status_category' => 'done'], + ]; + foreach ($fallback as $fb) { + $pStatuses[strtolower($fb['name'])] = $fb; + } + } + + // Sorteren: To Do / Nieuw (1), In behandeling / Uitvoering (2), Gesloten / Gereed (3) + uasort($pStatuses, function($a, $b) { + $order = ['new' => 1, 'indeterminate' => 2, 'done' => 3]; + $oa = $order[$a['status_category'] ?? 'indeterminate'] ?? 2; + $ob = $order[$b['status_category'] ?? 'indeterminate'] ?? 2; + if ($oa !== $ob) return $oa - $ob; + return strcasecmp($a['name'], $b['name']); + }); + + $statusList = array_values($pStatuses); + foreach ($statusList as $st) { + $allStatusesMap[strtolower($st['name'])] = $st; + } + + $projectStatusesData[] = [ + 'key' => $pKey, + 'name' => $pName, + 'color' => $pColor, + 'statuses' => $statusList, + ]; +} + +$allStatusesList = array_values($allStatusesMap); ksort($allProjects); ksort($allTypes); @@ -117,6 +204,8 @@ $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); +$projectStatusesJson = json_encode($projectStatusesData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); +$statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>
@@ -393,48 +482,48 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
-
+
@@ -446,51 +535,55 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
- -
- -
- + +
+ + +
+
+ - + + - - -
- - + +
+ +
+
+
+ • + +
+
+
+ `; + + statuses.forEach((st, sIdx) => { + const sNorm = (st.name || '').toLowerCase().trim(); + const compoundVal = `${pKeyLower}::${sNorm}`; + const cat = (st.status_category || '').toLowerCase().trim(); + const isDone = (cat === 'done' || isDoneStatus(sNorm)); + + const isChecked = selectedStatuses.has(compoundVal) || selectedStatuses.has(sNorm); + if (isChecked) { + selectedStatuses.add(compoundVal); + } + + let dotColor = 'bg-blue-500'; + if (cat === 'new' || sNorm.includes('to do') || sNorm.includes('open') || sNorm.includes('nieuw')) { + dotColor = 'bg-slate-400'; + } else if (isDone) { + dotColor = 'bg-emerald-500'; + } else if (sNorm.includes('wacht') || sNorm.includes('klant') || sNorm.includes('pending')) { + dotColor = 'bg-amber-500'; + } + + const safeKey = `${pKey}_${sNorm.replace(/[^a-z0-9]/gi, '_')}`; + const safeId = `chkStatus_${pIdx}_${sIdx}`; + + html += ` + + `; + }); + + html += ` +
+
+ `; + }); + + container.innerHTML = html; + updateStatusCounts(); +} + +function updateStatusCounts() { + const all = getDeduplicatedIssues(); + + (projectStatusesData || []).forEach(proj => { + const pKey = (proj.key || '').toUpperCase().trim(); + let projIssues = all.filter(iss => { + let k = (iss.project_key || '').toUpperCase().trim(); + if (!k && iss.key) { + const parts = iss.key.split('-'); + if (parts.length > 1) k = parts[0].toUpperCase().trim(); + } + const pn = (iss.project || '').toUpperCase().trim(); + return k === pKey || pn.includes(pKey); + }); + + // Wachtrij filter meenemen indien actief + if (currentQueue) { + projIssues = projIssues.filter(iss => isIssueInQueue(iss, currentQueue)); + } + + const projTotalEl = document.getElementById(`projTotal_${pKey}`); + if (projTotalEl) { + projTotalEl.innerText = `(${projIssues.length})`; + } + + (proj.statuses || []).forEach(st => { + const sNorm = (st.name || '').toLowerCase().trim(); + const safeKey = `${pKey}_${sNorm.replace(/[^a-z0-9]/gi, '_')}`; + const el = document.getElementById(`statusCount_${safeKey}`); + if (el) { + const cnt = projIssues.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 selectProjectStatuses(projectKey, selectAll) { + const targetKey = (projectKey || '').toUpperCase().trim(); + const chks = document.querySelectorAll(`.status-checkbox[data-project="${targetKey}"]`); + chks.forEach(chk => { + chk.checked = !!selectAll; + const val = chk.value.toLowerCase().trim(); + if (selectAll) { + selectedStatuses.add(val); } else { - 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"; + selectedStatuses.delete(val); } }); + try { + localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses])); + } catch (e) {} + + updateStatusDropdownLabel(); applyFilters(); } -// Toggle: Verberg Gesloten -function toggleHideClosed() { - hideClosed = !hideClosed; - updateHideClosedButton(); +function selectAllStatuses(selectAll) { + selectedStatuses.clear(); + const chks = document.querySelectorAll('.status-checkbox'); + chks.forEach(chk => { + chk.checked = !!selectAll; + if (selectAll && chk.value) { + selectedStatuses.add(chk.value.toLowerCase().trim()); + } + }); + + try { + localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses])); + } catch (e) {} + + updateStatusDropdownLabel(); applyFilters(); } -function 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 = `Verberg Gesloten`; +function selectActiveStatusesOnly() { + selectedStatuses.clear(); + const chks = document.querySelectorAll('.status-checkbox'); + chks.forEach(chk => { + const isDone = chk.getAttribute('data-is-done') === '1'; + chk.checked = !isDone; + if (!isDone && chk.value) { + selectedStatuses.add(chk.value.toLowerCase().trim()); + } + }); + + try { + localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses])); + } catch (e) {} + + updateStatusDropdownLabel(); + applyFilters(); +} + +function updateStatusDropdownLabel() { + const labelEl = document.getElementById('statusDropdownLabel'); + const badgeEl = document.getElementById('statusDropdownBadge'); + if (!labelEl) return; + + const total = getTotalStatusCount(); + 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 { - 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 = `Toon Ook Gesloten`; + 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 = `In behandeling (Actief)`; - } 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 = `In behandeling`; + let validSaved = false; + if (Array.isArray(saved) && saved.length > 0) { + const hasCompound = saved.some(s => String(s).includes('::')); + if (hasCompound) { + saved.forEach(s => { + if (s) selectedStatuses.add(String(s).toLowerCase().trim()); + }); + validSaved = true; + } } - if (typeof lucide !== 'undefined') lucide.createIcons(); -} -// Status dropdown verandering -function onStatusSelectChange(val) { - if (val && isDoneStatus(val)) { - hideClosed = false; - updateHideClosedButton(); + if (!validSaved) { + // Standaard alle actieve statussen selecteren, gesloten uit conform gebruikerswens + (projectStatusesData || []).forEach(proj => { + const pKeyLower = (proj.key || '').toLowerCase().trim(); + (proj.statuses || []).forEach(st => { + const sNorm = (st.name || '').toLowerCase().trim(); + const cat = (st.status_category || '').toLowerCase().trim(); + const isDone = (cat === 'done' || isDoneStatus(sNorm)); + if (!isDone) { + selectedStatuses.add(`${pKeyLower}::${sNorm}`); + } + }); + }); + if (selectedStatuses.size === 0) { + (projectStatusesData || []).forEach(proj => { + const pKeyLower = (proj.key || '').toLowerCase().trim(); + (proj.statuses || []).forEach(st => { + const sNorm = (st.name || '').toLowerCase().trim(); + selectedStatuses.add(`${pKeyLower}::${sNorm}`); + }); + }); + } } - applyFilters(); + + renderStatusCheckboxes(); + updateStatusDropdownLabel(); } // Project selectie via pill tabs @@ -1142,21 +1544,14 @@ function updateQueuePills() { } function updateProjectCounts() { - const assigned = jiraData.assigned_issues || []; - const team = jiraData.team_issues || []; - const all = [...assigned, ...team]; - + const all = getDeduplicatedIssues(); + const total = getTotalStatusCount(); 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 < total) { + currentList = currentList.filter(iss => isIssueStatusSelected(iss)); + } else if (selectedStatuses.size === 0) { + currentList = []; } const elAll = document.getElementById('projCount_all'); @@ -1181,21 +1576,14 @@ function updateProjectCounts() { } function updateQueueCounts() { - const assigned = jiraData.assigned_issues || []; - const team = jiraData.team_issues || []; - const all = [...assigned, ...team]; - + const all = getDeduplicatedIssues(); + const total = getTotalStatusCount(); 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 < total) { + currentList = currentList.filter(iss => isIssueStatusSelected(iss)); + } else if (selectedStatuses.size === 0) { + currentList = []; } if (currentProject) { @@ -1303,12 +1691,11 @@ function applyFilters() { 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 + const totalStatuses = getTotalStatusCount(); + if (selectedStatuses.size > 0 && selectedStatuses.size < totalStatuses) { + if (!isIssueStatusSelected(iss)) { return false; } } else if (selectedStatuses.size === 0) { -- 2.52.0