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.

As of 09.10.2025. See the latest version.

// ==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);

})();