Jira fix
This commit is contained in:
@@ -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'));
|
||||||
@@ -531,9 +536,34 @@ try {
|
|||||||
error_log("Fout bij ophalen jira filters/users: " . $e->getMessage());
|
error_log("Fout bij ophalen jira filters/users: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audit Logs ophalen met 3 maanden bewaartermijn en paginering per 40 items
|
||||||
|
$auditPerPage = 40;
|
||||||
|
$auditPage = max(1, (int)($_GET['audit_page'] ?? 1));
|
||||||
|
$totalAuditLogs = 0;
|
||||||
|
$totalAuditPages = 1;
|
||||||
$auditLogs = [];
|
$auditLogs = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$auditLogs = $db->query("SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 50")->fetchAll() ?: [];
|
// Bewaartermijn: verwijder logs ouder dan 3 maanden (90 dagen)
|
||||||
|
$db->exec("DELETE FROM audit_logs WHERE created_at < NOW() - INTERVAL 90 DAY");
|
||||||
|
|
||||||
|
$totalAuditLogs = (int)$db->query("SELECT COUNT(*) FROM audit_logs WHERE created_at >= NOW() - INTERVAL 90 DAY")->fetchColumn();
|
||||||
|
$totalAuditPages = max(1, (int)ceil($totalAuditLogs / $auditPerPage));
|
||||||
|
if ($auditPage > $totalAuditPages) {
|
||||||
|
$auditPage = $totalAuditPages;
|
||||||
|
}
|
||||||
|
$offset = ($auditPage - 1) * $auditPerPage;
|
||||||
|
|
||||||
|
$stmtLogs = $db->prepare("
|
||||||
|
SELECT * FROM audit_logs
|
||||||
|
WHERE created_at >= NOW() - INTERVAL 90 DAY
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT :limit OFFSET :offset
|
||||||
|
");
|
||||||
|
$stmtLogs->bindValue(':limit', $auditPerPage, PDO::PARAM_INT);
|
||||||
|
$stmtLogs->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmtLogs->execute();
|
||||||
|
$auditLogs = $stmtLogs->fetchAll() ?: [];
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
error_log("Fout bij ophalen audit logs: " . $e->getMessage());
|
error_log("Fout bij ophalen audit logs: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
@@ -631,7 +661,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 +680,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 +1231,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 +1258,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 +1393,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 +1423,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 +1503,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 +1604,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 +1665,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">
|
||||||
@@ -1818,10 +1771,13 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
Onwijzigbare Audit Trail
|
Onwijzigbare Audit Trail
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
Centrale registratie van alle inlogpogingen, 2FA-verificaties, IP-detecties en beheerdersacties conform NIST SP 800-63B.
|
Centrale registratie van alle inlogpogingen, 2FA-verificaties, IP-detecties en beheerdersacties conform NIST SP 800-63B (bewaartermijn: 3 maanden).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2 flex-shrink-0">
|
<div class="flex items-center space-x-2 flex-shrink-0">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/60">
|
||||||
|
<?= $totalAuditLogs ?> <?= $totalAuditLogs === 1 ? 'gebeurtenis' : 'gebeurtenissen' ?>
|
||||||
|
</span>
|
||||||
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20" title="Tijdzone: Europe/Amsterdam (UTC+2 in zomertijd / UTC+1 in wintertijd)">
|
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20" title="Tijdzone: Europe/Amsterdam (UTC+2 in zomertijd / UTC+1 in wintertijd)">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5 animate-pulse"></span>
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5 animate-pulse"></span>
|
||||||
Live Audit Logging Actief • NL Tijd
|
Live Audit Logging Actief • NL Tijd
|
||||||
@@ -1914,10 +1870,72 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Paginering Navigatie (40 items per pagina, 3 maanden retentie) -->
|
||||||
|
<?php if ($totalAuditLogs > 0): ?>
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-4 border-t border-slate-200 dark:border-slate-800 text-xs">
|
||||||
|
<div class="text-slate-500 dark:text-slate-400">
|
||||||
|
Toont <span class="font-semibold text-slate-800 dark:text-slate-200"><?= ($offset + 1) ?></span> t/m
|
||||||
|
<span class="font-semibold text-slate-800 dark:text-slate-200"><?= min($totalAuditLogs, $offset + count($auditLogs)) ?></span> van
|
||||||
|
<span class="font-semibold text-slate-800 dark:text-slate-200"><?= $totalAuditLogs ?></span> gebeurtenissen (bewaartermijn: 3 maanden)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($totalAuditPages > 1): ?>
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<!-- Vorige knop -->
|
||||||
|
<?php if ($auditPage > 1): ?>
|
||||||
|
<a href="?tab=security&audit_page=<?= ($auditPage - 1) ?>#tab-pane-security" class="inline-flex items-center space-x-1 px-3 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/80 font-medium transition cursor-pointer">
|
||||||
|
<i data-lucide="chevron-left" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Vorige</span>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="inline-flex items-center space-x-1 px-3 py-1.5 rounded-lg border border-slate-100 dark:border-slate-800 text-slate-300 dark:text-slate-600 bg-slate-50/50 dark:bg-slate-900/30 cursor-not-allowed">
|
||||||
|
<i data-lucide="chevron-left" class="w-3.5 h-3.5"></i>
|
||||||
|
<span>Vorige</span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Paginanummers -->
|
||||||
|
<?php
|
||||||
|
$startPage = max(1, $auditPage - 2);
|
||||||
|
$endPage = min($totalAuditPages, $auditPage + 2);
|
||||||
|
if ($startPage > 1) {
|
||||||
|
echo '<a href="?tab=security&audit_page=1#tab-pane-security" class="px-2.5 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 font-medium transition cursor-pointer">1</a>';
|
||||||
|
if ($startPage > 2) echo '<span class="px-1 text-slate-400">…</span>';
|
||||||
|
}
|
||||||
|
for ($p = $startPage; $p <= $endPage; $p++) {
|
||||||
|
if ($p === $auditPage) {
|
||||||
|
echo '<span class="px-2.5 py-1.5 rounded-lg font-bold bg-indigo-600 text-white shadow-xs">' . $p . '</span>';
|
||||||
|
} else {
|
||||||
|
echo '<a href="?tab=security&audit_page=' . $p . '#tab-pane-security" class="px-2.5 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 font-medium transition cursor-pointer">' . $p . '</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($endPage < $totalAuditPages) {
|
||||||
|
if ($endPage < $totalAuditPages - 1) echo '<span class="px-1 text-slate-400">…</span>';
|
||||||
|
echo '<a href="?tab=security&audit_page=' . $totalAuditPages . '#tab-pane-security" class="px-2.5 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 font-medium transition cursor-pointer">' . $totalAuditPages . '</a>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Volgende knop -->
|
||||||
|
<?php if ($auditPage < $totalAuditPages): ?>
|
||||||
|
<a href="?tab=security&audit_page=<?= ($auditPage + 1) ?>#tab-pane-security" class="inline-flex items-center space-x-1 px-3 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/80 font-medium transition cursor-pointer">
|
||||||
|
<span>Volgende</span>
|
||||||
|
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="inline-flex items-center space-x-1 px-3 py-1.5 rounded-lg border border-slate-100 dark:border-slate-800 text-slate-300 dark:text-slate-600 bg-slate-50/50 dark:bg-slate-900/30 cursor-not-allowed">
|
||||||
|
<span>Volgende</span>
|
||||||
|
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</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 +2006,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 +2219,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 +2272,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 +2320,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;
|
||||||
|
|||||||
+70
-6
@@ -129,6 +129,12 @@ function fociz_log_audit(?int $userId, string $action, string $resourceType = ''
|
|||||||
$statusNormalized,
|
$statusNormalized,
|
||||||
$createdAt
|
$createdAt
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Bewaartermijn audit logs: 3 maanden (90 dagen)
|
||||||
|
// Voer periodiek (1 op de 20 log-acties) een automatische opschoning uit
|
||||||
|
if (mt_rand(1, 20) === 1) {
|
||||||
|
$db->exec("DELETE FROM audit_logs WHERE created_at < NOW() - INTERVAL 90 DAY");
|
||||||
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
// Audit log mag NOOIT de applicatiewerking breken
|
// Audit log mag NOOIT de applicatiewerking breken
|
||||||
error_log("fociz_log_audit failure: " . $e->getMessage());
|
error_log("fociz_log_audit failure: " . $e->getMessage());
|
||||||
@@ -419,6 +425,36 @@ function fociz_get_device_name(?string $userAgent): string {
|
|||||||
return "{$browser} op {$platform}";
|
return "{$browser} op {$platform}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruimt verlopen sessies op en beperkt het aantal sessies per gebruiker tot maximaal 20 (FIFO / oldest out)
|
||||||
|
*/
|
||||||
|
function fociz_cleanup_user_sessions(PDO $db, int $userId): void {
|
||||||
|
try {
|
||||||
|
// 1. Markeer sessies waarvan de geldigheidsduur is verstreken direct als inactief
|
||||||
|
$stmtExp = $db->prepare("UPDATE user_sessions SET is_active = 0 WHERE user_id = ? AND is_active = 1 AND expires_at < NOW()");
|
||||||
|
$stmtExp->execute([$userId]);
|
||||||
|
|
||||||
|
// 2. Beperk tot maximaal 20 sessies per gebruiker (FIFO: behoud de 20 meest recente, ruim oudere op)
|
||||||
|
$stmtLimit = $db->prepare("
|
||||||
|
SELECT id FROM user_sessions
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY last_activity_at DESC, id DESC
|
||||||
|
LIMIT 20, 200
|
||||||
|
");
|
||||||
|
$stmtLimit->execute([$userId]);
|
||||||
|
$excessIds = $stmtLimit->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
if (!empty($excessIds)) {
|
||||||
|
$inClause = implode(',', array_map('intval', $excessIds));
|
||||||
|
$db->exec("DELETE FROM user_sessions WHERE id IN ($inClause)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Verwijder inactieve/verlopen sessies ouder dan 7 dagen over de gehele tabel
|
||||||
|
$db->exec("DELETE FROM user_sessions WHERE is_active = 0 AND (expires_at < NOW() - INTERVAL 7 DAY OR last_activity_at < NOW() - INTERVAL 7 DAY)");
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log("fociz_cleanup_user_sessions error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registreert of vernieuwt een actieve sessie in user_sessions (SOC 2 CC6.1 / ISO 27001 A.9.4.2)
|
* Registreert of vernieuwt een actieve sessie in user_sessions (SOC 2 CC6.1 / ISO 27001 A.9.4.2)
|
||||||
*/
|
*/
|
||||||
@@ -447,8 +483,8 @@ function fociz_register_active_session(int $userId, string $sessionId, int $maxI
|
|||||||
");
|
");
|
||||||
$stmt->execute([$userId, $hash, $ip, $ua, $deviceName, $expiresAt]);
|
$stmt->execute([$userId, $hash, $ip, $ua, $deviceName, $expiresAt]);
|
||||||
|
|
||||||
// Verwijder oude verlopen sessies (> 7 dagen geleden verlopen)
|
// Ruim verlopen sessies op en handhaaf max 20 sessies
|
||||||
$db->exec("DELETE FROM user_sessions WHERE expires_at < NOW() - INTERVAL 7 DAY");
|
fociz_cleanup_user_sessions($db, $userId);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
error_log("fociz_register_active_session error: " . $e->getMessage());
|
error_log("fociz_register_active_session error: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
@@ -505,7 +541,14 @@ function fociz_is_session_active(string $sessionId, int $userId): bool {
|
|||||||
fociz_register_active_session($userId, $sessionId, $maxIdleSeconds);
|
fociz_register_active_session($userId, $sessionId, $maxIdleSeconds);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return (!empty($row['is_active']));
|
if (empty($row['is_active'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!empty($row['expires_at']) && strtotime($row['expires_at']) < time()) {
|
||||||
|
$db->prepare("UPDATE user_sessions SET is_active = 0 WHERE session_id_hash = ?")->execute([$hash]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
return true; // Fallback op toegestaan bij databaseverstoring
|
return true; // Fallback op toegestaan bij databaseverstoring
|
||||||
}
|
}
|
||||||
@@ -567,7 +610,7 @@ function fociz_revoke_session_by_id(int $userId, int $sessionDbId): bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Haalt alle actieve sessies van een gebruiker op voor de profielpagina
|
* Haalt alle actieve sessies van een gebruiker op voor de profielpagina (maximaal 20, actieve expiratiedatum)
|
||||||
*/
|
*/
|
||||||
function fociz_get_user_active_sessions(int $userId, string $currentSessionId): array {
|
function fociz_get_user_active_sessions(int $userId, string $currentSessionId): array {
|
||||||
try {
|
try {
|
||||||
@@ -575,14 +618,35 @@ function fociz_get_user_active_sessions(int $userId, string $currentSessionId):
|
|||||||
ensure_sessions_table($db);
|
ensure_sessions_table($db);
|
||||||
$currentHash = hash('sha256', $currentSessionId);
|
$currentHash = hash('sha256', $currentSessionId);
|
||||||
|
|
||||||
|
// Voer direct opschoning uit: deactiveer verlopen sessies en handhaaf limiet van max 20
|
||||||
|
fociz_cleanup_user_sessions($db, $userId);
|
||||||
|
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT id, ip_address, user_agent, device_name, last_activity_at, created_at, (session_id_hash = ?) AS is_current
|
SELECT id, ip_address, user_agent, device_name, last_activity_at, created_at, (session_id_hash = ?) AS is_current
|
||||||
FROM user_sessions
|
FROM user_sessions
|
||||||
WHERE user_id = ? AND is_active = 1
|
WHERE user_id = ? AND is_active = 1 AND expires_at >= NOW()
|
||||||
ORDER BY is_current DESC, last_activity_at DESC
|
ORDER BY is_current DESC, last_activity_at DESC
|
||||||
|
LIMIT 20
|
||||||
");
|
");
|
||||||
$stmt->execute([$currentHash, $userId]);
|
$stmt->execute([$currentHash, $userId]);
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
$sessions = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
|
||||||
|
// Zorg dat het huidige apparaat altijd geregistreerd staat als actief
|
||||||
|
$hasCurrent = false;
|
||||||
|
foreach ($sessions as $s) {
|
||||||
|
if (!empty($s['is_current'])) {
|
||||||
|
$hasCurrent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$hasCurrent && !empty($currentSessionId)) {
|
||||||
|
$maxIdleSeconds = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800;
|
||||||
|
fociz_register_active_session($userId, $currentSessionId, $maxIdleSeconds);
|
||||||
|
$stmt->execute([$currentHash, $userId]);
|
||||||
|
$sessions = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sessions;
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,6 +331,82 @@ function jira_fetch_statuses(string $domain, string $email, string $apiToken): a
|
|||||||
return $statuses;
|
return $statuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Haalt project-specifieke statussen op uit Jira Cloud voor een specifiek project
|
||||||
|
*/
|
||||||
|
function jira_fetch_project_statuses(string $domain, string $email, string $apiToken, string $projectKey): array {
|
||||||
|
$cleanDomain = jira_normalize_domain($domain);
|
||||||
|
$pKey = strtoupper(trim($projectKey));
|
||||||
|
if (empty($cleanDomain) || empty($email) || empty($apiToken) || empty($pKey)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheKey = "jira_proj_statuses_" . $pKey . "_" . md5($cleanDomain);
|
||||||
|
$cached = fociz_cache_get($cacheKey);
|
||||||
|
if (is_array($cached) && !empty($cached)) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authHeader = jira_get_auth_header($email, $apiToken);
|
||||||
|
$url = $cleanDomain . '/rest/api/3/project/' . rawurlencode($pKey) . '/statuses';
|
||||||
|
|
||||||
|
$response = fociz_http_request($url, [
|
||||||
|
'method' => 'GET',
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => $authHeader,
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
'timeout' => 8,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response['status'] === 404 || $response['status'] === 405) {
|
||||||
|
$urlV2 = $cleanDomain . '/rest/api/2/project/' . rawurlencode($pKey) . '/statuses';
|
||||||
|
$response = fociz_http_request($urlV2, [
|
||||||
|
'method' => 'GET',
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => $authHeader,
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
'timeout' => 8,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$statuses = [];
|
||||||
|
if ($response['status'] === 200 && is_array($response['json'])) {
|
||||||
|
foreach ($response['json'] as $issueType) {
|
||||||
|
if (!empty($issueType['statuses']) && is_array($issueType['statuses'])) {
|
||||||
|
foreach ($issueType['statuses'] as $st) {
|
||||||
|
$sName = trim($st['name'] ?? '');
|
||||||
|
if (empty($sName)) continue;
|
||||||
|
$sKey = strtolower($sName);
|
||||||
|
$catKey = strtolower($st['statusCategory']['key'] ?? 'indeterminate');
|
||||||
|
$catName = $st['statusCategory']['name'] ?? 'In uitvoering';
|
||||||
|
$statuses[$sKey] = [
|
||||||
|
'id' => (string)($st['id'] ?? ''),
|
||||||
|
'name' => $sName,
|
||||||
|
'description' => $st['description'] ?? '',
|
||||||
|
'status_category' => $catKey,
|
||||||
|
'category_name' => $catName,
|
||||||
|
'color' => $st['statusCategory']['colorName'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$statuses = array_values($statuses);
|
||||||
|
usort($statuses, function($a, $b) {
|
||||||
|
$order = ['new' => 1, 'indeterminate' => 2, 'done' => 3];
|
||||||
|
$oa = $order[$a['status_category']] ?? 2;
|
||||||
|
$ob = $order[$b['status_category']] ?? 2;
|
||||||
|
if ($oa !== $ob) return $oa - $ob;
|
||||||
|
return strcasecmp($a['name'], $b['name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
fociz_cache_set($cacheKey, $statuses, 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $statuses;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Haalt issues op via een flexibele JQL query via Atlassian REST API
|
* Haalt issues op via een flexibele JQL query via Atlassian REST API
|
||||||
*/
|
*/
|
||||||
@@ -974,6 +1050,34 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$demoProjectStatuses = [
|
||||||
|
'SD' => [
|
||||||
|
['id' => '1', 'name' => 'To Do', 'status_category' => 'new', 'category_name' => 'Nieuw'],
|
||||||
|
['id' => '2', 'name' => 'In behandeling', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'],
|
||||||
|
['id' => '3', 'name' => 'Wachten op klant', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'],
|
||||||
|
['id' => '4', 'name' => 'Gesloten', 'status_category' => 'done', 'category_name' => 'Gereed'],
|
||||||
|
],
|
||||||
|
'IN' => [
|
||||||
|
['id' => '10', 'name' => 'Nieuw', 'status_category' => 'new', 'category_name' => 'Nieuw'],
|
||||||
|
['id' => '11', 'name' => 'In behandeling', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'],
|
||||||
|
['id' => '12', 'name' => 'In Review', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'],
|
||||||
|
['id' => '13', 'name' => 'Gereed', 'status_category' => 'done', 'category_name' => 'Gereed'],
|
||||||
|
['id' => '14', 'name' => 'Gesloten', 'status_category' => 'done', 'category_name' => 'Gereed'],
|
||||||
|
],
|
||||||
|
'AAN' => [
|
||||||
|
['id' => '20', 'name' => 'Nieuw', 'status_category' => 'new', 'category_name' => 'Nieuw'],
|
||||||
|
['id' => '21', 'name' => 'In behandeling', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'],
|
||||||
|
['id' => '22', 'name' => 'Goedgekeurd', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'],
|
||||||
|
['id' => '23', 'name' => 'Afgewezen', 'status_category' => 'done', 'category_name' => 'Gereed'],
|
||||||
|
['id' => '24', 'name' => 'Gesloten', 'status_category' => 'done', 'category_name' => 'Gereed'],
|
||||||
|
],
|
||||||
|
'GEN' => [
|
||||||
|
['id' => '30', 'name' => 'To Do', 'status_category' => 'new', 'category_name' => 'Nieuw'],
|
||||||
|
['id' => '31', 'name' => 'In Progress', 'status_category' => 'indeterminate', 'category_name' => 'In uitvoering'],
|
||||||
|
['id' => '32', 'name' => 'Done', 'status_category' => 'done', 'category_name' => 'Gereed'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'is_demo' => true,
|
'is_demo' => true,
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@@ -987,6 +1091,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
|
|||||||
'default_queue_filter' => $userQueuePref,
|
'default_queue_filter' => $userQueuePref,
|
||||||
'user_queue_filters' => $userQueuePrefs,
|
'user_queue_filters' => $userQueuePrefs,
|
||||||
'available_statuses' => $demoStatuses,
|
'available_statuses' => $demoStatuses,
|
||||||
|
'project_statuses' => $demoProjectStatuses,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1041,6 +1146,23 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
|
|||||||
$assignedIssues = $assignedRes['success'] ? $assignedRes['issues'] : [];
|
$assignedIssues = $assignedRes['success'] ? $assignedRes['issues'] : [];
|
||||||
$teamIssues = $teamRes['success'] ? $teamRes['issues'] : [];
|
$teamIssues = $teamRes['success'] ? $teamRes['issues'] : [];
|
||||||
|
|
||||||
|
// Project-specifieke statussen ophalen voor gebruikte projecten
|
||||||
|
$activeProjKeys = ['SD', 'IN', 'AAN', 'GEN'];
|
||||||
|
$allLoaded = array_merge($assignedIssues, $teamIssues);
|
||||||
|
foreach ($allLoaded as $iss) {
|
||||||
|
$pk = strtoupper(trim($iss['project_key'] ?? ''));
|
||||||
|
if (!empty($pk) && !in_array($pk, $activeProjKeys, true)) {
|
||||||
|
$activeProjKeys[] = $pk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$projectStatusesMap = [];
|
||||||
|
foreach ($activeProjKeys as $pk) {
|
||||||
|
$pStats = jira_fetch_project_statuses($domain, $email, $apiToken, $pk);
|
||||||
|
if (!empty($pStats)) {
|
||||||
|
$projectStatusesMap[$pk] = $pStats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'is_demo' => false,
|
'is_demo' => false,
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@@ -1054,6 +1176,7 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array
|
|||||||
'default_queue_filter' => $userQueuePref,
|
'default_queue_filter' => $userQueuePref,
|
||||||
'user_queue_filters' => $userQueuePrefs,
|
'user_queue_filters' => $userQueuePrefs,
|
||||||
'available_statuses' => $availableStatuses,
|
'available_statuses' => $availableStatuses,
|
||||||
|
'project_statuses' => $projectStatusesMap,
|
||||||
];
|
];
|
||||||
|
|
||||||
fociz_cache_set($cacheKey, $data, 60);
|
fociz_cache_set($cacheKey, $data, 60);
|
||||||
|
|||||||
+1
-1
@@ -1284,7 +1284,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
Centraal overzicht van alle actieve inlogapparaten conform ISO 27001 en SOC 2 richtlijnen.
|
Centraal overzicht van alle actieve inlogapparaten conform ISO 27001 en SOC 2 richtlijnen (maximaal 20 actieve sessies).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user