Merge pull request 'Jira 26' (#91) from dev into main

Reviewed-on: #91
This commit was merged in pull request #91.
This commit is contained in:
2026-09-15 20:32:23 +00:00
4 changed files with 412 additions and 76 deletions
+20 -3
View File
@@ -1349,7 +1349,20 @@ require_once __DIR__ . '/includes/header.php';
<?php foreach ($allJiraFilters as $filter): ?>
<?php
$fCodeNorm = strtoupper(trim($filter['filter_code'] ?? ''));
$assignedColleagues = $userNamesByFilter[$fCodeNorm] ?? ($userNamesByFilter[$filter['filter_code'] ?? ''] ?? []);
$fProjNorm = strtoupper(trim($filter['project_key'] ?? ''));
// Verzamel toegewezen collega's op basis van filter code én project key (case-insensitief)
$assignedColleagues = [];
if (!empty($userNamesByFilter[$fCodeNorm])) {
foreach ($userNamesByFilter[$fCodeNorm] as $cn) {
if (!in_array($cn, $assignedColleagues, true)) $assignedColleagues[] = $cn;
}
}
if ($fProjNorm !== '' && !empty($userNamesByFilter[$fProjNorm])) {
foreach ($userNamesByFilter[$fProjNorm] as $cn) {
if (!in_array($cn, $assignedColleagues, true)) $assignedColleagues[] = $cn;
}
}
$filterJsonSafe = htmlspecialchars(json_encode($filter), ENT_QUOTES, 'UTF-8');
?>
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 hover:border-indigo-500/40 transition flex flex-col md:flex-row md:items-center justify-between gap-4">
@@ -1495,10 +1508,14 @@ require_once __DIR__ . '/includes/header.php';
</td>
<td class="py-3 px-4">
<div class="flex flex-wrap gap-1.5 items-center">
<?php foreach ($allJiraFilters as $f):
<?php
$userSelectedNorm = array_map(function($c) { return strtoupper(trim($c)); }, $userSelectedCodes);
foreach ($allJiraFilters as $f):
if (!$f['is_active']) continue;
$code = $f['filter_code'];
$isChecked = in_array($code, $userSelectedCodes, true);
$cNorm = strtoupper(trim($code));
$pNorm = strtoupper(trim($f['project_key'] ?? ''));
$isChecked = in_array($cNorm, $userSelectedNorm, true) || ($pNorm !== '' && in_array($pNorm, $userSelectedNorm, true));
?>
<label class="inline-flex items-center space-x-1.5 px-2.5 py-1 rounded-lg border text-xs font-semibold cursor-pointer transition select-none <?= $isChecked ? 'bg-indigo-50 border-indigo-300 text-indigo-700 dark:bg-indigo-950/60 dark:border-indigo-700 dark:text-indigo-300' : 'bg-slate-50 border-slate-200 text-slate-600 dark:bg-slate-900/40 dark:border-slate-800 dark:text-slate-400 hover:border-slate-300 dark:hover:border-slate-700' ?>">
<input type="checkbox" name="user_filters[<?= (int)$usr['id'] ?>][]" value="<?= htmlspecialchars($code) ?>" <?= $isChecked ? 'checked' : '' ?> class="w-3.5 h-3.5 rounded text-indigo-600 border-slate-300 dark:border-slate-700 focus:ring-indigo-500 cursor-pointer">
+2 -2
View File
@@ -972,7 +972,7 @@ function get_logged_in_user(): ?array {
return null;
}
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, theme, notification_settings, default_jira_filter_code, fociz_widget_layout, created_at FROM users WHERE id = ?");
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, theme, notification_settings, default_jira_filter_code, default_jira_filter_codes, fociz_widget_layout, created_at FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
if ($user) {
@@ -1022,7 +1022,7 @@ function get_logged_in_user(): ?array {
// 3. Fallback dagsessie-token: herstelt sessie na browserherstart (uitsluitend vandaag én mits niet inactief!)
$dayToken = $_COOKIE['fociz_day_session'] ?? ($_COOKIE['fociz_remember'] ?? null);
if (!empty($dayToken) && is_string($dayToken)) {
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, theme, notification_settings, default_jira_filter_code, fociz_widget_layout, last_activity_at, last_login_at, created_at FROM users WHERE remember_token = ? AND remember_token_expires_at >= NOW() AND DATE(remember_token_expires_at) = ?");
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, theme, notification_settings, default_jira_filter_code, default_jira_filter_codes, fociz_widget_layout, last_activity_at, last_login_at, created_at FROM users WHERE remember_token = ? AND remember_token_expires_at >= NOW() AND DATE(remember_token_expires_at) = ?");
$stmt->execute([$dayToken, $today]);
$user = $stmt->fetch();
if ($user) {
+170 -68
View File
@@ -435,71 +435,136 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri
'customfield_10028', // Story Points (alternatief veld sommige instanties)
];
// 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST
$url = $cleanDomain . '/rest/api/3/search/jql';
$payload = [
'jql' => $jql,
'maxResults' => min($maxResults, 100),
'fields' => $fieldsList,
];
$allRawIssues = [];
$nextPageToken = null;
$startAt = 0;
$pageSize = 100;
$maxPages = (int)ceil($maxResults / $pageSize);
$useMethod = 'v3_post'; // 'v3_post', 'v3_get', 'v2_get'
$response = fociz_http_request($url, [
'method' => 'POST',
'headers' => [
'Authorization' => $authHeader,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
'body' => $payload,
'timeout' => 12,
]);
for ($page = 0; $page < $maxPages; $page++) {
$needed = $maxResults - count($allRawIssues);
if ($needed <= 0) break;
$currentPageSize = min($pageSize, $needed);
// 2. Fallback: GET /rest/api/3/search/jql met query parameters indien de instantie GET vereist
if ($response['status'] === 405 || $response['status'] === 400) {
$getParams = [
'jql' => $jql,
'maxResults' => min($maxResults, 100),
'fields' => implode(',', $fieldsList),
];
$urlGet = $cleanDomain . '/rest/api/3/search/jql?' . http_build_query($getParams);
$response = fociz_http_request($urlGet, [
'method' => 'GET',
'headers' => [
'Authorization' => $authHeader,
'Accept' => 'application/json',
],
'timeout' => 12,
]);
$response = null;
// 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST
if ($useMethod === 'v3_post') {
$url = $cleanDomain . '/rest/api/3/search/jql';
$payload = [
'jql' => $jql,
'maxResults' => $currentPageSize,
'fields' => $fieldsList,
];
if (!empty($nextPageToken)) {
$payload['nextPageToken'] = $nextPageToken;
}
$response = fociz_http_request($url, [
'method' => 'POST',
'headers' => [
'Authorization' => $authHeader,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
'body' => $payload,
'timeout' => 12,
]);
if ($response['status'] === 405 || $response['status'] === 400) {
$useMethod = 'v3_get';
} elseif ($response['status'] === 404) {
$useMethod = 'v2_get';
}
}
// 2. Fallback: GET /rest/api/3/search/jql met query parameters indien de instantie GET vereist
if ($useMethod === 'v3_get') {
$getParams = [
'jql' => $jql,
'maxResults' => $currentPageSize,
'fields' => implode(',', $fieldsList),
];
if (!empty($nextPageToken)) {
$getParams['nextPageToken'] = $nextPageToken;
} elseif ($startAt > 0) {
$getParams['startAt'] = $startAt;
}
$urlGet = $cleanDomain . '/rest/api/3/search/jql?' . http_build_query($getParams);
$response = fociz_http_request($urlGet, [
'method' => 'GET',
'headers' => [
'Authorization' => $authHeader,
'Accept' => 'application/json',
],
'timeout' => 12,
]);
if ($response['status'] === 404) {
$useMethod = 'v2_get';
}
}
// 3. Fallback naar API v2 voor on-premise Jira Server / Data Center installaties
if ($useMethod === 'v2_get') {
$getParams = [
'jql' => $jql,
'maxResults' => $currentPageSize,
'startAt' => $startAt,
'fields' => implode(',', $fieldsList),
];
$urlV2 = $cleanDomain . '/rest/api/2/search?' . http_build_query($getParams);
$response = fociz_http_request($urlV2, [
'method' => 'GET',
'headers' => [
'Authorization' => $authHeader,
'Accept' => 'application/json',
],
'timeout' => 12,
]);
}
if (!$response || $response['status'] !== 200 || !isset($response['json']['issues']) || !is_array($response['json']['issues'])) {
if ($page === 0) {
$errorMsg = $response['json']['errorMessages'][0] ?? ($response['json']['message'] ?? ($response['error'] ?? 'Kan Jira tickets niet ophalen'));
return [
'success' => false,
'error' => $errorMsg,
'issues' => [],
];
}
break;
}
$pageIssues = $response['json']['issues'];
if (empty($pageIssues)) {
break;
}
foreach ($pageIssues as $pi) {
$allRawIssues[] = $pi;
}
$nextPageToken = $response['json']['nextPageToken'] ?? null;
$totalInApi = (int)($response['json']['total'] ?? 0);
$startAt += count($pageIssues);
// Geen volgende pagina meer
if (empty($nextPageToken)) {
if ($totalInApi > 0 && $startAt >= $totalInApi) {
break;
}
if ($useMethod === 'v3_post') {
break;
}
if (count($pageIssues) < $currentPageSize) {
break;
}
}
}
// 3. Fallback naar API v2 voor on-premise Jira Server / Data Center installaties (waar /rest/api/2/search actief is)
if ($response['status'] === 404) {
$getParams = [
'jql' => $jql,
'maxResults' => min($maxResults, 100),
'fields' => implode(',', $fieldsList),
];
$urlV2 = $cleanDomain . '/rest/api/2/search?' . http_build_query($getParams);
$response = fociz_http_request($urlV2, [
'method' => 'GET',
'headers' => [
'Authorization' => $authHeader,
'Accept' => 'application/json',
],
'timeout' => 12,
]);
}
if ($response['status'] !== 200 || !isset($response['json']['issues'])) {
$errorMsg = $response['json']['errorMessages'][0] ?? ($response['json']['message'] ?? ($response['error'] ?? 'Kan Jira tickets niet ophalen'));
return [
'success' => false,
'error' => $errorMsg,
'issues' => [],
];
}
$rawIssues = $response['json']['issues'];
$rawIssues = $allRawIssues;
$issues = [];
foreach ($rawIssues as $item) {
@@ -631,7 +696,7 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri
/**
* Haalt alle actieve issues op die zijn toegewezen aan de ingelogde Jira gebruiker
*/
function jira_fetch_assigned_issues(string $domain, string $email, string $apiToken, int $maxResults = 100): array {
function jira_fetch_assigned_issues(string $domain, string $email, string $apiToken, int $maxResults = 250): array {
$jql = 'assignee = currentUser() ORDER BY updated DESC';
return jira_fetch_issues($domain, $email, $apiToken, $jql, $maxResults);
}
@@ -639,8 +704,18 @@ function jira_fetch_assigned_issues(string $domain, string $email, string $apiTo
/**
* Haalt openstaande team issues op die horen bij de projecten van de gebruiker
*/
function jira_fetch_team_issues(string $domain, string $email, string $apiToken, int $maxResults = 100): array {
$jql = 'assignee != currentUser() AND statusCategory != Done ORDER BY updated DESC';
function jira_fetch_team_issues(string $domain, string $email, string $apiToken, $projectKeys = [], int $maxResults = 500): array {
if (is_int($projectKeys)) {
$maxResults = $projectKeys;
$projectKeys = [];
}
$cleanKeys = array_values(array_filter(array_map('strtoupper', array_map('trim', (array)$projectKeys))));
$projClause = '';
if (!empty($cleanKeys)) {
$escaped = array_map(function($k) { return '"' . addslashes($k) . '"'; }, $cleanKeys);
$projClause = 'project in (' . implode(', ', $escaped) . ') AND ';
}
$jql = $projClause . 'statusCategory != Done ORDER BY updated DESC';
return jira_fetch_issues($domain, $email, $apiToken, $jql, $maxResults);
}
@@ -1126,10 +1201,37 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
}
}
// Ophalen van toegewezen taken (max 100)
$assignedRes = jira_fetch_assigned_issues($domain, $email, $apiToken, 100);
// Ophalen van teamtaken (max 100)
$teamRes = jira_fetch_team_issues($domain, $email, $apiToken, 100);
// Ophalen van toegewezen taken (max 250)
$assignedRes = jira_fetch_assigned_issues($domain, $email, $apiToken, 250);
// Bepaal de relevante projecten van de gebruiker om gericht en compleet op te halen
$targetProjKeys = [];
if (!empty($userQueuePrefs)) {
foreach ($userQueuePrefs as $code) {
$c = strtoupper(trim($code));
if ($c === 'DX') {
$targetProjKeys['SD'] = true;
} elseif (!in_array($c, ['MY_QUEUES'], true)) {
$foundProj = false;
foreach ($queueFilters as $qf) {
if (strtoupper(trim($qf['filter_code'] ?? '')) === $c && !empty($qf['project_key'])) {
$targetProjKeys[strtoupper(trim($qf['project_key']))] = true;
$foundProj = true;
break;
}
}
if (!$foundProj && strlen($c) <= 10) {
$targetProjKeys[$c] = true;
}
}
}
}
if (empty($targetProjKeys)) {
$targetProjKeys = array_fill_keys(['SD', 'IN', 'AAN', 'GEN'], true);
}
// Ophalen van teamtaken binnen de relevante projecten (met paginatie tot max 500)
$teamRes = jira_fetch_team_issues($domain, $email, $apiToken, array_keys($targetProjKeys), 500);
// Ophalen van Jira statussen
$availableStatuses = jira_fetch_statuses($domain, $email, $apiToken);
+220 -3
View File
@@ -26,6 +26,16 @@ $syncError = $jiraData['error'] ?? null;
$assignedIssues = $jiraData['assigned_issues'] ?? [];
$teamIssues = $jiraData['team_issues'] ?? [];
// Markeer expliciet welke tickets zijn toegewezen aan de ingelogde gebruiker
foreach ($assignedIssues as &$ai) {
$ai['is_assigned_to_me'] = true;
}
unset($ai);
$jiraData['assigned_issues'] = $assignedIssues;
$currentUserName = $currentUser['name'] ?? ($accountName ?: '');
$currentUserEmail = $currentUser['email'] ?? ($email ?: '');
// Haal borden en kolommen op voor het "Naar Bord" modal
$db = get_db_connection();
if ($isAdmin) {
@@ -306,6 +316,8 @@ $jiraDataJson = json_encode($jiraData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
$boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$projectStatusesJson = json_encode($projectStatusesData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$currentUserNameJson = json_encode($currentUserName, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$currentUserEmailJson = json_encode($currentUserEmail, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
?>
<div class="space-y-6 w-full">
@@ -615,6 +627,17 @@ $statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSO
<div id="projectStatusDropdownsWrapper" class="flex flex-wrap items-center gap-2">
</div>
<!-- Snelle optie: Toegewezen aan mij (uitvoerder) per geselecteerd project -->
<button type="button"
id="btnToggleAssignedToMePill"
onclick="toggleCurrentProjectOnlyAssigned()"
class="flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-semibold border transition cursor-pointer shadow-2xs flex-shrink-0 bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:border-slate-300 dark:hover:border-slate-600"
title="Filter op taken toegewezen aan mijzelf (uitvoerder)">
<i data-lucide="user-check" class="w-3.5 h-3.5 text-indigo-500 flex-shrink-0"></i>
<span id="labelAssignedToMePill">Toegewezen aan mij <span class="hidden sm:inline">(uitvoerder)</span></span>
<span id="badgeAssignedToMeCount" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300">0</span>
</button>
<!-- 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>
@@ -991,6 +1014,8 @@ const jiraData = <?= $jiraDataJson ?>;
const availableBoards = <?= $boardsJson ?>;
const projectStatusesData = <?= $projectStatusesJson ?>;
const allJiraStatuses = <?= $statusesJson ?>;
const currentUserName = <?= $currentUserNameJson ?>;
const currentUserEmail = <?= $currentUserEmailJson ?>;
let currentProject = '';
let currentQueue = '';
@@ -999,6 +1024,134 @@ let activeUrgentOnly = false;
let expandedKeys = new Set();
const ticketStore = new Map();
// Filter per project: 'Toegewezen aan mij (uitvoerder)'
let projectOnlyAssigned = new Set();
try {
const savedAssigned = localStorage.getItem('fociz_jira_project_only_assigned');
if (savedAssigned) {
const parsed = JSON.parse(savedAssigned);
if (Array.isArray(parsed)) {
parsed.forEach(p => projectOnlyAssigned.add(String(p).toUpperCase().trim()));
}
}
} catch (e) {}
// Set van alle aan mij toegewezen issue keys
const assignedIssueKeys = new Set((jiraData.assigned_issues || []).map(i => i.key));
function isIssueAssignedToMe(iss) {
if (!iss) return false;
if (iss.is_assigned_to_me === true) return true;
if (iss.key && assignedIssueKeys.has(iss.key)) return true;
const ass = (iss.assignee_name || '').toLowerCase().trim();
if (!ass || ass === 'niet toegewezen' || ass === 'unassigned') return false;
if (ass === 'mijzelf' || ass === 'myself' || ass === 'me') return true;
const myName = (currentUserName || jiraData.account_name || '').toLowerCase().trim();
const myEmail = (currentUserEmail || jiraData.email || '').toLowerCase().trim();
if (myName && (ass === myName || ass.includes(myName) || myName.includes(ass))) return true;
if (myEmail && (ass.includes(myEmail) || (iss.assignee_email && String(iss.assignee_email).toLowerCase().includes(myEmail)))) return true;
return false;
}
function isProjectOnlyAssignedActive(projectVal, issPKey) {
if (projectOnlyAssigned.has('ALL')) return true;
const pNorm = (projectVal || '').toUpperCase().trim();
const issPNorm = (issPKey || '').toUpperCase().trim();
if (pNorm && projectOnlyAssigned.has(pNorm)) return true;
if (!pNorm && issPNorm && projectOnlyAssigned.has(issPNorm)) return true;
return false;
}
function toggleProjectOnlyAssigned(pKey, forceVal) {
const key = (pKey || 'ALL').toUpperCase().trim();
const isCurrently = projectOnlyAssigned.has(key);
const willSet = (typeof forceVal === 'boolean') ? forceVal : !isCurrently;
if (willSet) {
projectOnlyAssigned.add(key);
} else {
projectOnlyAssigned.delete(key);
}
try {
localStorage.setItem('fociz_jira_project_only_assigned', JSON.stringify([...projectOnlyAssigned]));
} catch (e) {}
updateAssignedPillState();
updateStatusDropdownLabel();
updateStatusCounts();
applyFilters();
}
function toggleCurrentProjectOnlyAssigned() {
const targetKey = (currentProject || 'ALL').toUpperCase().trim();
toggleProjectOnlyAssigned(targetKey);
}
function updateAssignedPillState() {
const targetKey = (currentProject || 'ALL').toUpperCase().trim();
const isAssigned = projectOnlyAssigned.has(targetKey) || (currentProject === '' && projectOnlyAssigned.has('ALL'));
const btn = document.getElementById('btnToggleAssignedToMePill');
const badge = document.getElementById('badgeAssignedToMeCount');
const label = document.getElementById('labelAssignedToMePill');
if (!btn) return;
if (isAssigned) {
btn.className = "flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-bold border border-indigo-600 bg-indigo-600 text-white shadow-xs flex-shrink-0 cursor-pointer";
if (badge) {
badge.className = "px-1.5 py-0.5 rounded-full text-[10px] font-black bg-white/20 text-white";
}
} else {
btn.className = "flex items-center space-x-1.5 px-2.5 sm:px-3 py-1.5 rounded-xl text-xs font-semibold border transition cursor-pointer shadow-2xs flex-shrink-0 bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:border-slate-300 dark:hover:border-slate-600";
if (badge) {
badge.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300";
}
}
if (label) {
if (currentProject) {
label.innerHTML = `Toegewezen aan mij <span class="text-[10px] opacity-80 uppercase font-mono">(${escapeHtml(currentProject)})</span>`;
} else {
label.innerHTML = `Toegewezen aan mij <span class="hidden sm:inline">(uitvoerder)</span>`;
}
}
const all = getDeduplicatedIssues();
let targetList = all;
if (currentProject) {
targetList = all.filter(iss => isIssueInQueue(iss, currentProject));
}
const myCount = targetList.filter(iss => isIssueAssignedToMe(iss)).length;
if (badge) {
badge.innerText = myCount;
}
(projectStatusesData || []).forEach(proj => {
const pKey = (proj.key || '').toUpperCase().trim();
const chk = document.getElementById(`chkOnlyAssigned_${pKey}`);
if (chk) {
chk.checked = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
}
const cCnt = all.filter(iss => {
const k = getIssueProjectKey(iss);
return (k === pKey || (!k && (iss.project || '').toUpperCase().includes(pKey))) && isIssueAssignedToMe(iss);
}).length;
const cntEl = document.getElementById(`projAssignedCount_${pKey}`);
if (cntEl) cntEl.innerText = cCnt;
const btnMijn = document.getElementById(`btnDropdownMijn_${pKey}`);
if (btnMijn) {
const isPActive = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
if (isPActive) {
btnMijn.className = "px-1.5 py-0.5 rounded font-bold bg-indigo-600 text-white transition cursor-pointer";
} else {
btnMijn.className = "px-1.5 py-0.5 rounded font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer";
}
}
});
}
// Hulpfunctie voor deduplicatie van tickets (assigned + team)
function getDeduplicatedIssues() {
const assigned = jiraData.assigned_issues || [];
@@ -1274,12 +1427,39 @@ function renderStatusCheckboxes() {
Actief
</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" id="btnDropdownMijn_${escapeHtml(pKey)}" onclick="toggleProjectOnlyAssigned('${escapeHtml(pKey)}')" class="px-1.5 py-0.5 rounded font-bold ${projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL') ? 'bg-indigo-600 text-white' : 'text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60'} transition cursor-pointer" title="Toegewezen aan mij (uitvoerder)">
Mijn
</button>
<span class="text-slate-300 dark:text-slate-700">•</span>
<button type="button" onclick="selectProjectStatuses('${escapeHtml(pKey)}', false)" class="px-1.5 py-0.5 rounded font-bold text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition cursor-pointer">
Geen
</button>
</div>
</div>
<!-- Toegewezen aan mij (uitvoerder) toggle card -->
<div class="px-3 py-2 -mx-3.5 bg-slate-50/80 dark:bg-slate-800/50 border-b border-slate-100 dark:border-slate-800/80 flex items-center justify-between">
<label for="chkOnlyAssigned_${escapeHtml(pKey)}" class="flex items-center space-x-2 cursor-pointer select-none min-w-0 flex-1 group">
<div class="w-6 h-6 rounded-lg ${projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL') ? 'bg-indigo-600 text-white' : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300'} flex items-center justify-center flex-shrink-0 transition">
<i data-lucide="user-check" class="w-3.5 h-3.5"></i>
</div>
<div class="min-w-0 flex-1">
<div class="text-xs font-bold text-slate-800 dark:text-slate-200 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition truncate">Toegewezen aan mij (uitvoerder)</div>
<div class="text-[10px] text-slate-400">Alleen mijn taken in ${escapeHtml(pKey)}</div>
</div>
</label>
<div class="flex items-center space-x-2 flex-shrink-0 ml-2">
<span id="projAssignedCount_${escapeHtml(pKey)}" class="px-1.5 py-0.5 rounded-full text-[10px] font-black bg-indigo-100 dark:bg-indigo-950/70 text-indigo-700 dark:text-indigo-300">0</span>
<input
type="checkbox"
id="chkOnlyAssigned_${escapeHtml(pKey)}"
${projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL') ? 'checked' : ''}
onchange="toggleProjectOnlyAssigned('${escapeHtml(pKey)}', this.checked)"
class="w-4 h-4 rounded text-indigo-600 border-slate-300 dark:border-slate-700 focus:ring-indigo-500 cursor-pointer"
>
</div>
</div>
<div class="max-h-60 overflow-y-auto space-y-1 pr-1 custom-scrollbar" id="statusCheckboxList_${escapeHtml(pKey)}">
${statusRowsHtml}
</div>
@@ -1293,6 +1473,7 @@ function renderStatusCheckboxes() {
updateStatusCounts();
updateStatusDropdownLabel();
updateProjectStatusDropdownVisibility();
updateAssignedPillState();
}
function updateProjectStatusDropdownVisibility() {
@@ -1386,6 +1567,11 @@ function updateStatusCounts() {
projIssues = projIssues.filter(iss => isIssueInQueue(iss, currentProject));
}
// Toegewezen aan mij filter per project meenemen in de status tellingen
if (isProjectOnlyAssignedActive(currentProject, pKey)) {
projIssues = projIssues.filter(iss => isIssueAssignedToMe(iss));
}
const projTotalEl = document.getElementById(`projTotal_${pKey}`);
if (projTotalEl) {
projTotalEl.innerText = `(${projIssues.length})`;
@@ -1401,6 +1587,8 @@ function updateStatusCounts() {
}
});
});
updateAssignedPillState();
}
function onStatusCheckboxChange(projectKey) {
@@ -1556,11 +1744,18 @@ function updateStatusDropdownLabel() {
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";
}
}
// Toon indicator indien Toegewezen aan mij actief is voor dit project
const isAssignedOnly = projectOnlyAssigned.has(pKey) || projectOnlyAssigned.has('ALL');
if (isAssignedOnly) {
hasAnyCustomFilter = true;
labelEl.innerText += " • Mijn";
}
});
const btnReset = document.getElementById('btnResetFiltersInline');
if (btnReset) {
const isCustom = hasAnyCustomFilter || currentProject !== '' || document.getElementById('jiraSearchInput')?.value;
const isCustom = hasAnyCustomFilter || currentProject !== '' || projectOnlyAssigned.size > 0 || document.getElementById('jiraSearchInput')?.value;
if (isCustom) btnReset.classList.remove('hidden');
else btnReset.classList.add('hidden');
}
@@ -1628,6 +1823,7 @@ function selectProject(proj) {
closeAllProjectStatusDropdowns();
updateProjectPills();
updateProjectStatusDropdownVisibility();
updateAssignedPillState();
updateStatusCounts();
applyFilters();
}
@@ -1942,7 +2138,13 @@ function updateProjectCounts() {
}
const elAll = document.getElementById('projCount_all');
if (elAll) elAll.innerText = currentList.length;
if (elAll) {
let allFiltered = currentList;
if (projectOnlyAssigned.has('ALL')) {
allFiltered = allFiltered.filter(iss => isIssueAssignedToMe(iss));
}
elAll.innerText = allFiltered.length;
}
const pills = document.querySelectorAll('.project-pill');
pills.forEach(p => {
@@ -1950,7 +2152,11 @@ function updateProjectCounts() {
if (!pKey) return;
const el = document.getElementById(`projCount_${pKey}`);
if (el) {
el.innerText = currentList.filter(iss => isIssueInQueue(iss, pKey)).length;
let pList = currentList.filter(iss => isIssueInQueue(iss, pKey));
if (projectOnlyAssigned.has(pKey.toUpperCase()) || projectOnlyAssigned.has('ALL')) {
pList = pList.filter(iss => isIssueAssignedToMe(iss));
}
el.innerText = pList.length;
}
});
}
@@ -1995,6 +2201,10 @@ function clearSearch() {
function resetAllFilters() {
currentProject = '';
currentQueue = '';
projectOnlyAssigned.clear();
try {
localStorage.removeItem('fociz_jira_project_only_assigned');
} catch (e) {}
closeAllProjectStatusDropdowns();
selectAllStatuses(true);
@@ -2009,6 +2219,7 @@ function resetAllFilters() {
document.getElementById('clearSearchBtn')?.classList.add('hidden');
updateProjectPills();
updateProjectStatusDropdownVisibility();
updateAssignedPillState();
updateStatusCounts();
applyFilters();
}
@@ -2048,6 +2259,12 @@ function applyFilters() {
}
}
// 3. Toegewezen aan mij (uitvoerder) per project filter
const isAssignedFilterActive = isProjectOnlyAssignedActive(projectVal, issPKey);
if (isAssignedFilterActive && !isIssueAssignedToMe(iss)) {
return false;
}
// 4. Type filter (uit drawer)
if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) {
return false;