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