Enhance email rendering and Jira widget functionality

- Implemented a secure rendering method for email content using a sandboxed iframe to prevent XSS attacks.
- Added thorough HTML sanitization for incoming emails to mitigate potential exploits.
- Refactored email body rendering logic to utilize a dedicated function for better readability and maintainability.
- Updated Jira widget to fetch and display dashboard data, including assigned issues and team issues, with improved filtering options.
- Enhanced the user interface of the Jira widget with a standardized control toolbar for resizing and collapsing widgets.
- Introduced inline search functionality within the Jira widget for quick ticket filtering.
- Improved error handling and logging for Jira data fetching.
This commit is contained in:
2026-09-10 15:44:37 +02:00
parent 72ad1fb70e
commit e161c72fc4
6 changed files with 867 additions and 100 deletions
+132 -54
View File
@@ -320,6 +320,17 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
<span id="queueCount_SD" class="px-1.5 py-0.5 rounded-full text-[10px] bg-sky-100 dark:bg-sky-950/60 text-sky-800 dark:text-sky-300 font-bold">0</span>
</button>
<?php foreach ($queueFilters as $qf):
$code = $qf['filter_code'] ?? '';
if ($code === 'DX' || $code === 'SD' || empty($code)) continue;
?>
<button type="button" onclick="selectQueueFilter('<?= htmlspecialchars($code) ?>')" data-queue="<?= htmlspecialchars($code) ?>" class="queue-pill flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0">
<i data-lucide="filter" class="w-3.5 h-3.5 text-indigo-500"></i>
<span><?= htmlspecialchars($qf['filter_name'] ?? $code) ?></span>
<span id="queueCount_<?= htmlspecialchars($code) ?>" class="px-1.5 py-0.5 rounded-full text-[10px] bg-indigo-100 dark:bg-indigo-950/60 text-indigo-800 dark:text-indigo-300 font-bold">0</span>
</button>
<?php endforeach; ?>
<?php if ($isAdmin): ?>
<a href="/admin.php#jira-wachtrijen" title="Wachtrijfilters beheren in Admin" class="p-1.5 rounded-xl text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-200/60 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0">
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
@@ -677,6 +688,7 @@ let currentProject = '';
let currentQueue = jiraData.default_queue_filter || '';
let activeUrgentOnly = false;
let expandedKeys = new Set();
const ticketStore = new Map();
// Bereken statussen en KPI statistieken
function calculateKpis() {
@@ -758,9 +770,10 @@ function onProjectSelectChange(proj) {
function updateProjectPills() {
const pills = document.querySelectorAll('.project-pill');
const curNorm = (currentProject || '').trim().toLowerCase();
pills.forEach(p => {
const pKey = p.getAttribute('data-project') || '';
if (pKey === currentProject) {
const pKey = (p.getAttribute('data-project') || '').trim().toLowerCase();
if (pKey === curNorm) {
p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-indigo-600 text-white shadow-sm shadow-indigo-600/20 flex-shrink-0";
} else {
p.className = "project-pill flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700/80 border border-slate-200/80 dark:border-slate-700/80 flex-shrink-0";
@@ -768,6 +781,40 @@ function updateProjectPills() {
});
}
// Hulpfunctie: Bepaal of een ticket tot een specifieke wachtrij behoort (DX, SD of aangepast)
function isIssueInQueue(iss, qCode) {
if (!qCode) return true;
const code = qCode.toUpperCase();
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
const sum = (iss.summary || '').toLowerCase();
const pKey = (iss.project_key || '').toUpperCase().trim();
const dxTerms = ['koppeling', 'sso', 'endpoint', 'rest', 'datadump', 'uwv', 'migratie', 'api', 'webhook', 'integrat'];
const matchesDx = dxTerms.some(t => ol.includes(t) || sum.includes(t));
if (code === 'DX') {
return matchesDx;
}
if (code === 'SD') {
// SD is regulier Servicedesk (alle tickets die niet tot DX routering behoren)
return !matchesDx;
}
// Aangepaste wachtrijfilter uit de database
const fObj = (jiraData.queue_filters || []).find(f => f.filter_code.toUpperCase() === code);
if (fObj) {
if (fObj.project_key && pKey && fObj.project_key.toUpperCase() !== pKey) {
return false;
}
const clause = (fObj.jql_clause || '').toLowerCase();
if (ol && (clause.includes(ol) || ol.includes(code.toLowerCase()))) {
return true;
}
if (clause.includes('koppeling') && matchesDx) return true;
}
return true;
}
// Wachtrijfilter selectie
function selectQueueFilter(code) {
currentQueue = code;
@@ -796,18 +843,34 @@ function updateQueueCounts() {
let currentList = all;
if (currentTab === 'assigned') currentList = assigned;
else if (currentTab === 'team') currentList = team;
if (currentProject) {
currentList = currentList.filter(iss => iss.project === currentProject);
else if (currentTab === 'in_progress') {
currentList = all.filter(iss => {
const s = (iss.status || '').toLowerCase();
const sc = (iss.status_category || '').toLowerCase();
return s.includes('progress') || s.includes('bezig') || s.includes('review') || sc === 'indeterminate';
});
} else if (currentTab === 'done') {
currentList = all.filter(iss => {
const s = (iss.status || '').toLowerCase();
const sc = (iss.status_category || '').toLowerCase();
return s.includes('done') || s.includes('afgerond') || s.includes('gesloten') || sc === 'done';
});
}
if (currentProject) {
const pNorm = currentProject.trim().toLowerCase();
currentList = currentList.filter(iss => {
const pName = (iss.project || '').trim().toLowerCase();
const pKey = (iss.project_key || '').trim().toLowerCase();
return pName === pNorm || pKey === pNorm;
});
}
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)) {
if (isIssueInQueue(iss, 'DX')) {
dxCount++;
} else {
sdCount++;
@@ -821,6 +884,14 @@ function updateQueueCounts() {
if (elAll) elAll.innerText = currentList.length;
if (elDx) elDx.innerText = dxCount;
if (elSd) elSd.innerText = sdCount;
(jiraData.queue_filters || []).forEach(f => {
const el = document.getElementById(`queueCount_${f.filter_code}`);
if (el && f.filter_code !== 'DX' && f.filter_code !== 'SD') {
const cnt = currentList.filter(iss => isIssueInQueue(iss, f.filter_code)).length;
el.innerText = cnt;
}
});
}
// Snelkoppeling KPI-kaart Urgent
@@ -869,9 +940,9 @@ function resetAllFilters() {
function applyFilters() {
const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase();
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 || '';
const typeVal = (document.getElementById('filterType')?.value || '').trim().toLowerCase();
const priorityVal = (document.getElementById('filterPriority')?.value || '').trim();
const statusVal = (document.getElementById('filterStatus')?.value || '').trim().toLowerCase();
const clearBtn = document.getElementById('clearSearchBtn');
if (clearBtn) {
@@ -902,55 +973,54 @@ function applyFilters() {
});
}
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);
const prjKeyMatch = (iss.project_key || '').toLowerCase().includes(searchVal);
const descMatch = (iss.description || '').toLowerCase().includes(searchVal);
const repMatch = (iss.reporter_name || '').toLowerCase().includes(searchVal);
const assMatch = (iss.assignee_name || '').toLowerCase().includes(searchVal);
const olMatch = (iss.onderwerp_logging || '').toLowerCase().includes(searchVal);
if (!keyMatch && !sumMatch && !prjMatch && !descMatch && !repMatch && !olMatch) return false;
if (!keyMatch && !sumMatch && !prjMatch && !prjKeyMatch && !descMatch && !repMatch && !assMatch && !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;
}
if (priorityVal) {
if (priorityVal === 'High') {
if (iss.priority !== 'High' && iss.priority !== 'Highest') return false;
} else if (iss.priority !== priorityVal) {
if (projectVal) {
const pNorm = projectVal.trim().toLowerCase();
const pName = (iss.project || '').trim().toLowerCase();
const pKey = (iss.project_key || '').trim().toLowerCase();
if (pName !== pNorm && pKey !== pNorm) {
return false;
}
}
if (statusVal && iss.status !== statusVal) {
if (currentQueue) {
if (!isIssueInQueue(iss, currentQueue)) {
return false;
}
}
if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) {
return false;
}
if (priorityVal) {
const pVal = priorityVal.toLowerCase();
const ip = (iss.priority || '').toLowerCase();
if (pVal === 'high' || pVal === 'highest' || pVal === 'p1') {
if (!['highest', 'high', 'hoogste', 'hoog', 'urgent', 'p1'].includes(ip)) return false;
} else if (pVal === 'medium' || pVal === 'p2') {
if (!['medium', 'gemiddeld', 'normaal', 'p2'].includes(ip)) return false;
} else if (pVal === 'low' || pVal === 'lowest' || pVal === 'p3') {
if (!['low', 'lowest', 'laag', 'laagste', 'p3'].includes(ip)) return false;
} else if (ip !== pVal) {
return false;
}
}
if (statusVal && (iss.status || '').toLowerCase() !== statusVal) {
return false;
}
@@ -1010,6 +1080,7 @@ function renderTickets(tickets) {
let html = '';
tickets.forEach(ticket => {
ticketStore.set(ticket.key, ticket);
// Issue type styling & icon
let typeBadgeClass = 'bg-sky-50 text-sky-700 dark:bg-sky-950/40 dark:text-sky-400 border-sky-200 dark:border-sky-800/60';
let typeIcon = 'check-square';
@@ -1118,9 +1189,6 @@ function renderTickets(tickets) {
const detailsClass = isExpanded ? '' : 'hidden';
const chevronRotateClass = isExpanded ? 'rotate-180' : '';
// JSON stringified ticket data voor modalaanroep
const ticketSafeJson = JSON.stringify(ticket).replace(/"/g, '&quot;');
html += `
<div id="ticketCard_${escapeHtml(ticket.key)}" class="fociz-card p-4 sm:p-5 transition hover:border-indigo-500/40 dark:hover:border-indigo-500/40">
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-3">
@@ -1187,12 +1255,12 @@ function renderTickets(tickets) {
<i data-lucide="chevron-down" id="chevron_${escapeHtml(ticket.key)}" class="w-3.5 h-3.5 transition-transform duration-200 ${chevronRotateClass}"></i>
</button>
<button onclick='openPlanModal(${ticketSafeJson})' class="flex items-center space-x-1.5 px-3 py-2 rounded-xl text-xs font-bold bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-950/50 dark:hover:bg-indigo-900/60 dark:text-indigo-300 border border-indigo-200/80 dark:border-indigo-800/80 transition cursor-pointer shadow-2xs">
<button onclick="openPlanModal('${escapeHtml(ticket.key)}')" class="flex items-center space-x-1.5 px-3 py-2 rounded-xl text-xs font-bold bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-950/50 dark:hover:bg-indigo-900/60 dark:text-indigo-300 border border-indigo-200/80 dark:border-indigo-800/80 transition cursor-pointer shadow-2xs">
<i data-lucide="calendar" class="w-3.5 h-3.5"></i>
<span>Plan in Dagplanner</span>
</button>
<button onclick='openBoardModal(${ticketSafeJson})' class="flex items-center space-x-1.5 px-3 py-2 rounded-xl text-xs font-bold bg-emerald-50 hover:bg-emerald-100 text-emerald-700 dark:bg-emerald-950/50 dark:hover:bg-emerald-900/60 dark:text-emerald-300 border border-emerald-200/80 dark:border-emerald-800/80 transition cursor-pointer shadow-2xs">
<button onclick="openBoardModal('${escapeHtml(ticket.key)}')" class="flex items-center space-x-1.5 px-3 py-2 rounded-xl text-xs font-bold bg-emerald-50 hover:bg-emerald-100 text-emerald-700 dark:bg-emerald-950/50 dark:hover:bg-indigo-900/60 dark:text-emerald-300 border border-emerald-200/80 dark:border-emerald-800/80 transition cursor-pointer shadow-2xs">
<i data-lucide="kanban" class="w-3.5 h-3.5"></i>
<span>Naar Bord</span>
</button>
@@ -1263,11 +1331,11 @@ function renderTickets(tickets) {
</a>
</div>
<div class="flex items-center space-x-2">
<button type="button" onclick='openPlanModal(${ticketSafeJson})' class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-700 text-white transition cursor-pointer shadow-xs">
<button type="button" onclick='openPlanModal("${escapeHtml(ticket.key)}")' class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-700 text-white transition cursor-pointer shadow-xs">
<i data-lucide="calendar" class="w-3 h-3"></i>
<span>Inplannen in Dagplanner</span>
</button>
<button type="button" onclick='openBoardModal(${ticketSafeJson})' class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-emerald-600 hover:bg-emerald-700 text-white transition cursor-pointer shadow-xs">
<button type="button" onclick='openBoardModal("${escapeHtml(ticket.key)}")' class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-bold bg-emerald-600 hover:bg-emerald-700 text-white transition cursor-pointer shadow-xs">
<i data-lucide="kanban" class="w-3 h-3"></i>
<span>Naar Kanban Bord</span>
</button>
@@ -1289,7 +1357,12 @@ function renderTickets(tickets) {
}
// Modal 1: Plan in Dagplanner
function openPlanModal(ticket) {
function openPlanModal(item) {
const ticket = (typeof item === 'string')
? (ticketStore.get(item) || (jiraData.assigned_issues || []).concat(jiraData.team_issues || []).find(t => t.key === item))
: item;
if (!ticket) return;
document.getElementById('planIssueKey').value = ticket.key;
document.getElementById('planIssueSummary').value = ticket.summary;
document.getElementById('planIssueUrl').value = ticket.url;
@@ -1378,7 +1451,12 @@ async function submitPlanTask(e) {
}
// Modal 2: Plaats op Kanban Bord
function openBoardModal(ticket) {
function openBoardModal(item) {
const ticket = (typeof item === 'string')
? (ticketStore.get(item) || (jiraData.assigned_issues || []).concat(jiraData.team_issues || []).find(t => t.key === item))
: item;
if (!ticket) return;
document.getElementById('boardIssueKey').value = ticket.key;
document.getElementById('boardIssueSummary').value = ticket.summary;
document.getElementById('boardIssueUrl').value = ticket.url;