diff --git a/admin.php b/admin.php index d96e99e..1dcb0b1 100644 --- a/admin.php +++ b/admin.php @@ -93,6 +93,74 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $messageType = 'success'; } + // 2b. Jira Wachtrijfilter toevoegen of bewerken + elseif ($action === 'save_jira_queue_filter') { + $filterId = (int)($_POST['filter_id'] ?? 0); + $filterCode = strtoupper(trim($_POST['filter_code'] ?? '')); + $filterName = trim($_POST['filter_name'] ?? ''); + $projectKey = strtoupper(trim($_POST['project_key'] ?? '')); + $jqlClause = trim($_POST['jql_clause'] ?? ''); + $description = trim($_POST['description'] ?? ''); + $isActive = isset($_POST['is_active']) ? 1 : 0; + $sortOrder = (int)($_POST['sort_order'] ?? 0); + + if (empty($filterCode) || empty($filterName) || empty($jqlClause)) { + $message = "Filtercode, naam en JQL-clausule zijn verplicht."; + $messageType = 'error'; + } else { + if ($filterId > 0) { + $stmt = $db->prepare(" + UPDATE jira_queue_filters + SET filter_code = ?, filter_name = ?, project_key = ?, jql_clause = ?, description = ?, is_active = ?, sort_order = ?, updated_at = NOW() + WHERE id = ? + "); + $stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder, $filterId]); + $message = "Wachtrijfilter '{$filterName}' is succesvol bijgewerkt."; + } else { + $stmt = $db->prepare(" + INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + filter_name = VALUES(filter_name), + project_key = VALUES(project_key), + jql_clause = VALUES(jql_clause), + description = VALUES(description), + is_active = VALUES(is_active), + sort_order = VALUES(sort_order), + updated_at = NOW() + "); + $stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]); + $message = "Nieuw wachtrijfilter '{$filterName}' is succesvol toegevoegd."; + } + $messageType = 'success'; + } + } + + // 2c. Jira Wachtrijfilter verwijderen + elseif ($action === 'delete_jira_queue_filter') { + $filterId = (int)($_POST['filter_id'] ?? 0); + if ($filterId > 0) { + $stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?"); + $stmt->execute([$filterId]); + $message = "Wachtrijfilter is succesvol verwijderd."; + $messageType = 'success'; + } + } + + // 2d. Teamleden toewijzen aan Jira Wachtrijfilter + elseif ($action === 'save_user_jira_assignments') { + $assignments = $_POST['user_filter'] ?? []; + if (is_array($assignments)) { + $upd = $db->prepare("UPDATE users SET default_jira_filter_code = ? WHERE id = ?"); + foreach ($assignments as $uId => $fCode) { + $fCode = trim($fCode); + $upd->execute([$fCode !== '' ? $fCode : null, (int)$uId]); + } + $message = "Standaard Jira wachtrijtoewijzingen voor het team zijn succesvol opgeslagen."; + $messageType = 'success'; + } + } + // 3. Organisatie Werkdag Standaarden opslaan (Release v1.4) elseif ($action === 'save_workday_settings') { $workdayStart = trim($_POST['workday_start'] ?? '08:30'); @@ -215,6 +283,15 @@ try { error_log("Fout bij ophalen recente gebruikers: " . $e->getMessage()); } +$allUsersForJira = []; +$allJiraFilters = []; +try { + $allUsersForJira = $db->query("SELECT id, name, email, role, avatar_url, default_jira_filter_code FROM users ORDER BY name ASC")->fetchAll() ?: []; + $allJiraFilters = $db->query("SELECT * FROM jira_queue_filters ORDER BY sort_order ASC, id ASC")->fetchAll() ?: []; +} catch (Throwable $e) { + error_log("Fout bij ophalen jira filters/users: " . $e->getMessage()); +} + $pageTitle = 'Systeembeheer & Integraties — Fociz'; require_once __DIR__ . '/includes/header.php'; ?> @@ -497,6 +574,286 @@ require_once __DIR__ . '/includes/header.php'; + +
+
+
+
+ +
+
+
+

Jira Wachtrijfilters & Toewijzingen

+ + ServiceDesk & Teams + +
+

+ Beheer aangepaste JQL-wachtrijfilters (zoals DX en SD) en wijs teamleden toe aan hun standaard wachtrij. +

+
+
+ +
+ + +
+
+

+ + Geconfigureerde Wachtrijfilters () +

+ Worden getoond als snelle filtertabs op de Jira pagina +
+ + +
+

Nog geen wachtrijfilters ingesteld. Klik op "Nieuw Wachtrijfilter" om te beginnen.

+
+ + +
+ + +
+
+
+ + + + + + + + + Project: + + + + Alle projecten + + + + + Inactief + + +
+ + +

+ +

+ + + +
+ JQL: +
+ + +
+ Standaard gekoppeld aan: + + + + + + + + + + Nog niemand specifiek toegewezen + +
+
+ + +
+ +
+ + + +
+
+
+ +
+ +
+ + +
+
+
+

+ + Team Wachtrijmatrix: Standaard Wachtrij per Collega +

+

+ Wijs collega's toe aan hun primaire wachtrij (bijv. DX of SD). Wanneer zij de Jira pagina openen, wordt deze wachtrij direct als standaard getoond. +

+
+
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + +
TeamlidE-mailadresRolStandaard Wachtrijfilter
+
+ + Avatar + +
+ +
+ + +
+
+ + + + + + + +
+
+ +
+ +
+
+
+
+ + + +
@@ -860,6 +1217,49 @@ function toggleAdminGuide() { arrow.classList.toggle('rotate-180'); } } + +// Jira Wachtrijfilter Modalfuncties +function openJiraFilterModal() { + document.getElementById('jiraFilterModalTitle').innerText = 'Nieuw Wachtrijfilter Toevoegen'; + document.getElementById('filterModal_id').value = '0'; + document.getElementById('filterModal_code').value = ''; + document.getElementById('filterModal_name').value = ''; + document.getElementById('filterModal_project').value = 'SD'; + document.getElementById('filterModal_jql').value = ''; + document.getElementById('filterModal_desc').value = ''; + document.getElementById('filterModal_active').checked = true; + + const modal = document.getElementById('jiraFilterModal'); + modal.classList.remove('hidden'); + modal.classList.add('flex'); +} + +function editJiraFilter(filter) { + document.getElementById('jiraFilterModalTitle').innerText = `Wachtrijfilter '${filter.filter_name}' Bewerken`; + document.getElementById('filterModal_id').value = filter.id || '0'; + document.getElementById('filterModal_code').value = filter.filter_code || ''; + document.getElementById('filterModal_name').value = filter.filter_name || ''; + document.getElementById('filterModal_project').value = filter.project_key || ''; + document.getElementById('filterModal_jql').value = filter.jql_clause || ''; + document.getElementById('filterModal_desc').value = filter.description || ''; + document.getElementById('filterModal_active').checked = (filter.is_active != 0); + + const modal = document.getElementById('jiraFilterModal'); + modal.classList.remove('hidden'); + modal.classList.add('flex'); +} + +function closeJiraFilterModal() { + const modal = document.getElementById('jiraFilterModal'); + modal.classList.add('hidden'); + modal.classList.remove('flex'); +} + +document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + closeJiraFilterModal(); + } +}); diff --git a/api/integrations.php b/api/integrations.php index b8f78e7..7c8656c 100644 --- a/api/integrations.php +++ b/api/integrations.php @@ -272,6 +272,107 @@ try { exit; } + // 5a. Jira Wachtrijfilters Ophalen + if ($action === 'get_jira_queue_filters') { + $onlyActive = !isset($_GET['all']) && !isset($_POST['all']); + $filters = jira_get_queue_filters($onlyActive); + echo json_encode(['success' => true, 'filters' => $filters]); + exit; + } + + // 5b. Jira Wachtrijfilter Opslaan / Wijzigen (Alleen Beheerders) + if ($action === 'save_jira_queue_filter') { + if (($user['role'] ?? '') !== 'ADMIN') { + echo json_encode(['error' => 'Alleen beheerders mogen wachtrijfilters aanpassen.']); + exit; + } + + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $id = (int)($input['id'] ?? 0); + $filterCode = strtoupper(trim($input['filter_code'] ?? '')); + $filterName = trim($input['filter_name'] ?? ''); + $projectKey = strtoupper(trim($input['project_key'] ?? '')); + $jqlClause = trim($input['jql_clause'] ?? ''); + $description = trim($input['description'] ?? ''); + $isActive = isset($input['is_active']) ? (int)$input['is_active'] : 1; + $sortOrder = (int)($input['sort_order'] ?? 0); + + if (empty($filterCode) || empty($filterName) || empty($jqlClause)) { + echo json_encode(['error' => 'Code, naam en JQL-clausule zijn verplicht.']); + exit; + } + + if ($id > 0) { + $stmt = $db->prepare(" + UPDATE jira_queue_filters + SET filter_code = ?, filter_name = ?, project_key = ?, jql_clause = ?, description = ?, is_active = ?, sort_order = ?, updated_at = NOW() + WHERE id = ? + "); + $stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder, $id]); + } else { + $stmt = $db->prepare(" + INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + filter_name = VALUES(filter_name), + project_key = VALUES(project_key), + jql_clause = VALUES(jql_clause), + description = VALUES(description), + is_active = VALUES(is_active), + sort_order = VALUES(sort_order), + updated_at = NOW() + "); + $stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]); + } + + echo json_encode(['success' => true, 'message' => "Wachtrijfilter '{$filterName}' is succesvol opgeslagen."]); + exit; + } + + // 5c. Jira Wachtrijfilter Verwijderen (Alleen Beheerders) + if ($action === 'delete_jira_queue_filter') { + if (($user['role'] ?? '') !== 'ADMIN') { + echo json_encode(['error' => 'Alleen beheerders mogen wachtrijfilters verwijderen.']); + exit; + } + + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $id = (int)($input['id'] ?? 0); + $filterCode = trim($input['filter_code'] ?? ''); + + if ($id > 0) { + $stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?"); + $stmt->execute([$id]); + } elseif (!empty($filterCode)) { + $stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE filter_code = ?"); + $stmt->execute([$filterCode]); + } else { + echo json_encode(['error' => 'Filter ID of code ontbreekt.']); + exit; + } + + echo json_encode(['success' => true, 'message' => 'Wachtrijfilter is verwijderd.']); + exit; + } + + // 5d. Standaard Wachtrijfilter Toewijzen aan Gebruiker (Beheerders of Gebruiker zelf) + if ($action === 'set_user_jira_filter') { + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $targetUserId = (int)($input['user_id'] ?? $user['id']); + $filterCode = trim($input['filter_code'] ?? ''); + + if ($targetUserId !== (int)$user['id'] && ($user['role'] ?? '') !== 'ADMIN') { + echo json_encode(['error' => 'Geen toestemming om instellingen van andere gebruikers te wijzigen.']); + exit; + } + + $stmt = $db->prepare("UPDATE users SET default_jira_filter_code = ? WHERE id = ?"); + $stmt->execute([$filterCode ?: null, $targetUserId]); + + echo json_encode(['success' => true, 'message' => 'Wachtrijvoorkeur is succesvol bijgewerkt.']); + exit; + } + echo json_encode(['error' => 'Onbekende actie']); } catch (Exception $e) { http_response_code(500); diff --git a/api/live_sync.php b/api/live_sync.php new file mode 100644 index 0000000..0fe34b5 --- /dev/null +++ b/api/live_sync.php @@ -0,0 +1,152 @@ + $latestMail['id'], + 'subject' => $latestMail['subject'], + 'from_name' => $latestMail['from_name'], + 'from_email' => $latestMail['from_email'], + 'received_human' => $latestMail['received_human'], + 'web_link' => $latestMail['web_link'] ?? '/mail.php', + ]; + } + } + + // 2. Jira Tickets Controle (rekening houdend met persoonlijke toewijzing & default queue filter zoals DX of SD) + $userFilterCode = $user['default_jira_filter_code'] ?? ''; + $jiraData = jira_get_user_issues($userId); + $assignedIssues = $jiraData['assigned_issues'] ?? ($jiraData['issues'] ?? []); + $teamIssues = $jiraData['team_issues'] ?? []; + + $dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; + $relevantIssues = []; + + // Filter issues die specifiek relevant zijn voor deze medewerker + foreach (array_merge($assignedIssues, $teamIssues) as $iss) { + $isAssignedToMe = !empty($iss['assignee_name']) && (stripos($iss['assignee_name'], $user['name']) !== false || stripos($iss['assignee_name'], 'Niek') !== false); + $ol = strtolower(trim($iss['onderwerp_logging'] ?? '')); + + $isQueueMatch = false; + if (!empty($userFilterCode)) { + if ($userFilterCode === 'DX' && in_array($ol, $dxValues)) { + $isQueueMatch = true; + } elseif ($userFilterCode === 'SD' && ($ol === '' || $ol === 'empty' || !in_array($ol, $dxValues))) { + $isQueueMatch = true; + } + } + + if ($isAssignedToMe || $isQueueMatch) { + $relevantIssues[$iss['key']] = $iss; + } + } + + $hasNewJira = false; + $newJiraPayload = []; + $newestJiraKey = ''; + + if (!empty($relevantIssues)) { + // Pak het nieuwste ticket + $newest = reset($relevantIssues); + $newestJiraKey = $newest['key']; + + if (!empty($clientLastJiraKey) && $newestJiraKey !== $clientLastJiraKey) { + $hasNewJira = true; + $newJiraPayload = [ + 'key' => $newest['key'], + 'summary' => $newest['summary'], + 'priority' => $newest['priority'] ?? 'Medium', + 'issue_type' => $newest['issue_type'] ?? 'Taak', + 'project' => $newest['project'] ?? 'SD', + 'url' => $newest['url'] ?? ('/jira.php?highlight=' . $newest['key']), + ]; + } + } + + // 3. Outlook Agenda Controle (Tijdlijn events van vandaag) + $calendarChanged = false; + $todayEvents = []; + $stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1"); + $stmtInt->execute([$userId]); + $outlookInt = $stmtInt->fetch(); + + if ($outlookInt && !empty($outlookInt['access_token']) && empty($outlookInt['is_demo_mode'])) { + $todayEvents = microsoft_fetch_today_events($userId, $today); + } + + $calHash = md5(json_encode(array_map(function($e) { + return ($e['id'] ?? '') . ($e['start'] ?? '') . ($e['end'] ?? '') . ($e['subject'] ?? ''); + }, $todayEvents))); + + if (!empty($clientCalendarHash) && $calHash !== $clientCalendarHash) { + $calendarChanged = true; + } + + // 4. Update sync timestamps in database + $stmtUpdate = $db->prepare("UPDATE users SET last_mail_check_at = NOW(), last_jira_check_at = NOW() WHERE id = ?"); + $stmtUpdate->execute([$userId]); + + echo json_encode([ + 'success' => true, + 'server_time' => date('H:i:s'), + 'mail' => [ + 'has_new' => $hasNewMail, + 'unread_count' => $unreadCount, + 'latest_id' => $latestMail['id'] ?? '', + 'new_mail' => $newMailPayload, + ], + 'jira' => [ + 'has_new' => $hasNewJira, + 'newest_key' => $newestJiraKey, + 'new_issue' => $newJiraPayload, + ], + 'calendar' => [ + 'has_changes' => $calendarChanged, + 'hash' => $calHash, + 'count' => count($todayEvents), + ], + ]); + exit; + +} catch (Throwable $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage(), + ]); +} + diff --git a/api/mail.php b/api/mail.php new file mode 100644 index 0000000..52c8571 --- /dev/null +++ b/api/mail.php @@ -0,0 +1,188 @@ + false, 'error' => 'Geen bericht-ID opgegeven.']); + exit; + } + + $detail = microsoft_fetch_message_detail($userId, $messageId); + if (!$detail) { + echo json_encode(['success' => false, 'error' => 'Bericht niet gevonden of geen toegang.']); + exit; + } + + echo json_encode(['success' => true, 'message' => $detail]); + exit; + } + + // 3. Markeer als gelezen of ongelezen + if ($action === 'mark_read') { + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $messageId = trim($input['id'] ?? ''); + $isRead = isset($input['is_read']) ? (bool)$input['is_read'] : true; + + if (empty($messageId)) { + echo json_encode(['success' => false, 'error' => 'Geen bericht-ID opgegeven.']); + exit; + } + + $ok = microsoft_mark_message_read($userId, $messageId, $isRead); + echo json_encode(['success' => $ok]); + exit; + } + + // 4. E-mail beantwoorden + if ($action === 'reply') { + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $messageId = trim($input['id'] ?? ''); + $comment = trim($input['comment'] ?? ''); + $replyAll = !empty($input['reply_all']); + + if (empty($messageId) || empty($comment)) { + echo json_encode(['success' => false, 'error' => 'Bericht-ID en reactietekst zijn verplicht.']); + exit; + } + + $res = microsoft_reply_message($userId, $messageId, $comment, $replyAll); + echo json_encode($res); + exit; + } + + // 5. E-mail doorsturen + if ($action === 'forward') { + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $messageId = trim($input['id'] ?? ''); + $toInput = $input['to'] ?? ''; + $comment = trim($input['comment'] ?? ''); + + if (empty($messageId) || empty($toInput)) { + echo json_encode(['success' => false, 'error' => 'Bericht-ID en minimaal één geadresseerde zijn verplicht.']); + exit; + } + + $recipients = []; + if (is_array($toInput)) { + $recipients = $toInput; + } else { + $parts = explode(',', (string)$toInput); + foreach ($parts as $p) { + $p = trim($p); + if (filter_var($p, FILTER_VALIDATE_EMAIL)) { + $recipients[] = ['address' => $p, 'name' => $p]; + } + } + } + + if (empty($recipients)) { + echo json_encode(['success' => false, 'error' => 'Geen geldige e-mailadressen ingevoerd.']); + exit; + } + + $res = microsoft_forward_message($userId, $messageId, $recipients, $comment); + echo json_encode($res); + exit; + } + + // 6. E-mail direct inplannen in Fociz dagplanning + if ($action === 'plan_email') { + $input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + $messageId = trim($input['message_id'] ?? ''); + $subject = trim($input['subject'] ?? 'E-mail actiepunt'); + $planDate = trim($input['plan_date'] ?? date('Y-m-d')); + $timeBlockStart = !empty($input['time_block_start']) ? trim($input['time_block_start']) : null; + $durationMinutes = (int)($input['duration_minutes'] ?? 30); + $priority = in_array($input['priority'] ?? '', ['P1', 'P2', 'P3']) ? $input['priority'] : 'P2'; + $webLink = trim($input['web_link'] ?? 'https://outlook.office.com/mail/'); + $sender = trim($input['sender'] ?? ''); + + if (empty($subject)) { + echo json_encode(['success' => false, 'error' => 'Onderwerp is verplicht.']); + exit; + } + + if ($durationMinutes <= 0) $durationMinutes = 30; + + $timeBlockEnd = null; + if (!empty($timeBlockStart)) { + try { + $dtEnd = new DateTime("{$planDate} {$timeBlockStart}:00", new DateTimeZone('Europe/Amsterdam')); + $dtEnd->modify("+{$durationMinutes} minutes"); + $timeBlockEnd = $dtEnd->format('H:i'); + } catch (Throwable $t) {} + } + + // Zorg dat daily_plan bestaat + $stmtPlan = $db->prepare(" + INSERT INTO daily_plans (user_id, plan_date) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id) + "); + $stmtPlan->execute([$userId, $planDate]); + $planId = (int)$db->lastInsertId(); + + $taskTitle = "✉️ " . $subject; + if (!empty($sender)) { + $taskTitle .= " (van " . $sender . ")"; + } + + $stmtTask = $db->prepare(" + INSERT INTO tasks (user_id, daily_plan_id, title, priority, time_block_start, time_block_end, duration_minutes, source, external_id, external_url) + VALUES (?, ?, ?, ?, ?, ?, ?, 'OUTLOOK_MAIL', ?, ?) + "); + $stmtTask->execute([ + $userId, + $planId, + $taskTitle, + $priority, + $timeBlockStart, + $timeBlockEnd, + $durationMinutes, + $messageId, + $webLink + ]); + $taskId = (int)$db->lastInsertId(); + + echo json_encode([ + 'success' => true, + 'task_id' => $taskId, + 'plan_date' => $planDate, + 'message' => "E-mail succesvol ingepland voor {$planDate}." + ]); + exit; + } + + echo json_encode(['success' => false, 'error' => 'Onbekende e-mailactie.']); +} catch (Throwable $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} + diff --git a/api/webhooks/jira.php b/api/webhooks/jira.php new file mode 100644 index 0000000..ad3a800 --- /dev/null +++ b/api/webhooks/jira.php @@ -0,0 +1,40 @@ + 'Invalid JSON payload']); + exit; +} + +try { + $db = get_db_connection(); + $event = $data['webhookEvent'] ?? 'jira:unknown'; + $issueKey = $data['issue']['key'] ?? ''; + $summary = $data['issue']['fields']['summary'] ?? ''; + + // Sla event tijdelijk op of invalidate session cache + error_log("Jira Webhook ontvangen: {$event} voor {$issueKey} - {$summary}"); + + echo json_encode([ + 'received' => true, + 'event' => $event, + 'issue_key' => $issueKey, + 'timestamp' => date('Y-m-d H:i:s') + ]); + exit; +} catch (Throwable $t) { + http_response_code(500); + echo json_encode(['error' => $t->getMessage()]); +} + diff --git a/api/webhooks/microsoft.php b/api/webhooks/microsoft.php new file mode 100644 index 0000000..96b6d8b --- /dev/null +++ b/api/webhooks/microsoft.php @@ -0,0 +1,31 @@ + true]); + diff --git a/assets/avatars/avatar-01.svg b/assets/avatars/avatar-01.svg index 224692b..28023ef 100644 --- a/assets/avatars/avatar-01.svg +++ b/assets/avatars/avatar-01.svg @@ -1,18 +1 @@ - - - - - - - - - - - - - - - - - - +Fun Emoji SetDavis Uchehttps://www.figma.com/community/file/968125295144990435https://creativecommons.org/licenses/by/4.0/Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-02.svg b/assets/avatars/avatar-02.svg index d4b9bf2..7b6f5b3 100644 --- a/assets/avatars/avatar-02.svg +++ b/assets/avatars/avatar-02.svg @@ -1,22 +1 @@ - - - - - - - - - - - - - - - - - - - - - - +Fun Emoji SetDavis Uchehttps://www.figma.com/community/file/968125295144990435https://creativecommons.org/licenses/by/4.0/Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-03.svg b/assets/avatars/avatar-03.svg index 684089b..fbc8907 100644 --- a/assets/avatars/avatar-03.svg +++ b/assets/avatars/avatar-03.svg @@ -1,22 +1 @@ - - - - - - - - - - - - - - - - - - - - - - +Fun Emoji SetDavis Uchehttps://www.figma.com/community/file/968125295144990435https://creativecommons.org/licenses/by/4.0/Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-04.svg b/assets/avatars/avatar-04.svg index 8fc7bf5..13d17b4 100644 --- a/assets/avatars/avatar-04.svg +++ b/assets/avatars/avatar-04.svg @@ -1,20 +1 @@ - - - - - - - - - - - - - - - - - - - - +Fun Emoji SetDavis Uchehttps://www.figma.com/community/file/968125295144990435https://creativecommons.org/licenses/by/4.0/Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-05.svg b/assets/avatars/avatar-05.svg index 5acb03f..434ce20 100644 --- a/assets/avatars/avatar-05.svg +++ b/assets/avatars/avatar-05.svg @@ -1,18 +1 @@ - - - - - - - - - - - - - - - - - - +Fun Emoji SetDavis Uchehttps://www.figma.com/community/file/968125295144990435https://creativecommons.org/licenses/by/4.0/Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-06.svg b/assets/avatars/avatar-06.svg index 66025a4..bd1e23b 100644 --- a/assets/avatars/avatar-06.svg +++ b/assets/avatars/avatar-06.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - +Fun Emoji SetDavis Uchehttps://www.figma.com/community/file/968125295144990435https://creativecommons.org/licenses/by/4.0/Remix of „Fun Emoji Set” (https://www.figma.com/community/file/968125295144990435) by „Davis Uche”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-07.svg b/assets/avatars/avatar-07.svg index 8ca3bcc..729227c 100644 --- a/assets/avatars/avatar-07.svg +++ b/assets/avatars/avatar-07.svg @@ -1,24 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - +BotttsPablo Stanleyhttps://bottts.com/https://bottts.com/Remix of „Bottts” (https://bottts.com/) by „Pablo Stanley”, licensed under „Free for personal and commercial use” (https://bottts.com/) \ No newline at end of file diff --git a/assets/avatars/avatar-08.svg b/assets/avatars/avatar-08.svg index 8cd0bb9..91a2ad5 100644 --- a/assets/avatars/avatar-08.svg +++ b/assets/avatars/avatar-08.svg @@ -1,20 +1 @@ - - - - - - - - - - - - - - - - - - - - +BotttsPablo Stanleyhttps://bottts.com/https://bottts.com/Remix of „Bottts” (https://bottts.com/) by „Pablo Stanley”, licensed under „Free for personal and commercial use” (https://bottts.com/) \ No newline at end of file diff --git a/assets/avatars/avatar-09.svg b/assets/avatars/avatar-09.svg index 8a4a163..7fbf00b 100644 --- a/assets/avatars/avatar-09.svg +++ b/assets/avatars/avatar-09.svg @@ -1,21 +1 @@ - - - - - - - - - - - - - - - - - - - - - +BotttsPablo Stanleyhttps://bottts.com/https://bottts.com/Remix of „Bottts” (https://bottts.com/) by „Pablo Stanley”, licensed under „Free for personal and commercial use” (https://bottts.com/) \ No newline at end of file diff --git a/assets/avatars/avatar-10.svg b/assets/avatars/avatar-10.svg index 81bb3b2..2ff9383 100644 --- a/assets/avatars/avatar-10.svg +++ b/assets/avatars/avatar-10.svg @@ -1,22 +1 @@ - - - - - - - - - - - - - - - - - - - - - - +BotttsPablo Stanleyhttps://bottts.com/https://bottts.com/Remix of „Bottts” (https://bottts.com/) by „Pablo Stanley”, licensed under „Free for personal and commercial use” (https://bottts.com/) \ No newline at end of file diff --git a/assets/avatars/avatar-11.svg b/assets/avatars/avatar-11.svg index fe7cb1d..701f6c1 100644 --- a/assets/avatars/avatar-11.svg +++ b/assets/avatars/avatar-11.svg @@ -1,19 +1 @@ - - - - - - - - - - - - - - - - - - - +BotttsPablo Stanleyhttps://bottts.com/https://bottts.com/Remix of „Bottts” (https://bottts.com/) by „Pablo Stanley”, licensed under „Free for personal and commercial use” (https://bottts.com/) \ No newline at end of file diff --git a/assets/avatars/avatar-12.svg b/assets/avatars/avatar-12.svg index 5deec67..383bfda 100644 --- a/assets/avatars/avatar-12.svg +++ b/assets/avatars/avatar-12.svg @@ -1,23 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - +BotttsPablo Stanleyhttps://bottts.com/https://bottts.com/Remix of „Bottts” (https://bottts.com/) by „Pablo Stanley”, licensed under „Free for personal and commercial use” (https://bottts.com/) \ No newline at end of file diff --git a/assets/avatars/avatar-13.svg b/assets/avatars/avatar-13.svg index 87ed632..008d9ab 100644 --- a/assets/avatars/avatar-13.svg +++ b/assets/avatars/avatar-13.svg @@ -1,18 +1 @@ - - - - - - - - - - - - - - - - - - +LoreleiLisa Wischofskyhttps://www.figma.com/community/file/1198749693280469639https://creativecommons.org/publicdomain/zero/1.0/Remix of „Lorelei” (https://www.figma.com/community/file/1198749693280469639) by „Lisa Wischofsky”, licensed under „CC0 1.0” (https://creativecommons.org/publicdomain/zero/1.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-14.svg b/assets/avatars/avatar-14.svg index e99eab5..cf19283 100644 --- a/assets/avatars/avatar-14.svg +++ b/assets/avatars/avatar-14.svg @@ -1,27 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - +LoreleiLisa Wischofskyhttps://www.figma.com/community/file/1198749693280469639https://creativecommons.org/publicdomain/zero/1.0/Remix of „Lorelei” (https://www.figma.com/community/file/1198749693280469639) by „Lisa Wischofsky”, licensed under „CC0 1.0” (https://creativecommons.org/publicdomain/zero/1.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-15.svg b/assets/avatars/avatar-15.svg index 2366585..e5fbd9c 100644 --- a/assets/avatars/avatar-15.svg +++ b/assets/avatars/avatar-15.svg @@ -1,22 +1 @@ - - - - - - - - - - - - - - - - - - - - - - +LoreleiLisa Wischofskyhttps://www.figma.com/community/file/1198749693280469639https://creativecommons.org/publicdomain/zero/1.0/Remix of „Lorelei” (https://www.figma.com/community/file/1198749693280469639) by „Lisa Wischofsky”, licensed under „CC0 1.0” (https://creativecommons.org/publicdomain/zero/1.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-16.svg b/assets/avatars/avatar-16.svg index 60c5cc8..fe030f4 100644 --- a/assets/avatars/avatar-16.svg +++ b/assets/avatars/avatar-16.svg @@ -1,20 +1 @@ - - - - - - - - - - - - - - - - - - - - +LoreleiLisa Wischofskyhttps://www.figma.com/community/file/1198749693280469639https://creativecommons.org/publicdomain/zero/1.0/Remix of „Lorelei” (https://www.figma.com/community/file/1198749693280469639) by „Lisa Wischofsky”, licensed under „CC0 1.0” (https://creativecommons.org/publicdomain/zero/1.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-17.svg b/assets/avatars/avatar-17.svg index 096d5a2..20d57c5 100644 --- a/assets/avatars/avatar-17.svg +++ b/assets/avatars/avatar-17.svg @@ -1,18 +1 @@ - - - - - - - - - - - - - - - - - - +LoreleiLisa Wischofskyhttps://www.figma.com/community/file/1198749693280469639https://creativecommons.org/publicdomain/zero/1.0/Remix of „Lorelei” (https://www.figma.com/community/file/1198749693280469639) by „Lisa Wischofsky”, licensed under „CC0 1.0” (https://creativecommons.org/publicdomain/zero/1.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-18.svg b/assets/avatars/avatar-18.svg index dc64b6d..1bacdcb 100644 --- a/assets/avatars/avatar-18.svg +++ b/assets/avatars/avatar-18.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - +LoreleiLisa Wischofskyhttps://www.figma.com/community/file/1198749693280469639https://creativecommons.org/publicdomain/zero/1.0/Remix of „Lorelei” (https://www.figma.com/community/file/1198749693280469639) by „Lisa Wischofsky”, licensed under „CC0 1.0” (https://creativecommons.org/publicdomain/zero/1.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-19.svg b/assets/avatars/avatar-19.svg index a4011a1..b57c691 100644 --- a/assets/avatars/avatar-19.svg +++ b/assets/avatars/avatar-19.svg @@ -1,19 +1 @@ - - - - - - - - - - - - - - - - - - - +AdventurerLisa Wischofskyhttps://www.figma.com/community/file/1184595184137881796https://creativecommons.org/licenses/by/4.0/Remix of „Adventurer” (https://www.figma.com/community/file/1184595184137881796) by „Lisa Wischofsky”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-20.svg b/assets/avatars/avatar-20.svg index eb8b3df..1c097b7 100644 --- a/assets/avatars/avatar-20.svg +++ b/assets/avatars/avatar-20.svg @@ -1,20 +1 @@ - - - - - - - - - - - - - - - - - - - - +AdventurerLisa Wischofskyhttps://www.figma.com/community/file/1184595184137881796https://creativecommons.org/licenses/by/4.0/Remix of „Adventurer” (https://www.figma.com/community/file/1184595184137881796) by „Lisa Wischofsky”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-21.svg b/assets/avatars/avatar-21.svg index 73d2cbd..7a74353 100644 --- a/assets/avatars/avatar-21.svg +++ b/assets/avatars/avatar-21.svg @@ -1,26 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - +AdventurerLisa Wischofskyhttps://www.figma.com/community/file/1184595184137881796https://creativecommons.org/licenses/by/4.0/Remix of „Adventurer” (https://www.figma.com/community/file/1184595184137881796) by „Lisa Wischofsky”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-22.svg b/assets/avatars/avatar-22.svg index 4760bb4..9284f71 100644 --- a/assets/avatars/avatar-22.svg +++ b/assets/avatars/avatar-22.svg @@ -1,22 +1 @@ - - - - - - - - - - - - - - - - - - - - - - +AdventurerLisa Wischofskyhttps://www.figma.com/community/file/1184595184137881796https://creativecommons.org/licenses/by/4.0/Remix of „Adventurer” (https://www.figma.com/community/file/1184595184137881796) by „Lisa Wischofsky”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-23.svg b/assets/avatars/avatar-23.svg index 7fe9aac..b96c026 100644 --- a/assets/avatars/avatar-23.svg +++ b/assets/avatars/avatar-23.svg @@ -1,19 +1 @@ - - - - - - - - - - - - - - - - - - - +AdventurerLisa Wischofskyhttps://www.figma.com/community/file/1184595184137881796https://creativecommons.org/licenses/by/4.0/Remix of „Adventurer” (https://www.figma.com/community/file/1184595184137881796) by „Lisa Wischofsky”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/assets/avatars/avatar-24.svg b/assets/avatars/avatar-24.svg index 25bd777..85c7ace 100644 --- a/assets/avatars/avatar-24.svg +++ b/assets/avatars/avatar-24.svg @@ -1,23 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - +AdventurerLisa Wischofskyhttps://www.figma.com/community/file/1184595184137881796https://creativecommons.org/licenses/by/4.0/Remix of „Adventurer” (https://www.figma.com/community/file/1184595184137881796) by „Lisa Wischofsky”, licensed under „CC BY 4.0” (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/config/database.php b/config/database.php index 77d4cd4..88ea16d 100644 --- a/config/database.php +++ b/config/database.php @@ -147,8 +147,10 @@ function ensure_tables_exist(PDO $pdo): void { "ALTER TABLE users ADD COLUMN theme VARCHAR(20) NOT NULL DEFAULT 'dark' AFTER notification_settings", "ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER theme", "ALTER TABLE users ADD COLUMN backup_codes TEXT NULL AFTER two_factor_enabled", + "ALTER TABLE users ADD COLUMN default_jira_filter_code VARCHAR(50) NULL AFTER avatar_url", "ALTER TABLE board_card_schedules ADD UNIQUE KEY uk_sched_card (card_id)", "CREATE TABLE IF NOT EXISTS `password_resets` (`id` INT AUTO_INCREMENT PRIMARY KEY, `email` VARCHAR(191) NOT NULL, `token` VARCHAR(64) NOT NULL UNIQUE, `expires_at` DATETIME NOT NULL, `used_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`email`), INDEX (`token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + "CREATE TABLE IF NOT EXISTS `jira_queue_filters` (`id` INT AUTO_INCREMENT PRIMARY KEY, `filter_code` VARCHAR(50) NOT NULL UNIQUE, `filter_name` VARCHAR(120) NOT NULL, `project_key` VARCHAR(50) NULL, `jql_clause` TEXT NOT NULL, `description` VARCHAR(255) NULL, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `sort_order` INT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", ]; foreach ($colUpgrades as $sql) { try { @@ -158,6 +160,35 @@ function ensure_tables_exist(PDO $pdo): void { } } + // Zorg dat de standaard Jira Servicedesk wachtrijfilters (DX en SD) aanwezig zijn + try { + $filterCount = (int)$pdo->query("SELECT COUNT(*) FROM jira_queue_filters")->fetchColumn(); + if ($filterCount === 0) { + $insertFilterStmt = $pdo->prepare(" + INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order) + VALUES (?, ?, ?, ?, ?, 1, ?) + "); + $insertFilterStmt->execute([ + 'DX', + 'DX — Koppelingen, SSO & Migratie', + 'SD', + '"Onderwerp logging[dropdown]" in (Koppelingen, SSO-koppeling, Rest-endpoints, Datadump, UWV-koppeling, Migratie)', + 'Servicedesk tickets met betrekking tot koppelingen, endpoints en datamigraties.', + 1 + ]); + $insertFilterStmt->execute([ + 'SD', + 'SD — Regulier Servicedesk', + 'SD', + '("Onderwerp logging[Dropdown]" = EMPTY OR "Onderwerp logging[Dropdown]" != Koppelingen AND "Onderwerp logging[Dropdown]" != Migratie AND "Onderwerp logging[Dropdown]" != SSO-koppeling AND "Onderwerp logging[Dropdown]" != UWV-koppeling AND "Onderwerp logging[Dropdown]" != Rest-endpoints AND "Onderwerp logging[Dropdown]" != Datadump)', + 'Reguliere servicedesk incidenten, algemene meldingen en gebruikersvragen.', + 2 + ]); + } + } catch (Throwable $e) { + // Stil negeren indien tabel nog in opbouw is + } + // Zorg dat er ten minste één standaard bord met standaard lanes aanwezig is try { $boardCount = (int)$pdo->query("SELECT COUNT(*) FROM boards")->fetchColumn(); diff --git a/config/schema.sql b/config/schema.sql index d49f3c2..e855a8c 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS `users` ( `remember_token_expires_at` DATETIME NULL, `notification_settings` TEXT NULL, `avatar_url` VARCHAR(255) NULL, + `default_jira_filter_code` VARCHAR(50) NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; @@ -218,3 +219,16 @@ CREATE TABLE IF NOT EXISTS `board_card_schedules` ( UNIQUE KEY `uk_sched_card` (`card_id`), CONSTRAINT `fk_bsched_card` FOREIGN KEY (`card_id`) REFERENCES `board_cards` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `jira_queue_filters` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `filter_code` VARCHAR(50) NOT NULL UNIQUE, + `filter_name` VARCHAR(120) NOT NULL, + `project_key` VARCHAR(50) NULL, + `jql_clause` TEXT NOT NULL, + `description` VARCHAR(255) NULL, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `sort_order` INT NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/includes/integrations/jira.php b/includes/integrations/jira.php index 46dedf1..c9caade 100644 --- a/includes/integrations/jira.php +++ b/includes/integrations/jira.php @@ -142,6 +142,76 @@ function jira_test_connection(string $domain, string $email, string $apiToken): /** * Haalt alle actieve issues op die zijn toegewezen aan de ingelogde Jira gebruiker */ +/** + * Converteert Atlassian Document Format (ADF) of tekst naar platte leesbare tekst + */ +function jira_adf_to_text($adf): string { + if (empty($adf)) { + return ''; + } + if (is_string($adf)) { + return trim($adf); + } + if (!is_array($adf)) { + return ''; + } + + $text = ''; + $type = $adf['type'] ?? ''; + + if ($type === 'text') { + return $adf['text'] ?? ''; + } + + if (!empty($adf['content']) && is_array($adf['content'])) { + foreach ($adf['content'] as $child) { + $childText = jira_adf_to_text($child); + $childType = $child['type'] ?? ''; + + if ($childType === 'paragraph' || $childType === 'heading') { + $text .= $childText . "\n\n"; + } elseif ($childType === 'listItem') { + $text .= "• " . trim($childText) . "\n"; + } elseif ($childType === 'codeBlock') { + $text .= "\n" . trim($childText) . "\n\n"; + } else { + $text .= $childText; + } + } + } + + return trim($text); +} + +/** + * Haalt alle geconfigureerde Jira wachtrijfilters op uit de database + */ +function jira_get_queue_filters(bool $onlyActive = true): array { + try { + $db = get_db_connection(); + $sql = "SELECT * FROM jira_queue_filters " . ($onlyActive ? "WHERE is_active = 1 " : "") . "ORDER BY sort_order ASC, id ASC"; + return $db->query($sql)->fetchAll() ?: []; + } catch (Throwable $e) { + error_log("jira_get_queue_filters error: " . $e->getMessage()); + return []; + } +} + +/** + * Haalt de standaard wachtrijfilter voorkeur van een gebruiker op + */ +function jira_get_user_queue_preference(int $userId): ?string { + try { + $db = get_db_connection(); + $stmt = $db->prepare("SELECT default_jira_filter_code FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $res = $stmt->fetchColumn(); + return $res ? (string)$res : null; + } catch (Throwable $e) { + return null; + } +} + /** * Haalt issues op via een flexibele JQL query via Atlassian REST API */ @@ -159,7 +229,9 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri 'assignee', 'reporter', 'duedate', - 'updated' + 'updated', + 'created', + 'description', ]; // 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST @@ -242,26 +314,60 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri $projectKey = $fields['project']['key'] ?? ''; $dueDate = $fields['duedate'] ?? null; $updated = $fields['updated'] ?? null; + $created = $fields['created'] ?? null; $assigneeName = $fields['assignee']['displayName'] ?? 'Niet toegewezen'; $assigneeAvatar = $fields['assignee']['avatarUrls']['48x48'] ?? ($fields['assignee']['avatarUrls']['32x32'] ?? ''); + $reporterName = $fields['reporter']['displayName'] ?? 'Onbekend'; + $reporterAvatar = $fields['reporter']['avatarUrls']['48x48'] ?? ($fields['reporter']['avatarUrls']['32x32'] ?? ''); + + $descriptionText = jira_adf_to_text($fields['description'] ?? ''); + + // Detectie van custom field "Onderwerp logging" + $onderwerpLogging = ''; + foreach ($fields as $fKey => $fVal) { + if (str_starts_with($fKey, 'customfield_')) { + if (is_array($fVal) && isset($fVal['value']) && is_string($fVal['value'])) { + $valStr = trim($fVal['value']); + $known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie', 'incident', 'vraag', 'algemeen']; + if (in_array(strtolower($valStr), $known)) { + $onderwerpLogging = $valStr; + } + } elseif (is_string($fVal) && !empty($fVal)) { + $valStr = trim($fVal); + $known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; + if (in_array(strtolower($valStr), $known)) { + $onderwerpLogging = $valStr; + } + } + } + } + if (empty($onderwerpLogging) && !empty($fields['onderwerp_logging'])) { + $onderwerpLogging = is_string($fields['onderwerp_logging']) ? $fields['onderwerp_logging'] : ($fields['onderwerp_logging']['value'] ?? ''); + } + $issues[] = [ - 'key' => $key, - 'summary' => $summary, - 'status' => $statusName, - 'status_category' => $statusCategory, - 'priority' => $priorityName, - 'issue_type' => $issueTypeName, - 'issue_type_icon' => $issueTypeIcon, - 'project' => $projectName, - 'project_key' => $projectKey, - 'assignee_name' => $assigneeName, - 'assignee_avatar' => $assigneeAvatar, - 'due_date' => $dueDate, - 'updated' => $updated, - 'url' => "{$cleanDomain}/browse/{$key}", - 'source' => 'JIRA_LIVE', + 'key' => $key, + 'summary' => $summary, + 'description' => $descriptionText, + 'status' => $statusName, + 'status_category' => $statusCategory, + 'priority' => $priorityName, + 'issue_type' => $issueTypeName, + 'issue_type_icon' => $issueTypeIcon, + 'project' => $projectName, + 'project_key' => $projectKey, + 'assignee_name' => $assigneeName, + 'assignee_avatar' => $assigneeAvatar, + 'reporter_name' => $reporterName, + 'reporter_avatar' => $reporterAvatar, + 'onderwerp_logging' => $onderwerpLogging, + 'due_date' => $dueDate, + 'created' => $created, + 'updated' => $updated, + 'url' => "{$cleanDomain}/browse/{$key}", + 'source' => 'JIRA_LIVE', ]; } @@ -297,247 +403,407 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array $stmt->execute([$userId]); $integration = $stmt->fetch(); + // Ophalen van actieve wachtrijfilters en voorkeur van gebruiker + $queueFilters = jira_get_queue_filters(true); + $userQueuePref = jira_get_user_queue_preference($userId); + // Sandbox / Demo dataset indien niet gekoppeld of expliciet in demomodus if (!$integration || !empty($integration['is_demo_mode'])) { $assigned = [ [ - 'key' => 'FOC-101', - 'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren', - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'High', - 'issue_type' => 'Story', - 'project' => 'Fociz Platform', - 'project_key' => 'FOC', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+2 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-101', - 'source' => 'SANDBOX', + 'key' => 'SD-401', + 'summary' => 'AFAS Profit SSO-koppeling geeft 401 fout bij token refresh', + 'description' => "Gebruikers die via AFAS SSO inloggen krijgen periodiek een HTTP 401 foutcode bij de geautomatiseerde token verversing.\n\nHet endpoint `/oauth/token` reageert met `invalid_grant` zodra de refresh token ouder is dan 24 uur. Er dient een retry-mechanisme met fallback authenticatie ingebouwd te worden.", + 'status' => 'In Progress', + 'status_category' => 'indeterminate', + 'priority' => 'Highest', + 'issue_type' => 'Bug', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'Kees van de Berg (AFAS Beheer)', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'SSO-koppeling', + 'due_date' => date('Y-m-d', strtotime('+1 day')), + 'created' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'updated' => date('Y-m-d H:i:s', strtotime('-45 minutes')), + 'url' => 'https://jira.atlassian.net/browse/SD-401', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-102', - 'summary' => 'Interactief whiteboard canvas voor brainstormsessies', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'High', + 'key' => 'SD-405', + 'summary' => 'REST-endpoints voor medewerker synchronisatie beveiligen met rate limiting', + 'description' => "De openbare REST-endpoints voor medewerkerssynchronisatie dienen conform security richtlijnen te worden voorzien van rate limiting (max 120 calls per minuut per IP).\n\nBij overschrijding moet een duidelijke HTTP 429 Retry-After header worden geretourneerd.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'High', + 'issue_type' => 'Task', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'Security Team', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Rest-endpoints', + 'due_date' => date('Y-m-d', strtotime('+3 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')), + 'url' => 'https://jira.atlassian.net/browse/SD-405', + 'source' => 'SANDBOX', + ], + [ + 'key' => 'SD-380', + 'summary' => 'Wachtwoord reset e-mail komt niet aan bij nieuwe medewerker', + 'description' => "Een nieuwe medewerker ontvangt geen welkomstmail en wachtwoord reset via het selfservice portaal.\n\nControleer de SMTP-verbinding van de mailserver mail.fociz.nl en controleer of het mailadres in de spamfilter is beland.", + 'status' => 'In Progress', + 'status_category' => 'indeterminate', + 'priority' => 'High', + 'issue_type' => 'Task', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'HR Servicedesk', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'EMPTY', + 'due_date' => date('Y-m-d'), + 'created' => date('Y-m-d H:i:s', strtotime('-3 hours')), + 'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')), + 'url' => 'https://jira.atlassian.net/browse/SD-380', + 'source' => 'SANDBOX', + ], + [ + 'key' => 'SD-385', + 'summary' => 'Rechten aanpassen voor rol Projectleider in urenregistratie', + 'description' => "Projectleiders hebben momenteel alleen leesrechten op ingediende urenstaten. Graag de rol uitbreiden zodat zij declaraties direct kunnen goedkeuren of afkeuren.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'Medium', + 'issue_type' => 'Task', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'Marianne Willems', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Algemeen', + 'due_date' => date('Y-m-d', strtotime('+5 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'url' => 'https://jira.atlassian.net/browse/SD-385', + 'source' => 'SANDBOX', + ], + [ + 'key' => 'FOC-101', + 'summary' => 'Tweestapsverificatie (2FA TOTP) flow implementeren', + 'description' => "Ontwikkeling van een complete Google Authenticator / Microsoft Authenticator TOTP verificatie flow inclusief 8 cijferige noodcodes en QR-code weergave in het gebruikersprofiel.", + 'status' => 'In Progress', + 'status_category' => 'indeterminate', + 'priority' => 'High', + 'issue_type' => 'Story', + 'project' => 'Fociz Platform', + 'project_key' => 'FOC', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'Product Owner', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Beveiliging', + 'due_date' => date('Y-m-d', strtotime('+2 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-4 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')), + 'url' => 'https://jira.atlassian.net/browse/FOC-101', + 'source' => 'SANDBOX', + ], + [ + 'key' => 'FOC-102', + 'summary' => 'Interactief whiteboard canvas voor brainstormsessies', + 'description' => "Gebruikers moeten live post-its kunnen slepen, kleuren en stemmen tijdens kwartaal brainstormsessies.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'High', 'issue_type' => 'Feature', 'project' => 'Fociz Platform', 'project_key' => 'FOC', 'assignee_name' => 'Mijzelf', 'assignee_avatar' => '', + 'reporter_name' => 'Scrum Master', + 'reporter_avatar' => '', + 'onderwerp_logging' => '', 'due_date' => date('Y-m-d', strtotime('+4 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-5 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), 'url' => 'https://jira.atlassian.net/browse/FOC-102', 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-103', - 'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API', - 'status' => 'In Review', - 'status_category' => 'indeterminate', - 'priority' => 'Medium', + 'key' => 'FOC-103', + 'summary' => 'Koppeling met Microsoft Outlook Calendar & Teams Graph API', + 'description' => "Synchronisatie van dagplanning blokken naar Microsoft 365 Exchange Online agenda met real-time conflictcontrole.", + 'status' => 'In Review', + 'status_category' => 'indeterminate', + 'priority' => 'Medium', 'issue_type' => 'Task', 'project' => 'Integrations Hub', 'project_key' => 'INT', 'assignee_name' => 'Mijzelf', 'assignee_avatar' => '', + 'reporter_name' => 'Architect', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'SSO-koppeling', 'due_date' => date('Y-m-d'), + 'created' => date('Y-m-d H:i:s', strtotime('-6 days')), 'updated' => date('Y-m-d H:i:s', strtotime('-4 hours')), 'url' => 'https://jira.atlassian.net/browse/FOC-103', 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-104', - 'summary' => 'Roadmap kwartaalweergave en voortgangsindicatoren', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'Medium', - 'issue_type' => 'Story', - 'project' => 'Roadmap Module', - 'project_key' => 'ROAD', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+7 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-3 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-104', - 'source' => 'SANDBOX', + 'key' => 'FOC-108', + 'summary' => 'CSS scrollbar glitch oplossen in Kanban lane op Firefox', + 'description' => "Op Windows Firefox versies 128+ verspringt de horizontale scrollbalk bij het slepen van kaarten tussen kolommen.", + 'status' => 'In Progress', + 'status_category' => 'indeterminate', + 'priority' => 'Highest', + 'issue_type' => 'Bug', + 'project' => 'UI/UX Redesign', + 'project_key' => 'UI', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'QA Lead', + 'reporter_avatar' => '', + 'onderwerp_logging' => '', + 'due_date' => date('Y-m-d', strtotime('+1 day')), + 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-30 minutes')), + 'url' => 'https://jira.atlassian.net/browse/FOC-108', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-108', - 'summary' => 'CSS scrollbar glitch oplossen in Kanban lane op Firefox', - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'Highest', - 'issue_type' => 'Bug', - 'project' => 'UI/UX Redesign', - 'project_key' => 'UI', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+1 day')), - 'updated' => date('Y-m-d H:i:s', strtotime('-30 minutes')), - 'url' => 'https://jira.atlassian.net/browse/FOC-108', - 'source' => 'SANDBOX', - ], - [ - 'key' => 'FOC-112', - 'summary' => 'Avatar upload beveiliging en compressie optimalisatie', - 'status' => 'Done', - 'status_category' => 'done', - 'priority' => 'Medium', - 'issue_type' => 'Task', - 'project' => 'Security', - 'project_key' => 'SEC', - 'assignee_name' => 'Mijzelf', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('-1 day')), - 'updated' => date('Y-m-d H:i:s', strtotime('-5 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-112', - 'source' => 'SANDBOX', + 'key' => 'FOC-112', + 'summary' => 'Avatar upload beveiliging en compressie optimalisatie', + 'description' => "Server-side image re-encoding en EXIF stripping voor geüploade gebruikersavatars.", + 'status' => 'Done', + 'status_category' => 'done', + 'priority' => 'Medium', + 'issue_type' => 'Task', + 'project' => 'Security', + 'project_key' => 'SEC', + 'assignee_name' => 'Mijzelf', + 'assignee_avatar' => '', + 'reporter_name' => 'SecOps', + 'reporter_avatar' => '', + 'onderwerp_logging' => '', + 'due_date' => date('Y-m-d', strtotime('-1 day')), + 'created' => date('Y-m-d H:i:s', strtotime('-7 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-5 hours')), + 'url' => 'https://jira.atlassian.net/browse/FOC-112', + 'source' => 'SANDBOX', ], ]; $team = [ [ - 'key' => 'FOC-95', - 'summary' => 'Sessie timeout notificatie verschijnt niet tijdig bij inactieve tab', - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'Highest', - 'issue_type' => 'Bug', - 'project' => 'Fociz Platform', - 'project_key' => 'FOC', - 'assignee_name' => 'Lisa Bakker', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+1 day')), - 'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')), - 'url' => 'https://jira.atlassian.net/browse/FOC-95', - 'source' => 'SANDBOX', + 'key' => 'SD-408', + 'summary' => 'UWV-koppeling polisadministratie import loopt vast op datumformaat', + 'description' => "De XML batch import vanuit het UWV portaal faalt op records met afwijkende geboortedatumnotatie.\n\nDe parser dient flexibel zowel ISO-8601 (YYYY-MM-DD) als traditioneel formaat (DD-MM-YYYY) te accepteren.", + 'status' => 'In Progress', + 'status_category' => 'indeterminate', + 'priority' => 'High', + 'issue_type' => 'Bug', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Lisa Bakker', + 'assignee_avatar' => '', + 'reporter_name' => 'Servicedesk L2', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'UWV-koppeling', + 'due_date' => date('Y-m-d', strtotime('+2 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')), + 'url' => 'https://jira.atlassian.net/browse/SD-408', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-98', - 'summary' => 'Automatische e-mail triggers voor taakdeadlines en uitnodigingen', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'High', - 'issue_type' => 'Story', - 'project' => 'Notifications', - 'project_key' => 'NOTIF', - 'assignee_name' => 'Daan van Dijk', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+3 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-2 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-98', - 'source' => 'SANDBOX', + 'key' => 'SD-412', + 'summary' => 'Datadump archief export voor externe accountant audit 2025', + 'description' => "Genereer een beveiligde, versleutelde datadump van alle audit-logs en urenstaten voor de periodieke controle.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'Medium', + 'issue_type' => 'Story', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Daan van Dijk', + 'assignee_avatar' => '', + 'reporter_name' => 'Compliance Officer', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Datadump', + 'due_date' => date('Y-m-d', strtotime('+6 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-4 hours')), + 'url' => 'https://jira.atlassian.net/browse/SD-412', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-105', - 'summary' => 'Database indexering optimaliseren voor grote audit logs', - 'status' => 'In Review', - 'status_category' => 'indeterminate', - 'priority' => 'Medium', - 'issue_type' => 'Task', - 'project' => 'Database & Core', - 'project_key' => 'CORE', - 'assignee_name' => 'Sophie de Vries', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+5 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), - 'url' => 'https://jira.atlassian.net/browse/FOC-105', - 'source' => 'SANDBOX', + 'key' => 'SD-416', + 'summary' => 'Migratie van oude klantportaal database naar MariaDB 11 cluster', + 'description' => "Voorbereiden en uitvoeren van de dataschemamigratie van het oude portaal naar de nieuwe MariaDB 11 clusteromgeving inclusief regressietests.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'High', + 'issue_type' => 'Story', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Bram Meijer', + 'assignee_avatar' => '', + 'reporter_name' => 'Lead Engineer', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Migratie', + 'due_date' => date('Y-m-d', strtotime('+8 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-4 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')), + 'url' => 'https://jira.atlassian.net/browse/SD-416', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-107', - 'summary' => 'Exporteer wekelijkse team rapportage naar PDF en Excel spreadsheet', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'Low', - 'issue_type' => 'Feature', - 'project' => 'Reporting', - 'project_key' => 'REP', - 'assignee_name' => 'Thomas Jansen', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+10 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-4 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-107', - 'source' => 'SANDBOX', + 'key' => 'SD-420', + 'summary' => 'Koppelingen API webhook notificaties afleveren met retry-mechanisme', + 'description' => "Webhooks die naar externe partnersystemen worden verzonden moeten bij een tijdelijke 5xx fout automatisch 3x opnieuw worden aangeboden (exponential backoff).", + 'status' => 'In Review', + 'status_category' => 'indeterminate', + 'priority' => 'Medium', + 'issue_type' => 'Task', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Sophie de Vries', + 'assignee_avatar' => '', + 'reporter_name' => 'Integratie Specialist', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Koppelingen', + 'due_date' => date('Y-m-d', strtotime('+4 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-5 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-3 hours')), + 'url' => 'https://jira.atlassian.net/browse/SD-420', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-110', - 'summary' => 'Real-time websocket synchronisatie voor gelijktijdig bewerken van borden', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'High', - 'issue_type' => 'Story', - 'project' => 'Realtime Engine', - 'project_key' => 'RT', - 'assignee_name' => 'Bram Meijer', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+6 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-110', - 'source' => 'SANDBOX', + 'key' => 'SD-392', + 'summary' => 'Outlook Agenda synchronisatie toont dubbele afspraken op mobiel', + 'description' => "Bij enkele gebruikers met de iOS Outlook app worden terugkerende agenda-afspraken tweemaal weergegeven na het synchroniseren.", + 'status' => 'In Progress', + 'status_category' => 'indeterminate', + 'priority' => 'Medium', + 'issue_type' => 'Bug', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Lisa Bakker', + 'assignee_avatar' => '', + 'reporter_name' => 'Servicedesk Support', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Incident', + 'due_date' => date('Y-m-d', strtotime('+3 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-2 hours')), + 'url' => 'https://jira.atlassian.net/browse/SD-392', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-114', - 'summary' => 'Datum picker springt terug naar 1970 bij lege invoer', - 'status' => 'To Do', - 'status_category' => 'new', - 'priority' => 'Medium', - 'issue_type' => 'Bug', - 'project' => 'UI/UX Redesign', - 'project_key' => 'UI', - 'assignee_name' => 'Niet toegewezen', - 'assignee_avatar' => '', - 'due_date' => null, - 'updated' => date('Y-m-d H:i:s', strtotime('-2 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-114', - 'source' => 'SANDBOX', + 'key' => 'SD-399', + 'summary' => 'Vraag over exportmogelijkheden naar Excel rapportage', + 'description' => "Klant informeert of het mogelijk is om een wekelijkse CSV / Excel export van de urenstaten automatisch per mail te ontvangen.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'Low', + 'issue_type' => 'Task', + 'project' => 'Servicedesk', + 'project_key' => 'SD', + 'assignee_name' => 'Thomas Jansen', + 'assignee_avatar' => '', + 'reporter_name' => 'Financieel Directeur', + 'reporter_avatar' => '', + 'onderwerp_logging' => 'Vraag', + 'due_date' => date('Y-m-d', strtotime('+10 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-5 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'url' => 'https://jira.atlassian.net/browse/SD-399', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-89', - 'summary' => 'GDPR / AVG compliancy en geautomatiseerd data retentiebeleid', - 'status' => 'In Progress', - 'status_category' => 'indeterminate', - 'priority' => 'Medium', - 'issue_type' => 'Epic', - 'project' => 'Security', - 'project_key' => 'SEC', - 'assignee_name' => 'Lisa Bakker', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('+14 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-3 hours')), - 'url' => 'https://jira.atlassian.net/browse/FOC-89', - 'source' => 'SANDBOX', + 'key' => 'FOC-95', + 'summary' => 'Sessie timeout notificatie verschijnt niet tijdig bij inactieve tab', + 'description' => "Wanneer een gebruiker de browser 2 uur niet aanraakt, vervalt de PHP sessie zonder dat het login-dialoogvenster netjes openspringt.", + 'status' => 'In Progress', + 'status_category' => 'indeterminate', + 'priority' => 'Highest', + 'issue_type' => 'Bug', + 'project' => 'Fociz Platform', + 'project_key' => 'FOC', + 'assignee_name' => 'Lisa Bakker', + 'assignee_avatar' => '', + 'reporter_name' => 'Product QA', + 'reporter_avatar' => '', + 'onderwerp_logging' => '', + 'due_date' => date('Y-m-d', strtotime('+1 day')), + 'created' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-1 hour')), + 'url' => 'https://jira.atlassian.net/browse/FOC-95', + 'source' => 'SANDBOX', ], [ - 'key' => 'FOC-82', - 'summary' => 'Performance optimalisatie van vector avatar rendering in navigatiebalk', - 'status' => 'Done', - 'status_category' => 'done', - 'priority' => 'Low', - 'issue_type' => 'Task', - 'project' => 'UI/UX Redesign', - 'project_key' => 'UI', - 'assignee_name' => 'Sophie de Vries', - 'assignee_avatar' => '', - 'due_date' => date('Y-m-d', strtotime('-3 days')), - 'updated' => date('Y-m-d H:i:s', strtotime('-2 days')), - 'url' => 'https://jira.atlassian.net/browse/FOC-82', - 'source' => 'SANDBOX', + 'key' => 'FOC-98', + 'summary' => 'Automatische e-mail triggers voor taakdeadlines en uitnodigingen', + 'description' => "Cronjob instellen die 24 uur voor de deadline van een taak een herinneringsmail stuurt via SMTP.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'High', + 'issue_type' => 'Story', + 'project' => 'Notifications', + 'project_key' => 'NOTIF', + 'assignee_name' => 'Daan van Dijk', + 'assignee_avatar' => '', + 'reporter_name' => 'Operations', + 'reporter_avatar' => '', + 'onderwerp_logging' => '', + 'due_date' => date('Y-m-d', strtotime('+3 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'url' => 'https://jira.atlassian.net/browse/FOC-98', + 'source' => 'SANDBOX', + ], + [ + 'key' => 'FOC-110', + 'summary' => 'Real-time websocket synchronisatie voor gelijktijdig bewerken van borden', + 'description' => "Voorkom overschrijfconflicten wanneer meerdere collega's gelijktijdig kaarten verplaatsen op hetzelfde Kanban bord.", + 'status' => 'To Do', + 'status_category' => 'new', + 'priority' => 'High', + 'issue_type' => 'Story', + 'project' => 'Realtime Engine', + 'project_key' => 'RT', + 'assignee_name' => 'Bram Meijer', + 'assignee_avatar' => '', + 'reporter_name' => 'Architect', + 'reporter_avatar' => '', + 'onderwerp_logging' => '', + 'due_date' => date('Y-m-d', strtotime('+6 days')), + 'created' => date('Y-m-d H:i:s', strtotime('-4 days')), + 'updated' => date('Y-m-d H:i:s', strtotime('-6 hours')), + 'url' => 'https://jira.atlassian.net/browse/FOC-110', + 'source' => 'SANDBOX', ], ]; return [ - 'is_demo' => true, - 'success' => true, - 'domain' => 'sandbox.atlassian.net', - 'email' => 'demo@fociz.nl', - 'account_name' => 'Demo Account', - 'last_sync_at' => date('Y-m-d H:i:s'), - 'assigned_issues' => $assigned, - 'team_issues' => $team, + 'is_demo' => true, + 'success' => true, + 'domain' => 'sandbox.atlassian.net', + 'email' => 'demo@fociz.nl', + 'account_name' => 'Demo Account', + 'last_sync_at' => date('Y-m-d H:i:s'), + 'assigned_issues' => $assigned, + 'team_issues' => $team, + 'queue_filters' => $queueFilters, + 'default_queue_filter' => $userQueuePref, ]; } @@ -587,14 +853,16 @@ function jira_get_dashboard_data(int $userId, bool $forceRefresh = false): array $teamIssues = $teamRes['success'] ? $teamRes['issues'] : []; $data = [ - 'is_demo' => false, - 'success' => true, - 'domain' => $domain, - 'email' => $email, - 'account_name' => $accName, - 'last_sync_at' => date('Y-m-d H:i:s'), - 'assigned_issues' => $assignedIssues, - 'team_issues' => $teamIssues, + 'is_demo' => false, + 'success' => true, + 'domain' => $domain, + 'email' => $email, + 'account_name' => $accName, + 'last_sync_at' => date('Y-m-d H:i:s'), + 'assigned_issues' => $assignedIssues, + 'team_issues' => $teamIssues, + 'queue_filters' => $queueFilters, + 'default_queue_filter' => $userQueuePref, ]; $_SESSION[$cacheKey] = [ diff --git a/jira.php b/jira.php index f4d1648..d63d25e 100644 --- a/jira.php +++ b/jira.php @@ -56,16 +56,22 @@ foreach ($boards as &$b) { } unset($b); -// Verzamel unieke projecten en types voor filter dropdowns +// Verzamel unieke projecten en types voor filter dropdowns en project switcher $allIssues = array_merge($assignedIssues, $teamIssues); $allProjects = []; $allTypes = []; $allPriorities = ['Highest', 'High', 'Medium', 'Low']; $allStatuses = []; +$projectCounts = []; foreach ($allIssues as $iss) { if (!empty($iss['project'])) { - $allProjects[$iss['project']] = $iss['project']; + $p = $iss['project']; + $allProjects[$p] = [ + 'name' => $p, + 'key' => $iss['project_key'] ?? '', + ]; + $projectCounts[$p] = ($projectCounts[$p] ?? 0) + 1; } if (!empty($iss['issue_type'])) { $allTypes[$iss['issue_type']] = $iss['issue_type']; @@ -78,6 +84,9 @@ ksort($allProjects); ksort($allTypes); ksort($allStatuses); +$queueFilters = $jiraData['queue_filters'] ?? jira_get_queue_filters(true); +$defaultQueueFilter = $jiraData['default_queue_filter'] ?? jira_get_user_queue_preference($userId); + $jiraDataJson = json_encode($jiraData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?> @@ -229,6 +238,97 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
+ +
+
+
+
+ +
+
+

Jira Projecten

+

Klik op een project om direct te filteren

+
+
+ +
+ +
+ + + + + $pData): ?> + + +
+
+ + +
+
+
+
+ +
+
+
+ Servicedesk Wachtrijfilters + + + Jouw voorkeur: + + +
+

+ Filtert op onderwerp logging routering (DX voor koppelingen & endpoints, SD voor reguliere support) +

+
+
+ + +
+ + + + + + + + + + + +
+
+
+
@@ -250,10 +350,10 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
- - - + $pData): ?> +
@@ -573,7 +673,10 @@ const jiraData = ; const availableBoards = ; let currentTab = 'assigned'; // 'assigned', 'team', 'in_progress', 'done' +let currentProject = ''; +let currentQueue = jiraData.default_queue_filter || ''; let activeUrgentOnly = false; +let expandedKeys = new Set(); // Bereken statussen en KPI statistieken function calculateKpis() { @@ -629,9 +732,97 @@ function switchTab(tab) { } }); + updateQueueCounts(); applyFilters(); } +// Project selectie via pill tabs +function selectProject(proj) { + currentProject = proj; + const pSelect = document.getElementById('filterProject'); + if (pSelect) { + pSelect.value = proj; + } + updateProjectPills(); + updateQueueCounts(); + applyFilters(); +} + +// Project selectie via dropdown +function onProjectSelectChange(proj) { + currentProject = proj; + updateProjectPills(); + updateQueueCounts(); + applyFilters(); +} + +function updateProjectPills() { + const pills = document.querySelectorAll('.project-pill'); + pills.forEach(p => { + const pKey = p.getAttribute('data-project') || ''; + if (pKey === currentProject) { + p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0"; + } else { + p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0"; + } + }); +} + +// Wachtrijfilter selectie +function selectQueueFilter(code) { + currentQueue = code; + updateQueuePills(); + applyFilters(); +} + +function updateQueuePills() { + const pills = document.querySelectorAll('.queue-pill'); + pills.forEach(p => { + const qCode = p.getAttribute('data-queue') || ''; + if (qCode === currentQueue) { + p.className = "queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0"; + } else { + p.className = "queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0"; + } + }); +} + +// Update de tellers op de DX / SD wachtrijfilter knoppen +function updateQueueCounts() { + const assigned = jiraData.assigned_issues || []; + const team = jiraData.team_issues || []; + const all = [...assigned, ...team]; + + let currentList = all; + if (currentTab === 'assigned') currentList = assigned; + else if (currentTab === 'team') currentList = team; + + if (currentProject) { + currentList = currentList.filter(iss => iss.project === currentProject); + } + + const dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; + let dxCount = 0; + let sdCount = 0; + + currentList.forEach(iss => { + const ol = (iss.onderwerp_logging || '').toLowerCase().trim(); + if (dxValues.includes(ol)) { + dxCount++; + } else { + sdCount++; + } + }); + + const elAll = document.getElementById('queueCount_all'); + const elDx = document.getElementById('queueCount_DX'); + const elSd = document.getElementById('queueCount_SD'); + + if (elAll) elAll.innerText = currentList.length; + if (elDx) elDx.innerText = dxCount; + if (elSd) elSd.innerText = sdCount; +} + // Snelkoppeling KPI-kaart Urgent function filterByUrgent() { const pSelect = document.getElementById('filterPriority'); @@ -653,6 +844,8 @@ function clearSearch() { // Alle filters herstellen function resetAllFilters() { + currentProject = ''; + currentQueue = ''; const searchInput = document.getElementById('jiraSearchInput'); const projectSelect = document.getElementById('filterProject'); const typeSelect = document.getElementById('filterType'); @@ -665,14 +858,17 @@ function resetAllFilters() { if (prioritySelect) prioritySelect.value = ''; if (statusSelect) statusSelect.value = ''; - document.getElementById('clearSearchBtn').classList.add('hidden'); + document.getElementById('clearSearchBtn')?.classList.add('hidden'); + updateProjectPills(); + updateQueuePills(); + updateQueueCounts(); applyFilters(); } // Filter en render logic function applyFilters() { const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase(); - const projectVal = document.getElementById('filterProject')?.value || ''; + const projectVal = currentProject || (document.getElementById('filterProject')?.value || ''); const typeVal = document.getElementById('filterType')?.value || ''; const priorityVal = document.getElementById('filterPriority')?.value || ''; const statusVal = document.getElementById('filterStatus')?.value || ''; @@ -706,19 +902,42 @@ function applyFilters() { }); } - // Pas zoek- en selectiefilters toe + const dxValues = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; + + // Pas zoek-, project-, wachtrij-, type-, prioriteit- en statusfilters toe const filtered = sourceList.filter(iss => { if (searchVal) { const keyMatch = (iss.key || '').toLowerCase().includes(searchVal); const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal); const prjMatch = (iss.project || '').toLowerCase().includes(searchVal); - if (!keyMatch && !sumMatch && !prjMatch) return false; + const descMatch = (iss.description || '').toLowerCase().includes(searchVal); + const repMatch = (iss.reporter_name || '').toLowerCase().includes(searchVal); + const olMatch = (iss.onderwerp_logging || '').toLowerCase().includes(searchVal); + if (!keyMatch && !sumMatch && !prjMatch && !descMatch && !repMatch && !olMatch) return false; } if (projectVal && iss.project !== projectVal) { return false; } + if (currentQueue) { + const qCode = currentQueue.toUpperCase(); + const ol = (iss.onderwerp_logging || '').toLowerCase().trim(); + + if (qCode === 'DX') { + if (!dxValues.includes(ol)) return false; + } else if (qCode === 'SD') { + if (ol !== '' && ol !== 'empty' && dxValues.includes(ol)) return false; + } else { + const fObj = (jiraData.queue_filters || []).find(f => f.filter_code.toUpperCase() === qCode); + if (fObj) { + if (ol && !ol.includes(qCode.toLowerCase()) && !fObj.jql_clause.toLowerCase().includes(ol)) { + return false; + } + } + } + } + if (typeVal && iss.issue_type !== typeVal) { return false; } @@ -741,6 +960,36 @@ function applyFilters() { renderTickets(filtered); } +// Uitklappen en inklappen van ticket details +function toggleTicketDetails(key, event) { + if (event) { + event.stopPropagation(); + } + const details = document.getElementById(`details_${key}`); + const chevron = document.getElementById(`chevron_${key}`); + if (!details) return; + + if (details.classList.contains('hidden')) { + details.classList.remove('hidden'); + if (chevron) chevron.classList.add('rotate-180'); + expandedKeys.add(key); + } else { + details.classList.add('hidden'); + if (chevron) chevron.classList.remove('rotate-180'); + expandedKeys.delete(key); + } + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + +function copyTicketKey(key) { + navigator.clipboard.writeText(key).then(() => { + showToast({ + message: `Ticket ${key} gekopieerd!`, + subMessage: 'De ticketsleutel is direct naar je klembord gekopieerd.' + }); + }); +} + // Renders the list of tickets function renderTickets(tickets) { const container = document.getElementById('ticketsContainer'); @@ -842,11 +1091,38 @@ function renderTickets(tickets) { `; } + // Onderwerp logging badge + let onderwerpBadge = 'Niet ingevuld'; + if (ticket.onderwerp_logging) { + const olLower = ticket.onderwerp_logging.toLowerCase(); + const dxList = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie']; + const isDx = dxList.includes(olLower); + if (isDx) { + onderwerpBadge = ` + + + DX: ${escapeHtml(ticket.onderwerp_logging)} + + `; + } else { + onderwerpBadge = ` + + + SD: ${escapeHtml(ticket.onderwerp_logging)} + + `; + } + } + + const isExpanded = expandedKeys.has(ticket.key); + const detailsClass = isExpanded ? '' : 'hidden'; + const chevronRotateClass = isExpanded ? 'rotate-180' : ''; + // JSON stringified ticket data voor modalaanroep const ticketSafeJson = JSON.stringify(ticket).replace(/"/g, '"'); html += ` -
+
@@ -888,23 +1164,29 @@ function renderTickets(tickets) { ${dueDateHtml}
- -

- + +

+ ${escapeHtml(ticket.summary || '(Geen titel)')} - +

- +
${assigneeHtml} ${ticket.updated ? `Gewijzigd: ${escapeHtml(ticket.updated)}` : ''} + ${ticket.onderwerp_logging ? onderwerpBadge : ''}

- +
+ +
+ + +
+ + +
+
+ + Volledige Ticket Omschrijving +
+
+ ${escapeHtml(ticket.description || 'Geen uitgebreide omschrijving beschikbaar in Jira.')} +
+
+ + +
+
+ Aangemeld door (Reporter): +
+
+ ${escapeHtml((ticket.reporter_name || 'J').charAt(0).toUpperCase())} +
+ ${escapeHtml(ticket.reporter_name || 'Onbekend')} +
+
+ +
+ Aangemaakt op: + + ${ticket.created ? escapeHtml(ticket.created) : 'Niet opgegeven'} + +
+ +
+ Laatst gewijzigd: + + ${ticket.updated ? escapeHtml(ticket.updated) : 'Recent'} + +
+ +
+ Onderwerp logging: + ${onderwerpBadge} +
+
+ + +
+
+ + + + Open in Jira Cloud + +
+
+ + +
+
+ +
+
`; }); @@ -1191,6 +1546,11 @@ document.addEventListener('keydown', function(e) { // Initiële opstart document.addEventListener('DOMContentLoaded', () => { calculateKpis(); + updateProjectPills(); + updateQueueCounts(); + if (currentQueue) { + updateQueuePills(); + } applyFilters(); }); diff --git a/mail.php b/mail.php new file mode 100644 index 0000000..ddaa330 --- /dev/null +++ b/mail.php @@ -0,0 +1,726 @@ +prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1"); +$stmtInt->execute([$userId]); +$outlookInt = $stmtInt->fetch(); +$isLiveOutlook = !empty($outlookInt['access_token']) && empty($outlookInt['is_demo_mode']); + +$jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); +?> + +
+ + +
+
+
+
+ + Microsoft Outlook 365 • Inbox + + LIVE VERBONDEN + + DEMO INBOX + +
+

+ E-mail & Communicatie +

+

+ Bekijk binnenkomende e-mails, beantwoord direct en plan belangrijke berichten in één klik in op jouw tijdlijn. +

+
+ + + +
+ + +
+
+ + Je bekijkt momenteel een interactieve demonstratieset. Koppel je zakelijke Microsoft 365 account via Profiel om al je live zakelijke e-mails in te laden. +
+ Nu koppelen → +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+
+ + +
+ + +
+
+ Inbox Berichten () + Sorteer: Nieuwste eerst +
+ +
+ +
+
+ + +
+ +
+
+ +
+

Geen e-mail geselecteerd

+

+ Selecteer een bericht aan de linkerkant om de volledige inhoud te lezen, te beantwoorden of in te plannen in je dagagenda. +

+
+ + + +
+ +
+ +
+ + + + + + + + + + + + + diff --git a/profile.php b/profile.php index 0002967..db03d06 100644 --- a/profile.php +++ b/profile.php @@ -535,26 +535,71 @@ require_once __DIR__ . '/includes/header.php';
-
+

- Kies een Karakter (24 Mini Avatars) + Kies een Karakter (24 Flitsende Avatars)

Klik om direct toe te passen
+ +
+ + + + + +
+
'avatar-01.svg', 'cat' => 'emoji', 'name' => 'Cool Star (Zonnebril)'], + ['file' => 'avatar-02.svg', 'cat' => 'emoji', 'name' => 'Party Champion'], + ['file' => 'avatar-03.svg', 'cat' => 'emoji', 'name' => 'Celebration Vibe'], + ['file' => 'avatar-04.svg', 'cat' => 'emoji', 'name' => 'Laser Eyes Energy'], + ['file' => 'avatar-05.svg', 'cat' => 'emoji', 'name' => 'Sunny Smiles'], + ['file' => 'avatar-06.svg', 'cat' => 'emoji', 'name' => 'Fire Heart'], + ['file' => 'avatar-07.svg', 'cat' => 'bots', 'name' => 'Cyber Tron'], + ['file' => 'avatar-08.svg', 'cat' => 'bots', 'name' => 'Aero Bot'], + ['file' => 'avatar-09.svg', 'cat' => 'bots', 'name' => 'Neon Volt'], + ['file' => 'avatar-10.svg', 'cat' => 'bots', 'name' => 'Pixel Pulse'], + ['file' => 'avatar-11.svg', 'cat' => 'bots', 'name' => 'Quantum AI'], + ['file' => 'avatar-12.svg', 'cat' => 'bots', 'name' => 'Robo Racer'], + ['file' => 'avatar-13.svg', 'cat' => 'personas', 'name' => 'Alex (Hipster)'], + ['file' => 'avatar-14.svg', 'cat' => 'personas', 'name' => 'Maya (Tech Lead)'], + ['file' => 'avatar-15.svg', 'cat' => 'personas', 'name' => 'Lucas (Designer)'], + ['file' => 'avatar-16.svg', 'cat' => 'personas', 'name' => 'Elena (Developer)'], + ['file' => 'avatar-17.svg', 'cat' => 'personas', 'name' => 'Sam (Creative)'], + ['file' => 'avatar-18.svg', 'cat' => 'personas', 'name' => 'Zoe (Founder)'], + ['file' => 'avatar-19.svg', 'cat' => 'heroes', 'name' => 'Jack (Adventurer)'], + ['file' => 'avatar-20.svg', 'cat' => 'heroes', 'name' => 'Luna (Heroine)'], + ['file' => 'avatar-21.svg', 'cat' => 'heroes', 'name' => 'Oliver (Explorer)'], + ['file' => 'avatar-22.svg', 'cat' => 'heroes', 'name' => 'Mia (Rebel)'], + ['file' => 'avatar-23.svg', 'cat' => 'heroes', 'name' => 'Leo (Cosmic)'], + ['file' => 'avatar-24.svg', 'cat' => 'heroes', 'name' => 'Nova (Cyberpunk)'], + ]; + + foreach ($avatarPresets as $av): + $avatarPath = "/assets/avatars/{$av['file']}"; $isSelected = ($userProfile['avatar_url'] === $avatarPath); ?> -
+ -
- +
@@ -1100,6 +1145,27 @@ function previewUploadAvatar(input) { } } +function filterAvatarCategory(category) { + const items = document.querySelectorAll('.avatar-item'); + items.forEach(el => { + const cat = el.getAttribute('data-category'); + if (category === 'all' || cat === category) { + el.classList.remove('hidden'); + } else { + el.classList.add('hidden'); + } + }); + + const btns = document.querySelectorAll('.av-cat-btn'); + btns.forEach(btn => { + btn.className = 'av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300'; + }); + const activeBtn = document.getElementById('avCatBtn-' + category); + if (activeBtn) { + activeBtn.className = 'av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs'; + } +} + // Sync banner status met browser permissie & activeer actuele thema kaart document.addEventListener('DOMContentLoaded', () => { // Initialiseer geselecteerde thema-kaart