Dev #34

Merged
NiekRabelink merged 3 commits from dev into main 2026-09-10 12:10:19 +00:00
6 changed files with 1402 additions and 228 deletions
Showing only changes of commit 25c2ffeef8 - Show all commits
+400
View File
@@ -93,6 +93,74 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$messageType = 'success';
}
// 2b. Jira Wachtrijfilter toevoegen of bewerken
elseif ($action === 'save_jira_queue_filter') {
$filterId = (int)($_POST['filter_id'] ?? 0);
$filterCode = strtoupper(trim($_POST['filter_code'] ?? ''));
$filterName = trim($_POST['filter_name'] ?? '');
$projectKey = strtoupper(trim($_POST['project_key'] ?? ''));
$jqlClause = trim($_POST['jql_clause'] ?? '');
$description = trim($_POST['description'] ?? '');
$isActive = isset($_POST['is_active']) ? 1 : 0;
$sortOrder = (int)($_POST['sort_order'] ?? 0);
if (empty($filterCode) || empty($filterName) || empty($jqlClause)) {
$message = "Filtercode, naam en JQL-clausule zijn verplicht.";
$messageType = 'error';
} else {
if ($filterId > 0) {
$stmt = $db->prepare("
UPDATE jira_queue_filters
SET filter_code = ?, filter_name = ?, project_key = ?, jql_clause = ?, description = ?, is_active = ?, sort_order = ?, updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder, $filterId]);
$message = "Wachtrijfilter '{$filterName}' is succesvol bijgewerkt.";
} else {
$stmt = $db->prepare("
INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
filter_name = VALUES(filter_name),
project_key = VALUES(project_key),
jql_clause = VALUES(jql_clause),
description = VALUES(description),
is_active = VALUES(is_active),
sort_order = VALUES(sort_order),
updated_at = NOW()
");
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]);
$message = "Nieuw wachtrijfilter '{$filterName}' is succesvol toegevoegd.";
}
$messageType = 'success';
}
}
// 2c. Jira Wachtrijfilter verwijderen
elseif ($action === 'delete_jira_queue_filter') {
$filterId = (int)($_POST['filter_id'] ?? 0);
if ($filterId > 0) {
$stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?");
$stmt->execute([$filterId]);
$message = "Wachtrijfilter is succesvol verwijderd.";
$messageType = 'success';
}
}
// 2d. Teamleden toewijzen aan Jira Wachtrijfilter
elseif ($action === 'save_user_jira_assignments') {
$assignments = $_POST['user_filter'] ?? [];
if (is_array($assignments)) {
$upd = $db->prepare("UPDATE users SET default_jira_filter_code = ? WHERE id = ?");
foreach ($assignments as $uId => $fCode) {
$fCode = trim($fCode);
$upd->execute([$fCode !== '' ? $fCode : null, (int)$uId]);
}
$message = "Standaard Jira wachtrijtoewijzingen voor het team zijn succesvol opgeslagen.";
$messageType = 'success';
}
}
// 3. Organisatie Werkdag Standaarden opslaan (Release v1.4)
elseif ($action === 'save_workday_settings') {
$workdayStart = trim($_POST['workday_start'] ?? '08:30');
@@ -215,6 +283,15 @@ try {
error_log("Fout bij ophalen recente gebruikers: " . $e->getMessage());
}
$allUsersForJira = [];
$allJiraFilters = [];
try {
$allUsersForJira = $db->query("SELECT id, name, email, role, avatar_url, default_jira_filter_code FROM users ORDER BY name ASC")->fetchAll() ?: [];
$allJiraFilters = $db->query("SELECT * FROM jira_queue_filters ORDER BY sort_order ASC, id ASC")->fetchAll() ?: [];
} catch (Throwable $e) {
error_log("Fout bij ophalen jira filters/users: " . $e->getMessage());
}
$pageTitle = 'Systeembeheer & Integraties — Fociz';
require_once __DIR__ . '/includes/header.php';
?>
@@ -497,6 +574,286 @@ require_once __DIR__ . '/includes/header.php';
</form>
</div>
<!-- 2b. Atlassian Jira Wachtrijfilters & Gebruikerskoppelingen -->
<div class="fociz-card p-6 sm:p-7 space-y-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-xl bg-gradient-to-tr from-sky-500 to-indigo-600 text-white flex items-center justify-center font-bold shadow-md shadow-indigo-500/20 flex-shrink-0">
<i data-lucide="sliders" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Jira Wachtrijfilters & Toewijzingen</h2>
<span class="px-2.5 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 text-indigo-700 dark:text-indigo-400 text-[10px] font-bold">
ServiceDesk & Teams
</span>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
Beheer aangepaste JQL-wachtrijfilters (zoals DX en SD) en wijs teamleden toe aan hun standaard wachtrij.
</p>
</div>
</div>
<button type="button" onclick="openJiraFilterModal()" class="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer self-start sm:self-center">
<i data-lucide="plus" class="w-4 h-4"></i>
<span>Nieuw Wachtrijfilter</span>
</button>
</div>
<!-- Overzicht van actieve wachtrijfilters -->
<div>
<div class="flex items-center justify-between mb-3">
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider flex items-center space-x-1.5">
<i data-lucide="filter" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Geconfigureerde Wachtrijfilters (<?= count($allJiraFilters) ?>)</span>
</h3>
<span class="text-[11px] text-slate-400">Worden getoond als snelle filtertabs op de Jira pagina</span>
</div>
<?php if (empty($allJiraFilters)): ?>
<div class="p-6 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 text-center">
<p class="text-xs text-slate-500">Nog geen wachtrijfilters ingesteld. Klik op "Nieuw Wachtrijfilter" om te beginnen.</p>
</div>
<?php else: ?>
<?php
$userNamesByFilter = [];
foreach ($allUsersForJira as $u) {
if (!empty($u['default_jira_filter_code'])) {
$userNamesByFilter[$u['default_jira_filter_code']][] = $u['name'];
}
}
?>
<div class="grid grid-cols-1 gap-3.5">
<?php foreach ($allJiraFilters as $filter): ?>
<?php
$assignedColleagues = $userNamesByFilter[$filter['filter_code']] ?? [];
$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">
<div class="space-y-2 flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2">
<span class="font-mono text-xs font-black px-2.5 py-0.5 rounded-lg bg-indigo-100 text-indigo-800 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/80">
<?= htmlspecialchars($filter['filter_code']) ?>
</span>
<span class="text-xs font-bold text-slate-900 dark:text-white">
<?= htmlspecialchars($filter['filter_name']) ?>
</span>
<?php if (!empty($filter['project_key'])): ?>
<span class="text-[11px] font-medium px-2 py-0.5 rounded-md bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
Project: <?= htmlspecialchars($filter['project_key']) ?>
</span>
<?php else: ?>
<span class="text-[11px] font-medium px-2 py-0.5 rounded-md bg-slate-100 dark:bg-slate-800/60 text-slate-500">
Alle projecten
</span>
<?php endif; ?>
<?php if (empty($filter['is_active'])): ?>
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-50 text-rose-700 dark:bg-rose-950/40 dark:text-rose-400">
Inactief
</span>
<?php endif; ?>
</div>
<?php if (!empty($filter['description'])): ?>
<p class="text-[11px] text-slate-500 dark:text-slate-400">
<?= htmlspecialchars($filter['description']) ?>
</p>
<?php endif; ?>
<!-- JQL preview box -->
<div class="p-2.5 rounded-xl bg-white dark:bg-[#111622] border border-slate-200/90 dark:border-slate-800 text-[11px] font-mono text-slate-700 dark:text-indigo-300 break-all select-all">
<span class="text-slate-400 dark:text-slate-500 mr-1.5 font-sans font-bold">JQL:</span><?= htmlspecialchars($filter['jql_clause']) ?>
</div>
<!-- Toegewezen collega's -->
<div class="flex items-center space-x-2 text-[11px] text-slate-500 dark:text-slate-400 pt-0.5">
<span class="font-semibold text-slate-700 dark:text-slate-300">Standaard gekoppeld aan:</span>
<?php if (!empty($assignedColleagues)): ?>
<span class="inline-flex items-center gap-1.5 flex-wrap">
<?php foreach ($assignedColleagues as $cName): ?>
<span class="px-2 py-0.5 rounded-md bg-sky-50 text-sky-700 dark:bg-sky-950/50 dark:text-sky-300 border border-sky-200 dark:border-sky-800/60 font-medium">
<?= htmlspecialchars($cName) ?>
</span>
<?php endforeach; ?>
</span>
<?php else: ?>
<span class="italic text-slate-400">Nog niemand specifiek toegewezen</span>
<?php endif; ?>
</div>
</div>
<!-- Actieknoppen -->
<div class="flex items-center space-x-2 self-start md:self-center flex-shrink-0 pt-2 md:pt-0 border-t md:border-t-0 border-slate-200 dark:border-slate-800 w-full md:w-auto justify-end">
<button type="button" onclick="editJiraFilter(<?= $filterJsonSafe ?>)" class="px-3 py-1.5 rounded-xl bg-slate-200/80 hover:bg-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-200 text-xs font-semibold transition cursor-pointer">
Bewerken
</button>
<form method="POST" action="/admin.php" onsubmit="return confirm('Weet u zeker dat u het filter \'<?= htmlspecialchars(addslashes($filter['filter_name'])) ?>\' wilt verwijderen?');" class="inline">
<input type="hidden" name="action" value="delete_jira_queue_filter">
<input type="hidden" name="filter_id" value="<?= (int)$filter['id'] ?>">
<button type="submit" class="p-2 rounded-xl text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer" title="Verwijderen">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Team Wachtrijmatrix: Standaard Wachtrij per Teamlid -->
<div class="pt-6 border-t border-slate-100 dark:border-slate-800/80 space-y-4">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider flex items-center space-x-1.5">
<i data-lucide="users" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Team Wachtrijmatrix: Standaard Wachtrij per Collega</span>
</h3>
<p class="text-[11px] text-slate-500 dark:text-slate-400 mt-0.5">
Wijs collega's toe aan hun primaire wachtrij (bijv. DX of SD). Wanneer zij de Jira pagina openen, wordt deze wachtrij direct als standaard getoond.
</p>
</div>
</div>
<form method="POST" action="/admin.php" class="space-y-4">
<input type="hidden" name="action" value="save_user_jira_assignments">
<div class="overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800">
<table class="w-full text-left text-xs">
<thead class="bg-slate-100 dark:bg-[#0c0f17] text-slate-600 dark:text-slate-400 font-bold border-b border-slate-200 dark:border-slate-800">
<tr>
<th class="py-3 px-4">Teamlid</th>
<th class="py-3 px-4">E-mailadres</th>
<th class="py-3 px-4">Rol</th>
<th class="py-3 px-4 w-72">Standaard Wachtrijfilter</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800/80 bg-white dark:bg-[#111622]">
<?php foreach ($allUsersForJira as $usr): ?>
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition">
<td class="py-3 px-4">
<div class="flex items-center space-x-2.5">
<?php if (!empty($usr['avatar_url'])): ?>
<img src="<?= htmlspecialchars($usr['avatar_url']) ?>" alt="Avatar" class="w-7 h-7 rounded-full object-cover border border-slate-200 dark:border-slate-700">
<?php else: ?>
<div class="w-7 h-7 rounded-full bg-indigo-100 dark:bg-indigo-900/60 text-indigo-700 dark:text-indigo-300 font-bold flex items-center justify-center text-[10px]">
<?= strtoupper(substr($usr['name'], 0, 1)) ?>
</div>
<?php endif; ?>
<span class="font-bold text-slate-900 dark:text-white"><?= htmlspecialchars($usr['name']) ?></span>
</div>
</td>
<td class="py-3 px-4 font-mono text-slate-600 dark:text-slate-400">
<?= htmlspecialchars($usr['email']) ?>
</td>
<td class="py-3 px-4">
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold <?= $usr['role'] === 'ADMIN' ? 'bg-purple-100 text-purple-800 dark:bg-purple-950/60 dark:text-purple-300' : 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300' ?>">
<?= $usr['role'] ?>
</span>
</td>
<td class="py-3 px-4">
<select name="user_filter[<?= (int)$usr['id'] ?>]" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer">
<option value="">Geen voorkeur (alles tonen)</option>
<?php foreach ($allJiraFilters as $f): ?>
<?php if ($f['is_active']): ?>
<option value="<?= htmlspecialchars($f['filter_code']) ?>" <?= ($usr['default_jira_filter_code'] === $f['filter_code']) ? 'selected' : '' ?>>
<?= htmlspecialchars($f['filter_code']) ?> — <?= htmlspecialchars($f['filter_name']) ?>
</option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer">
Wachtrijtoewijzingen Opslaan
</button>
</div>
</form>
</div>
</div>
<!-- MODAL: Wachtrijfilter Toevoegen / Bewerken -->
<div id="jiraFilterModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl p-6 transition transform">
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center space-x-2.5">
<div class="w-9 h-9 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="filter" class="w-4 h-4"></i>
</div>
<div>
<h3 id="jiraFilterModalTitle" class="text-sm font-bold text-slate-900 dark:text-white">Wachtrijfilter Configureren</h3>
<p class="text-[11px] text-slate-400">Beheer JQL zoekclausule en koppeling</p>
</div>
</div>
<button type="button" onclick="closeJiraFilterModal()" 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 method="POST" action="/admin.php" class="mt-4 space-y-4">
<input type="hidden" name="action" value="save_jira_queue_filter">
<input type="hidden" id="filterModal_id" name="filter_id" value="0">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Filter Code <span class="text-red-500">*</span>
</label>
<input type="text" id="filterModal_code" name="filter_code" required placeholder="bijv. DX of SD" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white font-mono uppercase focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Doel Project (optioneel)
</label>
<input type="text" id="filterModal_project" name="project_key" placeholder="bijv. SD of SERVICEDESK" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white font-mono uppercase focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Filter Naam <span class="text-red-500">*</span>
</label>
<input type="text" id="filterModal_name" name="filter_name" required placeholder="bijv. DX — Koppelingen, SSO & Migratie" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
JQL Clausule <span class="text-red-500">*</span>
</label>
<textarea id="filterModal_jql" name="jql_clause" rows="3" required placeholder='bijv. "Onderwerp logging[dropdown]" in (Koppelingen, SSO-koppeling, Rest-endpoints, Datadump, UWV-koppeling, Migratie)' class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500"></textarea>
<span class="text-[10px] text-slate-400">Voer de exacte JQL filterclausule in die in Jira op de issues wordt toegepast.</span>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Omschrijving / Toelichting
</label>
<input type="text" id="filterModal_desc" name="description" placeholder="Korte toelichting voor het team" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div class="flex items-center justify-between pt-2">
<label class="flex items-center space-x-2 text-xs text-slate-700 dark:text-slate-300 cursor-pointer">
<input type="checkbox" id="filterModal_active" name="is_active" value="1" checked class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
<span>Filter is actief</span>
</label>
<div class="flex items-center space-x-2">
<button type="button" onclick="closeJiraFilterModal()" 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" class="px-4 py-2 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-500 text-white transition cursor-pointer shadow-xs">
Filter Opslaan
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 3. Organisatie Werkdag Standaarden (Release v1.4) -->
<div class="fociz-card p-6 sm:p-7 space-y-6">
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800/80">
@@ -860,6 +1217,49 @@ function toggleAdminGuide() {
arrow.classList.toggle('rotate-180');
}
}
// Jira Wachtrijfilter Modalfuncties
function openJiraFilterModal() {
document.getElementById('jiraFilterModalTitle').innerText = 'Nieuw Wachtrijfilter Toevoegen';
document.getElementById('filterModal_id').value = '0';
document.getElementById('filterModal_code').value = '';
document.getElementById('filterModal_name').value = '';
document.getElementById('filterModal_project').value = 'SD';
document.getElementById('filterModal_jql').value = '';
document.getElementById('filterModal_desc').value = '';
document.getElementById('filterModal_active').checked = true;
const modal = document.getElementById('jiraFilterModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function editJiraFilter(filter) {
document.getElementById('jiraFilterModalTitle').innerText = `Wachtrijfilter '${filter.filter_name}' Bewerken`;
document.getElementById('filterModal_id').value = filter.id || '0';
document.getElementById('filterModal_code').value = filter.filter_code || '';
document.getElementById('filterModal_name').value = filter.filter_name || '';
document.getElementById('filterModal_project').value = filter.project_key || '';
document.getElementById('filterModal_jql').value = filter.jql_clause || '';
document.getElementById('filterModal_desc').value = filter.description || '';
document.getElementById('filterModal_active').checked = (filter.is_active != 0);
const modal = document.getElementById('jiraFilterModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeJiraFilterModal() {
const modal = document.getElementById('jiraFilterModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeJiraFilterModal();
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
+101
View File
@@ -272,6 +272,107 @@ try {
exit;
}
// 5a. Jira Wachtrijfilters Ophalen
if ($action === 'get_jira_queue_filters') {
$onlyActive = !isset($_GET['all']) && !isset($_POST['all']);
$filters = jira_get_queue_filters($onlyActive);
echo json_encode(['success' => true, 'filters' => $filters]);
exit;
}
// 5b. Jira Wachtrijfilter Opslaan / Wijzigen (Alleen Beheerders)
if ($action === 'save_jira_queue_filter') {
if (($user['role'] ?? '') !== 'ADMIN') {
echo json_encode(['error' => 'Alleen beheerders mogen wachtrijfilters aanpassen.']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$id = (int)($input['id'] ?? 0);
$filterCode = strtoupper(trim($input['filter_code'] ?? ''));
$filterName = trim($input['filter_name'] ?? '');
$projectKey = strtoupper(trim($input['project_key'] ?? ''));
$jqlClause = trim($input['jql_clause'] ?? '');
$description = trim($input['description'] ?? '');
$isActive = isset($input['is_active']) ? (int)$input['is_active'] : 1;
$sortOrder = (int)($input['sort_order'] ?? 0);
if (empty($filterCode) || empty($filterName) || empty($jqlClause)) {
echo json_encode(['error' => 'Code, naam en JQL-clausule zijn verplicht.']);
exit;
}
if ($id > 0) {
$stmt = $db->prepare("
UPDATE jira_queue_filters
SET filter_code = ?, filter_name = ?, project_key = ?, jql_clause = ?, description = ?, is_active = ?, sort_order = ?, updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder, $id]);
} else {
$stmt = $db->prepare("
INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
filter_name = VALUES(filter_name),
project_key = VALUES(project_key),
jql_clause = VALUES(jql_clause),
description = VALUES(description),
is_active = VALUES(is_active),
sort_order = VALUES(sort_order),
updated_at = NOW()
");
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]);
}
echo json_encode(['success' => true, 'message' => "Wachtrijfilter '{$filterName}' is succesvol opgeslagen."]);
exit;
}
// 5c. Jira Wachtrijfilter Verwijderen (Alleen Beheerders)
if ($action === 'delete_jira_queue_filter') {
if (($user['role'] ?? '') !== 'ADMIN') {
echo json_encode(['error' => 'Alleen beheerders mogen wachtrijfilters verwijderen.']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$id = (int)($input['id'] ?? 0);
$filterCode = trim($input['filter_code'] ?? '');
if ($id > 0) {
$stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?");
$stmt->execute([$id]);
} elseif (!empty($filterCode)) {
$stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE filter_code = ?");
$stmt->execute([$filterCode]);
} else {
echo json_encode(['error' => 'Filter ID of code ontbreekt.']);
exit;
}
echo json_encode(['success' => true, 'message' => 'Wachtrijfilter is verwijderd.']);
exit;
}
// 5d. Standaard Wachtrijfilter Toewijzen aan Gebruiker (Beheerders of Gebruiker zelf)
if ($action === 'set_user_jira_filter') {
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$targetUserId = (int)($input['user_id'] ?? $user['id']);
$filterCode = trim($input['filter_code'] ?? '');
if ($targetUserId !== (int)$user['id'] && ($user['role'] ?? '') !== 'ADMIN') {
echo json_encode(['error' => 'Geen toestemming om instellingen van andere gebruikers te wijzigen.']);
exit;
}
$stmt = $db->prepare("UPDATE users SET default_jira_filter_code = ? WHERE id = ?");
$stmt->execute([$filterCode ?: null, $targetUserId]);
echo json_encode(['success' => true, 'message' => 'Wachtrijvoorkeur is succesvol bijgewerkt.']);
exit;
}
echo json_encode(['error' => 'Onbekende actie']);
} catch (Exception $e) {
http_response_code(500);
+31
View File
@@ -147,8 +147,10 @@ function ensure_tables_exist(PDO $pdo): void {
"ALTER TABLE users ADD COLUMN theme VARCHAR(20) NOT NULL DEFAULT 'dark' AFTER notification_settings",
"ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER theme",
"ALTER TABLE users ADD COLUMN backup_codes TEXT NULL AFTER two_factor_enabled",
"ALTER TABLE users ADD COLUMN default_jira_filter_code VARCHAR(50) NULL AFTER avatar_url",
"ALTER TABLE board_card_schedules ADD UNIQUE KEY uk_sched_card (card_id)",
"CREATE TABLE IF NOT EXISTS `password_resets` (`id` INT AUTO_INCREMENT PRIMARY KEY, `email` VARCHAR(191) NOT NULL, `token` VARCHAR(64) NOT NULL UNIQUE, `expires_at` DATETIME NOT NULL, `used_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`email`), INDEX (`token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS `jira_queue_filters` (`id` INT AUTO_INCREMENT PRIMARY KEY, `filter_code` VARCHAR(50) NOT NULL UNIQUE, `filter_name` VARCHAR(120) NOT NULL, `project_key` VARCHAR(50) NULL, `jql_clause` TEXT NOT NULL, `description` VARCHAR(255) NULL, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `sort_order` INT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
];
foreach ($colUpgrades as $sql) {
try {
@@ -158,6 +160,35 @@ function ensure_tables_exist(PDO $pdo): void {
}
}
// Zorg dat de standaard Jira Servicedesk wachtrijfilters (DX en SD) aanwezig zijn
try {
$filterCount = (int)$pdo->query("SELECT COUNT(*) FROM jira_queue_filters")->fetchColumn();
if ($filterCount === 0) {
$insertFilterStmt = $pdo->prepare("
INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order)
VALUES (?, ?, ?, ?, ?, 1, ?)
");
$insertFilterStmt->execute([
'DX',
'DX — Koppelingen, SSO & Migratie',
'SD',
'"Onderwerp logging[dropdown]" in (Koppelingen, SSO-koppeling, Rest-endpoints, Datadump, UWV-koppeling, Migratie)',
'Servicedesk tickets met betrekking tot koppelingen, endpoints en datamigraties.',
1
]);
$insertFilterStmt->execute([
'SD',
'SD — Regulier Servicedesk',
'SD',
'("Onderwerp logging[Dropdown]" = EMPTY OR "Onderwerp logging[Dropdown]" != Koppelingen AND "Onderwerp logging[Dropdown]" != Migratie AND "Onderwerp logging[Dropdown]" != SSO-koppeling AND "Onderwerp logging[Dropdown]" != UWV-koppeling AND "Onderwerp logging[Dropdown]" != Rest-endpoints AND "Onderwerp logging[Dropdown]" != Datadump)',
'Reguliere servicedesk incidenten, algemene meldingen en gebruikersvragen.',
2
]);
}
} catch (Throwable $e) {
// Stil negeren indien tabel nog in opbouw is
}
// Zorg dat er ten minste één standaard bord met standaard lanes aanwezig is
try {
$boardCount = (int)$pdo->query("SELECT COUNT(*) FROM boards")->fetchColumn();
+14
View File
@@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS `users` (
`remember_token_expires_at` DATETIME NULL,
`notification_settings` TEXT NULL,
`avatar_url` VARCHAR(255) NULL,
`default_jira_filter_code` VARCHAR(50) NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -218,3 +219,16 @@ CREATE TABLE IF NOT EXISTS `board_card_schedules` (
UNIQUE KEY `uk_sched_card` (`card_id`),
CONSTRAINT `fk_bsched_card` FOREIGN KEY (`card_id`) REFERENCES `board_cards` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `jira_queue_filters` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`filter_code` VARCHAR(50) NOT NULL UNIQUE,
`filter_name` VARCHAR(120) NOT NULL,
`project_key` VARCHAR(50) NULL,
`jql_clause` TEXT NOT NULL,
`description` VARCHAR(255) NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+365 -97
View File
@@ -142,6 +142,76 @@ function jira_test_connection(string $domain, string $email, string $apiToken):
/**
* Haalt alle actieve issues op die zijn toegewezen aan de ingelogde Jira gebruiker
*/
/**
* Converteert Atlassian Document Format (ADF) of tekst naar platte leesbare tekst
*/
function jira_adf_to_text($adf): string {
if (empty($adf)) {
return '';
}
if (is_string($adf)) {
return trim($adf);
}
if (!is_array($adf)) {
return '';
}
$text = '';
$type = $adf['type'] ?? '';
if ($type === 'text') {
return $adf['text'] ?? '';
}
if (!empty($adf['content']) && is_array($adf['content'])) {
foreach ($adf['content'] as $child) {
$childText = jira_adf_to_text($child);
$childType = $child['type'] ?? '';
if ($childType === 'paragraph' || $childType === 'heading') {
$text .= $childText . "\n\n";
} elseif ($childType === 'listItem') {
$text .= "• " . trim($childText) . "\n";
} elseif ($childType === 'codeBlock') {
$text .= "\n" . trim($childText) . "\n\n";
} else {
$text .= $childText;
}
}
}
return trim($text);
}
/**
* Haalt alle geconfigureerde Jira wachtrijfilters op uit de database
*/
function jira_get_queue_filters(bool $onlyActive = true): array {
try {
$db = get_db_connection();
$sql = "SELECT * FROM jira_queue_filters " . ($onlyActive ? "WHERE is_active = 1 " : "") . "ORDER BY sort_order ASC, id ASC";
return $db->query($sql)->fetchAll() ?: [];
} catch (Throwable $e) {
error_log("jira_get_queue_filters error: " . $e->getMessage());
return [];
}
}
/**
* Haalt de standaard wachtrijfilter voorkeur van een gebruiker op
*/
function jira_get_user_queue_preference(int $userId): ?string {
try {
$db = get_db_connection();
$stmt = $db->prepare("SELECT default_jira_filter_code FROM users WHERE id = ?");
$stmt->execute([$userId]);
$res = $stmt->fetchColumn();
return $res ? (string)$res : null;
} catch (Throwable $e) {
return null;
}
}
/**
* Haalt issues op via een flexibele JQL query via Atlassian REST API
*/
@@ -159,7 +229,9 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri
'assignee',
'reporter',
'duedate',
'updated'
'updated',
'created',
'description',
];
// 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST
@@ -242,13 +314,43 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri
$projectKey = $fields['project']['key'] ?? '';
$dueDate = $fields['duedate'] ?? null;
$updated = $fields['updated'] ?? null;
$created = $fields['created'] ?? null;
$assigneeName = $fields['assignee']['displayName'] ?? 'Niet toegewezen';
$assigneeAvatar = $fields['assignee']['avatarUrls']['48x48'] ?? ($fields['assignee']['avatarUrls']['32x32'] ?? '');
$reporterName = $fields['reporter']['displayName'] ?? 'Onbekend';
$reporterAvatar = $fields['reporter']['avatarUrls']['48x48'] ?? ($fields['reporter']['avatarUrls']['32x32'] ?? '');
$descriptionText = jira_adf_to_text($fields['description'] ?? '');
// Detectie van custom field "Onderwerp logging"
$onderwerpLogging = '';
foreach ($fields as $fKey => $fVal) {
if (str_starts_with($fKey, 'customfield_')) {
if (is_array($fVal) && isset($fVal['value']) && is_string($fVal['value'])) {
$valStr = trim($fVal['value']);
$known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie', 'incident', 'vraag', 'algemeen'];
if (in_array(strtolower($valStr), $known)) {
$onderwerpLogging = $valStr;
}
} elseif (is_string($fVal) && !empty($fVal)) {
$valStr = trim($fVal);
$known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
if (in_array(strtolower($valStr), $known)) {
$onderwerpLogging = $valStr;
}
}
}
}
if (empty($onderwerpLogging) && !empty($fields['onderwerp_logging'])) {
$onderwerpLogging = is_string($fields['onderwerp_logging']) ? $fields['onderwerp_logging'] : ($fields['onderwerp_logging']['value'] ?? '');
}
$issues[] = [
'key' => $key,
'summary' => $summary,
'description' => $descriptionText,
'status' => $statusName,
'status_category' => $statusCategory,
'priority' => $priorityName,
@@ -258,7 +360,11 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri
'project_key' => $projectKey,
'assignee_name' => $assigneeName,
'assignee_avatar' => $assigneeAvatar,
'reporter_name' => $reporterName,
'reporter_avatar' => $reporterAvatar,
'onderwerp_logging' => $onderwerpLogging,
'due_date' => $dueDate,
'created' => $created,
'updated' => $updated,
'url' => "{$cleanDomain}/browse/{$key}",
'source' => 'JIRA_LIVE',
@@ -297,12 +403,101 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
$stmt->execute([$userId]);
$integration = $stmt->fetch();
// Ophalen van actieve wachtrijfilters en voorkeur van gebruiker
$queueFilters = jira_get_queue_filters(true);
$userQueuePref = jira_get_user_queue_preference($userId);
// Sandbox / Demo dataset indien niet gekoppeld of expliciet in demomodus
if (!$integration || !empty($integration['is_demo_mode'])) {
$assigned = [
[
'key' => 'SD-401',
'summary' => 'AFAS Profit SSO-koppeling geeft 401 fout bij token refresh',
'description' => "Gebruikers die via AFAS SSO inloggen krijgen periodiek een HTTP 401 foutcode bij de geautomatiseerde token verversing.\n\nHet endpoint `/oauth/token` reageert met `invalid_grant` zodra de refresh token ouder is dan 24 uur. Er dient een retry-mechanisme met fallback authenticatie ingebouwd te worden.",
'status' => 'In Progress',
'status_category' => 'indeterminate',
'priority' => 'Highest',
'issue_type' => 'Bug',
'project' => 'Servicedesk',
'project_key' => 'SD',
'assignee_name' => 'Mijzelf',
'assignee_avatar' => '',
'reporter_name' => 'Kees van de Berg (AFAS Beheer)',
'reporter_avatar' => '',
'onderwerp_logging' => 'SSO-koppeling',
'due_date' => date('Y-m-d', strtotime('+1 day')),
'created' => date('Y-m-d H:i:s', strtotime('-1 day')),
'updated' => date('Y-m-d H:i:s', strtotime('-45 minutes')),
'url' => 'https://jira.atlassian.net/browse/SD-401',
'source' => 'SANDBOX',
],
[
'key' => 'SD-405',
'summary' => 'REST-endpoints voor medewerker synchronisatie beveiligen met rate limiting',
'description' => "De openbare REST-endpoints voor medewerkerssynchronisatie dienen conform security richtlijnen te worden voorzien van rate limiting (max 120 calls per minuut per IP).\n\nBij overschrijding moet een duidelijke HTTP 429 Retry-After header worden geretourneerd.",
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'High',
'issue_type' => 'Task',
'project' => 'Servicedesk',
'project_key' => 'SD',
'assignee_name' => 'Mijzelf',
'assignee_avatar' => '',
'reporter_name' => 'Security Team',
'reporter_avatar' => '',
'onderwerp_logging' => 'Rest-endpoints',
'due_date' => date('Y-m-d', strtotime('+3 days')),
'created' => date('Y-m-d H:i:s', strtotime('-2 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')),
'url' => 'https://jira.atlassian.net/browse/SD-405',
'source' => 'SANDBOX',
],
[
'key' => 'SD-380',
'summary' => 'Wachtwoord reset e-mail komt niet aan bij nieuwe medewerker',
'description' => "Een nieuwe medewerker ontvangt geen welkomstmail en wachtwoord reset via het selfservice portaal.\n\nControleer de SMTP-verbinding van de mailserver mail.fociz.nl en controleer of het mailadres in de spamfilter is beland.",
'status' => 'In Progress',
'status_category' => 'indeterminate',
'priority' => 'High',
'issue_type' => 'Task',
'project' => 'Servicedesk',
'project_key' => 'SD',
'assignee_name' => 'Mijzelf',
'assignee_avatar' => '',
'reporter_name' => 'HR Servicedesk',
'reporter_avatar' => '',
'onderwerp_logging' => 'EMPTY',
'due_date' => date('Y-m-d'),
'created' => date('Y-m-d H:i:s', strtotime('-3 hours')),
'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')),
'url' => 'https://jira.atlassian.net/browse/SD-380',
'source' => 'SANDBOX',
],
[
'key' => 'SD-385',
'summary' => 'Rechten aanpassen voor rol Projectleider in urenregistratie',
'description' => "Projectleiders hebben momenteel alleen leesrechten op ingediende urenstaten. Graag de rol uitbreiden zodat zij declaraties direct kunnen goedkeuren of afkeuren.",
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'Medium',
'issue_type' => 'Task',
'project' => 'Servicedesk',
'project_key' => 'SD',
'assignee_name' => 'Mijzelf',
'assignee_avatar' => '',
'reporter_name' => 'Marianne Willems',
'reporter_avatar' => '',
'onderwerp_logging' => 'Algemeen',
'due_date' => date('Y-m-d', strtotime('+5 days')),
'created' => date('Y-m-d H:i:s', strtotime('-3 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-1 day')),
'url' => 'https://jira.atlassian.net/browse/SD-385',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-101',
'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren',
'description' => "Ontwikkeling van een complete Google Authenticator / Microsoft Authenticator TOTP verificatie flow inclusief 8 cijferige noodcodes en QR-code weergave in het gebruikersprofiel.",
'status' => 'In Progress',
'status_category' => 'indeterminate',
'priority' => 'High',
@@ -311,7 +506,11 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'project_key' => 'FOC',
'assignee_name' => 'Mijzelf',
'assignee_avatar' => '',
'reporter_name' => 'Product Owner',
'reporter_avatar' => '',
'onderwerp_logging' => 'Beveiliging',
'due_date' => date('Y-m-d', strtotime('+2 days')),
'created' => date('Y-m-d H:i:s', strtotime('-4 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')),
'url' => 'https://jira.atlassian.net/browse/FOC-101',
'source' => 'SANDBOX',
@@ -319,6 +518,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
[
'key' => 'FOC-102',
'summary' => 'Interactief whiteboard canvas voor brainstormsessies',
'description' => "Gebruikers moeten live post-its kunnen slepen, kleuren en stemmen tijdens kwartaal brainstormsessies.",
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'High',
@@ -327,7 +527,11 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'project_key' => 'FOC',
'assignee_name' => 'Mijzelf',
'assignee_avatar' => '',
'reporter_name' => 'Scrum Master',
'reporter_avatar' => '',
'onderwerp_logging' => '',
'due_date' => date('Y-m-d', strtotime('+4 days')),
'created' => date('Y-m-d H:i:s', strtotime('-5 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-1 day')),
'url' => 'https://jira.atlassian.net/browse/FOC-102',
'source' => 'SANDBOX',
@@ -335,6 +539,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
[
'key' => 'FOC-103',
'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API',
'description' => "Synchronisatie van dagplanning blokken naar Microsoft 365 Exchange Online agenda met real-time conflictcontrole.",
'status' => 'In Review',
'status_category' => 'indeterminate',
'priority' => 'Medium',
@@ -343,30 +548,19 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'project_key' => 'INT',
'assignee_name' => 'Mijzelf',
'assignee_avatar' => '',
'reporter_name' => 'Architect',
'reporter_avatar' => '',
'onderwerp_logging' => 'SSO-koppeling',
'due_date' => date('Y-m-d'),
'created' => date('Y-m-d H:i:s', strtotime('-6 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-4 hours')),
'url' => 'https://jira.atlassian.net/browse/FOC-103',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-104',
'summary' => 'Roadmap kwartaalweergave en voortgangsindicatoren',
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'Medium',
'issue_type' => 'Story',
'project' => 'Roadmap Module',
'project_key' => 'ROAD',
'assignee_name' => 'Mijzelf',
'assignee_avatar' => '',
'due_date' => date('Y-m-d', strtotime('+7 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-3 days')),
'url' => 'https://jira.atlassian.net/browse/FOC-104',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-108',
'summary' => 'CSS scrollbar glitch oplossen in Kanban lane op Firefox',
'description' => "Op Windows Firefox versies 128+ verspringt de horizontale scrollbalk bij het slepen van kaarten tussen kolommen.",
'status' => 'In Progress',
'status_category' => 'indeterminate',
'priority' => 'Highest',
@@ -375,7 +569,11 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'project_key' => 'UI',
'assignee_name' => 'Mijzelf',
'assignee_avatar' => '',
'reporter_name' => 'QA Lead',
'reporter_avatar' => '',
'onderwerp_logging' => '',
'due_date' => date('Y-m-d', strtotime('+1 day')),
'created' => date('Y-m-d H:i:s', strtotime('-2 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-30 minutes')),
'url' => 'https://jira.atlassian.net/browse/FOC-108',
'source' => 'SANDBOX',
@@ -383,6 +581,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
[
'key' => 'FOC-112',
'summary' => 'Avatar upload beveiliging en compressie optimalisatie',
'description' => "Server-side image re-encoding en EXIF stripping voor geüploade gebruikersavatars.",
'status' => 'Done',
'status_category' => 'done',
'priority' => 'Medium',
@@ -391,7 +590,11 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'project_key' => 'SEC',
'assignee_name' => 'Mijzelf',
'assignee_avatar' => '',
'reporter_name' => 'SecOps',
'reporter_avatar' => '',
'onderwerp_logging' => '',
'due_date' => date('Y-m-d', strtotime('-1 day')),
'created' => date('Y-m-d H:i:s', strtotime('-7 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-5 hours')),
'url' => 'https://jira.atlassian.net/browse/FOC-112',
'source' => 'SANDBOX',
@@ -399,9 +602,136 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
];
$team = [
[
'key' => 'SD-408',
'summary' => 'UWV-koppeling polisadministratie import loopt vast op datumformaat',
'description' => "De XML batch import vanuit het UWV portaal faalt op records met afwijkende geboortedatumnotatie.\n\nDe parser dient flexibel zowel ISO-8601 (YYYY-MM-DD) als traditioneel formaat (DD-MM-YYYY) te accepteren.",
'status' => 'In Progress',
'status_category' => 'indeterminate',
'priority' => 'High',
'issue_type' => 'Bug',
'project' => 'Servicedesk',
'project_key' => 'SD',
'assignee_name' => 'Lisa Bakker',
'assignee_avatar' => '',
'reporter_name' => 'Servicedesk L2',
'reporter_avatar' => '',
'onderwerp_logging' => 'UWV-koppeling',
'due_date' => date('Y-m-d', strtotime('+2 days')),
'created' => date('Y-m-d H:i:s', strtotime('-2 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')),
'url' => 'https://jira.atlassian.net/browse/SD-408',
'source' => 'SANDBOX',
],
[
'key' => 'SD-412',
'summary' => 'Datadump archief export voor externe accountant audit 2025',
'description' => "Genereer een beveiligde, versleutelde datadump van alle audit-logs en urenstaten voor de periodieke controle.",
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'Medium',
'issue_type' => 'Story',
'project' => 'Servicedesk',
'project_key' => 'SD',
'assignee_name' => 'Daan van Dijk',
'assignee_avatar' => '',
'reporter_name' => 'Compliance Officer',
'reporter_avatar' => '',
'onderwerp_logging' => 'Datadump',
'due_date' => date('Y-m-d', strtotime('+6 days')),
'created' => date('Y-m-d H:i:s', strtotime('-3 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-4 hours')),
'url' => 'https://jira.atlassian.net/browse/SD-412',
'source' => 'SANDBOX',
],
[
'key' => 'SD-416',
'summary' => 'Migratie van oude klantportaal database naar MariaDB 11 cluster',
'description' => "Voorbereiden en uitvoeren van de dataschemamigratie van het oude portaal naar de nieuwe MariaDB 11 clusteromgeving inclusief regressietests.",
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'High',
'issue_type' => 'Story',
'project' => 'Servicedesk',
'project_key' => 'SD',
'assignee_name' => 'Bram Meijer',
'assignee_avatar' => '',
'reporter_name' => 'Lead Engineer',
'reporter_avatar' => '',
'onderwerp_logging' => 'Migratie',
'due_date' => date('Y-m-d', strtotime('+8 days')),
'created' => date('Y-m-d H:i:s', strtotime('-4 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')),
'url' => 'https://jira.atlassian.net/browse/SD-416',
'source' => 'SANDBOX',
],
[
'key' => 'SD-420',
'summary' => 'Koppelingen API webhook notificaties afleveren met retry-mechanisme',
'description' => "Webhooks die naar externe partnersystemen worden verzonden moeten bij een tijdelijke 5xx fout automatisch 3x opnieuw worden aangeboden (exponential backoff).",
'status' => 'In Review',
'status_category' => 'indeterminate',
'priority' => 'Medium',
'issue_type' => 'Task',
'project' => 'Servicedesk',
'project_key' => 'SD',
'assignee_name' => 'Sophie de Vries',
'assignee_avatar' => '',
'reporter_name' => 'Integratie Specialist',
'reporter_avatar' => '',
'onderwerp_logging' => 'Koppelingen',
'due_date' => date('Y-m-d', strtotime('+4 days')),
'created' => date('Y-m-d H:i:s', strtotime('-5 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-3 hours')),
'url' => 'https://jira.atlassian.net/browse/SD-420',
'source' => 'SANDBOX',
],
[
'key' => 'SD-392',
'summary' => 'Outlook Agenda synchronisatie toont dubbele afspraken op mobiel',
'description' => "Bij enkele gebruikers met de iOS Outlook app worden terugkerende agenda-afspraken tweemaal weergegeven na het synchroniseren.",
'status' => 'In Progress',
'status_category' => 'indeterminate',
'priority' => 'Medium',
'issue_type' => 'Bug',
'project' => 'Servicedesk',
'project_key' => 'SD',
'assignee_name' => 'Lisa Bakker',
'assignee_avatar' => '',
'reporter_name' => 'Servicedesk Support',
'reporter_avatar' => '',
'onderwerp_logging' => 'Incident',
'due_date' => date('Y-m-d', strtotime('+3 days')),
'created' => date('Y-m-d H:i:s', strtotime('-3 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')),
'url' => 'https://jira.atlassian.net/browse/SD-392',
'source' => 'SANDBOX',
],
[
'key' => 'SD-399',
'summary' => 'Vraag over exportmogelijkheden naar Excel rapportage',
'description' => "Klant informeert of het mogelijk is om een wekelijkse CSV / Excel export van de urenstaten automatisch per mail te ontvangen.",
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'Low',
'issue_type' => 'Task',
'project' => 'Servicedesk',
'project_key' => 'SD',
'assignee_name' => 'Thomas Jansen',
'assignee_avatar' => '',
'reporter_name' => 'Financieel Directeur',
'reporter_avatar' => '',
'onderwerp_logging' => 'Vraag',
'due_date' => date('Y-m-d', strtotime('+10 days')),
'created' => date('Y-m-d H:i:s', strtotime('-5 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-1 day')),
'url' => 'https://jira.atlassian.net/browse/SD-399',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-95',
'summary' => 'Sessie timeout notificatie verschijnt niet tijdig bij inactieve tab',
'description' => "Wanneer een gebruiker de browser 2 uur niet aanraakt, vervalt de PHP sessie zonder dat het login-dialoogvenster netjes openspringt.",
'status' => 'In Progress',
'status_category' => 'indeterminate',
'priority' => 'Highest',
@@ -410,7 +740,11 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'project_key' => 'FOC',
'assignee_name' => 'Lisa Bakker',
'assignee_avatar' => '',
'reporter_name' => 'Product QA',
'reporter_avatar' => '',
'onderwerp_logging' => '',
'due_date' => date('Y-m-d', strtotime('+1 day')),
'created' => date('Y-m-d H:i:s', strtotime('-2 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')),
'url' => 'https://jira.atlassian.net/browse/FOC-95',
'source' => 'SANDBOX',
@@ -418,6 +752,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
[
'key' => 'FOC-98',
'summary' => 'Automatische e-mail triggers voor taakdeadlines en uitnodigingen',
'description' => "Cronjob instellen die 24 uur voor de deadline van een taak een herinneringsmail stuurt via SMTP.",
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'High',
@@ -426,46 +761,19 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'project_key' => 'NOTIF',
'assignee_name' => 'Daan van Dijk',
'assignee_avatar' => '',
'reporter_name' => 'Operations',
'reporter_avatar' => '',
'onderwerp_logging' => '',
'due_date' => date('Y-m-d', strtotime('+3 days')),
'created' => date('Y-m-d H:i:s', strtotime('-3 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-2 days')),
'url' => 'https://jira.atlassian.net/browse/FOC-98',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-105',
'summary' => 'Database indexering optimaliseren voor grote audit logs',
'status' => 'In Review',
'status_category' => 'indeterminate',
'priority' => 'Medium',
'issue_type' => 'Task',
'project' => 'Database & Core',
'project_key' => 'CORE',
'assignee_name' => 'Sophie de Vries',
'assignee_avatar' => '',
'due_date' => date('Y-m-d', strtotime('+5 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-1 day')),
'url' => 'https://jira.atlassian.net/browse/FOC-105',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-107',
'summary' => 'Exporteer wekelijkse team rapportage naar PDF en Excel spreadsheet',
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'Low',
'issue_type' => 'Feature',
'project' => 'Reporting',
'project_key' => 'REP',
'assignee_name' => 'Thomas Jansen',
'assignee_avatar' => '',
'due_date' => date('Y-m-d', strtotime('+10 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-4 days')),
'url' => 'https://jira.atlassian.net/browse/FOC-107',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-110',
'summary' => 'Real-time websocket synchronisatie voor gelijktijdig bewerken van borden',
'description' => "Voorkom overschrijfconflicten wanneer meerdere collega's gelijktijdig kaarten verplaatsen op hetzelfde Kanban bord.",
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'High',
@@ -474,59 +782,15 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'project_key' => 'RT',
'assignee_name' => 'Bram Meijer',
'assignee_avatar' => '',
'reporter_name' => 'Architect',
'reporter_avatar' => '',
'onderwerp_logging' => '',
'due_date' => date('Y-m-d', strtotime('+6 days')),
'created' => date('Y-m-d H:i:s', strtotime('-4 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')),
'url' => 'https://jira.atlassian.net/browse/FOC-110',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-114',
'summary' => 'Datum picker springt terug naar 1970 bij lege invoer',
'status' => 'To Do',
'status_category' => 'new',
'priority' => 'Medium',
'issue_type' => 'Bug',
'project' => 'UI/UX Redesign',
'project_key' => 'UI',
'assignee_name' => 'Niet toegewezen',
'assignee_avatar' => '',
'due_date' => null,
'updated' => date('Y-m-d H:i:s', strtotime('-2 days')),
'url' => 'https://jira.atlassian.net/browse/FOC-114',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-89',
'summary' => 'GDPR / AVG compliancy en geautomatiseerd data retentiebeleid',
'status' => 'In Progress',
'status_category' => 'indeterminate',
'priority' => 'Medium',
'issue_type' => 'Epic',
'project' => 'Security',
'project_key' => 'SEC',
'assignee_name' => 'Lisa Bakker',
'assignee_avatar' => '',
'due_date' => date('Y-m-d', strtotime('+14 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-3 hours')),
'url' => 'https://jira.atlassian.net/browse/FOC-89',
'source' => 'SANDBOX',
],
[
'key' => 'FOC-82',
'summary' => 'Performance optimalisatie van vector avatar rendering in navigatiebalk',
'status' => 'Done',
'status_category' => 'done',
'priority' => 'Low',
'issue_type' => 'Task',
'project' => 'UI/UX Redesign',
'project_key' => 'UI',
'assignee_name' => 'Sophie de Vries',
'assignee_avatar' => '',
'due_date' => date('Y-m-d', strtotime('-3 days')),
'updated' => date('Y-m-d H:i:s', strtotime('-2 days')),
'url' => 'https://jira.atlassian.net/browse/FOC-82',
'source' => 'SANDBOX',
],
];
return [
@@ -538,6 +802,8 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'last_sync_at' => date('Y-m-d H:i:s'),
'assigned_issues' => $assigned,
'team_issues' => $team,
'queue_filters' => $queueFilters,
'default_queue_filter' => $userQueuePref,
];
}
@@ -595,6 +861,8 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
'last_sync_at' => date('Y-m-d H:i:s'),
'assigned_issues' => $assignedIssues,
'team_issues' => $teamIssues,
'queue_filters' => $queueFilters,
'default_queue_filter' => $userQueuePref,
];
$_SESSION[$cacheKey] = [
+376 -16
View File
@@ -56,16 +56,22 @@ foreach ($boards as &$b) {
}
unset($b);
// Verzamel unieke projecten en types voor filter dropdowns
// 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'])) {
$allProjects[$iss['project']] = $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'];
@@ -78,6 +84,9 @@ 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);
?>
@@ -229,6 +238,97 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
</div>
</div>
<!-- 1. Snelle Project Switcher (Pill Tabs) -->
<div class="fociz-card p-4 sm:p-5">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 pb-3 mb-3 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
<i data-lucide="folder-kanban" class="w-4 h-4"></i>
</div>
<div>
<h2 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">Jira Projecten</h2>
<p class="text-[11px] text-slate-500 dark:text-slate-400">Klik op een project om direct te filteren</p>
</div>
</div>
<div class="text-[11px] text-slate-400 hidden sm:block">
<span><?= count($allProjects) ?> actieve projecten gevonden</span>
</div>
</div>
<div class="flex items-center gap-2 overflow-x-auto pb-1" id="projectPillsContainer">
<!-- Alle Projecten Pill -->
<button type="button" onclick="selectProject('')" data-project="" class="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">
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
<span>Alle Projecten</span>
<span class="px-2 py-0.5 rounded-full text-[10px] bg-white/20 text-white font-black"><?= count($allIssues) ?></span>
</button>
<!-- Per project pills -->
<?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-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">
<?php if (!empty($pData['key'])): ?>
<span class="px-1.5 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.5 h-3.5 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-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300"><?= $projectCounts[$pName] ?? 0 ?></span>
</button>
<?php endforeach; ?>
</div>
</div>
<!-- 2. Servicedesk Wachtrijfilter Sub-balk (DX / SD) -->
<div id="queueFiltersBar" class="fociz-card p-4 bg-gradient-to-r from-slate-50 via-indigo-50/30 to-sky-50/20 dark:from-[#0c0f17] dark:via-indigo-950/20 dark:to-sky-950/20 border-indigo-200/70 dark:border-indigo-900/40">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 rounded-xl bg-indigo-500/15 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold flex-shrink-0">
<i data-lucide="filter" class="w-4 h-4"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<span class="text-xs font-bold text-slate-900 dark:text-white">Servicedesk Wachtrijfilters</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>
<p class="text-[11px] text-slate-500 dark:text-slate-400">
Filtert op onderwerp logging routering (DX voor koppelingen & endpoints, SD voor reguliere support)
</p>
</div>
</div>
<!-- Knoppen rij voor wachtrijen -->
<div class="flex items-center gap-2 overflow-x-auto" id="queueButtonsContainer">
<button type="button" onclick="selectQueueFilter('')" data-queue="" class="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-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300 flex-shrink-0">
<i data-lucide="list" class="w-3.5 h-3.5"></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.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">
<i data-lucide="zap" class="w-3.5 h-3.5 text-amber-500"></i>
<span>DX Wachtrij (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.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">
<i data-lucide="wrench" class="w-3.5 h-3.5 text-sky-500"></i>
<span>SD Wachtrij (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 if ($isAdmin): ?>
<a href="/admin.php#jira-wachtrijen" title="Wachtrijfilters beheren in Admin" class="p-1.5 rounded-xl 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>
<!-- Filterbalk & Zoekveld -->
<div class="fociz-card p-4">
<div class="grid grid-cols-1 md:grid-cols-12 gap-3">
@@ -250,10 +350,10 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
<!-- Filter: Project -->
<div class="md:col-span-2">
<select id="filterProject" onchange="applyFilters()" class="w-full bg-slate-50 dark:bg-slate-900 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">
<select id="filterProject" onchange="onProjectSelectChange(this.value)" class="w-full bg-slate-50 dark:bg-slate-900 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="">Alle projecten</option>
<?php foreach ($allProjects as $p): ?>
<option value="<?= htmlspecialchars($p) ?>"><?= htmlspecialchars($p) ?></option>
<?php foreach ($allProjects as $pName => $pData): ?>
<option value="<?= htmlspecialchars($pName) ?>"><?= htmlspecialchars($pName) ?></option>
<?php endforeach; ?>
</select>
</div>
@@ -573,7 +673,10 @@ 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();
// Bereken statussen en KPI statistieken
function calculateKpis() {
@@ -629,9 +732,97 @@ function switchTab(tab) {
}
});
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');
pills.forEach(p => {
const pKey = p.getAttribute('data-project') || '';
if (pKey === currentProject) {
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";
}
});
}
// 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;
if (currentProject) {
currentList = currentList.filter(iss => iss.project === currentProject);
}
const dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
let dxCount = 0;
let sdCount = 0;
currentList.forEach(iss => {
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
if (dxValues.includes(ol)) {
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;
}
// Snelkoppeling KPI-kaart Urgent
function filterByUrgent() {
const pSelect = document.getElementById('filterPriority');
@@ -653,6 +844,8 @@ function clearSearch() {
// Alle filters herstellen
function resetAllFilters() {
currentProject = '';
currentQueue = '';
const searchInput = document.getElementById('jiraSearchInput');
const projectSelect = document.getElementById('filterProject');
const typeSelect = document.getElementById('filterType');
@@ -665,14 +858,17 @@ function resetAllFilters() {
if (prioritySelect) prioritySelect.value = '';
if (statusSelect) statusSelect.value = '';
document.getElementById('clearSearchBtn').classList.add('hidden');
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 = document.getElementById('filterProject')?.value || '';
const projectVal = currentProject || (document.getElementById('filterProject')?.value || '');
const typeVal = document.getElementById('filterType')?.value || '';
const priorityVal = document.getElementById('filterPriority')?.value || '';
const statusVal = document.getElementById('filterStatus')?.value || '';
@@ -706,19 +902,42 @@ function applyFilters() {
});
}
// Pas zoek- en selectiefilters toe
const dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
// 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);
if (!keyMatch && !sumMatch && !prjMatch) return false;
const descMatch = (iss.description || '').toLowerCase().includes(searchVal);
const repMatch = (iss.reporter_name || '').toLowerCase().includes(searchVal);
const olMatch = (iss.onderwerp_logging || '').toLowerCase().includes(searchVal);
if (!keyMatch && !sumMatch && !prjMatch && !descMatch && !repMatch && !olMatch) return false;
}
if (projectVal && iss.project !== projectVal) {
return false;
}
if (currentQueue) {
const qCode = currentQueue.toUpperCase();
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
if (qCode === 'DX') {
if (!dxValues.includes(ol)) return false;
} else if (qCode === 'SD') {
if (ol !== '' && ol !== 'empty' && dxValues.includes(ol)) return false;
} else {
const fObj = (jiraData.queue_filters || []).find(f => f.filter_code.toUpperCase() === qCode);
if (fObj) {
if (ol && !ol.includes(qCode.toLowerCase()) && !fObj.jql_clause.toLowerCase().includes(ol)) {
return false;
}
}
}
}
if (typeVal && iss.issue_type !== typeVal) {
return false;
}
@@ -741,6 +960,36 @@ function applyFilters() {
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');
@@ -842,11 +1091,38 @@ function renderTickets(tickets) {
`;
}
// 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' : '';
// JSON stringified ticket data voor modalaanroep
const ticketSafeJson = JSON.stringify(ticket).replace(/"/g, '&quot;');
html += `
<div class="fociz-card p-4 sm:p-5 transition hover:border-indigo-500/40 dark:hover:border-indigo-500/40">
<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 -->
@@ -888,23 +1164,29 @@ function renderTickets(tickets) {
${dueDateHtml}
</div>
<!-- Ticket Titel / Summary -->
<h3 class="text-sm sm:text-base font-bold text-slate-900 dark:text-white leading-snug">
<a href="${escapeHtml(ticket.url)}" target="_blank" rel="noopener noreferrer" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition">
<!-- 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)')}
</a>
</span>
</h3>
<!-- Sub-info: Assignee & Update timestamp -->
<!-- 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 -->
<!-- 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(${ticketSafeJson})' 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>
@@ -921,6 +1203,79 @@ function renderTickets(tickets) {
</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(${ticketSafeJson})' 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(${ticketSafeJson})' 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>
`;
});
@@ -1191,6 +1546,11 @@ document.addEventListener('keydown', function(e) {
// Initiële opstart
document.addEventListener('DOMContentLoaded', () => {
calculateKpis();
updateProjectPills();
updateQueueCounts();
if (currentQueue) {
updateQueuePills();
}
applyFilters();
});
</script>