diff --git a/agent/js/ai_ticket_summary.js b/agent/js/ai_ticket_summary.js deleted file mode 100644 index 9741d3533..000000000 --- a/agent/js/ai_ticket_summary.js +++ /dev/null @@ -1,14 +0,0 @@ -$('#summaryModal').on('shown.bs.modal', function (e) { - // Perform AJAX request to get the summary - $.ajax({ - url: 'post.php?ai_ticket_summary', - method: 'POST', - data: { ticket_id: }, - success: function(response) { - $('#summaryContent').html(response); - }, - error: function() { - $('#summaryContent').html('Error generating summary.'); - } - }); -}); \ No newline at end of file diff --git a/agent/js/ticket_change_client.js b/agent/js/ticket_change_client.js index f6ef14621..95bb43443 100644 --- a/agent/js/ticket_change_client.js +++ b/agent/js/ticket_change_client.js @@ -1,53 +1,67 @@ -$(document).ready(function() { +document.addEventListener('DOMContentLoaded', function () { // Function to load contacts for a given client function loadContacts(clientId) { - if (!clientId) return; + if (!clientId) { + return; + } - var $contactSelect = $('#contact_select'); - $contactSelect.html(''); + const contactSelect = document.getElementById('contact_select'); + if (!contactSelect) { + return; + } + contactSelect.innerHTML = ''; - $.ajax({ - url: 'ajax.php', - type: 'GET', - dataType: 'json', - data: { - get_client_contacts: 1, - client_id: clientId - }, - success: function(response) { - $contactSelect.empty(); + const query = new URLSearchParams({ + get_client_contacts: 1, + client_id: clientId + }); + + fetch('ajax.php?' + query.toString(), { + method: 'GET', + headers: { 'Accept': 'application/json' }, + credentials: 'same-origin' + }) + .then(function (res) { + if (!res.ok) { + throw new Error('HTTP ' + res.status); + } + return res.json(); + }) + .then(function (response) { + contactSelect.innerHTML = ''; if (response.contacts && response.contacts.length > 0) { - $contactSelect.append(''); - $.each(response.contacts, function(i, contact) { - $contactSelect.append( - $(''); + contactSelect.appendChild(new Option('No contacts found', '')); } - // Refresh Select2 - refreshTomSelect($contactSelect[0]); - }, - error: function(xhr, status, error) { + // Let Tom Select re-read the replaced option list + refreshTomSelect(contactSelect); + }) + .catch(function (error) { console.error('AJAX Error:', error); - $contactSelect.html(''); - } - }); + contactSelect.innerHTML = ''; + }); + } + + const clientSelect = document.getElementById('client_select'); + if (!clientSelect) { + return; } // Load contacts for the currently selected client when modal opens - var initialClientId = $('#client_select').val(); - loadContacts(initialClientId); + loadContacts(clientSelect.value); // Load contacts when client changes - $('#client_select').on('change', function() { - var clientId = $(this).val(); - loadContacts(clientId); + clientSelect.addEventListener('change', function () { + loadContacts(this.value); }); }); diff --git a/agent/js/ticket_tasks_modal.js b/agent/js/ticket_tasks_modal.js index 5513bb177..451fb1d06 100644 --- a/agent/js/ticket_tasks_modal.js +++ b/agent/js/ticket_tasks_modal.js @@ -86,11 +86,11 @@ }); } - // jQuery parses a data-tasks attribute holding JSON into an array on its own, - // but hand it a string and it stays a string - so handle both - function readTemplateTasks($option) { + // dataset always gives a string; jQuery used to auto-parse JSON here, so + // parse it explicitly and still tolerate an already-parsed value + function readTemplateTasks(option) { - const tasks = $option.data('tasks'); + const tasks = option.dataset.tasks; if (!tasks) { return []; @@ -107,42 +107,56 @@ return tasks; } - $(document).off('click.ticketTasks').on('click.ticketTasks', '#ticketTaskAdd', function () { + document.addEventListener('click', function (e) { + if (!e.target.closest('#ticketTaskAdd')) { + return; + } addTaskRow('', ''); }); - $(document).off('click.ticketTaskRemove').on('click.ticketTaskRemove', '.ticket-task-remove', function () { - $(this).closest('.ticket-task-row').remove(); + document.addEventListener('click', function (e) { + const remove = e.target.closest('.ticket-task-remove'); + if (!remove) { + return; + } + const row = remove.closest('.ticket-task-row'); + if (row) { + row.remove(); + } }); // Ticket template picker - fills in the subject, details and task rows - $(document).off('change.ticketTemplate').on('change.ticketTemplate', '#ticket_template_select', function () { - - const $option = $(this).find(':selected'); - - // Selecting "- No Template -" only unlinks the template - it must not wipe - // whatever the user has already written or added - if (!parseInt($option.val(), 10)) { + document.addEventListener('change', function (e) { + const select = e.target.closest('#ticket_template_select'); + if (!select) { return; } - const templateSubject = $option.data('subject') || ''; - const templateDetails = $option.data('details') || ''; + const option = select.options[select.selectedIndex]; - $('#subjectInput').val(templateSubject); + // Selecting "- No Template -" only unlinks the template - it must not wipe + // whatever the user has already written or added + if (!parseInt(option.value, 10)) { + return; + } + + const templateSubject = option.dataset.subject || ''; + const templateDetails = option.dataset.details || ''; + + document.getElementById('subjectInput').value = templateSubject; if (window.tinymce) { const editor = tinymce.get('detailsInput'); if (editor) { editor.setContent(templateDetails); } else { - $('#detailsInput').val(templateDetails); + document.getElementById('detailsInput').value = templateDetails; } } else { - $('#detailsInput').val(templateDetails); + document.getElementById('detailsInput').value = templateDetails; } - setTaskRows(readTemplateTasks($option)); + setTaskRows(readTemplateTasks(option)); }); })(); diff --git a/agent/js/tickets_add_modal.js b/agent/js/tickets_add_modal.js index 7c947d76c..14406853f 100644 --- a/agent/js/tickets_add_modal.js +++ b/agent/js/tickets_add_modal.js @@ -21,14 +21,14 @@ // If the client selector is disabled, we must be on a client-specific page instead. Trigger the lists to update. if (clientSelectDropdown.disabled) { - let client_id = $(clientSelectDropdown).find(':selected').val(); + let client_id = clientSelectDropdown.value; populateLists(client_id); } // Listener for client selection. Populate select lists when a client is selected - $(clientSelectDropdown).on('change', function (e) { - let client_id = $(this).find(':selected').val(); + clientSelectDropdown.addEventListener('change', function () { + let client_id = this.value; // Update the dependent dropdown lists populateLists(client_id); @@ -77,7 +77,7 @@ dropdown.innerHTML = ''; // A multi-select keeps showing its old selections until select2 is told the value changed - clearTomSelect(dropdown instanceof Element ? dropdown : $(dropdown)[0]); + clearTomSelect(dropdown); if (placeholderLabel !== null) { dropdown[dropdown.length] = new Option(placeholderLabel, placeholderValue); @@ -89,7 +89,7 @@ // Redraws a select2 component after its options have been replaced function refreshDropdown(dropdown) { if (dropdown) { - refreshTomSelect(dropdown instanceof Element ? dropdown : $(dropdown)[0]); + refreshTomSelect(dropdown); } } diff --git a/agent/js/tickets_kanban.js b/agent/js/tickets_kanban.js index 861863019..94d7adb42 100644 --- a/agent/js/tickets_kanban.js +++ b/agent/js/tickets_kanban.js @@ -1,4 +1,39 @@ -$(document).ready(function () { +/** + * $.post replacement. jQuery serialised nested arrays/objects into PHP-style + * bracket params (positions[0][status_id]=...), which is what ajax.php parses, + * so that encoding is reproduced here rather than sending JSON. + */ +function itflowPostForm(url, data) { + const params = new URLSearchParams(); + + (function add(prefix, value) { + if (Array.isArray(value)) { + value.forEach(function (v, i) { + add(prefix + '[' + i + ']', v); + }); + } else if (value !== null && typeof value === 'object') { + Object.keys(value).forEach(function (k) { + add(prefix ? prefix + '[' + k + ']' : k, value[k]); + }); + } else { + params.append(prefix, value === true ? 'true' : String(value)); + } + })('', data); + + return fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + credentials: 'same-origin', + body: params.toString() + }).then(function (res) { + if (!res.ok) { + throw new Error('HTTP ' + res.status); + } + return res.text(); + }); +} + +document.addEventListener('DOMContentLoaded', function () { // ------------------------------- // Drag: Kanban Columns (Statuses) // ------------------------------- @@ -8,16 +43,16 @@ $(document).ready(function () { draggable: '.kanban-column', onEnd: function () { const columnPositions = Array.from(document.querySelectorAll('#kanban-board .kanban-column')).map((col, index) => ({ - status_id: $(col).data('status-id'), + status_id: col.dataset.statusId, status_kanban: index })); if (CONFIG_TICKET_MOVING_COLUMNS === 1) { - $.post('ajax.php', { + itflowPostForm('ajax.php', { update_kanban_status_position: true, positions: columnPositions - }).fail((xhr) => { - console.error('Error updating status order:', xhr.responseText); + }).catch((err) => { + console.error('Error updating status order:', err); }); } } @@ -46,15 +81,15 @@ $(document).ready(function () { return; } - const columnId = $(target).data('status-id'); + const columnId = target.dataset.statusId; const positions = Array.from(target.querySelectorAll('.task')).map((card, index) => { - const ticketId = $(card).data('ticket-id'); - const oldStatus = ticketId === $(movedEl).data('ticket-id') - ? $(movedEl).data('ticket-status-id') + const ticketId = card.dataset.ticketId; + const oldStatus = ticketId === movedEl.dataset.ticketId + ? movedEl.dataset.ticketStatusId : false; - $(card).data('ticket-status-id', columnId); // update DOM + card.dataset.ticketStatusId = columnId; // update DOM return { ticket_id: ticketId, @@ -64,11 +99,11 @@ $(document).ready(function () { }; }); - $.post('ajax.php', { + itflowPostForm('ajax.php', { update_kanban_ticket: true, positions: positions - }).fail((xhr) => { - console.error('Error updating ticket positions:', xhr.responseText); + }).catch((err) => { + console.error('Error updating ticket positions:', err); }); // Refresh placeholders after update @@ -81,7 +116,9 @@ $(document).ready(function () { // 📱 Touch Support: Show drag handle on mobile // ------------------------------- if (isTouchDevice()) { - $('.drag-handle-class').css('display', 'inline'); + document.querySelectorAll('.drag-handle-class').forEach(function (el) { + el.style.display = 'inline'; + }); } // ------------------------------- diff --git a/js/ajax_modal.js b/js/ajax_modal.js index 6ab86d57a..2bf39537e 100644 --- a/js/ajax_modal.js +++ b/js/ajax_modal.js @@ -1,61 +1,90 @@ // Ajax Modal Load Script -$(document).on('click', '.ajax-modal', function (e) { - e.preventDefault(); - - const $trigger = $(this); - - // Prefer data-modal-url, fallback to href - let modalUrl = $trigger.data('modal-url') || $trigger.attr('href') || '#'; - const modalSize = $trigger.data('modal-size') || 'md'; - const modalId = 'ajaxModal_' + Date.now(); - - // If no usable URL, bail - if (!modalUrl || modalUrl === '#') { - console.warn('ajax-modal: No modal URL found on trigger:', this); - return; - } - - // Show loading spinner while fetching content - const loadingSpinner = ` - `; - $('.app-main').append(loadingSpinner); - - // Make AJAX request - $.ajax({ - url: modalUrl, - method: 'GET', - dataType: 'json', - success: function (response) { - $('#modal-loading-spinner').remove(); - - if (response.error) { - alert(response.error); +document.addEventListener('click', function (e) { + const trigger = e.target.closest('.ajax-modal'); + if (!trigger) { return; - } - - const modalHtml = ` - `; - - $('.app-main').append(modalHtml); - const $modal = $('#' + modalId); - bootstrap.Modal.getOrCreateInstance($modal[0]).show(); - - $modal.on('hidden.bs.modal', function () { - $(this).remove(); - }); - }, - error: function (xhr, status, error) { - $('#modal-loading-spinner').remove(); - alert('Error loading modal content. Please try again.'); - console.error('Modal AJAX Error:', status, error); } - }); + e.preventDefault(); + + // Prefer data-modal-url, fallback to href + const modalUrl = trigger.dataset.modalUrl || trigger.getAttribute('href') || '#'; + const modalSize = trigger.dataset.modalSize || 'md'; + const modalId = 'ajaxModal_' + Date.now(); + + // If no usable URL, bail + if (!modalUrl || modalUrl === '#') { + console.warn('ajax-modal: No modal URL found on trigger:', trigger); + return; + } + + const host = document.querySelector('.app-main') || document.body; + + // Show loading spinner while fetching content + const spinner = document.createElement('div'); + spinner.id = 'modal-loading-spinner'; + spinner.className = 'text-center p-5'; + spinner.innerHTML = ''; + host.appendChild(spinner); + + function clearSpinner() { + const el = document.getElementById('modal-loading-spinner'); + if (el) { + el.remove(); + } + } + + fetch(modalUrl, { + method: 'GET', + headers: { 'Accept': 'application/json' }, + credentials: 'same-origin' + }) + .then(function (res) { + if (!res.ok) { + throw new Error('HTTP ' + res.status); + } + return res.json(); + }) + .then(function (response) { + clearSpinner(); + + if (response.error) { + alert(response.error); + return; + } + + const wrapper = document.createElement('div'); + wrapper.className = 'modal fade'; + wrapper.id = modalId; + wrapper.tabIndex = -1; + wrapper.innerHTML = + ''; + host.appendChild(wrapper); + + // innerHTML does not execute