Jira - Custom script specific for work usecase

Changes jira display for custom label styling and caches labels in local storage for swimlane view. Includes a settings panel.

נכון ליום 09-10-2025. ראה הגרסה האחרונה.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Jira - Custom script specific for work usecase
// @namespace    http://tampermonkey.net/
// @version      7.5
// @description  Changes jira display for custom label styling and caches labels in local storage for swimlane view. Includes a settings panel.
// @author       Roy
// @match        https://jira.onderwijstransparant.nl/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- INSTELLINGEN BEHEER ---

    /**
     * Laadt de instellingen uit localStorage. Geeft standaardwaarden terug als er geen zijn.
     * @returns {Object} Het instellingen-object.
     */
    function loadSettings() {
        const defaults = {
            highlightBugs: true,
            setBillableToNo: true,
            setDefaultComponent: false,
            defaultComponent: 'OT Software',
            setDefaultRegion: false,
        };
        try {
            const savedSettings = localStorage.getItem('jiraCustomScriptSettings');
            return savedSettings ? { ...defaults, ...JSON.parse(savedSettings) } : defaults;
        } catch (e) {
            console.error('Fout bij het laden van instellingen:', e);
            return defaults;
        }
    }

    /**
     * Slaat het instellingen-object op in localStorage.
     * @param {Object} settings - Het instellingen-object om op te slaan.
     */
    function saveSettings(settings) {
        try {
            localStorage.setItem('jiraCustomScriptSettings', JSON.stringify(settings));
        } catch (e) {
            console.error('Fout bij het opslaan van instellingen:', e);
        }
    }


    // --- FUNCTIES VOOR LOKALE OPSLAG (CACHE) ---
    function getLabelCache() {
        try {
            const cachedData = localStorage.getItem('jiraLabelCache');
            return cachedData ? JSON.parse(cachedData) : {};
        } catch (e) { console.error('Fout bij het lezen van de Jira label-cache:', e); return {}; }
    }

    function saveLabelCache(cache) {
        try {
            localStorage.setItem('jiraLabelCache', JSON.stringify(cache));
        } catch (e) { console.error('Fout bij het opslaan van de Jira label-cache:', e); }
    }

    // --- UI INJECTIE (INSTELLINGEN-PANEEL) ---

    /**
     * Creëert en injecteert de UI voor het instellingenpaneel en de knop om het te openen.
     */
    function setupSettingsUI() {
        if (document.getElementById('jira-custom-settings-modal')) return; // Voorkom dubbel injecteren

        const modal = document.createElement('div');
        modal.id = 'jira-custom-settings-modal';
        Object.assign(modal.style, {
            display: 'none', position: 'fixed', zIndex: '1001', left: '50%', top: '50%',
            transform: 'translate(-50%, -50%)', backgroundColor: '#f5f5f5', padding: '25px',
            border: '1px solid #ccc', borderRadius: '8px', boxShadow: '0 4px 15px rgba(0,0,0,0.2)',
            minWidth: '400px'
        });

        const overlay = document.createElement('div');
        overlay.id = 'jira-custom-settings-overlay';
        Object.assign(overlay.style, {
            display: 'none', position: 'fixed', zIndex: '1000', left: '0', top: '0',
            width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.5)'
        });
        overlay.onclick = () => {
            modal.style.display = 'none';
            overlay.style.display = 'none';
        };

        const currentSettings = loadSettings();
        modal.innerHTML = `
            <h3 style="margin-top: 0; border-bottom: 1px solid #ddd; padding-bottom: 10px;">Script Instellingen</h3>
            <div>
                <label style="display: flex; align-items: center; cursor: pointer; padding: 8px 0;">
                    <input type="checkbox" id="setting-highlight-bugs" ${currentSettings.highlightBugs ? 'checked' : ''}>
                    <span style="margin-left: 8px;">Bugs een rode achtergrond geven</span>
                </label>
                <label style="display: flex; align-items: center; cursor: pointer; padding: 8px 0;">
                    <input type="checkbox" id="setting-set-billable" ${currentSettings.setBillableToNo ? 'checked' : ''}>
                    <span style="margin-left: 8px;">'Facturabel' standaard op "No" zetten (bij aanmaken)</span>
                </label>
                 <label style="display: flex; align-items: center; cursor: pointer; padding: 8px 0;">
                    <input type="checkbox" id="setting-default-region" ${currentSettings.setDefaultRegion ? 'checked' : ''}>
                    <span style="margin-left: 8px;">Standaard regio op "ALG" zetten</span>
                </label>
                <label style="display: flex; align-items: center; cursor: pointer; padding: 8px 0;">
                    <input type="checkbox" id="setting-default-component" ${currentSettings.setDefaultComponent ? 'checked' : ''}>
                    <span style="margin-left: 8px;">Standaard component instellen:</span>
                </label>
                <select id="setting-component-choice" style="margin-left: 30px; padding: 4px;" ${!currentSettings.setDefaultComponent ? 'disabled' : ''}>
                    <option value="OT Software" ${currentSettings.defaultComponent === 'OT Software' ? 'selected' : ''}>OT Software</option>
                    <option value="OT Producten" ${currentSettings.defaultComponent === 'OT Producten' ? 'selected' : ''}>OT Producten</option>
                </select>
            </div>
            <div style="margin-top: 20px; text-align: right;">
                <button id="save-settings-btn" style="padding: 8px 16px; border: none; background-color: #0052cc; color: white; border-radius: 3px; cursor: pointer;">Opslaan en sluiten</button>
            </div>
        `;

        const settingsButton = document.createElement('div');
        settingsButton.innerHTML = '⚙️';
        Object.assign(settingsButton.style, {
            position: 'fixed', bottom: '20px', right: '20px', zIndex: '999',
            backgroundColor: '#fff', border: '1px solid #ccc', borderRadius: '50%',
            width: '40px', height: '40px', display: 'flex', alignItems: 'center',
            justifyContent: 'center', cursor: 'pointer', boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
            fontSize: '24px'
        });
        settingsButton.title = 'Jira Script Instellingen';
        settingsButton.onclick = () => {
            modal.style.display = 'block';
            overlay.style.display = 'block';
        };

        document.body.appendChild(overlay);
        document.body.appendChild(modal);
        document.body.appendChild(settingsButton);

        // Event listener om de dropdown te enablen/disablen
        document.getElementById('setting-default-component').addEventListener('change', (e) => {
            document.getElementById('setting-component-choice').disabled = !e.target.checked;
        });


        document.getElementById('save-settings-btn').addEventListener('click', () => {
            const newSettings = {
                highlightBugs: document.getElementById('setting-highlight-bugs').checked,
                setBillableToNo: document.getElementById('setting-set-billable').checked,
                setDefaultComponent: document.getElementById('setting-default-component').checked,
                defaultComponent: document.getElementById('setting-component-choice').value,
                setDefaultRegion: document.getElementById('setting-default-region').checked,
            };
            saveSettings(newSettings);
            modal.style.display = 'none';
            overlay.style.display = 'none';
            alert('Instellingen opgeslagen. De pagina wordt herladen om de wijzigingen toe te passen.');
            location.reload();
        });
    }

    /**
     * Injecteert een <style> tag in de <head> van de pagina met alle custom CSS.
     */
    function injectCustomStyles(settings) {
        const styleId = 'jira-custom-card-styles';
        if (document.getElementById(styleId)) {
            document.getElementById(styleId).remove(); // Verwijder oude stijl om bij te werken
        }

        const style = document.createElement('style');
        style.id = styleId;

        // Bouw de CSS string op. Voeg bug-stijlen conditioneel toe.
        let css = `
            .aui-label.ghx-title-label { background-color: #ffffff; color: #333; border: 1px solid #ccc; }
            .ghx-issue-compact.ghx-divider { height: auto !important; margin: 5px 0 !important; background: transparent !important; border: none !important; box-shadow: none !important; display: flex !important; align-items: center; justify-content: center; padding: 10px 0; position: relative; overflow: visible; cursor: move !important; }
            .ghx-issue-compact.ghx-divider::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background-color: #ccc; z-index: 0; }
            .ghx-issue-compact.ghx-divider .ghx-row > *:not(.ghx-summary) { display: none !important; }
            .ghx-issue-compact.ghx-divider .ghx-summary { display: block !important; text-align: center; font-size: 14px; font-weight: bold; color: #707070; background-color: #f5f5f5; padding: 2px 15px; border-radius: 10px; position: relative; z-index: 1; flex-grow: 0 !important; opacity: 1 !important; }
            .ghx-issue-compact.ghx-divider .ghx-grabber, .ghx-issue-compact.ghx-divider .ghx-end, .ghx-issue-compact.ghx-divider .ghx-plan-extra-fields { display: none !important; }
            .ghx-issue-compact.ghx-divider .ghx-row { display: block !important; text-align: center; }
            .aui-label.ghx-time-critical-label { background-color: #ff5722; color: white; border-color: #e64a19; display: flex; align-items: center; gap: 5px; font-weight: bold; border-radius: 3px; }
            .ghx-cached-labels { margin-top: 5px; padding-left: 28px; display: flex; flex-wrap: wrap; gap: 5px; }
            .ghx-status-indicator { display: flex; flex-direction: column-reverse; gap: 1px; width: 8px; height: 11px; justify-content: center; flex-shrink: 0; margin: 0 2px; vertical-align: middle; }
            .ghx-status-indicator-bar { height: 3px; width: 100%; background-color: #fff; border: 1px solid #ccc; box-sizing: border-box; }
            .ghx-issue-compact.status-inprogress .ghx-status-indicator-bar:nth-child(-n+1) { background-color: #59afe1; border-color: #59afe1; }
            .ghx-issue-compact.status-test .ghx-status-indicator-bar:nth-child(-n+2) { background-color: #f6c342; border-color: #f6c342; }
            .ghx-issue-compact.status-done .ghx-status-indicator-bar, .ghx-issue-compact.status-closed .ghx-status-indicator-bar { background-color: #8eb021; border-color: #8eb021; }
        `;

        if (settings.highlightBugs) {
            css += `
                .ghx-issue-compact.ghx-bug-card,
                .ghx-swimlane .ghx-issue.ghx-bug-card,
                .ghx-issue-compact.ghx-bug-card .ghx-end {
                    background-color: #ffe7e7 !important;
                }
            `;
        }

        style.innerHTML = css;
        document.head.appendChild(style);
    }


    function createSizeIcon(sizeText) {
        const icon = document.createElement('div');
        icon.textContent = sizeText;
        Object.assign(icon.style, { width: '24px', height: '24px', borderRadius: '50%', backgroundColor: '#f5f5f5', border: '1px solid #ccc', color: '#333', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold', fontSize: '12px', flexShrink: '0', fontFamily: 'Arial, sans-serif' });
        return icon;
    }

    const labelActions = {
        'divider': { type: 'custom', handler: function(card) { if (card.classList.contains('ghx-divider')) return false; card.classList.add('ghx-divider'); const summary = card.querySelector('.ghx-summary'); if (summary) { summary.style.opacity = '1'; const inner = summary.querySelector('.ghx-inner'); if (inner && inner.dataset.originalTitle) { inner.innerHTML = inner.dataset.originalTitle; } } card.querySelectorAll('.ghx-plan-extra-fields, .ghx-end.ghx-row').forEach(el => el.remove()); return false; } },
        'p048': { type: 'style', styles: { backgroundColor: '#f79232', color: '#fff', borderColor: '#f79232' } },
        'urgent': { type: 'style', styles: { backgroundColor: '#d04437', color: 'white', borderColor: '#d04437' } },
        'on-hold': { type: 'custom', handler: function(card) { if (card.querySelector('.on-hold-label')) return false; const onHoldLabel = document.createElement('span'); onHoldLabel.textContent = `🛑 On Hold`; onHoldLabel.className = 'aui-label on-hold-label'; Object.assign(onHoldLabel.style, { marginLeft: '5px', flexShrink: '0' }); const keyElement = card.querySelector('.ghx-key'); const summaryElement = card.querySelector('.ghx-summary'); if (keyElement) keyElement.insertAdjacentElement('afterend', onHoldLabel); if (summaryElement) summaryElement.style.opacity = '0.6'; return false; } },
        'small': { type: 'custom', handler: function(card, labelText, rightContainer) { if (!card.querySelector('.size-icon-s')) { const icon = createSizeIcon('S'); icon.classList.add('size-icon-s'); rightContainer.appendChild(icon); } return false; } },
        'medium': { type: 'custom', handler: function(card, labelText, rightContainer) { if (!card.querySelector('.size-icon-m')) { const icon = createSizeIcon('M'); icon.classList.add('size-icon-m'); rightContainer.appendChild(icon); } return false; } },
        'large': { type: 'custom', handler: function(card, labelText, rightContainer) { if (!card.querySelector('.size-icon-l')) { const icon = createSizeIcon('L'); icon.classList.add('size-icon-l'); rightContainer.appendChild(icon); } return false; } },
        'onderzoek': { type: 'custom', handler: function(card, labelText, rightContainer) { if (card.querySelector('.research-label')) return false; const researchLabel = document.createElement('span'); researchLabel.className = 'aui-label research-label'; researchLabel.innerHTML = '🔍 Onderzoek'; Object.assign(researchLabel.style, { borderColor: '#3572b0', display: 'flex', alignItems: 'center', gap: '4px' }); rightContainer.appendChild(researchLabel); return false; } },
        'tijdskritisch': { type: 'custom', handler: function(card, labelText, rightContainer) { if (card.querySelector('.ghx-time-critical-label')) return false; const timeCriticalLabel = document.createElement('span'); timeCriticalLabel.className = 'aui-label ghx-time-critical-label'; timeCriticalLabel.innerHTML = '⏰ Tijdskritisch'; rightContainer.appendChild(timeCriticalLabel); return false; } },
        'release': { type: 'custom', handler: function(card, labelText, rightContainer) { if (card.querySelector('.release-label')) return false; const releaseLabel = document.createElement('span'); releaseLabel.className = 'aui-label release-label'; releaseLabel.innerHTML = '🔨 Uitvoeren release'; Object.assign(releaseLabel.style, { display: 'flex', alignItems: 'center', gap: '4px' }); rightContainer.appendChild(releaseLabel); return false; } }
    };

    function processSingleCard(card, settings) {
        const labelTooltip = card.querySelector('span[data-tooltip^="Labels:"]');
        if (labelTooltip && labelTooltip.dataset.tooltip.toLowerCase().includes('divider')) {
            labelActions['divider'].handler(card);
            card.classList.add('ghx-layout-processed');
            return;
        }

        card.querySelector('.ghx-flags')?.remove();
        const typeSpan = card.querySelector('.ghx-type');
        if (settings.highlightBugs) {
            if (typeSpan && typeSpan.title.toLowerCase() === 'bug') {
                card.classList.add('ghx-bug-card');
            } else {
                card.classList.remove('ghx-bug-card');
            }
        } else {
             card.classList.remove('ghx-bug-card');
        }


        const summaryElementReset = card.querySelector('.ghx-summary');
        if (summaryElementReset) { summaryElementReset.style.opacity = '1'; const innerSpan = summaryElementReset.querySelector('.ghx-inner'); if (innerSpan && innerSpan.dataset.originalTitle) { innerSpan.innerHTML = innerSpan.dataset.originalTitle; } }
        const issueContent = card.querySelector('.ghx-issue-content');
        if (!issueContent) return;
        const mainRow = issueContent.querySelector('.ghx-row');
        if (!mainRow) return;
        mainRow.querySelectorAll('.on-hold-label, .ghx-status-indicator').forEach(el => el.remove());
        Object.assign(mainRow.style, { display: 'flex', alignItems: 'center', gap: '5px' });
        const masterRightContainer = document.createElement('span');
        masterRightContainer.className = 'ghx-end';
        Object.assign(masterRightContainer.style, { display: 'flex', alignItems: 'center', gap: '8px', marginLeft: 'auto', flexShrink: '0' });
        const labelsSubContainer = document.createElement('span');
        Object.assign(labelsSubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' });
        const sizeIconsSubContainer = document.createElement('span');
        Object.assign(sizeIconsSubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' });
        const titleCategorySubContainer = document.createElement('span');
        Object.assign(titleCategorySubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' });
        const otherFieldsSubContainer = document.createElement('span');
        Object.assign(otherFieldsSubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' });
        const elementsToProcess = [];
        const containersToRemove = [];
        card.querySelectorAll('.ghx-plan-extra-fields, .ghx-issue-content > .ghx-end.ghx-row').forEach(container => { elementsToProcess.push(...container.children); containersToRemove.push(container); });
        const statusField = elementsToProcess.find(field => field.matches('span[data-tooltip^="Status:"]'));
        if (statusField) { const statusText = (statusField.dataset.tooltip || '').replace('Status:', '').trim().toLowerCase(); const statusClasses = ['status-open', 'status-reopened', 'status-inprogress', 'status-test', 'status-closed', 'status-done']; card.classList.remove(...statusClasses); const statusMap = { 'open': { class: 'status-open', name: 'Open' }, 'reopened': { class: 'status-reopened', name: 'Reopened' }, 'in progress': { class: 'status-inprogress', name: 'In Progress' }, 'test': { class: 'status-test', name: 'Test' }, 'closed': { class: 'status-closed', name: 'Closed' }, 'done': { class: 'status-done', name: 'Done' } }; if (statusMap[statusText]) { card.classList.add(statusMap[statusText].class); const progressIndicator = document.createElement('div'); progressIndicator.className = 'ghx-status-indicator'; progressIndicator.title = `Status: ${statusMap[statusText].name}`; for (let i = 0; i < 3; i++) { const bar = document.createElement('div'); bar.className = 'ghx-status-indicator-bar'; progressIndicator.appendChild(bar); } if(typeSpan) typeSpan.insertAdjacentElement('afterend', progressIndicator); } }
        elementsToProcess.forEach(field => { if (field.matches('span[data-tooltip^="Status:"]') || field.classList.contains('ghx-extra-field-seperator')) return; if (field.matches('span[data-tooltip^="Labels:"]')) { const labelContentEl = field.querySelector('.ghx-extra-field-content'); const labels = (labelContentEl && labelContentEl.textContent.trim() !== 'None') ? labelContentEl.textContent.trim().split(/,\s*/).filter(Boolean) : []; const ticketKeyElement = card.querySelector('.ghx-key a'); if (ticketKeyElement) { const ticketKey = ticketKeyElement.textContent.trim(); const cache = getLabelCache(); if (JSON.stringify(cache[ticketKey]) !== JSON.stringify(labels)) { cache[ticketKey] = labels; saveLabelCache(cache); } } labels.forEach(label => { const labelLower = label.toLowerCase(); const action = labelActions[labelLower]; let addToRightAsDefault = true; if (action?.type === 'custom') { let targetContainer = labelsSubContainer; if (['small', 'medium', 'large'].includes(labelLower)) { targetContainer = sizeIconsSubContainer; } if (action.handler(card, label, targetContainer) === false) { addToRightAsDefault = false; } } if (addToRightAsDefault) { const lozenge = document.createElement('span'); lozenge.className = 'aui-label'; lozenge.textContent = label; if (action?.type === 'style') Object.assign(lozenge.style, action.styles); labelsSubContainer.appendChild(lozenge); } }); } else { otherFieldsSubContainer.appendChild(field.cloneNode(true)); } });
        containersToRemove.forEach(container => container.remove());
        mainRow.querySelector('.ghx-end')?.remove();
        if (labelsSubContainer.hasChildNodes()) masterRightContainer.appendChild(labelsSubContainer);
        if (sizeIconsSubContainer.hasChildNodes()) masterRightContainer.appendChild(sizeIconsSubContainer);
        if (titleCategorySubContainer.hasChildNodes()) masterRightContainer.appendChild(titleCategorySubContainer);
        if (otherFieldsSubContainer.hasChildNodes()) masterRightContainer.appendChild(otherFieldsSubContainer);
        if (masterRightContainer.hasChildNodes()) { mainRow.appendChild(masterRightContainer); }
        card.classList.add('ghx-layout-processed');
    }

    function processSwimlaneCards(settings) {
        const labelCache = getLabelCache();
        const swimlaneCards = document.querySelectorAll('.ghx-swimlane .ghx-issue:not(.ghx-swimlane-processed)');

        swimlaneCards.forEach(card => {
            const issueKey = card.dataset.issueKey;
            if (!issueKey) return;

            const typeSpan = card.querySelector('.ghx-type');
            if (settings.highlightBugs) {
                if (typeSpan && typeSpan.title.toLowerCase() === 'bug') {
                    card.classList.add('ghx-bug-card');
                } else {
                    card.classList.remove('ghx-bug-card');
                }
            } else {
                card.classList.remove('ghx-bug-card');
            }


            const cachedLabels = labelCache[issueKey];
            if (cachedLabels && Array.isArray(cachedLabels) && cachedLabels.length > 0) {
                let labelContainer = card.querySelector('.ghx-cached-labels');
                if (!labelContainer) {
                    labelContainer = document.createElement('div');
                    labelContainer.className = 'ghx-cached-labels';
                    card.querySelector('.ghx-issue-fields')?.appendChild(labelContainer);
                }
                labelContainer.innerHTML = '';
                card.style.opacity = '1';
                cachedLabels.forEach(label => {
                    const labelLower = label.toLowerCase();
                    if (labelLower === 'divider') return;
                    const action = labelActions[labelLower];
                    const lozenge = document.createElement('span');
                    lozenge.className = 'aui-label';
                    lozenge.textContent = label;
                    if (action?.type === 'style') { Object.assign(lozenge.style, action.styles); }
                    if (labelLower === 'tijdskritisch') { lozenge.className = 'aui-label ghx-time-critical-label'; lozenge.innerHTML = '⏰ Tijdskritisch'; }
                    else if (labelLower === 'onderzoek') { lozenge.innerHTML = '🔍 Onderzoek'; }
                    else if (labelLower === 'release') { lozenge.innerHTML = '🔨 Uitvoeren release'; }
                    else if (labelLower === 'on-hold') { lozenge.textContent = '🛑 On Hold'; card.style.opacity = '0.7'; }
                    labelContainer.appendChild(lozenge);
                });
            }
            card.classList.add('ghx-swimlane-processed');
        });
    }

    /**
     * Past het 'Create/Edit Issue' scherm aan op basis van instellingen.
     * @param {Object} settings - Het huidige instellingen-object.
     */
    function enhanceCreateEditScreen(settings) {
        // 1. Facturabel
        if (settings.setBillableToNo) {
            const billableSelect = document.getElementById('customfield_10207');
            if (billableSelect && !billableSelect.dataset.scriptProcessed) {
                const label = document.querySelector('label[for="customfield_10207"]');
                const isRequired = label && label.querySelector('.icon-required');
                if (isRequired && billableSelect.value === '') {
                    const noOption = Array.from(billableSelect.options).find(opt => opt.text.trim() === 'No');
                    if (noOption) {
                        billableSelect.value = noOption.value;
                        billableSelect.dispatchEvent(new Event('change', { bubbles: true }));
                    }
                }
                billableSelect.dataset.scriptProcessed = 'true';
            }
        }

        // 2. Component
        if (settings.setDefaultComponent) {
            const componentsContainer = document.getElementById('components-multi-select');
            if (componentsContainer && !componentsContainer.dataset.scriptProcessed) {
                const hasExistingComponent = componentsContainer.querySelector('.items li');
                if (!hasExistingComponent) {
                    const textarea = document.getElementById('components-textarea');
                    if (textarea) {
                        textarea.focus();
                        textarea.value = settings.defaultComponent;
                        textarea.dispatchEvent(new Event('input', { bubbles: true }));
                        textarea.dispatchEvent(new Event('keyup', { bubbles: true }));

                        setTimeout(() => {
                            const suggestionsContainer = document.getElementById('components-suggestions');
                            if(suggestionsContainer) {
                                const suggestionLink = Array.from(suggestionsContainer.querySelectorAll('li a')).find(a => a.title.trim() === settings.defaultComponent);
                                if(suggestionLink) {
                                    suggestionLink.click();
                                } else {
                                     textarea.blur();
                                }
                            }
                        }, 250);
                    }
                }
                componentsContainer.dataset.scriptProcessed = 'true';
            }
        }

        // 3. Region
        if (settings.setDefaultRegion) {
            const regionSelect = document.getElementById('customfield_10210');
            if (regionSelect && !regionSelect.dataset.scriptProcessed) {
                 const isRequired = document.querySelector('label[for="customfield_10210"] .icon-required');
                 if (isRequired && regionSelect.value === '') {
                     const algOption = Array.from(regionSelect.options).find(opt => opt.text.trim() === 'ALG');
                     if (algOption) {
                         regionSelect.value = algOption.value;
                         regionSelect.dispatchEvent(new Event('change', { bubbles: true }));
                     }
                 }
                 regionSelect.dataset.scriptProcessed = 'true';
            }
        }
    }

    /**
     * Zoekt naar onverwerkte elementen en voert de juiste processoren uit.
     */
    function runAllProcessors(settings) {
        // Verwerk kaarten op het bord en in swimlanes
        const compactCards = document.querySelectorAll('.ghx-issue-compact:not(.ghx-layout-processed)');
        if (compactCards.length > 0) {
            compactCards.forEach(card => processSingleCard(card, settings));
        }
        processSwimlaneCards(settings);

        // Verwerk velden op het create/edit scherm
        enhanceCreateEditScreen(settings);
    }

    // --- Script Initialisatie ---
    const currentSettings = loadSettings();
    injectCustomStyles(currentSettings);
    setupSettingsUI();

    const observer = new MutationObserver(() => {
        clearTimeout(observer.timeout);
        // Laad de instellingen opnieuw voor het geval ze in een ander tabblad zijn gewijzigd.
        observer.timeout = setTimeout(() => runAllProcessors(loadSettings()), 100);
    });

    observer.observe(document.body, { childList: true, subtree: true });

    setTimeout(() => runAllProcessors(currentSettings), 500);

})();