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:
@@ -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, '"');
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user