Jira en admin aanpassingen #83
@@ -17,8 +17,10 @@ $db = get_db_connection();
|
|||||||
$message = null;
|
$message = null;
|
||||||
$messageType = 'success';
|
$messageType = 'success';
|
||||||
|
|
||||||
$validTabs = ['users', 'calendar', 'microsoft', 'jira', 'ai', 'security', 'system'];
|
$validTabs = ['users', 'calendar', 'microsoft', 'projects', 'security', 'system'];
|
||||||
$activeTab = $_GET['tab'] ?? 'users';
|
$activeTab = $_GET['tab'] ?? 'users';
|
||||||
|
if ($activeTab === 'jira') $activeTab = 'projects';
|
||||||
|
if ($activeTab === 'ai') $activeTab = 'system';
|
||||||
if (!in_array($activeTab, $validTabs, true)) {
|
if (!in_array($activeTab, $validTabs, true)) {
|
||||||
$activeTab = 'users';
|
$activeTab = 'users';
|
||||||
}
|
}
|
||||||
@@ -33,8 +35,11 @@ if (!empty($_GET['success'])) {
|
|||||||
|
|
||||||
// POST Verwerking VÓÓR header output zodat redirects en opslaan vlekkeloos werken
|
// POST Verwerking VÓÓR header output zodat redirects en opslaan vlekkeloos werken
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
if (!empty($_POST['active_tab']) && in_array($_POST['active_tab'], $validTabs, true)) {
|
$postedTab = $_POST['active_tab'] ?? '';
|
||||||
$activeTab = $_POST['active_tab'];
|
if ($postedTab === 'jira') $postedTab = 'projects';
|
||||||
|
if ($postedTab === 'ai') $postedTab = 'system';
|
||||||
|
if (!empty($postedTab) && in_array($postedTab, $validTabs, true)) {
|
||||||
|
$activeTab = $postedTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!verify_csrf_token($_POST['csrf_token'] ?? null)) {
|
if (!verify_csrf_token($_POST['csrf_token'] ?? null)) {
|
||||||
@@ -87,7 +92,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
// 2. Centraal Jira Bedrijfsdomein opslaan
|
// 2. Centraal Jira Bedrijfsdomein opslaan
|
||||||
elseif ($action === 'save_jira_domain') {
|
elseif ($action === 'save_jira_domain') {
|
||||||
$activeTab = 'jira';
|
$activeTab = 'projects';
|
||||||
$rawDomain = trim($_POST['jira_domain'] ?? '');
|
$rawDomain = trim($_POST['jira_domain'] ?? '');
|
||||||
$companyDomain = !empty($rawDomain) ? jira_normalize_domain($rawDomain) : '';
|
$companyDomain = !empty($rawDomain) ? jira_normalize_domain($rawDomain) : '';
|
||||||
|
|
||||||
@@ -115,7 +120,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
// 2b. Jira Wachtrijfilter toevoegen of bewerken
|
// 2b. Jira Wachtrijfilter toevoegen of bewerken
|
||||||
elseif ($action === 'save_jira_queue_filter') {
|
elseif ($action === 'save_jira_queue_filter') {
|
||||||
$activeTab = 'jira';
|
$activeTab = 'projects';
|
||||||
$filterId = (int)($_POST['filter_id'] ?? 0);
|
$filterId = (int)($_POST['filter_id'] ?? 0);
|
||||||
$filterCode = strtoupper(trim($_POST['filter_code'] ?? ''));
|
$filterCode = strtoupper(trim($_POST['filter_code'] ?? ''));
|
||||||
$filterName = trim($_POST['filter_name'] ?? '');
|
$filterName = trim($_POST['filter_name'] ?? '');
|
||||||
@@ -142,13 +147,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order)
|
INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
filter_name = VALUES(filter_name),
|
filter_name = VALUES(filter_name),
|
||||||
project_key = VALUES(project_key),
|
project_key = VALUES(project_key),
|
||||||
jql_clause = VALUES(jql_clause),
|
jql_clause = VALUES(jql_clause),
|
||||||
description = VALUES(description),
|
description = VALUES(description),
|
||||||
is_active = VALUES(is_active),
|
is_active = VALUES(is_active),
|
||||||
sort_order = VALUES(sort_order),
|
sort_order = VALUES(sort_order),
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
");
|
");
|
||||||
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]);
|
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]);
|
||||||
$message = "Nieuw wachtrijfilter '{$filterName}' is succesvol toegevoegd.";
|
$message = "Nieuw wachtrijfilter '{$filterName}' is succesvol toegevoegd.";
|
||||||
@@ -160,7 +165,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
// 2c. Jira Wachtrijfilter verwijderen
|
// 2c. Jira Wachtrijfilter verwijderen
|
||||||
elseif ($action === 'delete_jira_queue_filter') {
|
elseif ($action === 'delete_jira_queue_filter') {
|
||||||
$activeTab = 'jira';
|
$activeTab = 'projects';
|
||||||
$filterId = (int)($_POST['filter_id'] ?? 0);
|
$filterId = (int)($_POST['filter_id'] ?? 0);
|
||||||
if ($filterId > 0) {
|
if ($filterId > 0) {
|
||||||
$stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?");
|
$stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?");
|
||||||
@@ -173,7 +178,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
// 2d. Teamleden toewijzen aan Jira Wachtrijfilters (meervoudig)
|
// 2d. Teamleden toewijzen aan Jira Wachtrijfilters (meervoudig)
|
||||||
elseif ($action === 'save_user_jira_assignments') {
|
elseif ($action === 'save_user_jira_assignments') {
|
||||||
$activeTab = 'jira';
|
$activeTab = 'projects';
|
||||||
$assignments = $_POST['user_filters'] ?? [];
|
$assignments = $_POST['user_filters'] ?? [];
|
||||||
$submittedUserIds = isset($_POST['user_ids']) && is_array($_POST['user_ids']) ? array_map('intval', $_POST['user_ids']) : [];
|
$submittedUserIds = isset($_POST['user_ids']) && is_array($_POST['user_ids']) ? array_map('intval', $_POST['user_ids']) : [];
|
||||||
|
|
||||||
@@ -372,12 +377,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
$message = "Brainstorm categorieën zijn succesvol opgeslagen.";
|
$message = "Brainstorm categorieën zijn succesvol opgeslagen.";
|
||||||
$messageType = 'success';
|
$messageType = 'success';
|
||||||
$activeTab = 'system';
|
$activeTab = 'projects';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. AI Co-Pilot instellingen opslaan (Release v1.5)
|
// 4. AI Co-Pilot instellingen opslaan (Release v1.5)
|
||||||
elseif ($action === 'save_ai_settings') {
|
elseif ($action === 'save_ai_settings') {
|
||||||
$activeTab = 'ai';
|
$activeTab = 'system';
|
||||||
try {
|
try {
|
||||||
$aiEnabled = isset($_POST['ai_enabled']) ? 1 : 0;
|
$aiEnabled = isset($_POST['ai_enabled']) ? 1 : 0;
|
||||||
$aiModel = trim((string)($_POST['ai_model'] ?? 'gemini-1.5-pro'));
|
$aiModel = trim((string)($_POST['ai_model'] ?? 'gemini-1.5-pro'));
|
||||||
@@ -631,7 +636,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Tab Navigatie Header (7 Tabs) -->
|
<!-- Tab Navigatie Header (6 Tabs) -->
|
||||||
<div class="fociz-card p-2 shadow-xs">
|
<div class="fociz-card p-2 shadow-xs">
|
||||||
<div class="flex items-center space-x-1 sm:space-x-2 overflow-x-auto no-scrollbar" id="adminTabsNav">
|
<div class="flex items-center space-x-1 sm:space-x-2 overflow-x-auto no-scrollbar" id="adminTabsNav">
|
||||||
<button type="button" onclick="switchAdminTab('users')" id="tab-btn-users" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
<button type="button" onclick="switchAdminTab('users')" id="tab-btn-users" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
||||||
@@ -650,17 +655,13 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
<span class="w-2 h-2 rounded-full bg-teal-400"></span>
|
<span class="w-2 h-2 rounded-full bg-teal-400"></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onclick="switchAdminTab('jira')" id="tab-btn-jira" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
<button type="button" onclick="switchAdminTab('projects')" id="tab-btn-projects" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
||||||
<i data-lucide="check-square" class="w-4 h-4"></i>
|
<i data-lucide="layers" class="w-4 h-4"></i>
|
||||||
<span>Jira</span>
|
<span>Projecten</span>
|
||||||
<?php if ($jiraConnectedCount > 0): ?>
|
<?php if ($jiraConnectedCount > 0): ?>
|
||||||
<span class="ml-1 px-1.5 py-0.5 rounded-full text-[10px] bg-sky-500/20 text-sky-400 font-extrabold"><?= $jiraConnectedCount ?></span>
|
<span class="ml-1 px-1.5 py-0.5 rounded-full text-[10px] bg-sky-500/20 text-sky-400 font-extrabold"><?= $jiraConnectedCount ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onclick="switchAdminTab('ai')" id="tab-btn-ai" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
|
||||||
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
|
||||||
<span>AI Co-Pilot</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" onclick="switchAdminTab('security')" id="tab-btn-security" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
<button type="button" onclick="switchAdminTab('security')" id="tab-btn-security" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
||||||
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
||||||
<span>Beveiliging & Audit</span>
|
<span>Beveiliging & Audit</span>
|
||||||
@@ -1205,8 +1206,8 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 4: Jira -->
|
<!-- Tab 4: Projecten (Jira & Brainstorm Categorieën) -->
|
||||||
<div id="tab-pane-jira" class="admin-tab-pane space-y-8 hidden">
|
<div id="tab-pane-projects" class="admin-tab-pane space-y-8 hidden">
|
||||||
<!-- 2. Atlassian Jira Bedrijfsinstellingen -->
|
<!-- 2. Atlassian Jira Bedrijfsinstellingen -->
|
||||||
<div class="fociz-card p-6 sm:p-7">
|
<div class="fociz-card p-6 sm:p-7">
|
||||||
<div class="flex items-center space-x-3.5 pb-6 border-b border-slate-100 dark:border-slate-800/80">
|
<div class="flex items-center space-x-3.5 pb-6 border-b border-slate-100 dark:border-slate-800/80">
|
||||||
@@ -1232,7 +1233,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
|
|
||||||
<form method="POST" class="mt-6 space-y-4">
|
<form method="POST" class="mt-6 space-y-4">
|
||||||
<input type="hidden" name="action" value="save_jira_domain">
|
<input type="hidden" name="action" value="save_jira_domain">
|
||||||
<input type="hidden" name="active_tab" value="jira">
|
<input type="hidden" name="active_tab" value="projects">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -1367,7 +1368,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</button>
|
</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">
|
<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="action" value="delete_jira_queue_filter">
|
||||||
<input type="hidden" name="active_tab" value="jira">
|
<input type="hidden" name="active_tab" value="projects">
|
||||||
<input type="hidden" name="filter_id" value="<?= (int)$filter['id'] ?>">
|
<input type="hidden" name="filter_id" value="<?= (int)$filter['id'] ?>">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||||
<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">
|
<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">
|
||||||
@@ -1397,7 +1398,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
|
|
||||||
<form method="POST" action="/admin.php" class="space-y-4">
|
<form method="POST" action="/admin.php" class="space-y-4">
|
||||||
<input type="hidden" name="action" value="save_user_jira_assignments">
|
<input type="hidden" name="action" value="save_user_jira_assignments">
|
||||||
<input type="hidden" name="active_tab" value="jira">
|
<input type="hidden" name="active_tab" value="projects">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||||
|
|
||||||
<div class="overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800">
|
<div class="overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800">
|
||||||
@@ -1477,6 +1478,87 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Brainstorm Categorieën Beheer Card -->
|
||||||
|
<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-5 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-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
|
||||||
|
<i data-lucide="lightbulb" 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">Brainstorm Categorieën</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-300 text-[10px] font-bold">
|
||||||
|
Beheerbaar
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400">Beheer de beschikbare categorieën voor nieuwe brainstormsessies</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" class="space-y-5">
|
||||||
|
<input type="hidden" name="action" value="save_brainstorm_categories">
|
||||||
|
<input type="hidden" name="active_tab" value="projects">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="grid grid-cols-12 gap-3 px-3 py-1.5 text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider border-b border-slate-100 dark:border-slate-800">
|
||||||
|
<div class="col-span-5 sm:col-span-4">Categorienaam</div>
|
||||||
|
<div class="col-span-5 sm:col-span-6">Omschrijving / Toelichting</div>
|
||||||
|
<div class="col-span-1 text-center">Actief</div>
|
||||||
|
<div class="col-span-1 text-right">Verwijder</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php foreach ($brainstormCategories as $bIdx => $bCat): ?>
|
||||||
|
<div class="grid grid-cols-12 gap-3 items-center p-3 rounded-2xl bg-slate-50/70 dark:bg-[#0c0f17] border border-slate-200/70 dark:border-slate-800/70 hover:border-slate-300 dark:hover:border-slate-700 transition">
|
||||||
|
<div class="col-span-5 sm:col-span-4">
|
||||||
|
<input type="text" name="categories[<?= $bIdx ?>][name]" value="<?= htmlspecialchars($bCat['name']) ?>" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-5 sm:col-span-6">
|
||||||
|
<input type="text" name="categories[<?= $bIdx ?>][description]" value="<?= htmlspecialchars($bCat['description'] ?? '') ?>" placeholder="Optionele beschrijving..." class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1 text-center">
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" name="categories[<?= $bIdx ?>][is_active]" value="1" <?= !empty($bCat['is_active']) ? 'checked' : '' ?> class="w-4 h-4 text-indigo-600 rounded border-slate-300 dark:border-slate-700 focus:ring-indigo-500">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1 text-right">
|
||||||
|
<label class="cursor-pointer text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 transition" title="Vink aan om deze categorie te verwijderen">
|
||||||
|
<input type="checkbox" name="categories[<?= $bIdx ?>][is_delete]" value="1" class="w-4 h-4 text-rose-600 rounded border-slate-300 dark:border-slate-700 focus:ring-rose-500">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nieuwe categorie toevoegen regel -->
|
||||||
|
<div class="p-4 rounded-2xl bg-indigo-50/40 dark:bg-indigo-950/20 border border-dashed border-indigo-300/80 dark:border-indigo-800/60 space-y-3">
|
||||||
|
<div class="flex items-center space-x-2 text-xs font-bold text-indigo-900 dark:text-indigo-200">
|
||||||
|
<i data-lucide="plus-circle" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
|
||||||
|
<span>Nieuwe Categorie Toevoegen</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-center">
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Categorienaam</label>
|
||||||
|
<input type="text" name="new_category_name" placeholder="Bijv. Duurzaamheid & MVO" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-7">
|
||||||
|
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Omschrijving</label>
|
||||||
|
<input type="text" name="new_category_desc" placeholder="Doel of richting van deze categorie..." class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 flex items-center space-x-1.5">
|
||||||
|
<i data-lucide="check" class="w-4 h-4"></i>
|
||||||
|
<span>Brainstorm Categorieën Opslaan</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- MODAL: Wachtrijfilter Toevoegen / Bewerken -->
|
<!-- MODAL: Wachtrijfilter Toevoegen / Bewerken -->
|
||||||
<div id="jiraFilterModal" onclick="if(event.target === this) closeJiraFilterModal()" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
<div id="jiraFilterModal" onclick="if(event.target === this) closeJiraFilterModal()" 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="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">
|
||||||
@@ -1497,7 +1579,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
|
|
||||||
<form method="POST" action="/admin.php" class="mt-4 space-y-4">
|
<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" name="action" value="save_jira_queue_filter">
|
||||||
<input type="hidden" name="active_tab" value="jira">
|
<input type="hidden" name="active_tab" value="projects">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||||
<input type="hidden" id="filterModal_id" name="filter_id" value="0">
|
<input type="hidden" id="filterModal_id" name="filter_id" value="0">
|
||||||
|
|
||||||
@@ -1558,161 +1640,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 5: AI Co-Pilot -->
|
<!-- Tab 5: Beveiliging & Audit -->
|
||||||
<div id="tab-pane-ai" class="admin-tab-pane space-y-8 hidden">
|
|
||||||
<!-- 4. AI & Brainstorm Co-Pilot (Release v1.5) -->
|
|
||||||
<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">
|
|
||||||
<div class="flex items-center space-x-3.5">
|
|
||||||
<div class="w-12 h-12 rounded-xl bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800/60 flex items-center justify-center text-amber-600 dark:text-amber-400 font-bold shadow-xs">
|
|
||||||
<i data-lucide="sparkles" class="w-6 h-6"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">AI-Co-pilot</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<span class="px-2.5 py-1 rounded-full bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800/60 text-xs font-bold flex items-center space-x-1">
|
|
||||||
<i data-lucide="shield-check" class="w-3.5 h-3.5"></i>
|
|
||||||
<span>Zero-Data Leakage</span>
|
|
||||||
</span>
|
|
||||||
<span class="px-2.5 py-1 rounded-full <?= $aiSettings['has_key'] ? 'bg-teal-50 dark:bg-teal-950/40 text-teal-700 dark:text-teal-400 border border-teal-200 dark:border-teal-800/60' : 'bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-800/60' ?> text-xs font-bold inline-flex items-center space-x-1.5">
|
|
||||||
<i data-lucide="<?= $aiSettings['has_key'] ? 'check-circle-2' : 'key' ?>" class="w-3.5 h-3.5 flex-shrink-0"></i>
|
|
||||||
<span><?= $aiSettings['has_key'] ? 'AI Actief (' . htmlspecialchars((string)($aiSettings['ai_model'] ?? 'Gemini Pro')) . ')' : 'Sleutel Vereist' ?></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- AI Verbruik & Statistieken Dashboard -->
|
|
||||||
<div class="p-4 rounded-2xl bg-slate-50/80 dark:bg-[#0c0f17]/90 border border-slate-200/80 dark:border-slate-800/80 space-y-3">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<i data-lucide="bar-chart-2" class="w-4 h-4 text-indigo-500"></i>
|
|
||||||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">AI Verbruik</span>
|
|
||||||
</div>
|
|
||||||
<?php if (!empty($aiUsageStats['last_used_at'])): ?>
|
|
||||||
<span class="text-[11px] text-slate-400">Laatst gebruikt: <?= htmlspecialchars(date('d-m-Y H:i', strtotime($aiUsageStats['last_used_at']))) ?></span>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="text-[11px] text-slate-400 italic">Nog geen verbruik geregistreerd</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
||||||
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
|
||||||
<span class="block text-[10px] uppercase font-bold text-slate-400">Totaal Aanroepen</span>
|
|
||||||
<span class="text-lg font-black text-slate-900 dark:text-white"><?= number_format((int)($aiUsageStats['total_requests'] ?? 0), 0, ',', '.') ?></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
|
||||||
<span class="block text-[10px] uppercase font-bold text-slate-400">Totaal Tokens</span>
|
|
||||||
<span class="text-lg font-black text-indigo-600 dark:text-indigo-400"><?= number_format((int)($aiUsageStats['total_tokens'] ?? 0), 0, ',', '.') ?></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
|
||||||
<span class="block text-[10px] uppercase font-bold text-slate-400">Prompt / Invoer</span>
|
|
||||||
<span class="text-lg font-black text-slate-700 dark:text-slate-300"><?= number_format((int)($aiUsageStats['total_prompt_tokens'] ?? 0), 0, ',', '.') ?></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
|
||||||
<span class="block text-[10px] uppercase font-bold text-slate-400">Output / Gegenereerd</span>
|
|
||||||
<span class="text-lg font-black text-teal-600 dark:text-teal-400"><?= number_format((int)($aiUsageStats['total_completion_tokens'] ?? 0), 0, ',', '.') ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (!empty($aiUsageStats['by_model'])): ?>
|
|
||||||
<div class="pt-2 flex flex-wrap items-center gap-2 text-[11px] border-t border-slate-200/40 dark:border-slate-800/40">
|
|
||||||
<span class="font-semibold text-slate-500 dark:text-slate-400">Per model:</span>
|
|
||||||
<?php foreach ($aiUsageStats['by_model'] as $mName => $mCount): ?>
|
|
||||||
<span class="px-2 py-0.5 rounded-md bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700">
|
|
||||||
<strong><?= htmlspecialchars((string)$mName) ?></strong>: <?= (int)$mCount ?>x
|
|
||||||
</span>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ZERO-DATA LEAKAGE PRIVACY FIREWALL BANNER -->
|
|
||||||
<div class="p-3.5 rounded-2xl bg-emerald-50/80 dark:bg-emerald-950/20 border border-emerald-200/80 dark:border-emerald-800/40 flex items-start space-x-3">
|
|
||||||
<div class="w-8 h-8 rounded-xl bg-emerald-100 dark:bg-emerald-900/40 text-emerald-700 dark:text-emerald-400 flex items-center justify-center flex-shrink-0 mt-0.5">
|
|
||||||
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs space-y-1">
|
|
||||||
<strong class="text-emerald-950 dark:text-emerald-200 font-bold block text-xs">Strikte Zero-Data Leakage Privacy Garantie</strong>
|
|
||||||
<p class="text-emerald-800/90 dark:text-emerald-300/90 text-[11px] leading-relaxed">
|
|
||||||
Er wordt <strong>onder géén enkele voorwaarde data uit Jira tickets, Outlook e-mails of Microsoft Teams</strong> naar de AI verzonden. De ingebouwde privacy firewall onderschept en filtert automatisch ticketnummers, Atlassian-links, mailheaders en communicatietraces vóór verzending.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="POST" action="/admin.php" id="adminAiForm" class="space-y-4">
|
|
||||||
<input type="hidden" name="action" value="save_ai_settings">
|
|
||||||
<input type="hidden" name="active_tab" value="ai">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
||||||
|
|
||||||
<div class="p-3.5 rounded-2xl bg-indigo-50/50 dark:bg-indigo-950/20 border border-indigo-200/60 dark:border-indigo-800/40">
|
|
||||||
<label class="flex items-center space-x-2.5 text-xs text-indigo-950 dark:text-indigo-200 cursor-pointer select-none">
|
|
||||||
<input type="checkbox" name="ai_enabled" value="1" <?= $aiSettings['ai_enabled'] ? 'checked' : '' ?> class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
|
|
||||||
<span class="font-bold">AI Co-Pilot inschakelen voor alle gebruikers in de organisatie</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">AI Provider & Model</label>
|
|
||||||
<select name="ai_model" id="admin_ai_model" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
||||||
<optgroup label="Google Gemini (Aanbevolen)">
|
|
||||||
<option value="gemini-3.6-flash" <?= ($aiSettings['ai_model'] === 'gemini-3.6-flash' || empty($aiSettings['ai_model']) || $aiSettings['ai_model'] === 'gemini') ? 'selected' : '' ?>>Google Gemini 3.6 Flash (Aanbevolen, Snel & Nieuwste)</option>
|
|
||||||
<option value="gemini-1.5-pro" <?= $aiSettings['ai_model'] === 'gemini-1.5-pro' ? 'selected' : '' ?>>Google Gemini 1.5 Pro (Slim & Uitgebreid)</option>
|
|
||||||
<option value="gemini-2.0-flash" <?= $aiSettings['ai_model'] === 'gemini-2.0-flash' ? 'selected' : '' ?>>Google Gemini 2.0 Flash</option>
|
|
||||||
<option value="gemini-1.5-flash" <?= $aiSettings['ai_model'] === 'gemini-1.5-flash' ? 'selected' : '' ?>>Google Gemini 1.5 Flash</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="OpenAI">
|
|
||||||
<option value="gpt-4o-mini" <?= $aiSettings['ai_model'] === 'gpt-4o-mini' ? 'selected' : '' ?>>OpenAI GPT-4o-mini</option>
|
|
||||||
<option value="gpt-4o" <?= $aiSettings['ai_model'] === 'gpt-4o' ? 'selected' : '' ?>>OpenAI GPT-4o</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
<input type="hidden" name="ai_provider" id="admin_ai_provider" value="<?= str_contains((string)($aiSettings['ai_model'] ?? ''), 'gpt') ? 'openai' : 'gemini' ?>">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Google Gemini / AI API Sleutel</label>
|
|
||||||
<div class="relative">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="ai_api_key"
|
|
||||||
id="admin_ai_api_key"
|
|
||||||
value="<?= $aiSettings['has_key'] ? '••••••••••••••••' : '' ?>"
|
|
||||||
placeholder="AIzaSy... (Google) of sk-... (OpenAI)"
|
|
||||||
class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-9 py-2 text-slate-900 dark:text-white text-xs font-mono placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
||||||
>
|
|
||||||
<button type="button" onclick="togglePasswordVisibility('admin_ai_api_key', this)" class="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
|
|
||||||
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span class="block text-[11px] text-slate-500 dark:text-slate-400 mt-1">Haal een gratis of betaalde Gemini API sleutel via <a href="https://aistudio.google.com/app/apikey" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:underline">Google AI Studio →</a></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Live Test Resultaat Alert -->
|
|
||||||
<div id="adminAiTestResult" class="hidden p-3 rounded-xl text-xs"></div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
|
||||||
<button type="button" onclick="testAdminAiConnection()" id="adminAiTestBtn" class="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 text-xs font-semibold transition cursor-pointer flex items-center space-x-2">
|
|
||||||
<i data-lucide="activity" class="w-3.5 h-3.5 text-indigo-500"></i>
|
|
||||||
<span id="adminAiTestBtnText">Test Verbinding</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<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 flex items-center space-x-2">
|
|
||||||
<i data-lucide="save" class="w-3.5 h-3.5"></i>
|
|
||||||
<span>AI Instellingen Opslaan</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab 6: Beveiliging & Audit -->
|
|
||||||
<div id="tab-pane-security" class="admin-tab-pane space-y-8 hidden">
|
<div id="tab-pane-security" class="admin-tab-pane space-y-8 hidden">
|
||||||
<!-- 6. Sessie Inactiviteit & Beveiligingsbeleid (Release v1.6) -->
|
<!-- 6. Sessie Inactiviteit & Beveiligingsbeleid (Release v1.6) -->
|
||||||
<div class="fociz-card p-6 sm:p-7 space-y-6">
|
<div class="fociz-card p-6 sm:p-7 space-y-6">
|
||||||
@@ -1917,7 +1845,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 7: Systeem & Database (inclusief SMTP Mail Server) -->
|
<!-- Tab 6: Systeem & Database (inclusief SMTP Mail Server & AI Co-Pilot) -->
|
||||||
<div id="tab-pane-system" class="admin-tab-pane space-y-8 hidden">
|
<div id="tab-pane-system" class="admin-tab-pane space-y-8 hidden">
|
||||||
<!-- 5. SMTP Mail Server & Notificaties (Release v1.6) -->
|
<!-- 5. SMTP Mail Server & Notificaties (Release v1.6) -->
|
||||||
<div class="fociz-card p-6 sm:p-7 space-y-6">
|
<div class="fociz-card p-6 sm:p-7 space-y-6">
|
||||||
@@ -1988,6 +1916,139 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- AI-Co-pilot Card -->
|
||||||
|
<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">
|
||||||
|
<div class="flex items-center space-x-3.5">
|
||||||
|
<div class="w-12 h-12 rounded-xl bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800/60 flex items-center justify-center text-amber-600 dark:text-amber-400 font-bold shadow-xs">
|
||||||
|
<i data-lucide="sparkles" class="w-6 h-6"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">AI-Co-pilot</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="px-2.5 py-1 rounded-full <?= $aiSettings['has_key'] ? 'bg-teal-50 dark:bg-teal-950/40 text-teal-700 dark:text-teal-400 border border-teal-200 dark:border-teal-800/60' : 'bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-800/60' ?> text-xs font-bold inline-flex items-center space-x-1.5">
|
||||||
|
<i data-lucide="<?= $aiSettings['has_key'] ? 'check-circle-2' : 'key' ?>" class="w-3.5 h-3.5 flex-shrink-0"></i>
|
||||||
|
<span><?= $aiSettings['has_key'] ? 'AI Actief (' . htmlspecialchars((string)($aiSettings['ai_model'] ?? 'Gemini Pro')) . ')' : 'Sleutel Vereist' ?></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI Verbruik & Statistieken Dashboard -->
|
||||||
|
<div class="p-4 rounded-2xl bg-slate-50/80 dark:bg-[#0c0f17]/90 border border-slate-200/80 dark:border-slate-800/80 space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i data-lucide="bar-chart-2" class="w-4 h-4 text-indigo-500"></i>
|
||||||
|
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">AI Verbruik</span>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($aiUsageStats['last_used_at'])): ?>
|
||||||
|
<span class="text-[11px] text-slate-400">Laatst gebruikt: <?= htmlspecialchars(date('d-m-Y H:i', strtotime($aiUsageStats['last_used_at']))) ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-[11px] text-slate-400 italic">Nog geen verbruik geregistreerd</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
|
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
||||||
|
<span class="block text-[10px] uppercase font-bold text-slate-400">Totaal Aanroepen</span>
|
||||||
|
<span class="text-lg font-black text-slate-900 dark:text-white"><?= number_format((int)($aiUsageStats['total_requests'] ?? 0), 0, ',', '.') ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
||||||
|
<span class="block text-[10px] uppercase font-bold text-slate-400">Totaal Tokens</span>
|
||||||
|
<span class="text-lg font-black text-indigo-600 dark:text-indigo-400"><?= number_format((int)($aiUsageStats['total_tokens'] ?? 0), 0, ',', '.') ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
||||||
|
<span class="block text-[10px] uppercase font-bold text-slate-400">Prompt / Invoer</span>
|
||||||
|
<span class="text-lg font-black text-slate-700 dark:text-slate-300"><?= number_format((int)($aiUsageStats['total_prompt_tokens'] ?? 0), 0, ',', '.') ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
||||||
|
<span class="block text-[10px] uppercase font-bold text-slate-400">Output / Gegenereerd</span>
|
||||||
|
<span class="text-lg font-black text-teal-600 dark:text-teal-400"><?= number_format((int)($aiUsageStats['total_completion_tokens'] ?? 0), 0, ',', '.') ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($aiUsageStats['by_model'])): ?>
|
||||||
|
<div class="pt-2 flex flex-wrap items-center gap-2 text-[11px] border-t border-slate-200/40 dark:border-slate-800/40">
|
||||||
|
<span class="font-semibold text-slate-500 dark:text-slate-400">Per model:</span>
|
||||||
|
<?php foreach ($aiUsageStats['by_model'] as $mName => $mCount): ?>
|
||||||
|
<span class="px-2 py-0.5 rounded-md bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700">
|
||||||
|
<strong><?= htmlspecialchars((string)$mName) ?></strong>: <?= (int)$mCount ?>x
|
||||||
|
</span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="/admin.php" id="adminAiForm" class="space-y-4">
|
||||||
|
<input type="hidden" name="action" value="save_ai_settings">
|
||||||
|
<input type="hidden" name="active_tab" value="system">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||||
|
|
||||||
|
<div class="p-3.5 rounded-2xl bg-indigo-50/50 dark:bg-indigo-950/20 border border-indigo-200/60 dark:border-indigo-800/40">
|
||||||
|
<label class="flex items-center space-x-2.5 text-xs text-indigo-950 dark:text-indigo-200 cursor-pointer select-none">
|
||||||
|
<input type="checkbox" name="ai_enabled" value="1" <?= $aiSettings['ai_enabled'] ? 'checked' : '' ?> class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
|
||||||
|
<span class="font-bold">AI Co-Pilot inschakelen voor alle gebruikers in de organisatie</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">AI Provider & Model</label>
|
||||||
|
<select name="ai_model" id="admin_ai_model" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||||
|
<optgroup label="Google Gemini (Aanbevolen)">
|
||||||
|
<option value="gemini-3.6-flash" <?= ($aiSettings['ai_model'] === 'gemini-3.6-flash' || empty($aiSettings['ai_model']) || $aiSettings['ai_model'] === 'gemini') ? 'selected' : '' ?>>Google Gemini 3.6 Flash (Aanbevolen, Snel & Nieuwste)</option>
|
||||||
|
<option value="gemini-1.5-pro" <?= $aiSettings['ai_model'] === 'gemini-1.5-pro' ? 'selected' : '' ?>>Google Gemini 1.5 Pro (Slim & Uitgebreid)</option>
|
||||||
|
<option value="gemini-2.0-flash" <?= $aiSettings['ai_model'] === 'gemini-2.0-flash' ? 'selected' : '' ?>>Google Gemini 2.0 Flash</option>
|
||||||
|
<option value="gemini-1.5-flash" <?= $aiSettings['ai_model'] === 'gemini-1.5-flash' ? 'selected' : '' ?>>Google Gemini 1.5 Flash</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="OpenAI">
|
||||||
|
<option value="gpt-4o-mini" <?= $aiSettings['ai_model'] === 'gpt-4o-mini' ? 'selected' : '' ?>>OpenAI GPT-4o-mini</option>
|
||||||
|
<option value="gpt-4o" <?= $aiSettings['ai_model'] === 'gpt-4o' ? 'selected' : '' ?>>OpenAI GPT-4o</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
<input type="hidden" name="ai_provider" id="admin_ai_provider" value="<?= str_contains((string)($aiSettings['ai_model'] ?? ''), 'gpt') ? 'openai' : 'gemini' ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Google Gemini / AI API Sleutel</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="ai_api_key"
|
||||||
|
id="admin_ai_api_key"
|
||||||
|
value="<?= $aiSettings['has_key'] ? '••••••••••••••••' : '' ?>"
|
||||||
|
placeholder="AIzaSy... (Google) of sk-... (OpenAI)"
|
||||||
|
class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-9 py-2 text-slate-900 dark:text-white text-xs font-mono placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<button type="button" onclick="togglePasswordVisibility('admin_ai_api_key', this)" class="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
|
||||||
|
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span class="block text-[11px] text-slate-500 dark:text-slate-400 mt-1">Haal een gratis of betaalde Gemini API sleutel via <a href="https://aistudio.google.com/app/apikey" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:underline">Google AI Studio →</a></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Live Test Resultaat Alert -->
|
||||||
|
<div id="adminAiTestResult" class="hidden p-3 rounded-xl text-xs"></div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
||||||
|
<button type="button" onclick="testAdminAiConnection()" id="adminAiTestBtn" class="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 text-xs font-semibold transition cursor-pointer flex items-center space-x-2">
|
||||||
|
<i data-lucide="activity" class="w-3.5 h-3.5 text-indigo-500"></i>
|
||||||
|
<span id="adminAiTestBtnText">Test Verbinding</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<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 flex items-center space-x-2">
|
||||||
|
<i data-lucide="save" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>AI Instellingen Opslaan</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Systeem Gezondheid & Beveiliging -->
|
<!-- Systeem Gezondheid & Beveiliging -->
|
||||||
<div class="fociz-card p-6 space-y-4">
|
<div class="fociz-card p-6 space-y-4">
|
||||||
@@ -2068,86 +2129,6 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Brainstorm Categorieën Beheer Card -->
|
|
||||||
<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-5 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-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
|
|
||||||
<i data-lucide="lightbulb" 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">Brainstorm Categorieën</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-300 text-[10px] font-bold">
|
|
||||||
Beheerbaar
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400">Beheer de beschikbare categorieën voor nieuwe brainstormsessies</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="POST" class="space-y-5">
|
|
||||||
<input type="hidden" name="action" value="save_brainstorm_categories">
|
|
||||||
<input type="hidden" name="active_tab" value="system">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="grid grid-cols-12 gap-3 px-3 py-1.5 text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider border-b border-slate-100 dark:border-slate-800">
|
|
||||||
<div class="col-span-5 sm:col-span-4">Categorienaam</div>
|
|
||||||
<div class="col-span-5 sm:col-span-6">Omschrijving / Toelichting</div>
|
|
||||||
<div class="col-span-1 text-center">Actief</div>
|
|
||||||
<div class="col-span-1 text-right">Verwijder</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php foreach ($brainstormCategories as $bIdx => $bCat): ?>
|
|
||||||
<div class="grid grid-cols-12 gap-3 items-center p-3 rounded-2xl bg-slate-50/70 dark:bg-[#0c0f17] border border-slate-200/70 dark:border-slate-800/70 hover:border-slate-300 dark:hover:border-slate-700 transition">
|
|
||||||
<div class="col-span-5 sm:col-span-4">
|
|
||||||
<input type="text" name="categories[<?= $bIdx ?>][name]" value="<?= htmlspecialchars($bCat['name']) ?>" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-5 sm:col-span-6">
|
|
||||||
<input type="text" name="categories[<?= $bIdx ?>][description]" value="<?= htmlspecialchars($bCat['description'] ?? '') ?>" placeholder="Optionele beschrijving..." class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
||||||
</div>
|
|
||||||
<div class="col-span-1 text-center">
|
|
||||||
<label class="relative inline-flex items-center cursor-pointer">
|
|
||||||
<input type="checkbox" name="categories[<?= $bIdx ?>][is_active]" value="1" <?= !empty($bCat['is_active']) ? 'checked' : '' ?> class="w-4 h-4 text-indigo-600 rounded border-slate-300 dark:border-slate-700 focus:ring-indigo-500">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-1 text-right">
|
|
||||||
<label class="cursor-pointer text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 transition" title="Vink aan om deze categorie te verwijderen">
|
|
||||||
<input type="checkbox" name="categories[<?= $bIdx ?>][is_delete]" value="1" class="w-4 h-4 text-rose-600 rounded border-slate-300 dark:border-slate-700 focus:ring-rose-500">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nieuwe categorie toevoegen regel -->
|
|
||||||
<div class="p-4 rounded-2xl bg-indigo-50/40 dark:bg-indigo-950/20 border border-dashed border-indigo-300/80 dark:border-indigo-800/60 space-y-3">
|
|
||||||
<div class="flex items-center space-x-2 text-xs font-bold text-indigo-900 dark:text-indigo-200">
|
|
||||||
<i data-lucide="plus-circle" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
|
|
||||||
<span>Nieuwe Categorie Toevoegen</span>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-center">
|
|
||||||
<div class="sm:col-span-5">
|
|
||||||
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Categorienaam</label>
|
|
||||||
<input type="text" name="new_category_name" placeholder="Bijv. Duurzaamheid & MVO" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-7">
|
|
||||||
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Omschrijving</label>
|
|
||||||
<input type="text" name="new_category_desc" placeholder="Doel of richting van deze categorie..." class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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 flex items-center space-x-1.5">
|
|
||||||
<i data-lucide="check" class="w-4 h-4"></i>
|
|
||||||
<span>Brainstorm Categorieën Opslaan</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -2201,9 +2182,11 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ADMIN_TABS = ['users', 'calendar', 'microsoft', 'jira', 'ai', 'security', 'system'];
|
const ADMIN_TABS = ['users', 'calendar', 'microsoft', 'projects', 'security', 'system'];
|
||||||
|
|
||||||
function switchAdminTab(tabName, updateUrl = true) {
|
function switchAdminTab(tabName, updateUrl = true) {
|
||||||
|
if (tabName === 'jira') tabName = 'projects';
|
||||||
|
if (tabName === 'ai') tabName = 'system';
|
||||||
if (!ADMIN_TABS.includes(tabName)) {
|
if (!ADMIN_TABS.includes(tabName)) {
|
||||||
tabName = 'users';
|
tabName = 'users';
|
||||||
}
|
}
|
||||||
@@ -2247,12 +2230,23 @@ function switchAdminTab(tabName, updateUrl = true) {
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
let initialTab = urlParams.get('tab') || window.location.hash.replace('#', '') || '<?= htmlspecialchars($activeTab) ?>';
|
let initialTab = urlParams.get('tab') || window.location.hash.replace('#', '') || '<?= htmlspecialchars($activeTab) ?>';
|
||||||
|
if (initialTab === 'jira') initialTab = 'projects';
|
||||||
|
if (initialTab === 'ai') initialTab = 'system';
|
||||||
if (!ADMIN_TABS.includes(initialTab)) {
|
if (!ADMIN_TABS.includes(initialTab)) {
|
||||||
initialTab = 'users';
|
initialTab = 'users';
|
||||||
}
|
}
|
||||||
switchAdminTab(initialTab, false);
|
switchAdminTab(initialTab, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', function() {
|
||||||
|
let hashTab = window.location.hash.replace('#', '');
|
||||||
|
if (hashTab === 'jira') hashTab = 'projects';
|
||||||
|
if (hashTab === 'ai') hashTab = 'system';
|
||||||
|
if (ADMIN_TABS.includes(hashTab)) {
|
||||||
|
switchAdminTab(hashTab, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function copyAdminRedirectUri() {
|
function copyAdminRedirectUri() {
|
||||||
const input = document.getElementById('adminRedirectUri');
|
const input = document.getElementById('adminRedirectUri');
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|||||||
@@ -77,23 +77,62 @@ foreach ($allIssues as $iss) {
|
|||||||
$allTypes[$iss['issue_type']] = $iss['issue_type'];
|
$allTypes[$iss['issue_type']] = $iss['issue_type'];
|
||||||
}
|
}
|
||||||
if (!empty($iss['status'])) {
|
if (!empty($iss['status'])) {
|
||||||
$allStatuses[$iss['status']] = $iss['status'];
|
$sName = trim($iss['status']);
|
||||||
|
$sKey = strtolower($sName);
|
||||||
|
$allStatuses[$sKey] = [
|
||||||
|
'name' => $sName,
|
||||||
|
'status_category' => $iss['status_category'] ?? 'indeterminate',
|
||||||
|
'category_name' => '',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statussen uitbreiden met officiële Jira instantie statussen
|
// Statussen uitbreiden met ALLE officiële Jira Cloud instantie statussen
|
||||||
$availableStatuses = $jiraData['available_statuses'] ?? [];
|
$availableStatuses = $jiraData['available_statuses'] ?? [];
|
||||||
if (!empty($availableStatuses)) {
|
if (!empty($availableStatuses)) {
|
||||||
foreach ($availableStatuses as $st) {
|
foreach ($availableStatuses as $st) {
|
||||||
if (!empty($st['name'])) {
|
if (!empty($st['name'])) {
|
||||||
$allStatuses[$st['name']] = $st['name'];
|
$sName = trim($st['name']);
|
||||||
|
$sKey = strtolower($sName);
|
||||||
|
$cat = $st['status_category'] ?? ($allStatuses[$sKey]['status_category'] ?? 'indeterminate');
|
||||||
|
$allStatuses[$sKey] = [
|
||||||
|
'name' => $sName,
|
||||||
|
'status_category' => $cat,
|
||||||
|
'category_name' => $st['category_name'] ?? '',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback statussen als Jira nog geen verbinding heeft
|
||||||
|
if (empty($allStatuses)) {
|
||||||
|
$fallbackList = [
|
||||||
|
['name' => 'To Do', 'status_category' => 'new'],
|
||||||
|
['name' => 'In behandeling', 'status_category' => 'indeterminate'],
|
||||||
|
['name' => 'In Progress', 'status_category' => 'indeterminate'],
|
||||||
|
['name' => 'Wachten op klant', 'status_category' => 'indeterminate'],
|
||||||
|
['name' => 'In Review', 'status_category' => 'indeterminate'],
|
||||||
|
['name' => 'Gesloten', 'status_category' => 'done'],
|
||||||
|
['name' => 'Done', 'status_category' => 'done'],
|
||||||
|
];
|
||||||
|
foreach ($fallbackList as $fb) {
|
||||||
|
$allStatuses[strtolower($fb['name'])] = $fb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorteren: To Do / Nieuw (1), In Uitvoering / Behandeling (2), Gesloten / Gereed (3), daarbinnen alfabetisch
|
||||||
|
uasort($allStatuses, function($a, $b) {
|
||||||
|
$order = ['new' => 1, 'indeterminate' => 2, 'done' => 3];
|
||||||
|
$oa = $order[$a['status_category'] ?? 'indeterminate'] ?? 2;
|
||||||
|
$ob = $order[$b['status_category'] ?? 'indeterminate'] ?? 2;
|
||||||
|
if ($oa !== $ob) return $oa - $ob;
|
||||||
|
return strcasecmp($a['name'], $b['name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$allStatusesList = array_values($allStatuses);
|
||||||
|
|
||||||
ksort($allProjects);
|
ksort($allProjects);
|
||||||
ksort($allTypes);
|
ksort($allTypes);
|
||||||
ksort($allStatuses);
|
|
||||||
|
|
||||||
$queueFilters = $jiraData['queue_filters'] ?? jira_get_queue_filters(true);
|
$queueFilters = $jiraData['queue_filters'] ?? jira_get_queue_filters(true);
|
||||||
$defaultQueueFilter = $jiraData['default_queue_filter'] ?? jira_get_user_queue_preference($userId);
|
$defaultQueueFilter = $jiraData['default_queue_filter'] ?? jira_get_user_queue_preference($userId);
|
||||||
@@ -101,6 +140,7 @@ $userQueueFilters = $jiraData['user_queue_filters'] ?? jira_get_user_queue_prefe
|
|||||||
|
|
||||||
$jiraDataJson = json_encode($jiraData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
|
$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);
|
$boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
|
||||||
|
$statusesJson = json_encode($allStatusesList, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="space-y-6 w-full">
|
<div class="space-y-6 w-full">
|
||||||
@@ -211,7 +251,7 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
|||||||
|
|
||||||
<!-- 1. 4 KPI Statistieken Kaarten -->
|
<!-- 1. 4 KPI Statistieken Kaarten -->
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
<div onclick="switchTab('assigned')" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
|
<div onclick="filterByMyTickets()" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Aan mij toegewezen</span>
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Aan mij toegewezen</span>
|
||||||
<div class="w-7 h-7 rounded-lg bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 flex items-center justify-center transition group-hover:scale-110">
|
<div class="w-7 h-7 rounded-lg bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 flex items-center justify-center transition group-hover:scale-110">
|
||||||
@@ -224,7 +264,7 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onclick="switchTab('team')" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
|
<div onclick="filterByAllTickets()" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Team & Projecten</span>
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">Team & Projecten</span>
|
||||||
<div class="w-7 h-7 rounded-lg bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 flex items-center justify-center transition group-hover:scale-110">
|
<div class="w-7 h-7 rounded-lg bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 flex items-center justify-center transition group-hover:scale-110">
|
||||||
@@ -237,7 +277,7 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onclick="switchTab('in_progress')" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
|
<div onclick="selectActiveStatusesOnly()" class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/70 dark:border-slate-700/60 cursor-pointer transition group">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">In Uitvoering</span>
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400">In Uitvoering</span>
|
||||||
<div class="w-7 h-7 rounded-lg bg-amber-50 dark:bg-amber-950/40 text-amber-600 dark:text-amber-400 flex items-center justify-center transition group-hover:scale-110">
|
<div class="w-7 h-7 rounded-lg bg-amber-50 dark:bg-amber-950/40 text-amber-600 dark:text-amber-400 flex items-center justify-center transition group-hover:scale-110">
|
||||||
@@ -423,101 +463,83 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<?php if ($isAdmin): ?>
|
<?php if ($isAdmin): ?>
|
||||||
<a href="/admin.php#jira" title="Wachtrijfilters beheren in Beheer" class="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0">
|
<a href="/admin.php#projects" title="Wachtrijfilters beheren in Beheer" class="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0">
|
||||||
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
|
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rij 3: Tabs, Verberg Gesloten, In behandeling & Zoekveld -->
|
<!-- Rij 3: Status Multi-select Dropdown & Zoekveld -->
|
||||||
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-3">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
<!-- Tabs Navigatie -->
|
|
||||||
<div class="flex items-center space-x-1 sm:space-x-1.5 overflow-x-auto pb-1 lg:pb-0">
|
<!-- Linkerkant: Statussen multi-select dropdown -->
|
||||||
<button onclick="switchTab('assigned')" id="tabBtn_assigned" class="flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800/60 flex-shrink-0">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<i data-lucide="user-check" class="w-3.5 h-3.5"></i>
|
<div class="relative" id="statusDropdownWrapper">
|
||||||
<span>Aan mij</span>
|
<button
|
||||||
<span id="tabCount_assigned" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-200/70 dark:bg-indigo-800/60 text-indigo-800 dark:text-indigo-200">
|
type="button"
|
||||||
<?= count($assignedIssues) ?>
|
onclick="toggleStatusDropdown()"
|
||||||
</span>
|
id="statusDropdownBtn"
|
||||||
</button>
|
class="flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-semibold bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 text-slate-800 dark:text-slate-200 transition cursor-pointer shadow-2xs"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<i data-lucide="check-square" class="w-3.5 h-3.5 text-indigo-500"></i>
|
||||||
|
<span>Status:</span>
|
||||||
|
<span id="statusDropdownLabel" class="font-bold text-indigo-600 dark:text-indigo-400">Alle statussen</span>
|
||||||
|
<span id="statusDropdownBadge" class="hidden px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300"></span>
|
||||||
|
<i data-lucide="chevron-down" id="statusDropdownChevron" class="w-3.5 h-3.5 text-slate-400 transition-transform duration-200"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button onclick="switchTab('team')" id="tabBtn_team" class="flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
|
<!-- Dropdown Menu met Checkboxes -->
|
||||||
<i data-lucide="users" class="w-3.5 h-3.5"></i>
|
<div
|
||||||
<span>Team & Projecten</span>
|
id="statusDropdownMenu"
|
||||||
<span id="tabCount_team" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
|
class="hidden absolute left-0 top-full mt-2 w-72 max-w-[90vw] bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 py-3 px-3.5 z-50 text-xs space-y-2.5"
|
||||||
<?= count($teamIssues) ?>
|
>
|
||||||
</span>
|
<!-- Quick actions header -->
|
||||||
</button>
|
<div class="flex items-center justify-between pb-2 border-b border-slate-100 dark:border-slate-800">
|
||||||
|
<span class="font-bold text-slate-900 dark:text-white text-[11px] uppercase tracking-wider">Jira Statussen</span>
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<button type="button" onclick="selectAllStatuses(true)" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950/60 transition cursor-pointer">
|
||||||
|
Alles
|
||||||
|
</button>
|
||||||
|
<span class="text-slate-300 dark:text-slate-700">•</span>
|
||||||
|
<button type="button" onclick="selectActiveStatusesOnly()" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-950/60 transition cursor-pointer" title="Alleen actieve taken (geen gesloten)">
|
||||||
|
Actief
|
||||||
|
</button>
|
||||||
|
<span class="text-slate-300 dark:text-slate-700">•</span>
|
||||||
|
<button type="button" onclick="selectAllStatuses(false)" class="px-2 py-0.5 rounded-md text-[10px] font-bold text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||||
|
Geen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button onclick="switchTab('in_progress')" id="tabBtn_in_progress" class="flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
|
<!-- Status Checkboxes List (scrollable) -->
|
||||||
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
|
<div class="max-h-64 overflow-y-auto space-y-1 pr-1 custom-scrollbar" id="statusCheckboxList">
|
||||||
<span>In Uitvoering</span>
|
<!-- Dynamisch gevuld via renderStatusCheckboxes() -->
|
||||||
<span id="tabCount_in_progress" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
|
</div>
|
||||||
0
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="switchTab('done')" id="tabBtn_done" class="flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0">
|
<!-- Actieve filters reset knop als er iets gefilterd is -->
|
||||||
<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
|
<button type="button" onclick="resetAllFilters()" id="btnResetFiltersInline" class="hidden flex items-center space-x-1 px-2.5 py-1.5 rounded-xl text-xs font-semibold text-slate-500 hover:text-slate-800 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||||
<span>Afgerond</span>
|
<i data-lucide="rotate-ccw" class="w-3 h-3"></i>
|
||||||
<span id="tabCount_done" class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
|
<span>Filters wissen</span>
|
||||||
0
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Acties: Verberg Gesloten + Dropdown + Zoeken -->
|
<!-- Rechterkant: Zoekveld -->
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="relative w-full sm:w-64">
|
||||||
<!-- Toggle: Verberg Gesloten (Standaard Actief!) -->
|
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2 pointer-events-none"></i>
|
||||||
<button type="button" onclick="toggleHideClosed()" id="btnHideClosed" class="flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80 shadow-2xs" title="Verberg afgeronde en gesloten tickets">
|
<input
|
||||||
<i data-lucide="eye-off" id="iconHideClosed" class="w-3.5 h-3.5 text-emerald-600"></i>
|
type="text"
|
||||||
<span>Verberg Gesloten</span>
|
id="jiraSearchInput"
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
placeholder="Zoek in alle tickets..."
|
||||||
|
oninput="applyFilters()"
|
||||||
|
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl pl-7 pr-6 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<button onclick="clearSearch()" id="clearSearchBtn" class="hidden absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer">
|
||||||
|
<i data-lucide="x" class="w-3 h-3"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Snelfilter: In behandeling -->
|
|
||||||
<button type="button" onclick="toggleOnlyInProgress()" id="btnOnlyInProgress" class="flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700" title="Toon alleen tickets die op In Behandeling staan">
|
|
||||||
<i data-lucide="play" class="w-3 h-3 text-blue-500"></i>
|
|
||||||
<span>In behandeling</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Status Dropdown met Jira statussen -->
|
|
||||||
<div class="relative">
|
|
||||||
<select id="filterStatus" onchange="onStatusSelectChange(this.value)" class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl pl-2.5 pr-7 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer">
|
|
||||||
<option value="">Alle statussen</option>
|
|
||||||
<optgroup label="Nieuw / To Do">
|
|
||||||
<option value="To Do">To Do</option>
|
|
||||||
<option value="Nieuw">Nieuw</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="In Uitvoering / Behandeling">
|
|
||||||
<option value="In behandeling">In behandeling</option>
|
|
||||||
<option value="In Progress">In Progress</option>
|
|
||||||
<option value="Wachten op klant">Wachten op klant</option>
|
|
||||||
<option value="In Review">In Review</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="Afgerond / Gesloten">
|
|
||||||
<option value="Gereed">Gereed</option>
|
|
||||||
<option value="Gesloten">Gesloten</option>
|
|
||||||
<option value="Done">Done</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Zoekveld -->
|
|
||||||
<div class="relative w-full sm:w-44">
|
|
||||||
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2 pointer-events-none"></i>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="jiraSearchInput"
|
|
||||||
placeholder="Zoek ticket..."
|
|
||||||
oninput="applyFilters()"
|
|
||||||
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl pl-7 pr-6 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
||||||
>
|
|
||||||
<button onclick="clearSearch()" id="clearSearchBtn" class="hidden absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer">
|
|
||||||
<i data-lucide="x" class="w-3 h-3"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -873,14 +895,28 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
|||||||
const jiraData = <?= $jiraDataJson ?>;
|
const jiraData = <?= $jiraDataJson ?>;
|
||||||
const availableBoards = <?= $boardsJson ?>;
|
const availableBoards = <?= $boardsJson ?>;
|
||||||
|
|
||||||
let currentTab = 'assigned'; // 'assigned', 'team', 'in_progress', 'done'
|
|
||||||
let currentProject = '';
|
let currentProject = '';
|
||||||
let currentQueue = (jiraData.user_queue_filters && jiraData.user_queue_filters.length > 0) ? 'MY_QUEUES' : (jiraData.default_queue_filter || '');
|
let currentQueue = (jiraData.user_queue_filters && jiraData.user_queue_filters.length > 0) ? 'MY_QUEUES' : (jiraData.default_queue_filter || '');
|
||||||
let hideClosed = true; // Standaard gesloten tickets verbergen conform gebruikerswens!
|
let selectedStatuses = new Set();
|
||||||
let onlyInProgress = false;
|
|
||||||
let activeUrgentOnly = false;
|
let activeUrgentOnly = false;
|
||||||
let expandedKeys = new Set();
|
let expandedKeys = new Set();
|
||||||
const ticketStore = new Map();
|
const ticketStore = new Map();
|
||||||
|
const allJiraStatuses = <?= $statusesJson ?>;
|
||||||
|
|
||||||
|
// Hulpfunctie voor deduplicatie van tickets (assigned + team)
|
||||||
|
function getDeduplicatedIssues() {
|
||||||
|
const assigned = jiraData.assigned_issues || [];
|
||||||
|
const team = jiraData.team_issues || [];
|
||||||
|
const seen = new Set();
|
||||||
|
const list = [];
|
||||||
|
[...assigned, ...team].forEach(iss => {
|
||||||
|
if (iss && iss.key && !seen.has(iss.key)) {
|
||||||
|
seen.add(iss.key);
|
||||||
|
list.push(iss);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
function isDoneStatus(st) {
|
function isDoneStatus(st) {
|
||||||
const s = (st || '').toLowerCase().trim();
|
const s = (st || '').toLowerCase().trim();
|
||||||
@@ -893,15 +929,14 @@ function isInProgressStatus(st, sc) {
|
|||||||
return c === 'indeterminate' || s.includes('progress') || s.includes('bezig') || s.includes('review') || s.includes('uitvoering') || s.includes('behandeling') || s.includes('wachten');
|
return c === 'indeterminate' || s.includes('progress') || s.includes('bezig') || s.includes('review') || s.includes('uitvoering') || s.includes('behandeling') || s.includes('wachten');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bereken statussen en KPI statistieken
|
// Bereken statussen en KPI statistieken voor de drawer
|
||||||
function calculateKpis() {
|
function calculateKpis() {
|
||||||
|
const all = getDeduplicatedIssues();
|
||||||
const assigned = jiraData.assigned_issues || [];
|
const assigned = jiraData.assigned_issues || [];
|
||||||
const team = jiraData.team_issues || [];
|
const team = jiraData.team_issues || [];
|
||||||
const all = [...assigned, ...team];
|
|
||||||
|
|
||||||
let inProgressCount = 0;
|
let inProgressCount = 0;
|
||||||
let highPriorityCount = 0;
|
let highPriorityCount = 0;
|
||||||
let doneCount = 0;
|
|
||||||
|
|
||||||
all.forEach(iss => {
|
all.forEach(iss => {
|
||||||
const s = (iss.status || '').toLowerCase();
|
const s = (iss.status || '').toLowerCase();
|
||||||
@@ -911,10 +946,7 @@ function calculateKpis() {
|
|||||||
if (isInProgressStatus(s, sc)) {
|
if (isInProgressStatus(s, sc)) {
|
||||||
inProgressCount++;
|
inProgressCount++;
|
||||||
}
|
}
|
||||||
if (isDoneStatus(s) || sc === 'done') {
|
if (p === 'highest' || p === 'high' || p === 'p1') {
|
||||||
doneCount++;
|
|
||||||
}
|
|
||||||
if (p === 'highest' || p === 'high') {
|
|
||||||
highPriorityCount++;
|
highPriorityCount++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -929,21 +961,11 @@ function calculateKpis() {
|
|||||||
if (elStatInProg) elStatInProg.innerText = inProgressCount;
|
if (elStatInProg) elStatInProg.innerText = inProgressCount;
|
||||||
if (elStatHigh) elStatHigh.innerText = highPriorityCount;
|
if (elStatHigh) elStatHigh.innerText = highPriorityCount;
|
||||||
|
|
||||||
const elTabAssigned = document.getElementById('tabCount_assigned');
|
|
||||||
const elTabTeam = document.getElementById('tabCount_team');
|
|
||||||
const elTabInProg = document.getElementById('tabCount_in_progress');
|
|
||||||
const elTabDone = document.getElementById('tabCount_done');
|
|
||||||
|
|
||||||
if (elTabAssigned) elTabAssigned.innerText = assigned.length;
|
|
||||||
if (elTabTeam) elTabTeam.innerText = team.length;
|
|
||||||
if (elTabInProg) elTabInProg.innerText = inProgressCount;
|
|
||||||
if (elTabDone) elTabDone.innerText = doneCount;
|
|
||||||
|
|
||||||
updateProjectCounts();
|
updateProjectCounts();
|
||||||
updateQueueCounts();
|
updateQueueCounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uitklappen en inklappen van de filters & statistieken drawer
|
// Drawer open/dicht
|
||||||
function toggleJiraFiltersDrawer(forceOpen) {
|
function toggleJiraFiltersDrawer(forceOpen) {
|
||||||
const drawer = document.getElementById('jiraFiltersDrawer');
|
const drawer = document.getElementById('jiraFiltersDrawer');
|
||||||
const chevron = document.getElementById('filtersDrawerChevron');
|
const chevron = document.getElementById('filtersDrawerChevron');
|
||||||
@@ -961,7 +983,7 @@ function toggleJiraFiltersDrawer(forceOpen) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bepaal aantal actieve filters voor de badge in de rechter onderhoek
|
// Bepaal aantal actieve filters voor badge
|
||||||
function updateActiveFilterBadge() {
|
function updateActiveFilterBadge() {
|
||||||
const badge = document.getElementById('activeFilterBadge');
|
const badge = document.getElementById('activeFilterBadge');
|
||||||
if (!badge) return;
|
if (!badge) return;
|
||||||
@@ -971,8 +993,7 @@ function updateActiveFilterBadge() {
|
|||||||
if (currentQueue && currentQueue !== 'MY_QUEUES') activeCount++;
|
if (currentQueue && currentQueue !== 'MY_QUEUES') activeCount++;
|
||||||
if (document.getElementById('filterType')?.value) activeCount++;
|
if (document.getElementById('filterType')?.value) activeCount++;
|
||||||
if (document.getElementById('filterPriority')?.value) activeCount++;
|
if (document.getElementById('filterPriority')?.value) activeCount++;
|
||||||
if (document.getElementById('filterStatus')?.value) activeCount++;
|
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) activeCount++;
|
||||||
if (onlyInProgress) activeCount++;
|
|
||||||
|
|
||||||
if (activeCount > 0) {
|
if (activeCount > 0) {
|
||||||
badge.innerText = activeCount;
|
badge.innerText = activeCount;
|
||||||
@@ -982,73 +1003,236 @@ function updateActiveFilterBadge() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wissel tussen tabs
|
// ==========================================
|
||||||
function switchTab(tab) {
|
// STATUS MULTI-SELECT CHECKBOX DROPDOWN LOGICA
|
||||||
currentTab = tab;
|
// ==========================================
|
||||||
activeUrgentOnly = false;
|
|
||||||
|
|
||||||
// Update tab knoppen styling
|
function renderStatusCheckboxes() {
|
||||||
const tabs = ['assigned', 'team', 'in_progress', 'done'];
|
const container = document.getElementById('statusCheckboxList');
|
||||||
tabs.forEach(t => {
|
if (!container) return;
|
||||||
const btn = document.getElementById(`tabBtn_${t}`);
|
|
||||||
if (!btn) return;
|
if (!allJiraStatuses || allJiraStatuses.length === 0) {
|
||||||
if (t === tab) {
|
container.innerHTML = '<div class="py-2 text-center text-slate-400 italic text-[11px]">Geen Jira statussen gevonden</div>';
|
||||||
btn.className = "flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800/60 flex-shrink-0";
|
return;
|
||||||
} else {
|
}
|
||||||
btn.className = "flex items-center space-x-1.5 px-3 py-1.5 text-xs font-bold rounded-xl transition cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60 border border-transparent flex-shrink-0";
|
|
||||||
|
let html = '';
|
||||||
|
allJiraStatuses.forEach((st, idx) => {
|
||||||
|
const sNorm = (st.name || '').toLowerCase().trim();
|
||||||
|
const isChecked = selectedStatuses.has(sNorm);
|
||||||
|
const cat = (st.status_category || '').toLowerCase().trim();
|
||||||
|
|
||||||
|
let dotColor = 'bg-blue-500';
|
||||||
|
if (cat === 'new' || sNorm.includes('to do') || sNorm.includes('open') || sNorm.includes('nieuw')) {
|
||||||
|
dotColor = 'bg-slate-400';
|
||||||
|
} else if (cat === 'done' || isDoneStatus(sNorm)) {
|
||||||
|
dotColor = 'bg-emerald-500';
|
||||||
|
} else if (sNorm.includes('wacht') || sNorm.includes('klant') || sNorm.includes('pending')) {
|
||||||
|
dotColor = 'bg-amber-500';
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeId = 'chkStatus_' + idx;
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<label for="${safeId}" class="flex items-center justify-between px-2.5 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/80 cursor-pointer transition select-none group">
|
||||||
|
<div class="flex items-center space-x-2.5 min-w-0 flex-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="${safeId}"
|
||||||
|
value="${escapeHtml(sNorm)}"
|
||||||
|
${isChecked ? 'checked' : ''}
|
||||||
|
onchange="onStatusCheckboxChange()"
|
||||||
|
class="status-checkbox w-4 h-4 rounded text-indigo-600 border-slate-300 dark:border-slate-700 focus:ring-indigo-500 cursor-pointer flex-shrink-0"
|
||||||
|
>
|
||||||
|
<span class="w-2 h-2 rounded-full ${dotColor} flex-shrink-0"></span>
|
||||||
|
<span class="font-semibold text-slate-800 dark:text-slate-200 truncate text-xs group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition">${escapeHtml(st.name)}</span>
|
||||||
|
</div>
|
||||||
|
<span id="statusCount_${idx}" class="text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 ml-2 flex-shrink-0">0</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
updateStatusCounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatusCounts() {
|
||||||
|
const all = getDeduplicatedIssues();
|
||||||
|
allJiraStatuses.forEach((st, idx) => {
|
||||||
|
const sNorm = (st.name || '').toLowerCase().trim();
|
||||||
|
const el = document.getElementById(`statusCount_${idx}`);
|
||||||
|
if (el) {
|
||||||
|
let currentList = all;
|
||||||
|
if (currentProject) {
|
||||||
|
const pNorm = currentProject.trim().toUpperCase();
|
||||||
|
currentList = currentList.filter(iss => {
|
||||||
|
const pk = (iss.project_key || '').trim().toUpperCase();
|
||||||
|
const pn = (iss.project || '').trim().toLowerCase();
|
||||||
|
return pk === pNorm || pn.includes(pNorm.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (currentQueue) {
|
||||||
|
currentList = currentList.filter(iss => isIssueInQueue(iss, currentQueue));
|
||||||
|
}
|
||||||
|
const cnt = currentList.filter(iss => (iss.status || '').toLowerCase().trim() === sNorm).length;
|
||||||
|
el.innerText = cnt;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStatusDropdown(force) {
|
||||||
|
const menu = document.getElementById('statusDropdownMenu');
|
||||||
|
const chevron = document.getElementById('statusDropdownChevron');
|
||||||
|
const btn = document.getElementById('statusDropdownBtn');
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
const willOpen = (typeof force === 'boolean') ? force : menu.classList.contains('hidden');
|
||||||
|
if (willOpen) {
|
||||||
|
menu.classList.remove('hidden');
|
||||||
|
if (chevron) chevron.classList.add('rotate-180');
|
||||||
|
if (btn) btn.setAttribute('aria-expanded', 'true');
|
||||||
|
updateStatusCounts();
|
||||||
|
} else {
|
||||||
|
menu.classList.add('hidden');
|
||||||
|
if (chevron) chevron.classList.remove('rotate-180');
|
||||||
|
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const wrapper = document.getElementById('statusDropdownWrapper');
|
||||||
|
if (wrapper && !wrapper.contains(e.target)) {
|
||||||
|
toggleStatusDropdown(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onStatusCheckboxChange() {
|
||||||
|
selectedStatuses.clear();
|
||||||
|
const checked = document.querySelectorAll('.status-checkbox:checked');
|
||||||
|
checked.forEach(chk => {
|
||||||
|
if (chk.value) selectedStatuses.add(chk.value.toLowerCase().trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
updateStatusDropdownLabel();
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllStatuses(selectAll) {
|
||||||
|
selectedStatuses.clear();
|
||||||
|
const chks = document.querySelectorAll('.status-checkbox');
|
||||||
|
chks.forEach(chk => {
|
||||||
|
chk.checked = !!selectAll;
|
||||||
|
if (selectAll && chk.value) {
|
||||||
|
selectedStatuses.add(chk.value.toLowerCase().trim());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
updateStatusDropdownLabel();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle: Verberg Gesloten
|
function selectActiveStatusesOnly() {
|
||||||
function toggleHideClosed() {
|
selectedStatuses.clear();
|
||||||
hideClosed = !hideClosed;
|
const chks = document.querySelectorAll('.status-checkbox');
|
||||||
updateHideClosedButton();
|
chks.forEach(chk => {
|
||||||
|
const val = (chk.value || '').toLowerCase().trim();
|
||||||
|
const isDone = isDoneStatus(val);
|
||||||
|
chk.checked = !isDone;
|
||||||
|
if (!isDone) {
|
||||||
|
selectedStatuses.add(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('fociz_jira_selected_statuses', JSON.stringify([...selectedStatuses]));
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
updateStatusDropdownLabel();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHideClosedButton() {
|
function updateStatusDropdownLabel() {
|
||||||
const btn = document.getElementById('btnHideClosed');
|
const labelEl = document.getElementById('statusDropdownLabel');
|
||||||
if (!btn) return;
|
const badgeEl = document.getElementById('statusDropdownBadge');
|
||||||
if (hideClosed) {
|
if (!labelEl) return;
|
||||||
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800/80 shadow-2xs";
|
|
||||||
btn.innerHTML = `<i data-lucide="eye-off" class="w-3.5 h-3.5 text-emerald-600"></i><span>Verberg Gesloten</span><span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>`;
|
const total = allJiraStatuses.length;
|
||||||
|
const selected = selectedStatuses.size;
|
||||||
|
|
||||||
|
if (selected === total || (total === 0 && selected === 0)) {
|
||||||
|
labelEl.innerText = "Alle statussen";
|
||||||
|
if (badgeEl) badgeEl.classList.add('hidden');
|
||||||
|
} else if (selected === 0) {
|
||||||
|
labelEl.innerText = "Geen geselecteerd";
|
||||||
|
if (badgeEl) {
|
||||||
|
badgeEl.innerText = "0";
|
||||||
|
badgeEl.classList.remove('hidden');
|
||||||
|
badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-rose-100 dark:bg-rose-950/60 text-rose-700 dark:text-rose-300";
|
||||||
|
}
|
||||||
|
} else if (selected === 1) {
|
||||||
|
const singleNorm = [...selectedStatuses][0];
|
||||||
|
const match = allJiraStatuses.find(st => (st.name || '').toLowerCase().trim() === singleNorm);
|
||||||
|
labelEl.innerText = match ? match.name : singleNorm;
|
||||||
|
if (badgeEl) {
|
||||||
|
badgeEl.innerText = "1";
|
||||||
|
badgeEl.classList.remove('hidden');
|
||||||
|
badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700";
|
labelEl.innerText = `${selected} statussen`;
|
||||||
btn.innerHTML = `<i data-lucide="eye" class="w-3.5 h-3.5 text-slate-400"></i><span>Toon Ook Gesloten</span>`;
|
if (badgeEl) {
|
||||||
|
badgeEl.innerText = `${selected}/${total}`;
|
||||||
|
badgeEl.classList.remove('hidden');
|
||||||
|
badgeEl.className = "px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-indigo-100 dark:bg-indigo-950/60 text-indigo-700 dark:text-indigo-300";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const btnReset = document.getElementById('btnResetFiltersInline');
|
||||||
|
if (btnReset) {
|
||||||
|
const isCustom = selected < total || currentProject !== '' || (currentQueue !== '' && currentQueue !== 'MY_QUEUES') || document.getElementById('jiraSearchInput')?.value;
|
||||||
|
if (isCustom) btnReset.classList.remove('hidden');
|
||||||
|
else btnReset.classList.add('hidden');
|
||||||
}
|
}
|
||||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle: Snelfilter Alleen In Behandeling
|
function initStatusSelection() {
|
||||||
function toggleOnlyInProgress() {
|
selectedStatuses = new Set();
|
||||||
onlyInProgress = !onlyInProgress;
|
let saved = null;
|
||||||
updateOnlyInProgressButton();
|
try {
|
||||||
applyFilters();
|
const raw = localStorage.getItem('fociz_jira_selected_statuses');
|
||||||
}
|
if (raw) saved = JSON.parse(raw);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
function updateOnlyInProgressButton() {
|
if (Array.isArray(saved) && saved.length > 0) {
|
||||||
const btn = document.getElementById('btnOnlyInProgress');
|
saved.forEach(s => {
|
||||||
if (!btn) return;
|
if (s) selectedStatuses.add(String(s).toLowerCase().trim());
|
||||||
if (onlyInProgress) {
|
});
|
||||||
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-blue-600 text-white shadow-xs";
|
|
||||||
btn.innerHTML = `<i data-lucide="play" class="w-3 h-3 text-white fill-white"></i><span>In behandeling (Actief)</span>`;
|
|
||||||
} else {
|
} else {
|
||||||
btn.className = "flex items-center space-x-1.5 px-2.5 py-1.5 rounded-xl text-xs font-semibold transition cursor-pointer bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700";
|
// Standaard alle actieve statussen selecteren, gesloten uit conform gebruikerswens
|
||||||
btn.innerHTML = `<i data-lucide="play" class="w-3 h-3 text-blue-500"></i><span>In behandeling</span>`;
|
allJiraStatuses.forEach(st => {
|
||||||
|
const sNorm = (st.name || '').toLowerCase().trim();
|
||||||
|
const cat = (st.status_category || '').toLowerCase().trim();
|
||||||
|
if (cat !== 'done' && !isDoneStatus(sNorm)) {
|
||||||
|
selectedStatuses.add(sNorm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (selectedStatuses.size === 0) {
|
||||||
|
allJiraStatuses.forEach(st => {
|
||||||
|
selectedStatuses.add((st.name || '').toLowerCase().trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status dropdown verandering
|
renderStatusCheckboxes();
|
||||||
function onStatusSelectChange(val) {
|
updateStatusDropdownLabel();
|
||||||
if (val && isDoneStatus(val)) {
|
|
||||||
hideClosed = false;
|
|
||||||
updateHideClosedButton();
|
|
||||||
}
|
|
||||||
applyFilters();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project selectie via pill tabs
|
// Project selectie via pill tabs
|
||||||
@@ -1093,7 +1277,7 @@ function updateProjectPills() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort (SD, IN, AAN, GEN, DX of MY_QUEUES)
|
// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort
|
||||||
function isIssueInQueue(iss, qCode) {
|
function isIssueInQueue(iss, qCode) {
|
||||||
if (!qCode) return true;
|
if (!qCode) return true;
|
||||||
const code = qCode.toUpperCase().trim();
|
const code = qCode.toUpperCase().trim();
|
||||||
@@ -1144,7 +1328,6 @@ function isIssueInQueue(iss, qCode) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wachtrijfilter selectie
|
|
||||||
function selectQueueFilter(code) {
|
function selectQueueFilter(code) {
|
||||||
currentQueue = code;
|
currentQueue = code;
|
||||||
updateQueuePills();
|
updateQueuePills();
|
||||||
@@ -1164,23 +1347,14 @@ function updateQueuePills() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update de tellers op de project buttons
|
|
||||||
function updateProjectCounts() {
|
function updateProjectCounts() {
|
||||||
const assigned = jiraData.assigned_issues || [];
|
const all = getDeduplicatedIssues();
|
||||||
const team = jiraData.team_issues || [];
|
|
||||||
const all = [...assigned, ...team];
|
|
||||||
|
|
||||||
let currentList = all;
|
let currentList = all;
|
||||||
if (currentTab === 'assigned') currentList = assigned;
|
|
||||||
else if (currentTab === 'team') currentList = team;
|
|
||||||
else if (currentTab === 'in_progress') {
|
|
||||||
currentList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category));
|
|
||||||
} else if (currentTab === 'done') {
|
|
||||||
currentList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hideClosed && currentTab !== 'done') {
|
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
|
||||||
currentList = currentList.filter(iss => !isDoneStatus(iss.status) && (iss.status_category || '').toLowerCase() !== 'done');
|
currentList = currentList.filter(iss => selectedStatuses.has((iss.status || '').toLowerCase().trim()));
|
||||||
|
} else if (selectedStatuses.size === 0) {
|
||||||
|
currentList = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const elAll = document.getElementById('projCount_all');
|
const elAll = document.getElementById('projCount_all');
|
||||||
@@ -1204,23 +1378,14 @@ function updateProjectCounts() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update de tellers op de wachtrijfilter knoppen
|
|
||||||
function updateQueueCounts() {
|
function updateQueueCounts() {
|
||||||
const assigned = jiraData.assigned_issues || [];
|
const all = getDeduplicatedIssues();
|
||||||
const team = jiraData.team_issues || [];
|
|
||||||
const all = [...assigned, ...team];
|
|
||||||
|
|
||||||
let currentList = all;
|
let currentList = all;
|
||||||
if (currentTab === 'assigned') currentList = assigned;
|
|
||||||
else if (currentTab === 'team') currentList = team;
|
|
||||||
else if (currentTab === 'in_progress') {
|
|
||||||
currentList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category));
|
|
||||||
} else if (currentTab === 'done') {
|
|
||||||
currentList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hideClosed && currentTab !== 'done') {
|
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
|
||||||
currentList = currentList.filter(iss => !isDoneStatus(iss.status) && (iss.status_category || '').toLowerCase() !== 'done');
|
currentList = currentList.filter(iss => selectedStatuses.has((iss.status || '').toLowerCase().trim()));
|
||||||
|
} else if (selectedStatuses.size === 0) {
|
||||||
|
currentList = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentProject) {
|
if (currentProject) {
|
||||||
@@ -1257,7 +1422,22 @@ function updateQueueCounts() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snelkoppeling KPI-kaart Urgent
|
function filterByMyTickets() {
|
||||||
|
const sInput = document.getElementById('jiraSearchInput');
|
||||||
|
if (sInput) {
|
||||||
|
sInput.value = 'Mijzelf';
|
||||||
|
}
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByAllTickets() {
|
||||||
|
const sInput = document.getElementById('jiraSearchInput');
|
||||||
|
if (sInput) {
|
||||||
|
sInput.value = '';
|
||||||
|
}
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
function filterByUrgent() {
|
function filterByUrgent() {
|
||||||
const pSelect = document.getElementById('filterPriority');
|
const pSelect = document.getElementById('filterPriority');
|
||||||
if (pSelect) {
|
if (pSelect) {
|
||||||
@@ -1266,48 +1446,40 @@ function filterByUrgent() {
|
|||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zoekopdracht wissen
|
|
||||||
function clearSearch() {
|
function clearSearch() {
|
||||||
const input = document.getElementById('jiraSearchInput');
|
const input = document.getElementById('jiraSearchInput');
|
||||||
if (input) {
|
if (input) {
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
document.getElementById('clearSearchBtn').classList.add('hidden');
|
document.getElementById('clearSearchBtn')?.classList.add('hidden');
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alle filters herstellen
|
|
||||||
function resetAllFilters() {
|
function resetAllFilters() {
|
||||||
currentProject = '';
|
currentProject = '';
|
||||||
currentQueue = '';
|
currentQueue = '';
|
||||||
hideClosed = true;
|
selectAllStatuses(true);
|
||||||
onlyInProgress = false;
|
|
||||||
|
|
||||||
const searchInput = document.getElementById('jiraSearchInput');
|
const searchInput = document.getElementById('jiraSearchInput');
|
||||||
const typeSelect = document.getElementById('filterType');
|
const typeSelect = document.getElementById('filterType');
|
||||||
const prioritySelect = document.getElementById('filterPriority');
|
const prioritySelect = document.getElementById('filterPriority');
|
||||||
const statusSelect = document.getElementById('filterStatus');
|
|
||||||
|
|
||||||
if (searchInput) searchInput.value = '';
|
if (searchInput) searchInput.value = '';
|
||||||
if (typeSelect) typeSelect.value = '';
|
if (typeSelect) typeSelect.value = '';
|
||||||
if (prioritySelect) prioritySelect.value = '';
|
if (prioritySelect) prioritySelect.value = '';
|
||||||
if (statusSelect) statusSelect.value = '';
|
|
||||||
|
|
||||||
document.getElementById('clearSearchBtn')?.classList.add('hidden');
|
document.getElementById('clearSearchBtn')?.classList.add('hidden');
|
||||||
updateHideClosedButton();
|
|
||||||
updateOnlyInProgressButton();
|
|
||||||
updateProjectPills();
|
updateProjectPills();
|
||||||
updateQueuePills();
|
updateQueuePills();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter en render logic
|
// Filter en render logica
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase();
|
const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase();
|
||||||
const projectVal = (currentProject || '').toUpperCase().trim();
|
const projectVal = (currentProject || '').toUpperCase().trim();
|
||||||
const typeVal = (document.getElementById('filterType')?.value || '').trim().toLowerCase();
|
const typeVal = (document.getElementById('filterType')?.value || '').trim().toLowerCase();
|
||||||
const priorityVal = (document.getElementById('filterPriority')?.value || '').trim();
|
const priorityVal = (document.getElementById('filterPriority')?.value || '').trim();
|
||||||
const statusVal = (document.getElementById('filterStatus')?.value || '').trim().toLowerCase();
|
|
||||||
|
|
||||||
const clearBtn = document.getElementById('clearSearchBtn');
|
const clearBtn = document.getElementById('clearSearchBtn');
|
||||||
if (clearBtn) {
|
if (clearBtn) {
|
||||||
@@ -1315,43 +1487,24 @@ function applyFilters() {
|
|||||||
else clearBtn.classList.add('hidden');
|
else clearBtn.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
const assigned = jiraData.assigned_issues || [];
|
const all = getDeduplicatedIssues();
|
||||||
const team = jiraData.team_issues || [];
|
|
||||||
const all = [...assigned, ...team];
|
|
||||||
|
|
||||||
let sourceList = [];
|
// Filter tickets
|
||||||
if (currentTab === 'assigned') {
|
const filtered = all.filter(iss => {
|
||||||
sourceList = assigned;
|
|
||||||
} else if (currentTab === 'team') {
|
|
||||||
sourceList = team;
|
|
||||||
} else if (currentTab === 'in_progress') {
|
|
||||||
sourceList = all.filter(iss => isInProgressStatus(iss.status, iss.status_category));
|
|
||||||
} else if (currentTab === 'done') {
|
|
||||||
sourceList = all.filter(iss => isDoneStatus(iss.status) || (iss.status_category || '').toLowerCase() === 'done');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pas filters toe
|
|
||||||
const filtered = sourceList.filter(iss => {
|
|
||||||
const issPKey = (iss.project_key || '').toUpperCase().trim();
|
const issPKey = (iss.project_key || '').toUpperCase().trim();
|
||||||
const issPName = (iss.project || '').toLowerCase().trim();
|
const issPName = (iss.project || '').toLowerCase().trim();
|
||||||
const issStatus = (iss.status || '').toLowerCase().trim();
|
const issStatus = (iss.status || '').toLowerCase().trim();
|
||||||
const issCat = (iss.status_category || '').toLowerCase().trim();
|
|
||||||
|
|
||||||
// 1. Verberg Gesloten filter (als tab niet 'done' is en er niet expliciet een gesloten status gekozen is)
|
// 1. Status Filter via Checkboxes
|
||||||
if (currentTab !== 'done' && hideClosed && !isDoneStatus(statusVal)) {
|
if (selectedStatuses.size > 0 && selectedStatuses.size < allJiraStatuses.length) {
|
||||||
if (issCat === 'done' || isDoneStatus(issStatus)) {
|
if (!selectedStatuses.has(issStatus)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if (selectedStatuses.size === 0) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Snelfilter "Alleen in behandeling"
|
// 2. Project Filter (SD, IN, AAN, GEN of ander)
|
||||||
if (onlyInProgress) {
|
|
||||||
if (!isInProgressStatus(issStatus, issCat)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Project Filter (SD, IN, AAN, GEN of ander)
|
|
||||||
if (projectVal) {
|
if (projectVal) {
|
||||||
let matchesProj = (issPKey === projectVal);
|
let matchesProj = (issPKey === projectVal);
|
||||||
if (!matchesProj) {
|
if (!matchesProj) {
|
||||||
@@ -1364,26 +1517,19 @@ function applyFilters() {
|
|||||||
if (!matchesProj) return false;
|
if (!matchesProj) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Wachtrijfilter
|
// 3. Wachtrijfilter (MY_QUEUES, SD, IN, AAN, GEN, DX)
|
||||||
if (currentQueue) {
|
if (currentQueue) {
|
||||||
if (!isIssueInQueue(iss, currentQueue)) {
|
if (!isIssueInQueue(iss, currentQueue)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Specifieke status selectie
|
// 4. Type filter (uit drawer)
|
||||||
if (statusVal) {
|
|
||||||
if (issStatus !== statusVal) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Type filter
|
|
||||||
if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) {
|
if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Prioriteit filter
|
// 5. Prioriteit filter (uit drawer)
|
||||||
if (priorityVal) {
|
if (priorityVal) {
|
||||||
const pVal = priorityVal.toLowerCase();
|
const pVal = priorityVal.toLowerCase();
|
||||||
const ip = (iss.priority || '').toLowerCase();
|
const ip = (iss.priority || '').toLowerCase();
|
||||||
@@ -1398,7 +1544,7 @@ function applyFilters() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Zoekterm
|
// 6. Zoekterm
|
||||||
if (searchVal) {
|
if (searchVal) {
|
||||||
const keyMatch = (iss.key || '').toLowerCase().includes(searchVal);
|
const keyMatch = (iss.key || '').toLowerCase().includes(searchVal);
|
||||||
const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal);
|
const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal);
|
||||||
@@ -1417,6 +1563,7 @@ function applyFilters() {
|
|||||||
updateActiveFilterBadge();
|
updateActiveFilterBadge();
|
||||||
updateProjectCounts();
|
updateProjectCounts();
|
||||||
updateQueueCounts();
|
updateQueueCounts();
|
||||||
|
updateStatusCounts();
|
||||||
renderTickets(filtered);
|
renderTickets(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2266,8 +2413,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
calculateKpis();
|
calculateKpis();
|
||||||
updateHideClosedButton();
|
initStatusSelection();
|
||||||
updateOnlyInProgressButton();
|
|
||||||
updateProjectPills();
|
updateProjectPills();
|
||||||
updateQueueCounts();
|
updateQueueCounts();
|
||||||
if (currentQueue) {
|
if (currentQueue) {
|
||||||
|
|||||||
Reference in New Issue
Block a user