feat(widget): enhance widget settings menu with improved styling and functionality
This commit is contained in:
+13
-1
@@ -4,16 +4,28 @@
|
|||||||
* Ontvangt webhook events van Atlassian Jira Cloud (issue_created, issue_updated, etc.)
|
* Ontvangt webhook events van Atlassian Jira Cloud (issue_created, issue_updated, etc.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once __DIR__ . '/../../config/database.php';
|
require_once __DIR__ . '/../../config/database.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Beveiligingscontrole: Verifieer geheime webhook token indien geconfigureerd in .env
|
||||||
|
$configuredSecret = fociz_env('JIRA_WEBHOOK_SECRET', '');
|
||||||
|
if (!empty($configuredSecret)) {
|
||||||
|
$incomingSecret = $_GET['secret'] ?? ($_SERVER['HTTP_X_ATLASSIAN_WEBHOOK_SECRET'] ?? '');
|
||||||
|
if (!hash_equals($configuredSecret, $incomingSecret)) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Onbevoegde webhook aanroep']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$rawPayload = file_get_contents('php://input');
|
$rawPayload = file_get_contents('php://input');
|
||||||
$data = json_decode($rawPayload, true);
|
$data = json_decode($rawPayload, true);
|
||||||
|
|
||||||
if (!$data) {
|
if (!$data) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['error' => 'Invalid JSON payload']);
|
echo json_encode(['error' => 'Ongeldige JSON payload']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ if (!headers_sent()) {
|
|||||||
header('X-XSS-Protection: 1; mode=block');
|
header('X-XSS-Protection: 1; mode=block');
|
||||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
|
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
|
||||||
|
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||||
|
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
|
||||||
|
}
|
||||||
header_remove('X-Powered-By');
|
header_remove('X-Powered-By');
|
||||||
}
|
}
|
||||||
@ini_set('expose_php', 'off');
|
@ini_set('expose_php', 'off');
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri
|
|||||||
$authHeader = jira_get_auth_header($email, $apiToken);
|
$authHeader = jira_get_auth_header($email, $apiToken);
|
||||||
|
|
||||||
$fieldsList = [
|
$fieldsList = [
|
||||||
|
'*navigable',
|
||||||
'summary',
|
'summary',
|
||||||
'status',
|
'status',
|
||||||
'priority',
|
'priority',
|
||||||
@@ -233,6 +234,8 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri
|
|||||||
'updated',
|
'updated',
|
||||||
'created',
|
'created',
|
||||||
'description',
|
'description',
|
||||||
|
'labels',
|
||||||
|
'components',
|
||||||
];
|
];
|
||||||
|
|
||||||
// 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST
|
// 1. Moderne Atlassian Jira Cloud endpoint (/rest/api/3/search/jql) conform CHANGE-2046 via POST
|
||||||
@@ -325,29 +328,67 @@ function jira_fetch_issues(string $domain, string $email, string $apiToken, stri
|
|||||||
|
|
||||||
$descriptionText = jira_adf_to_text($fields['description'] ?? '');
|
$descriptionText = jira_adf_to_text($fields['description'] ?? '');
|
||||||
|
|
||||||
// Detectie van custom field "Onderwerp logging"
|
// Detectie van custom field "Onderwerp logging" of routeringsveld
|
||||||
$onderwerpLogging = '';
|
$onderwerpLogging = '';
|
||||||
|
$knownTopics = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie', 'incident', 'vraag', 'algemeen'];
|
||||||
|
|
||||||
foreach ($fields as $fKey => $fVal) {
|
foreach ($fields as $fKey => $fVal) {
|
||||||
if (str_starts_with($fKey, 'customfield_')) {
|
if (str_starts_with($fKey, 'customfield_')) {
|
||||||
if (is_array($fVal) && isset($fVal['value']) && is_string($fVal['value'])) {
|
if (is_array($fVal)) {
|
||||||
|
if (isset($fVal['value']) && is_string($fVal['value'])) {
|
||||||
$valStr = trim($fVal['value']);
|
$valStr = trim($fVal['value']);
|
||||||
$known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie', 'incident', 'vraag', 'algemeen'];
|
if (!empty($valStr)) {
|
||||||
if (in_array(strtolower($valStr), $known)) {
|
foreach ($knownTopics as $kt) {
|
||||||
|
if (str_contains(strtolower($valStr), $kt)) {
|
||||||
$onderwerpLogging = $valStr;
|
$onderwerpLogging = $valStr;
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($onderwerpLogging) && (stripos($fKey, 'onderwerp') !== false || stripos($fKey, 'logging') !== false)) {
|
||||||
|
$onderwerpLogging = $valStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} elseif (is_string($fVal) && !empty($fVal)) {
|
} elseif (is_string($fVal) && !empty($fVal)) {
|
||||||
$valStr = trim($fVal);
|
$valStr = trim($fVal);
|
||||||
$known = ['koppelingen', 'sso-koppeling', 'rest-endpoints', 'datadump', 'uwv-koppeling', 'migratie'];
|
foreach ($knownTopics as $kt) {
|
||||||
if (in_array(strtolower($valStr), $known)) {
|
if (str_contains(strtolower($valStr), $kt)) {
|
||||||
$onderwerpLogging = $valStr;
|
$onderwerpLogging = $valStr;
|
||||||
|
break 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($onderwerpLogging) && !empty($fields['onderwerp_logging'])) {
|
if (empty($onderwerpLogging) && !empty($fields['onderwerp_logging'])) {
|
||||||
$onderwerpLogging = is_string($fields['onderwerp_logging']) ? $fields['onderwerp_logging'] : ($fields['onderwerp_logging']['value'] ?? '');
|
$onderwerpLogging = is_string($fields['onderwerp_logging']) ? $fields['onderwerp_logging'] : ($fields['onderwerp_logging']['value'] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: kijk naar labels & componenten
|
||||||
|
if (empty($onderwerpLogging) && !empty($fields['labels']) && is_array($fields['labels'])) {
|
||||||
|
foreach ($fields['labels'] as $lbl) {
|
||||||
|
$lblLower = strtolower(trim($lbl));
|
||||||
|
foreach ($knownTopics as $kt) {
|
||||||
|
if (str_contains($lblLower, $kt)) {
|
||||||
|
$onderwerpLogging = ucfirst($kt);
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: detecteer onderwerp op basis van steekwoorden in summary
|
||||||
|
if (empty($onderwerpLogging)) {
|
||||||
|
$sumLower = strtolower($summary);
|
||||||
|
if (str_contains($sumLower, 'sso')) $onderwerpLogging = 'SSO-koppeling';
|
||||||
|
elseif (str_contains($sumLower, 'endpoint') || str_contains($sumLower, 'rest') || str_contains($sumLower, 'api')) $onderwerpLogging = 'Rest-endpoints';
|
||||||
|
elseif (str_contains($sumLower, 'uwv')) $onderwerpLogging = 'UWV-koppeling';
|
||||||
|
elseif (str_contains($sumLower, 'datadump') || str_contains($sumLower, 'dump')) $onderwerpLogging = 'Datadump';
|
||||||
|
elseif (str_contains($sumLower, 'migratie')) $onderwerpLogging = 'Migratie';
|
||||||
|
elseif (str_contains($sumLower, 'koppeling')) $onderwerpLogging = 'Koppelingen';
|
||||||
|
}
|
||||||
|
|
||||||
$issues[] = [
|
$issues[] = [
|
||||||
'key' => $key,
|
'key' => $key,
|
||||||
'summary' => $summary,
|
'summary' => $summary,
|
||||||
|
|||||||
@@ -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>
|
<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>
|
</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): ?>
|
<?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">
|
<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>
|
<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 currentQueue = jiraData.default_queue_filter || '';
|
||||||
let activeUrgentOnly = false;
|
let activeUrgentOnly = false;
|
||||||
let expandedKeys = new Set();
|
let expandedKeys = new Set();
|
||||||
|
const ticketStore = new Map();
|
||||||
|
|
||||||
// Bereken statussen en KPI statistieken
|
// Bereken statussen en KPI statistieken
|
||||||
function calculateKpis() {
|
function calculateKpis() {
|
||||||
@@ -758,9 +770,10 @@ function onProjectSelectChange(proj) {
|
|||||||
|
|
||||||
function updateProjectPills() {
|
function updateProjectPills() {
|
||||||
const pills = document.querySelectorAll('.project-pill');
|
const pills = document.querySelectorAll('.project-pill');
|
||||||
|
const curNorm = (currentProject || '').trim().toLowerCase();
|
||||||
pills.forEach(p => {
|
pills.forEach(p => {
|
||||||
const pKey = p.getAttribute('data-project') || '';
|
const pKey = (p.getAttribute('data-project') || '').trim().toLowerCase();
|
||||||
if (pKey === currentProject) {
|
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";
|
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 {
|
} 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";
|
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
|
// Wachtrijfilter selectie
|
||||||
function selectQueueFilter(code) {
|
function selectQueueFilter(code) {
|
||||||
currentQueue = code;
|
currentQueue = code;
|
||||||
@@ -796,18 +843,34 @@ function updateQueueCounts() {
|
|||||||
let currentList = all;
|
let currentList = all;
|
||||||
if (currentTab === 'assigned') currentList = assigned;
|
if (currentTab === 'assigned') currentList = assigned;
|
||||||
else if (currentTab === 'team') currentList = team;
|
else if (currentTab === 'team') currentList = team;
|
||||||
|
else if (currentTab === 'in_progress') {
|
||||||
if (currentProject) {
|
currentList = all.filter(iss => {
|
||||||
currentList = currentList.filter(iss => iss.project === currentProject);
|
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 dxCount = 0;
|
||||||
let sdCount = 0;
|
let sdCount = 0;
|
||||||
|
|
||||||
currentList.forEach(iss => {
|
currentList.forEach(iss => {
|
||||||
const ol = (iss.onderwerp_logging || '').toLowerCase().trim();
|
if (isIssueInQueue(iss, 'DX')) {
|
||||||
if (dxValues.includes(ol)) {
|
|
||||||
dxCount++;
|
dxCount++;
|
||||||
} else {
|
} else {
|
||||||
sdCount++;
|
sdCount++;
|
||||||
@@ -821,6 +884,14 @@ function updateQueueCounts() {
|
|||||||
if (elAll) elAll.innerText = currentList.length;
|
if (elAll) elAll.innerText = currentList.length;
|
||||||
if (elDx) elDx.innerText = dxCount;
|
if (elDx) elDx.innerText = dxCount;
|
||||||
if (elSd) elSd.innerText = sdCount;
|
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
|
// Snelkoppeling KPI-kaart Urgent
|
||||||
@@ -869,9 +940,9 @@ function resetAllFilters() {
|
|||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase();
|
const searchVal = (document.getElementById('jiraSearchInput')?.value || '').trim().toLowerCase();
|
||||||
const projectVal = currentProject || (document.getElementById('filterProject')?.value || '');
|
const projectVal = currentProject || (document.getElementById('filterProject')?.value || '');
|
||||||
const typeVal = document.getElementById('filterType')?.value || '';
|
const typeVal = (document.getElementById('filterType')?.value || '').trim().toLowerCase();
|
||||||
const priorityVal = document.getElementById('filterPriority')?.value || '';
|
const priorityVal = (document.getElementById('filterPriority')?.value || '').trim();
|
||||||
const statusVal = document.getElementById('filterStatus')?.value || '';
|
const statusVal = (document.getElementById('filterStatus')?.value || '').trim().toLowerCase();
|
||||||
|
|
||||||
const clearBtn = document.getElementById('clearSearchBtn');
|
const clearBtn = document.getElementById('clearSearchBtn');
|
||||||
if (clearBtn) {
|
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
|
// Pas zoek-, project-, wachtrij-, type-, prioriteit- en statusfilters toe
|
||||||
const filtered = sourceList.filter(iss => {
|
const filtered = sourceList.filter(iss => {
|
||||||
if (searchVal) {
|
if (searchVal) {
|
||||||
const keyMatch = (iss.key || '').toLowerCase().includes(searchVal);
|
const keyMatch = (iss.key || '').toLowerCase().includes(searchVal);
|
||||||
const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal);
|
const sumMatch = (iss.summary || '').toLowerCase().includes(searchVal);
|
||||||
const prjMatch = (iss.project || '').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 descMatch = (iss.description || '').toLowerCase().includes(searchVal);
|
||||||
const repMatch = (iss.reporter_name || '').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);
|
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) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentQueue) {
|
if (currentQueue) {
|
||||||
const qCode = currentQueue.toUpperCase();
|
if (!isIssueInQueue(iss, currentQueue)) {
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeVal && iss.issue_type !== typeVal) {
|
if (typeVal && (iss.issue_type || '').toLowerCase() !== typeVal) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priorityVal) {
|
if (priorityVal) {
|
||||||
if (priorityVal === 'High') {
|
const pVal = priorityVal.toLowerCase();
|
||||||
if (iss.priority !== 'High' && iss.priority !== 'Highest') return false;
|
const ip = (iss.priority || '').toLowerCase();
|
||||||
} else if (iss.priority !== priorityVal) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusVal && iss.status !== statusVal) {
|
if (statusVal && (iss.status || '').toLowerCase() !== statusVal) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,6 +1080,7 @@ function renderTickets(tickets) {
|
|||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
tickets.forEach(ticket => {
|
tickets.forEach(ticket => {
|
||||||
|
ticketStore.set(ticket.key, ticket);
|
||||||
// Issue type styling & icon
|
// 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 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';
|
let typeIcon = 'check-square';
|
||||||
@@ -1118,9 +1189,6 @@ function renderTickets(tickets) {
|
|||||||
const detailsClass = isExpanded ? '' : 'hidden';
|
const detailsClass = isExpanded ? '' : 'hidden';
|
||||||
const chevronRotateClass = isExpanded ? 'rotate-180' : '';
|
const chevronRotateClass = isExpanded ? 'rotate-180' : '';
|
||||||
|
|
||||||
// JSON stringified ticket data voor modalaanroep
|
|
||||||
const ticketSafeJson = JSON.stringify(ticket).replace(/"/g, '"');
|
|
||||||
|
|
||||||
html += `
|
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 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">
|
<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>
|
<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>
|
||||||
|
|
||||||
<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>
|
<i data-lucide="calendar" class="w-3.5 h-3.5"></i>
|
||||||
<span>Plan in Dagplanner</span>
|
<span>Plan in Dagplanner</span>
|
||||||
</button>
|
</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>
|
<i data-lucide="kanban" class="w-3.5 h-3.5"></i>
|
||||||
<span>Naar Bord</span>
|
<span>Naar Bord</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -1263,11 +1331,11 @@ function renderTickets(tickets) {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<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>
|
<i data-lucide="calendar" class="w-3 h-3"></i>
|
||||||
<span>Inplannen in Dagplanner</span>
|
<span>Inplannen in Dagplanner</span>
|
||||||
</button>
|
</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>
|
<i data-lucide="kanban" class="w-3 h-3"></i>
|
||||||
<span>Naar Kanban Bord</span>
|
<span>Naar Kanban Bord</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -1289,7 +1357,12 @@ function renderTickets(tickets) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Modal 1: Plan in Dagplanner
|
// 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('planIssueKey').value = ticket.key;
|
||||||
document.getElementById('planIssueSummary').value = ticket.summary;
|
document.getElementById('planIssueSummary').value = ticket.summary;
|
||||||
document.getElementById('planIssueUrl').value = ticket.url;
|
document.getElementById('planIssueUrl').value = ticket.url;
|
||||||
@@ -1378,7 +1451,12 @@ async function submitPlanTask(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Modal 2: Plaats op Kanban Bord
|
// 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('boardIssueKey').value = ticket.key;
|
||||||
document.getElementById('boardIssueSummary').value = ticket.summary;
|
document.getElementById('boardIssueSummary').value = ticket.summary;
|
||||||
document.getElementById('boardIssueUrl').value = ticket.url;
|
document.getElementById('boardIssueUrl').value = ticket.url;
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ async function selectMail(id) {
|
|||||||
const res = await fetch(`/api/mail.php?action=get_message&id=${encodeURIComponent(id)}`);
|
const res = await fetch(`/api/mail.php?action=get_message&id=${encodeURIComponent(id)}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success && data.message) {
|
if (data.success && data.message) {
|
||||||
bodyEl.innerHTML = data.message.body_html || `<pre class="font-sans whitespace-pre-wrap">${escapeHtml(data.message.body_text || found.body_preview)}</pre>`;
|
renderMailBodyContent(bodyEl, data.message.body_html, data.message.body_text || found.body_preview);
|
||||||
found.is_read = true;
|
found.is_read = true;
|
||||||
updateUnreadCount();
|
updateUnreadCount();
|
||||||
renderMailList(allMessages);
|
renderMailList(allMessages);
|
||||||
@@ -543,10 +543,123 @@ async function selectMail(id) {
|
|||||||
bodyEl.innerHTML = `<div class="p-4 rounded-xl bg-red-50 text-red-700 text-xs">${data.error || 'Kon bericht niet inladen.'}</div>`;
|
bodyEl.innerHTML = `<div class="p-4 rounded-xl bg-red-50 text-red-700 text-xs">${data.error || 'Kon bericht niet inladen.'}</div>`;
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
bodyEl.innerHTML = `<p class="whitespace-pre-wrap">${escapeHtml(found.body_preview)}</p>`;
|
renderMailBodyContent(bodyEl, null, found.body_preview);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grondige HTML sanitization voor inkomende e-mails (XSS & exploit preventie)
|
||||||
|
function sanitizeEmailHtml(html) {
|
||||||
|
if (!html) return '';
|
||||||
|
|
||||||
|
// 1. Verwijder alle script tags en inhoud
|
||||||
|
let cleaned = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
||||||
|
|
||||||
|
// 2. Verwijder potentieel gevaarlijke tags
|
||||||
|
cleaned = cleaned.replace(/<\/?(object|embed|applet|meta|base|form|input|button|textarea|select)\b[^>]*>/gi, '');
|
||||||
|
|
||||||
|
// 3. Verwijder inline JavaScript event handlers (onload, onerror, onclick, onmouseover, etc.)
|
||||||
|
cleaned = cleaned.replace(/\s+on[a-z0-9_-]+\s*=\s*(?:'[^']*'|"[^"]*"|[^\s>]+)/gi, '');
|
||||||
|
|
||||||
|
// 4. Neutraliseer javascript: en data:text/html pseudo-protocollen in links en afbeeldingen
|
||||||
|
cleaned = cleaned.replace(/(href|src)\s*=\s*['"]\s*(?:javascript|data:text\/html):[^'"]*['"]/gi, '$1="#"');
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Veilig renderen van e-mail HTML via een geïsoleerde sandboxed iframe
|
||||||
|
function renderMailBodyContent(container, htmlContent, plainContent) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
if (!htmlContent) {
|
||||||
|
const pre = document.createElement('pre');
|
||||||
|
pre.className = 'font-sans whitespace-pre-wrap text-sm text-slate-800 dark:text-slate-200 leading-relaxed p-2';
|
||||||
|
pre.textContent = plainContent || '(Geen inhoud)';
|
||||||
|
container.appendChild(pre);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeHtml = sanitizeEmailHtml(htmlContent);
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
|
||||||
|
// Sandboxed iframe: strikte isolatie (geen allow-scripts)
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.id = 'mailBodyIframe';
|
||||||
|
iframe.setAttribute('sandbox', 'allow-same-origin allow-popups');
|
||||||
|
iframe.className = 'w-full border-0 rounded-xl transition-all';
|
||||||
|
iframe.style.width = '100%';
|
||||||
|
iframe.style.minHeight = '350px';
|
||||||
|
iframe.style.border = 'none';
|
||||||
|
iframe.style.background = isDark ? '#0c0f17' : '#ffffff';
|
||||||
|
iframe.style.display = 'block';
|
||||||
|
|
||||||
|
container.appendChild(iframe);
|
||||||
|
|
||||||
|
const docHtml = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<base target="_blank">
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
color-scheme: ${isDark ? 'dark' : 'light'};
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 14px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: ${isDark ? '#f1f5f9' : '#0f172a'};
|
||||||
|
background-color: ${isDark ? '#0c0f17' : '#ffffff'};
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
pre, code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${safeHtml}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
iframe.srcdoc = docHtml;
|
||||||
|
|
||||||
|
iframe.onload = () => {
|
||||||
|
try {
|
||||||
|
const innerDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
if (innerDoc) {
|
||||||
|
const updateHeight = () => {
|
||||||
|
const scrollH = innerDoc.documentElement.scrollHeight || innerDoc.body.scrollHeight;
|
||||||
|
iframe.style.height = Math.max(scrollH + 35, 280) + 'px';
|
||||||
|
};
|
||||||
|
updateHeight();
|
||||||
|
if (window.ResizeObserver) {
|
||||||
|
const observer = new ResizeObserver(updateHeight);
|
||||||
|
observer.observe(innerDoc.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
iframe.style.height = '600px';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function updateUnreadCount() {
|
function updateUnreadCount() {
|
||||||
const unread = allMessages.filter(m => !m.is_read).length;
|
const unread = allMessages.filter(m => !m.is_read).length;
|
||||||
const el = document.getElementById('countUnread');
|
const el = document.getElementById('countUnread');
|
||||||
|
|||||||
+610
-27
@@ -230,16 +230,30 @@ for ($i = 0; $i < 7; $i++) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Haal Jira issues op voor de snelle import widget
|
// 5. Haal Jira dashboard data op voor de snelle import widget en wachtrijfiltering
|
||||||
$jiraIssues = [];
|
$jiraAssignedIssues = [];
|
||||||
|
$jiraTeamIssues = [];
|
||||||
|
$jiraAllIssues = [];
|
||||||
|
$jiraQueueFilters = [];
|
||||||
$isJiraLive = false;
|
$isJiraLive = false;
|
||||||
try {
|
try {
|
||||||
$jiraData = jira_get_user_issues($userId, $forceRefresh);
|
$jiraDashboardData = jira_get_dashboard_data($userId, $forceRefresh);
|
||||||
$jiraIssues = $jiraData['issues'] ?? [];
|
$jiraAssignedIssues = $jiraDashboardData['assigned_issues'] ?? [];
|
||||||
$isJiraLive = !empty($jiraData['success']) && empty($jiraData['is_demo']);
|
$jiraTeamIssues = $jiraDashboardData['team_issues'] ?? [];
|
||||||
|
$jiraQueueFilters = $jiraDashboardData['queue_filters'] ?? [];
|
||||||
|
$isJiraLive = !empty($jiraDashboardData['success']) && empty($jiraDashboardData['is_demo']);
|
||||||
|
|
||||||
|
$seenJiraKeys = [];
|
||||||
|
foreach (array_merge($jiraAssignedIssues, $jiraTeamIssues) as $iss) {
|
||||||
|
if (!empty($iss['key']) && !isset($seenJiraKeys[$iss['key']])) {
|
||||||
|
$seenJiraKeys[$iss['key']] = true;
|
||||||
|
$jiraAllIssues[] = $iss;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
error_log("Fout bij ophalen Jira issues: " . $e->getMessage());
|
error_log("Fout bij ophalen Jira issues in planner: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
$jiraIssues = $jiraAllIssues;
|
||||||
|
|
||||||
// 5b. Haal recente e-mails op voor de Outlook E-mail Widget
|
// 5b. Haal recente e-mails op voor de Outlook E-mail Widget
|
||||||
$recentEmails = [];
|
$recentEmails = [];
|
||||||
@@ -565,7 +579,53 @@ for ($h = $startHour; $h <= $endHour; $h++) {
|
|||||||
default => 'lg:col-span-6'
|
default => 'lg:col-span-6'
|
||||||
};
|
};
|
||||||
?>
|
?>
|
||||||
<div class="<?= $colClass ?> col-span-1 min-w-0 transition-all" data-widget-id="<?= htmlspecialchars($wId) ?>">
|
<div class="<?= $colClass ?> col-span-1 min-w-0 transition-all duration-300 fociz-widget-wrapper group/widget" data-widget-id="<?= htmlspecialchars($wId) ?>" data-col-span="<?= $span ?>" id="widgetWrapper_<?= htmlspecialchars($wId) ?>">
|
||||||
|
<!-- Bovenste rand van de widget: Prachtig gestyled en direct geïntegreerd in de widget -->
|
||||||
|
<div class="widget-header-bar flex items-center justify-between px-3.5 py-1.5 bg-slate-50/90 dark:bg-[#0c0f17]/90 border border-b-0 border-slate-200/80 dark:border-slate-800/80 rounded-t-2xl select-none backdrop-blur-xs transition-colors group-hover/widget:border-indigo-400/50 dark:group-hover/widget:border-indigo-500/30">
|
||||||
|
<!-- Links: Drag Handle Grip + Widget Titel -->
|
||||||
|
<div class="widget-drag-handle flex items-center space-x-1.5 cursor-grab active:cursor-grabbing text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 py-0.5 px-1 -ml-1 rounded-lg hover:bg-slate-200/50 dark:hover:bg-slate-800/50 transition group/drag" draggable="true" data-widget-id="<?= htmlspecialchars($wId) ?>" title="Sleep om te verplaatsen">
|
||||||
|
<i data-lucide="grip-vertical" class="w-3.5 h-3.5 text-slate-400 group-hover/drag:text-indigo-500 transition-colors flex-shrink-0"></i>
|
||||||
|
<span class="text-xs font-bold text-slate-700 dark:text-slate-300 tracking-wide truncate max-w-[160px] sm:max-w-[240px]"><?= htmlspecialchars($widget['title']) ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rechts: Tandwieltje (Formaat & Verbergen) & Inklappen -->
|
||||||
|
<div class="flex items-center space-x-1 relative">
|
||||||
|
<!-- Tandwieltje Knop met Dropdown -->
|
||||||
|
<div class="relative">
|
||||||
|
<button type="button" onclick="toggleWidgetSettingsMenu('<?= $wId ?>', event)" class="widget-settings-btn p-1 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-200/60 dark:hover:bg-slate-800/60 transition cursor-pointer" title="Widget instellingen (formaat en verbergen)">
|
||||||
|
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Dropdown Popover Menu -->
|
||||||
|
<div id="widgetSettingsMenu_<?= htmlspecialchars($wId) ?>" class="widget-settings-dropdown hidden absolute right-0 top-full mt-1.5 z-50 w-52 p-2.5 rounded-2xl bg-white/95 dark:bg-[#111624]/95 backdrop-blur-md border border-slate-200 dark:border-slate-800 shadow-2xl space-y-2.5">
|
||||||
|
<div>
|
||||||
|
<span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 block mb-1.5">Breedte instellen</span>
|
||||||
|
<div class="grid grid-cols-4 gap-1">
|
||||||
|
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 4)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 4 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="4" title="1/3 breedte (4 kolommen)">1/3</button>
|
||||||
|
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 6)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 6 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="6" title="1/2 breedte (6 kolommen)">1/2</button>
|
||||||
|
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 8)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 8 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="8" title="2/3 breedte (8 kolommen)">2/3</button>
|
||||||
|
<button type="button" onclick="setWidgetSpan('<?= $wId ?>', 12)" class="widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer <?= $span == 12 ? 'bg-indigo-600 border-indigo-600 text-white shadow-2xs' : 'bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' ?>" data-span="12" title="Volledige breedte (12 kolommen)">Vol</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-1.5 border-t border-slate-100 dark:border-slate-800/80">
|
||||||
|
<button type="button" onclick="hideWidgetOnScreen('<?= $wId ?>')" class="w-full flex items-center space-x-2 px-2 py-1.5 rounded-xl text-slate-600 dark:text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-500/10 transition cursor-pointer text-left text-xs font-semibold">
|
||||||
|
<i data-lucide="eye-off" class="w-3.5 h-3.5 flex-shrink-0"></i>
|
||||||
|
<span>Widget verbergen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inklappen / Uitklappen Knop -->
|
||||||
|
<button type="button" onclick="toggleWidgetCollapse('<?= $wId ?>')" class="p-1 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-200/60 dark:hover:bg-slate-800/60 transition cursor-pointer" title="Inklappen / Uitklappen">
|
||||||
|
<i data-lucide="chevron-up" class="w-3.5 h-3.5 widget-collapse-icon transition-transform duration-200" id="collapse_icon_<?= htmlspecialchars($wId) ?>"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Widget Content Body: sluit naadloos aan op de bovenste rand -->
|
||||||
|
<div class="widget-content-body transition-all duration-200 [&>.fociz-card]:rounded-t-none [&>.fociz-card]:border-t-0" id="widget_body_<?= htmlspecialchars($wId) ?>">
|
||||||
|
|
||||||
<?php if ($wId === 'daily_goal'): ?>
|
<?php if ($wId === 'daily_goal'): ?>
|
||||||
<!-- Widget 1: Dagfocus & Hoofddoel -->
|
<!-- Widget 1: Dagfocus & Hoofddoel -->
|
||||||
@@ -1017,52 +1077,84 @@ for ($h = $startHour; $h <= $endHour; $h++) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php elseif ($wId === 'jira_queue'): ?>
|
<?php elseif ($wId === 'jira_queue'): ?>
|
||||||
<!-- Widget 5: Jira Tickets & Wachtrijen -->
|
<!-- Widget 5: Jira Tickets & Wachtrijen (Interactive v2.0) -->
|
||||||
<div id="jiraPanel" class="fociz-card p-6">
|
<div id="jiraPanel" class="fociz-card p-5 sm:p-6">
|
||||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 mb-3">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 mb-3 gap-2">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2.5">
|
||||||
<div class="w-7 h-7 rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
|
<div class="w-7 h-7 rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
|
||||||
<i data-lucide="layers" class="w-4 h-4"></i>
|
<i data-lucide="layers" class="w-4 h-4"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
|
||||||
Jira Wachtrij & Tickets
|
Jira Wachtrijen & Tickets
|
||||||
</h3>
|
</h3>
|
||||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold <?= $isJiraLive ? 'bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400' ?>">
|
</div>
|
||||||
<?= count($jiraIssues) ?>
|
<span id="plannerJiraCountBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold <?= $isJiraLive ? 'bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400' ?>">
|
||||||
|
<?= count($jiraAllIssues) ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="/jira.php" class="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition flex items-center space-x-1 font-semibold">
|
<a href="/jira.php" class="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition flex items-center space-x-1 font-semibold flex-shrink-0">
|
||||||
<span>Alle Tickets</span>
|
<span>Jira Hub</span>
|
||||||
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
|
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (empty($jiraIssues)): ?>
|
<!-- Filter Queue Pills & Quick Search -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2.5 mb-3">
|
||||||
|
<div class="flex items-center space-x-1 overflow-x-auto pb-1 sm:pb-0 scrollbar-none" id="plannerJiraFilterTabs">
|
||||||
|
<button type="button" onclick="setPlannerJiraQueue('')" id="pjPill_all" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-indigo-600 text-white shadow-2xs flex-shrink-0 cursor-pointer">
|
||||||
|
Alle (<span id="pjCount_all"><?= count($jiraAllIssues) ?></span>)
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="setPlannerJiraQueue('my')" id="pjPill_my" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer">
|
||||||
|
Mijn (<span id="pjCount_my"><?= count($jiraAssignedIssues) ?></span>)
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="setPlannerJiraQueue('DX')" id="pjPill_DX" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer">
|
||||||
|
DX Queue (<span id="pjCount_DX">0</span>)
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="setPlannerJiraQueue('SD')" id="pjPill_SD" class="planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer">
|
||||||
|
SD Queue (<span id="pjCount_SD">0</span>)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative min-w-[140px] sm:w-44 flex-shrink-0">
|
||||||
|
<i data-lucide="search" class="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="plannerJiraSearch"
|
||||||
|
oninput="filterPlannerJira()"
|
||||||
|
placeholder="Filter tickets..."
|
||||||
|
class="w-full pl-8 pr-2 py-1 text-xs bg-slate-50 dark:bg-[#0c0f17] border border-slate-200/80 dark:border-slate-800/80 rounded-xl text-slate-800 dark:text-slate-200 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ticket List Container -->
|
||||||
|
<div id="plannerJiraListContainer" class="space-y-2 max-h-72 overflow-y-auto pr-1">
|
||||||
|
<?php if (empty($jiraAllIssues)): ?>
|
||||||
<div class="text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
|
<div class="text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
|
||||||
<i data-lucide="ticket" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
|
<i data-lucide="ticket" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
|
||||||
<p>Geen openstaande tickets gevonden in Jira.</p>
|
<p>Geen openstaande tickets gevonden in Jira.</p>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="space-y-2 max-h-72 overflow-y-auto pr-1">
|
<?php foreach (array_slice($jiraAllIssues, 0, 8) as $issue): ?>
|
||||||
<?php foreach (array_slice($jiraIssues, 0, 8) as $issue): ?>
|
|
||||||
<div class="fociz-card-interactive flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 transition shadow-2xs">
|
<div class="fociz-card-interactive flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 transition shadow-2xs">
|
||||||
<div class="truncate mr-2 min-w-0 flex-1">
|
<div class="truncate mr-2 min-w-0 flex-1">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span class="font-mono text-[10px] font-bold text-sky-700 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-1.5 py-0.5 rounded">
|
<span class="font-mono text-[10px] font-bold text-sky-700 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-1.5 py-0.5 rounded">
|
||||||
<?= htmlspecialchars($issue['key']) ?>
|
<?= htmlspecialchars($issue['key'] ?? '') ?>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
|
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
|
||||||
<?= htmlspecialchars($issue['summary']) ?>
|
<?= htmlspecialchars($issue['summary'] ?? '') ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[10px] text-slate-500 mt-0.5 block">
|
<span class="text-[10px] text-slate-500 mt-0.5 block">
|
||||||
<?= htmlspecialchars($issue['status']) ?> • <?= htmlspecialchars($issue['priority']) ?>
|
<?= htmlspecialchars($issue['status'] ?? '') ?> • <?= htmlspecialchars($issue['priority'] ?? '') ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onclick="quickImportJira(this, '<?= htmlspecialchars(addslashes($issue['summary'])) ?>', '<?= htmlspecialchars($issue['key']) ?>', '<?= htmlspecialchars(addslashes($issue['url'] ?? '')) ?>')"
|
onclick="quickImportJiraByKey(this, '<?= htmlspecialchars($issue['key'] ?? '') ?>')"
|
||||||
class="flex items-center space-x-1 text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20 px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
|
class="flex items-center space-x-1 text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20 px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
|
||||||
title="Plan in als dagtaak"
|
title="Plan in als dagtaak"
|
||||||
>
|
>
|
||||||
@@ -1071,10 +1163,16 @@ for ($h = $startHour; $h <= $endHour; $h++) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Lege Toestand (verborgen by default) -->
|
||||||
|
<div id="plannerJiraEmptyState" class="hidden text-center py-6 text-slate-400 dark:text-slate-500 text-xs">
|
||||||
|
<i data-lucide="ticket" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
|
||||||
|
<p>Geen tickets gevonden voor deze filterselectie.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php elseif ($wId === 'calendar_meetings'): ?>
|
<?php elseif ($wId === 'calendar_meetings'): ?>
|
||||||
<!-- Widget 6: Agenda & Teams Meetings -->
|
<!-- Widget 6: Agenda & Teams Meetings -->
|
||||||
<div class="fociz-card p-6" id="plannerMeetingsWidget">
|
<div class="fociz-card p-6" id="plannerMeetingsWidget">
|
||||||
@@ -1424,7 +1522,8 @@ for ($h = $startHour; $h <= $endHour; $h++) {
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
</div>
|
</div><!-- /.widget-content-body -->
|
||||||
|
</div><!-- /.fociz-widget-wrapper -->
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
@@ -1805,6 +1904,482 @@ async function saveWidgetLayoutConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- HTML Escaping Helper (XSS preventie) ---
|
||||||
|
function escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Floating Toast Notificatie ---
|
||||||
|
function showWidgetToast(htmlMessage, type = 'success', duration = 3500) {
|
||||||
|
let toast = document.getElementById('widgetGlobalToast');
|
||||||
|
if (!toast) {
|
||||||
|
toast = document.createElement('div');
|
||||||
|
toast.id = 'widgetGlobalToast';
|
||||||
|
toast.className = 'fixed bottom-6 right-6 z-50 transform translate-y-20 opacity-0 transition-all duration-300 pointer-events-auto flex items-center space-x-2.5 px-4 py-2.5 rounded-2xl bg-slate-900/95 dark:bg-white text-white dark:text-slate-900 text-xs font-semibold shadow-2xl border border-slate-700/60 dark:border-slate-200/80';
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = type === 'success' ? 'check-circle-2' : 'info';
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="w-5 h-5 rounded-full ${type === 'success' ? 'bg-emerald-500/20 text-emerald-400 dark:text-emerald-600' : 'bg-indigo-500/20 text-indigo-400 dark:text-indigo-600'} flex items-center justify-center flex-shrink-0">
|
||||||
|
<i data-lucide="${icon}" class="w-3.5 h-3.5"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">${htmlMessage}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.classList.remove('translate-y-20', 'opacity-0');
|
||||||
|
toast.classList.add('translate-y-0', 'opacity-100');
|
||||||
|
|
||||||
|
if (window._widgetToastTimeout) clearTimeout(window._widgetToastTimeout);
|
||||||
|
window._widgetToastTimeout = setTimeout(() => {
|
||||||
|
toast.classList.add('translate-y-20', 'opacity-0');
|
||||||
|
toast.classList.remove('translate-y-0', 'opacity-100');
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Achtergrond Auto-Save voor Widget Layout ---
|
||||||
|
async function persistWidgetLayoutAuto() {
|
||||||
|
try {
|
||||||
|
const payload = focizWidgets.map((w, idx) => ({
|
||||||
|
id: w.id,
|
||||||
|
enabled: !!w.enabled,
|
||||||
|
col_span: parseInt(w.col_span, 10) || 6,
|
||||||
|
order: idx + 1
|
||||||
|
}));
|
||||||
|
|
||||||
|
const res = await fetch('/api/planner.php?action=save_widget_layout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ layout: payload })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return data.success;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Fout bij automatisch opslaan widget layout:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- In-Widget Inline Breedte Aanpassen (1/3, 1/2, 2/3, 100%) ---
|
||||||
|
function setWidgetSpan(wId, span) {
|
||||||
|
span = parseInt(span, 10);
|
||||||
|
const wrapper = document.getElementById(`widgetWrapper_${wId}`) || document.querySelector(`.fociz-widget-wrapper[data-widget-id="${wId}"]`);
|
||||||
|
if (!wrapper) return;
|
||||||
|
|
||||||
|
// Verwijder alle mogelijke col-span klassen
|
||||||
|
const spanClasses = ['lg:col-span-12', 'lg:col-span-8', 'lg:col-span-7', 'lg:col-span-6', 'lg:col-span-5', 'lg:col-span-4'];
|
||||||
|
spanClasses.forEach(c => wrapper.classList.remove(c));
|
||||||
|
|
||||||
|
// Voeg nieuwe klasse toe
|
||||||
|
wrapper.classList.add(`lg:col-span-${span}`);
|
||||||
|
wrapper.setAttribute('data-col-span', span);
|
||||||
|
|
||||||
|
// Knoppen styling bijwerken binnen het instellingen dropdown menu
|
||||||
|
const buttons = wrapper.querySelectorAll('.widget-span-btn');
|
||||||
|
buttons.forEach(btn => {
|
||||||
|
const bSpan = parseInt(btn.getAttribute('data-span'), 10);
|
||||||
|
if (bSpan === span) {
|
||||||
|
btn.className = "widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer bg-indigo-600 border-indigo-600 text-white shadow-2xs";
|
||||||
|
} else {
|
||||||
|
btn.className = "widget-span-btn py-1 text-center text-[11px] font-bold rounded-lg border transition cursor-pointer bg-slate-50 dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update in-memory array
|
||||||
|
const item = focizWidgets.find(x => x.id === wId);
|
||||||
|
if (item) {
|
||||||
|
item.col_span = span;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sluit het instellingen menu
|
||||||
|
document.getElementById(`widgetSettingsMenu_${wId}`)?.classList.add('hidden');
|
||||||
|
|
||||||
|
// Trigger resize voor SVG en diagrammen
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
|
||||||
|
// Automatisch opslaan naar database
|
||||||
|
persistWidgetLayoutAuto();
|
||||||
|
|
||||||
|
const spanDescriptions = {
|
||||||
|
4: '1/3 schermbreedte (33%)',
|
||||||
|
6: '1/2 schermbreedte (50%)',
|
||||||
|
8: '2/3 schermbreedte (66%)',
|
||||||
|
12: 'volledige breedte (100%)'
|
||||||
|
};
|
||||||
|
showWidgetToast(`Formaat aangepast naar ${spanDescriptions[span] || span + ' kolommen'}`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Widget Instellingen Popover Menu (Tandwieltje) ---
|
||||||
|
function toggleWidgetSettingsMenu(wId, event) {
|
||||||
|
if (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
const menu = document.getElementById(`widgetSettingsMenu_${wId}`);
|
||||||
|
if (!menu) return;
|
||||||
|
const isCurrentlyHidden = menu.classList.contains('hidden');
|
||||||
|
// Sluit alle open menu's eerst
|
||||||
|
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
|
||||||
|
if (isCurrentlyHidden) {
|
||||||
|
menu.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Widget Inklappen / Uitklappen ---
|
||||||
|
function toggleWidgetCollapse(wId) {
|
||||||
|
const body = document.getElementById(`widget_body_${wId}`);
|
||||||
|
const icon = document.getElementById(`collapse_icon_${wId}`);
|
||||||
|
if (!body) return;
|
||||||
|
|
||||||
|
const isNowHidden = body.classList.toggle('hidden');
|
||||||
|
if (icon) {
|
||||||
|
if (isNowHidden) {
|
||||||
|
icon.classList.add('rotate-180');
|
||||||
|
} else {
|
||||||
|
icon.classList.remove('rotate-180');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(`fociz_widget_collapsed_${wId}`, isNowHidden ? '1' : '0');
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Widget Verbergen van Scherm met Undo Toast ---
|
||||||
|
function hideWidgetOnScreen(wId) {
|
||||||
|
document.getElementById(`widgetSettingsMenu_${wId}`)?.classList.add('hidden');
|
||||||
|
const wrapper = document.getElementById(`widgetWrapper_${wId}`) || document.querySelector(`.fociz-widget-wrapper[data-widget-id="${wId}"]`);
|
||||||
|
const item = focizWidgets.find(x => x.id === wId);
|
||||||
|
const title = item ? item.title : 'Widget';
|
||||||
|
|
||||||
|
if (wrapper) {
|
||||||
|
wrapper.style.transition = 'all 0.25s ease';
|
||||||
|
wrapper.style.opacity = '0';
|
||||||
|
wrapper.style.transform = 'scale(0.95)';
|
||||||
|
setTimeout(() => {
|
||||||
|
wrapper.remove();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
item.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
persistWidgetLayoutAuto();
|
||||||
|
showWidgetToast(`Widget "${title}" verborgen <button type="button" onclick="undoHideWidget('${wId}')" class="underline font-bold ml-2 text-indigo-400 hover:text-indigo-300 cursor-pointer">Ongedaan maken</button>`, 'info', 6000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function undoHideWidget(wId) {
|
||||||
|
const item = focizWidgets.find(x => x.id === wId);
|
||||||
|
if (item) {
|
||||||
|
item.enabled = true;
|
||||||
|
}
|
||||||
|
persistWidgetLayoutAuto();
|
||||||
|
showWidgetToast('Widget herstellen...', 'success', 2000);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Direct On-Screen Drag & Drop Engine (Live Shift & Reflow) ---
|
||||||
|
let draggedWidgetEl = null;
|
||||||
|
let dragInitialIndex = -1;
|
||||||
|
|
||||||
|
function initPlannerWidgets() {
|
||||||
|
const grid = document.getElementById('focizWidgetGrid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
// Herstel eventuele ingeklapte widgets vanuit localStorage
|
||||||
|
document.querySelectorAll('.fociz-widget-wrapper').forEach(wrapper => {
|
||||||
|
const wId = wrapper.getAttribute('data-widget-id');
|
||||||
|
try {
|
||||||
|
if (localStorage.getItem(`fociz_widget_collapsed_${wId}`) === '1') {
|
||||||
|
const body = document.getElementById(`widget_body_${wId}`);
|
||||||
|
const icon = document.getElementById(`collapse_icon_${wId}`);
|
||||||
|
if (body) body.classList.add('hidden');
|
||||||
|
if (icon) icon.classList.add('rotate-180');
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Luister naar drag handles
|
||||||
|
document.querySelectorAll('.widget-drag-handle').forEach(handle => {
|
||||||
|
handle.setAttribute('draggable', 'true');
|
||||||
|
|
||||||
|
handle.ondragstart = (e) => {
|
||||||
|
const wrapper = handle.closest('.fociz-widget-wrapper');
|
||||||
|
if (!wrapper) return;
|
||||||
|
draggedWidgetEl = wrapper;
|
||||||
|
|
||||||
|
// Bewaar beginpositie om te controleren op wijzigingen
|
||||||
|
const allWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
|
||||||
|
dragInitialIndex = allWrappers.indexOf(draggedWidgetEl);
|
||||||
|
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', wrapper.getAttribute('data-widget-id'));
|
||||||
|
|
||||||
|
// Sluit eventuele open popover menu's
|
||||||
|
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (draggedWidgetEl) {
|
||||||
|
draggedWidgetEl.classList.add('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl');
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
handle.ondragend = (e) => {
|
||||||
|
if (draggedWidgetEl) {
|
||||||
|
draggedWidgetEl.classList.remove('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl');
|
||||||
|
}
|
||||||
|
saveCurrentGridOrderIfChanged();
|
||||||
|
draggedWidgetEl = null;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dragover op grid: verplaatst de widget live in het DOM zodat andere blokken direct opzij schuiven
|
||||||
|
grid.ondragover = (e) => {
|
||||||
|
if (!draggedWidgetEl) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
|
||||||
|
// Check of cursor onderaan het grid zweeft
|
||||||
|
const gridRect = grid.getBoundingClientRect();
|
||||||
|
if (e.clientY > gridRect.bottom - 40) {
|
||||||
|
if (grid.lastElementChild !== draggedWidgetEl) {
|
||||||
|
grid.appendChild(draggedWidgetEl);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check of cursor bovenaan het grid zweeft
|
||||||
|
if (e.clientY < gridRect.top + 40) {
|
||||||
|
if (grid.firstElementChild !== draggedWidgetEl) {
|
||||||
|
grid.prepend(draggedWidgetEl);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = e.target.closest('.fociz-widget-wrapper');
|
||||||
|
if (target && target !== draggedWidgetEl && target.parentElement === grid) {
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
const midY = rect.top + rect.height / 2;
|
||||||
|
const midX = rect.left + rect.width / 2;
|
||||||
|
|
||||||
|
// Bepaal of de muis voorbij het midden is (verticaal of horizontaal in dezelfde rij)
|
||||||
|
const isAfter = (e.clientY > midY) || (Math.abs(e.clientY - midY) < rect.height * 0.35 && e.clientX > midX);
|
||||||
|
|
||||||
|
if (isAfter) {
|
||||||
|
if (target.nextElementSibling !== draggedWidgetEl) {
|
||||||
|
target.after(draggedWidgetEl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (target.previousElementSibling !== draggedWidgetEl) {
|
||||||
|
target.before(draggedWidgetEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
grid.ondrop = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (draggedWidgetEl) {
|
||||||
|
draggedWidgetEl.classList.remove('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl');
|
||||||
|
}
|
||||||
|
saveCurrentGridOrderIfChanged();
|
||||||
|
draggedWidgetEl = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCurrentGridOrderIfChanged() {
|
||||||
|
const grid = document.getElementById('focizWidgetGrid');
|
||||||
|
if (!grid) return;
|
||||||
|
const currentWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
|
||||||
|
const currentIndex = currentWrappers.indexOf(draggedWidgetEl);
|
||||||
|
|
||||||
|
let hasOrderChanged = false;
|
||||||
|
currentWrappers.forEach((el, index) => {
|
||||||
|
const wId = el.getAttribute('data-widget-id');
|
||||||
|
const item = focizWidgets.find(x => x.id === wId);
|
||||||
|
if (item) {
|
||||||
|
if (item.order !== index + 1) {
|
||||||
|
item.order = index + 1;
|
||||||
|
hasOrderChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasOrderChanged || (dragInitialIndex !== -1 && currentIndex !== dragInitialIndex)) {
|
||||||
|
persistWidgetLayoutAuto();
|
||||||
|
showWidgetToast('Widget verplaatst & indeling opgeslagen', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sluit popup menu's als er buiten wordt geklikt
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!e.target.closest('.widget-settings-dropdown') && !e.target.closest('.widget-settings-btn')) {
|
||||||
|
document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Jira Queue & Tickets Widget Engine (v2.0) ---
|
||||||
|
const plannerJiraAssigned = <?= json_encode($jiraAssignedIssues, JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
const plannerJiraAll = <?= json_encode($jiraAllIssues, JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
const plannerTicketStore = new Map();
|
||||||
|
plannerJiraAll.forEach(iss => {
|
||||||
|
if (iss && iss.key) plannerTicketStore.set(iss.key, iss);
|
||||||
|
});
|
||||||
|
|
||||||
|
let activePlannerJiraQueue = ''; // '': Alle, 'my': Mijn, 'DX': DX Queue, 'SD': SD Queue
|
||||||
|
|
||||||
|
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 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') return !matchesDx;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPlannerJiraQueue(queueCode) {
|
||||||
|
activePlannerJiraQueue = queueCode;
|
||||||
|
|
||||||
|
// Tab styling bijwerken
|
||||||
|
['all', 'my', 'DX', 'SD'].forEach(tab => {
|
||||||
|
const btn = document.getElementById(`pjPill_${tab}`);
|
||||||
|
if (!btn) return;
|
||||||
|
const isActive = (tab === 'all' && !queueCode) || (tab === queueCode);
|
||||||
|
if (isActive) {
|
||||||
|
btn.className = "planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-indigo-600 text-white shadow-2xs flex-shrink-0 cursor-pointer";
|
||||||
|
} else {
|
||||||
|
btn.className = "planner-jira-tab px-2.5 py-1 rounded-lg text-[11px] font-bold transition bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0 cursor-pointer";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
filterPlannerJira();
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterPlannerJira() {
|
||||||
|
const searchVal = (document.getElementById('plannerJiraSearch')?.value || '').toLowerCase().trim();
|
||||||
|
const listContainer = document.getElementById('plannerJiraListContainer');
|
||||||
|
const emptyState = document.getElementById('plannerJiraEmptyState');
|
||||||
|
if (!listContainer) return;
|
||||||
|
|
||||||
|
// Tellers bijwerken
|
||||||
|
let dxCount = 0;
|
||||||
|
let sdCount = 0;
|
||||||
|
plannerJiraAll.forEach(iss => {
|
||||||
|
if (isIssueInQueue(iss, 'DX')) dxCount++;
|
||||||
|
else sdCount++;
|
||||||
|
});
|
||||||
|
const elAll = document.getElementById('pjCount_all');
|
||||||
|
const elMy = document.getElementById('pjCount_my');
|
||||||
|
const elDx = document.getElementById('pjCount_DX');
|
||||||
|
const elSd = document.getElementById('pjCount_SD');
|
||||||
|
if (elAll) elAll.innerText = plannerJiraAll.length;
|
||||||
|
if (elMy) elMy.innerText = plannerJiraAssigned.length;
|
||||||
|
if (elDx) elDx.innerText = dxCount;
|
||||||
|
if (elSd) elSd.innerText = sdCount;
|
||||||
|
|
||||||
|
// Filteren
|
||||||
|
let filtered = plannerJiraAll.filter(iss => {
|
||||||
|
if (activePlannerJiraQueue === 'my') {
|
||||||
|
const isAssigned = plannerJiraAssigned.some(a => a.key === iss.key);
|
||||||
|
if (!isAssigned) return false;
|
||||||
|
} else if (activePlannerJiraQueue === 'DX') {
|
||||||
|
if (!isIssueInQueue(iss, 'DX')) return false;
|
||||||
|
} else if (activePlannerJiraQueue === 'SD') {
|
||||||
|
if (!isIssueInQueue(iss, 'SD')) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchVal) {
|
||||||
|
const k = (iss.key || '').toLowerCase();
|
||||||
|
const s = (iss.summary || '').toLowerCase();
|
||||||
|
const p = (iss.project || '').toLowerCase();
|
||||||
|
const ol = (iss.onderwerp_logging || '').toLowerCase();
|
||||||
|
if (!k.includes(searchVal) && !s.includes(searchVal) && !p.includes(searchVal) && !ol.includes(searchVal)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Badge totale resultaten bijwerken
|
||||||
|
const badge = document.getElementById('plannerJiraCountBadge');
|
||||||
|
if (badge) badge.innerText = filtered.length;
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
listContainer.innerHTML = '';
|
||||||
|
if (emptyState) emptyState.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emptyState) emptyState.classList.add('hidden');
|
||||||
|
|
||||||
|
listContainer.innerHTML = filtered.slice(0, 15).map(issue => {
|
||||||
|
const keyEsc = escapeHtml(issue.key || '');
|
||||||
|
const summaryEsc = escapeHtml(issue.summary || '');
|
||||||
|
const statusEsc = escapeHtml(issue.status || 'To Do');
|
||||||
|
const priorityEsc = escapeHtml(issue.priority || 'Medium');
|
||||||
|
const isDx = isIssueInQueue(issue, 'DX');
|
||||||
|
const queuePill = isDx
|
||||||
|
? '<span class="px-1.5 py-0.5 rounded text-[9px] font-bold bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20">DX</span>'
|
||||||
|
: '<span class="px-1.5 py-0.5 rounded text-[9px] font-bold bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-200 dark:border-blue-500/20">SD</span>';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="fociz-card-interactive flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 transition shadow-2xs">
|
||||||
|
<div class="truncate mr-2 min-w-0 flex-1">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="font-mono text-[10px] font-bold text-sky-700 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-1.5 py-0.5 rounded">
|
||||||
|
${keyEsc}
|
||||||
|
</span>
|
||||||
|
${queuePill}
|
||||||
|
<span class="text-xs font-semibold text-slate-900 dark:text-white truncate">
|
||||||
|
${summaryEsc}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-slate-500 mt-0.5 block">
|
||||||
|
${statusEsc} • ${priorityEsc}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick="quickImportJiraByKey(this, '${keyEsc}')"
|
||||||
|
class="flex items-center space-x-1 text-[11px] font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20 px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
|
||||||
|
title="Plan in als dagtaak"
|
||||||
|
>
|
||||||
|
<i data-lucide="plus" class="w-3 h-3"></i>
|
||||||
|
<span>Plan</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quickImportJiraByKey(btn, issueKey) {
|
||||||
|
const issue = plannerTicketStore.get(issueKey);
|
||||||
|
if (!issue) return;
|
||||||
|
return quickImportJira(btn, issue.summary || issueKey, issue.key, issue.url || '');
|
||||||
|
}
|
||||||
|
|
||||||
// --- Direct E-mail Inplannen als Dagtaak ---
|
// --- Direct E-mail Inplannen als Dagtaak ---
|
||||||
function openPlanEmailDirect(mailId, subject, sender, webLink) {
|
function openPlanEmailDirect(mailId, subject, sender, webLink) {
|
||||||
document.getElementById('planEmailId').value = mailId;
|
document.getElementById('planEmailId').value = mailId;
|
||||||
@@ -2045,6 +2620,10 @@ async function switchPlannerDate(newDate, event = null, pushState = true) {
|
|||||||
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
initPlannerWidgets();
|
||||||
|
if (document.getElementById('plannerJiraListContainer')) {
|
||||||
|
filterPlannerJira();
|
||||||
|
}
|
||||||
|
|
||||||
// Garandeer dat het scherm exact op dezelfde scrollhoogte blijft
|
// Garandeer dat het scherm exact op dezelfde scrollhoogte blijft
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
@@ -2679,6 +3258,10 @@ function triggerManualSync() {
|
|||||||
// Initialisatie bij pagina-inladen
|
// Initialisatie bij pagina-inladen
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
restoreTimerStateIfAny();
|
restoreTimerStateIfAny();
|
||||||
|
initPlannerWidgets();
|
||||||
|
if (document.getElementById('plannerJiraListContainer')) {
|
||||||
|
filterPlannerJira();
|
||||||
|
}
|
||||||
|
|
||||||
if (isOutlookConnected) {
|
if (isOutlookConnected) {
|
||||||
if (syncIntervalSeconds > 0) {
|
if (syncIntervalSeconds > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user